Skip to content

Commit 1420b7d

Browse files
authored
match_object() - Improve serialization for properties/iterators/enums (#17)
1 parent 317805f commit 1420b7d

File tree

2 files changed

+84
-9
lines changed

2 files changed

+84
-9
lines changed

localstack_snapshot/snapshots/prototype.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import json
33
import logging
44
import os
5+
from collections.abc import Iterator
56
from datetime import datetime, timezone
7+
from enum import Enum
68
from json import JSONDecodeError
79
from pathlib import Path
810
from re import Pattern
@@ -169,16 +171,27 @@ def _update(self, key: str, obj_state: dict) -> None:
169171
def match_object(self, key: str, obj: object) -> None:
170172
def _convert_object_to_dict(obj_):
171173
if isinstance(obj_, dict):
172-
for key in list(obj_.keys()):
173-
if key.startswith("_"):
174-
del obj_[key]
175-
else:
176-
obj_[key] = _convert_object_to_dict(obj_[key])
177-
elif isinstance(obj_, list):
178-
for idx, val in enumerate(obj_):
179-
obj_[idx] = _convert_object_to_dict(val)
174+
# Serialize the values of the dictionary, while skipping any private keys (starting with '_')
175+
return {
176+
key_: _convert_object_to_dict(obj_[key_])
177+
for key_ in obj_
178+
if not key_.startswith("_")
179+
}
180+
elif isinstance(obj_, (list, Iterator)):
181+
return [_convert_object_to_dict(val) for val in obj_]
182+
elif isinstance(obj_, Enum):
183+
return obj_.value
180184
elif hasattr(obj_, "__dict__"):
181-
return _convert_object_to_dict(obj_.__dict__)
185+
# This is an object - let's try to convert it to a dictionary
186+
# A naive approach would be to use the '__dict__' object directly, but that only lists the attributes
187+
# In order to also serialize the properties, we use the __dir__() method
188+
# Filtering by everything that is not a method gives us both attributes and properties
189+
# We also (still) skip private attributes/properties, so everything that starts with an underscore
190+
return {
191+
k: _convert_object_to_dict(getattr(obj_, k))
192+
for k in obj_.__dir__()
193+
if not k.startswith("_") and type(getattr(obj_, k, "")).__name__ != "method"
194+
}
182195
return obj_
183196

184197
return self.match(key, _convert_object_to_dict(obj))

tests/test_snapshots.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import io
2+
from enum import Enum
23

34
import pytest
45

@@ -75,6 +76,67 @@ def __init__(self, name):
7576
sm.match_object("key_a", CustomObject(name="myname"))
7677
sm._assert_all()
7778

79+
def test_match_object_lists_and_iterators(self):
80+
class CustomObject:
81+
def __init__(self, name):
82+
self.name = name
83+
self.my_list = [9, 8, 7, 6, 5]
84+
self.my_iterator = (x for x in range(5))
85+
86+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
87+
sm.recorded_state = {
88+
"key_a": {"name": "myname", "my_iterator": [0, 1, 2, 3, 4], "my_list": [9, 8, 7, 6, 5]}
89+
}
90+
sm.match_object("key_a", CustomObject(name="myname"))
91+
sm._assert_all()
92+
93+
def test_match_object_include_properties(self):
94+
class CustomObject:
95+
def __init__(self, name):
96+
self.name = name
97+
self._internal = "n/a"
98+
99+
def some_method(self):
100+
# method should not be serialized
101+
return False
102+
103+
@property
104+
def some_prop(self):
105+
# properties should be serialized
106+
return True
107+
108+
@property
109+
def some_iterator(self):
110+
for i in range(3):
111+
yield i
112+
113+
@property
114+
def _private_prop(self):
115+
# private properties should be ignored
116+
return False
117+
118+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
119+
sm.recorded_state = {
120+
"key_a": {"name": "myname", "some_prop": True, "some_iterator": [0, 1, 2]}
121+
}
122+
sm.match_object("key_a", CustomObject(name="myname"))
123+
sm._assert_all()
124+
125+
def test_match_object_enums(self):
126+
class TestEnum(Enum):
127+
value1 = "Value 1"
128+
value2 = "Value 2"
129+
130+
class CustomObject:
131+
def __init__(self, name):
132+
self.name = name
133+
self.my_enum = TestEnum.value2
134+
135+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
136+
sm.recorded_state = {"key_a": {"name": "myname", "my_enum": "Value 2"}}
137+
sm.match_object("key_a", CustomObject(name="myname"))
138+
sm._assert_all()
139+
78140
def test_match_object_change(self):
79141
class CustomObject:
80142
def __init__(self, name):

0 commit comments

Comments
 (0)