From 4b9f337d655edc07117ce883a41027891be4fae2 Mon Sep 17 00:00:00 2001 From: Frank Hoffmann <15r10nk-git@polarbit.de> Date: Sat, 14 Sep 2024 11:31:28 +0200 Subject: [PATCH 1/4] fix: dirty_equals can be compared multiple times --- ...917_192956_15r10nk-git_fix_dirty_equals.md | 47 +++++++ pyproject.toml | 5 + src/inline_snapshot/_inline_snapshot.py | 121 +++++++++++++++--- src/inline_snapshot/extra.py | 2 +- tests/test_inline_snapshot.py | 56 ++++++++ 5 files changed, 209 insertions(+), 22 deletions(-) create mode 100644 changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md diff --git a/changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md b/changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md new file mode 100644 index 00000000..ec2091f7 --- /dev/null +++ b/changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md @@ -0,0 +1,47 @@ + + + + + + +### Fixed + +- A snapshot which contains an dirty-equals expression can now be compared multiple times. + + ``` python + def test_something(): + greeting = "hello" + for name in ["alex", "bob"]: + assert (name, greeting) == snapshot((IsString(), "hello")) + ``` + + diff --git a/pyproject.toml b/pyproject.toml index f439a583..99159b84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,3 +141,8 @@ exclude = "tests/.*_samples" [tool.pyright] venv = "test-3-12" venvPath = ".nox" + + +[tool.scriv] +format = "md" +version = "literal: pyproject.toml: project.version" diff --git a/src/inline_snapshot/_inline_snapshot.py b/src/inline_snapshot/_inline_snapshot.py index 07696c8c..64ea8478 100644 --- a/src/inline_snapshot/_inline_snapshot.py +++ b/src/inline_snapshot/_inline_snapshot.py @@ -169,27 +169,49 @@ def _new_code(self): assert False def _get_changes(self) -> Iterator[Change]: - # generic fallback - new_token = value_to_token(self._old_value) - if ( - self._ast_node is not None - and self._token_of_node(self._ast_node) != new_token - ): - flag = "update" - else: - return + def handle(node, value): + if isinstance(value, list): + if not isinstance(node, ast.List): + return + for n, v in zip(node.elts, value): + yield from handle(n, v) + elif isinstance(value, tuple): + if not isinstance(node, ast.Tuple): + return + for n, v in zip(node.elts, value): + yield from handle(n, v) + + elif isinstance(value, dict): + if not isinstance(node, ast.Dict): + return + for vk, nk, n in zip(value.keys(), node.keys, node.values): + try: + # this is just a sanity check, dicts should be ordered + node_key = ast.literal_eval(nk) + except Exception: + assert False + else: + assert node_key == vk - new_code = self._token_to_code(new_token) + yield from handle(n, value[vk]) + else: + if update_allowed(value): + new_token = value_to_token(value) + if self._token_of_node(node) != new_token: + new_code = self._token_to_code(new_token) + + yield Replace( + node=self._ast_node, + source=self._source, + new_code=new_code, + flag="update", + old_value=self._old_value, + new_value=self._old_value, + ) - yield Replace( - node=self._ast_node, - source=self._source, - new_code=new_code, - flag=flag, - old_value=self._old_value, - new_value=self._old_value, - ) + if self._source is not None: + yield from handle(self._ast_node, self._old_value) # functions which determine the type @@ -252,8 +274,57 @@ def __eq__(self, other): if self._old_value is undefined: _missing_values += 1 + def use_valid_old_values(old_value, new_value): + + if isinstance(new_value, dirty_equals.DirtyEquals): + assert False + + if ( + isinstance(new_value, list) + and isinstance(old_value, list) + or isinstance(new_value, tuple) + and isinstance(old_value, tuple) + ): + diff = add_x(align(old_value, new_value)) + old = iter(old_value) + new = iter(new_value) + result = [] + for c in diff: + if c in "mx": + old_value_element = next(old) + new_value_element = next(new) + result.append( + use_valid_old_values(old_value_element, new_value_element) + ) + elif c == "i": + result.append(next(new)) + elif c == "d": + pass + else: + assert False + + return type(new_value)(result) + + elif isinstance(new_value, dict) and isinstance(old_value, dict): + result = {} + + for key, new_value_element in new_value.items(): + if key in old_value: + result[key] = use_valid_old_values( + old_value[key], new_value_element + ) + else: + result[key] = new_value_element + + return result + + if new_value == old_value: + return old_value + else: + return new_value + if self._new_value is undefined: - self._new_value = clone(other) + self._new_value = use_valid_old_values(self._old_value, clone(other)) return self._visible_value() == other @@ -371,14 +442,22 @@ def check(old_value, old_node, new_value): return # generic fallback - new_token = value_to_token(new_value) + + # because IsStr() != IsStr() + if type(old_value) is type(new_value) and not update_allowed(new_value): + return + + if old_node is None: + new_token = [] + else: + new_token = value_to_token(new_value) if not old_value == new_value: flag = "fix" elif ( self._ast_node is not None - and self._token_of_node(old_node) != new_token and update_allowed(old_value) + and self._token_of_node(old_node) != new_token ): flag = "update" else: diff --git a/src/inline_snapshot/extra.py b/src/inline_snapshot/extra.py index cb85969a..3dc37e7a 100644 --- a/src/inline_snapshot/extra.py +++ b/src/inline_snapshot/extra.py @@ -103,7 +103,7 @@ def test_prints(): === "ignore stdout" - ``` python hl_lines="3 9" + ``` python hl_lines="3 9 10" from inline_snapshot import snapshot from inline_snapshot.extra import prints from dirty_equals import IsStr diff --git a/tests/test_inline_snapshot.py b/tests/test_inline_snapshot.py index b1ff9533..a924ac63 100644 --- a/tests/test_inline_snapshot.py +++ b/tests/test_inline_snapshot.py @@ -10,6 +10,7 @@ from inline_snapshot import snapshot from inline_snapshot._inline_snapshot import Flags from inline_snapshot._utils import triple_quote +from inline_snapshot.testing import Example from inline_snapshot.testing._example import snapshot_env @@ -1048,3 +1049,58 @@ def test_thing(): result = project.run("--inline-snapshot=report") assert result.report == snapshot("") + + +def test_compare_dirty_equals_twice() -> None: + + Example( + """ +from dirty_equals import IsStr +from inline_snapshot import snapshot + +for x in 'ab': + assert x == snapshot(IsStr()) + assert [x,5] == snapshot([IsStr(),3]) + assert {'a':x,'b':5} == snapshot({'a':IsStr(),'b':3}) + +""" + ).run_inline( + ["--inline-snapshot=fix"], + changed_files=snapshot( + { + "test_something.py": """\ + +from dirty_equals import IsStr +from inline_snapshot import snapshot + +for x in 'ab': + assert x == snapshot(IsStr()) + assert [x,5] == snapshot([IsStr(),5]) + assert {'a':x,'b':5} == snapshot({'a':IsStr(),'b':5}) + +""" + } + ), + ) + + +def test_dirty_equals_in_unused_snapshot() -> None: + + Example( + """ +from dirty_equals import IsStr +from inline_snapshot import snapshot + +snapshot([IsStr(),3]) +snapshot((IsStr(),3)) +snapshot({1:IsStr(),2:3}) + +t=(1,2) +d={1:2} +l=[1,2] +snapshot([t,d,l]) +""" + ).run_inline( + ["--inline-snapshot=fix"], + changed_files=snapshot({}), + ) From 32fcb0de82e43fd0d25141c4930599e67d5a656d Mon Sep 17 00:00:00 2001 From: Frank Hoffmann <15r10nk-git@polarbit.de> Date: Sun, 22 Sep 2024 13:54:11 +0200 Subject: [PATCH 2/4] fix: show a warning if the user uses star-expressions inside Lists --- ...917_192956_15r10nk-git_fix_dirty_equals.md | 8 +- src/inline_snapshot/_inline_snapshot.py | 62 +++++++++----- src/inline_snapshot/testing/_example.py | 28 ++++--- tests/test_inline_snapshot.py | 80 ++++++++++++++++++- 4 files changed, 143 insertions(+), 35 deletions(-) diff --git a/changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md b/changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md index ec2091f7..d6743dee 100644 --- a/changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md +++ b/changelog.d/20240917_192956_15r10nk-git_fix_dirty_equals.md @@ -16,12 +16,14 @@ Uncomment the section that is right (remove the HTML comment wrapper). - A bullet item for the Added category. --> -