diff options
author | John L. Villalovos <john@sodarock.com> | 2021-11-30 08:35:58 -0800 |
---|---|---|
committer | John L. Villalovos <john@sodarock.com> | 2021-11-30 08:35:58 -0800 |
commit | 1839c9e7989163a5cc9a201241942b7faca6e214 (patch) | |
tree | f6fcb4f19fb62e6db8234a6f96d367ec093cab27 | |
parent | 09a973ee379d82af05a5080decfaec16d2f4eab3 (diff) | |
download | gitlab-jlvillal/attribute_help.tar.gz |
chore: attempt to be more informative for missing attributesjlvillal/attribute_help
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
-rw-r--r-- | docs/faq.rst | 12 | ||||
-rw-r--r-- | gitlab/base.py | 38 | ||||
-rw-r--r-- | gitlab/mixins.py | 2 | ||||
-rw-r--r-- | tests/unit/test_base.py | 24 |
4 files changed, 71 insertions, 5 deletions
diff --git a/docs/faq.rst b/docs/faq.rst index 0f914ed..cdc81a8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -16,6 +16,18 @@ I cannot edit the merge request / issue I've just retrieved See the :ref:`merge requests example <merge_requests_examples>` and the :ref:`issues examples <issues_examples>`. +.. _attribute_error_list: + +I get an ``AttributeError`` when accessing attributes of an object retrieved via a ``list()`` call. + Fetching a list of objects, doesn’t always include all attributes in the + objects. To retrieve an object with all attributes use a ``get()`` call. + + Example with projects:: + + for projects in gl.projects.list(): + # Retrieve project object with all attributes + project = gl.projects.get(project.id) + How can I clone the repository of a project? python-gitlab doesn't provide an API to clone a project. You have to use a git library or call the ``git`` command. diff --git a/gitlab/base.py b/gitlab/base.py index 5e5f57b..f7b52fa 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -16,9 +16,11 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import importlib +import textwrap from types import ModuleType from typing import Any, Dict, Iterable, NamedTuple, Optional, Tuple, Type +import gitlab from gitlab import types as g_types from gitlab.exceptions import GitlabParsingError @@ -32,6 +34,12 @@ __all__ = [ ] +_URL_ATTRIBUTE_ERROR = ( + f"https://python-gitlab.readthedocs.io/en/{gitlab.__version__}/" + f"faq.html#attribute-error-list" +) + + class RESTObject(object): """Represents an object built from server data. @@ -45,13 +53,20 @@ class RESTObject(object): _id_attr: Optional[str] = "id" _attrs: Dict[str, Any] + _created_from_list: bool # Indicates if object was created from a list() action _module: ModuleType _parent_attrs: Dict[str, Any] _short_print_attr: Optional[str] = None _updated_attrs: Dict[str, Any] manager: "RESTManager" - def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None: + def __init__( + self, + manager: "RESTManager", + attrs: Dict[str, Any], + *, + created_from_list: bool = False, + ) -> None: if not isinstance(attrs, dict): raise GitlabParsingError( "Attempted to initialize RESTObject with a non-dictionary value: " @@ -64,6 +79,7 @@ class RESTObject(object): "_attrs": attrs, "_updated_attrs": {}, "_module": importlib.import_module(self.__module__), + "_created_from_list": created_from_list, } ) self.__dict__["_parent_attrs"] = self.manager.parent_attrs @@ -106,8 +122,22 @@ class RESTObject(object): except KeyError: try: return self.__dict__["_parent_attrs"][name] - except KeyError: - raise AttributeError(name) + except KeyError as exc: + message = ( + f"{type(self).__name__!r} object has no attribute {name!r}" + ) + if self._created_from_list: + message = ( + f"{message}\n\n" + + textwrap.fill( + f"{self.__class__!r} was created via a list() call and " + f"only a subset of the data may be present. To ensure " + f"all data is present get the object using a " + f"get(object.id) call. For more details, see:" + ) + + f"\n\n{_URL_ATTRIBUTE_ERROR}" + ) + raise AttributeError(message) from exc def __setattr__(self, name: str, value: Any) -> None: self.__dict__["_updated_attrs"][name] = value @@ -229,7 +259,7 @@ class RESTObjectList(object): def next(self) -> RESTObject: data = self._list.next() - return self._obj_cls(self.manager, data) + return self._obj_cls(self.manager, data, created_from_list=True) @property def current_page(self) -> int: diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 0159ecd..ed3dbdc 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -240,7 +240,7 @@ class ListMixin(_RestManagerBase): assert self._obj_cls is not None obj = self.gitlab.http_list(path, **data) if isinstance(obj, list): - return [self._obj_cls(self, item) for item in obj] + return [self._obj_cls(self, item, created_from_list=True) for item in obj] else: return base.RESTObjectList(self, self._obj_cls, obj) diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index 137f480..3ca0206 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -90,6 +90,30 @@ class TestRESTObject: with pytest.raises(gitlab.exceptions.GitlabParsingError): FakeObject(fake_manager, ["a", "list", "fails"]) + def test_missing_attribute_does_not_raise_custom(self, fake_gitlab, fake_manager): + """Ensure a missing attribute does not raise our custom error message + if the RESTObject was not created from a list""" + obj = FakeObject(manager=fake_manager, attrs={"foo": "bar"}) + with pytest.raises(AttributeError) as excinfo: + obj.missing_attribute + exc_str = str(excinfo.value) + assert "missing_attribute" in exc_str + assert "was created via a list()" not in exc_str + assert base._URL_ATTRIBUTE_ERROR not in exc_str + + def test_missing_attribute_from_list_raises_custom(self, fake_gitlab, fake_manager): + """Ensure a missing attribute raises our custom error message if the + RESTObject was created from a list""" + obj = FakeObject( + manager=fake_manager, attrs={"foo": "bar"}, created_from_list=True + ) + with pytest.raises(AttributeError) as excinfo: + obj.missing_attribute + exc_str = str(excinfo.value) + assert "missing_attribute" in exc_str + assert "was created via a list()" in exc_str + assert base._URL_ATTRIBUTE_ERROR in exc_str + def test_picklability(self, fake_manager): obj = FakeObject(fake_manager, {"foo": "bar"}) original_obj_module = obj._module |