| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -190,6 +190,37 @@ a project (the previous example used 2 API calls): | |||
| 190 | 190 | project = gl.projects.get(1, lazy=True) # no API call | |
| 191 | 191 | project.star() # API call | |
| 192 | 192 | ||
| 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 | + | ||
| 193 | 224 | Pagination | |
| 194 | 225 | ========== | |
| 195 | 226 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -31,3 +31,8 @@ How can I clone the repository of a project? | |||
| 31 | 31 | print(project.attributes) # displays all the attributes | |
| 32 | 32 | git_url = project.ssh_url_to_repo | |
| 33 | 33 | 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`. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -45,6 +45,7 @@ class RESTObject(object): | |||
| 45 | 45 | _attrs: Dict[str, Any] | |
| 46 | 46 | _module: ModuleType | |
| 47 | 47 | _parent_attrs: Dict[str, Any] | |
| 48 | + _persist_attrs: bool | ||
| 48 | 49 | _short_print_attr: Optional[str] = None | |
| 49 | 50 | _updated_attrs: Dict[str, Any] | |
| 50 | 51 | manager: "RESTManager" | |
@@ -59,6 +60,7 @@ def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None: | |||
| 59 | 60 | } | |
| 60 | 61 | ) | |
| 61 | 62 | self.__dict__["_parent_attrs"] = self.manager.parent_attrs | |
| 63 | + self.__dict__["_persist_attrs"] = False | ||
| 62 | 64 | self._create_managers() | |
| 63 | 65 | ||
| 64 | 66 | def __getstate__(self) -> Dict[str, Any]: | |
@@ -153,7 +155,11 @@ def _create_managers(self) -> None: | |||
| 153 | 155 | ||
| 154 | 156 | def _update_attrs(self, new_attrs: Dict[str, Any]) -> None: | |
| 155 | 157 | 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 | ||
| 157 | 163 | ||
| 158 | 164 | def get_id(self): | |
| 159 | 165 | """Returns the id of the resource.""" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -162,10 +162,12 @@ class RefreshMixin(_RestObjectBase): | |||
| 162 | 162 | manager: base.RESTManager | |
| 163 | 163 | ||
| 164 | 164 | @exc.on_http_error(exc.GitlabGetError) | |
| 165 | - def refresh(self, **kwargs: Any) -> None: | ||
| 165 | + def refresh(self, persist_attributes: bool = None, **kwargs: Any) -> None: | ||
| 166 | 166 | """Refresh a single object from server. | |
| 167 | 167 | ||
| 168 | 168 | Args: | |
| 169 | + persist_attributes: Whether to keep existing local attributes that | ||
| 170 | + were not fetched from the server on refresh | ||
| 169 | 171 | **kwargs: Extra options to send to the server (e.g. sudo) | |
| 170 | 172 | ||
| 171 | 173 | Returns None (updates the object) | |
@@ -174,6 +176,9 @@ def refresh(self, **kwargs: Any) -> None: | |||
| 174 | 176 | GitlabAuthenticationError: If authentication is not correct | |
| 175 | 177 | GitlabGetError: If the server cannot perform the request | |
| 176 | 178 | """ | |
| 179 | + if persist_attributes is not None: | ||
| 180 | + self.__dict__["_persist_attrs"] = persist_attributes | ||
| 181 | + | ||
| 177 | 182 | if self._id_attr: | |
| 178 | 183 | path = "%s/%s" % (self.manager.path, self.id) | |
| 179 | 184 | else: | |
@@ -529,18 +534,23 @@ def _get_updated_data(self) -> Dict[str, Any]: | |||
| 529 | 534 | ||
| 530 | 535 | return updated_data | |
| 531 | 536 | ||
| 532 | - def save(self, **kwargs: Any) -> None: | ||
| 537 | + def save(self, persist_attributes: bool = None, **kwargs: Any) -> None: | ||
| 533 | 538 | """Save the changes made to the object to the server. | |
| 534 | 539 | ||
| 535 | 540 | The object is updated to match what the server returns. | |
| 536 | 541 | ||
| 537 | 542 | Args: | |
| 543 | + persist_attributes: Whether to keep existing local attributes that | ||
| 544 | + were not fetched from the server on save | ||
| 538 | 545 | **kwargs: Extra options to send to the server (e.g. sudo) | |
| 539 | 546 | ||
| 540 | 547 | Raise: | |
| 541 | 548 | GitlabAuthenticationError: If authentication is not correct | |
| 542 | 549 | GitlabUpdateError: If the server cannot perform the request | |
| 543 | 550 | """ | |
| 551 | + if persist_attributes is not None: | ||
| 552 | + self.__dict__["_persist_attrs"] = persist_attributes | ||
| 553 | + | ||
| 544 | 554 | updated_data = self._get_updated_data() | |
| 545 | 555 | # Nothing to update. Server fails if sent an empty dict. | |
| 546 | 556 | if not updated_data: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -128,11 +128,25 @@ def test_update_attrs(self, fake_manager): | |||
| 128 | 128 | assert {"foo": "foo", "bar": "bar"} == obj._attrs | |
| 129 | 129 | assert {} == obj._updated_attrs | |
| 130 | 130 | ||
| 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 | + | ||
| 134 | 148 | obj._update_attrs({"foo": "foo"}) | |
| 135 | - assert {"foo": "foo"} == obj._attrs | ||
| 149 | + assert expected_attrs == obj._attrs | ||
| 136 | 150 | assert {} == obj._updated_attrs | |
| 137 | 151 | ||
| 138 | 152 | def test_dir_unique(self, fake_manager): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -234,6 +234,24 @@ def test_project_stars(project): | |||
| 234 | 234 | assert project.star_count == 0 | |
| 235 | 235 | ||
| 236 | 236 | ||
| 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 | + | ||
| 237 | 255 | def test_project_tags(project, project_file): | |
| 238 | 256 | tag = project.tags.create({"tag_name": "v1.0", "ref": "master"}) | |
| 239 | 257 | assert len(project.tags.list()) == 1 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -142,6 +142,24 @@ def test_user_custom_attributes(gl, user): | |||
| 142 | 142 | assert len(user.customattributes.list()) == 0 | |
| 143 | 143 | ||
| 144 | 144 | ||
| 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 | + | ||
| 145 | 163 | def test_user_impersonation_tokens(gl, user): | |
| 146 | 164 | token = user.impersonationtokens.create( | |
| 147 | 165 | {"name": "token1", "scopes": ["api", "read_user"]} | |
| Back | FazBrowse Home | New Git URL |
0 commit comments