Skip to content

Commit

Permalink
Merge pull request #85 from 15r10nk/code_repr
Browse files Browse the repository at this point in the history
feat: customize repr (#73)
  • Loading branch information
15r10nk authored Jul 7, 2024
2 parents 3fa52bf + fde435d commit d5ef78f
Show file tree
Hide file tree
Showing 23 changed files with 1,162 additions and 145 deletions.
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pip install inline-snapshot
- **Preserved Black Formatting:** Retains formatting consistency with Black formatting.
- **External File Storage:** Store snapshots externally using `outsource(data)`.
- **Seamless Pytest Integration:** Integrated seamlessly with pytest for effortless testing.
- **Customizable:** code generation can be customized with [@customize_repr](https://15r10nk.github.io/inline-snapshot/customize_repr)
- **Comprehensive Documentation:** Access detailed [documentation](https://15r10nk.github.io/inline-snapshot/) for complete guidance.


Expand All @@ -51,7 +52,7 @@ def test_something():
You can now run the tests and record the correct values.

```
$ pytest --inline-snapshot=create
$ pytest --inline-snapshot=review
```

<!-- inline-snapshot: create outcome-passed=1 -->
Expand All @@ -63,7 +64,8 @@ def test_something():
assert 1548 * 18489 == snapshot(28620972)
```

inline-snapshot provides more advanced features like:
The following examples show how you can use inline-snapshot in your tests. Take a look at the
[documentation](https://15r10nk.github.io/inline-snapshot/) if you want to know more.

<!-- inline-snapshot: create fix trim this outcome-passed=1 -->
```python
Expand Down Expand Up @@ -100,6 +102,56 @@ def test_something():
assert outsource("large string\n" * 1000) == snapshot(
external("8bf10bdf2c30*.txt")
)

assert "generates\nmultiline\nstrings" == snapshot(
"""\
generates
multiline
strings\
"""
)
```


`snapshot()` can also be used as parameter for functions:

<!-- inline-snapshot: create fix trim this outcome-passed=1 -->
```python
from inline_snapshot import snapshot
import subprocess as sp
import sys


def run_python(cmd, stdout=None, stderr=None):
result = sp.run([sys.executable, "-c", cmd], capture_output=True)
if stdout is not None:
assert result.stdout.decode() == stdout
if stderr is not None:
assert result.stderr.decode() == stderr


def test_cmd():
run_python(
"print('hello world')",
stdout=snapshot(
"""\
hello world
"""
),
stderr=snapshot(""),
)

run_python(
"1/0",
stdout=snapshot(""),
stderr=snapshot(
"""\
Traceback (most recent call last):
File "<string>", line 1, in <module>
ZeroDivisionError: division by zero
"""
),
)
```

<!-- -8<- [start:Feedback] -->
Expand Down
105 changes: 105 additions & 0 deletions docs/code_generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@


You can use almost any python datatype and also complex values like `datatime.date`, because `repr()` is used to convert the values to source code.
The default `__repr__()` behaviour can be [customized](customize_repr.md).
It might be necessary to import the right modules to match the `repr()` output.

=== "original code"
<!-- inline-snapshot: outcome-passed=1 outcome-errors=1 -->
```python
from inline_snapshot import snapshot
import datetime


def something():
return {
"name": "hello",
"one number": 5,
"numbers": list(range(10)),
"sets": {1, 2, 15},
"datetime": datetime.date(1, 2, 22),
"complex stuff": 5j + 3,
"bytes": b"byte abc\n\x16",
}


def test_something():
assert something() == snapshot()
```
=== "--inline-snapshot=create"
<!-- inline-snapshot: create outcome-passed=1 -->
```python
from inline_snapshot import snapshot
import datetime


def something():
return {
"name": "hello",
"one number": 5,
"numbers": list(range(10)),
"sets": {1, 2, 15},
"datetime": datetime.date(1, 2, 22),
"complex stuff": 5j + 3,
"bytes": b"byte abc\n\x16",
}


def test_something():
assert something() == snapshot(
{
"name": "hello",
"one number": 5,
"numbers": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"sets": {1, 2, 15},
"datetime": datetime.date(1, 2, 22),
"complex stuff": (3 + 5j),
"bytes": b"byte abc\n\x16",
}
)
```

The code is generated in the following way:

1. The value is copied with `value = copy.deepcopy(value)` and it is checked if the copied value is equal to the original value.
2. The code is generated with `repr(value)` (which can be [customized](customize_repr.md))
3. Strings which contain newlines are converted to triple quoted strings.

!!! note
Missing newlines at start or end are escaped (since 0.4.0).

=== "original code"
<!-- inline-snapshot: outcome-passed=1 -->
``` python
def test_something():
assert "first line\nsecond line" == snapshot(
"""first line
second line"""
)
```

=== "--inline-snapshot=update"
<!-- inline-snapshot: update outcome-passed=1 -->
``` python
def test_something():
assert "first line\nsecond line" == snapshot(
"""\
first line
second line\
"""
)
```


4. The code is formatted with black.


5. The whole file is formatted with black if it was formatted before.

!!! note
The black formatting of the whole file could not work for the following reasons:

1. black is configured with cli arguments and not in a configuration file.<br>
**Solution:** configure black in a [configuration file](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file)
2. inline-snapshot uses a different black version.<br>
**Solution:** specify which black version inline-snapshot should use by adding black with a specific version to your dependencies.
114 changes: 114 additions & 0 deletions docs/customize_repr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@



`repr()` can be used to convert a python object into a source code representation of the object, but this does not work for every type.
Here are some examples:
```pycon
>>> repr(int)
"<class 'int'>"

>>> from enum import Enum
>>> E = Enum("E", ["a", "b"])
>>> repr(E.a)
'<E.a: 1>'
```

`customize_repr` can be used to overwrite the default `repr()` behaviour.

The implementation for `Enum` looks like this:

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

This implementation is then used by inline-snapshot if `repr()` is called during the code generation, but not in normal code.

<!-- inline-snapshot: create fix this outcome-passed=1 -->
```python
from enum import Enum


def test_enum():
E = Enum("E", ["a", "b"])

# normal repr
assert repr(E.a) == "<E.a: 1>"

# the special implementation to convert the Enum into a code
assert E.a == snapshot(E.a)
```

## builtin datatypes

inline-snapshot comes with a special implementation for the following types:
```python exec="1"
from inline_snapshot._code_repr import code_repr_dispatch, code_repr

for name, obj in sorted(
(
getattr(
obj, "_inline_snapshot_name", f"{obj.__module__}.{obj.__qualname__}"
),
obj,
)
for obj in code_repr_dispatch.registry.keys()
):
if obj is not object:
print(f"- `{name}`")
```

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"
print('--8<-- "inline_snapshot/_code_repr.py:list"')
```

!!! note
using `#!python f"{obj!r}"` or `#!c PyObject_Repr()` will not work, because inline-snapshot replaces `#!python builtins.repr` during the code generation.

## customize

You can also use `repr()` inside `__repr__()`, if you want to make your own type compatible with inline-snapshot.

<!-- inline-snapshot: create fix this outcome-passed=1 -->
```python
from enum import Enum


class Pair:
def __init__(self, a, b):
self.a = a
self.b = b

def __repr__(self):
# this would not work
# return f"Pair({self.a!r}, {self.b!r})"

# you have to use repr()
return f"Pair({repr(self.a)}, {repr(self.b)})"

def __eq__(self, other):
if not isinstance(other, Pair):
return NotImplemented
return self.a == other.a and self.b == other.b


def test_enum():
E = Enum("E", ["a", "b"])

# the special repr implementation is used recursive here
# to convert every Enum to the correct representation
assert Pair(E.a, [E.b]) == snapshot(Pair(E.a, [E.b]))
```

you can also customize the representation of datatypes in other libraries:

``` python
from inline_snapshot import customize_repr
from other_lib import SomeType


@customize_repr
def _(value: SomeType):
return f"SomeType(x={repr(value.x)})"
```
Loading

0 comments on commit d5ef78f

Please sign in to comment.