Skip to content

Commit 599c906

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 160ae82 commit 599c906

3 files changed

Lines changed: 123 additions & 22 deletions

File tree

‎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: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,21 @@ def fake_manager(fake_gitlab):
4848

4949
@pytest.fixture
5050
def fake_object(fake_manager):
51-
return FakeObject(fake_manager, {"attr1": [1, 2, 3]})
51+
return FakeObject(fake_manager, {"attr1": "foo", "alist": [1, 2, 3]})
52+
53+
54+
@pytest.fixture
55+
def fake_object_with_parent(fake_manager):
56+
class MGR(base.RESTManager):
57+
_path = "/tests/{test_id}/cases"
58+
_obj_cls = object
59+
_from_parent_attrs = {"test_id": "id"}
60+
61+
class Parent:
62+
id = 42
63+
64+
mgr_with_parent = MGR(fake_gitlab, parent=Parent())
65+
return FakeObject(mgr_with_parent, {"attr1": "foo", "alist": [1, 2, 3]})
5266

5367

5468
class TestRESTManager:
@@ -313,22 +327,73 @@ def test_repr(self, fake_manager):
313327
assert repr(obj) == "<FakeObject>"
314328

315329
def test_attributes_get(self, fake_object):
316-
assert fake_object.attr1 == [1, 2, 3]
330+
assert fake_object.attr1 == "foo"
317331
result = fake_object.attributes
318-
assert result == {"attr1": [1, 2, 3]}
332+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
319333

320334
def test_attributes_shows_updates(self, fake_object):
321335
# Updated attribute value is reflected in `attributes`
322336
fake_object.attr1 = "hello"
323-
assert fake_object.attributes == {"attr1": "hello"}
337+
assert fake_object.attributes == {"attr1": "hello", "alist": [1, 2, 3]}
324338
assert fake_object.attr1 == "hello"
325339
# New attribute is in `attributes`
326340
fake_object.new_attrib = "spam"
327-
assert fake_object.attributes == {"attr1": "hello", "new_attrib": "spam"}
341+
assert fake_object.attributes == {
342+
"attr1": "hello",
343+
"new_attrib": "spam",
344+
"alist": [1, 2, 3],
345+
}
328346

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

0 commit comments

Comments
 (0)