From a7e8cfbae8e53d2c4b1fb75d57d42f00db8abd81 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 20 Jul 2022 08:34:08 -0700 Subject: [PATCH] chore: add a `lazy` boolean attribute to `RESTObject` This can be used to tell if a `RESTObject` was created using `lazy=True`. Add a message to the `AttributeError` if attribute access fails for an instance created with `lazy=True`. --- gitlab/base.py | 9 ++++++ gitlab/mixins.py | 4 +-- tests/unit/mixins/test_mixin_methods.py | 43 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/gitlab/base.py b/gitlab/base.py index 920617b33..dd5924063 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -62,6 +62,7 @@ class RESTObject: _parent_attrs: Dict[str, Any] _repr_attr: Optional[str] = None _updated_attrs: Dict[str, Any] + _lazy: bool manager: "RESTManager" def __init__( @@ -70,6 +71,7 @@ def __init__( attrs: Dict[str, Any], *, created_from_list: bool = False, + lazy: bool = False, ) -> None: if not isinstance(attrs, dict): raise GitlabParsingError( @@ -84,6 +86,7 @@ def __init__( "_updated_attrs": {}, "_module": importlib.import_module(self.__module__), "_created_from_list": created_from_list, + "_lazy": lazy, } ) self.__dict__["_parent_attrs"] = self.manager.parent_attrs @@ -137,6 +140,12 @@ def __getattr__(self, name: str) -> Any: ) + f"\n\n{_URL_ATTRIBUTE_ERROR}" ) + elif self._lazy: + message = f"{message}\n\n" + textwrap.fill( + f"If you tried to access object attributes returned from the server, " + f"note that {self.__class__!r} was created as a `lazy` object and was " + f"not initialized with any data." + ) raise AttributeError(message) def __setattr__(self, name: str, value: Any) -> None: diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 519e83f1e..f33a1fcf7 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -135,11 +135,11 @@ def get( if lazy is True: if TYPE_CHECKING: assert self._obj_cls._id_attr is not None - return self._obj_cls(self, {self._obj_cls._id_attr: id}) + return self._obj_cls(self, {self._obj_cls._id_attr: id}, lazy=lazy) server_data = self.gitlab.http_get(path, **kwargs) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) - return self._obj_cls(self, server_data) + return self._obj_cls(self, server_data, lazy=lazy) class GetWithoutIdMixin(HeadMixin, _RestManagerBase): diff --git a/tests/unit/mixins/test_mixin_methods.py b/tests/unit/mixins/test_mixin_methods.py index c40ccbd81..9121453c4 100644 --- a/tests/unit/mixins/test_mixin_methods.py +++ b/tests/unit/mixins/test_mixin_methods.py @@ -45,9 +45,52 @@ class M(GetMixin, FakeManager): assert isinstance(obj, FakeObject) assert obj.foo == "bar" assert obj.id == 42 + assert obj._lazy is False assert responses.assert_call_count(url, 1) is True +def test_get_mixin_lazy(gl): + class M(GetMixin, FakeManager): + pass + + url = "http://localhost/api/v4/tests/42" + + mgr = M(gl) + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url=url, + json={"id": 42, "foo": "bar"}, + status=200, + match=[responses.matchers.query_param_matcher({})], + ) + obj = mgr.get(42, lazy=True) + assert isinstance(obj, FakeObject) + assert not hasattr(obj, "foo") + assert obj.id == 42 + assert obj._lazy is True + # a `lazy` get does not make a network request + assert not rsps.calls + + +def test_get_mixin_lazy_missing_attribute(gl): + class FakeGetManager(GetMixin, FakeManager): + pass + + manager = FakeGetManager(gl) + obj = manager.get(1, lazy=True) + assert obj.id == 1 + with pytest.raises(AttributeError) as exc: + obj.missing_attribute + # undo `textwrap.fill()` + message = str(exc.value).replace("\n", " ") + assert "'FakeObject' object has no attribute 'missing_attribute'" in message + assert ( + "note that was " + "created as a `lazy` object and was not initialized with any data." + ) in message + + @responses.activate def test_head_mixin(gl): class M(GetMixin, FakeManager):