From bb77eb53b4ff5f97cda3bac4b9b0e73af90515f5 Mon Sep 17 00:00:00 2001 From: noahnu Date: Mon, 2 Aug 2021 14:39:34 -0400 Subject: [PATCH] feat: support regex path type matching --- README.md | 3 ++- src/syrupy/matchers.py | 15 +++++++++---- .../__snapshots__/test_amber_matchers.ambr | 19 ++++++++++++++++ .../extensions/amber/test_amber_matchers.py | 22 +++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8978f657..13d18365 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ It should return the replacement value to be serialized or the original unmutate Syrupy comes with built-in helpers that can be used to make easy work of using property matchers. -###### `path_type(mapping=None, *, types=(), strict=True)` +###### `path_type(mapping=None, *, types=(), strict=True, regex=False)` 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. @@ -146,6 +146,7 @@ When strict, this will raise a `ValueError` if the types specified are not match | `mapping` | Dict of path string to tuples of class types, including primitives e.g. (MyClass, UUID, datetime, int, str) | | `types` | Tuple of class types used if none of the path strings from the mapping are matched | | `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 | +| `regex` | If true, the `mapping` key is treated as a regular expression when matching paths | ```py from syrupy.matchers import path_type diff --git a/src/syrupy/matchers.py b/src/syrupy/matchers.py index c4aa95fe..42689323 100644 --- a/src/syrupy/matchers.py +++ b/src/syrupy/matchers.py @@ -1,3 +1,4 @@ +import re from gettext import gettext from typing import ( TYPE_CHECKING, @@ -29,6 +30,7 @@ def path_type( *, types: Tuple["PropertyValueType", ...] = (), strict: bool = True, + regex: bool = False, ) -> "PropertyMatcher": """ Factory to create a matcher using path and type mapping @@ -36,14 +38,19 @@ def path_type( if not mapping and not types: raise PathTypeError(gettext("Both mapping and types argument cannot be empty")) + def _path_match(path: str, pattern: str) -> bool: + if regex: + return re.fullmatch(pattern, path) is not None + return path == pattern + def path_type_matcher( *, data: "SerializableData", path: "PropertyPath" ) -> Optional["SerializableData"]: path_str = ".".join(str(p) for p, _ in path) if mapping: - for path_to_match in mapping: - if path_to_match == path_str: - for type_to_match in mapping[path_to_match]: + for pattern in mapping: + if _path_match(path_str, pattern): + for type_to_match in mapping[pattern]: if isinstance(data, type_to_match): return Repr(DataSerializer.object_type(data)) if strict: @@ -51,7 +58,7 @@ def path_type_matcher( gettext( "{} at '{}' of type {} does not " "match any of the expected types: {}" - ).format(data, path_str, data.__class__, types) + ).format(data, path_str, data.__class__, mapping[pattern]) ) for type_to_match in types: if isinstance(data, type_to_match): diff --git a/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr b/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr index a1683cb7..912071bb 100644 --- a/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr +++ b/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr @@ -33,6 +33,25 @@ ], } --- +# name: test_matches_regex_in_regex_mode + { + 'any_number': , + 'any_number_adjacent': 'hi', + 'data': { + 'list': [ + { + 'date_created': , + 'k': '1', + }, + { + 'date_created': , + 'k': '2', + }, + ], + }, + 'specific_number': 5, + } +--- # name: test_raises_unexpected_type { 'date_created': , diff --git a/tests/syrupy/extensions/amber/test_amber_matchers.py b/tests/syrupy/extensions/amber/test_amber_matchers.py index 303bcc99..08335a46 100644 --- a/tests/syrupy/extensions/amber/test_amber_matchers.py +++ b/tests/syrupy/extensions/amber/test_amber_matchers.py @@ -70,3 +70,25 @@ def matcher(data, path): }, "c": ["Replace this one", "Do not replace this one"], } == snapshot + + +def test_matches_regex_in_regex_mode(snapshot): + my_matcher = path_type( + { + r"data\.list\..*\.date_created": (datetime.datetime,), + r"any_number": (int,), + }, + regex=True, + ) + actual = { + "data": { + "list": [ + {"k": "1", "date_created": datetime.datetime.now()}, + {"k": "2", "date_created": datetime.datetime.now()}, + ], + }, + "any_number": 3, + "any_number_adjacent": "hi", + "specific_number": 5, + } + assert actual == snapshot(matcher=my_matcher)