Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Container #121

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/customize_repr.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_enum():

inline-snapshot comes with a special implementation for the following types:

```python exec="1"
``` python exec="1"
from inline_snapshot._code_repr import code_repr_dispatch, code_repr

for name, obj in sorted(
Expand All @@ -60,7 +60,7 @@ for name, obj in sorted(

Container types like `dict` or `dataclass` need a special implementation because it is necessary that the implementation uses `repr()` for the child elements.

```python exec="1" result="python"
``` python exec="1" result="python"
print('--8<-- "src/inline_snapshot/_code_repr.py:list"')
```

Expand Down
171 changes: 167 additions & 4 deletions docs/eq_snapshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,31 @@ Example:
def test_something():
assert 2 + 40 == snapshot(42)
```
## unmanaged snapshot parts

inline-snapshots manages everything inside `snapshot(...)`, which means that the developer should not change these parts, but there are cases where it is useful to give the developer a bit more control over the snapshot content.

## dirty-equals
Therefor some types will be ignored by inline-snapshot and will **not be updated or fixed**, even if they cause tests to fail.

These types are:

* dirty-equals expression
* dynamic code inside `Is(...)`
* and snapshots inside snapshots.

inline-snapshot is able to handle these types inside the following containers:

* list
* tuple
* dict
* namedtuple
* dataclass
<!--
* pydantic models
* attrs
-->

### dirty-equals

It might be, that larger snapshots with many lists and dictionaries contain some values which change frequently and are not relevant for the test.
They might be part of larger data structures and be difficult to normalize.
Expand Down Expand Up @@ -82,7 +104,7 @@ Example:

inline-snapshot tries to change only the values that it needs to change in order to pass the equality comparison.
This allows to replace parts of the snapshot with [dirty-equals](https://dirty-equals.helpmanual.io/latest/) expressions.
This expressions are preserved as long as the `==` comparison with them is `True`.
This expressions are preserved even if the `==` comparison with them is `False`.

Example:

Expand Down Expand Up @@ -159,8 +181,149 @@ Example:
)
```

!!! note
The current implementation looks only into lists, dictionaries and tuples and not into the representation of other data structures.
### Is(...)

`Is()` can be used to put runtime values inside snapshots.
It tells inline-snapshot that the developer wants control over some part of the snapshot.

<!-- inline-snapshot: create fix first_block outcome-passed=1 -->
``` python
from inline_snapshot import snapshot, Is

current_version = "1.5"


def request():
return {"data": "page data", "version": current_version}


def test_function():
assert request() == snapshot(
{"data": "page data", "version": Is(current_version)}
)
```

The `current_version` can now be changed without having to correct the snapshot.

`Is()` can also be used when the snapshot is evaluated multiple times.

=== "original code"
<!-- inline-snapshot: first_block outcome-failed=1 outcome-errors=1 -->
``` python
from inline_snapshot import snapshot, Is


def test_function():
for c in "abc":
assert [c, "correct"] == snapshot([Is(c), "wrong"])
```

=== "--inline-snapshot=fix"
<!-- inline-snapshot: fix outcome-passed=1 -->
``` python hl_lines="6"
from inline_snapshot import snapshot, Is


def test_function():
for c in "abc":
assert [c, "correct"] == snapshot([Is(c), "correct"])
```

### inner snapshots

Snapshots can be used inside other snapshots in different use cases.

#### conditional snapshots
It is possible to describe version specific parts of snapshots by replacing the specific part with `#!python snapshot() if some_condition else snapshot()`.
The test has to be executed in each specific condition to fill the snapshots.

The following example shows how this can be used to run a tests with two different library versions:

=== "my_lib v1"

<!-- inline-snapshot-lib: my_lib.py -->
``` python
version = 1


def get_schema():
return [{"name": "var_1", "type": "int"}]
```

=== "my_lib v2"

<!-- inline-snapshot-lib: my_lib.py -->
``` python
version = 2


def get_schema():
return [{"name": "var_1", "type": "string"}]
```


<!-- inline-snapshot: create fix first_block outcome-passed=1 -->
``` python
from inline_snapshot import snapshot
from my_lib import version, get_schema


def test_function():
assert get_schema() == snapshot(
[
{
"name": "var_1",
"type": snapshot("int") if version < 2 else snapshot("string"),
}
]
)
```

The advantage of this approach is that the test uses always the correct values for each library version.

#### common snapshot parts

Another usecase is the extraction of common snapshot parts into an extra snapshot:

<!-- inline-snapshot: create fix first_block outcome-passed=1 -->
``` python
from inline_snapshot import snapshot


def some_data(name):
return {"header": "really long header\n" * 5, "your name": name}


def test_function():

header = snapshot(
"""\
really long header
really long header
really long header
really long header
really long header
"""
)

assert some_data("Tom") == snapshot(
{
"header": header,
"your name": "Tom",
}
)

assert some_data("Bob") == snapshot(
{
"header": header,
"your name": "Bob",
}
)
```

This simplifies test data and allows inline-snapshot to update your values if required.
It makes also sure that the header is the same in both cases.


## pytest options

Expand Down
4 changes: 2 additions & 2 deletions docs/pytest.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ inline-snapshot provides one pytest option with different flags (*create*,
Snapshot comparisons return always `True` if you use one of the flags *create*, *fix* or *review*.
This is necessary because the whole test needs to be run to fix all snapshots like in this case:

```python
``` python
from inline_snapshot import snapshot


Expand All @@ -30,7 +30,7 @@ def test_something():
Approve the changes of the given [category](categories.md).
These flags can be combined with *report* and *review*.

```python title="test_something.py"
``` python title="test_something.py"
from inline_snapshot import snapshot


Expand Down
20 changes: 10 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,6 @@ omit = [
parallel = true
source_pkgs = ["inline_snapshot", "tests"]

[tool.hatch.envs.coverage]
dependencies = [
"coverage"
]
env-vars.TOP = "{root}"
scripts.report = "coverage html"

[tool.hatch.envs.docs]
dependencies = [
"markdown-exec[ansi]>=1.8.0",
Expand Down Expand Up @@ -119,6 +112,12 @@ extra-dependencies = [
]
env-vars.TOP = "{root}"

[tool.hatch.envs.hatch-test.scripts]
run = "pytest{env:HATCH_TEST_ARGS:} {args}"
run-cov = "coverage run -m pytest{env:HATCH_TEST_ARGS:} {args}"
cov-combine = "coverage combine"
cov-report=["coverage report","coverage html"]

[tool.hatch.envs.types]
extra-dependencies = [
"mypy>=1.0.0",
Expand Down Expand Up @@ -164,6 +163,7 @@ venvPath = ".nox"
format = "md"
version = "command: cz bump --get-next"

[tool.inline-snapshot.shortcuts]
sfix="create,fix"
review="create,review"
[tool.pytest.ini_options]
markers = [
"no_rewriting: The test does not use the ast-nodes for rewriting",
]
12 changes: 11 additions & 1 deletion src/inline_snapshot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@
from ._external import external
from ._external import outsource
from ._inline_snapshot import snapshot
from ._is import Is
from ._types import Category
from ._types import Snapshot

__all__ = ["snapshot", "external", "outsource", "customize_repr", "HasRepr"]
__all__ = [
"snapshot",
"external",
"outsource",
"customize_repr",
"HasRepr",
"Is",
"Category",
"Snapshot",
]

__version__ = "0.14.0"
3 changes: 3 additions & 0 deletions src/inline_snapshot/_adapter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .adapter import get_adapter_type

__all__ = ("get_adapter_type",)
71 changes: 71 additions & 0 deletions src/inline_snapshot/_adapter/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

import ast
import typing

from inline_snapshot._source_file import SourceFile


def get_adapter_type(value):
from inline_snapshot._adapter.dataclass_adapter import get_adapter_for_type

adapter = get_adapter_for_type(type(value))
if adapter is not None:
return adapter

if isinstance(value, list):
from .sequence_adapter import ListAdapter

return ListAdapter

if type(value) is tuple:
from .sequence_adapter import TupleAdapter

return TupleAdapter

if isinstance(value, dict):
from .dict_adapter import DictAdapter

return DictAdapter

from .value_adapter import ValueAdapter

return ValueAdapter


class Item(typing.NamedTuple):
value: typing.Any
node: ast.expr


class Adapter:
context: SourceFile

def __init__(self, context):
self.context = context

def get_adapter(self, old_value, new_value) -> Adapter:
if type(old_value) is not type(new_value):
from .value_adapter import ValueAdapter

return ValueAdapter(self.context)

adapter_type = get_adapter_type(old_value)
if adapter_type is not None:
return adapter_type(self.context)
assert False

def assign(self, old_value, old_node, new_value):
raise NotImplementedError(cls)

@classmethod
def map(cls, value, map_function):
raise NotImplementedError(cls)

@classmethod
def repr(cls, value):
raise NotImplementedError(cls)


def adapter_map(value, map_function):
return get_adapter_type(value).map(value, map_function)
Loading