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

feat: customize repr (#73) #85

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
106 changes: 106 additions & 0 deletions docs/code_generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@

## Code generation

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 overwritten with [register_repr](register_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"fglecg\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"fglecg\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"fglecg\n\x16",
}
)
```

The code is generated in the following way:

1. The value is copied with `value = copy.deepcopy(value)`
2. The code is generated with `repr(value)` (which can be [customized](register_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.
103 changes: 0 additions & 103 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,108 +201,5 @@ or passed as an argument to a function:



## Code generation

You can use almost any python datatype and also complex values like `datatime.date`, because `repr()` is used to convert the values to a source code.
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"fglecg\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"fglecg\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"fglecg\n\x16",
}
)
```

The code is generated in the following way:

1. The value is copied with `value = copy.deepcopy(value)`
2. The code is generated with `repr(value)`
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.

!!! 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.

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

--8<-- "README.md:Feedback"
89 changes: 89 additions & 0 deletions docs/register_repr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@



`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>'
```

`register_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"])

assert repr(E.a) == "<E.a: 1>"
assert E.a == snapshot(E.a)
```

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 k in sorted(
f"{k.__module__}.{k.__qualname__}"
for k in code_repr_dispatch.registry.keys()
):
print(f"- `{k}`")
```

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.

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]))
```
4 changes: 3 additions & 1 deletion inline_snapshot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from ._code_repr import HasRepr
from ._code_repr import register_repr
from ._external import external
from ._external import outsource
from ._inline_snapshot import snapshot

__all__ = ["snapshot", "external", "outsource"]
__all__ = ["snapshot", "external", "outsource", "register_repr", "HasRepr"]

__version__ = "0.10.2"