diff options
| author | John L. Villalovos <john@sodarock.com> | 2022-07-20 08:38:43 -0700 |
|---|---|---|
| committer | John L. Villalovos <john@sodarock.com> | 2022-07-20 08:38:43 -0700 |
| commit | 08ac071abcbc28af04c0fa655576e25edbdaa4e2 (patch) | |
| tree | 1867dc97f539247ac70018f43894d28d1add0190 | |
| parent | e5affc8749797293c1373c6af96334f194875038 (diff) | |
| download | gitlab-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.rst | 32 | ||||
| -rw-r--r-- | gitlab/base.py | 32 | ||||
| -rw-r--r-- | tests/unit/test_base.py | 87 |
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]} |
