Skip to content

Commit 796c700

Browse files
committed
fix(base): allow persisting attributes when updating object
1 parent b563cdc commit 796c700

File tree

7 files changed

+109
-7
lines changed

7 files changed

+109
-7
lines changed

docs/api-usage.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,37 @@ a project (the previous example used 2 API calls):
190190
project = gl.projects.get(1, lazy=True) # no API call
191191
project.star() # API call
192192
193+
.. _persist_attributes:
194+
195+
Persisting local attributes
196+
===========================
197+
198+
When methods manipulate an existing object, such as with ``refresh()`` and ``save()``,
199+
the object will only have attributes that were returned by the server. In some cases,
200+
such as when the initial request fetches attributes that are needed later for additional
201+
processing, this may not be desired:
202+
203+
.. code-block:: python
204+
205+
project = gl.projects.get(1, statistics=True)
206+
project.statistics
207+
208+
project.refresh()
209+
project.statistics # AttributeError
210+
211+
To avoid this, pass ``persist_attributes=True`` to ``refresh()``/``save()`` calls:
212+
213+
.. code-block:: python
214+
215+
project = gl.projects.get(1, statistics=True)
216+
project.statistics
217+
218+
project.refresh(persist_attributes=True)
219+
project.statistics
220+
221+
The ``persist_attributes`` setting is itself persisted in the object and can be reused
222+
for later ``refresh()`` and ``save()`` calls.
223+
193224
Pagination
194225
==========
195226

docs/faq.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ How can I clone the repository of a project?
3131
print(project.attributes) # displays all the attributes
3232
git_url = project.ssh_url_to_repo
3333
subprocess.call(['git', 'clone', git_url])
34+
35+
I get an ``AttributeError`` when accessing attributes after ``save()`` or ``refresh()``.
36+
You are most likely trying to access an attribute that was not returned
37+
by the server on the second request. Use the ``persist_attributes=True``
38+
argument to override this - see :ref:`persist_attributes`.

gitlab/base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class RESTObject(object):
4545
_attrs: Dict[str, Any]
4646
_module: ModuleType
4747
_parent_attrs: Dict[str, Any]
48+
_persist_attrs: bool
4849
_short_print_attr: Optional[str] = None
4950
_updated_attrs: Dict[str, Any]
5051
manager: "RESTManager"
@@ -59,6 +60,7 @@ def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None:
5960
}
6061
)
6162
self.__dict__["_parent_attrs"] = self.manager.parent_attrs
63+
self.__dict__["_persist_attrs"] = False
6264
self._create_managers()
6365

6466
def __getstate__(self) -> Dict[str, Any]:
@@ -153,7 +155,11 @@ def _create_managers(self) -> None:
153155

154156
def _update_attrs(self, new_attrs: Dict[str, Any]) -> None:
155157
self.__dict__["_updated_attrs"] = {}
156-
self.__dict__["_attrs"] = new_attrs
158+
159+
if self.__dict__["_persist_attrs"] is True:
160+
self.__dict__["_attrs"].update(new_attrs)
161+
else:
162+
self.__dict__["_attrs"] = new_attrs
157163

158164
def get_id(self):
159165
"""Returns the id of the resource."""

gitlab/mixins.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,12 @@ class RefreshMixin(_RestObjectBase):
162162
manager: base.RESTManager
163163

164164
@exc.on_http_error(exc.GitlabGetError)
165-
def refresh(self, **kwargs: Any) -> None:
165+
def refresh(self, persist_attributes: bool = None, **kwargs: Any) -> None:
166166
"""Refresh a single object from server.
167167
168168
Args:
169+
persist_attributes: Whether to keep existing local attributes that
170+
were not fetched from the server on refresh
169171
**kwargs: Extra options to send to the server (e.g. sudo)
170172
171173
Returns None (updates the object)
@@ -174,6 +176,9 @@ def refresh(self, **kwargs: Any) -> None:
174176
GitlabAuthenticationError: If authentication is not correct
175177
GitlabGetError: If the server cannot perform the request
176178
"""
179+
if persist_attributes is not None:
180+
self.__dict__["_persist_attrs"] = persist_attributes
181+
177182
if self._id_attr:
178183
path = "%s/%s" % (self.manager.path, self.id)
179184
else:
@@ -529,18 +534,23 @@ def _get_updated_data(self) -> Dict[str, Any]:
529534

530535
return updated_data
531536

