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(types): explicit property matcher and filter types kwargs #515

Merged
merged 3 commits into from
Jun 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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