Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(amber): add property matcher support #245

Merged
merged 32 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6e8e9a8
wip: add matcher named argument
iamogbz May 31, 2020
188cdb2
fix: make arguments optional when using call syntax
iamogbz May 31, 2020
5acaa76
feat(amber): add property matcher support
iamogbz May 31, 2020
fd6aed1
test: property matcher
iamogbz May 31, 2020
2a87d54
test: property matcher
iamogbz May 31, 2020
494a415
test: property matcher
iamogbz May 31, 2020
023aaee
style: lint fix
iamogbz May 31, 2020
a25f1ff
Merge branch 'master' into property-matcher
iamogbz Jun 2, 2020
06ff637
Merge branch 'master' into property-matcher
iamogbz Jun 3, 2020
8601346
Merge branch 'master' into property-matcher
iamogbz Jun 3, 2020
2203ca5
Merge branch 'master' into property-matcher
iamogbz Jun 4, 2020
7d6db55
Merge branch 'master' into property-matcher
iamogbz Jun 4, 2020
c346450
docs: add documentation
iamogbz Jun 4, 2020
d1d2560
chore: update serialize kwargs
iamogbz Jun 4, 2020
1cdf09e
feat: add path_type matcher factory helper
iamogbz Jun 4, 2020
da50aff
docs: update path type signature
iamogbz Jun 4, 2020
9b4d72e
wip: include parent types in property path
iamogbz Jun 5, 2020
ca36eb7
Merge branch 'master' into property-matcher
iamogbz Jun 5, 2020
efde470
refactor: move types to explicit argument
iamogbz Jun 5, 2020
a710834
chore: more docs
iamogbz Jun 5, 2020
8a0e62b
cr: update doc wording
iamogbz Jun 6, 2020
6b060f9
refactor: split amber data serializer
iamogbz Jun 6, 2020
76ea77e
style: formatting
iamogbz Jun 7, 2020
6fa2405
refactor: reuse serialize logic
iamogbz Jun 7, 2020
9d835c1
chore: remove unused type def
iamogbz Jun 7, 2020
bca40d2
chore: use gettext for messages
iamogbz Jun 7, 2020
31bb9e4
refactor: reuse repr and kwargs
iamogbz Jun 7, 2020
cd82b38
refactor: repr init param
iamogbz Jun 7, 2020
3153fd2
refactor: serializer to order functions by use
iamogbz Jun 7, 2020
4438753
chore: add specific path type error
iamogbz Jun 7, 2020
03dd8c4
Merge branch 'master' into property-matcher
iamogbz Jun 9, 2020
8de6399
cr: update docs
Jun 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/syrupy/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .location import TestLocation
from .extensions.base import AbstractSyrupyExtension
from .session import SnapshotSession
from .types import SerializableData, SerializedData # noqa: F401
from .types import PropertyMatcher, SerializableData, SerializedData