532-
def save(self, **kwargs: Any) -> None:
537+
def save(self, persist_attributes: bool = None, **kwargs: Any) -> None:
533538
"""Save the changes made to the object to the server.
534539
535540
The object is updated to match what the server returns.
536541
537542
Args:
543+
persist_attributes: Whether to keep existing local attributes that
544+
were not fetched from the server on save
538545
**kwargs: Extra options to send to the server (e.g. sudo)
539546
540547
Raise:
541548
GitlabAuthenticationError: If authentication is not correct
542549
GitlabUpdateError: If the server cannot perform the request
543550
"""
551+
if persist_attributes is not None:
552+
self.__dict__["_persist_attrs"] = persist_attributes
553+
544554
updated_data = self._get_updated_data()
545555
# Nothing to update. Server fails if sent an empty dict.
546556
if not updated_data:

gitlab/tests/test_base.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,25 @@ def test_update_attrs(self, fake_manager):
128128
assert {"foo": "foo", "bar": "bar"} == obj._attrs
129129
assert {} == obj._updated_attrs
130130

131-
def test_update_attrs_deleted(self, fake_manager):
132-
obj = FakeObject(fake_manager, {"foo": "foo", "bar": "bar"})
133-
obj.bar = "baz"
131+
@pytest.mark.parametrize(
132+
"initial_attrs,persist_attrs,assigned_attr,expected_attrs",
133+
[
134+
({"foo": "foo", "bar": "bar"}, None, "baz", {"foo": "foo"}),
135+
({"foo": "foo", "bar": "bar"}, False, "baz", {"foo": "foo"}),
136+
({"foo": "foo", "bar": "bar"}, True, "baz", {"foo": "foo", "bar": "baz"}),
137+
],
138+
)
139+
def test_update_attrs_deleted(
140+
self, fake_manager, initial_attrs, persist_attrs, assigned_attr, expected_attrs
141+
):
142+
obj = FakeObject(fake_manager, initial_attrs)
143+
obj._attrs["bar"] = assigned_attr
144+
145+
if persist_attrs is not None:
146+
obj.__dict__["_persist_attrs"] = persist_attrs
147+
134148
obj._update_attrs({"foo": "foo"})
135-
assert {"foo": "foo"} == obj._attrs
149+
assert expected_attrs == obj._attrs
136150
assert {} == obj._updated_attrs
137151

138152
def test_dir_unique(self, fake_manager):

tools/functional/api/test_projects.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,24 @@ def test_project_stars(project):
234234
assert project.star_count == 0
235235

236236

237+
@pytest.mark.parametrize(
238+
"refresh_kwargs,hasattr_before,hasattr_after",
239+
[
240+
({}, True, False),
241+
({"persist_attributes": True}, True, True),
242+
({"persist_attributes": False}, True, False),
243+
],
244+
)
245+
def test_project_statistics_after_refresh(
246+
gl, project, refresh_kwargs, hasattr_before, hasattr_after
247+
):
248+
project = gl.projects.get(project.id, statistics=True)
249+
assert hasattr(project, "statistics") == hasattr_before
250+
251+
project.refresh(**refresh_kwargs)
252+
assert hasattr(project, "statistics") == hasattr_after
253+
254+
237255
def test_project_tags(project, project_file):
238256
tag = project.tags.create({"tag_name": "v1.0", "ref": "master"})
239257
assert len(project.tags.list()) == 1

tools/functional/api/test_users.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,24 @@ def test_user_custom_attributes(gl, user):
142142
assert len(user.customattributes.list()) == 0
143143

144144

145+
@pytest.mark.parametrize(
146+
"save_kwargs,hasattr_before,hasattr_after",
147+
[
148+
({}, True, False),
149+
({"persist_attributes": True}, True, True),
150+
({"persist_attributes": False}, True, False),
151+
],
152+
)
153+
def test_user_custom_attributes_after_save(
154+
gl, user, save_kwargs, hasattr_before, hasattr_after
155+
):
156+
user = gl.users.get(user.id, with_custom_attributes=True)
157+
assert hasattr(user, "custom_attributes") == hasattr_before
158+
159+
user.save(**save_kwargs)
160+
assert hasattr(user, "custom_attributes") == hasattr_after
161+
162+
145163
def test_user_impersonation_tokens(gl, user):
146164
token = user.impersonationtokens.create(
147165
{"name": "token1", "scopes": ["api", "read_user"]}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy