8000 chore: attempt to be more informative for missing attributes · python-gitlab/python-gitlab@b7cd66b · GitHub
[go: up one dir, main page]

Skip to content

Commit b7cd66b

Browse files
chore: attempt to be more informative for missing attributes
A commonly reported issue from users on Gitter is that they get an AttributeError for an attribute that should be present. This is often caused due to the fact that they used the `list()` method to retrieve the object and objects retrieved this way often only have a subset of the full data. Add more details in the AttributeError message that explains the situation to users. This will hopefully allow them to resolve the issue. Update the FAQ in the docs to add a section discussing the issue. Closes #1138
1 parent 5bca87c commit b7cd66b

File tree

4 files changed

+62
-5
lines changed

4 files changed

+62
-5
lines changed

docs/faq.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ I cannot edit the merge request / issue I've just retrieved
1616
See the :ref:`merge requests example <merge_requests_examples>` and the
1717
:ref:`issues examples <issues_examples>`.
1818

19+
.. _attribute_error_list:
20+
21+
I get an ``AttributeError`` when accessing attributes of an object retrieved via a ``list()`` call.
22+
Fetching a list of objects, doesn’t always include all attributes in the
23+
objects. To retrieve an object with all attributes use a ``get()`` call.
24+
25+
Example with projects::
26+
27+
for projects in gl.projects.list():
28+
# Retrieve project object with all attributes
29+
project = gl.projects.get(project.id)
30+
1931
How can I clone the repository of a project?
2032
python-gitlab doesn't provide an API to clone a project. You have to use a
2133
git library or call the ``git`` command.

gitlab/base.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,19 @@ class RESTObject(object):
4545

4646
_id_attr: Optional[str] = "id"
4747
_attrs: Dict[str, Any]
48+
_list_created_object: bool # Indicates if object was created from a list() action
4849
_module: ModuleType
4950
_parent_attrs: Dict[str, Any]
5051
_short_print_attr: Optional[str] = None
5152
_updated_attrs 10000 : Dict[str, Any]
5253
manager: "RESTManager"
5354

54-
def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None:
55+
def __init__(
56+
self,
57+
manager: "RESTManager",
58+
attrs: Dict[str, Any],
59+
list_created_object: bool = False,
60+
) -> None:
5561
if not isinstance(attrs, dict):
5662
raise GitlabParsingError(
5763
"Attempted to initialize RESTObject with a non-dictionary value: "
@@ -64,6 +70,7 @@ def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None:
6470
"_attrs": attrs,
6571
"_updated_attrs": {},
6672
"_module": importlib.import_module(self.__module__),
73+
"_list_created_object": list_created_object,
6774
}
6875
)
6976
self.__dict__["_parent_attrs"] = self.manager.parent_attrs
@@ -107,7 +114,18 @@ def __getattr__(self, name: str) -> Any:
107114
try:
108115
return self.__dict__["_parent_attrs"][name]
109116
except KeyError:
110-
raise AttributeError(name)
117+
message = name
118+
if self._list_created_object:
119+
message = (
120+
f"Unable to find attribute {name!r}. This object "
121+
f"({self.__class__}) was created via a list() call and "
122+
f"only a subset of the data may be present. To ensure all "
123+
f"data is present get the object using a get(object.id) "
124+
f"call. See "
125+
f"https://python-gitlab.readthedocs.io/en/stable/"
126+
f"faq.html#attribute-error-list for details."
127+
)
128+
raise AttributeError(message)
111129

112130
def __setattr__(self, name: str, value: Any) -> None:
113131
self.__dict__["_updated_attrs"][name] = value
@@ -225,7 +243,7 @@ def __next__(self) -> RESTObject:
225243

226244
def next(self) -> RESTObject:
227245
data = self._list.next()
228-
return self._obj_cls(self.manager, data)
246+
return self._obj_cls(manager=self.manager, attrs=data, list_created_object=True)
229247

230248
@property
231249
def current_page(self) -> int:

gitlab/mixins.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,12 @@ def list(self, **kwargs: Any) -> Union[base.RESTObjectList, List[base.RESTObject
240240
assert self._obj_cls is not None
241241
obj = self.gitlab.http_list(path, **data)
242242
if isinstance(obj, list):
243-
return [self._obj_cls(self, item) for item in obj]
243+
return [
244+
self._obj_cls(manager=self, attrs=item, list_created_object=True)
245+
for item in obj
246+
]
244247
else:
245-
return base.RESTObjectList(self, self._obj_cls, obj)
248+
return base.RESTObjectList(manager=self, obj_cls=self._obj_cls, _list=obj)
246249

247250

248251
class RetrieveMixin(ListMixin, GetMixin):

tests/unit/test_base.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,30 @@ def test_instantiate_non_dict(self, fake_gitlab, fake_manager):
9090
with pytest.raises(gitlab.exceptions.GitlabParsingError):
9191
FakeObject(fake_manager, ["a", "list", "fails"])
9292

93+
def test_missing_attribute(self, fake_gitlab, fake_manager):
94+
obj = FakeObject(manager=fake_manager, attrs={"foo": "bar"})
95+
with pytest.raises(AttributeError) as excinfo:
96+
obj.missing_attribute
97+
assert str(excinfo.value) == "missing_attribute"
98+
assert "was created via a list()" not in str(excinfo.value)
99+
assert (
100+
"https://python-gitlab.readthedocs.io/en/stable/faq.html"
101+
"#attribute-error-list"
102+
) not in str(excinfo.value)
103+
104+
# Test for more informative message if a list() created object
105+
obj = FakeObject(
106+
manager=fake_manager, attrs={"foo": "bar"}, list_created_object=True
107+
)
108+
with pytest.raises(AttributeError) as excinfo:
109+
obj.missing_attribute
110+
assert "missing_attribute" in str(excinfo.value)
111+
assert "was created via a list()" in str(excinfo.value)
112+
assert (
113+
"https://python-gitlab.readthedocs.io/en/stable/faq.html"
114+
"#attribute-error-list"
115+
) in str(excinfo.value)
116+
93117
def test_picklability(self, fake_manager):
94118
obj = FakeObject(fake_manager, {"foo": "bar"})
95119
original_obj_module = obj._module

0 commit comments

Comments
 (0)
0