@attr.s
Expand Down Expand Up @@ -45,6 +45,7 @@ class SnapshotAssertion:
_test_location: "TestLocation" = attr.ib(kw_only=True)
_update_snapshots: bool = attr.ib(kw_only=True)
_extension: Optional["AbstractSyrupyExtension"] = attr.ib(init=False, default=None)
_matcher: Optional["PropertyMatcher"] = attr.ib(init=False, default=None)
_executions: int = attr.ib(init=False, default=0)
_execution_results: Dict[int, "AssertionResult"] = attr.ib(init=False, factory=dict)
_post_assert_actions: List[Callable[..., None]] = attr.ib(init=False, factory=list)
Expand Down Expand Up @@ -88,10 +89,13 @@ def use_extension(
def assert_match(self, data: "SerializableData") -> None:
assert self == data

def _serialize(self, data: "SerializableData") -> "SerializedData":
return self.extension.serialize(data, matcher=self._matcher)
iamogbz marked this conversation as resolved.
Show resolved Hide resolved

def get_assert_diff(self, data: "SerializableData") -> List[str]:
assertion_result = self._execution_results[self.num_executions - 1]
snapshot_data = assertion_result.recalled_data
serialized_data = self.extension.serialize(data)
serialized_data = self._serialize(data)
diff: List[str] = []
if snapshot_data is None:
diff.append(gettext("Snapshot does not exist!"))
Expand All @@ -100,7 +104,10 @@ def get_assert_diff(self, data: "SerializableData") -> List[str]:
return diff

def __call__(
self, *, extension_class: Optional[Type["AbstractSyrupyExtension"]]
self,
*,
extension_class: Optional[Type["AbstractSyrupyExtension"]] = None,
matcher: Optional["PropertyMatcher"] = None,
) -> "SnapshotAssertion":
"""
Modifies assertion instance options
Expand All @@ -112,6 +119,13 @@ def clear_extension() -> None:
self._extension = None

self._post_assert_actions.append(clear_extension)
if matcher:
self._matcher = matcher

def clear_matcher() -> None:
self._matcher = None

self._post_assert_actions.append(clear_matcher)
return self

def __repr__(self) -> str:
Expand All @@ -131,7 +145,7 @@ def _assert(self, data: "SerializableData") -> bool:
assertion_success = False
try:
snapshot_data = self._recall_data(index=self.num_executions)
serialized_data = self.extension.serialize(data)
serialized_data = self._serialize(data)
matches = snapshot_data is not None and serialized_data == snapshot_data
assertion_success = matches
if not matches and self._update_snapshots:
Expand Down
2 changes: 1 addition & 1 deletion src/syrupy/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


if TYPE_CHECKING:
from .types import SerializedData # noqa: F401
from .types import SerializedData


@attr.s(frozen=True)
Expand Down
78 changes: 66 additions & 12 deletions src/syrupy/extensions/amber.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


if TYPE_CHECKING:
from syrupy.types import SerializableData
from syrupy.types import PropertyMatcher, PropertyPath, SerializableData


class DataSerializer:
Expand Down Expand Up @@ -98,6 +98,8 @@ def serialize_string(
data: "SerializableData",
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
if "\n" in data:
Expand All @@ -118,6 +120,8 @@ def serialize_number(
data: "SerializableData",
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
return cls.with_indent(repr(data), depth)
Expand All @@ -128,12 +132,21 @@ def serialize_set(
data: "SerializableData",
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
return (
cls.with_indent(f"{cls.object_type(data)} {{\n", depth)
+ "".join(
f"{cls.serialize(d, depth=depth + 1, visited=visited)},\n"
cls.serialize(
d,
depth=depth + 1,
matcher=matcher,
path=(*path, d),
visited=visited,
)
+ ",\n"
for d in cls.sort(data)
)
+ cls.with_indent("}", depth)
Expand All @@ -145,17 +158,21 @@ def serialize_dict(
data: "SerializableData",
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
kwargs = {"depth": depth + 1, "visited": visited}
kwargs = {"depth": depth + 1, "matcher": matcher, "visited": visited}
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.serialize(**{"data": key, **kwargs}),
iamogbz marked this conversation as resolved.
Show resolved Hide resolved
cls.serialize(**{"data": data[key], **kwargs}),
cls.serialize(
**{"data": data[key], "path": (*path, key), **kwargs}
),
)
for key in cls.sort(data.keys())
)
Expand All @@ -171,7 +188,13 @@ def __is_namedtuple(cls, obj: Any) -> bool:

@classmethod
def serialize_namedtuple(
cls, data: Any, *, depth: int = 0, visited: Optional[Set[Any]] = None
cls,
data: Any,
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
return (
cls.with_indent(f"{cls.object_type(data)} (\n", depth)
Expand All @@ -181,7 +204,11 @@ def serialize_namedtuple(
(
cls.with_indent(name, depth=depth + 1),
cls.serialize(
data=getattr(data, name), depth=depth + 1, visited=visited
data=getattr(data, name),
depth=depth + 1,
matcher=matcher,
path=(*path, name),
visited=visited,
),
)
for name in cls.sort(data._fields)
Expand All @@ -196,6 +223,8 @@ def serialize_iterable(
data: "SerializableData",
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
open_paren, close_paren = next(
Expand All @@ -206,14 +235,28 @@ def serialize_iterable(
return (
cls.with_indent(f"{cls.object_type(data)} {open_paren}\n", depth)
+ "".join(
f"{cls.serialize(d, depth=depth + 1, visited=visited)},\n" for d in data
cls.serialize(
d,
depth=depth + 1,
matcher=matcher,
path=(*path, i),
visited=visited,
)
+ ",\n"
for i, d in enumerate(data)
)
+ cls.with_indent(close_paren, depth)
)

@classmethod
def serialize_unknown(
cls, data: Any, *, depth: int = 0, visited: Optional[Set[Any]] = None
cls,
data: Any,
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
if data.__class__.__repr__ != object.__repr__:
return cls.with_indent(repr(data), depth)
Expand All @@ -226,7 +269,11 @@ def serialize_unknown(
(
cls.with_indent(name, depth=depth + 1),
cls.serialize(
data=getattr(data, name), depth=depth + 1, visited=visited
data=getattr(data, name),
depth=depth + 1,
matcher=matcher,
path=(*path, name),
visited=visited,
),
)
for name in cls.sort(dir(data))
Expand All @@ -242,16 +289,21 @@ def serialize(
data: "SerializableData",
*,
depth: int = 0,
matcher: Optional["PropertyMatcher"] = None,
path: "PropertyPath" = (),
visited: Optional[Set[Any]] = None,
) -> str:
visited = visited if visited is not None else set()
data_id = id(data)
if depth > cls._max_depth or data_id in visited:
data = cls.MarkerDepthMax()

if matcher:
data = matcher(data, path)
iamogbz marked this conversation as resolved.
Show resolved Hide resolved
serialize_kwargs = {
"data": data,
"depth": depth,
"matcher": matcher,
"path": path,
"visited": {*visited, data_id},
}
serialize_method = cls.serialize_unknown
Expand Down Expand Up @@ -283,12 +335,14 @@ class AmberSnapshotExtension(AbstractSyrupyExtension):
```
"""

def serialize(self, data: "SerializableData") -> str:
def serialize(
self, data: "SerializableData", *, matcher: Optional["PropertyMatcher"]
) -> str:
"""
Returns the serialized form of 'data' to be compared
with the snapshot data written to disk.
"""
return DataSerializer.serialize(data)
return DataSerializer.serialize(data, matcher=matcher)

def delete_snapshots(
self, snapshot_location: str, snapshot_names: Set[str]
Expand Down
8 changes: 5 additions & 3 deletions src/syrupy/extensions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@

if TYPE_CHECKING:
from syrupy.location import TestLocation
from syrupy.types import SerializableData, SerializedData
from syrupy.types import PropertyMatcher, SerializableData, SerializedData


class SnapshotSerializer(ABC):
@abstractmethod
def serialize(self, data: "SerializableData") -> "SerializedData":
def serialize(
self, data: "SerializableData", *, matcher: Optional["PropertyMatcher"]
) -> "SerializedData":
"""
Serializes a python object / data structure into a string
to be used for comparison with snapshot data from disk.
Expand Down Expand Up @@ -337,7 +339,7 @@ def __limit_context(self, lines: List[str]) -> Iterator[str]:
if num_lines:
if num_lines > self._context_line_max:
count_leading_whitespace: Callable[[str], int] = (
lambda s: len(s) - len(s.lstrip()) # noqa: E731
lambda s: len(s) - len(s.lstrip())
)
if self._context_line_count:
num_space = (
Expand Down
11 changes: 8 additions & 3 deletions src/syrupy/extensions/image.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from typing import TYPE_CHECKING
from typing import (
TYPE_CHECKING,
Optional,
)

from .single_file import SingleFileSnapshotExtension


if TYPE_CHECKING:
from syrupy.types import SerializableData
from syrupy.types import PropertyMatcher, SerializableData


class PNGImageSnapshotExtension(SingleFileSnapshotExtension):
Expand All @@ -18,5 +21,7 @@ class SVGImageSnapshotExtension(SingleFileSnapshotExtension):
def _file_extension(self) -> str:
return "svg"

def serialize(self, data: "SerializableData") -> bytes:
def serialize(
self, data: "SerializableData", *, matcher: Optional["PropertyMatcher"]
) -> bytes:
return str(data).encode("utf-8")
9 changes: 7 additions & 2 deletions src/syrupy/extensions/single_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@


if TYPE_CHECKING:
from syrupy.types import SerializableData, SerializedData # noqa: F401
from syrupy.types import (
PropertyMatcher,
SerializableData,
)


class SingleFileSnapshotExtension(AbstractSyrupyExtension):
def serialize(self, data: "SerializableData") -> bytes:
def serialize(
self, data: "SerializableData", *, matcher: Optional["PropertyMatcher"]
) -> bytes:
return bytes(data)

def get_snapshot_name(self, *, index: int = 0) -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/syrupy/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@


if TYPE_CHECKING:
from .assertion import SnapshotAssertion # noqa: F401
from .assertion import SnapshotAssertion


@attr.s
Expand Down
2 changes: 1 addition & 1 deletion src/syrupy/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

if TYPE_CHECKING:
from .assertion import SnapshotAssertion
from .extensions.base import AbstractSyrupyExtension # noqa: F401
from .extensions.base import AbstractSyrupyExtension


@attr.s
Expand Down
6 changes: 6 additions & 0 deletions src/syrupy/types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from typing import (
Any,
Callable,
Hashable,
Optional,
Tuple,
Union,
)


SerializableData = Any
SerializedData = Union[str, bytes]
PropertyPath = Tuple[Hashable, ...]
iamogbz marked this conversation as resolved.
Show resolved Hide resolved
PropertyMatcher = Callable[[SerializableData, PropertyPath], Optional[SerializableData]]
Loading