Skip to content

Commit

Permalink
feat(amber): change serialization to be py syntax like
Browse files Browse the repository at this point in the history
BREAKING CHANGE: update to serialization requires regeneration of snapshots

Migration Guide
* `pytest --snapshot-update` to regenerate amber snapshots
* `PropertyFilter` type is now explicit about keyword arguments
* `PropertyMatcher` type is now explicit about keyword arguments
  • Loading branch information
iamogbz committed Jun 5, 2021
1 parent b48ed55 commit 0a12e70
Show file tree
Hide file tree
Showing 19 changed files with 400 additions and 353 deletions.
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,13 @@ def test_bar(snapshot):
}))
```

```ambr
```py
# name: test_bar
<class 'dict'> {
'date_created': <class 'datetime'>,
dict({
'date_created': datetime,
'value': 'Some computed value!!',
}
---
})
# ---
```

#### `exclude`
Expand Down Expand Up @@ -203,15 +203,15 @@ def test_bar(snapshot):
assert actual == snapshot(exclude=props("id", "1"))
```

```ambr
```py
# name: test_bar
<class 'dict'> {
'list': <class 'list'> [
dict({
'list': list([
1,
3,
],
}
---
]),
})
# ---
```

###### `paths(path_string, *path_strings)`
Expand All @@ -231,15 +231,15 @@ def test_bar(snapshot):
assert actual == snapshot(exclude=paths("date", "list.1"))
```

```ambr
```py
# name: test_bar
<class 'dict'> {
'list': <class 'list'> [
dict({
'list': list([
1,
3,
],
}
---
]),
})
# ---
```

#### `extension_class`
Expand Down
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from syrupy.utils import env_context

typing.TYPE_CHECKING = True
typing.TYPE_CHECKING = False
pytest_plugins = "pytester"


Expand Down
6 changes: 2 additions & 4 deletions src/syrupy/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,8 @@ def __call__(
self.__with_prop("_matcher", matcher)
return self

def __repr__(self) -> str:
attrs_to_repr = ["name", "num_executions"]
attrs_repr = ", ".join(f"{a}={repr(getattr(self, a))}" for a in attrs_to_repr)
return f"SnapshotAssertion({attrs_repr})"
def __dir__(self) -> List[str]:
return ["name", "num_executions"]

def __eq__(self, other: "SerializableData") -> bool:
return self._assert(other)
Expand Down
8 changes: 0 additions & 8 deletions src/syrupy/extensions/amber/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@
class AmberSnapshotExtension(AbstractSyrupyExtension):
"""
An amber snapshot file stores data in the following format:
```
# name: test_name_1
data
---
# name: test_name_2
data
```
"""

def serialize(self, data: "SerializableData", **kwargs: Any) -> str:
Expand Down
95 changes: 51 additions & 44 deletions src/syrupy/extensions/amber/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
NamedTuple,
Optional,
Set,
Tuple,
Union,
)

from syrupy.constants import SYMBOL_ELLIPSIS
Expand All @@ -25,8 +28,10 @@
SerializableData,
)

