Skip to content

Commit

Permalink
feat: default serialization for custom objects (#101)
Browse files Browse the repository at this point in the history
* wip: custom repr example

* wip: provide sensible default serialization for custom objects

* chore: update changelog

* refactor: perform check on class

* refactor: one less line

* cr: indent keys

* fix: support callable and limit repr to public

* cr: skip methods when serializing custom objects
  • Loading branch information
iamogbz committed Jan 12, 2020
1 parent c634fa6 commit 6e99629
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 52 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ From v1.0.0 onwards, this project adheres to [Semantic Versioning](https://semve

## Master (Unreleased)

- Add support for custom objects repr (#101)
- Add support for nested test classes (#99)
- Removes `_snapshot_subdirectory_name` from `SnapshotFossilizer` (#99)
- Remove `_snapshot_subdirectory_name` from `SnapshotFossilizer` (#99)

## v0.1.0

Expand Down
26 changes: 2 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,37 +62,15 @@ A snapshot file should be generated under a `__snapshots__` directory in the sam

#### Custom Objects

The default serializer supports all python built-in types and when in doubt it falls back to __repr__.
The default serializer supports all python built-in types and provides a sensible default for custom objects.

This means your custom object needs to implement `__repr__` to prevent non deterministic snapshots.
If you need to customise your object snapshot, is as easy as overriding the default `__repr__` implementation.

```python
def __repr__(self) -> str:
return "MyCustomClass(...)"
```

It is recommended to split representation into multiple lines.

```python
def __repr__(self):
state = "\n".join(
f" {a}={getattr(self, a)}"
for a in sorted(dir(self))
if not a.startswith("__")
)
return f"{self.__class__.__name__}(\n{state}\n)"
```

This makes the snapshot diff in case of failures or updates easy to review.

```ambr
MyCustomSmartReprClass(
prop1=1
prop2=a
prop3={1, 2, 3}
)
```

### Options

These are the cli options exposed to `pytest` by the plugin.
Expand Down
21 changes: 20 additions & 1 deletion src/syrupy/extensions/amber.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,26 @@ def serialize_iterable(
def serialize_unknown(
cls, data: Any, *, depth: int = 0, visited: Optional[Set[Any]] = None
) -> str:
return cls.with_indent(repr(data), depth)
if data.__class__.__repr__ != object.__repr__:
return cls.with_indent(repr(data), depth)

return (
cls.with_indent(f"{cls.object_type(data)} {{\n", depth)
+ "".join(
f"{serialized_key}={serialized_value.lstrip(cls._indent)}\n"
for serialized_key, serialized_value in (
(
cls.with_indent(name, depth=depth + 1),
cls.serialize(
data=getattr(data, name), depth=depth + 1, visited=visited
),
)
for name in cls.sort(dir(data))
if not name.startswith("_") and not callable(getattr(data, name))
)
)
+ cls.with_indent("}", depth)
)

@classmethod
def serialize(
Expand Down
35 changes: 35 additions & 0 deletions tests/__snapshots__/test_extension_amber.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,41 @@
# name: test_bool[True]
True
---
# name: test_custom_object_repr
<class 'CustomClass'> {
a=1
b='2'
c=<class 'list'> [
1,
2,
3,
...,
]
d=<class 'dict'> {
'a': 1,
'b': 2,
'c': 3,
'd': ...,
}
x=<class 'CustomClass'> {
a=1
b='2'
c=<class 'list'> [
1,
2,
3,
...,
]
d=<class 'dict'> {
'a': 1,
'b': 2,
'c': 3,
'd': ...,
}
x=None
}
}
---
# name: test_cycle[cyclic0]
<class 'list'> [
1,
Expand Down
22 changes: 15 additions & 7 deletions tests/examples/__snapshots__/test_custom_object_repr.ambr
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# name: test_snapshot_attr_class
MyCustomReprClass(prop1=1, prop2='a', prop3={1, 2, 3})
---
# name: test_snapshot_smart_class
MyCustomSmartReprClass(
# name: test_snapshot_custom_class
<class 'MyCustomClass'> {
prop1=1
prop2=a
prop3={1, 2, 3}
prop2='a'
prop3=<class 'set'> {
1,
2,
3,
}
}
---
# name: test_snapshot_custom_repr_class
MyCustomReprClass(
prop1=1,
prop2='a',
prop3={1, 2, 3},
)
---
30 changes: 11 additions & 19 deletions tests/examples/test_custom_object_repr.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
import attr


@attr.s
class MyCustomReprClass:
prop1 = attr.ib(default=1)
prop2 = attr.ib(default="a")
prop3 = attr.ib(default={1, 2, 3})


def test_snapshot_attr_class(snapshot):
assert MyCustomReprClass() == snapshot


class MyCustomSmartReprClass:
class MyCustomClass:
prop1 = 1
prop2 = "a"
prop3 = {1, 2, 3}


def test_snapshot_custom_class(snapshot):
assert MyCustomClass() == snapshot


class MyCustomReprClass(MyCustomClass):
def __repr__(self):
state = "\n".join(
f" {a}={getattr(self, a)}"
f" {a}={repr(getattr(self, a))},"
for a in sorted(dir(self))
if not a.startswith("__")
if not a.startswith("_")
)
return f"{self.__class__.__name__}(\n{state}\n)"


def test_snapshot_smart_class(snapshot):
assert MyCustomSmartReprClass() == snapshot
def test_snapshot_custom_repr_class(snapshot):
assert MyCustomReprClass() == snapshot
27 changes: 27 additions & 0 deletions tests/test_extension_amber.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,33 @@ def test_cycle(cyclic, snapshot):
assert cyclic == snapshot


class CustomClass:
a = 1
b = "2"
c = list_cycle
d = dict_cycle
_protected_variable = None
__private_variable = None

def __init__(self, x=None):
self.x = x
self._y = 1
self.__z = 2

def public_method(self, a, b=1, *, c, d=None):
pass

def _protected_method(self):
pass

def __private_method(self):
pass


def test_custom_object_repr(snapshot):
assert CustomClass(CustomClass()) == snapshot


class TestClass:
def test_class_method_name(self, snapshot):
assert snapshot == "this is in a test class"
Expand Down

0 comments on commit 6e99629

Please sign in to comment.