Skip to content

Commit aa9343d

Browse files
committed
feat: new external implementation
1 parent 98c456f commit aa9343d

61 files changed

Lines changed: 4116 additions & 1160 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/setup/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ runs:
1212
uses: astral-sh/setup-uv@v5
1313
with:
1414
python-version: ${{inputs.python-version}}
15+
version: 0.7.22
1516

1617
- name: Set up Python
1718
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0

.github/workflows/ci.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,22 @@ jobs:
2424
strategy:
2525
matrix:
2626
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', pypy3.9, pypy3.10]
27-
os: [ubuntu-latest, windows-latest, macos-13]
27+
os: [ubuntu-latest]
2828
extra_deps: ['"--with=pydantic<2"', '"--with=pydantic>2"']
29+
include:
30+
- os: windows-latest
31+
python-version: '3.8'
32+
extra_deps: '"--with=pydantic>2"'
33+
- os: windows-latest
34+
python-version: '3.13'
35+
extra_deps: '"--with=pydantic>2"'
36+
37+
- os: macos-13
38+
python-version: '3.8'
39+
extra_deps: '"--with=pydantic>2"'
40+
- os: macos-13
41+
python-version: '3.13'
42+
extra_deps: '"--with=pydantic>2"'
2943
env:
3044
TOP: ${{github.workspace}}
3145
COVERAGE_PROCESS_START: ${{github.workspace}}/pyproject.toml

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def test_something():
121121
)
122122

123123
assert outsource("large string\n" * 1000) == snapshot(
124-
external("8bf10bdf2c30*.txt")
124+
external("hash:8bf10bdf2c30*.txt")
125125
)
126126

