Skip to content

Commit e88e065

Browse files
authored
Merge pull request #356 from 15r10nk/error-output
UsageError("unmanaged values can not be compared with snapshots") when using -k flag #355
2 parents fddfa4a + ea554e6 commit e88e065

File tree

10 files changed

+271
-73
lines changed

10 files changed

+271
-73
lines changed

.github/copilot-instructions.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copilot Instructions
2+
3+
## Changelog entries
4+
5+
To create a new changelog entry, run:
6+
7+
```
8+
uvx scriv create --add
9+
```
10+
11+
This creates a new Markdown file in `changelog.d/` and stages it with git.
12+
Then fill in the relevant section(s) in that file. The default template contains
13+
headers for **Added**, **Changed**, **Fixed**, and **Removed** — delete the ones
14+
that don't apply and write one or two sentences under the relevant header.
15+
16+
Example result in `changelog.d/<fragment>.md`:
17+
18+
```markdown
19+
### Fixed
20+
21+
- Fixed snapshot comparison for external files when `_changes()` raises during report generation.
22+
```
23+
24+
Do **not** edit `CHANGELOG.md` directly — that file is assembled from fragments via `scriv collect`.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### Added
2+
3+
- Added `context_managers` parameter to `Example.run_inline()`, allowing tests to inject context managers (e.g. `unittest.mock.patch`) that are active during `show_report()`.
4+
5+
### Fixed
6+
7+
- Fixed `UsageError("unmanaged values can not be compared with snapshots")` raised during session teardown when using `-k` to filter tests ([#355](https://github.com/15r10nk/inline-snapshot/issues/355)). This was caused by inline-snapshot trying to update snapshots that were never compared. This is a rare edge case that caused problems when matchers were used, so it has been removed.

src/inline_snapshot/_inline_snapshot.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any
44
from typing import Iterator
55
from typing import TypeVar
6+
from typing import cast
67

78
from inline_snapshot._adapter_context import AdapterContext
89
from inline_snapshot._customize._custom_undefined import CustomUndefined
@@ -114,12 +115,18 @@ def result(self):
114115
def create_raw(obj, context: AdapterContext):
115116
return obj
116117

118+
def __repr__(self):
119+
if self._expr:
120+
return ast.unparse(self._expr.node)
121+
else:
122+
return "snapshot(...)"
123+
117124
def _changes(self) -> Iterator[ChangeBase]:
118125

119126
if (
120127
isinstance(self._value._old_value, CustomUndefined)
121128
if self._expr is None
122-
else not self._expr.node.args
129+
else not cast(ast.Call, self._expr.node).args
123130
):
124131

125132
if isinstance(self._value._new_value, CustomUndefined):
@@ -130,7 +137,9 @@ def _changes(self) -> Iterator[ChangeBase]:
130137
yield CallArg(
131138
flag="create",
132139
file=self._value._file,
133-
node=self._expr.node if self._expr is not None else None,
140+
node=(
141+
cast(ast.Call, self._expr.node) if self._expr is not None else None
142+
),
134143
arg_pos=0,
135144
arg_name=None,
136145
new_code=new_code,

src/inline_snapshot/_snapshot/undecided_value.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from inline_snapshot._customize._custom_sequence import CustomTuple
1515
from inline_snapshot._customize._custom_undefined import CustomUndefined
1616
from inline_snapshot._customize._custom_unmanaged import CustomUnmanaged
17-
from inline_snapshot._new_adapter import NewAdapter
1817
from inline_snapshot._new_adapter import warn_star_expression
1918
from inline_snapshot._unmanaged import is_unmanaged
2019

@@ -141,15 +140,8 @@ def _new_code(self):
141140
assert False
142141

143142
def _get_changes(self) -> Iterator[ChangeBase]:
144-
assert isinstance(self._new_value, CustomUndefined)
145-
146-
new_value = self.to_custom(self._old_value._eval())
147-
148-
adapter = NewAdapter(self._context)
149-
150-
for change in adapter.compare(self._old_value, self._ast_node, new_value):
151-
assert change.flag == "update", change
152-
yield change
143+
yield from ()
144+
return
153145

154146
def __eq__(self, other):
155147
if compare_only():

src/inline_snapshot/_snapshot_session.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
import os
23
import sys
34
import tokenize
@@ -6,6 +7,7 @@
67
from types import SimpleNamespace
78
from typing import Dict
89
from typing import List
10+
from typing import cast
911

1012
from executing import is_pytest_compatible
1113
from rich import box
@@ -425,7 +427,25 @@ def console():
425427

426428
for snapshot in state().snapshots.values():
427429
all_categories = set()
428-
for change in snapshot._changes():
430+
try:
431+
change_list = list(snapshot._changes())
432+
except Exception as exception:
433+
context = ""
434+
if expr := getattr(snapshot, "_expr", None):
435+
code = expr.frame.f_code
436+
context = f"""
437+
file: {code.co_filename}
438+
line: {cast(ast.expr,expr.node).lineno}\
439+
"""
440+
raise RuntimeError(
441+
f"""
442+
error during change collection for snapshot ({snapshot})
443+
snapshot.\
444+
{context}
445+
"""
446+
) from exception
447+
448+
for change in change_list:
429449
changes[change.flag].append(change)
430450
all_categories.add(change.flag)
431451

src/inline_snapshot/_types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
from typing import Iterator
44
from typing import Literal
5+
from typing import Optional
56
from typing import Protocol
67
from typing import TypeVar
78

9+
from executing import Executing
10+
811
from inline_snapshot._change import ChangeBase
912

1013
T = TypeVar("T", covariant=True)
1114

1215

1316
class SnapshotRefBase:
17+
_expr: Optional[Executing]
18+
1419
def _changes(self) -> Iterator[ChangeBase]:
1520
raise NotImplementedError
1621

src/inline_snapshot/testing/_example.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
import traceback
1111
import uuid
1212
from argparse import ArgumentParser
13+
from contextlib import ExitStack
1314
from contextlib import contextmanager
1415
from io import StringIO
1516
from pathlib import Path
1617
from tempfile import TemporaryDirectory
1718
from typing import Callable
19+
from typing import ContextManager
20+
from typing import Sequence
1821
from unittest.mock import patch
1922

2023
from rich.console import Console
@@ -261,6 +264,7 @@ def run_inline(
261264
self,
262265
args: list[str] = [],
263266
*,
267+
context_managers: Sequence[ContextManager] = (),
264268
reported_categories: Snapshot[list[Category]] | None = None,
265269
changed_files: Snapshot[dict[str, str]] | None = None,
266270
report: Snapshot[str] | None = None,
@@ -274,6 +278,7 @@ def run_inline(
274278
275279
Parameters:
276280
args: inline-snapshot arguments (supports only "--inline-snapshot=fix|create|...").
281+
context_managers: list of context managers to use when the code is executed.
277282
reported_categories: snapshot of categories which inline-snapshot thinks could be applied.
278283
changed_files: snapshot of files which are changed by this run.
279284
raises: snapshot of the exception raised during test execution.
@@ -311,7 +316,11 @@ def run_inline(
311316

312317
raised_exception = []
313318

314-
with deterministic_uuid(), chdir(tmp_path), temp_environ(TERM="unknown"):
319+
with ExitStack() as stack:
320+
stack.enter_context(deterministic_uuid())
321+
stack.enter_context(chdir(tmp_path))
322+
stack.enter_context(temp_environ(TERM="unknown"))
323+
315324
session = SnapshotSession()
316325

317326
def report_error(message):
@@ -324,6 +333,9 @@ def report_error(message):
324333
sys.path.insert(0, str(tmp_path))
325334

326335
try:
336+
for cm in context_managers:
337+
stack.enter_context(cm)
338+
327339
enter_snapshot_context()
328340
session.load_config(
329341
tmp_path / "pyproject.toml",
@@ -394,11 +406,15 @@ def fail(message):
394406
if not tests_found:
395407
raise UsageError("no test_*() functions in the example")
396408

397-
session.show_report(console)
409+
try:
410+
session.show_report(console)
398411

399-
for snapshot in state().snapshots.values():
400-
for change in snapshot._changes():
401-
snapshot_flags.add(change.flag)
412+
for snapshot in state().snapshots.values():
413+
for change in snapshot._changes():
414+
snapshot_flags.add(change.flag)
415+
except Exception as e:
416+
traceback.print_exc()
417+
raised_exception.append(e)
402418

403419
except StopTesting as e:
404420
assert stderr == f"ERROR: {e}\n"
@@ -414,9 +430,11 @@ def fail(message):
414430
if raises is None:
415431
raise raised_exception[0]
416432

417-
assert raises == "\n".join(
433+
raises_text = "\n".join(
418434
f"{type(e).__name__}:\n" + str(e) for e in raised_exception
419435
)
436+
raises_text = raises_text.replace(str(tmp_path), "<tmp>")
437+
assert raises == raises_text
420438
else:
421439
assert raises == None
422440

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Tests for error handling during change collection in show_report()."""
2+
3+
import sys
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from inline_snapshot import snapshot
9+
from inline_snapshot._external._external import External
10+
from inline_snapshot._external._external_file import ExternalFile
11+
from inline_snapshot._inline_snapshot import SnapshotReference
12+
from inline_snapshot.testing import Example
13+
from tests.utils import path_transform
14+
15+
16+
@pytest.mark.skipif(
17+
sys.platform != "linux",
18+
reason="test only on linux to prevent problems with path separators",
19+
)
20+
@pytest.mark.parametrize(
21+
"snapshot_type,expr",
22+
[
23+
(SnapshotReference, "snapshot()"),
24+
(External, "external()"),
25+
(ExternalFile, "external_file('test.txt')"),
26+
],
27+
)
28+
def test_change_collection_error(expr, snapshot_type):
29+
"""RuntimeError includes file and line from snapshot._expr when _changes() raises."""
30+
Example(
31+
f"""\
32+
from inline_snapshot import snapshot,external,external_file
33+
34+
def test_a():
35+
assert "hello" == {expr}
36+
"""
37+
).run_inline(
38+
["--inline-snapshot=report"],
39+
context_managers=[
40+
patch.object(
41+
snapshot_type,
42+
"_changes",
43+
side_effect=ValueError("simulated internal error"),
44+
)
45+
],
46+
raises=path_transform(
47+
snapshot(
48+
{
49+
"SnapshotReference": """\
50+
RuntimeError:
51+
52+
error during change collection for snapshot (snapshot())
53+
snapshot.
54+
file: <tmp>/tests/test_something.py
55+
line: 4
56+
""",
57+
"External": """\
58+
AssertionError:
59+
60+
RuntimeError:
61+
62+
error during change collection for snapshot (external("uuid:"))
63+
snapshot.
64+
file: <tmp>/tests/test_something.py
65+
line: 4
66+
""",
67+
"ExternalFile": """\
68+
AssertionError:
69+
70+
RuntimeError:
71+
72+
error during change collection for snapshot (external_file('<tmp>/tests/test.txt'))
73+
snapshot.
74+
""",
75+
}
76+
)[snapshot_type.__name__]
77+
),
78+
)

0 commit comments

Comments
 (0)