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

+32
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

+18-14
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

+79-8
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)