127127
assert "generates\nmultiline\nstrings" == snapshot(
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!--
2+
A new scriv changelog fragment.
3+
4+
Uncomment the section that is right (remove the HTML comment wrapper).
5+
-->
6+
7+
### Added
8+
9+
- New `external()` implementation with support for different data formats.
10+
- Ability to declare custom external formats with `@register_format`.
11+
- `external()` can now be used without `snapshot()`, such as `assert "long text" == external()` or inside snapshots like dirty-equals.
12+
13+
### Changed
14+
15+
- **BREAKING CHANGE**: You now have to declare format aliases if you used `outsource()` with a different suffix than `.txt` or `.bin` in the past.
16+
17+
``` python
18+
from inline_snapshot import register_format_alias, external
19+
20+
# Can be declared in conftest.py
21+
register_format_alias(".html", ".txt")
22+
23+
24+
def test_html():
25+
assert outsource("<html></html>", suffix=".html") == snapshot()
26+
```
27+
28+
29+
<!--
30+
### Deprecated
31+
32+
- A bullet item for the Deprecated category.
33+
34+
-->
35+
<!--
36+
### Fixed
37+
38+
- A bullet item for the Fixed category.
39+
40+
-->
41+
<!--
42+
### Security
43+
44+
- A bullet item for the Security category.
45+
46+
-->

conftest.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import pytest
22

3-
from inline_snapshot._external import DiscStorage
4-
from tests.utils import snapshot_env
5-
from tests.utils import useStorage
3+
from inline_snapshot.testing._example import snapshot_env
64

75

86
@pytest.fixture(autouse=True)
9-
def snapshot_env_for_doctest(request, tmp_path):
7+
def snapshot_env_for_doctest(request):
108
if hasattr(request.node, "dtest"):
119
with snapshot_env():
12-
storage = DiscStorage(tmp_path / ".storage")
13-
with useStorage(storage):
14-
yield
10+
yield
1511
else:
1612
yield

docs/alternatives.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
inline-snapshot is not the only snapshot library for python.
1+
inline-snapshot is not the only snapshot testing library for python.
22
There are several others to:
33

44
* [syrupy](https://github.com/syrupy-project/syrupy)
@@ -13,7 +13,7 @@ If you miss a feature that is available in other libraries, please let me know.
1313
<iframe
1414
src="https://pypacktrends.com/embed?packages=inline-snapshot&packages=snapshottest&packages=syrupy&packages=pytest-snapshot&packages=pytest-insta&time_range=2years"
1515
width="100%"
16-
height="480"
16+
height="520"
1717
frameborder="0"
1818
>
1919
</iframe>

docs/assets/number_set_output.png

65.4 KB
Loading

docs/configuration.md

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ default-flags=["report"]
77
default-flags-tui=["create", "review"]
88
format-command=""
99
show-updates=false
10+
default-storage="uuid"
1011

1112
[tool.inline-snapshot.shortcuts]
1213
review=["review"]
@@ -36,24 +37,45 @@ fix=["create","fix"]
3637
* **shortcuts:** allows you to define custom commands to simplify your workflows.
3738
`--fix` and `--review` are defined by default, but this configuration can be changed to fit your needs.
3839

39-
* **storage-dir:** allows you to define the directory where inline-snapshot stores data files such as external snapshots.
40+
* **storage-dir:** allows you to define the directory where inline-snapshot stores data files such as external snapshots stored with the `hash:` protocol.
4041
By default, it will be `<pytest_config_dir>/.inline-snapshot`,
4142
where `<pytest_config_dir>` is replaced by the directory containing the Pytest configuration file, if any.
4243
External snapshots will be stored in the `external` subfolder of the storage directory.
4344
* **format-command:[](){#format-command}** allows you to specify a custom command which is used to format the python code after code is changed.
44-
``` toml
45-
[tool.inline-snapshot]
46-
format-command="ruff format --stdin-filename {filename}"
47-
```
48-
The placeholder `{filename}` can be used to specify the filename if it is needed to find the correct formatting options for this file.
45+
46+
=== "ruff format"
47+
``` toml
48+
[tool.inline-snapshot]
49+
format-command="ruff format --stdin-filename {filename}"
50+
```
51+
52+
=== "ruff format & lint"
53+
``` toml
54+
[tool.inline-snapshot]
55+
format-command="ruff check --fix-only --stdin-filename {filename} | ruff format --stdin-filename {filename}"
56+
```
57+
58+
=== "black"
59+
``` toml
60+
[tool.inline-snapshot]
61+
format-command="black --stdin-filename {filename} -"
62+
```
63+
64+
=== "no command (default)"
65+
inline-snapshot will format only the snapshot values with black when you specified no format command but needs black installed with `inline-snapshot[black]`.
66+
67+
The placeholder `{filename}` can be used to specify the filename if it is needed to find the correct formatting options for this file.
4968

5069
!!! important
5170
The command should **not** format the file on disk. The current file content (with the new code changes) is passed to *stdin* and the formatted content should be written to *stdout*.
5271

53-
You can also use a `|` if you want to use multiple commands.
54-
``` toml
55-
[tool.inline-snapshot]
56-
format-command="ruff check --fix-only --stdin-filename {filename} | ruff format --stdin-filename {filename}"
57-
```
58-
5972
* **show-updates:**[](){#show-updates} shows updates in reviews and reports.
73+
74+
75+
* **default-storage:**[](){#default-storage} defines the default storage protocol to be used when creating snapshots without an explicit storage protocol (e.g. like `external()`).
76+
Possible values are `hash` and `uuid`.
77+
external snapshots created by `outsource()` do not currently support this setting due to some internal limitations and will always use the old `hash` protocol.
78+
79+
* **tests-dir:** can be used to define where your tests are located.
80+
The default is `<pytest_config_dir>/tests` if it exists or `<pytest_config_dir>` if you have no tests directory,
81+
where `<pytest_config_dir>` is replaced by the directory containing the Pytest configuration file, if any.

docs/external/external.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
Storing snapshots in the source code is the main feature of inline snapshots.
2+
This has the advantage that you can easily see changes in code reviews. However, it also has some drawbacks:
3+
4+
* It is problematic to snapshot a large amount of data, as it consumes significant space in your tests.
5+
* Binary data or images are not human-readable in your tests.
6+
7+
`external()` solves this problem and integrates nicely with inline snapshots.
8+
It stores a reference to the external data in a special `external()` object, which can be used like `snapshot()`.
9+
10+
There are different storage protocols, such as [*hash*](#hash) or [*uuid*](#uuid), and different file formats, such as *.txt*, *.bin*, and *.json*. It is also possible to implement [*custom*](register_format.md) file formats.
11+
12+
13+
Example:
14+
15+
<!-- inline-snapshot: first_block outcome-failed=1 outcome-errors=1 -->
16+
``` python
17+
from inline_snapshot import external
18+
19+
20+
def test_something():
21+
# inline-snapshot can determine the correct file types
22+
assert "string" == external()
23+
assert b"bytes" == external()
24+
25+
# Data structures with lists and dictionaries are stored as JSON
26+
assert ["json", "like", "data"] == external()
27+
28+
# You can also explicitly specify the storage protocol
29+
assert "other text" == external("uuid:")
30+
31+
# And the format (.json instead of the default .txt in this case)
32+
assert "other text" == external("uuid:.json")
33+
```
34+
35+
inline-snapshot will then fill in the missing parts when you create your snapshots. It will keep your specified protocols and file types and generate names for your snapshots.
36+
37+
<!-- inline-snapshot: create outcome-passed=1 outcome-errors=1 -->
38+
``` python hl_lines="6 7 10 11 12 15 16 17 20 21 22"
39+
from inline_snapshot import external
40+
41+
42+
def test_something():
43+
# inline-snapshot can determine the correct file types
44+
assert "string" == external("uuid:e3e70682-c209-4cac-a29f-6fbed82c07cd.txt")
45+
assert b"bytes" == external("uuid:f728b4fa-4248-4e3a-8a5d-2f346baa9455.bin")
46+
47+
# Data structures with lists and dictionaries are stored as JSON
48+
assert ["json", "like", "data"] == external(
49+
"uuid:eb1167b3-67a9-4378-bc65-c1e582e2e662.json"
50+
)
51+
52+
# You can also explicitly specify the storage protocol
53+
assert "other text" == external(
54+
"uuid:f7c1bd87-4da5-4709-9471-3d60c8a70639.txt"
55+
)
56+
57+
# And the format (.json instead of the default .txt in this case)
58+
assert "other text" == external(
59+
"uuid:e443df78-9558-467f-9ba9-1faf7a024204.json"
60+
)
61+
```
62+
63+
The `external()` function can also be used inside other data structures.
64+
65+
<!-- inline-snapshot: first_block outcome-failed=1 outcome-errors=1 -->
66+
``` python
67+
from inline_snapshot import snapshot, external
68+
69+
70+
def test_something():
71+
assert ["long text\n" * times for times in [1, 2, 1000]] == snapshot(
72+
[..., ..., external()]
73+
)
74+
```
75+
76+
<!-- inline-snapshot: create fix outcome-passed=1 outcome-errors=1 -->
77+
``` python hl_lines="6 7 8 9 10 11 12 13"
78+
from inline_snapshot import snapshot, external
79+
80+
81+
def test_something():
82+
assert ["long text\n" * times for times in [1, 2, 1000]] == snapshot(
83+
[
84+
"long text\n",
85+
"""\
86+
long text
87+
long text
88+
""",
89+
external("uuid:e3e70682-c209-4cac-a29f-6fbed82c07cd.txt"),
90+
]
91+
)
92+
```
93+
94+
## Storage Protocols
95+
96+
### UUID
97+
98+
The `uuid:` storage protocol is the default protocol and stores the external files relative to the test files in `__inline_snapshot__/<test_file>/<qualname>/<uuid>.suffix`.
99+
100+
- :material-plus:{.green} Files are co-located with the file/function where your value is used.
101+
- :material-plus:{.green} The use of a UUID allows inline-snapshot to find the external file even if file or function names of a test function have changed.
102+
- :material-minus:{.red} Distinguishing multiple external snapshots in the same function remains challenging.
103+
104+
### Hash
105+
106+
The `hash:` storage can be used to store snapshot files based on the hash of their content. This was the first storage protocol supported by inline-snapshot and can still be useful in some cases. It also preserves backward compatibility with older inline-snapshot versions.
107+
108+
- :material-plus:{.green} Value changes cause source code changes because the hash changes.
109+
- :material-minus:{.red} GitHub/GitLab web UIs cannot be used to view the diffs, because the filename changes.
110+
111+
## Formats
112+
113+
inline-snapshot supports several built-in formats for external snapshots. The format used is determined by the given data type: bytes are stored in a `.bin` file, and strings are stored in a `.txt` file by default. More complex data types are stored in a `.json` file.
114+
115+
<!--[[[cog
116+
from inline_snapshot._global_state import state
117+
import cog
118+
119+
cog.out("|Suffix|Priority|Description|\n")
120+
cog.out("|---|---|---|\n")
121+
for format in sorted(state().all_formats.values(),key=lambda f:-f.priority):
122+
cog.out(f"| `{format.suffix}` | {format.priority}| {format.__doc__}|\n")
123+
124+
]]]-->
125+
|Suffix|Priority|Description|
126+
|---|---|---|
127+
| `.bin` | 0| Stores bytes in `.bin` files and shows them as a hexdump.|
128+
| `.txt` | 0| Stores strings in `.txt` files.|
129+
| `.json` | -10| Stores the data with `json.dump()`.|
130+
<!--[[[end]]]-->
131+
132+
[Custom formats](register_format.md) are also supported.
133+
134+
You can also use format aliases if you want to use specific file suffixes that have the same handling as existing formats.
135+
You must specify the suffix in this case.
136+
137+
<!-- inline-snapshot: first_block outcome-failed=1 outcome-errors=1 -->
138+
``` python
139+
from inline_snapshot import register_format_alias, external
140+
141+
register_format_alias(".html", ".txt")
142+
143+
144+
def test():
145+
assert "<html></html>" == external(".html")
146+
```
147+
148+
inline-snapshot uses the given suffix to create an external snapshot.
149+
150+
<!-- inline-snapshot: create outcome-passed=1 outcome-errors=1 -->
151+
``` python hl_lines="7 8 9"
152+
from inline_snapshot import register_format_alias, external
153+
154+
register_format_alias(".html", ".txt")
155+
156+
157+
def test():
158+
assert "<html></html>" == external(
159+
"uuid:e3e70682-c209-4cac-a29f-6fbed82c07cd.html"
160+
)
161+
```
162+
163+
!!! important "Breaking Change"
164+
`register_format_alias()` is required if you used `outsource(value, suffix="html")` and are migrating from inline-snapshot prior to version 0.24.
165+
166+
## pytest Options
167+
168+
It interacts with the following `--inline-snapshot` flags:
169+
170+
- `create`: Creates new external files.
171+
- `fix`: Changes external files.
172+
- `trim`: Removes all snapshots from the storage that are not referenced with `external(...)` in the code.

docs/external/external_file.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
`external_file()` is a lower-level solution than `external()`.
2+
It accepts one argument, which is the path to the file where your external object should be stored.
3+
It will only *create* or *fix* the given files and will never *trim* unused files.
4+
5+
::: inline_snapshot
6+
options:
7+
heading_level: 3
8+
members: [external_file]
9+
show_root_heading: false
10+
show_bases: false
11+
show_source: false
12+
13+
You can use it to generate files in your project.
14+
15+
``` python
16+
def test_generate_doc():
17+
assert generate_features_doc() == external_file("all_features.md")
18+
```
19+
20+
inline-snapshot checks whether your documentation is up to date and displays a diff that you can approve if necessary.
21+
22+
Another use case is to check if some files in your project are correct by reading the file, transforming it, and comparing it with the current version.
23+
The transformation (`eval_code_blocks()` in the example) of the text should produce the same result if everything is correct.
24+
The test will fail if the transformation results in different output, and inline-snapshot will show you the diff, as it does for other external comparisons.
25+
26+
``` python
27+
def test_files():
28+
for file in root.rglob("*.md", format=".txt"):
29+
current_text = file.read_text()
30+
31+
# eval_code_blocks is a custom function that could run your examples in a project-specific way and store the output in the documentation.
32+
# It is up to you to implement such functions for your specific use case.
33+
correct_text = eval_code_blocks(current_text)
34+
35+
assert correct_text == external_file(file)
36+
```

0 commit comments

Comments
 (0)