Skip to content

Commit 1839c9e

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 09a973e commit 1839c9e

File tree

4 files changed

+71
-5
lines changed

4 files changed

+71
-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: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
import importlib
19+
import textwrap
1920
from types import ModuleType
2021
from typing import Any, Dict, Iterable, NamedTuple, Optional, Tuple, Type
2122

23+
import gitlab
2224
from gitlab import types as g_types
2325
from gitlab.exceptions import GitlabParsingError
2426

@@ -32,6 +34,12 @@
3234
]
3335

3436

37+
_URL_ATTRIBUTE_ERROR = (
38+
f"https://python-gitlab.readthedocs.io/en/{gitlab.__version__}/"
39+
f"faq.html#attribute-error-list"
40+
)
41+
42+
3543
class RESTObject(object):
3644
"""Represents an object built from server data.
3745
@@ -45,13 +53,20 @@ class RESTObject(object):
4553

4654
_id_attr: Optional[str] = "id"
4755
_attrs: Dict[str, Any]
56+
_created_from_list: bool # Indicates if object was created from a list() action
4857
_module: ModuleType
4958
_parent_attrs: Dict[str, Any]
5059
_short_print_attr: Optional[str] = None
5160
_updated_attrs: Dict[str, Any]
5261
manager: "RESTManager"
5362

54-
def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None:
63+
def __init__(
64+
self,
65+
manager: "RESTManager",
66+
attrs: Dict[str, Any],
67+
*,
68+
created_from_list: bool = False,
69+
) -> None:
5570
if not isinstance(attrs, dict):
5671
raise GitlabParsingError(
5772
"Attempted to initialize RESTObject with a non-dictionary value: "
@@ -64,6 +79,7 @@ def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None:
6479
"_attrs": attrs,
6580
"_updated_attrs": {},
6681
"_module": importlib.import_module(self.__module__),
82+
"_created_from_list": created_from_list,
6783
}
6884
)
6985
self.__dict__["_parent_attrs"] = self.manager.parent_attrs
@@ -106,8 +122,22 @@ def __getattr__(self, name: str) -> Any:
106122
except KeyError:
107123
try:
108124
return self.__dict__["_parent_attrs"][name]
109-
except KeyError:
110-
raise AttributeError(name)
125+
except KeyError as exc:
126+
message = (
127+
f"{type(self).__name__!r} object has no attribute {name!r}"
128+
)
129+
if self._created_from_list:
130+
message = (
131+
f"{message}\n\n"
132+
+ textwrap.fill(
133+
f"{self.__class__!r} was created via a list() call and "
134+
f"only a subset of the data may be present. To ensure "
135+
f"all data is present get the object using a "
136+
f"get(object.id) call. For more details, see:"
137+
)
138+
+ f"\n\n{_URL_ATTRIBUTE_ERROR}"
139+
)
140+
raise AttributeError(message) from exc
111141

112142
def __setattr__(self, name: str, value: Any) -> None:
113143
self.__dict__["_updated_attrs"][name] = value
@@ -229,7 +259,7 @@ def __next__(self) -> RESTObject:
229259

230260
def next(self) -> RESTObject:
231261
data = self._list.next()
232-
return self._obj_cls(self.manager, data)
262+
return self._obj_cls(self.manager, data, created_from_list=True)
233263

234264
@property
235265
def current_page(self) -> int:

gitlab/mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ 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 [self._obj_cls(self, item, created_from_list=True) for item in obj]
244244
else:
245245
return base.RESTObjectList(self, self._obj_cls, obj)
246246

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_does_not_raise_custom(self, fake_gitlab, fake_manager):
94+
"""Ensure a missing attribute does not raise our custom error message
95+
if the RESTObject was not created from a list"""
96+
obj = FakeObject(manager=fake_manager, attrs={"foo": "bar"})
97+
with pytest.raises(AttributeError) as excinfo:
98+
obj.missing_attribute
99+
exc_str = str(excinfo.value)
100+
assert "missing_attribute" in exc_str
101+
assert "was created via a list()" not in exc_str
102+
assert base._URL_ATTRIBUTE_ERROR not in exc_str
103+
104+
def test_missing_attribute_from_list_raises_custom(self, fake_gitlab, fake_manager):
105+
"""Ensure a missing attribute raises our custom error message if the
106+
RESTObject was created from a list"""
107+
obj = FakeObject(
108+
manager=fake_manager, attrs={"foo": "bar"}, created_from_list=True
109+
)
110+
with pytest.raises(AttributeError) as excinfo:
111+
obj.missing_attribute
112+
exc_str = str(excinfo.value)
113+
assert "missing_attribute" in exc_str
114+
assert "was created via a list()" in exc_str
115+
assert base._URL_ATTRIBUTE_ERROR in exc_str
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)