Skip to content

Commit 08ac071

Browse files
feat: add asdict() and to_json() methods to Gitlab Objects
Add an `asdict()` method that returns a dictionary representation copy of the Gitlab Object. This is a copy and changes made to it will have no impact on the Gitlab Object. The `asdict()` method name was chosen as both the `dataclasses` and `attrs` libraries have an `asdict()` function which has the similar purpose of creating a dictionary represenation of an object. Also add a `to_json()` method that returns a JSON string representation of the object. Closes: #1116
1 parent e5affc8 commit 08ac071

File tree

3 files changed

+129
-22
lines changed

3 files changed

+129
-22
lines changed

docs/api-usage.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,38 @@ the value on the object is accepted:
214214
issue.my_super_awesome_feature_flag = "random_value"
215215
issue.save()
216216
217+
You can get a dictionary representation copy of the Gitlab Object. Modifications made to
218+
the dictionary will have no impact on the GitLab Object.
219+
220+
* `asdict()` method. Returns a dictionary representation of the Gitlab object.
221+
* `attributes` property. Returns a dictionary representation of the Gitlab
222+
object. Also returns any relevant parent object attributes.
223+
224+
.. note::
225+
226+
`attributes` returns the parent object attributes that are defined in
227+
`object._from_parent_attrs`. What this can mean is that for example a `ProjectIssue`
228+
object will have a `project_id` key in the dictionary returned from `attributes` but
229+
`asdict()` will not.
230+
231+
232+
.. code-block:: python
233+
234+
project = gl.projects.get(1)
235+
project_dict = project.asdict()
236+
237+
# Or a dictionary representation also containing some of the parent attributes
238+
issue = project.issues.get(1)
239+
attribute_dict = issue.attributes
240+
241+
You can get a JSON string represenation of the Gitlab Object. For example:
242+
243+
.. code-block:: python
244+
245+
project = gl.projects.get(1)
246+
print(project.to_json())
247+
# Use arguments supported by `json.dump()`
248+
print(project.to_json(sort_keys=True, indent=4))
217249
218250
Base types
219251
==========

gitlab/base.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import copy
1919
import importlib
20+
import json
2021
import pprint
2122
import textwrap
2223
from types import ModuleType
@@ -143,15 +144,26 @@ def __getattr__(self, name: str) -> Any:
143144
def __setattr__(self, name: str, value: Any) -> None:
144145
self.__dict__["_updated_attrs"][name] = value
145146

147+
def asdict(self, *, with_parent_attrs: bool = False) -> Dict[str, Any]:
148+
data = {}
149+
if with_parent_attrs:
150+
data.update(copy.deepcopy(self._parent_attrs))
151+
data.update(copy.deepcopy(self._attrs))
152+
data.update(copy.deepcopy(self._updated_attrs))
153+
return data
154+
155+
@property
156+
def attributes(self) -> Dict[str, Any]:
157+
return self.asdict(with_parent_attrs=True)
158+
159+
def to_json(self, *, with_parent_attrs: bool = False, **kwargs: Any) -> str:
160+
return json.dumps(self.asdict(with_parent_attrs=with_parent_attrs), **kwargs)
161+
146162
def __str__(self) -> str:
147-
data = self._attrs.copy()
148-
data.update(self._updated_attrs)
149-
return f"{type(self)} => {data}"
163+
return f"{type(self)} => {self.asdict()}"
150164

151165
def pformat(self) -> str:
152-
data = self._attrs.copy()
153-
data.update(self._updated_attrs)
154-
return f"{type(self)} => \n{pprint.pformat(data)}"
166+
return f"{type(self)} => \n{pprint.pformat(self.asdict())}"
155167

156168
def pprint(self) -> None:
157169
print(self.pformat())
@@ -242,14 +254,6 @@ def encoded_id(self) -> Optional[Union[int, str]]:
242254
obj_id = gitlab.utils.EncodedId(obj_id)
243255
return obj_id
244256

245-
@property
246-
def attributes(self) -> Dict[str, Any]:
247-
data = {}
248-
data.update(copy.deepcopy(self._parent_attrs))
249-
data.update(copy.deepcopy(self._attrs))
250-
data.update(copy.deepcopy(self._updated_attrs))
251-
return data
252-
253257

