Skip to content

Commit 973e68f

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 973e68f

File tree

3 files changed

+96
-22
lines changed

3 files changed

+96
-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: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ 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]})
5252

5353

5454
class TestRESTManager:
@@ -313,22 +313,60 @@ def test_repr(self, fake_manager):
313313
assert repr(obj) == "<FakeObject>"
314314

315315
def test_attributes_get(self, fake_object):
316-
assert fake_object.attr1 == [1, 2, 3]
316+
assert fake_object.attr1 == "foo"
317317
result = fake_object.attributes
318-
assert result == {"attr1": [1, 2, 3]}
318+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
319319

320320
def test_attributes_shows_updates(self, fake_object):
321321
# Updated attribute value is reflected in `attributes`
322322
fake_object.attr1 = "hello"
323-
assert fake_object.attributes == {"attr1": "hello"}
323+
assert fake_object.attributes == {"attr1": "hello", "alist": [1, 2, 3]}
324324
assert fake_object.attr1 == "hello"
325325
# New attribute is in `attributes`
326326
fake_object.new_attrib = "spam"
327-
assert fake_object.attributes == {"attr1": "hello", "new_attrib": "spam"}
327+
assert fake_object.attributes == {
328+
"attr1": "hello",
329+
"new_attrib": "spam",
330+
"alist": [1, 2, 3],
331+
}
328332

329333
def test_attributes_is_copy(self, fake_object):
330334
# Modifying the dictionary does not cause modifications to the object
331335
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]}
336+
result["alist"].append(10)
337+
assert result == {"attr1": "foo", "alist": [1, 2, 3, 10]}
338+
assert fake_object.attributes == {"attr1": "foo", "alist": [1, 2, 3]}
339+
340+
def test_asdict(self, fake_object):
341+
assert fake_object.attr1 == "foo"
342+
result = fake_object.asdict()
343+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
344+
345+
def test_asdict_modify_dict_does_not_change_object(self, fake_object):
346+
result = fake_object.asdict()
347+
# Demonstrate modifying the dictionary does not modify the object
348+
result["attr1"] = "testing"
349+
result["alist"].append(4)
350+
assert result == {"attr1": "testing", "alist": [1, 2, 3, 4]}
351+
assert fake_object.attr1 == "foo"
352+
assert fake_object.alist == [1, 2, 3]
353+
354+
def test_asdict_modify_dict_does_not_change_object2(self, fake_object):
355+
# Modify attribute and then ensure modifying a list in the returned dict won't
356+
# modify the list in the object.
357+
fake_object.attr1 = [9, 7, 8]
358+
assert fake_object.asdict() == {
359+
"attr1": [9, 7, 8],
360+
"alist": [1, 2, 3],
361+
}
362+
result = fake_object.asdict()
363+
result["attr1"].append(1)
364+
assert fake_object.asdict() == {
365+
"attr1": [9, 7, 8],
366+
"alist": [1, 2, 3],
367+
}
368+
369+
def test_asdict_modify_object(self, fake_object):
370+
# asdict() returns the updated value
371+
fake_object.attr1 = "spam"
372+
assert fake_object.asdict() == {"attr1": "spam", "alist": [1, 2, 3]}

0 commit comments

Comments
 (0)