Skip to content

Commit c188cb0

Browse files
feat: add an asdict() method to the 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.
1 parent d5de4b1 commit c188cb0

File tree

3 files changed

+105
-6
lines changed

3 files changed

+105
-6
lines changed

docs/api-usage.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,35 @@ 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. This can also be used
219+
to create a JSON representation of the object. There are two ways to retrieve a
220+
dictionary representation of the Gitlab Object.
221+
222+
* `asdict()` method. Returns a dictionary with updated attributes having precedence.
223+
* `attributes` property. Returns a dictionary with original attributes having
224+
precedence and then updated attributes. Also returns any relevant parent object
225+
attributes.
226+
227+
.. note::
228+
229+
`attributes` returns the parent object attributes that are defined in
230+
`object._from_parent_attrs`. What this can mean is that for example a `ProjectIssue`
231+
object will have a `project_id` key in the dictionary returned from `attributes` but
232+
`asdict()` will not.
233+
234+
235+
.. code-block:: python
236+
237+
project = gl.projects.get(1)
238+
project_dict = project.asdict()
239+
# Do a JSON dump of the object
240+
print(json.dumps(project.asdict()))
241+
242+
# Or a dictionary representation also containing some of the parent attributes
243+
issue = project.issues.get(1)
244+
attribute_dict = issue.attributes
245+
217246
218247
Base types
219248
==========

gitlab/base.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# You should have received a copy of the GNU Lesser General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

18+
import copy
1819
import importlib
1920
import pprint
2021
import textwrap
@@ -142,15 +143,16 @@ def __getattr__(self, name: str) -> Any:
142143
def __setattr__(self, name: str, value: Any) -> None:
143144
self.__dict__["_updated_attrs"][name] = value
144145

146+
def asdict(self) -> Dict[str, Any]:
147+
data = copy.deepcopy(self._attrs)
148+
data.update(copy.deepcopy(self._updated_attrs))
149+
return data
150+
145151
def __str__(self) -> str:
146-
data = self._attrs.copy()
147-
data.update(self._updated_attrs)
148-
return f"{type(self)} => {data}"
152+
return f"{type(self)} => {self.asdict()}"
149153

150154
def pformat(self) -> str:
151-
data = self._attrs.copy()
152-
data.update(self._updated_attrs)
153-
return f"{type(self)} => \n{pprint.pformat(data)}"
155+
return f"{type(self)} => \n{pprint.pformat(self.asdict())}"
154156

155157
def pprint(self) -> None:
156158
print(self.pformat())

tests/unit/test_base.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,71 @@ def test_repr(self, fake_manager):
306306

307307
FakeObject._id_attr = None
308308
assert repr(obj) == "<FakeObject>"
309+
310+
def test_asdict(self, fake_manager):
311+
fake_object = FakeObject(fake_manager, {"attr1": "foo", "alist": [1, 2, 3]})
312+
assert fake_object.attr1 == "foo"
313+
result = fake_object.asdict()
314+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
315+
# Demonstrate modifying the dictionary does not modify the object
316+
result["attr1"] = "testing"
317+
result["alist"].append(4)
318+
assert result == {"attr1": "testing", "alist": [1, 2, 3, 4]}
319+
assert fake_object.attr1 == "foo"
320+
assert fake_object.alist == [1, 2, 3]
321+
# asdict() returns the updated value
322+
fake_object.attr1 = "spam"
323+
assert fake_object.asdict() == {"attr1": "spam", "alist": [1, 2, 3]}
324+
# Modify attribute and then ensure modifying a list in the returned dict won't
325+
# modify the list in the object.
326+
fake_object.attr1 = [9, 7, 8]
327+
assert fake_object.asdict() == {
328+
"attr1": [9, 7, 8],
329+
"alist": [1, 2, 3],
330+
}
331+
result = fake_object.asdict()
332+
result["attr1"].append(1)
333+
assert fake_object.asdict() == {
334+
"attr1": [9, 7, 8],
335+
"alist": [1, 2, 3],
336+
}
337+
338+
def test_attributes(self, fake_manager):
339+
fake_object = FakeObject(fake_manager, {"attr1": [1, 2, 3]})
340+
assert fake_object.attr1 == [1, 2, 3]
341+
result = fake_object.attributes
342+
assert result == {"attr1": [1, 2, 3]}
343+
344+
# Updated attribute value is not reflected in `attributes`
345+
fake_object.attr1 = "hello"
346+
assert fake_object.attributes == {"attr1": [1, 2, 3]}
347+
assert fake_object.attr1 == "hello"
348+
# New attribute is in `attributes`
349+
fake_object.new_attrib = "spam"
350+
assert fake_object.attributes == {"attr1": [1, 2, 3], "new_attrib": "spam"}
351+
352+
# Modifying the dictionary can cause modification to the object :(
353+
result = fake_object.attributes
354+
result["attr1"].append(10)
355+
assert result == {"attr1": [1, 2, 3, 10], "new_attrib": "spam"}
356+
assert fake_object.attributes == {"attr1": [1, 2, 3, 10], "new_attrib": "spam"}
357+
assert fake_object.attr1 == "hello"
358+
359+
360+
def test_asdict_vs_attributes(self, fake_manager):
361+
fake_object = FakeObject(fake_manager, {"attr1": "foo"})
362+
assert fake_object.attr1 == "foo"
363+
result = fake_object.asdict()
364+
assert result == {"attr1": "foo"}
365+
366+
# New attribute added, return same result
367+
assert fake_object.attributes == fake_object.asdict()
368+
fake_object.attr2 = "eggs"
369+
assert fake_object.attributes == fake_object.asdict()
370+
# Update attribute, return different result
371+
fake_object.attr1 = "hello"
372+
assert fake_object.attributes != fake_object.asdict()
373+
# asdict() returns the updated value
374+
assert fake_object.asdict() == {"attr1": "hello", "attr2": "eggs"}
375+
# `attributes` returns original value
376+
assert fake_object.attributes == {"attr1": "foo", "attr2": "eggs"}

0 commit comments

Comments
 (0)