254258
class RESTObjectList:
255259
"""Generator object representing a list of RESTObject's.

tests/unit/test_base.py

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ class FakeManager(base.RESTManager):
3636
_path = "/tests"
3737

3838

39+
class FakeParent:
40+
id = 42
41+
42+
43+
class FakeManagerWithParent(base.RESTManager):
44+
_path = "/tests/{test_id}/cases"
45+
_obj_cls = FakeObject
46+
_from_parent_attrs = {"test_id": "id"}
47+
48+
3949
@pytest.fixture
4050
def fake_gitlab():
4151
return FakeGitlab()
@@ -46,9 +56,19 @@ def fake_manager(fake_gitlab):
4656
return FakeManager(fake_gitlab)
4757

4858

59+
@pytest.fixture
60+
def fake_manager_with_parent(fake_gitlab):
61+
return FakeManagerWithParent(fake_gitlab, parent=FakeParent)
62+
63+
4964
@pytest.fixture
5065
def fake_object(fake_manager):
51-
return FakeObject(fake_manager, {"attr1": [1, 2, 3]})
66+
return FakeObject(fake_manager, {"attr1": "foo", "alist": [1, 2, 3]})
67+
68+
69+
@pytest.fixture
70+
def fake_object_with_parent(fake_manager_with_parent):
71+
return FakeObject(fake_manager_with_parent, {"attr1": "foo", "alist": [1, 2, 3]})
5272

5373

5474
class TestRESTManager:
@@ -313,22 +333,73 @@ def test_repr(self, fake_manager):
313333
assert repr(obj) == "<FakeObject>"
314334

315335
def test_attributes_get(self, fake_object):
316-
assert fake_object.attr1 == [1, 2, 3]
336+
assert fake_object.attr1 == "foo"
317337
result = fake_object.attributes
318-
assert result == {"attr1": [1, 2, 3]}
338+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
319339

320340
def test_attributes_shows_updates(self, fake_object):
321341
# Updated attribute value is reflected in `attributes`
322342
fake_object.attr1 = "hello"
323-
assert fake_object.attributes == {"attr1": "hello"}
343+
assert fake_object.attributes == {"attr1": "hello", "alist": [1, 2, 3]}
324344
assert fake_object.attr1 == "hello"
325345
# New attribute is in `attributes`
326346
fake_object.new_attrib = "spam"
327-
assert fake_object.attributes == {"attr1": "hello", "new_attrib": "spam"}
347+
assert fake_object.attributes == {
348+
"attr1": "hello",
349+
"new_attrib": "spam",
350+
"alist": [1, 2, 3],
351+
}
328352

329353
def test_attributes_is_copy(self, fake_object):
330354
# Modifying the dictionary does not cause modifications to the object
331355
result = fake_object.attributes
332-
result["attr1"].append(10)
333-
assert result == {"attr1": [1, 2, 3, 10]}
334-
assert fake_object.attributes == {"attr1": [1, 2, 3]}
356+
result["alist"].append(10)
357+
assert result == {"attr1": "foo", "alist": [1, 2, 3, 10]}
358+
assert fake_object.attributes == {"attr1": "foo", "alist": [1, 2, 3]}
359+
360+
def test_attributes_has_parent_attrs(self, fake_object_with_parent):
361+
assert fake_object_with_parent.attr1 == "foo"
362+
result = fake_object_with_parent.attributes
363+
assert result == {"attr1": "foo", "alist": [1, 2, 3], "test_id": "42"}
364+
365+
def test_asdict(self, fake_object):
366+
assert fake_object.attr1 == "foo"
367+
result = fake_object.asdict()
368+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
369+
370+
def test_asdict_no_parent_attrs(self, fake_object_with_parent):
371+
assert fake_object_with_parent.attr1 == "foo"
372+
result = fake_object_with_parent.asdict()
373+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
374+
assert "test_id" not in fake_object_with_parent.asdict()
375+
assert "test_id" not in fake_object_with_parent.asdict(with_parent_attrs=False)
376+
assert "test_id" in fake_object_with_parent.asdict(with_parent_attrs=True)
377+
378+
def test_asdict_modify_dict_does_not_change_object(self, fake_object):
379+
result = fake_object.asdict()
380+
# Demonstrate modifying the dictionary does not modify the object
381+
result["attr1"] = "testing"
382+
result["alist"].append(4)
383+
assert result == {"attr1": "testing", "alist": [1, 2, 3, 4]}
384+
assert fake_object.attr1 == "foo"
385+
assert fake_object.alist == [1, 2, 3]
386+
387+
def test_asdict_modify_dict_does_not_change_object2(self, fake_object):
388+
# Modify attribute and then ensure modifying a list in the returned dict won't
389+
# modify the list in the object.
390+
fake_object.attr1 = [9, 7, 8]
391+
assert fake_object.asdict() == {
392+
"attr1": [9, 7, 8],
393+
"alist": [1, 2, 3],
394+
}
395+
result = fake_object.asdict()
396+
result["attr1"].append(1)
397+
assert fake_object.asdict() == {
398+
"attr1": [9, 7, 8],
399+
"alist": [1, 2, 3],
400+
}
401+
402+
def test_asdict_modify_object(self, fake_object):
403+
# asdict() returns the updated value
404+
fake_object.attr1 = "spam"
405+
assert fake_object.asdict() == {"attr1": "spam", "alist": [1, 2, 3]}

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