Skip to content

Commit 7220c55

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 64d01ef commit 7220c55

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
@@ -192,6 +192,35 @@ You can print a Gitlab Object. For example:
192192
# Or explicitly via `pformat()`. This is equivalent to the above.
193193
print(project.pformat())
194194
195+
You can get a dictionary representation copy of the Gitlab Object. Modifications made to
196+
the dictionary will have no impact on the GitLab Object. This can also be used
197+
to create a JSON representation of the object. There are two ways to retrieve a
198+
dictionary representation of the Gitlab Object.
199+
200+
* `asdict()` method. Returns a dictionary with updated attributes having precedence.
201+
* `attributes` property. Returns a dictionary with original attributes having
202+
precedence and then updated attributes. Also returns any relevant parent object
203+
attributes.
204+
205+
.. note::
206+
207+
`attributes` returns the parent object attributes that are defined in
208+
`object._from_parent_attrs`. What this can mean is that for example a `ProjectIssue`
209+
object will have a `project_id` key in the dictionary returned from `attributes` but
210+
`asdict()` will not.
211+
212+
213+
.. code-block:: python
214+
215+
project = gl.projects.get(1)
216+
project_dict = project.asdict()
217+
# Do a JSON dump of the object
218+
print(json.dumps(project.asdict()))
219+
220+
# Or a dictionary representation also containing some of the parent attributes
221+
issue = project.issues.get(1)
222+
attribute_dict = issue.attributes
223+
195224
196225
Base types
197226
==========

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
@@ -144,15 +145,16 @@ def __getattr__(self, name: str) -> Any:
144145
def __setattr__(self, name: str, value: Any) -> None:
145146
self.__dict__["_updated_attrs"][name] = value
146147

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

152156
def pformat(self) -> str:
153-
data = self._attrs.copy()
154-
data.update(self._updated_attrs)
155-
return f"{type(self)} => \n{pprint.pformat(data)}"
157+
return f"{type(self)} => \n{pprint.pformat(self.asdict())}"
156158

157159
def pprint(self) -> None:
158160
print(self.pformat())

tests/unit/test_base.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,71 @@ def test_pprint(self, capfd, fake_manager):
249249
" 'ham': 'eggseggseggseggseggseggseggseggseggseggseggseggseggseggseggs'}\n"
250250
)
251251
assert stderr == ""
252+
253+
def test_asdict(self, fake_manager):
254+
fake_object = FakeObject(fake_manager, {"attr1": "foo", "alist": [1, 2, 3]})
255+
assert fake_object.attr1 == "foo"
256+
result = fake_object.asdict()
257+
assert result == {"attr1": "foo", "alist": [1, 2, 3]}
258+
# Demonstrate modifying the dictionary does not modify the object
259+
result["attr1"] = "testing"
260+
result["alist"].append(4)
261+
assert result == {"attr1": "testing", "alist": [1, 2, 3, 4]}
262+
assert fake_object.attr1 == "foo"
263+
assert fake_object.alist == [1, 2, 3]
264+
# asdict() returns the updated value
265+
fake_object.attr1 = "spam"
266+
assert fake_object.asdict() == {"attr1": "spam", "alist": [1, 2, 3]}
267+
# Modify attribute and then ensure modifying a list in the returned dict won't
268+
# modify the list in the object.
269+
fake_object.attr1 = [9, 7, 8]
270+
assert fake_object.asdict() == {
271+
"attr1": [9, 7, 8],
272+
"alist": [1, 2, 3],
273+
}
274+
result = fake_object.asdict()
275+
result["attr1"].append(1)
276+
assert fake_object.asdict() == {
277+
"attr1": [9, 7, 8],
278+
"alist": [1, 2, 3],
279+
}
280+
281+
def test_attributes(self, fake_manager):
282+
fake_object = FakeObject(fake_manager, {"attr1": [1, 2, 3]})
283+
assert fake_object.attr1 == [1, 2, 3]
284+
result = fake_object.attributes
285+
assert result == {"attr1": [1, 2, 3]}
286+
287+
# Updated attribute value is not reflected in `attributes`
288+
fake_object.attr1 = "hello"
289+
assert fake_object.attributes == {"attr1": [1, 2, 3]}
290+
assert fake_object.attr1 == "hello"
291+
# New attribute is in `attributes`
292+
fake_object.new_attrib = "spam"
293+
assert fake_object.attributes == {"attr1": [1, 2, 3], "new_attrib": "spam"}
294+
295+
# Modifying the dictionary can cause modification to the object :(
296+
result = fake_object.attributes
297+
result["attr1"].append(10)
298+
assert result == {"attr1": [1, 2, 3, 10], "new_attrib": "spam"}
299+
assert fake_object.attributes == {"attr1": [1, 2, 3, 10], "new_attrib": "spam"}
300+
assert fake_object.attr1 == "hello"
301+
302+
303+
def test_asdict_vs_attributes(self, fake_manager):
304+
fake_object = FakeObject(fake_manager, {"attr1": "foo"})
305+
assert fake_object.attr1 == "foo"
306+
result = fake_object.asdict()
307+
assert result == {"attr1": "foo"}
308+
309+
# New attribute added, return same result
310+
assert fake_object.attributes == fake_object.asdict()
311+
fake_object.attr2 = "eggs"
312+
assert fake_object.attributes == fake_object.asdict()
313+
# Update attribute, return different result
314+
fake_object.attr1 = "hello"
315+
assert fake_object.attributes != fake_object.asdict()
316+
# asdict() returns the updated value
317+
assert fake_object.asdict() == {"attr1": "hello", "attr2": "eggs"}
318+
# `attributes` returns original value
319+
assert fake_object.attributes == {"attr1": "foo", "attr2": "eggs"}

0 commit comments

Comments
 (0)