PropertyValueFilter = Callable[[SerializableData], bool]
PropertyValueGetter = Callable[..., SerializableData]
PropertyValueFilter = Callable[["PropertyName"], bool]
PropertyValueGetter = Callable[
["SerializableData", "PropertyName"], "SerializableData"
]
IterableEntries = Tuple[
Iterable["PropertyName"],
"PropertyValueGetter",
Expand All @@ -42,11 +47,20 @@ def __repr__(self) -> str:
return self._repr


def attr_getter(o: "SerializableData", p: "PropertyName") -> "SerializableData":
return getattr(o, str(p))


def item_getter(o: "SerializableData", p: "PropertyName") -> "SerializableData":
return o[p]


class DataSerializer:
_indent: str = " "
_max_depth: int = 99
_marker_divider: str = "---"
_marker_name: str = "# name:"
_marker_comment = "# "
_marker_divider: str = f"{_marker_comment}---"
_marker_name: str = f"{_marker_comment}name:"
_marker_crn: str = "\r\n"

@classmethod
Expand Down Expand Up @@ -157,14 +171,12 @@ def _serialize(

@classmethod
def serialize_number(
cls, data: "SerializableData", *, depth: int = 0, **kwargs: Any
cls, data: Union[int, float], *, depth: int = 0, **kwargs: Any
) -> str:
return cls.__serialize_plain(data=data, depth=depth)

@classmethod
def serialize_string(
cls, data: "SerializableData", *, depth: int = 0, **kwargs: Any
) -> str:
def serialize_string(cls, data: str, *, depth: int = 0, **kwargs: Any) -> str:
if all(c not in data for c in cls._marker_crn):
return cls.__serialize_plain(data=data, depth=depth)

Expand All @@ -175,60 +187,57 @@ def serialize_string(
for line in str(data).splitlines(keepends=True)
),
depth=depth,
open_tag="'",
close_tag="'",
open_tag="'''",
close_tag="'''",
include_type=False,
ends="",
)

@classmethod
def serialize_iterable(cls, data: "SerializableData", **kwargs: Any) -> str:
open_paren, close_paren = next(
parens
for iter_type, parens in {
GeneratorType: ("(", ")"),
list: ("[", "]"),
tuple: ("(", ")"),
}.items()
if isinstance(data, iter_type)
)
def serialize_iterable(
cls, data: Iterable["SerializableData"], **kwargs: Any
) -> str:
open_paren, close_paren = (None, None)
if isinstance(data, list):
open_paren, close_paren = ("[", "]")

values = list(data)
return cls.__serialize_iterable(
data=data,
resolve_entries=(range(len(values)), lambda _, p: values[p], None),
open_tag=open_paren,
close_tag=close_paren,
resolve_entries=(range(len(values)), item_getter, None),
open_paren=open_paren,
close_paren=close_paren,
**kwargs,
)

@classmethod
def serialize_set(cls, data: "SerializableData", **kwargs: Any) -> str:
def serialize_set(cls, data: Set["SerializableData"], **kwargs: Any) -> str:
return cls.__serialize_iterable(
data=data,
resolve_entries=(cls.sort(data), lambda _, p: p, None),
open_tag="{",
close_tag="}",
open_paren="{",
close_paren="}",
**kwargs,
)

@classmethod
def serialize_namedtuple(cls, data: "SerializableData", **kwargs: Any) -> str:
def serialize_namedtuple(cls, data: NamedTuple, **kwargs: Any) -> str:
return cls.__serialize_iterable(
data=data,
resolve_entries=(cls.sort(data._fields), getattr, None),
open_tag="(",
close_tag=")",
resolve_entries=(cls.sort(data._fields), attr_getter, None),
separator="=",
**kwargs,
)

@classmethod
def serialize_dict(cls, data: "SerializableData", **kwargs: Any) -> str:
def serialize_dict(
cls, data: Dict["PropertyName", "SerializableData"], **kwargs: Any
) -> str:
return cls.__serialize_iterable(
data=data,
resolve_entries=(cls.sort(data.keys()), lambda d, p: d[p], None),
open_tag="{",
close_tag="}",
resolve_entries=(cls.sort(data.keys()), item_getter, None),
open_paren="{",
close_paren="}",
separator=": ",
serialize_key=True,
**kwargs,
Expand All @@ -242,13 +251,11 @@ def serialize_unknown(cls, data: Any, *, depth: int = 0, **kwargs: Any) -> str:
return cls.__serialize_iterable(
data=data,
resolve_entries=(
(name for name in cls.sort(dir(data)) if not name.startswith("_")),
getattr,
iter(name for name in cls.sort(dir(data)) if not name.startswith("_")),
attr_getter,
lambda v: not callable(v),
),
depth=depth,
open_tag="{",
close_tag="}",
separator="=",
**kwargs,
)
Expand All @@ -266,7 +273,7 @@ def sort(cls, iterable: Iterable[Any]) -> Iterable[Any]:

@classmethod
def object_type(cls, data: "SerializableData") -> str:
return f"<class '{data.__class__.__name__}'>"
return f"{data.__class__.__name__}"

@classmethod
def __is_namedtuple(cls, obj: Any) -> bool:
Expand All @@ -289,8 +296,8 @@ def __serialize_iterable(
*,
data: "SerializableData",
resolve_entries: "IterableEntries",
open_tag: str,
close_tag: str,
open_paren: Optional[str] = None,
close_paren: Optional[str] = None,
depth: int = 0,
exclude: Optional["PropertyFilter"] = None,
path: "PropertyPath" = (),
Expand Down Expand Up @@ -331,8 +338,8 @@ def value_str(key: "PropertyName", value: "SerializableData") -> str:
data=data,
lines=(f"{key_str(key)}{value_str(key, value)}," for key, value in entries),
depth=depth,
open_tag=open_tag,
close_tag=close_tag,
open_tag=f"({open_paren or ''}",
close_tag=f"{close_paren or ''})",
)

@classmethod
Expand All @@ -349,7 +356,7 @@ def __serialize_lines(
) -> str:
lines = ends.join(lines)
lines_end = "\n" if lines else ""
maybe_obj_type = f"{cls.object_type(data)} " if include_type else ""
maybe_obj_type = f"{cls.object_type(data)}" if include_type else ""
formatted_open_tag = cls.with_indent(f"{maybe_obj_type}{open_tag}", depth)
formatted_close_tag = cls.with_indent(close_tag, depth)
return f"{formatted_open_tag}\n{lines}{lines_end}{formatted_close_tag}"
4 changes: 2 additions & 2 deletions src/syrupy/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def paths(path_string: str, *path_strings: str) -> "PropertyFilter":
Factory to create a filter using list of paths
"""

def path_filter(prop: "PropertyName", path: "PropertyPath") -> bool:
def path_filter(*, prop: "PropertyName", path: "PropertyPath") -> bool:
path_str = ".".join(str(p) for p, _ in (*path, (prop, None)))
return any(path_str == p for p in (path_string, *path_strings))

Expand All @@ -25,7 +25,7 @@ def props(prop_name: str, *prop_names: str) -> "PropertyFilter":
Factory to create filter using list of props
"""

def prop_filter(prop: "PropertyName", path: "PropertyPath") -> bool:
def prop_filter(*, prop: "PropertyName", path: "PropertyPath") -> bool:
return any(str(prop) == p for p in (prop_name, *prop_names))

return prop_filter
2 changes: 1 addition & 1 deletion src/syrupy/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def path_type(
raise PathTypeError(gettext("Both mapping and types argument cannot be empty"))

def path_type_matcher(
data: "SerializableData", path: "PropertyPath"
*, data: "SerializableData", path: "PropertyPath"
) -> Optional["SerializableData"]:
path_str = ".".join(str(p) for p, _ in path)
if mapping:
Expand Down
24 changes: 21 additions & 3 deletions src/syrupy/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import (
Any,
Callable,
Hashable,
Optional,
Tuple,
Expand All @@ -14,5 +13,24 @@
PropertyValueType = Type[SerializableData]
PropertyPathEntry = Tuple[PropertyName, PropertyValueType]
PropertyPath = Tuple[PropertyPathEntry, ...]
PropertyMatcher = Callable[..., Optional[SerializableData]]
PropertyFilter = Callable[..., bool]
try:
# Python minimum version 3.8
# https://docs.python.org/3/library/typing.html#typing.Protocol
from typing import Protocol

class PropertyMatcher(Protocol):
def __call__(
self,
*,
data: "SerializableData",
path: "PropertyPath",
) -> Optional["SerializableData"]:
...

class PropertyFilter(Protocol):
def __call__(self, *, prop: "PropertyName", path: "PropertyPath") -> bool:
...


except ImportError:
pass
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# name: test_case_1
'Syrupy is amazing!'
---
# ---
12 changes: 6 additions & 6 deletions tests/examples/__snapshots__/test_custom_object_repr.ambr
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# name: test_snapshot_custom_class
<class 'MyCustomClass'> {
MyCustomClass(
prop1=1,
prop2='a',
prop3=<class 'set'> {
prop3=set({
1,
2,
3,
},
}
---
}),
)
# ---
# name: test_snapshot_custom_repr_class
MyCustomReprClass(
prop1=1,
prop2='a',
prop3={1, 2, 3},
)
---
# ---
Loading

0 comments on commit 0a12e70

Please sign in to comment.