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 30 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
1 change: 1 addition & 0 deletions .prettierrc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tabWidth = 2
73 changes: 71 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __repr__(self) -> str:
return "MyCustomClass(...)"
```

### Options
### CLI Options

These are the cli options exposed to `pytest` by the plugin.

Expand All @@ -80,7 +80,74 @@ These are the cli options exposed to `pytest` by the plugin.
| `--snapshot-warn-unused` | Prints a warning on unused snapshots rather than fail the test suite. | `False` |
| `--snapshot-default-extension` | Use to change the default snapshot extension class. | `syrupy.extensions.amber.AmberSnapshotExtension` |

### Built-In Extensions
### Assertion Options

These are the options available on the `snapshot` assertion fixture.
Use of these options are one shot and do not persist across assertions.
For more persitent options see the [advanced usage](#advanced-usage).
iamogbz marked this conversation as resolved.
Show resolved Hide resolved

#### `matcher`

This allows you to match on a property path and value to control how specific object shapes are serialized.

The matcher is a function that takes two keyword arguments:
iamogbz marked this conversation as resolved.
Show resolved Hide resolved

| Argument | Description |
| -------- | ----------------------------------------------------------------------------- |
| `data` | Current serializable value being matched on |
| `path` | Ordered path traversed to the current value e.g. `(("a", dict), ("b", dict))` |

**NOTE:** Do not mutate the value received as it could cause unintended side effects.

##### Built-In Matchers

Syrupy comes with a few built-in matcher presets that can be used to make easy work of using property matchers.
iamogbz marked this conversation as resolved.
Show resolved Hide resolved

###### `path_type(mapping=None, *, types=(), strict=True)`

Easy way to build a matcher that uses the path and value type to replace serialized data.
When strict, this will raise a `ValueError` if the types specified are not matched.

| Argument | Description |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `mapping` | Dict of path string to tuples of class types |
| `types` | Tuple of class types used if none of the path strings from the mapping are matched |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it only work on class types? What about primitive types?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

| `strict` | If a path is matched but the value at the path does not match one of the class types in the tuple then a `PathTypeError` is raised |

```py
from syrupy.matchers import path_type

def test_bar(snapshot):
actual = {
"date_created": datetime.now(),
"value": "Some computed value!",
}
assert actual == snapshot(matcher=path_type({
"date_created": (datetime,),
"nested.path.id": (int,),
}))
```

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

#### `extension_class`

This is a way to modify how the snapshot matches and serializes your data in a single assertion.

```py
def test_foo(snapshot):
actual_svg = "<svg></svg>"
assert actual_svg = snapshot(extension_class=SVGImageSnapshotExtension)
```

##### Built-In Extensions

Syrupy comes with a few built-in preset configurations for you to choose from. You should also feel free to extend the `AbstractSyrupyExtension` if your project has a need not captured by one our built-ins.

Expand Down Expand Up @@ -141,7 +208,9 @@ To develop locally, clone this repository and run `. script/bootstrap` to instal

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This section is automatically generated via tagging the all-contributors bot in a PR:

```text
Expand Down
4 changes: 2 additions & 2 deletions src/syrupy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ def pytest_assertrepr_compare(op: str, left: Any, right: Any) -> Optional[List[s
assert_msg = reset(
f"{snapshot_style(left.name)} {op} {received_style('received')}"
)
return [assert_msg] + left.get_assert_diff(right)
return [assert_msg] + left.get_assert_diff()
elif isinstance(right, SnapshotAssertion):
assert_msg = reset(
f"{received_style('received')} {op} {snapshot_style(right.name)}"
)
return [assert_msg] + right.get_assert_diff(left)
return [assert_msg] + right.get_assert_diff()
return None


Expand Down
24 changes: 19 additions & 5 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 get_assert_diff(self, data: "SerializableData") -> List[str]:
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) -> 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 = assertion_result.asserted_data or ""
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
Loading