summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn L. Villalovos <john@sodarock.com>2022-07-20 08:38:43 -0700
committerJohn L. Villalovos <john@sodarock.com>2022-07-20 08:38:43 -0700
commit08ac071abcbc28af04c0fa655576e25edbdaa4e2 (patch)
tree1867dc97f539247ac70018f43894d28d1add0190
parente5affc8749797293c1373c6af96334f194875038 (diff)
downloadgitlab-08ac071abcbc28af04c0fa655576e25edbdaa4e2.tar.gz
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
-rw-r--r--docs/api-usage.rst32
-rw-r--r--gitlab/base.py32
-rw-r--r--tests/unit/test_base.py87
3 files changed, 129 insertions, 22 deletions
diff --git a/docs/api-usage.rst b/docs/api-usage.rst
index 000633f..24d0890 100644
--- a/docs/api-usage.rst
+++ b/docs/api-usage.rst
@@ -214,6 +214,38 @@ the value on the object is accepted:
issue.my_super_awesome_feature_flag = "random_value"
issue.save()
+You can get a dictionary representation copy of the Gitlab Object. Modifications made to
+the dictionary will have no impact on the GitLab Object.
+
+ * `asdict()` method. Returns a dictionary representation of the Gitlab object.
+ * `attributes` property. Returns a dictionary representation of the Gitlab
+ object. Also returns any relevant parent object attributes.
+
+.. note::
+
+ `attributes` returns the parent object attributes that are defined in
+ `object._from_parent_attrs`. What this can mean is that for example a `ProjectIssue`
+ object will have a `project_id` key in the dictionary returned from `attributes` but
+ `asdict()` will not.
+
+
+.. code-block:: python
+
+ project = gl.projects.get(1)
+ project_dict = project.asdict()
+
+ # Or a dictionary representation also containing some of the parent attributes
+ issue = project.issues.get(1)
+ attribute_dict = issue.attributes
+
+You can get a JSON string represenation of the Gitlab Object. For example:
+
+.. code-block:: python
+
+ project = gl.projects.get(1)
+ print(project.to_json())
+ # Use arguments supported by `json.dump()`
+ print(project.to_json(sort_keys=True, indent=4))
Base types
==========
diff --git a/gitlab/base.py b/gitlab/base.py
index 7de76e0..dd69d00 100644
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -17,6 +17,7 @@
import copy
import importlib
+import json
import pprint
import textwrap
from types import ModuleType
@@ -143,15 +144,26 @@ class RESTObject:
def __setattr__(self, name: str, value: Any) -> None:
self.__dict__["_updated_attrs"][name] = value
+ def asdict(self, *, with_parent_attrs: bool = False) -> Dict[str, Any]:
+ data = {}
+ if with_parent_attrs:
+ data.update(copy.deepcopy(self._parent_attrs))
+ data.update(copy.deepcopy(self._attrs))
+ data.update(copy.deepcopy(self._updated_attrs))
+ return data
+
+ @property
+ def attributes(self) -> Dict[str, Any]:
+ return self.asdict(with_parent_attrs=True)
+
+ def to_json(self, *, with_parent_attrs: bool = False, **kwargs: Any) -> str:
+ return json.dumps(self.asdict(with_parent_attrs=with_parent_attrs), **kwargs)
+
def __str__(self) -> str:
- data = self._attrs.copy()
- data.update(self._updated_attrs)
- return f"{type(self)} => {data}"
+ return f"{type(self)} => {self.asdict()}"
def pformat(self) -> str:
- data = self._attrs.copy()
- data.update(self._updated_attrs)
- return f"{type(self)} => \n{pprint.pformat(data)}"
+ return f"{type(self)} => \n{pprint.pformat(self.asdict())}"
def pprint(self) -> None:
print(self.pformat())
@@ -242,14 +254,6 @@ class RESTObject:
obj_id = gitlab.utils.EncodedId(obj_id)
return obj_id
- @property
- def attributes(self) -> Dict[str, Any]:
- data = {}
- data.update(copy.deepcopy(self._parent_attrs))
- data.update(copy.deepcopy(self._attrs))
- data.update(copy.deepcopy(self._updated_attrs))
- return data
-
class RESTObjectList:
"""Generator object representing a list of RESTObject's.
diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py
index 53e484e..529135a 100644
--- a/tests/unit/test_base.py
+++ b/tests/unit/test_base.py
@@ -36,6 +36,16 @@ class FakeManager(base.RESTManager):
_path = "/tests"
+class FakeParent:
+ id = 42
+
+
+class FakeManagerWithParent(base.RESTManager):
+ _path = "/tests/{test_id}/cases"
+ _obj_cls = FakeObject
+ _from_parent_attrs = {"test_id": "id"}
+
+
@pytest.fixture
def fake_gitlab():
return FakeGitlab()
@@ -47,8 +57,18 @@ def fake_manager(fake_gitlab):
@pytest.fixture
+def fake_manager_with_parent(fake_gitlab):
+ return FakeManagerWithParent(fake_gitlab, parent=FakeParent)
+
+
+@pytest.fixture
def fake_object(fake_manager):
- return FakeObject(fake_manager, {"attr1": [1, 2, 3]})
+ return FakeObject(fake_manager, {"attr1": "foo", "alist": [1, 2, 3]})
+
+
+@pytest.fixture
+def fake_object_with_parent(fake_manager_with_parent):
+ return FakeObject(fake_manager_with_parent, {"attr1": "foo", "alist": [1, 2, 3]})
class TestRESTManager:
@@ -313,22 +333,73 @@ class TestRESTObject:
assert repr(obj) == "<FakeObject>"
def test_attributes_get(self, fake_object):
- assert fake_object.attr1 == [1, 2, 3]
+ assert fake_object.attr1 == "foo"
result = fake_object.attributes
- assert result == {"attr1": [1, 2, 3]}
+ assert result == {"attr1": "foo", "alist": [1, 2, 3]}
def test_attributes_shows_updates(self, fake_object):
# Updated attribute value is reflected in `attributes`
fake_object.attr1 = "hello"
- assert fake_object.attributes == {"attr1": "hello"}
+ assert fake_object.attributes == {"attr1": "hello", "alist": [1, 2, 3]}
assert fake_object.attr1 == "hello"
# New attribute is in `attributes`
fake_object.new_attrib = "spam"
- assert fake_object.attributes == {"attr1": "hello", "new_attrib": "spam"}
+ assert fake_object.attributes == {
+ "attr1": "hello",
+ "new_attrib": "spam",
+ "alist": [1, 2, 3],
+ }
def test_attributes_is_copy(self, fake_object):
# Modifying the dictionary does not cause modifications to the object
result = fake_object.attributes
- result["attr1"].append(10)
- assert result == {"attr1": [1, 2, 3, 10]}
- assert fake_object.attributes == {"attr1": [1, 2, 3]}
+ result["alist"].append(10)
+ assert result == {"attr1": "foo", "alist": [1, 2, 3, 10]}
+ assert fake_object.attributes == {"attr1": "foo", "alist": [1, 2, 3]}
+
+ def test_attributes_has_parent_attrs(self, fake_object_with_parent):
+ assert fake_object_with_parent.attr1 == "foo"
+ result = fake_object_with_parent.attributes
+ assert result == {"attr1": "foo", "alist": [1, 2, 3], "test_id": "42"}
+
+ def test_asdict(self, fake_object):
+ assert fake_object.attr1 == "foo"
+ result = fake_object.asdict()
+ assert result == {"attr1": "foo", "alist": [1, 2, 3]}
+
+ def test_asdict_no_parent_attrs(self, fake_object_with_parent):
+ assert fake_object_with_parent.attr1 == "foo"
+ result = fake_object_with_parent.asdict()
+ assert result == {"attr1": "foo", "alist": [1, 2, 3]}
+ assert "test_id" not in fake_object_with_parent.asdict()
+ assert "test_id" not in fake_object_with_parent.asdict(with_parent_attrs=False)
+ assert "test_id" in fake_object_with_parent.asdict(with_parent_attrs=True)
+
+ def test_asdict_modify_dict_does_not_change_object(self, fake_object):
+ result = fake_object.asdict()
+ # Demonstrate modifying the dictionary does not modify the object
+ result["attr1"] = "testing"
+ result["alist"].append(4)
+ assert result == {"attr1": "testing", "alist": [1, 2, 3, 4]}
+ assert fake_object.attr1 == "foo"
+ assert fake_object.alist == [1, 2, 3]
+
+ def test_asdict_modify_dict_does_not_change_object2(self, fake_object):
+ # Modify attribute and then ensure modifying a list in the returned dict won't
+ # modify the list in the object.
+ fake_object.attr1 = [9, 7, 8]
+ assert fake_object.asdict() == {
+ "attr1": [9, 7, 8],
+ "alist": [1, 2, 3],
+ }
+ result = fake_object.asdict()
+ result["attr1"].append(1)
+ assert fake_object.asdict() == {
+ "attr1": [9, 7, 8],
+ "alist": [1, 2, 3],
+ }
+
+ def test_asdict_modify_object(self, fake_object):
+ # asdict() returns the updated value
+ fake_object.attr1 = "spam"
+ assert fake_object.asdict() == {"attr1": "spam", "alist": [1, 2, 3]}