diff --git a/README.md b/README.md index 1f6757ca..3501e6e8 100644 --- a/README.md +++ b/README.md @@ -164,13 +164,13 @@ def test_bar(snapshot): })) ``` -```ambr +```py # name: test_bar - { - 'date_created': , + dict({ + 'date_created': datetime, 'value': 'Some computed value!!', - } ---- + }) +# --- ``` #### `exclude` @@ -206,15 +206,15 @@ def test_bar(snapshot): assert actual == snapshot(exclude=props("id", "1")) ``` -```ambr +```py # name: test_bar - { - 'list': [ + dict({ + 'list': list([ 1, 3, - ], - } ---- + ]), + }) +# --- ``` ###### `paths(path_string, *path_strings)` @@ -234,15 +234,15 @@ def test_bar(snapshot): assert actual == snapshot(exclude=paths("date", "list.1")) ``` -```ambr +```py # name: test_bar - { - 'list': [ + dict({ + 'list': list([ 1, 3, - ], - } ---- + ]), + }) +# --- ``` #### `extension_class` diff --git a/poetry.lock b/poetry.lock index f97971e7..ab2b3cfd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -88,7 +88,7 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.3" +version = "8.0.4" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -171,6 +171,14 @@ sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +[[package]] +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" +optional = false +python-versions = ">=3.6, <3.7" + [[package]] name = "deprecated" version = "1.2.13" @@ -410,7 +418,7 @@ testing = ["coverage", "nose"] [[package]] name = "platformdirs" -version = "2.5.0" +version = "2.5.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -806,7 +814,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = '>=3.6,<4' -content-hash = "9acdc9c9f96f9526accaf3775d39f107a4a9ad69e47820f51502c156bb685aa3" +content-hash = "402b369ef2796217a1a19accbc61fa1713e5a705c4fc07357f9697677410b8ba" [metadata.files] atomicwrites = [ @@ -907,8 +915,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, ] codecov = [ {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, @@ -1002,6 +1010,10 @@ cryptography = [ {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, ] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, +] deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, @@ -1101,8 +1113,8 @@ pkginfo = [ {file = "pkginfo-1.8.2.tar.gz", hash = "sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff"}, ] platformdirs = [ - {file = "platformdirs-2.5.0-py3-none-any.whl", hash = "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb"}, - {file = "platformdirs-2.5.0.tar.gz", hash = "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b"}, + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, diff --git a/pyproject.toml b/pyproject.toml index 4371799c..f659c21c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,9 @@ syrupy = 'syrupy' [tool.poetry.dependencies] python = '>=3.6,<4' -attrs = '>=18.2.0,<22.0.0' colored = '>=1.3.92,<2.0.0' pytest = '>=5.1.0,<8.0.0' +dataclasses = { version = '^0.8.0', python = '>=3.6,<3.7' } [tool.poetry.group.test.dependencies] codecov = '^2.1.12' diff --git a/src/syrupy/assertion.py b/src/syrupy/assertion.py index e5428e7a..b66cec75 100644 --- a/src/syrupy/assertion.py +++ b/src/syrupy/assertion.py @@ -1,4 +1,8 @@ import traceback +from dataclasses import ( + dataclass, + field, +) from gettext import gettext from typing import ( TYPE_CHECKING, @@ -11,8 +15,6 @@ Union, ) -import attr - from .exceptions import SnapshotDoesNotExist if TYPE_CHECKING: @@ -27,16 +29,16 @@ ) -@attr.s +@dataclass class AssertionResult: - snapshot_location: str = attr.ib() - snapshot_name: str = attr.ib() - asserted_data: Optional["SerializedData"] = attr.ib() - recalled_data: Optional["SerializedData"] = attr.ib() - created: bool = attr.ib() - updated: bool = attr.ib() - success: bool = attr.ib() - exception: Optional[Exception] = attr.ib() + snapshot_location: str + snapshot_name: str + asserted_data: Optional["SerializedData"] + recalled_data: Optional["SerializedData"] + created: bool + updated: bool + success: bool + exception: Optional[Exception] @property def final_data(self) -> Optional["SerializedData"]: @@ -45,43 +47,56 @@ def final_data(self) -> Optional["SerializedData"]: return self.recalled_data -@attr.s(cmp=False, repr=False) +@dataclass(eq=False, order=False, repr=False) class SnapshotAssertion: - name: str = attr.ib(default="snapshot") - _session: "SnapshotSession" = attr.ib(kw_only=True) - _extension_class: Type["AbstractSyrupyExtension"] = attr.ib(kw_only=True) - _test_location: "PyTestLocation" = attr.ib(kw_only=True) - _update_snapshots: bool = attr.ib(kw_only=True) - _exclude: Optional["PropertyFilter"] = attr.ib( - init=False, default=None, kw_only=True + session: "SnapshotSession" + extension_class: Type["AbstractSyrupyExtension"] + test_location: "PyTestLocation" + update_snapshots: bool + + name: str = "snapshot" + + _exclude: Optional["PropertyFilter"] = field( + init=False, + default=None, + ) + _custom_index: Optional[str] = field( + init=False, + default=None, + ) + _extension: Optional["AbstractSyrupyExtension"] = field( + init=False, + default=None, ) - _custom_index: Optional[str] = attr.ib(init=False, default=None, kw_only=True) - _extension: Optional["AbstractSyrupyExtension"] = attr.ib( - init=False, default=None, kw_only=True + _executions: int = field( + init=False, + default=0, ) - _executions: int = attr.ib(init=False, default=0, kw_only=True) - _execution_results: Dict[int, "AssertionResult"] = attr.ib( - init=False, factory=dict, kw_only=True + _execution_results: Dict[int, "AssertionResult"] = field( + init=False, + default_factory=dict, ) - _matcher: Optional["PropertyMatcher"] = attr.ib( - init=False, default=None, kw_only=True + _matcher: Optional["PropertyMatcher"] = field( + init=False, + default=None, ) - _post_assert_actions: List[Callable[..., None]] = attr.ib( - init=False, factory=list, kw_only=True + _post_assert_actions: List[Callable[..., None]] = field( + init=False, + default_factory=list, ) - def __attrs_post_init__(self) -> None: - self._session.register_request(self) + def __post_init__(self) -> None: + self.session.register_request(self) def __init_extension( self, extension_class: Type["AbstractSyrupyExtension"] ) -> "AbstractSyrupyExtension": - return extension_class(test_location=self._test_location) + return extension_class(test_location=self.test_location) @property def extension(self) -> "AbstractSyrupyExtension": if not self._extension: - self._extension = self.__init_extension(self._extension_class) + self._extension = self.__init_extension(self.extension_class) return self._extension @property @@ -106,10 +121,10 @@ def use_extension( specified extension class. This does not preserve assertion index or state. """ return self.__class__( - update_snapshots=self._update_snapshots, - test_location=self._test_location, - extension_class=extension_class or self._extension_class, - session=self._session, + update_snapshots=self.update_snapshots, + test_location=self.test_location, + extension_class=extension_class or self.extension_class, + session=self.session, ) def assert_match(self, data: "SerializableData") -> None: @@ -193,7 +208,7 @@ def _assert(self, data: "SerializableData") -> bool: serialized_data=serialized_data, snapshot_data=snapshot_data ) assertion_success = matches - if not matches and self._update_snapshots: + if not matches and self.update_snapshots: self.extension.write_snapshot( data=serialized_data, index=self.index, diff --git a/src/syrupy/data.py b/src/syrupy/data.py index 254285b8..18fb41b8 100644 --- a/src/syrupy/data.py +++ b/src/syrupy/data.py @@ -1,4 +1,7 @@ -from types import MappingProxyType +from dataclasses import ( + dataclass, + field, +) from typing import ( TYPE_CHECKING, Dict, @@ -7,8 +10,6 @@ Optional, ) -import attr - from .constants import ( SNAPSHOT_EMPTY_FOSSIL_KEY, SNAPSHOT_UNKNOWN_FOSSIL_KEY, @@ -18,28 +19,28 @@ from .types import SerializedData -@attr.s(frozen=True) +@dataclass(frozen=True) class Snapshot: - name: str = attr.ib() - data: Optional["SerializedData"] = attr.ib(default=None) + name: str + data: Optional["SerializedData"] = None -@attr.s(frozen=True) +@dataclass(frozen=True) class SnapshotEmpty(Snapshot): - name: str = attr.ib(default=SNAPSHOT_EMPTY_FOSSIL_KEY, init=False) + name: str = SNAPSHOT_EMPTY_FOSSIL_KEY -@attr.s(frozen=True) +@dataclass(frozen=True) class SnapshotUnknown(Snapshot): - name: str = attr.ib(default=SNAPSHOT_UNKNOWN_FOSSIL_KEY, init=False) + name: str = SNAPSHOT_UNKNOWN_FOSSIL_KEY -@attr.s +@dataclass class SnapshotFossil: """A collection of snapshots at a save location""" - location: str = attr.ib() - _snapshots: Dict[str, "Snapshot"] = attr.ib(factory=dict) + location: str + _snapshots: Dict[str, "Snapshot"] = field(default_factory=dict) @property def has_snapshots(self) -> bool: @@ -67,31 +68,31 @@ def __iter__(self) -> Iterator["Snapshot"]: return iter(self._snapshots.values()) -SNAPSHOTS_EMPTY = MappingProxyType({SnapshotEmpty().name: SnapshotEmpty()}) -SNAPSHOTS_UNKNOWN = MappingProxyType({SnapshotUnknown().name: SnapshotUnknown()}) - - -@attr.s(frozen=True) +@dataclass class SnapshotEmptyFossil(SnapshotFossil): """This is a saved fossil that is known to be empty and thus can be removed""" - _snapshots: Dict[str, "Snapshot"] = attr.ib(default=SNAPSHOTS_EMPTY, init=False) + _snapshots: Dict[str, "Snapshot"] = field( + default_factory=lambda: {SnapshotEmpty().name: SnapshotEmpty()} + ) @property def has_snapshots(self) -> bool: return False -@attr.s(frozen=True) +@dataclass class SnapshotUnknownFossil(SnapshotFossil): """This is a saved fossil that is unclaimed by any extension currently in use""" - _snapshots: Dict[str, "Snapshot"] = attr.ib(default=SNAPSHOTS_UNKNOWN, init=False) + _snapshots: Dict[str, "Snapshot"] = field( + default_factory=lambda: {SnapshotUnknown().name: SnapshotUnknown()} + ) -@attr.s +@dataclass class SnapshotFossils: - _snapshot_fossils: Dict[str, "SnapshotFossil"] = attr.ib(factory=dict) + _snapshot_fossils: Dict[str, "SnapshotFossil"] = field(default_factory=dict) def get(self, location: str) -> Optional["SnapshotFossil"]: return self._snapshot_fossils.get(location) @@ -119,13 +120,13 @@ def __contains__(self, key: str) -> bool: return key in self._snapshot_fossils -@attr.s +@dataclass class DiffedLine: - a: str = attr.ib(default=None) - b: str = attr.ib(default=None) - c: List[str] = attr.ib(factory=list) - diff_a: str = attr.ib(default="") - diff_b: str = attr.ib(default="") + a: Optional[str] = None + b: Optional[str] = None + c: List[str] = field(default_factory=list) + diff_a: str = "" + diff_b: str = "" @property def has_snapshot(self) -> bool: diff --git a/src/syrupy/extensions/amber/serializer.py b/src/syrupy/extensions/amber/serializer.py index c276b69d..b9ef4879 100644 --- a/src/syrupy/extensions/amber/serializer.py +++ b/src/syrupy/extensions/amber/serializer.py @@ -60,7 +60,7 @@ class DataSerializer: _indent: str = " " _max_depth: int = 99 _marker_comment: str = "# " - _marker_divider: str = "---" + _marker_divider: str = f"{_marker_comment}---" _marker_name: str = f"{_marker_comment}name:" _marker_crn: str = "\r\n" @@ -189,8 +189,8 @@ def serialize_string(cls, data: str, *, depth: int = 0, **kwargs: Any) -> str: for line in str(data).splitlines(keepends=True) ), depth=depth, - open_tag="'", - close_tag="'", + open_tag="'''", + close_tag="'''", include_type=False, ends="", ) @@ -199,21 +199,16 @@ def serialize_string(cls, data: str, *, depth: int = 0, **kwargs: Any) -> str: def serialize_iterable( cls, data: Iterable["SerializableData"], **kwargs: Any ) -> str: - open_paren, close_paren = next( - parens - for iter_type, parens in { - GeneratorType: ("(", ")"), - list: ("[", "]"), - tuple: ("(", ")"), - }.items() - if isinstance(data, iter_type) - ) + open_paren, close_paren = (None, None) + if isinstance(data, list): + open_paren, close_paren = ("[", "]") + values = list(data) return cls.__serialize_iterable( data=data, resolve_entries=(range(len(values)), item_getter, None), - open_tag=open_paren, - close_tag=close_paren, + open_paren=open_paren, + close_paren=close_paren, **kwargs, ) @@ -222,8 +217,8 @@ 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), - open_tag="{", - close_tag="}", + open_paren="{", + close_paren="}", **kwargs, ) @@ -232,8 +227,6 @@ def serialize_namedtuple(cls, data: NamedTuple, **kwargs: Any) -> str: return cls.__serialize_iterable( data=data, resolve_entries=(cls.sort(data._fields), attr_getter, None), - open_tag="(", - close_tag=")", separator="=", **kwargs, ) @@ -245,8 +238,8 @@ def serialize_dict( return cls.__serialize_iterable( data=data, resolve_entries=(cls.sort(data.keys()), item_getter, None), - open_tag="{", - close_tag="}", + open_paren="{", + close_paren="}", separator=": ", serialize_key=True, **kwargs, @@ -265,8 +258,6 @@ def serialize_unknown(cls, data: Any, *, depth: int = 0, **kwargs: Any) -> str: lambda v: not callable(v), ), depth=depth, - open_tag="{", - close_tag="}", separator="=", **kwargs, ) @@ -284,7 +275,7 @@ def sort(cls, iterable: Iterable[Any]) -> Iterable[Any]: @classmethod def object_type(cls, data: "SerializableData") -> str: - return f"" + return f"{data.__class__.__name__}" @classmethod def __is_namedtuple(cls, obj: Any) -> bool: @@ -307,8 +298,8 @@ def __serialize_iterable( *, data: "SerializableData", resolve_entries: "IterableEntries", - open_tag: str, - close_tag: str, + open_paren: Optional[str] = None, + close_paren: Optional[str] = None, depth: int = 0, exclude: Optional["PropertyFilter"] = None, path: "PropertyPath" = (), @@ -349,8 +340,8 @@ def value_str(key: "PropertyName", value: "SerializableData") -> str: data=data, lines=(f"{key_str(key)}{value_str(key, value)}," for key, value in entries), depth=depth, - open_tag=open_tag, - close_tag=close_tag, + open_tag=f"({open_paren or ''}", + close_tag=f"{close_paren or ''})", ) @classmethod @@ -367,7 +358,7 @@ def __serialize_lines( ) -> str: lines = ends.join(lines) lines_end = "\n" if lines else "" - maybe_obj_type = f"{cls.object_type(data)} " if include_type else "" + maybe_obj_type = f"{cls.object_type(data)}" if include_type else "" formatted_open_tag = cls.with_indent(f"{maybe_obj_type}{open_tag}", depth) formatted_close_tag = cls.with_indent(close_tag, depth) return f"{formatted_open_tag}\n{lines}{lines_end}{formatted_close_tag}" diff --git a/src/syrupy/extensions/base.py b/src/syrupy/extensions/base.py index 7a8930e0..419f1c2d 100644 --- a/src/syrupy/extensions/base.py +++ b/src/syrupy/extensions/base.py @@ -273,15 +273,16 @@ def _marker_carriage(self) -> str: def __diff_lines(self, a: str, b: str) -> Iterator[str]: for line in self.__diffed_lines(a, b): show_ends = ( - self.__strip_ends(line.a[1:]) == self.__strip_ends(line.b[1:]) + self.__strip_ends(line.a[1:] if line.a is not None else "") + == self.__strip_ends(line.b[1:] if line.b is not None else "") if line.is_complete else False ) - if line.has_snapshot: + if line.has_snapshot and line.a is not None: yield self.__format_line( line.a, line.diff_a, snapshot_style, snapshot_diff_style, show_ends ) - if line.has_received: + if line.has_received and line.b is not None: yield self.__format_line( line.b, line.diff_b, received_style, received_diff_style, show_ends ) diff --git a/src/syrupy/location.py b/src/syrupy/location.py index 2c0e1180..91b920f2 100644 --- a/src/syrupy/location.py +++ b/src/syrupy/location.py @@ -1,25 +1,28 @@ +from dataclasses import ( + dataclass, + field, +) from pathlib import Path from typing import ( Iterator, Optional, ) -import attr import pytest from syrupy.constants import PYTEST_NODE_SEP -@attr.s +@dataclass class PyTestLocation: - _node: "pytest.Item" = attr.ib() - nodename: Optional[str] = attr.ib(init=False) - testname: str = attr.ib(init=False) - methodname: str = attr.ib(init=False) - modulename: str = attr.ib(init=False) - filepath: str = attr.ib(init=False) + _node: "pytest.Item" + nodename: Optional[str] = field(init=False) + testname: str = field(init=False) + methodname: str = field(init=False) + modulename: str = field(init=False) + filepath: str = field(init=False) - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: self.filepath = getattr(self._node, "fspath") # noqa: B009 obj = getattr(self._node, "obj") # noqa: B009 self.modulename = obj.__module__ diff --git a/src/syrupy/report.py b/src/syrupy/report.py index 639014ef..e6424507 100644 --- a/src/syrupy/report.py +++ b/src/syrupy/report.py @@ -1,5 +1,9 @@ import importlib from collections import defaultdict +from dataclasses import ( + dataclass, + field, +) from gettext import ( gettext, ngettext, @@ -17,8 +21,6 @@ Set, ) -import attr - from .constants import PYTEST_NODE_SEP from .data import ( Snapshot, @@ -43,7 +45,7 @@ from .assertion import SnapshotAssertion -@attr.s +@dataclass class SnapshotReport: """ This class is responsible for determining the test summary and post execution @@ -51,21 +53,21 @@ class SnapshotReport: information used for removal of unused or orphaned snapshots and fossils. """ - base_dir: str = attr.ib() - collected_items: Set["pytest.Item"] = attr.ib() - selected_items: Dict[str, bool] = attr.ib() - options: "argparse.Namespace" = attr.ib() - assertions: List["SnapshotAssertion"] = attr.ib() - discovered: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) - created: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) - failed: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) - matched: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) - updated: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) - used: "SnapshotFossils" = attr.ib(factory=SnapshotFossils) - _provided_test_paths: Dict[str, List[str]] = attr.ib(factory=dict) - _keyword_expressions: Set["Expression"] = attr.ib(factory=set) - _collected_items_by_nodeid: Dict[str, "pytest.Item"] = attr.ib( - factory=dict, init=False + base_dir: str + collected_items: Set["pytest.Item"] + selected_items: Dict[str, bool] + options: "argparse.Namespace" + assertions: List["SnapshotAssertion"] + discovered: "SnapshotFossils" = field(default_factory=SnapshotFossils) + created: "SnapshotFossils" = field(default_factory=SnapshotFossils) + failed: "SnapshotFossils" = field(default_factory=SnapshotFossils) + matched: "SnapshotFossils" = field(default_factory=SnapshotFossils) + updated: "SnapshotFossils" = field(default_factory=SnapshotFossils) + used: "SnapshotFossils" = field(default_factory=SnapshotFossils) + _provided_test_paths: Dict[str, List[str]] = field(default_factory=dict) + _keyword_expressions: Set["Expression"] = field(default_factory=set) + _collected_items_by_nodeid: Dict[str, "pytest.Item"] = field( + default_factory=dict, init=False ) @property @@ -80,7 +82,7 @@ def warn_unused_snapshots(self) -> bool: def include_snapshot_details(self) -> bool: return bool(self.options.include_snapshot_details) - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: self.__parse_invocation_args() self._collected_items_by_nodeid = { getattr(item, "nodeid"): item for item in self.collected_items # noqa: B009 @@ -425,7 +427,7 @@ def _ran_items_match_location(self, snapshot_location: str) -> bool: ) -@attr.s(frozen=True) +@dataclass(frozen=True) class Expression: """ Dumbed down version of _pytest.mark.expression.Expression not available in < 6.0 @@ -434,7 +436,7 @@ class Expression: module is not public. This only supports inclusion based on simple string matching. """ - code: FrozenSet[str] = attr.ib(factory=frozenset) + code: FrozenSet[str] = field(default_factory=frozenset) def evaluate(self, matcher: Callable[[str], bool]) -> bool: return any(map(matcher, self.code)) diff --git a/src/syrupy/session.py b/src/syrupy/session.py index a95e3d0d..014d5e72 100644 --- a/src/syrupy/session.py +++ b/src/syrupy/session.py @@ -1,4 +1,8 @@ from collections import defaultdict +from dataclasses import ( + dataclass, + field, +) from pathlib import Path from typing import ( TYPE_CHECKING, @@ -11,7 +15,6 @@ Set, ) -import attr import pytest from .constants import EXIT_STATUS_FAIL_UNUSED @@ -23,30 +26,30 @@ from .extensions.base import AbstractSyrupyExtension -@attr.s +@dataclass class SnapshotSession: # pytest.Session - _pytest_session: Any = attr.ib() + pytest_session: Any # Snapshot report generated on finish - report: Optional["SnapshotReport"] = attr.ib(default=None) + report: Optional["SnapshotReport"] = None # All the collected test items - _collected_items: Set["pytest.Item"] = attr.ib(factory=set) + _collected_items: Set["pytest.Item"] = field(default_factory=set) # All the selected test items. Will be set to False until the test item is run. - _selected_items: Dict[str, bool] = attr.ib(factory=dict) - _assertions: List["SnapshotAssertion"] = attr.ib(factory=list) - _extensions: Dict[str, "AbstractSyrupyExtension"] = attr.ib(factory=dict) + _selected_items: Dict[str, bool] = field(default_factory=dict) + _assertions: List["SnapshotAssertion"] = field(default_factory=list) + _extensions: Dict[str, "AbstractSyrupyExtension"] = field(default_factory=dict) - _locations_discovered: DefaultDict[str, Set[Any]] = attr.ib( - factory=lambda: defaultdict(set) + _locations_discovered: DefaultDict[str, Set[Any]] = field( + default_factory=lambda: defaultdict(set) ) @property def update_snapshots(self) -> bool: - return bool(self._pytest_session.config.option.update_snapshots) + return bool(self.pytest_session.config.option.update_snapshots) @property def warn_unused_snapshots(self) -> bool: - return bool(self._pytest_session.config.option.warn_unused_snapshots) + return bool(self.pytest_session.config.option.warn_unused_snapshots) def collect_items(self, items: List["pytest.Item"]) -> None: self._collected_items.update(self.filter_valid_items(items)) @@ -70,11 +73,11 @@ def ran_item(self, nodeid: str) -> None: def finish(self) -> int: exitstatus = 0 self.report = SnapshotReport( - base_dir=self._pytest_session.config.rootdir, + base_dir=self.pytest_session.config.rootdir, collected_items=self._collected_items, selected_items=self._selected_items, assertions=self._assertions, - options=self._pytest_session.config.option, + options=self.pytest_session.config.option, ) if self.report.num_unused: if self.update_snapshots: diff --git a/tests/examples/__snaps_example__/test_custom_snapshot_directory.ambr b/tests/examples/__snaps_example__/test_custom_snapshot_directory.ambr index 9c3dc6da..03976d12 100644 --- a/tests/examples/__snaps_example__/test_custom_snapshot_directory.ambr +++ b/tests/examples/__snaps_example__/test_custom_snapshot_directory.ambr @@ -1,3 +1,3 @@ # name: test_case_1 'Syrupy is amazing!' ---- +# --- diff --git a/tests/examples/__snapshots__/test_custom_object_repr.ambr b/tests/examples/__snapshots__/test_custom_object_repr.ambr index 573f98cf..2cb487e7 100644 --- a/tests/examples/__snapshots__/test_custom_object_repr.ambr +++ b/tests/examples/__snapshots__/test_custom_object_repr.ambr @@ -1,18 +1,18 @@ # name: test_snapshot_custom_class - { + MyCustomClass( prop1=1, prop2='a', - prop3= { + prop3=set({ 1, 2, 3, - }, - } ---- + }), + ) +# --- # name: test_snapshot_custom_repr_class MyCustomReprClass( prop1=1, prop2='a', prop3={1, 2, 3}, ) ---- +# --- diff --git a/tests/examples/__snapshots__/test_custom_snapshot_name.ambr b/tests/examples/__snapshots__/test_custom_snapshot_name.ambr index a6b95093..24dd5540 100644 --- a/tests/examples/__snapshots__/test_custom_snapshot_name.ambr +++ b/tests/examples/__snapshots__/test_custom_snapshot_name.ambr @@ -1,3 +1,3 @@ # name: test_canadian_nameπŸ‡¨πŸ‡¦ 'Name should be test_canadian_nameπŸ‡¨πŸ‡¦.' ---- +# --- diff --git a/tests/examples/__snapshots__/test_custom_snapshot_name_suffix.ambr b/tests/examples/__snapshots__/test_custom_snapshot_name_suffix.ambr index 1364ee10..26c024eb 100644 --- a/tests/examples/__snapshots__/test_custom_snapshot_name_suffix.ambr +++ b/tests/examples/__snapshots__/test_custom_snapshot_name_suffix.ambr @@ -1,6 +1,6 @@ # name: test_snapshot_custom_snapshot_name_suffix[test_is_amazing] 'Syrupy is amazing!' ---- +# --- # name: test_snapshot_custom_snapshot_name_suffix[test_is_awesome] 'Syrupy is awesome!' ---- +# --- diff --git a/tests/integration/test_snapshot_option_update.py b/tests/integration/test_snapshot_option_update.py index 2ce6fe71..12f4533d 100644 --- a/tests/integration/test_snapshot_option_update.py +++ b/tests/integration/test_snapshot_option_update.py @@ -128,7 +128,7 @@ def test_update_failure_shows_snapshot_diff(run_testcases, testcases_updated): ( r".*assert snapshot == \['this', 'will', 'not', 'match'\]", r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", - r".* \[", + r".* list\(\[", r".* ...", r".* 'will',", r".* - 'be',", @@ -138,7 +138,7 @@ def test_update_failure_shows_snapshot_diff(run_testcases, testcases_updated): r".* \]", r".*assert \['this', 'will', 'fail'\] == snapshot", r".*AssertionError: assert \[\+ received\] == \[- snapshot\]", - r".* \[", + r".* list\(\[", r".* ...", r".* 'will',", r".* - 'be',", @@ -147,7 +147,7 @@ def test_update_failure_shows_snapshot_diff(run_testcases, testcases_updated): r".* \]", r".*assert snapshot == \['this', 'will', 'be', 'too', 'much'\]", r".*AssertionError: assert \[- snapshot\] == \[\+ received\]", - r".* \[", + r".* list\(\[", r".* ...", r".* 'be',", r".* - 'updated',", diff --git a/tests/syrupy/extensions/__snapshots__/test_base.ambr b/tests/syrupy/extensions/__snapshots__/test_base.ambr index a2db256d..ae4f37b3 100644 --- a/tests/syrupy/extensions/__snapshots__/test_base.ambr +++ b/tests/syrupy/extensions/__snapshots__/test_base.ambr @@ -1,5 +1,5 @@ # name: TestSnapshotReporter.test_diff_lines[-0-SnapshotReporterNoContext] - ' + ''' ... - line 2 + line 02 @@ -7,10 +7,10 @@ - line 04 + line 4 ... - ' ---- + ''' +# --- # name: TestSnapshotReporter.test_diff_lines[-0-SnapshotReporter] - ' + ''' line 0 line 1 - line 2 @@ -21,18 +21,18 @@ line 5 ... line 7 - ' ---- + ''' +# --- # name: TestSnapshotReporter.test_diff_lines[-1-SnapshotReporterNoContext] - ' + ''' ... - line 3 + line 3 ... - ' ---- + ''' +# --- # name: TestSnapshotReporter.test_diff_lines[-1-SnapshotReporter] - ' + ''' line 0 ... line 2 @@ -41,10 +41,10 @@ line 4 ... line 7 - ' ---- + ''' +# --- # name: TestSnapshotReporter.test_diff_lines[-2-SnapshotReporterNoContext] - ' + ''' - line 0␍ + line 0␀ ... @@ -53,10 +53,10 @@ - line 3␀ + line 3␍␀ ... - ' ---- + ''' +# --- # name: TestSnapshotReporter.test_diff_lines[-2-SnapshotReporter] - ' + ''' - line 0␍ + line 0␀ line 1 @@ -67,5 +67,5 @@ line 4 ... line 7 - ' ---- + ''' +# --- diff --git a/tests/syrupy/extensions/amber/__snapshots__/test_amber_filters.ambr b/tests/syrupy/extensions/amber/__snapshots__/test_amber_filters.ambr index d5a34f73..6c53156e 100644 --- a/tests/syrupy/extensions/amber/__snapshots__/test_amber_filters.ambr +++ b/tests/syrupy/extensions/amber/__snapshots__/test_amber_filters.ambr @@ -1,36 +1,36 @@ # name: test_filters_error_prop[path_filter] - { + WithNested( include_me='prop value', - nested= { + nested=CustomClass( include_me='prop value', - }, - } ---- + ), + ) +# --- # name: test_filters_error_prop[prop_filter] - { + WithNested( include_me='prop value', - nested= { + nested=CustomClass( include_me='prop value', - }, - } ---- + ), + ) +# --- # name: test_filters_expected_paths - { - 'list': [ + dict({ + 'list': list([ 2, - ], - 'nested': { + ]), + 'nested': dict({ 'other': 'value', - }, - } ---- + }), + }) +# --- # name: test_filters_expected_props - { - 'list': [ + dict({ + 'list': list([ 2, - ], - 'nested': { + ]), + 'nested': dict({ 'other': 'value', - }, - } ---- + }), + }) +# --- diff --git a/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr b/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr index 912071bb..d4558392 100644 --- a/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr +++ b/tests/syrupy/extensions/amber/__snapshots__/test_amber_matchers.ambr @@ -1,64 +1,64 @@ # name: test_matches_expected_type - { - 'date_created': , - 'nested': { - 'id': , - }, - 'some_uuid': , - } ---- + dict({ + 'date_created': datetime, + 'nested': dict({ + 'id': int, + }), + 'some_uuid': UUID, + }) +# --- # name: test_matches_non_deterministic_snapshots - { + dict({ 'a': UUID(...), - 'b': { + 'b': dict({ 'b_1': 'This is deterministic', 'b_2': datetime.datetime(...), - }, - 'c': [ + }), + 'c': list([ 'Your wish is my command', 'Do not replace this one', - ], - } ---- + ]), + }) +# --- # name: test_matches_non_deterministic_snapshots.1 - { + dict({ 'a': UUID('06335e84-2872-4914-8c5d-3ed07d2a2f16'), - 'b': { + 'b': dict({ 'b_1': 'This is deterministic', 'b_2': datetime.datetime(2020, 5, 31, 0, 0), - }, - 'c': [ + }), + 'c': list([ 'Replace this one', 'Do not replace this one', - ], - } ---- + ]), + }) +# --- # name: test_matches_regex_in_regex_mode - { - 'any_number': , + dict({ + 'any_number': int, 'any_number_adjacent': 'hi', - 'data': { - 'list': [ - { - 'date_created': , + 'data': dict({ + 'list': list([ + dict({ + 'date_created': datetime, 'k': '1', - }, - { - 'date_created': , + }), + dict({ + 'date_created': datetime, 'k': '2', - }, - ], - }, + }), + ]), + }), 'specific_number': 5, - } ---- + }) +# --- # name: test_raises_unexpected_type - { - 'date_created': , + dict({ + 'date_created': datetime, 'date_updated': datetime.date(2020, 6, 1), - 'nested': { - 'id': , - }, - 'some_uuid': , - } ---- + 'nested': dict({ + 'id': int, + }), + 'some_uuid': UUID, + }) +# --- diff --git a/tests/syrupy/extensions/amber/__snapshots__/test_amber_serializer.ambr b/tests/syrupy/extensions/amber/__snapshots__/test_amber_serializer.ambr index 392d7e53..22cb2fe1 100644 --- a/tests/syrupy/extensions/amber/__snapshots__/test_amber_serializer.ambr +++ b/tests/syrupy/extensions/amber/__snapshots__/test_amber_serializer.ambr @@ -1,439 +1,439 @@ # name: TestClass.TestNestedClass.test_nested_class_method[x] 'parameterized nested class method x' ---- +# --- # name: TestClass.TestNestedClass.test_nested_class_method[y] 'parameterized nested class method y' ---- +# --- # name: TestClass.TestNestedClass.test_nested_class_method[z] 'parameterized nested class method z' ---- +# --- # name: TestClass.test_class_method_name 'this is in a test class' ---- +# --- # name: TestClass.test_class_method_parametrized[a] 'a' ---- +# --- # name: TestClass.test_class_method_parametrized[b] 'b' ---- +# --- # name: TestClass.test_class_method_parametrized[c] 'c' ---- +# --- # name: TestSubClass.TestNestedClass.test_nested_class_method[x] 'parameterized nested class method x' ---- +# --- # name: TestSubClass.TestNestedClass.test_nested_class_method[y] 'parameterized nested class method y' ---- +# --- # name: TestSubClass.TestNestedClass.test_nested_class_method[z] 'parameterized nested class method z' ---- +# --- # name: TestSubClass.test_class_method_name 'this is in a test class' ---- +# --- # name: TestSubClass.test_class_method_parametrized[a] 'a' ---- +# --- # name: TestSubClass.test_class_method_parametrized[b] 'b' ---- +# --- # name: TestSubClass.test_class_method_parametrized[c] 'c' ---- +# --- # name: test_bool[False] False ---- +# --- # name: test_bool[True] True ---- +# --- # name: test_custom_object_repr - { + CustomClass( a=1, b='2', - c= [ + c=list([ 1, 2, 3, ..., - ], - d= { + ]), + d=dict({ 'a': 1, 'b': 2, 'c': 3, 'd': ..., - }, - x= { + }), + x=CustomClass( a=1, b='2', - c= [ + c=list([ 1, 2, 3, ..., - ], - d= { + ]), + d=dict({ 'a': 1, 'b': 2, 'c': 3, 'd': ..., - }, + }), x=None, - }, - } ---- + ), + ) +# --- # name: test_cycle[cyclic0] - [ + list([ 1, 2, 3, ..., - ] ---- + ]) +# --- # name: test_cycle[cyclic1] - { + dict({ 'a': 1, 'b': 2, 'c': 3, 'd': ..., - } ---- + }) +# --- # name: test_deeply_nested_multiline_string_in_dict - { - 'value_a': { - 'value_b': ' + dict({ + 'value_a': dict({ + 'value_b': ''' line 1 line 2 line 3 - ', - }, - } ---- + ''', + }), + }) +# --- # name: test_dict[actual0] - { - 'a': { + dict({ + 'a': dict({ 'e': False, - }, + }), 'b': True, 'c': 'Some text.', - 'd': [ + 'd': list([ '1', 2, - ], - } ---- + ]), + }) +# --- # name: test_dict[actual1] - { - 'a': { + dict({ + 'a': dict({ 'e': False, - }, + }), 'b': True, 'c': 'Some ttext.', - 'd': [ + 'd': list([ '1', 2, - ], - } ---- + ]), + }) +# --- # name: test_dict[actual2] - { - ' + dict({ + ''' multi line key - ': 'Some morre text.', + ''': 'Some morre text.', 'a': 'Some ttext.', 1: True, - ( + ExampleTuple( a=1, b=2, c=3, d=4, - ): { + ): dict({ 'e': False, - }, - { + }), + frozenset({ '1', '2', - }: [ + }): list([ '1', 2, - ], - } ---- + ]), + }) +# --- # name: test_dict[actual3] - { - } ---- + dict({ + }) +# --- # name: test_dict[actual4] - { - 'key': [ - ' + dict({ + 'key': list([ + ''' line1 line2 - ', - ], - } ---- + ''', + ]), + }) +# --- # name: test_dict[actual5] - { - 'key': [ + dict({ + 'key': list([ 1, - ' + ''' line1 line2 - ', + ''', 2, - ' + ''' line3 line4 - ', - ], - } ---- + ''', + ]), + }) +# --- # name: test_dict[actual6] - { - 'key': [ + dict({ + 'key': list([ 1, - [ - ' + list([ + ''' line1 line2 - ', - ], + ''', + ]), 2, - ], - } ---- + ]), + }) +# --- # name: test_doubly_parametrized[bar-foo] 'foo' ---- +# --- # name: test_doubly_parametrized[bar-foo].1 'bar' ---- +# --- # name: test_empty_snapshot None ---- +# --- # name: test_empty_snapshot.1 '' ---- +# --- # name: test_list[actual0] - [ - ] ---- + list([ + ]) +# --- # name: test_list[actual1] - [ + list([ 'this', 'is', 'a', 'list', - ] ---- + ]) +# --- # name: test_list[actual2] - [ + list([ 'contains', 'empty', - [ - ], - ] ---- + list([ + ]), + ]) +# --- # name: test_list[actual3] - [ + list([ 1, 2, 'string', - { + dict({ 'key': 'value', - }, - ] ---- + }), + ]) +# --- # name: test_multiline_string_in_dict - { - 'value': ' + dict({ + 'value': ''' line 1 line 2 - ', - } ---- + ''', + }) +# --- # name: test_multiple_snapshots 'First.' ---- +# --- # name: test_multiple_snapshots.1 'Second.' ---- +# --- # name: test_multiple_snapshots.2 'Third.' ---- +# --- # name: test_newline_control_characters - ' + ''' line 1 line 2 - ' ---- + ''' +# --- # name: test_newline_control_characters.1 - ' + ''' line 1 line 2 - ' ---- + ''' +# --- # name: test_newline_control_characters.2 - ' + ''' line 1 line 2 - ' ---- + ''' +# --- # name: test_newline_control_characters.3 - ' + ''' line 1 line 2 - ' ---- + ''' +# --- # name: test_newline_control_characters.4 - ' + ''' line 1 line 2 - ' ---- + ''' +# --- # name: test_newline_control_characters.5 - ' + ''' line 1 line 2 - ' ---- + ''' +# --- # name: test_numbers 3.5 ---- +# --- # name: test_numbers.1 7 ---- +# --- # name: test_numbers.2 0.3333333333333333 ---- +# --- # name: test_parameter_with_dot[value.with.dot] 'value.with.dot' ---- +# --- # name: test_reflection - { + SnapshotAssertion( name='snapshot', num_executions=0, - } ---- + ) +# --- # name: test_set[actual0] - { + set({ 'a', 'is', 'set', 'this', - } ---- + }) +# --- # name: test_set[actual1] - { + set({ 'contains', 'frozen', - { + frozenset({ '1', '2', - }, - } ---- + }), + }) +# --- # name: test_set[actual2] - { + set({ 'contains', 'tuple', - ( + tuple( 1, 2, ), - } ---- + }) +# --- # name: test_set[actual3] - { + set({ 'contains', 'namedtuple', - ( + ExampleTuple( a=1, b=2, c=3, d=4, ), - } ---- + }) +# --- # name: test_set[actual4] - { - } ---- + set({ + }) +# --- # name: test_snapshot_markers - ' + ''' # # - --- + # --- # name: - ' ---- + ''' +# --- # name: test_string[0] '' ---- +# --- # name: test_string[10] b'Byte string' ---- +# --- # name: test_string[1] 'Raw string' ---- +# --- # name: test_string[2] 'Escaped \\n' ---- +# --- # name: test_string[3] 'Backslash \\u U' ---- +# --- # name: test_string[4] 'πŸ₯žπŸπŸ―' ---- +# --- # name: test_string[5] 'singleline:' ---- +# --- # name: test_string[6] '- singleline' ---- +# --- # name: test_string[7] - ' + ''' multi-line line 2 line 3 - ' ---- + ''' +# --- # name: test_string[8] - ' + ''' multi-line line 2 line 3 - ' ---- + ''' +# --- # name: test_string[9] "string with 'quotes'" ---- +# --- # name: test_tuple - ( + tuple( 'this', 'is', - ( + tuple( 'a', 'tuple', ), ) ---- +# --- # name: test_tuple.1 - ( + ExampleTuple( a='this', b='is', c='a', - d= { + d=set({ 'named', 'tuple', - }, + }), ) ---- +# --- # name: test_tuple.2 - ( + tuple( ) ---- +# --- diff --git a/tests/syrupy/extensions/image/__snapshots__/test_image_png.ambr b/tests/syrupy/extensions/image/__snapshots__/test_image_png.ambr index 850ded2d..03ca82c6 100644 --- a/tests/syrupy/extensions/image/__snapshots__/test_image_png.ambr +++ b/tests/syrupy/extensions/image/__snapshots__/test_image_png.ambr @@ -1,3 +1,3 @@ # name: test_multiple_snapshot_extensions.1 b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x002\x00\x00\x002\x04\x03\x00\x00\x00\xec\x11\x95\x82\x00\x00\x00\x1bPLTE\xcc\xcc\xcc\x96\x96\x96\xaa\xaa\xaa\xb7\xb7\xb7\xb1\xb1\xb1\x9c\x9c\x9c\xbe\xbe\xbe\xa3\xa3\xa3\xc5\xc5\xc5\x05\xa4\xf2?\x00\x00\x00\tpHYs\x00\x00\x0e\xc4\x00\x00\x0e\xc4\x01\x95+\x0e\x1b\x00\x00\x00AIDAT8\x8dc`\x18\x05\xa3\x80\xfe\x80I\xd9\xdc\x00F\xa2\x02\x16\x86\x88\x00\xa6\x16\x10\x89.\xc3\x1a" \xc0\x11\x01"\xd1e\xd8\x12#\x028"@$\x86=*\xe6\x06L- \x92zn\x1f\x05\xc3\x1b\x00\x00\xe5\xfb\x08g\r"af\x00\x00\x00\x00IEND\xaeB`\x82' ---- +# --- diff --git a/tests/syrupy/extensions/image/__snapshots__/test_image_svg.ambr b/tests/syrupy/extensions/image/__snapshots__/test_image_svg.ambr index 6a118b13..84618e01 100644 --- a/tests/syrupy/extensions/image/__snapshots__/test_image_svg.ambr +++ b/tests/syrupy/extensions/image/__snapshots__/test_image_svg.ambr @@ -1,3 +1,3 @@ # name: test_multiple_snapshot_extensions.1 '50 x 50' ---- +# --- diff --git a/tests/syrupy/extensions/json/__snapshots__/test_json_matchers/test_matcher.json b/tests/syrupy/extensions/json/__snapshots__/test_json_matchers/test_matcher.json index 5f7b5f70..0ae5a6b2 100644 --- a/tests/syrupy/extensions/json/__snapshots__/test_json_matchers/test_matcher.json +++ b/tests/syrupy/extensions/json/__snapshots__/test_json_matchers/test_matcher.json @@ -1,8 +1,8 @@ { - "date": "", + "date": "datetime", "foo": { - "another_date": "", + "another_date": "datetime", "x": "y" }, - "int": "" + "int": "int" } diff --git a/tests/syrupy/extensions/json/__snapshots__/test_json_serializer/test_snapshot_markers.json b/tests/syrupy/extensions/json/__snapshots__/test_json_serializer/test_snapshot_markers.json index 953ba3da..e35f9ba8 100644 --- a/tests/syrupy/extensions/json/__snapshots__/test_json_serializer/test_snapshot_markers.json +++ b/tests/syrupy/extensions/json/__snapshots__/test_json_serializer/test_snapshot_markers.json @@ -1 +1 @@ -"# \n # \n---\n# name:" +"# \n # \n# ---\n# name:"