Skip to content

Commit

Permalink
feat(types): explicit property matcher and filter types kwargs (#515)
Browse files Browse the repository at this point in the history
* feat(types): make property matcher and filter types explicit about keyword arguments

* test: snapshot markers and assertions

* refactor(amber): explicit about serializer method accepted types
  • Loading branch information
iamogbz authored Jun 5, 2021
1 parent b48ed55 commit 8dddebf
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 35 deletions.
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
46 changes: 31 additions & 15 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_comment: str = "# "
_marker_divider: str = "---"
_marker_name: str = "# name:"
_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 @@ -182,7 +194,9 @@ def serialize_string(
)

@classmethod
def serialize_iterable(cls, data: "SerializableData", **kwargs: Any) -> str:
def serialize_iterable(
cls, data: Iterable["SerializableData"], **kwargs: Any
) -> str:
open_paren, close_paren = next(
parens
for iter_type, parens in {
Expand All @@ -195,14 +209,14 @@ def serialize_iterable(cls, data: "SerializableData", **kwargs: Any) -> str:
values = list(data)
return cls.__serialize_iterable(
data=data,
resolve_entries=(range(len(values)), lambda _, p: values[p], None),
resolve_entries=(range(len(values)), item_getter, None),
open_tag=open_paren,
close_tag=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),
Expand All @@ -212,21 +226,23 @@ def serialize_set(cls, data: "SerializableData", **kwargs: Any) -> str:
)

@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),
resolve_entries=(cls.sort(data._fields), attr_getter, None),
open_tag="(",
close_tag=")",
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),
resolve_entries=(cls.sort(data.keys()), item_getter, None),
open_tag="{",
close_tag="}",
separator=": ",
Expand All @@ -243,7 +259,7 @@ def serialize_unknown(cls, data: Any, *, depth: int = 0, **kwargs: Any) -> str:
data=data,
resolve_entries=(
(name for name in cls.sort(dir(data)) if not name.startswith("_")),
getattr,
attr_getter,
lambda v: not callable(v),
),
depth=depth,
Expand Down
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
Expand Up @@ -136,6 +136,11 @@
---
# name: test_dict[actual2]
<class 'dict'> {
'
multi
line
key
': 'Some morre text.',
'a': 'Some ttext.',
1: True,
<class 'ExampleTuple'> (
Expand Down Expand Up @@ -309,7 +314,10 @@
'value.with.dot'
---
# name: test_reflection
SnapshotAssertion(name='snapshot', num_executions=0)
<class 'SnapshotAssertion'> {
name='snapshot',
num_executions=0,
}
---
# name: test_set[actual0]
<class 'set'> {
Expand Down Expand Up @@ -355,6 +363,14 @@
<class 'set'> {
}
---
# name: test_snapshot_markers
'
#
#
---
# name:
'
---
# name: test_string[0]
''
---
Expand Down
16 changes: 16 additions & 0 deletions tests/syrupy/extensions/amber/test_amber_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import pytest

from syrupy.extensions.amber.serializer import DataSerializer


def test_non_snapshots(snapshot):
with pytest.raises(AssertionError):
Expand All @@ -17,6 +19,19 @@ def test_empty_snapshot(snapshot):
assert snapshot == ""


def test_snapshot_markers(snapshot):
"""
Test snapshot markers do not break serialization when in snapshot data
"""
marker_strings = (
DataSerializer._marker_comment,
f"{DataSerializer._indent}{DataSerializer._marker_comment}",
DataSerializer._marker_divider,
DataSerializer._marker_name,
)
assert snapshot == "\n".join(marker_strings)


def test_newline_control_characters(snapshot):
assert snapshot == "line 1\nline 2"
assert snapshot == "line 1\r\nline 2"
Expand Down Expand Up @@ -100,6 +115,7 @@ def test_set(snapshot, actual):
{
1: True,
"a": "Some ttext.",
"multi\nline\nkey": "Some morre text.",
frozenset({"1", "2"}): ["1", 2],
ExampleTuple(a=1, b=2, c=3, d=4): {"e": False},
},
Expand Down

0 comments on commit 8dddebf

Please sign in to comment.