pax_global_header 0000666 0000000 0000000 00000000064 14764761403 0014526 g ustar 00root root 0000000 0000000 52 comment=7edafe7713c88ae15e12d96d37833af0f9f1f6f3
python-inline-snapshot-0.20.7/ 0000775 0000000 0000000 00000000000 14764761403 0016246 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/.github/ 0000775 0000000 0000000 00000000000 14764761403 0017606 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/.github/FUNDING.yml 0000664 0000000 0000000 00000000020 14764761403 0021413 0 ustar 00root root 0000000 0000000 github: 15r10nk
python-inline-snapshot-0.20.7/.github/actions/ 0000775 0000000 0000000 00000000000 14764761403 0021246 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/.github/actions/setup/ 0000775 0000000 0000000 00000000000 14764761403 0022406 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/.github/actions/setup/action.yml 0000664 0000000 0000000 00000001042 14764761403 0024403 0 ustar 00root root 0000000 0000000 name: General Setup
description: checkout & setup python
inputs:
python-version: # id of input
description: the python version to use
required: false
default: '3.12'
runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{inputs.python-version}}
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: ${{inputs.python-version}}
architecture: x64
allow-prereleases: true
python-inline-snapshot-0.20.7/.github/workflows/ 0000775 0000000 0000000 00000000000 14764761403 0021643 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/.github/workflows/ci.yml 0000664 0000000 0000000 00000010265 14764761403 0022765 0 ustar 00root root 0000000 0000000 name: CI
on:
pull_request:
push:
branches: [main]
jobs:
mypy:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/setup
with:
python-version: ${{matrix.python-version}}
- run: uvx hatch run +py=${{matrix.python-version}} types:check
test:
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', pypy3.9, pypy3.10]
os: [ubuntu-latest, windows-latest, macos-13]
extra_deps: ['"--with=pytest==8.3.3" "--with=pydantic<2"', '"--with=pytest>=8.3.4" "--with=pydantic>2"']
env:
TOP: ${{github.workspace}}
COVERAGE_PROCESS_START: ${{github.workspace}}/pyproject.toml
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/setup
with:
python-version: ${{matrix.python-version}}
- run: |
uv run ${{matrix.extra_deps}} --extra black --extra dirty-equals -m ${{ matrix.os == 'ubuntu-latest' && 'coverage run -m' || '' }} pytest -n=auto -vv
- run: |
uv run -m coverage combine
mv .coverage .coverage.${{ matrix.python-version }}-${{matrix.os}}-${{strategy.job-index}}
if: matrix.os == 'ubuntu-latest'
- name: Upload coverage data
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
if: matrix.os == 'ubuntu-latest'
with:
name: coverage-data-${{github.run_id}}-${{ matrix.python-version }}-${{matrix.os}}-${{strategy.job-index}}
path: .coverage.*
include-hidden-files: true
if-no-files-found: ignore
coverage:
name: Combine & check coverage
env:
TOP: ${{github.workspace}}
if: always()
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/setup
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
pattern: coverage-data-${{github.run_id}}-*
merge-multiple: true
- name: Combine coverage & fail if it's <100%
run: |
uv pip install --upgrade coverage[toml]
coverage combine
coverage html --skip-covered --skip-empty
# Report and write to summary.
coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
# Report again and fail if under 100%.
coverage report --fail-under=100
- name: Upload HTML report if check failed
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: html-report
path: htmlcov
if: ${{ failure() }}
publish:
name: Publish new release
runs-on: ubuntu-latest
needs: [test, coverage]
environment: pypi
permissions:
# IMPORTANT: this permission is mandatory for Trusted Publishing
id-token: write
# this permission is mandatory to create github releases
contents: write
steps:
- name: Checkout main
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: ./.github/actions/setup
- name: Check if the commit has a vx.y.z tag
id: check-version
run: |
if git tag --list --points-at ${{ github.sha }} | grep -q -E '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "is new version"
echo "should_continue=true" >> "$GITHUB_OUTPUT"
else
echo "is not a new version"
echo "should_continue=false" >> "$GITHUB_OUTPUT"
fi
- run: uv pip install hatch scriv
- name: build package
run: hatch build
- name: Publish package distributions to PyPI
if: ${{ steps.check-version.outputs.should_continue == 'true' }}
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
- name: create github release
if: ${{ steps.check-version.outputs.should_continue == 'true' }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: scriv github-release
python-inline-snapshot-0.20.7/.github/workflows/deploy_development_docs.yml 0000664 0000000 0000000 00000001122 14764761403 0027270 0 ustar 00root root 0000000 0000000 name: deploy development docs
on:
push:
branches: [main]
jobs:
build:
name: Deploy development docs
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: pip install hatch
- name: publish docs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "Frank Hoffmann"
git config user.email "15r10nk@users.noreply.github.com"
git fetch origin gh-pages --depth=1
hatch run docs:mike deploy --push development
python-inline-snapshot-0.20.7/.github/workflows/docs.yml 0000664 0000000 0000000 00000001366 14764761403 0023324 0 ustar 00root root 0000000 0000000 name: Publish docs via GitHub Pages
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
jobs:
build:
name: Deploy docs for new version
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: pip install hatch
- run: hatch version | sed -rne "s:([0-9]*)\.([0-9]*)\..*:INLINE_SNAPSHOT_VERSION=\1.\2:p" >> ${GITHUB_ENV}
- name: publish docs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "Frank Hoffmann"
git config user.email "15r10nk@users.noreply.github.com"
git fetch origin gh-pages --depth=1
hatch run docs:mike deploy -u --push ${INLINE_SNAPSHOT_VERSION} latest
python-inline-snapshot-0.20.7/.github/workflows/test_docs.yml 0000664 0000000 0000000 00000000503 14764761403 0024353 0 ustar 00root root 0000000 0000000 name: test docs
on:
pull_request:
jobs:
build:
name: Deploy development docs
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: pip install hatch
- name: test docs
run: |
hatch run docs:build
python-inline-snapshot-0.20.7/.gitignore 0000664 0000000 0000000 00000001613 14764761403 0020237 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
.pytest_cache
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask instance folder
instance/
# Sphinx documentation
docs/_build/
# MkDocs documentation
/site/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
.mutmut-cache
html
python-inline-snapshot-0.20.7/.pre-commit-config.yaml 0000664 0000000 0000000 00000003761 14764761403 0022536 0 ustar 00root root 0000000 0000000 ci:
autofix_pr: false
autoupdate_commit_msg: 'style: pre-commit autoupdate'
autofix_commit_msg: 'style: pre-commit fixed styling'
autoupdate_schedule: monthly
repos:
- hooks:
- id: check-ast
- id: check-merge-conflict
- id: trailing-whitespace
- id: mixed-line-ending
- id: fix-byte-order-marker
- id: check-case-conflict
- id: check-json
- id: end-of-file-fixer
repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
- hooks:
- args:
- --in-place
- --expand-star-imports
- --remove-all-unused-imports
- --ignore-init-module-imports
id: autoflake
repo: https://github.com/myint/autoflake
rev: v2.3.1
- repo: local
hooks:
- id: replace-words
name: Replace Words
entry: python3 scripts/replace_words.py
language: system
files: \.(md|py)$
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort (python)
- hooks:
- args:
- --py38-plus
id: pyupgrade
repo: https://github.com/asottile/pyupgrade
rev: v3.17.0
- hooks:
- id: black
repo: https://github.com/psf/black
rev: 24.8.0
- hooks:
- id: blacken-docs
args: [-l80]
repo: https://github.com/adamchainz/blacken-docs
rev: 1.18.0
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.14.0
hooks:
- id: pretty-format-yaml
args: [--autofix, --indent, '2']
- hooks:
- id: commitizen
stages:
- commit-msg
repo: https://github.com/commitizen-tools/commitizen
rev: v3.28.0
# - repo: https://github.com/PyCQA/docformatter
# rev: v1.7.5
# hooks:
# - id: docformatter
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.23
hooks:
- id: validate-pyproject
# Optional extra validations from SchemaStore:
additional_dependencies: ['validate-pyproject-schema-store[all]']
- repo: https://github.com/rhysd/actionlint
rev: v1.7.4
hooks:
- id: actionlint
python-inline-snapshot-0.20.7/CHANGELOG.md 0000664 0000000 0000000 00000033526 14764761403 0020070 0 ustar 00root root 0000000 0000000
# 0.20.7 — 2025-03-14
## Changed
- Tests with failed snapshot comparisons now always result in a pytest `Error`, even if snapshots have been fixed or created.
# 0.20.6 — 2025-03-13
## Fixed
- Do not skip snapshots in conditional marked xfail tests.
``` python
@pytest.mark.xfail(False, reason="...")
def test_a():
assert 5 == snapshot(3) # <- this will be fixed
@pytest.mark.xfail(True, reason="...")
def test_b():
assert 5 == snapshot(3) # <- this not
```
# 0.20.5 — 2025-03-04
## Fixed
- correct normalization of "python3.9" to "python" in tests
# 0.20.4 — 2025-03-03
## Fixed
- Prevent a crash if a value cannot be copied and a UsageError is raised.
# 0.20.3 — 2025-02-28
## Fixed
- Use the black API directly to format python code.
This solves issues with the upcomming click 8.2.0 (#202) and problems in multithreading (https://github.com/15r10nk/inline-snapshot/pull/193#issuecomment-2660393512).
# 0.20.2 — 2025-02-13
## Fixed
- snapshots inside tests which are marked as xfail are now ignored (#184)
- Fixed a crash caused by the following code:
``` python
snapshot(tuple())
# or
snapshot(dict())
```
# 0.20.1 — 2025-02-04
## Fixed
- Fixed a windows bug with the readline module (#189)
# 0.20.0 — 2025-02-01
## Changed
- pytest assert rewriting works now together with inline-snapshot if you use `cpython>=3.11`
- `...` is now a special value to *create* snapshot values.
The value change in `assert [5,4] == snapshot([5,...])` is now a *create* (previously it was a *fix*)
## Fixed
- fixed some issues with dataclass arguments
- fixed an issue where --inline-snapshot=review discarded the user input and never formatted
the code if you used cpython 3.13.
# 0.19.3 — 2025-01-15
## Fixed
- raise no assertion for positional arguments inside constructor methods.
# 0.19.2 — 2025-01-15
## Fixed
- fixed a crash when you changed the snapshot to use a custom constructor method for dataclass/pydantic models.
example:
``` python
from inline_snapshot import snapshot
from pydantic import BaseModel
class A(BaseModel):
a: int
@classmethod
def from_str(cls, s):
return cls(a=int(s))
def test_something():
assert A(a=2) == snapshot(A.from_str("1"))
```
# 0.19.1 — 2025-01-12
## Added
- added the optional `inline-snapshot[dirty-equals]` dependency to depend on the dirty-equals version which works in combination with inline-snapshot.
## Fixed
- snapshots with pydantic models can now be compared multiple times
``` python
class A(BaseModel):
a: int
def test_something():
for _ in [1, 2]:
assert A(a=1) == snapshot(A(a=1))
```
# 0.19.0 — 2025-01-10
## Added
- You can now specify which tool you want to use to format your code by setting a `format-command` in your [configuration](https://15r10nk.github.io/inline-snapshot/latest/configuration/#format-command).
## Changed
- **BREAKING-CHANGE** you have to install `inline-snapshot[black]` now if you want to format your code like in the previous versions. This option is not required if you use a `format-command`.
## Fixed
- Load default config values even if `[tool.inline-snapshot]` is missing.
This makes the documented default shortcuts `--review` and `--fix` work.
# 0.18.2 — 2025-01-02
## Changed
- added `[dependency-groups]` to *pyproject.toml* and use uv and pytest to run tests in CI.
## Fixed
- use '.model_fields' on pydantic model class and not instance. This fixes a deprecation warning in the upcoming pydantic v2.11 (#169)
# 0.18.1 — 2024-12-22
## Fixed
- uv is now only used during test time if you run the inline-snapshot tests with `pytest --use-uv`
This solves a problem if you want to package inline-snapshot in distributions (#165)
# 0.18.0 — 2024-12-21
## Added
- Support for a new `storage-dir` configuration option, to tell inline-snapshot where to store data files such as external snapshots.
## Fixed
- pydantic v1 is supported again. pydantic v1 & v2 create now the same snapshots. You can use `.dict()` to get the same snapshots like in inline-snapshot-0.15.0 for pydantic v1.
``` python
class M(BaseModel):
name: str
def test_pydantic():
m = M(name="Tom")
assert m == snapshot(M(name="Tom"))
assert m.dict() == snapshot({"name": "Tom"})
```
- Find `pyproject.toml` file in parent directories, not just next to the Pytest configuration file.
# 0.17.1 — 2024-12-17
## Fixed
- Code generation for sets is now deterministic.
``` python
def test():
assert {1j, 2j, 1, 2, 3} == snapshot({1, 1j, 2, 2j, 3})
```
# 0.17.0 — 2024-12-14
## Added
- [attrs](https://www.attrs.org/en/stable/index.html) can now contain unmanaged values
``` python
import datetime as dt
import uuid
import attrs
from dirty_equals import IsDatetime
from inline_snapshot import Is, snapshot
@attrs.define
class Attrs:
ts: dt.datetime
id: uuid.UUID
def test():
id = uuid.uuid4()
assert Attrs(dt.datetime.now(), id) == snapshot(
Attrs(ts=IsDatetime(), id=Is(id))
)
```
# 0.16.0 — 2024-12-12
## Added
- [`inline_snapshot.extra.warns`](https://15r10nk.github.io/inline-snapshot/latest/extra/#inline_snapshot.extra.warns) to captures warnings and compares them against expected warnings.
``` python
def test_warns():
with warns(snapshot([(8, "UserWarning: some problem")]), include_line=True):
warn("some problem")
```
# 0.15.1 — 2024-12-10
## Fixed
- solved a bug caused by a variable inside a snapshot (#148)
# 0.15.0 — 2024-12-10
## Added
- snapshots [inside snapshots](https://15r10nk.github.io/inline-snapshot/latest/eq_snapshot/#inner-snapshots) are now supported.
``` python
assert get_schema() == snapshot(
[
{
"name": "var_1",
"type": snapshot("int") if version < 2 else snapshot("string"),
}
]
)
```
- [runtime values](https://15r10nk.github.io/inline-snapshot/latest/eq_snapshot/#is) can now be part of snapshots.
``` python
from inline_snapshot import snapshot, Is
current_version = "1.5"
assert request() == snapshot(
{"data": "page data", "version": Is(current_version)}
)
```
- [f-strings](https://15r10nk.github.io/inline-snapshot/latest/eq_snapshot/#f-strings) can now also be used within snapshots, but are currently not *fixed* by inline-snapshot.
## Changed
- *dirty-equals* expressions are now treated like *runtime values* or *snapshots* within snapshots and are not modified by inline-snapshot.
## Fixed
- inline-snapshot checks now if the given command line flags (`--inline-snapshot=...`) are valid
- `Example(...).run_pytest(raise=snapshot(...))` uses now the flags from the current run and not the flags from the Example.
# 0.14.2 — 2024-12-07
## Fixed
- do not crash when handling raw f-strings (`rf""`,`RF""`,...) (#143)
# 0.14.1 — 2024-12-04
## Fixed
- Don't crash for snapshots like `snapshot(f"")` (#139)
It first appeared with pytest-8.3.4, but already existed before for cpython-3.11.
f-strings in snapshots are currently not official supported, but they should not lead to crashes.
- skip formatting if black returns an error (#138)
# 0.14.0 — 2024-11-10
## Removed
- removed the `"Programming Language :: Python :: Implementation :: PyPy"` classifier which was incorrect, because inline-snapshot can not fix snapshots on pypy.
inline-snapshot now enforces `--inline-snapshot=disable` when used with an implementation other than cpython, which allows it to be used in packages that want to support pypy.
## Added
- command line shortcuts can be defined to simplify your workflows. `--review` and `--fix` are defined by default. See the [documentation](https://15r10nk.github.io/inline-snapshot/latest/configuration/) for details.
## Changed
- `--inline-snapshot=create/fix/trim/update` will no longer show reports for other categories.
You can use `--inline-snapshot=create,report` if you want to use the old behaviour.
# 0.13.4 — 2024-11-07
## Changed
- use tomli instead of toml (#130)
# 0.13.3 — 2024-09-24
## Fixed
- removed non-optional dirty-equals dependency (#118)
# 0.13.2 — 2024-09-24
## Changed
- star-expressions in list or dicts where never valid and cause a warning now.
```
other=[2]
assert [5,2]==snapshot([5,*other])
```
## Fixed
- A snapshot which contains an dirty-equals expression can now be compared multiple times.
``` python
def test_something():
greeting = "hello"
for name in ["alex", "bob"]:
assert (name, greeting) == snapshot((IsString(), "hello"))
```
## v0.13.1 (2024-09-18)
### Fix
- Use tomllib instead of PyPI toml on Python 3.11 and later
## v0.13.0 (2024-09-10)
### Feat
- added extra.prints
- 3.13 support
- strings with one line-break at the end become no multiline strings
## v0.12.1 (2024-08-05)
### Fix
- add license to project metadata and some other fixes in pyproject.toml (#104)
## v0.12.0 (2024-07-22)
### Feat
- implement extra.raises
- added inline_snapshot.testing.Example which can be used to test 3rd-party extensions
## v0.11.0 (2024-07-07)
### Feat
- check if the result of copy.deepcopy() is equal to the copied value
- support for `enum.Enum`, `enum.Flag`, `type` and omitting of default values (#73)
## v0.10.2 (2024-05-28)
### Fix
- changed how --inline-snapshot=disable works in combination with xdist (#90)
- fix typo, rename 'theme' with 'them'
## v0.10.1 (2024-05-26)
### Fix
- trigger no update for trailing comma changes
## v0.10.0 (2024-05-21)
### BREAKING CHANGE
- removed support for python 3.7
- removed `--inline-snapshot-disable` option and replaced it with `--inline-snapshot=disable`
### Feat
- new flags: *disable*, *short-report*, *report* and *review*
- added config option and environment variable to specify default flags
- show diff of changed snapshots in pytest report
- interactive *review* mode
## v0.9.0 (2024-05-07)
### Feat
- check if inline-snapshot is used in combination with xdist and notify the user that this is not possible
### Fix
- change the quoting of strings does not trigger an update
## v0.8.2 (2024-04-24)
### Fix
- removed restriction that the snapshot functions has to be called snapshot (#72)
- report error in tear down for sub-snapshots with missing values (#70)
- element access in sub-snapshots does not create new values
## v0.8.1 (2024-04-22)
### Fix
- make typing less strict
## v0.8.0 (2024-04-09)
### Feat
- prevent dirty-equal values from triggering of updates
- fix lists by calculating the alignment of the changed values
- insert dict items
- delete dict items
- preserve not changed dict-values and list-elements
### Fix
- update with UndecidedValue
- handle dicts with mulitple insertions and deletions
- handle lists with mulitple insertions and deletions
- fixed typing and coverage
### Refactor
- removed old _needs_* logic
- removed get_result
- use _get_changes api for DictValue
- use _get_changes api for CollectionValue
- use _get_changes api for MinMaxValue
- use _get_changes
- moved some functions
## v0.7.0 (2024-02-27)
### Feat
- removed old --update-snapshots option
## v0.6.1 (2024-01-28)
### Fix
- use utf-8 encoding to read and write source files
## v0.6.0 (2023-12-10)
### Feat
- store snapshot values in external files
## v0.5.2 (2023-11-13)
### Fix
- remove upper bound from dependency in pyproject.toml
## v0.5.1 (2023-10-20)
### Fix
- show better error messages
## v0.5.0 (2023-10-15)
### Feat
- support 3.12
### Fix
- do not change empty snapshot if it is not used
## v0.4.0 (2023-09-29)
### Feat
- escaped linebreak at the start/end of multiline strings
### Fix
- added py.typed
## v0.3.2 (2023-07-31)
### Fix
- handle update flag in sub-snapshots correctly
- fixed some edge cases where sub-snapshots had problems with some flags
- string literal concatenation should trigger no update
## v0.3.1 (2023-07-14)
### Fix
- added `__all__` to inline_snapshot
- flags fix/trim/create/update are changing the matching snapshots
## v0.3.0 (2023-07-12)
### BREAKING CHANGE
- values have to be copyable with `copy.deepcopy`
### Fix
- snapshot the current value of mutable objects
``` python
l = [1]
assert l == snapshot([1]) # old behaviour: snapshot([1, 2])
l.append(2)
assert l == snapshot([1, 2])
```
## v0.2.1 (2023-07-09)
### Fix
- black configuration files are respected
## v0.2.0 (2023-06-20)
### Feat
- `value <= snapshot()` to ensure that something gets smaller/larger over time (number of iterations of an algorithm you want to optimize for example),
- `value in snapshot()` to check if your value is in a known set of values,
- `snapshot()[key]` to generate new sub-snapshots on demand.
- convert strings with newlines to triple quoted strings
``` python
assert "a\nb\n" == snapshot(
"""a
b
"""
)
```
- preserve black formatting
## v0.1.2 (2022-12-11)
### Fix
- updated executing
## v0.1.1 (2022-12-08)
### Fix
- fixed typo in pytest plugin name
## v0.1.0 (2022-07-25)
### Feat
- first inline-snapshot version
python-inline-snapshot-0.20.7/CONTRIBUTING.md 0000664 0000000 0000000 00000002725 14764761403 0020505 0 ustar 00root root 0000000 0000000 # Contributing
Contributions are welcome.
Please create an issue before writing a pull request so we can discuss what needs to be changed.
# Testing
The code can be tested with [hatch](https://hatch.pypa.io/latest/tutorials/testing/overview/)
* `hatch test` can be used to test all supported python versions and to check for coverage.
* `hatch test -py 3.10 -- --sw` runs pytest for python 3.10 with the `--sw` argument.
The preferred way to test inline-snapshot is by using [`inline-snapshot.texting.Example`](https://15r10nk.github.io/inline-snapshot/latest/testing/).
You will see some other fixtures which are used inside the tests, but these are old ways to write the tests and I try to use the new `Example` class to write new tests.
# Coverage
This project has a hard coverage requirement of 100% (which is checked in CI).
You can also check the coverage locally with `hatch test -acp`.
The goal here is to find different edge cases which might have bugs.
However, it is possible to exclude some code from the coverage.
Code can be marked with `pragma: no cover`, if it can not be tested for some reason.
This makes it easy to spot uncovered code in the source.
Impossible conditions can be handled with `assert False`.
``` python
if some_condition:
...
if some_other_codition:
...
else:
assert False, "unreachable because ..."
```
This serves also as an additional check during runtime.
# Commits
Please use [pre-commit](https://pre-commit.com/) for your commits.
python-inline-snapshot-0.20.7/LICENSE 0000664 0000000 0000000 00000002072 14764761403 0017254 0 ustar 00root root 0000000 0000000
The MIT License (MIT)
Copyright (c) 2022 Frank Hoffmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
python-inline-snapshot-0.20.7/MANIFEST.in 0000664 0000000 0000000 00000000141 14764761403 0020000 0 ustar 00root root 0000000 0000000 include LICENSE
include README.rst
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
python-inline-snapshot-0.20.7/README.md 0000664 0000000 0000000 00000015310 14764761403 0017525 0 ustar 00root root 0000000 0000000

[](https://15r10nk.github.io/inline-snapshot/latest/)
[](https://pypi.org/project/inline-snapshot/)

[](https://pypacktrends.com/?packages=inline-snapshot&time_range=2years)
[](https://15r10nk.github.io/inline-snapshot/latest/contributing/#coverage)
[](https://github.com/sponsors/15r10nk)
## Installation
You can install "inline-snapshot" via [pip](https://pypi.org/project/pip/):
``` bash
pip install inline-snapshot
```
## Key Features
- **Intuitive Semantics:** `snapshot(x)` mirrors `x` for easy understanding.
- **Versatile Comparison Support:** Equipped with
[`x == snapshot(...)`](https://15r10nk.github.io/inline-snapshot/latest/eq_snapshot/),
[`x <= snapshot(...)`](https://15r10nk.github.io/inline-snapshot/latest/cmp_snapshot/),
[`x in snapshot(...)`](https://15r10nk.github.io/inline-snapshot/latest/in_snapshot/), and
[`snapshot(...)[key]`](https://15r10nk.github.io/inline-snapshot/latest/getitem_snapshot/).
- **Enhanced Control Flags:** Utilize various [flags](https://15r10nk.github.io/inline-snapshot/latest/pytest/) for precise control of which snapshots you want to change.
- **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/latest/customize_repr)
- **Nested Snapshot Support:** snapshots can contain [other snapshots](https://15r10nk.github.io/inline-snapshot/latest/eq_snapshot/#inner-snapshots)
- **Fuzzy Matching:** Incorporate [dirty-equals](https://15r10nk.github.io/inline-snapshot/latest/eq_snapshot/#dirty-equals) for flexible comparisons within snapshots.
- **Dynamic Snapshot Content:** snashots can contain [non-constant values](https://15r10nk.github.io/inline-snapshot/latest/eq_snapshot/#is)
- **Comprehensive Documentation:** Access detailed [documentation](https://15r10nk.github.io/inline-snapshot/latest) for complete guidance.
## Usage
You can use `snapshot()` instead of the value which you want to compare with.
``` python
from inline_snapshot import snapshot
def test_something():
assert 1548 * 18489 == snapshot()
```
You can now run the tests and record the correct values.
```
$ pytest --inline-snapshot=review
```
``` python hl_lines="5"
from inline_snapshot import snapshot
def test_something():
assert 1548 * 18489 == snapshot(28620972)
```
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/latest) if you want to know more.
``` python
from inline_snapshot import snapshot, outsource, external
def test_something():
for number in range(5):
# testing for numeric limits
assert number <= snapshot(4)
assert number >= snapshot(0)
for c in "hello world":
# test if something is part of a set
assert c in snapshot(["h", "e", "l", "o", " ", "w", "r", "d"])
s = snapshot(
{
0: {"square": 0, "pow_of_two": False},
1: {"square": 1, "pow_of_two": True},
2: {"square": 4, "pow_of_two": True},
3: {"square": 9, "pow_of_two": False},
4: {"square": 16, "pow_of_two": True},
}
)
for number in range(5):
# create sub-snapshots at runtime
assert s[number]["square"] == number**2
assert s[number]["pow_of_two"] == (
(number & (number - 1) == 0) and number != 0
)
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:
``` 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 "", line 1, in
ZeroDivisionError: division by zero
"""
),
)
```
## Feedback
inline-snapshot provides some advanced ways to work with snapshots.
I would like to know how these features are used to further improve this small library.
Let me know if you've found interesting use cases for this library via [twitter](https://twitter.com/15r10nk), [fosstodon](https://fosstodon.org/deck/@15r10nk) or in the github [discussions](https://github.com/15r10nk/inline-snapshot/discussions/new?category=show-and-tell).
## Sponsors
I would like to thank my sponsors. Without them, I would not be able to invest so much time in my projects.
### Bronze sponsor 🥉
## Issues
If you encounter any problems, please [report an issue](https://github.com/15r10nk/inline-snapshot/issues) along with a detailed description.
## License
Distributed under the terms of the [MIT](http://opensource.org/licenses/MIT) license, "inline-snapshot" is free and open source software.
python-inline-snapshot-0.20.7/changelog.d/ 0000775 0000000 0000000 00000000000 14764761403 0020417 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/changelog.d/.gitkeep 0000664 0000000 0000000 00000000000 14764761403 0022036 0 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/conftest.py 0000664 0000000 0000000 00000000547 14764761403 0020453 0 ustar 00root root 0000000 0000000 import pytest
from tests.utils import snapshot_env
from tests.utils import storage # noqa
from tests.utils import useStorage
@pytest.fixture(autouse=True)
def snapshot_env_for_doctest(request, storage):
if hasattr(request.node, "dtest"):
with snapshot_env():
with useStorage(storage):
yield
else:
yield
python-inline-snapshot-0.20.7/docs/ 0000775 0000000 0000000 00000000000 14764761403 0017176 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/docs/assets/ 0000775 0000000 0000000 00000000000 14764761403 0020500 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/docs/assets/favicon.svg 0000664 0000000 0000000 00000010123 14764761403 0022643 0 ustar 00root root 0000000 0000000
python-inline-snapshot-0.20.7/docs/assets/logo.svg 0000664 0000000 0000000 00000121734 14764761403 0022171 0 ustar 00root root 0000000 0000000
python-inline-snapshot-0.20.7/docs/assets/logo_orig.svg 0000664 0000000 0000000 00000017232 14764761403 0023206 0 ustar 00root root 0000000 0000000
python-inline-snapshot-0.20.7/docs/categories.md 0000664 0000000 0000000 00000013560 14764761403 0021652 0 ustar 00root root 0000000 0000000
Each snapshot change is assigned to a different category. This is done because inline-snapshot supports more than just `==` checks.
There are changes which:
* [create](#create) new snapshot values
* [fix](#fix) your tests
* [update](#update) only the syntax to a new representation
* [trim](#trim) unused pieces from your snapshots
*Create* and *fix* are mainly used, but it is good to know what type of change you are approving, because it helps with the decision if this changes should be applied.
## Categories
### Create
These changes are made when new snapshots are created.
The result of each comparison is `True`, which allows to run the whole test to fill all new snapshots with values.
Example:
### Fix
These changes are made when the snapshots comparison does not return `True` any more (depending on the operation `==`, `<=`, `in`).
The result of each comparison is `True` if you change something from this category, which allows to run the whole test and to fix other snapshots.
!!! info
The main reason for the different categories is to make the number of changes in the **fix** category as small as possible.
The changes in the **fix** category are the only changes which change the value of the snapshots and should be reviewed carefully.
### Trim
These changes are made when parts of the snapshots are removed which are no longer needed, or if limits can be reduced.
``` python
from inline_snapshot import snapshot
def test_something():
assert 2 <= snapshot(8)
assert 8 in snapshot([5, 8])
s = snapshot({"key1": 1, "key2": 2})
assert 2 == s["key2"]
```
``` python hl_lines="5 7 9"
from inline_snapshot import snapshot
def test_something():
assert 2 <= snapshot(2)
assert 8 in snapshot([8])
s = snapshot({"key2": 2})
assert 2 == s["key2"]
```
There might be problems in cases where you use the same snapshot in different tests, run only one test and trim the snapshot with `pytest -k test_a --inline-snapshot=trim` in this case:
``` python
from inline_snapshot import snapshot
s = snapshot(5)
def test_a():
assert 2 <= s
def test_b():
assert 5 <= s
```
``` python hl_lines="1"
from inline_snapshot import snapshot
s = snapshot(2)
def test_a():
assert 2 <= s
def test_b():
assert 5 <= s
```
The value of the snapshot is reduced to `2`, because `test_a()` was the only test running and inline-snapshot does not know about `5 <= s`.
It is recommended to use trim only if you run your complete test suite.
### Update
Changes in the update category do not change the value in the code, just the representation. The reason might be that `#!python repr()` of the object has changed or that inline-snapshot provides some new logic which changes the representation. Like with the strings in the following example:
=== "original"
``` python
from inline_snapshot import snapshot
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __repr__(self):
# return f"Vector(x={self.x}, y={self.y})"
return f"Vector({self.x}, {self.y})"
def test_something():
assert "a\nb\nc\n" == snapshot("a\nb\nc\n")
assert 5 == snapshot(4 + 1)
assert Vector(1, 2) == snapshot(Vector(x=1, y=2))
```
=== "--inline-snapshot=update"
``` python hl_lines="20 21 22 23 24 25 26 28 30"
from inline_snapshot import snapshot
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __repr__(self):
# return f"Vector(x={self.x}, y={self.y})"
return f"Vector({self.x}, {self.y})"
def test_something():
assert "a\nb\nc\n" == snapshot(
"""\
a
b
c
"""
)
assert 5 == snapshot(5)
assert Vector(1, 2) == snapshot(Vector(1, 2))
```
The approval of this type of changes is easier, because inline-snapshot assures that the value has not changed.
python-inline-snapshot-0.20.7/docs/changelog.md 0000664 0000000 0000000 00000000636 14764761403 0021454 0 ustar 00root root 0000000 0000000
``` python exec="1"
from pathlib import Path
from subprocess import run
new_changes = list(Path.cwd().glob("changelog.d/*.md"))
next_version = (
run(["cz", "bump", "--get-next"], capture_output=True)
.stdout.decode()
.strip()
)
if new_changes:
print(f"# upcomming version ({next_version})")
for file in new_changes:
print(file.read_text().replace("###", "##"))
```
--8<-- "CHANGELOG.md"
python-inline-snapshot-0.20.7/docs/cmp_snapshot.md 0000664 0000000 0000000 00000006661 14764761403 0022227 0 ustar 00root root 0000000 0000000 ## General
A snapshot can be compared against any value with `<=` or `>=`.
This can be used to create a upper/lower bound for some result.
The snapshot value can be trimmed to the lowest/largest valid value.
Example:
=== "original code"
``` python
from inline_snapshot import snapshot
def gcd(x, y):
iterations = 0
if x > y:
small = y
else:
small = x
for i in range(1, small + 1):
iterations += 1
if (x % i == 0) and (y % i == 0):
gcd = i
return gcd, iterations
def test_gcd():
result, iterations = gcd(12, 18)
assert result == snapshot()
assert iterations <= snapshot()
```
=== "--inline-snapshot=create"
``` python hl_lines="21 22"
from inline_snapshot import snapshot
def gcd(x, y):
iterations = 0
if x > y:
small = y
else:
small = x
for i in range(1, small + 1):
iterations += 1
if (x % i == 0) and (y % i == 0):
gcd = i
return gcd, iterations
def test_gcd():
result, iterations = gcd(12, 18)
assert result == snapshot(6)
assert iterations <= snapshot(12)
```
=== "optimized code "
``` python hl_lines="5 7 9 10"
from inline_snapshot import snapshot
def gcd(x, y):
# use Euclidean Algorithm
iterations = 0
while y:
iterations += 1
x, y = y, x % y
return abs(x), iterations
def test_gcd():
result, iterations = gcd(12, 18)
assert result == snapshot(6)
assert iterations <= snapshot(12)
```
=== "--inline-snapshot=trim"
``` python hl_lines="17"
from inline_snapshot import snapshot
def gcd(x, y):
# use Euclidean Algorithm
iterations = 0
while y:
iterations += 1
x, y = y, x % y
return abs(x), iterations
def test_gcd():
result, iterations = gcd(12, 18)
assert result == snapshot(6)
assert iterations <= snapshot(3)
```
!!! warning
This should not be used to check for any flaky values like the runtime of some code, because it will randomly break your tests.
The same snapshot value can also be used in multiple assertions.
=== "original code"
``` python
from inline_snapshot import snapshot
def test_something():
value = snapshot()
assert 5 <= value
assert 6 <= value
```
=== "--inline-snapshot=create"
``` python hl_lines="5"
from inline_snapshot import snapshot
def test_something():
value = snapshot(6)
assert 5 <= value
assert 6 <= value
```
## pytest options
It interacts with the following `--inline-snapshot` flags:
- `create` create a new value if the snapshot value is undefined.
- `fix` record the new value and store it in the source code if it is contradicts the comparison.
- `trim` record the new value and store it in the source code if it is more strict than the old one.
python-inline-snapshot-0.20.7/docs/code_generation.md 0000664 0000000 0000000 00000010327 14764761403 0022650 0 ustar 00root root 0000000 0000000
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"
``` 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"
``` python hl_lines="18 19 20 21 22 23 24 25 26 27 28"
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))
* or a special internal implementation for container types to support [unmanaged snapshot values](eq_snapshot.md#unmanaged-snapshot-values).
This can currently not be customized.
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"
``` python
from inline_snapshot import snapshot
def test_something():
assert "first line\nsecond line" == snapshot(
"""first line
second line"""
)
```
=== "--inline-snapshot=update"
``` python hl_lines="6 7 8 9"
from inline_snapshot import snapshot
def test_something():
assert "first line\nsecond line" == snapshot(
"""\
first line
second line\
"""
)
```
4. The new code fragments are formatted with black if it is installed.
!!! note
Black is an optional dependency since inline-snapshot v0.19.0.
You can install it with:
``` sh
pip install inline-snapshot[black]
```
5. The whole file is formatted
* with black if it was formatted with black 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.
**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.
**Solution:** specify which black version inline-snapshot should use by adding black with a specific version to your dependencies.
3. black is not installed. Black is an optional dependency since inline-snapshot v0.19.0
* or with the [format-command][format-command] if you defined one.
python-inline-snapshot-0.20.7/docs/configuration.md 0000664 0000000 0000000 00000003525 14764761403 0022374 0 ustar 00root root 0000000 0000000 Default configuration:
``` toml
[tool.inline-snapshot]
hash-length=15
default-flags=["short-report"]
format-command=""
[tool.inline-snapshot.shortcuts]
review=["review"]
fix=["create","fix"]
```
* **hash-length:** specifies the length of the hash used by `external()` in the code representation.
This does not affect the hash length used to store the data.
The hash should be long enough to avoid hash collisions.
* **default-flags:** defines which flags should be used if there are no flags specified with `--inline-snapshot=...`.
You can also use the environment variable `INLINE_SNAPSHOT_DEFAULT_FLAGS=...` to specify the flags and to override those in the configuration file.
* **shortcuts:** allows you to define custom commands to simplify your workflows.
`--fix` and `--review` are defined by default, but this configuration can be changed to fit your needs.
* **storage-dir:** allows you to define the directory where inline-snapshot stores data files such as external snapshots.
By default, it will be `/.inline-snapshot`,
where `` is replaced by the directory containing the Pytest configuration file, if any.
External snapshots will be stored in the `external` subfolder of the storage directory.
* **format-command:[](){#format-command}** allows you to specify a custom command which is used to format the python code after code is changed.
``` toml
[tool.inline-snapshot]
format-command="ruff format --stdin-filename {filename}"
```
The placeholder `{filename}` can be used to specify the filename if it is needed to find the correct formatting options for this file.
!!! important
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*.
python-inline-snapshot-0.20.7/docs/contributing.md 0000664 0000000 0000000 00000000031 14764761403 0022221 0 ustar 00root root 0000000 0000000 --8<-- "CONTRIBUTING.md"
python-inline-snapshot-0.20.7/docs/customize_repr.md 0000664 0000000 0000000 00000006465 14764761403 0022605 0 ustar 00root root 0000000 0000000
`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)
""
>>> from enum import Enum
>>> E = Enum("E", ["a", "b"])
>>> repr(E.a)
''
```
`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<-- "src/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.
``` python
from inline_snapshot import snapshot
from enum import Enum
def test_enum():
E = Enum("E", ["a", "b"])
# normal repr
assert repr(E.a) == ""
# 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}`")
```
!!! note
Container types like `dict`, `list`, `tuple` or `dataclass` are handled in a different way, because inline-snapshot also needs to inspect these types to implement [unmanaged](/eq_snapshot.md#unmanaged-snapshot-values) snapshot values.
## customize recursive repr
You can also use `repr()` inside `__repr__()`, if you want to make your own type compatible with inline-snapshot.
``` python
from inline_snapshot import snapshot
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]))
```
!!! note
using `#!python f"{obj!r}"` or `#!c PyObject_Repr()` will not work, because inline-snapshot replaces `#!python builtins.repr` during the code generation. The only way to use the custom repr implementation is to use the `repr()` function.
!!! note
This implementation allows inline-snapshot to use the custom `repr()` recursively, but it does not allow you to use [unmanaged](/eq_snapshot.md#unmanaged-snapshot-values) snapshot values like `#!python Pair(Is(some_var),5)`
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)})"
```
python-inline-snapshot-0.20.7/docs/eq_snapshot.md 0000664 0000000 0000000 00000027662 14764761403 0022061 0 ustar 00root root 0000000 0000000 ## General
A snapshot can be compared with any value using `==`.
The value can be recorded with `--inline-snapshot=create` if the snapshot is empty.
The value can later be changed with `--inline-snapshot=fix` if the value the snapshot is compared with has changed.
Example:
=== "original code"
``` python
from inline_snapshot import snapshot
def test_something():
assert 2 + 4 == snapshot()
```
=== "--inline-snapshot=create"
``` python hl_lines="5"
from inline_snapshot import snapshot
def test_something():
assert 2 + 4 == snapshot(6)
```
=== "value changed"
``` python hl_lines="5"
from inline_snapshot import snapshot
def test_something():
assert 2 + 40 == snapshot(4)
```
=== "--inline-snapshot=fix"
``` python hl_lines="5"
from inline_snapshot import snapshot
def test_something():
assert 2 + 40 == snapshot(42)
```
## unmanaged snapshot values
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 the control over the snapshot content back.
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](#dirty-equals) expressions,
* [dynamic code](#is) inside `Is(...)`,
* [snapshots](#inner-snapshots) inside snapshots and
* [f-strings](#f-strings).
inline-snapshot is able to handle these types within the following containers:
* list
* tuple
* dict
* [namedtuple](https://docs.python.org/3/library/collections.html#collections.namedtuple)
* [dataclass](https://docs.python.org/3/library/dataclasses.html)
* [attrs](https://www.attrs.org/en/stable/index.html)
Other types are converted with a [customizable](customize_repr.md) `repr()` into code. It is not possible to use unmanaged snapshot values within these objects.
### dirty-equals
!!! note
Use the optional *dirty-equals* dependency to install the version that works best in combination with inline-snapshot.
``` sh
pip install inline-snapshot[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.
Example:
=== "original code"
``` python
from inline_snapshot import snapshot
import datetime
def get_data():
return {
"date": datetime.datetime.utcnow(),
"payload": "some data",
}
def test_function():
assert get_data() == snapshot()
```
=== "--inline-snapshot=create"
``` python hl_lines="13 14 15"
from inline_snapshot import snapshot
import datetime
def get_data():
return {
"date": datetime.datetime.utcnow(),
"payload": "some data",
}
def test_function():
assert get_data() == snapshot(
{"date": datetime.datetime(2024, 3, 14, 0, 0), "payload": "some data"}
)
```
The date can be replaced with the [dirty-equals](https://dirty-equals.helpmanual.io/latest/) expression `IsDatetime()`.
Example:
=== "using IsDatetime()"
``` python
from inline_snapshot import snapshot
from dirty_equals import IsDatetime
import datetime
def get_data():
return {
"date": datetime.datetime.utcnow(),
"payload": "some data",
}
def test_function():
assert get_data() == snapshot(
{
"date": IsDatetime(),
"payload": "some data",
}
)
```
=== "changed payload"
``` python hl_lines="9"
from inline_snapshot import snapshot
from dirty_equals import IsDatetime
import datetime
def get_data():
return {
"date": datetime.datetime.utcnow(),
"payload": "data changed for some good reason",
}
def test_function():
assert get_data() == snapshot(
{
"date": IsDatetime(),
"payload": "some data",
}
)
```
=== "--inline-snapshot=fix"
``` python hl_lines="17"
from inline_snapshot import snapshot
from dirty_equals import IsDatetime
import datetime
def get_data():
return {
"date": datetime.datetime.utcnow(),
"payload": "data changed for some good reason",
}
def test_function():
assert get_data() == snapshot(
{
"date": IsDatetime(),
"payload": "data changed for some good reason",
}
)
```
`snapshot()` can also be used inside dirty-equals expressions.
One useful example is `IsJson()`:
=== "using IsJson()"
``` python
from dirty_equals import IsJson
from inline_snapshot import snapshot
def test_foo():
assert {"json_data": '{"value": 1}'} == snapshot(
{"json_data": IsJson(snapshot())}
)
```
=== "--inline-snapshot=create"
``` python hl_lines="7"
from dirty_equals import IsJson
from inline_snapshot import snapshot
def test_foo():
assert {"json_data": '{"value": 1}'} == snapshot(
{"json_data": IsJson(snapshot({"value": 1}))}
)
```
The general rule is that functions to which you pass a *snapshot* as an argument can only use `==` (or other snapshot operations) on this argument.
!!! important
You cannot use a *snapshot* for every dirty equals argument, but only for those that also support dirty equals expressions.
### 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.
``` 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 snapshot does not need to be fixed when `current_version` changes in the future, but `"page data"` will still be fixed if it changes.
`Is()` can also be used when the snapshot is evaluated multiple times, which is useful in loops or parametrized tests.
=== "original code"
``` python
from inline_snapshot import snapshot, Is
def test_function():
for c in "abc":
assert [c, "correct"] == snapshot([Is(c), "wrong"])
```
=== "--inline-snapshot=fix"
``` 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 also possible to use snapshots inside snapshots.
This is useful 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.py v1"
``` python
version = 1
def get_schema():
return [{"name": "var_1", "type": "int"}]
```
=== "my_lib.py v2"
``` python
version = 2
def get_schema():
return [{"name": "var_1", "type": "string"}]
```
``` 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.
You can also extract the version logic into its own function.
``` python
from inline_snapshot import snapshot, Snapshot
from my_lib import version, get_schema
def version_snapshot(v1: Snapshot, v2: Snapshot):
return v1 if version < 2 else v2
def test_function():
assert get_schema() == snapshot(
[
{
"name": "var_1",
"type": version_snapshot(
v1=snapshot("int"), v2=snapshot("string")
),
}
]
)
```
#### common snapshot parts
Another use case is the extraction of common snapshot parts into an extra snapshot:
``` 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.
### f-strings
*f-strings* are not generated by inline-snapshot, but they can be used in snapshots if you want to replace some dynamic part of a string value.
``` python
from inline_snapshot import snapshot
def get_error():
# example code which generates an error message
return __file__ + ": error at line 5"
def test_get_error():
assert get_error() == snapshot(f"{__file__}: error at line 5")
```
It is not required to wrap the changed value in `Is(f"...")`, because inline-snapshot knows that *f-strings* are only generated by the developer.
!!! Warning "Limitation"
inline-snapshot is currently not able to fix the string constants within *f-strings*.
`#!python f"...{var}..."` works **currently** like `#!python Is(f"...{var}...")` and issues a warning if the value changes, giving you the opportunity to fix your f-string.
`#!python f"...{var}..."` will in the **future** work like `#!python f"...{Is(var)}"`. inline-snapshot will then be able to *fix* the string parts within the f-string.
## pytest options
It interacts with the following `--inline-snapshot` flags:
- `create` create a new value if the snapshot value is undefined.
- `fix` record the value parts and store them in the source code if it is different from the current one.
- `update` update parts of the value if their representation has changed.
Parts which are replaced with dirty-equals expressions are not updated.
python-inline-snapshot-0.20.7/docs/extra.md 0000664 0000000 0000000 00000000166 14764761403 0020646 0 ustar 00root root 0000000 0000000
::: inline_snapshot.extra
options:
heading_level: 1
show_root_heading: true
show_source: true
python-inline-snapshot-0.20.7/docs/getitem_snapshot.md 0000664 0000000 0000000 00000002140 14764761403 0023072 0 ustar 00root root 0000000 0000000 ## General
It is possible to generate sub-snapshots during runtime.
This sub-snapshots can be used like a normal snapshot.
Example:
=== "original code"
``` python
from inline_snapshot import snapshot
def test_something():
s = snapshot()
assert s["a"] == 4
assert s["b"] == 5
```
=== "--inline-snapshot=create"
``` python hl_lines="5"
from inline_snapshot import snapshot
def test_something():
s = snapshot({"a": 4, "b": 5})
assert s["a"] == 4
assert s["b"] == 5
```
`s[key]` can be used with every normal snapshot operation including `s[key1][key2]`.
## pytest options
It interacts with the following `--inline-snapshot` flags:
- `create` create a new value if the snapshot value is undefined or create a new sub-snapshot if one is missing.
- `trim` remove sub-snapshots if they are not needed any more.
The flags `fix` and `update` are applied recursive to all sub-snapshots.
python-inline-snapshot-0.20.7/docs/in_snapshot.md 0000664 0000000 0000000 00000002144 14764761403 0022046 0 ustar 00root root 0000000 0000000 ## General
It is possible to check if an value is in a snapshot. The value of the generated snapshot will be a list of all values which are tested.
Example:
=== "original code"
``` python
from inline_snapshot import snapshot
def test_something():
s = snapshot()
assert 5 in s
assert 5 in s
assert 8 in s
for v in ["a", "b"]:
assert v in s
```
=== "--inline-snapshot=create"
``` python hl_lines="5"
from inline_snapshot import snapshot
def test_something():
s = snapshot([5, 8, "a", "b"])
assert 5 in s
assert 5 in s
assert 8 in s
for v in ["a", "b"]:
assert v in s
```
## pytest options
It interacts with the following `--inline-snapshot` flags:
- `create` create a new value if the snapshot value is undefined.
- `fix` adds a value to the list if it is missing.
- `trim` removes a value from the list if it is not necessary.
python-inline-snapshot-0.20.7/docs/index.md 0000664 0000000 0000000 00000013423 14764761403 0020632 0 ustar 00root root 0000000 0000000
--8<-- "README.md:Header"
# Welcome to inline-snapshot
inline-snapshot can be used for different things:
* golden master/approval/snapshot testing.
The idea is that you have a function with a currently unknown result and you want to write a tests, which ensures that the result does not change during refactoring.
* Compare things which are complex like lists with lot of numbers or complex data structures.
* Things which might change during the development like error messages.
`inline-snapshot` automates the process of recording, storing and updating the value you want to compare with.
The value is converted with `repr()` and stored in the source file as argument of the `snapshot()` function.
## Usage
You can use `snapshot()` instead of the value which you want to compare with and run the tests to record the correct values.
=== "original code"
``` python
from inline_snapshot import snapshot
def something():
return 1548 * 18489
def test_something():
assert something() == snapshot()
```
=== "--inline-snapshot=create"
``` python hl_lines="9"
from inline_snapshot import snapshot
def something():
return 1548 * 18489
def test_something():
assert something() == snapshot(28620972)
```
Your tests will break, if you change your code by adding `// 18`.
Maybe that is correct and you should fix your code, or
your code is correct and you want to update your test results.
=== "changed code"
``` python hl_lines="5"
from inline_snapshot import snapshot
def something():
return (1548 * 18489) // 18
def test_something():
assert something() == snapshot(28620972)
```
=== "--inline-snapshot=fix"
``` python hl_lines="9"
from inline_snapshot import snapshot
def something():
return (1548 * 18489) // 18
def test_something():
assert something() == snapshot(1590054)
```
Please verify the new results. `git diff` will give you a good overview over all changed results.
Use `pytest -k test_something --inline-snapshot=fix` if you only want to change one test.
## Supported operations
You can use `snapshot(x)` like you can use `x` in your assertion with a limited set of operations:
- [`value == snapshot()`](eq_snapshot.md) to compare with something,
- [`value <= snapshot()`](cmp_snapshot.md) to ensure that something gets smaller/larger over time (number of iterations of an algorithm you want to optimize for example),
- [`value in snapshot()`](in_snapshot.md) to check if your value is in a known set of values,
- [`snapshot()[key]`](getitem_snapshot.md) to generate new sub-snapshots on demand.
!!! warning
One snapshot can only be used with one operation.
The following code will not work:
``` python
from inline_snapshot import snapshot
def test_something():
s = snapshot(5)
assert 5 <= s
assert 5 == s
# Error:
# > assert 5 == s
# E TypeError: This snapshot cannot be use with `==`, because it was previously used with `x <= snapshot`
```
## Supported usage
It is possible to place `snapshot()` anywhere in the tests and reuse it multiple times.
=== "original code"
``` python
from inline_snapshot import snapshot
def something():
return 21 * 2
result = snapshot()
def test_something():
...
assert something() == result
def test_something_again():
...
assert something() == result
```
=== "--inline-snapshot=create"
``` python hl_lines="8"
from inline_snapshot import snapshot
def something():
return 21 * 2
result = snapshot(42)
def test_something():
...
assert something() == result
def test_something_again():
...
assert something() == result
```
`snapshot()` can also be used in loops:
=== "original code"
``` python
from inline_snapshot import snapshot
def test_loop():
for name in ["Mia", "Eva", "Leo"]:
assert len(name) == snapshot()
```
=== "--inline-snapshot=create"
``` python hl_lines="6"
from inline_snapshot import snapshot
def test_loop():
for name in ["Mia", "Eva", "Leo"]:
assert len(name) == snapshot(3)
```
or passed as an argument to a function:
=== "original code"
``` python
from inline_snapshot import snapshot
def check_string_len(string, snapshot_value):
assert len(string) == snapshot_value
def test_string_len():
check_string_len("abc", snapshot())
check_string_len("1234", snapshot())
check_string_len(".......", snapshot())
```
=== "--inline-snapshot=create"
``` python hl_lines="9 10 11"
from inline_snapshot import snapshot
def check_string_len(string, snapshot_value):
assert len(string) == snapshot_value
def test_string_len():
check_string_len("abc", snapshot(3))
check_string_len("1234", snapshot(4))
check_string_len(".......", snapshot(7))
```
--8<-- "README.md:Feedback"
python-inline-snapshot-0.20.7/docs/limitations.md 0000664 0000000 0000000 00000000706 14764761403 0022057 0 ustar 00root root 0000000 0000000
## pytest assert rewriting is disabled
inline-snapshot must disable pytest assert-rewriting if you use report/review/create/fix/trim/update flags.
## xdist is not supported
You can not use inline-snapshot in combination with `pytest-xdist`. The use of `-n=...` implies `--inline-snapshot=disable`.
## works only with cpython
inline-snapshot works currently only with cpython. `--inline-snapshot=disable` is enforced for every other implementation.
python-inline-snapshot-0.20.7/docs/outsource.md 0000664 0000000 0000000 00000005466 14764761403 0021563 0 ustar 00root root 0000000 0000000 ## General
!!! info
This feature is currently under development. See this [issue](https://github.com/15r10nk/inline-snapshot/issues/86) for more information.
Storing snapshots in the source code is the main feature of inline snapshots.
This has the advantage that you can easily see changes in code reviews. But it also has some problems:
* It is problematic to snapshot a lot of data, because it takes up a lot of space in your tests.
* Binary data or images are not readable in your tests.
The `outsource()` function solves this problem and integrates itself nicely with the inline snapshot.
It stores the data in a special `external()` object that can be compared in snapshots.
The object is represented by the hash of the data.
The actual data is stored in a separate file in your project.
This allows the test to be renamed and moved around in your code without losing the connection to the stored data.
Example:
=== "original code"
``` python
from inline_snapshot import snapshot, outsource
def test_something():
assert outsource("long text\n" * 1000) == snapshot()
```
=== "--inline-snapshot=create"
``` python hl_lines="3 4 7 8 9"
from inline_snapshot import snapshot, outsource
from inline_snapshot import external
def test_something():
assert outsource("long text\n" * 1000) == snapshot(
external("f5a956460453*.txt")
)
```
The `external` object can be used inside other data structures.
=== "original code"
``` python
from inline_snapshot import snapshot, outsource
def test_something():
assert [
outsource("long text\n" * times) for times in [50, 100, 1000]
] == snapshot()
```
=== "--inline-snapshot=create"
``` python hl_lines="3 4 9 10 11 12 13 14 15"
from inline_snapshot import snapshot, outsource
from inline_snapshot import external
def test_something():
assert [
outsource("long text\n" * times) for times in [50, 100, 1000]
] == snapshot(
[
external("362ad8374ed6*.txt"),
external("5755afea3f8d*.txt"),
external("f5a956460453*.txt"),
]
)
```
## API
::: inline_snapshot.outsource
options:
show_root_heading: true
::: inline_snapshot.external
options:
show_root_heading: true
## pytest options
It interacts with the following `--inline-snapshot` flags:
- `trim` removes every snapshots form the storage which is not referenced with `external(...)` in the code.
python-inline-snapshot-0.20.7/docs/plugins/ 0000775 0000000 0000000 00000000000 14764761403 0020657 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/docs/plugins/pyproject.toml 0000664 0000000 0000000 00000001425 14764761403 0023575 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]
[project]
authors = [
{name = "Frank Hoffmann", email = "15r10nk@polarbit.de"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy"
]
dependencies = [
"mkdocs"
]
license = "MIT"
name = "replace-url"
requires-python = ">=3.8"
version = "0.3.0"
[project.entry-points."mkdocs.plugins"]
replace-url = "replace_url:ReplaceUrlPlugin"
python-inline-snapshot-0.20.7/docs/plugins/replace_url.py 0000664 0000000 0000000 00000000262 14764761403 0023526 0 ustar 00root root 0000000 0000000 import mkdocs
class ReplaceUrlPlugin(mkdocs.plugins.BasePlugin):
def on_page_content(self, html, page, config, files):
return html.replace("docs/assets", "assets")
python-inline-snapshot-0.20.7/docs/pytest.md 0000664 0000000 0000000 00000006573 14764761403 0021063 0 ustar 00root root 0000000 0000000
inline-snapshot provides one pytest option with different flags (*create*,
*fix*,
*trim*,
*update*,
*short-report*,
*report*,
*disable*).
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
from inline_snapshot import snapshot
def test_something():
assert 1 == snapshot(5)
assert 2 <= snapshot(5)
```
!!! note
Every flag with the exception of *disable* and *short-report* disables the pytest assert-rewriting.
## --inline-snapshot=create,fix,trim,update
Approve the changes of the given [category](categories.md).
These flags can be combined with *report* and *review*.
``` python title="test_something.py"
from inline_snapshot import snapshot
def test_something():
assert 1 == snapshot()
assert 2 <= snapshot(5)
```
```bash exec="1" title="something" result="ansi"
cd $(mktemp -d)
export FORCE_COLOR=256
export COLUMNS=80
function run(){
echo -en "\x1b[1;34m> "
echo $@
echo -en "\x1b[0m"
$@
echo
}
black -q - > test_something.py << EOF
from inline_snapshot import snapshot
def test_something():
assert 1 == snapshot()
assert 2 <= snapshot(5)
EOF
run pytest test_something.py --inline-snapshot=create,report
```
## --inline-snapshot=short-report
give a short report over which changes can be made to the snapshots
```bash exec="1" title="something" result="ansi"
cd $(mktemp -d)
export FORCE_COLOR=256
export COLUMNS=80
function run(){
echo -en "\x1b[1;34m> "
echo $@
echo -en "\x1b[0m"
python -m $@
echo
}
black -q - > test_something.py << EOF
from inline_snapshot import snapshot
def test_something():
assert 1 == snapshot()
assert 2 <= snapshot(5)
EOF
run pytest test_something.py --inline-snapshot=short-report
```
!!! info
short-report exists mainly to show that snapshots have changed with enabled pytest assert-rewriting.
This option will be replaced with *report* when this restriction is lifted.
## --inline-snapshot=report
Shows a diff report over which changes can be made to the snapshots
```bash exec="1" title="something" result="ansi"
cd $(mktemp -d)
export FORCE_COLOR=256
export COLUMNS=80
function run(){
echo -en "\x1b[1;34m> "
echo $@
echo -en "\x1b[0m"
$@
echo
}
black -q - > test_something.py << EOF
from inline_snapshot import snapshot
def test_something():
assert 1 == snapshot()
assert 2 <= snapshot(5)
EOF
run pytest test_something.py --inline-snapshot=report
```
## --inline-snapshot=review
Shows a diff report for each category and ask if you want to apply the changes
```bash exec="1" title="something" result="ansi"
cd $(mktemp -d)
export FORCE_COLOR=256
export COLUMNS=80
function run(){
echo -en "\x1b[1;34m> "
echo $@
echo -en "\x1b[0m"
$@
echo
}
black -q - > test_something.py << EOF
from inline_snapshot import snapshot
def test_something():
assert 1 == snapshot()
assert 2 <= snapshot(5)
EOF
yes | run pytest test_something.py --inline-snapshot=review
```
## --inline-snapshot=disable
Disables all the snapshot logic. `snapshot(x)` will just return `x`.
This can be used if you think exclude that snapshot logic causes a problem in your tests, or if you want to speedup your CI.
!!! info "deprecation"
This option was previously called `--inline-snapshot-disable`
python-inline-snapshot-0.20.7/docs/testing.md 0000664 0000000 0000000 00000005206 14764761403 0021200 0 ustar 00root root 0000000 0000000
`inline_snapshot.testing` provides tools which can be used to test inline-snapshot workflows.
This might be useful if you want to build your own libraries based on inline-snapshot.
The following example shows how you can use the `Example` class to test what inline-snapshot would do with given the source code. The snapshots in the argument are asserted inside the `run_*` methods, but only when they are provided.
=== "original"
``` python
from inline_snapshot.testing import Example
from inline_snapshot import snapshot
def test_something():
Example(
{
"test_a.py": """\
from inline_snapshot import snapshot
def test_a():
assert 1+1 == snapshot()
"""
}
).run_inline( # run without flags
reported_categories=snapshot(),
).run_pytest( # run without flags and check the pytest report
changed_files=snapshot(),
report=snapshot(),
).run_pytest( # run with create flag and check the changed files
["--inline-snapshot=create"],
changed_files=snapshot(),
)
```
=== "--inline-snapshot=create"
``` python hl_lines="16 18 19 20 21 22 23 24 27 28 29 30 31 32 33 34 35"
from inline_snapshot.testing import Example
from inline_snapshot import snapshot
def test_something():
Example(
{
"test_a.py": """\
from inline_snapshot import snapshot
def test_a():
assert 1+1 == snapshot()
"""
}
).run_inline( # run without flags
reported_categories=snapshot(["create"]),
).run_pytest( # run without flags and check the pytest report
changed_files=snapshot({}),
report=snapshot(
"""\
Error: one snapshot is missing a value (--inline-snapshot=create)
You can also use --inline-snapshot=review to approve the changes interactively\
"""
),
).run_pytest( # run with create flag and check the changed files
["--inline-snapshot=create"],
changed_files=snapshot(
{
"test_a.py": """\
from inline_snapshot import snapshot
def test_a():
assert 1+1 == snapshot(2)
"""
}
),
)
```
## API
::: inline_snapshot.testing.Example
options:
heading_level: 3
show_root_heading: true
show_root_full_path: false
show_source: false
annotations_path: brief
python-inline-snapshot-0.20.7/docs/theme/ 0000775 0000000 0000000 00000000000 14764761403 0020300 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/docs/theme/main.html 0000664 0000000 0000000 00000003200 14764761403 0022105 0 ustar 00root root 0000000 0000000 {% extends "base.html" %}
{% block outdated %}
You're not viewing the latest version.
Click here to go to latest.
{% endblock %}
{% block announce %}
{{ super() }}
Become a
{% include ".icons/octicons/heart-fill-16.svg" %}
Sponsor
or follow @15r10nk on
{% include ".icons/fontawesome/brands/x-twitter.svg" %}
or
{% include ".icons/fontawesome/brands/mastodon.svg" %}
for updates
{% endblock %}
python-inline-snapshot-0.20.7/docs/third_party.md 0000664 0000000 0000000 00000002544 14764761403 0022056 0 ustar 00root root 0000000 0000000
Third-party extensions can be used to enhance the testing experience with other frameworks.
The goal of inline-snapshot is to provide the core functionality for many different use cases.
List of current third-party extensions:
* [inline-snapshot-pandas](https://pypi.org/project/inline-snapshot-pandas/) pandas integration for inline-snapshot (insider only)
!!! info "How to add your extension to this list?"
Your package name has to start with `inline-snapshot-` and has to be available on [PyPI](https://pypi.org).
The summary of your package will be used as description.
I will update this list from time to time but you can accelerate this process by creating a new [issue](https://github.com/15r10nk/inline-snapshot/issues).
python-inline-snapshot-0.20.7/docs/types.md 0000664 0000000 0000000 00000000223 14764761403 0020661 0 ustar 00root root 0000000 0000000
::: inline_snapshot
options:
heading_level: 1
members: [Snapshot,Category]
show_root_heading: true
show_bases: false
python-inline-snapshot-0.20.7/mkdocs.yml 0000664 0000000 0000000 00000004437 14764761403 0020261 0 ustar 00root root 0000000 0000000 site_name: inline-snapshot
site_url: https://15r10nk.github.io/inline-snapshot/
repo_url: https://github.com/15r10nk/inline-snapshot/
edit_uri: edit/main/docs
theme:
name: material
custom_dir: docs/theme
logo: assets/favicon.svg
favicon: assets/favicon.svg
features:
- toc.follow
- content.code.annotate
- navigation.tabs
palette:
- media: (prefers-color-scheme)
# Palette toggle for light mode
- scheme: default
media: '(prefers-color-scheme: light)'
primary: teal
# Palette toggle for dark mode
- scheme: slate
media: '(prefers-color-scheme: dark)'
primary: teal
validation:
links:
absolute_links: relative_to_docs
watch:
- CONTRIBUTING.md
- CHANGELOG.md
- README.md
- src/inline_snapshot
nav:
- Home:
- Introduction: index.md
- Configuration: configuration.md
- pytest integration: pytest.md
- Categories: categories.md
- Code generation: code_generation.md
- Limitations: limitations.md
- Changelog: changelog.md
- Core:
- x == snapshot(): eq_snapshot.md
- x <= snapshot(): cmp_snapshot.md
- x in snapshot(): in_snapshot.md
- snapshot()[key]: getitem_snapshot.md
- outsource(data): outsource.md
- '@customize_repr': customize_repr.md
- types: types.md
- Extensions:
- first-party (extra): extra.md
- third-party: third_party.md
- Development:
- Testing: testing.md
- Contributing: contributing.md
markdown_extensions:
- toc:
permalink: true
- admonition
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets:
check_paths: true
- pymdownx.superfences
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- attr_list
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
plugins:
- mkdocstrings:
handlers:
python:
options:
show_symbol_type_heading: true
show_symbol_type_toc: true
- social
- search
- markdown-exec:
ansi: required
- replace-url
- autorefs
extra:
social:
- icon: fontawesome/brands/x-twitter
link: https://x.com/15r10nk
- icon: fontawesome/brands/mastodon
link: https://fosstodon.org/@15r10nk
version:
provider: mike
default:
- latest
- development
python-inline-snapshot-0.20.7/pyproject.toml 0000664 0000000 0000000 00000013037 14764761403 0021166 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]
[project]
authors = [
{name = "Frank Hoffmann", email = "15r10nk-git@polarbit.de"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Framework :: Pytest",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Topic :: Software Development :: Testing",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"asttokens>=2.0.5",
"executing>=2.2.0",
"rich>=13.7.1",
"tomli>=2.0.0; python_version < '3.11'"
]
description = "golden master/snapshot/approval testing library which puts the values right into your source code"
keywords = []
name = "inline-snapshot"
readme = "README.md"
requires-python = ">=3.8"
version = "0.20.7"
[project.optional-dependencies]
black = [
"black>=23.3.0",
]
dirty-equals =[
"dirty-equals>=0.9.0",
]
[dependency-groups]
dev = [
"hypothesis>=6.75.5",
"mypy>=1.2.0",
"pyright>=1.1.359",
"pytest-subtests>=0.11.0",
"pytest-freezer>=0.4.8",
"pytest-mock>=3.14.0",
"pytest-xdist>=3.6.1",
"coverage[toml]>=7.6.1",
"coverage-enable-subprocess>=1.0",
"pytest>=8",
"attrs>=24.3.0",
"pydantic>=1",
]
[project.entry-points.pytest11]
inline_snapshot = "inline_snapshot.pytest_plugin"
[project.urls]
Changelog = "https://15r10nk.github.io/inline-snapshot/latest/changelog/"
Discussions = "https://github.com/15r10nk/inline-snapshots/discussions"
Documentation = "https://15r10nk.github.io/inline-snapshot/latest"
Funding = "https://github.com/sponsors/15r10nk"
Homepage = "https://15r10nk.github.io/inline-snapshot/latest"
Issues = "https://github.com/15r10nk/inline-snapshots/issues"
Repository = "https://github.com/15r10nk/inline-snapshot/"
[tool.commitizen]
major_version_zero = true
tag_format = "v$major.$minor.$patch$prerelease"
version_files = [
"src/inline_snapshot/__init__.py:__version__"
]
version_provider = "pep621"
[tool.coverage.paths]
inline_snapshot = ["src/inline_snapshot", "*/inline_snapshot/src/inline_snapshot"]
tests = ["tests", "*/inline_snapshot/tests"]
[tool.coverage.report]
exclude_lines = ["assert False", "raise NotImplemented", "# pragma: no cover", "if TYPE_CHECKING:"]
[tool.coverage.run]
branch = true
data_file = "$TOP/.coverage"
omit = [
"src/inline_snapshot/__about__.py"
]
parallel = true
source_pkgs = ["inline_snapshot", "tests"]
[tool.hatch.envs.docs]
dependencies = [
"markdown-exec[ansi]>=1.8.0",
"mkdocs>=1.4.2",
"mkdocs-material[imaging]>=9.5.17",
"mike",
"mkdocstrings[python]>=0.19.0",
"mkdocs-autorefs",
"replace-url @ {root:uri}/docs/plugins",
"pytest",
"black",
"commitizen"
]
[tool.hatch.envs.default]
installer="uv"
[tool.hatch.envs.cov.scripts]
github=[
"- rm htmlcov/*",
"gh run download -n html-report -D htmlcov",
"xdg-open htmlcov/index.html",
]
[tool.hatch.envs.docs.scripts]
build = "mkdocs build --strict"
deploy = "mkdocs gh-deploy"
export-deps = "pip freeze"
serve = "mkdocs serve"
[tool.hatch.envs.cog]
dependencies=["cogapp","lxml","requests"]
scripts.update="cog -r docs/**.md"
[tool.hatch.envs.gen]
dependencies=["pysource-minimize"]
scripts.test=["python testing/generate_tests.py"]
[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.13", "3.12", "3.11", "3.10", "3.9", "3.8","pypy3.9","pypy3.10"]
extra-deps=["low","hight"]
[tool.hatch.envs.hatch-test.overrides]
matrix.extra-deps.dependencies = [
{ value = "pytest==8.3.3", if = ["low"] },
{ value = "pytest>=8.3.4", if = ["hight"] },
{ value = "pydantic<2", if = ["low"] },
{ value = "pydantic>=2", if = ["hight"] },
]
[tool.hatch.envs.hatch-test]
extra-dependencies = [
"inline-snapshot[black,dirty-equals]",
"dirty-equals>=0.9.0",
"hypothesis>=6.75.5",
"mypy>=1.2.0",
"pyright>=1.1.359",
"pytest-subtests>=0.11.0",
"pytest-freezer>=0.4.8",
"pytest-mock>=3.14.0"
]
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 = [
"inline-snapshot[black,dirty-equals]",
"mypy>=1.0.0",
"pytest",
"hypothesis>=6.75.5",
"pydantic",
"attrs",
"typing-extensions"
]
[[tool.hatch.envs.types.matrix]]
python = ["3.8", "3.9", "3.10", "3.11", "3.12","3.13"]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src/inline_snapshot tests}"
[tool.mypy]
exclude = "tests/.*_samples"
[tool.pyright]
venv = "test-3-12"
venvPath = ".nox"
[tool.hatch.envs.release]
detached=true
dependencies=[
"scriv",
"commitizen"
]
[tool.hatch.envs.release.scripts]
create=[
"scriv collect",
"- pre-commit run -a",
"cz bump"
]
publish=[
"git push --force-with-lease",
"git push --tags",
]
# only as an backup now
publish-package=[
"- rm dist/*",
"hatch build",
"hatch publish",
"scriv github-release"
]
[tool.scriv]
format = "md"
version = "command: cz bump --get-next"
[tool.pytest.ini_options]
markers=["no_rewriting: marks tests which need no code rewriting and can be used with pypy"]
[tool.isort]
profile="black"
force_single_line=true
python-inline-snapshot-0.20.7/scripts/ 0000775 0000000 0000000 00000000000 14764761403 0017735 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/scripts/replace_words.py 0000664 0000000 0000000 00000001123 14764761403 0023135 0 ustar 00root root 0000000 0000000 import re
import sys
def replace_words(file_path, replacements):
with open(file_path) as file:
content = file.read()
for old_word, new_word in replacements.items():
content = re.sub(rf"\b{re.escape(old_word)}\b", new_word, content)
with open(file_path, "w") as file:
file.write(content)
if __name__ == "__main__":
replacements = {
"http://localhost:8000/inline-snapshot/": "https://15r10nk.github.io/inline-snapshot/latest/",
}
for file_path in sys.argv[1:]:
print(file_path)
replace_words(file_path, replacements)
python-inline-snapshot-0.20.7/src/ 0000775 0000000 0000000 00000000000 14764761403 0017035 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/src/inline_snapshot/ 0000775 0000000 0000000 00000000000 14764761403 0022232 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/src/inline_snapshot/__init__.py 0000664 0000000 0000000 00000000732 14764761403 0024345 0 ustar 00root root 0000000 0000000 from ._code_repr import HasRepr
from ._code_repr import customize_repr
from ._exceptions import UsageError
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",
"Is",
"Category",
"Snapshot",
"UsageError",
]
__version__ = "0.20.7"
python-inline-snapshot-0.20.7/src/inline_snapshot/_adapter/ 0000775 0000000 0000000 00000000000 14764761403 0024011 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/src/inline_snapshot/_adapter/__init__.py 0000664 0000000 0000000 00000000107 14764761403 0026120 0 ustar 00root root 0000000 0000000 from .adapter import get_adapter_type
__all__ = ("get_adapter_type",)
python-inline-snapshot-0.20.7/src/inline_snapshot/_adapter/adapter.py 0000664 0000000 0000000 00000004443 14764761403 0026010 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import ast
import typing
from dataclasses import dataclass
from inline_snapshot._source_file import SourceFile
def get_adapter_type(value):
from inline_snapshot._adapter.generic_call_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
@dataclass
class FrameContext:
globals: dict
locals: dict
@dataclass
class AdapterContext:
file: SourceFile
frame: FrameContext | None
def eval(self, node):
assert self.frame is not None
return eval(
compile(ast.Expression(node), self.file.filename, "eval"),
self.frame.globals,
self.frame.locals,
)
class Adapter:
context: AdapterContext
def __init__(self, context: AdapterContext):
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(self)
def value_assign(self, old_value, old_node, new_value):
from .value_adapter import ValueAdapter
adapter = ValueAdapter(self.context)
result = yield from adapter.assign(old_value, old_node, new_value)
return result
@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)
python-inline-snapshot-0.20.7/src/inline_snapshot/_adapter/dict_adapter.py 0000664 0000000 0000000 00000011143 14764761403 0027006 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import ast
import warnings
from .._change import Delete
from .._change import DictInsert
from ..syntax_warnings import InlineSnapshotSyntaxWarning
from .adapter import Adapter
from .adapter import Item
from .adapter import adapter_map
class DictAdapter(Adapter):
@classmethod
def repr(cls, value):
result = (
"{"
+ ", ".join(f"{repr(k)}: {repr(value)}" for k, value in value.items())
+ "}"
)
if type(value) is not dict:
result = f"{repr(type(value))}({result})"
return result
@classmethod
def map(cls, value, map_function):
return {k: adapter_map(v, map_function) for k, v in value.items()}
@classmethod
def items(cls, value, node):
if node is None or not isinstance(node, ast.Dict):
return [Item(value=value, node=None) for value in value.values()]
result = []
for value_key, node_key, node_value in zip(
value.keys(), node.keys, node.values
):
try:
# this is just a sanity check, dicts should be ordered
node_key = ast.literal_eval(node_key)
except Exception:
pass
else:
assert node_key == value_key
result.append(Item(value=value[value_key], node=node_value))
return result
def assign(self, old_value, old_node, new_value):
if old_node is not None:
if not (
isinstance(old_node, ast.Dict) and len(old_value) == len(old_node.keys)
):
result = yield from self.value_assign(old_value, old_node, new_value)
return result
for key, value in zip(old_node.keys, old_node.values):
if key is None:
warnings.warn_explicit(
"star-expressions are not supported inside snapshots",
filename=self.context.file._source.filename,
lineno=value.lineno,
category=InlineSnapshotSyntaxWarning,
)
return old_value
for value, node in zip(old_value.keys(), old_node.keys):
try:
# this is just a sanity check, dicts should be ordered
node_value = ast.literal_eval(node)
except:
continue
assert node_value == value
result = {}
for key, node in zip(
old_value.keys(),
(old_node.values if old_node is not None else [None] * len(old_value)),
):
if key not in new_value:
# delete entries
yield Delete("fix", self.context.file._source, node, old_value[key])
to_insert = []
insert_pos = 0
for key, new_value_element in new_value.items():
if key not in old_value:
# add new values
to_insert.append((key, new_value_element))
result[key] = new_value_element
else:
if isinstance(old_node, ast.Dict):
node = old_node.values[list(old_value.keys()).index(key)]
else:
node = None
# check values with same keys
result[key] = yield from self.get_adapter(
old_value[key], new_value[key]
).assign(old_value[key], node, new_value[key])
if to_insert:
new_code = [
(
self.context.file._value_to_code(k),
self.context.file._value_to_code(v),
)
for k, v in to_insert
]
yield DictInsert(
"fix",
self.context.file._source,
old_node,
insert_pos,
new_code,
to_insert,
)
to_insert = []
insert_pos += 1
if to_insert:
new_code = [
(
self.context.file._value_to_code(k),
self.context.file._value_to_code(v),
)
for k, v in to_insert
]
yield DictInsert(
"fix",
self.context.file._source,
old_node,
len(old_value),
new_code,
to_insert,
)
return result
python-inline-snapshot-0.20.7/src/inline_snapshot/_adapter/generic_call_adapter.py 0000664 0000000 0000000 00000032266 14764761403 0030503 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import ast
import warnings
from abc import ABC
from collections import defaultdict
from dataclasses import MISSING
from dataclasses import fields
from dataclasses import is_dataclass
from typing import Any
from .._change import CallArg
from .._change import Delete
from ..syntax_warnings import InlineSnapshotSyntaxWarning
from .adapter import Adapter
from .adapter import Item
from .adapter import adapter_map
def get_adapter_for_type(typ):
subclasses = GenericCallAdapter.__subclasses__()
options = [cls for cls in subclasses if cls.check_type(typ)]
if not options:
return
assert len(options) == 1
return options[0]
class Argument:
value: Any
is_default: bool = False
def __init__(self, value, is_default=False):
self.value = value
self.is_default = is_default
class GenericCallAdapter(Adapter):
@classmethod
def check_type(cls, typ) -> bool:
raise NotImplementedError(cls)
@classmethod
def arguments(cls, value) -> tuple[list[Argument], dict[str, Argument]]:
raise NotImplementedError(cls)
@classmethod
def argument(cls, value, pos_or_name) -> Any:
raise NotImplementedError(cls)
@classmethod
def repr(cls, value):
args, kwargs = cls.arguments(value)
arguments = [repr(value.value) for value in args] + [
f"{key}={repr(value.value)}"
for key, value in kwargs.items()
if not value.is_default
]
return f"{repr(type(value))}({', '.join(arguments)})"
@classmethod
def map(cls, value, map_function):
new_args, new_kwargs = cls.arguments(value)
return type(value)(
*[adapter_map(arg.value, map_function) for arg in new_args],
**{
k: adapter_map(kwarg.value, map_function)
for k, kwarg in new_kwargs.items()
},
)
@classmethod
def items(cls, value, node):
new_args, new_kwargs = cls.arguments(value)
if node is not None:
assert isinstance(node, ast.Call)
assert all(kw.arg for kw in node.keywords)
kw_arg_node = {kw.arg: kw.value for kw in node.keywords if kw.arg}.get
def pos_arg_node(pos):
return node.args[pos]
else:
def kw_arg_node(_):
return None
def pos_arg_node(_):
return None
return [
Item(value=arg.value, node=pos_arg_node(i))
for i, arg in enumerate(new_args)
] + [
Item(value=kw.value, node=kw_arg_node(name))
for name, kw in new_kwargs.items()
]
def assign(self, old_value, old_node, new_value):
if old_node is None or not isinstance(old_node, ast.Call):
result = yield from self.value_assign(old_value, old_node, new_value)
return result
call_type = self.context.eval(old_node.func)
if not (isinstance(call_type, type) and self.check_type(call_type)):
result = yield from self.value_assign(old_value, old_node, new_value)
return result
# positional arguments
for pos_arg in old_node.args:
if isinstance(pos_arg, ast.Starred):
warnings.warn_explicit(
"star-expressions are not supported inside snapshots",
filename=self.context.file._source.filename,
lineno=pos_arg.lineno,
category=InlineSnapshotSyntaxWarning,
)
return old_value
# keyword arguments
for kw in old_node.keywords:
if kw.arg is None:
warnings.warn_explicit(
"star-expressions are not supported inside snapshots",
filename=self.context.file._source.filename,
lineno=kw.value.lineno,
category=InlineSnapshotSyntaxWarning,
)
return old_value
new_args, new_kwargs = self.arguments(new_value)
# positional arguments
result_args = []
for i, (new_value_element, node) in enumerate(zip(new_args, old_node.args)):
old_value_element = self.argument(old_value, i)
result = yield from self.get_adapter(
old_value_element, new_value_element.value
).assign(old_value_element, node, new_value_element.value)
result_args.append(result)
if len(old_node.args) > len(new_args):
for arg_pos, node in list(enumerate(old_node.args))[len(new_args) :]:
yield Delete(
"fix",
self.context.file._source,
node,
self.argument(old_value, arg_pos),
)
if len(old_node.args) < len(new_args):
for insert_pos, value in list(enumerate(new_args))[len(old_node.args) :]:
yield CallArg(
flag="fix",
file=self.context.file._source,
node=old_node,
arg_pos=insert_pos,
arg_name=None,
new_code=self.context.file._value_to_code(value.value),
new_value=value.value,
)
# keyword arguments
result_kwargs = {}
for kw in old_node.keywords:
if (missing := kw.arg not in new_kwargs) or new_kwargs[kw.arg].is_default:
# delete entries
yield Delete(
"fix" if missing else "update",
self.context.file._source,
kw.value,
self.argument(old_value, kw.arg),
)
old_node_kwargs = {kw.arg: kw.value for kw in old_node.keywords}
to_insert = []
insert_pos = 0
for key, new_value_element in new_kwargs.items():
if new_value_element.is_default:
continue
if key not in old_node_kwargs:
# add new values
to_insert.append((key, new_value_element.value))
result_kwargs[key] = new_value_element.value
else:
node = old_node_kwargs[key]
# check values with same keys
old_value_element = self.argument(old_value, key)
result_kwargs[key] = yield from self.get_adapter(
old_value_element, new_value_element.value
).assign(old_value_element, node, new_value_element.value)
if to_insert:
for key, value in to_insert:
yield CallArg(
flag="fix",
file=self.context.file._source,
node=old_node,
arg_pos=insert_pos,
arg_name=key,
new_code=self.context.file._value_to_code(value),
new_value=value,
)
to_insert = []
insert_pos += 1
if to_insert:
for key, value in to_insert:
yield CallArg(
flag="fix",
file=self.context.file._source,
node=old_node,
arg_pos=insert_pos,
arg_name=key,
new_code=self.context.file._value_to_code(value),
new_value=value,
)
return type(old_value)(*result_args, **result_kwargs)
class DataclassAdapter(GenericCallAdapter):
@classmethod
def check_type(cls, value):
return is_dataclass(value)
@classmethod
def arguments(cls, value):
kwargs = {}
for field in fields(value): # type: ignore
if field.repr:
field_value = getattr(value, field.name)
is_default = False
if field.default != MISSING and field.default == field_value:
is_default = True
if (
field.default_factory != MISSING
and field.default_factory() == field_value
):
is_default = True
kwargs[field.name] = Argument(value=field_value, is_default=is_default)
return ([], kwargs)
def argument(self, value, pos_or_name):
if isinstance(pos_or_name, str):
return getattr(value, pos_or_name)
else:
args = [field for field in fields(value) if field.init]
return args[pos_or_name]
try:
import attrs
except ImportError: # pragma: no cover
pass
else:
class AttrAdapter(GenericCallAdapter):
@classmethod
def check_type(cls, value):
return attrs.has(value)
@classmethod
def arguments(cls, value):
kwargs = {}
for field in attrs.fields(type(value)):
if field.repr:
field_value = getattr(value, field.name)
is_default = False
if field.default is not attrs.NOTHING:
default_value = (
field.default
if not isinstance(field.default, attrs.Factory)
else (
field.default.factory()
if not field.default.takes_self
else field.default.factory(value)
)
)
if default_value == field_value:
is_default = True
kwargs[field.name] = Argument(
value=field_value, is_default=is_default
)
return ([], kwargs)
def argument(self, value, pos_or_name):
assert isinstance(pos_or_name, str)
return getattr(value, pos_or_name)
try:
import pydantic
except ImportError: # pragma: no cover
pass
else:
# import pydantic
if pydantic.version.VERSION.startswith("1."):
# pydantic v1
from pydantic.fields import Undefined as PydanticUndefined # type: ignore[attr-defined,no-redef]
def get_fields(value):
return value.__fields__
else:
# pydantic v2
from pydantic_core import PydanticUndefined
def get_fields(value):
return type(value).model_fields
from pydantic import BaseModel
class PydanticContainer(GenericCallAdapter):
@classmethod
def check_type(cls, value):
return issubclass(value, BaseModel)
@classmethod
def arguments(cls, value):
kwargs = {}
for name, field in get_fields(value).items(): # type: ignore
if getattr(field, "repr", True):
field_value = getattr(value, name)
is_default = False
if (
field.default is not PydanticUndefined
and field.default == field_value
):
is_default = True
if (
field.default_factory is not None
and field.default_factory() == field_value
):
is_default = True
kwargs[name] = Argument(value=field_value, is_default=is_default)
return ([], kwargs)
@classmethod
def argument(cls, value, pos_or_name):
assert isinstance(pos_or_name, str)
return getattr(value, pos_or_name)
class IsNamedTuple(ABC):
_inline_snapshot_name = "namedtuple"
_fields: tuple
_field_defaults: dict
@classmethod
def __subclasshook__(cls, t):
b = t.__bases__
if len(b) != 1 or b[0] != tuple:
return False
f = getattr(t, "_fields", None)
if not isinstance(f, tuple):
return False
return all(type(n) == str for n in f)
class NamedTupleAdapter(GenericCallAdapter):
@classmethod
def check_type(cls, value):
return issubclass(value, IsNamedTuple)
@classmethod
def arguments(cls, value: IsNamedTuple):
return (
[],
{
field: Argument(value=getattr(value, field))
for field in value._fields
if field not in value._field_defaults
or getattr(value, field) != value._field_defaults[field]
},
)
def argument(self, value, pos_or_name):
assert isinstance(pos_or_name, str)
return getattr(value, pos_or_name)
class DefaultDictAdapter(GenericCallAdapter):
@classmethod
def check_type(cls, value):
return issubclass(value, defaultdict)
@classmethod
def arguments(cls, value: defaultdict):
return (
[Argument(value=value.default_factory), Argument(value=dict(value))],
{},
)
def argument(self, value, pos_or_name):
assert isinstance(pos_or_name, int)
if pos_or_name == 0:
return value.default_factory
elif pos_or_name == 1:
return dict(value)
assert False
python-inline-snapshot-0.20.7/src/inline_snapshot/_adapter/sequence_adapter.py 0000664 0000000 0000000 00000007437 14764761403 0027706 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import ast
import warnings
from collections import defaultdict
from .._align import add_x
from .._align import align
from .._change import Delete
from .._change import ListInsert
from .._compare_context import compare_context
from ..syntax_warnings import InlineSnapshotSyntaxWarning
from .adapter import Adapter
from .adapter import Item
from .adapter import adapter_map
class SequenceAdapter(Adapter):
node_type: type
value_type: type
braces: str
trailing_comma: bool
@classmethod
def repr(cls, value):
if len(value) == 1 and cls.trailing_comma:
seq = repr(value[0]) + ","
else:
seq = ", ".join(map(repr, value))
return cls.braces[0] + seq + cls.braces[1]
@classmethod
def map(cls, value, map_function):
result = [adapter_map(v, map_function) for v in value]
return cls.value_type(result)
@classmethod
def items(cls, value, node):
if node is None or not isinstance(node, cls.node_type):
return [Item(value=v, node=None) for v in value]
assert len(value) == len(node.elts)
return [Item(value=v, node=n) for v, n in zip(value, node.elts)]
def assign(self, old_value, old_node, new_value):
if old_node is not None:
if not isinstance(
old_node, ast.List if isinstance(old_value, list) else ast.Tuple
):
result = yield from self.value_assign(old_value, old_node, new_value)
return result
for e in old_node.elts:
if isinstance(e, ast.Starred):
warnings.warn_explicit(
"star-expressions are not supported inside snapshots",
filename=self.context.file.filename,
lineno=e.lineno,
category=InlineSnapshotSyntaxWarning,
)
return old_value
with compare_context():
diff = add_x(align(old_value, new_value))
old = zip(
old_value,
old_node.elts if old_node is not None else [None] * len(old_value),
)
new = iter(new_value)
old_position = 0
to_insert = defaultdict(list)
result = []
for c in diff:
if c in "mx":
old_value_element, old_node_element = next(old)
new_value_element = next(new)
v = yield from self.get_adapter(
old_value_element, new_value_element
).assign(old_value_element, old_node_element, new_value_element)
result.append(v)
old_position += 1
elif c == "i":
new_value_element = next(new)
new_code = self.context.file._value_to_code(new_value_element)
result.append(new_value_element)
to_insert[old_position].append((new_code, new_value_element))
elif c == "d":
old_value_element, old_node_element = next(old)
yield Delete(
"fix",
self.context.file._source,
old_node_element,
old_value_element,
)
old_position += 1
else:
assert False
for position, code_values in to_insert.items():
yield ListInsert(
"fix", self.context.file._source, old_node, position, *zip(*code_values)
)
return self.value_type(result)
class ListAdapter(SequenceAdapter):
node_type = ast.List
value_type = list
braces = "[]"
trailing_comma = False
class TupleAdapter(SequenceAdapter):
node_type = ast.Tuple
value_type = tuple
braces = "()"
trailing_comma = True
python-inline-snapshot-0.20.7/src/inline_snapshot/_adapter/value_adapter.py 0000664 0000000 0000000 00000004124 14764761403 0027200 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import ast
import warnings
from .._change import Replace
from .._code_repr import value_code_repr
from .._sentinels import undefined
from .._unmanaged import Unmanaged
from .._unmanaged import update_allowed
from .._utils import value_to_token
from ..syntax_warnings import InlineSnapshotInfo
from .adapter import Adapter
class ValueAdapter(Adapter):
@classmethod
def repr(cls, value):
return value_code_repr(value)
@classmethod
def map(cls, value, map_function):
return map_function(value)
def assign(self, old_value, old_node, new_value):
# generic fallback
# because IsStr() != IsStr()
if isinstance(old_value, Unmanaged):
return old_value
if old_node is None:
new_token = []
else:
new_token = value_to_token(new_value)
if isinstance(old_node, ast.JoinedStr) and isinstance(new_value, str):
if not old_value == new_value:
warnings.warn_explicit(
f"inline-snapshot will be able to fix f-strings in the future.\nThe current string value is:\n {new_value!r}",
filename=self.context.file._source.filename,
lineno=old_node.lineno,
category=InlineSnapshotInfo,
)
return old_value
if not old_value == new_value:
if old_value is undefined:
flag = "create"
else:
flag = "fix"
elif (
old_node is not None
and update_allowed(old_value)
and self.context.file._token_of_node(old_node) != new_token
):
flag = "update"
else:
# equal and equal repr
return old_value
new_code = self.context.file._token_to_code(new_token)
yield Replace(
node=old_node,
file=self.context.file._source,
new_code=new_code,
flag=flag,
old_value=old_value,
new_value=new_value,
)
return new_value
python-inline-snapshot-0.20.7/src/inline_snapshot/_align.py 0000664 0000000 0000000 00000003577 14764761403 0024051 0 ustar 00root root 0000000 0000000 from itertools import groupby
def align(seq_a, seq_b) -> str:
start = 0
for a, b in zip(seq_a, seq_b):
if a == b:
start += 1
else:
break
if start == len(seq_a) == len(seq_b):
return "m" * start
end = 0
for a, b in zip(reversed(seq_a[start:]), reversed(seq_b[start:])):
if a == b:
end += 1
else:
break
diff = nw_align(seq_a[start : len(seq_a) - end], seq_b[start : len(seq_b) - end])
return "m" * start + diff + "m" * end
def nw_align(seq_a, seq_b) -> str:
matrix: list = [[(0, "e")] + [(0, "i")] * len(seq_b)]
for a in seq_a:
last = matrix[-1]
new_line = [(0, "d")]
for bi, b in enumerate(seq_b, 1):
la, lc, lb = new_line[-1], last[bi - 1], last[bi]
values = [(la[0], "i"), (lb[0], "d")]
if a == b:
values.append((lc[0] + 1, "m"))
new_line.append(max(values))
matrix.append(new_line)
# backtrack
ai = len(seq_a)
bi = len(seq_b)
d = ""
track = ""
while d != "e":
_, d = matrix[ai][bi]
if d == "m":
ai -= 1
bi -= 1
elif d == "i":
bi -= 1
elif d == "d":
ai -= 1
if d != "e":
track += d
return track[::-1]
def add_x(track):
"""Replaces an `id` with the same number of insertions and deletions with
x."""
groups = [(c, len(list(v))) for c, v in groupby(track)]
i = 0
result = ""
while i < len(groups):
g = groups[i]
if i == len(groups) - 1:
result += g[0] * g[1]
break
ng = groups[i + 1]
if g[0] == "d" and ng[0] == "i" and g[1] == ng[1]:
result += "x" * g[1]
i += 1
else:
result += g[0] * g[1]
i += 1
return result
python-inline-snapshot-0.20.7/src/inline_snapshot/_change.py 0000664 0000000 0000000 00000017773 14764761403 0024207 0 ustar 00root root 0000000 0000000 import ast
from collections import defaultdict
from dataclasses import dataclass
from typing import Any
from typing import DefaultDict
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
from typing import cast
from asttokens.util import Token
from executing.executing import EnhancedAST
from inline_snapshot._source_file import SourceFile
from ._rewrite_code import ChangeRecorder
from ._rewrite_code import end_of
from ._rewrite_code import start_of
@dataclass()
class Change:
flag: str
file: SourceFile
@property
def filename(self):
return self.file.filename
def apply(self, recorder: ChangeRecorder):
raise NotImplementedError()
@dataclass()
class Delete(Change):
node: ast.AST
old_value: Any
@dataclass()
class AddArgument(Change):
node: ast.Call
position: Optional[int]
name: Optional[str]
new_code: str
new_value: Any
@dataclass()
class ListInsert(Change):
node: ast.List
position: int
new_code: List[str]
new_values: List[Any]
@dataclass()
class DictInsert(Change):
node: ast.Dict
position: int
new_code: List[Tuple[str, str]]
new_values: List[Tuple[Any, Any]]
@dataclass()
class Replace(Change):
node: ast.AST
new_code: str
old_value: Any
new_value: Any
def apply(self, recorder: ChangeRecorder):
change = recorder.new_change()
range = self.file.asttokens().get_text_positions(self.node, False)
change.replace(range, self.new_code, filename=self.filename)
@dataclass()
class CallArg(Change):
node: Optional[ast.Call]
arg_pos: Optional[int]
arg_name: Optional[str]
new_code: str
new_value: Any
TokenRange = Tuple[Token, Token]
def brace_tokens(source, node) -> TokenRange:
first_token, *_, end_token = source.asttokens().get_tokens(node)
return first_token, end_token
def generic_sequence_update(
source: SourceFile,
parent: Union[ast.List, ast.Tuple, ast.Dict, ast.Call],
brace_tokens: TokenRange,
parent_elements: List[Union[TokenRange, None]],
to_insert: Dict[int, List[str]],
recorder: ChangeRecorder,
):
rec = recorder.new_change()
new_code = []
deleted = False
last_token, end_token = brace_tokens
is_start = True
elements = 0
for index, entry in enumerate(parent_elements):
if index in to_insert:
new_code += to_insert[index]
if entry is None:
deleted = True
else:
first_token, new_last_token = entry
elements += len(new_code) + 1
if deleted or new_code:
code = ""
if new_code:
code = ", ".join(new_code) + ", "
if not is_start:
code = ", " + code
rec.replace(
(end_of(last_token), start_of(first_token)),
code,
filename=source.filename,
)
new_code = []
deleted = False
last_token = new_last_token
is_start = False
if len(parent_elements) in to_insert:
new_code += to_insert[len(parent_elements)]
elements += len(new_code)
if new_code or deleted or elements == 1 or len(parent_elements) <= 1:
code = ", ".join(new_code)
if not is_start and code:
code = ", " + code
if elements == 1 and isinstance(parent, ast.Tuple):
# trailing comma for tuples (1,)
code += ","
rec.replace(
(end_of(last_token), start_of(end_token)),
code,
filename=source.filename,
)
def apply_all(all_changes: List[Change], recorder: ChangeRecorder):
by_parent: Dict[
EnhancedAST, List[Union[Delete, DictInsert, ListInsert, CallArg]]
] = defaultdict(list)
sources: Dict[EnhancedAST, SourceFile] = {}
for change in all_changes:
if isinstance(change, Delete):
node = cast(EnhancedAST, change.node).parent
if isinstance(node, ast.keyword):
node = node.parent
by_parent[node].append(change)
sources[node] = change.file
elif isinstance(change, (DictInsert, ListInsert, CallArg)):
node = cast(EnhancedAST, change.node)
by_parent[node].append(change)
sources[node] = change.file
else:
change.apply(recorder)
for parent, changes in by_parent.items():
source = sources[parent]
if isinstance(parent, (ast.List, ast.Tuple)):
to_delete = {
change.node for change in changes if isinstance(change, Delete)
}
to_insert = {
change.position: change.new_code
for change in changes
if isinstance(change, ListInsert)
}
def list_token_range(entry):
r = list(source.asttokens().get_tokens(entry))
return r[0], r[-1]
generic_sequence_update(
source,
parent,
brace_tokens(source, parent),
[None if e in to_delete else list_token_range(e) for e in parent.elts],
to_insert,
recorder,
)
elif isinstance(parent, ast.Call):
to_delete = {
change.node for change in changes if isinstance(change, Delete)
}
atok = source.asttokens()
def arg_token_range(node):
if isinstance(node.parent, ast.keyword):
node = node.parent
r = list(atok.get_tokens(node))
return r[0], r[-1]
braces_left = atok.next_token(list(atok.get_tokens(parent.func))[-1])
assert braces_left.string == "("
braces_right = list(atok.get_tokens(parent))[-1]
assert braces_right.string == ")"
to_insert = DefaultDict(list)
for change in changes:
if isinstance(change, CallArg):
if change.arg_name is not None:
position = (
change.arg_pos
if change.arg_pos is not None
else len(parent.args) + len(parent.keywords)
)
to_insert[position].append(
f"{change.arg_name} = {change.new_code}"
)
else:
assert change.arg_pos is not None
to_insert[change.arg_pos].append(change.new_code)
generic_sequence_update(
source,
parent,
(braces_left, braces_right),
[
None if e in to_delete else arg_token_range(e)
for e in parent.args + [kw.value for kw in parent.keywords]
],
to_insert,
recorder,
)
elif isinstance(parent, ast.Dict):
to_delete = {
change.node for change in changes if isinstance(change, Delete)
}
to_insert = {
change.position: [f"{key}: {value}" for key, value in change.new_code]
for change in changes
if isinstance(change, DictInsert)
}
def dict_token_range(key, value):
return (
list(source.asttokens().get_tokens(key))[0],
list(source.asttokens().get_tokens(value))[-1],
)
generic_sequence_update(
source,
parent,
brace_tokens(source, parent),
[
None if value in to_delete else dict_token_range(key, value)
for key, value in zip(parent.keys, parent.values)
],
to_insert,
recorder,
)
else:
assert False, parent
python-inline-snapshot-0.20.7/src/inline_snapshot/_code_repr.py 0000664 0000000 0000000 00000007227 14764761403 0024715 0 ustar 00root root 0000000 0000000 import ast
from enum import Enum
from enum import Flag
from functools import singledispatch
from unittest import mock
real_repr = repr
class HasRepr:
"""This class is used for objects where `__repr__()` returns an non-
parsable representation.
HasRepr uses the type and repr of the value for equal comparison.
You can change `__repr__()` to return valid python code or use
`@customize_repr` to customize repr which is used by inline-
snapshot.
"""
def __init__(self, type, str_repr: str) -> None:
self._type = type
self._str_repr = str_repr
def __repr__(self):
return f"HasRepr({self._type.__qualname__}, {self._str_repr!r})"
def __eq__(self, other):
if isinstance(other, HasRepr):
if other._type is not self._type:
return False
else:
if type(other) is not self._type:
return False
other_repr = code_repr(other)
return other_repr == self._str_repr or other_repr == repr(self)
def used_hasrepr(tree):
return [
n
for n in ast.walk(tree)
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Name)
and n.func.id == "HasRepr"
and len(n.args) == 2
]
@singledispatch
def code_repr_dispatch(value):
return real_repr(value)
def customize_repr(f):
"""Register a funtion which should be used to get the code representation
of a object.
``` python
@customize_repr
def _(obj: MyCustomClass):
return f"MyCustomClass(attr={repr(obj.attr)})"
```
it is important to use `repr()` inside the implementation, because it is mocked to return the code represenation
you dont have to provide a custom implementation if:
* __repr__() of your class returns a valid code representation,
* and __repr__() uses `repr()` to get the representaion of the child objects
"""
code_repr_dispatch.register(f)
def code_repr(obj):
with mock.patch("builtins.repr", mocked_code_repr):
return mocked_code_repr(obj)
def mocked_code_repr(obj):
from inline_snapshot._adapter.adapter import get_adapter_type
adapter = get_adapter_type(obj)
assert adapter is not None
return adapter.repr(obj)
def value_code_repr(obj):
if not type(obj) == type(obj): # pragma: no cover
# this was caused by https://github.com/samuelcolvin/dirty-equals/issues/104
# dispatch will not work in cases like this
return (
f"HasRepr({repr(type(obj))}, '< type(obj) can not be compared with == >')"
)
result = code_repr_dispatch(obj)
try:
ast.parse(result)
except SyntaxError:
return real_repr(HasRepr(type(obj), result))
return result
# -8<- [start:Enum]
@customize_repr
def _(value: Enum):
return f"{type(value).__qualname__}.{value.name}"
# -8<- [end:Enum]
@customize_repr
def _(value: Flag):
name = type(value).__qualname__
return " | ".join(f"{name}.{flag.name}" for flag in type(value) if flag in value)
def sort_set_values(set_values):
is_sorted = False
try:
set_values = sorted(set_values)
is_sorted = True
except TypeError:
pass
set_values = list(map(repr, set_values))
if not is_sorted:
set_values = sorted(set_values)
return set_values
@customize_repr
def _(value: set):
if len(value) == 0:
return "set()"
return "{" + ", ".join(sort_set_values(value)) + "}"
@customize_repr
def _(value: frozenset):
if len(value) == 0:
return "frozenset()"
return "frozenset({" + ", ".join(sort_set_values(value)) + "})"
@customize_repr
def _(value: type):
return value.__qualname__
python-inline-snapshot-0.20.7/src/inline_snapshot/_compare_context.py 0000664 0000000 0000000 00000000430 14764761403 0026132 0 ustar 00root root 0000000 0000000 from contextlib import contextmanager
def compare_only():
return _eq_check_only
_eq_check_only = False
@contextmanager
def compare_context():
global _eq_check_only
old_eq_only = _eq_check_only
_eq_check_only = True
yield
_eq_check_only = old_eq_only
python-inline-snapshot-0.20.7/src/inline_snapshot/_config.py 0000664 0000000 0000000 00000003211 14764761403 0024205 0 ustar 00root root 0000000 0000000 import os
import sys
from dataclasses import dataclass
from dataclasses import field
from pathlib import Path
from typing import Dict
from typing import List
from typing import Optional
if sys.version_info >= (3, 11):
from tomllib import loads
else:
from tomli import loads
@dataclass
class Config:
hash_length: int = 12
default_flags: List[str] = field(default_factory=lambda: ["short-report"])
shortcuts: Dict[str, List[str]] = field(default_factory=dict)
format_command: Optional[str] = None
storage_dir: Optional[Path] = None
config = Config()
def read_config(path: Path) -> Config:
result = Config()
config = {}
if path.exists():
data = loads(path.read_text("utf-8"))
try:
config = data["tool"]["inline-snapshot"]
except KeyError:
pass
try:
result.hash_length = config["hash-length"]
except KeyError:
pass
try:
result.default_flags = config["default-flags"]
except KeyError:
pass
result.shortcuts = config.get(
"shortcuts", {"fix": ["create", "fix"], "review": ["review"]}
)
if storage_dir := config.get("storage-dir"):
storage_dir = Path(storage_dir)
if not storage_dir.is_absolute():
# Make it relative to pyproject.toml, and absolute.
storage_dir = path.parent.joinpath(storage_dir).absolute()
result.storage_dir = storage_dir
result.format_command = config.get("format-command", None)
env_var = "INLINE_SNAPSHOT_DEFAULT_FLAGS"
if env_var in os.environ:
result.default_flags = os.environ[env_var].split(",")
return result
python-inline-snapshot-0.20.7/src/inline_snapshot/_exceptions.py 0000664 0000000 0000000 00000000046 14764761403 0025124 0 ustar 00root root 0000000 0000000 class UsageError(Exception):
pass
python-inline-snapshot-0.20.7/src/inline_snapshot/_external.py 0000664 0000000 0000000 00000012247 14764761403 0024573 0 ustar 00root root 0000000 0000000 import hashlib
import pathlib
import re
from typing import Optional
from typing import Set
from typing import Union
from . import _config
from ._global_state import state
class HashError(Exception):
pass
class DiscStorage:
def __init__(self, directory):
self.directory = pathlib.Path(directory)
def _ensure_directory(self):
self.directory.mkdir(exist_ok=True, parents=True)
gitignore = self.directory / ".gitignore"
if not gitignore.exists():
gitignore.write_text(
"# ignore all snapshots which are not refered in the source\n*-new.*\n",
"utf-8",
)
def save(self, name, data):
assert "*" not in name
self._ensure_directory()
(self.directory / name).write_bytes(data)
def read(self, name):
return self._lookup_path(name).read_bytes()
def prune_new_files(self):
for file in self.directory.glob("*-new.*"):
file.unlink()
def list(self) -> Set[str]:
if self.directory.exists():
return {item.name for item in self.directory.iterdir()} - {".gitignore"}
else:
return set()
def persist(self, name):
try:
file = self._lookup_path(name)
except HashError:
return
if file.stem.endswith("-new"):
stem = file.stem[:-4]
file.rename(file.with_name(stem + file.suffix))
def _lookup_path(self, name) -> pathlib.Path:
files = list(self.directory.glob(name))
if len(files) > 1:
raise HashError(f"hash collision files={sorted(f.name for f in files)}")
if not files:
raise HashError(f"hash {name!r} is not found in the DiscStorage")
return files[0]
def lookup_all(self, name) -> Set[str]:
return {file.name for file in self.directory.glob(name)}
def remove(self, name):
self._lookup_path(name).unlink()
class external:
def __init__(self, name: str):
"""External objects are used as a representation for outsourced data.
You should not create them directly.
The external data is by default stored inside `/.inline-snapshot/external`,
where `` is replaced by the directory containing the Pytest configuration file, if any.
To store data in a different location, set the `storage-dir` option in pyproject.toml.
Data which is outsourced but not referenced in the source code jet has a '-new' suffix in the filename.
Parameters:
name: the name of the external stored object.
"""
m = re.fullmatch(r"([0-9a-fA-F]*)\*?(\.[a-zA-Z0-9]*)", name)
if m:
self._hash, self._suffix = m.groups()
else:
raise ValueError(
"path has to be of the form . or *."
)
@property
def _path(self):
return f"{self._hash}*{self._suffix}"
def __repr__(self):
"""Returns the representation of the external object.
The length of the hash can be specified in the
[config](configuration.md).
"""
hash = self._hash[: _config.config.hash_length]
if len(hash) == 64:
return f'external("{hash}{self._suffix}")'
else:
return f'external("{hash}*{self._suffix}")'
def __eq__(self, other):
"""Two external objects are equal if they have the same hash and
suffix."""
if not isinstance(other, external):
return NotImplemented
min_hash_len = min(len(self._hash), len(other._hash))
if self._hash[:min_hash_len] != other._hash[:min_hash_len]:
return False
if self._suffix != other._suffix:
return False
return True
def _load_value(self):
assert state().storage is not None
return state().storage.read(self._path)
def outsource(data: Union[str, bytes], *, suffix: Optional[str] = None) -> external:
"""Outsource some data into an external file.
``` pycon
>>> png_data = b"some_bytes" # should be the replaced with your actual data
>>> outsource(png_data, suffix=".png")
external("212974ed1835*.png")
```
Parameters:
data: data which should be outsourced. strings are encoded with `"utf-8"`.
suffix: overwrite file suffix. The default is `".bin"` if data is an instance of `#!python bytes` and `".txt"` for `#!python str`.
Returns:
The external data.
"""
if isinstance(data, str):
data = data.encode("utf-8")
if suffix is None:
suffix = ".txt"
elif isinstance(data, bytes):
if suffix is None:
suffix = ".bin"
else:
raise TypeError("data has to be of type bytes | str")
if not suffix or suffix[0] != ".":
raise ValueError("suffix has to start with a '.' like '.png'")
m = hashlib.sha256()
m.update(data)
hash = m.hexdigest()
storage = state().storage
assert storage is not None
name = hash + suffix
if not storage.lookup_all(name):
path = hash + "-new" + suffix
storage.save(path, data)
return external(name)
python-inline-snapshot-0.20.7/src/inline_snapshot/_find_external.py 0000664 0000000 0000000 00000005123 14764761403 0025566 0 ustar 00root root 0000000 0000000 import ast
import pathlib
from typing import Set
from executing import Source
from ._global_state import state
from ._rewrite_code import ChangeRecorder
from ._rewrite_code import end_of
from ._rewrite_code import start_of
def contains_import(tree, module, name):
for node in tree.body:
if (
isinstance(node, ast.ImportFrom)
and node.module == module
and any(alias.name == name for alias in node.names)
):
return True
return False
def used_externals_in(source) -> Set[str]:
tree = ast.parse(source)
if not contains_import(tree, "inline_snapshot", "external"):
return set()
usages = []
for node in ast.walk(tree):
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "external"
):
usages.append(node)
return {
u.args[0].value
for u in usages
if u.args and isinstance(u.args[0], ast.Constant)
}
def used_externals() -> Set[str]:
result = set()
for filename in state().files_with_snapshots:
result |= used_externals_in(pathlib.Path(filename).read_text("utf-8"))
return result
def unused_externals() -> Set[str]:
storage = state().storage
assert storage is not None
unused_externals = storage.list()
for name in used_externals():
unused_externals -= storage.lookup_all(name)
return unused_externals
def ensure_import(filename, imports, recorder: ChangeRecorder):
source = Source.for_filename(filename)
change = recorder.new_change()
tree = source.tree
token = source.asttokens()
to_add = []
for module, names in imports.items():
for name in names:
if not contains_import(tree, module, name):
to_add.append((module, name))
assert isinstance(tree, ast.Module)
last_import = None
for node in tree.body:
if not isinstance(node, (ast.ImportFrom, ast.Import)):
break
last_import = node
if last_import is None:
position = start_of(tree.body[0].first_token) # type: ignore
else:
last_token = last_import.last_token # type: ignore
while True:
next_token = token.next_token(last_token)
if last_token.end[0] == next_token.end[0]:
last_token = next_token
else:
break
position = end_of(last_token)
code = ""
for module, name in to_add:
code += f"\nfrom {module} import {name}\n"
change.insert(position, code, filename=filename)
python-inline-snapshot-0.20.7/src/inline_snapshot/_flags.py 0000664 0000000 0000000 00000001756 14764761403 0024050 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from typing import Set
from typing import cast
from ._types import Category
class Flags:
"""
fix: the value needs to be changed to pass the tests
update: the value should be updated because the token-stream has changed
create: the snapshot is empty `snapshot()`
trim: the snapshot contains more values than neccessary. 1 could be trimmed in `5 in snapshot([1,5])`.
"""
def __init__(self, flags: set[Category] = set()):
self.create = "create" in flags
self.fix = "fix" in flags
self.trim = "trim" in flags
self.update = "update" in flags
def to_set(self) -> set[Category]:
return cast(Set[Category], {k for k, v in self.__dict__.items() if v})
def __iter__(self):
return (k for k, v in self.__dict__.items() if v)
def __repr__(self):
return f"Flags({self.to_set()})"
@staticmethod
def all() -> Flags:
return Flags({"fix", "create", "update", "trim"})
python-inline-snapshot-0.20.7/src/inline_snapshot/_format.py 0000664 0000000 0000000 00000005607 14764761403 0024243 0 ustar 00root root 0000000 0000000 import subprocess as sp
import warnings
from rich.markup import escape
from . import _config
from ._problems import raise_problem
def enforce_formatting():
return _config.config.format_command is not None
def file_mode_for_path(path):
from black import FileMode
from black import find_pyproject_toml
from black import parse_pyproject_toml
mode = FileMode()
pyproject_path = find_pyproject_toml((), path)
if pyproject_path is not None:
config = parse_pyproject_toml(pyproject_path)
if "line_length" in config:
mode.line_length = int(config["line_length"])
if "skip_magic_trailing_comma" in config:
mode.magic_trailing_comma = not config["skip_magic_trailing_comma"]
if "skip_string_normalization" in config:
# The ``black`` command line argument is
# ``--skip-string-normalization``, but the parameter for
# ``black.Mode`` needs to be the opposite boolean of
# ``skip-string-normalization``, hence the inverse boolean
mode.string_normalization = not config["skip_string_normalization"]
if "preview" in config:
mode.preview = config["preview"]
return mode
def format_code(text, filename):
if _config.config.format_command is not None:
format_command = _config.config.format_command.format(filename=filename)
result = sp.run(
format_command, shell=True, input=text.encode("utf-8"), capture_output=True
)
if result.returncode != 0:
raise_problem(
f"""\
[b]The format_command '{escape(format_command)}' caused the following error:[/b]
"""
+ result.stdout.decode("utf-8")
+ result.stderr.decode("utf-8")
)
return text
return result.stdout.decode("utf-8")
try:
from black import format_str
except ImportError:
raise_problem(
f"""\
[b]inline-snapshot is not able to format your code.[/b]
This issue can be solved by:
* installing {escape('inline-snapshot[black]')} which gives you the same formatting like in older versions
* adding a `format-command` to your pyproject.toml (see [link=https://15r10nk.github.io/inline-snapshot/latest/configuration/#format-command]https://15r10nk.github.io/inline-snapshot/latest/configuration/#format-command[/link] for more information).
"""
)
return text
with warnings.catch_warnings():
warnings.simplefilter("ignore")
mode = file_mode_for_path(filename)
try:
return format_str(text, mode=mode)
except:
raise_problem(
"""\
[b]black could not format your code, which might be caused by this issue:[/b]
[link=https://github.com/15r10nk/inline-snapshot/issues/138]https://github.com/15r10nk/inline-snapshot/issues/138[/link]\
"""
)
return text
python-inline-snapshot-0.20.7/src/inline_snapshot/_global_state.py 0000664 0000000 0000000 00000001633 14764761403 0025406 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import contextlib
from dataclasses import dataclass
from dataclasses import field
from typing import TYPE_CHECKING
from typing import Generator
from ._flags import Flags
if TYPE_CHECKING:
from ._external import DiscStorage
@dataclass
class State:
# snapshot
missing_values: int = 0
incorrect_values: int = 0
snapshots: dict = field(default_factory=dict)
update_flags: Flags = field(default_factory=Flags)
active: bool = True
files_with_snapshots: set[str] = field(default_factory=set)
# external
storage: DiscStorage | None = None
_current = State()
_current.active = False
def state() -> State:
global _current
return _current
@contextlib.contextmanager
def snapshot_env() -> Generator[State]:
global _current
old = _current
_current = State()
try:
yield _current
finally:
_current = old
python-inline-snapshot-0.20.7/src/inline_snapshot/_inline_snapshot.py 0000664 0000000 0000000 00000007704 14764761403 0026150 0 ustar 00root root 0000000 0000000 import ast
import inspect
from typing import Any
from typing import TypeVar
from typing import cast
from executing import Source
from inline_snapshot._source_file import SourceFile
from ._adapter.adapter import AdapterContext
from ._adapter.adapter import FrameContext
from ._change import CallArg
from ._global_state import state
from ._sentinels import undefined
from ._snapshot.undecided_value import UndecidedValue
class ReprWrapper:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __repr__(self):
return self.func.__name__
_T = TypeVar("_T")
def repr_wrapper(func: _T) -> _T:
return ReprWrapper(func) # type: ignore
@repr_wrapper
def snapshot(obj: Any = undefined) -> Any:
"""`snapshot()` is a placeholder for some value.
`pytest --inline-snapshot=create` will create the value which matches your conditions.
>>> assert 5 == snapshot()
>>> assert 5 <= snapshot()
>>> assert 5 >= snapshot()
>>> assert 5 in snapshot()
`snapshot()[key]` can be used to create sub-snapshots.
The generated value will be inserted as argument to `snapshot()`
>>> assert 5 == snapshot(5)
`snapshot(value)` has general the semantic of an noop which returns `value`.
"""
if not state().active:
if obj is undefined:
raise AssertionError(
"your snapshot is missing a value run pytest with --inline-snapshot=create"
)
else:
return obj
frame = inspect.currentframe()
assert frame is not None
frame = frame.f_back
assert frame is not None
frame = frame.f_back
assert frame is not None
expr = Source.executing(frame)
source = cast(Source, getattr(expr, "source", None) if expr is not None else None)
context = AdapterContext(
file=SourceFile(source),
frame=FrameContext(globals=frame.f_globals, locals=frame.f_locals),
)
module = inspect.getmodule(frame)
if module is not None and module.__file__ is not None:
state().files_with_snapshots.add(module.__file__)
key = id(frame.f_code), frame.f_lasti
if key not in state().snapshots:
node = expr.node
if node is None:
# we can run without knowing of the calling expression but we will not be able to fix code
state().snapshots[key] = SnapshotReference(obj, None, context)
else:
assert isinstance(node, ast.Call)
state().snapshots[key] = SnapshotReference(obj, expr, context)
else:
state().snapshots[key]._re_eval(obj, context)
return state().snapshots[key]._value
def used_externals(tree):
return [
n.args[0].value
for n in ast.walk(tree)
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Name)
and n.func.id == "external"
and n.args
and isinstance(n.args[0], ast.Constant)
]
class SnapshotReference:
def __init__(self, value, expr, context: AdapterContext):
self._expr = expr
node = expr.node.args[0] if expr is not None and expr.node.args else None
self._value = UndecidedValue(value, node, context)
def _changes(self):
if (
self._value._old_value is undefined
if self._expr is None
else not self._expr.node.args
):
if self._value._new_value is undefined:
return
new_code = self._value._new_code()
yield CallArg(
flag="create",
file=self._value._file,
node=self._expr.node if self._expr is not None else None,
arg_pos=0,
arg_name=None,
new_code=new_code,
new_value=self._value._new_value,
)
else:
yield from self._value._get_changes()
def _re_eval(self, obj, context: AdapterContext):
self._value._re_eval(obj, context)
python-inline-snapshot-0.20.7/src/inline_snapshot/_is.py 0000664 0000000 0000000 00000000451 14764761403 0023356 0 ustar 00root root 0000000 0000000 import typing
from typing import TYPE_CHECKING
if TYPE_CHECKING:
T = typing.TypeVar("T")
def Is(v: T) -> T:
return v
else:
class Is:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other
python-inline-snapshot-0.20.7/src/inline_snapshot/_problems.py 0000664 0000000 0000000 00000000566 14764761403 0024575 0 ustar 00root root 0000000 0000000 from rich.console import Console
all_problems = set()
def raise_problem(message):
all_problems.add(message)
def report_problems(console: Console):
global all_problems
if not all_problems:
return
console.rule("[red]Problems")
for problem in all_problems:
console.print(f"{problem}")
console.print()
all_problems = set()
python-inline-snapshot-0.20.7/src/inline_snapshot/_rewrite_code.py 0000664 0000000 0000000 00000014004 14764761403 0025415 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import logging
import pathlib
import sys
from collections import defaultdict
from collections.abc import Iterable
from dataclasses import dataclass
from difflib import unified_diff
from itertools import islice
import asttokens.util
from asttokens import LineNumbers
from ._format import enforce_formatting
from ._format import format_code
if sys.version_info >= (3, 10):
from itertools import pairwise
else:
from itertools import tee
def pairwise(iterable): # type: ignore
a, b = tee(iterable)
next(b, None)
return zip(a, b)
@dataclass(order=True)
class SourcePosition:
lineno: int
col_offset: int
def offset(self, line_numbers):
return line_numbers.line_to_offset(self.lineno, self.col_offset)
@dataclass(order=True)
class SourceRange:
start: SourcePosition
end: SourcePosition
def __post_init__(self):
if self.start > self.end:
raise ValueError("range start should be lower then end")
@dataclass(order=True)
class Replacement:
range: SourceRange
text: str
change_id: int = 0
def start_of(obj) -> SourcePosition:
if isinstance(obj, asttokens.util.Token):
return SourcePosition(lineno=obj.start[0], col_offset=obj.start[1])
if isinstance(obj, SourcePosition):
return obj
if isinstance(obj, SourceRange):
return obj.start
if isinstance(obj, tuple) and len(obj) == 2:
return SourcePosition(lineno=obj[0], col_offset=obj[1])
assert False
def end_of(obj) -> SourcePosition:
if isinstance(obj, asttokens.util.Token):
return SourcePosition(lineno=obj.end[0], col_offset=obj.end[1])
if isinstance(obj, SourceRange):
return obj.end
return start_of(obj)
def range_of(obj):
if isinstance(obj, tuple) and len(obj) == 2:
return SourceRange(start_of(obj[0]), end_of(obj[1]))
return SourceRange(start_of(obj), end_of(obj))
class UsageError(Exception):
pass
class Change: # ChangeSet
_next_change_id = 0
def __init__(self, change_recorder):
self.change_recorder = change_recorder
self.change_recorder._changes.append(self)
self.change_id = self._next_change_id
type(self)._next_change_id += 1
def replace(self, node, new_contend, *, filename):
assert isinstance(new_contend, str)
self._replace(
filename,
range_of(node),
new_contend,
)
def delete(self, node, *, filename):
self.replace(node, "", filename=filename)
def insert(self, node, new_content, *, filename):
self.replace(start_of(node), new_content, filename=filename)
def _replace(self, filename, range, new_contend):
source = self.change_recorder.get_source(filename)
source.replacements.append(
Replacement(range=range, text=new_contend, change_id=self.change_id)
)
source._check()
class SourceFile:
def __init__(self, filename: pathlib.Path):
self.replacements: list[Replacement] = []
self.filename = filename
self.source = self.filename.read_text("utf-8")
def rewrite(self):
new_code = self.new_code()
with open(self.filename, "bw") as code:
code.write(new_code.encode())
def virtual_write(self):
self.source = self.new_code()
def _check(self):
replacements = list(self.replacements)
replacements.sort()
for r in replacements:
assert r.range.start <= r.range.end, r
for lhs, rhs in pairwise(replacements):
assert lhs.range.end <= rhs.range.start, (lhs, rhs)
def new_code(self) -> str:
"""Returns the new file contend or None if there are no replacepents to
apply."""
replacements = list(self.replacements)
replacements.sort()
self._check()
code = self.filename.read_text("utf-8")
format_whole_file = enforce_formatting() or code == format_code(
code, self.filename
)
if not format_whole_file:
logging.info(f"file is not formatted with black: {self.filename}")
import black
logging.info(f"black version: {black.__version__}")
line_numbers = LineNumbers(code)
new_code = asttokens.util.replace(
code,
[
(
r.range.start.offset(line_numbers),
r.range.end.offset(line_numbers),
r.text,
)
for r in replacements
],
)
if format_whole_file:
new_code = format_code(new_code, self.filename)
return new_code
def diff(self):
return "\n".join(
islice(
unified_diff(self.source.splitlines(), self.new_code().splitlines()),
2,
None,
)
).strip()
class ChangeRecorder:
def __init__(self):
self._source_files = defaultdict(SourceFile)
self._changes = []
def get_source(self, filename) -> SourceFile:
filename = pathlib.Path(filename)
if filename not in self._source_files:
self._source_files[filename] = SourceFile(filename)
return self._source_files[filename]
def files(self) -> Iterable[SourceFile]:
return self._source_files.values()
def new_change(self):
return Change(self)
def num_fixes(self):
changes = set()
for file in self._source_files.values():
changes.update(change.change_id for change in file.replacements)
return len(changes)
def fix_all(self):
for file in self._source_files.values():
file.rewrite()
def virtual_write(self):
for file in self._source_files.values():
file.virtual_write()
def dump(self): # pragma: no cover
for file in self._source_files.values():
print("file:", file.filename)
for change in file.replacements:
print(" change:", change)
python-inline-snapshot-0.20.7/src/inline_snapshot/_sentinels.py 0000664 0000000 0000000 00000000020 14764761403 0024737 0 ustar 00root root 0000000 0000000 undefined = ...
python-inline-snapshot-0.20.7/src/inline_snapshot/_snapshot/ 0000775 0000000 0000000 00000000000 14764761403 0024230 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/src/inline_snapshot/_snapshot/collection_value.py 0000664 0000000 0000000 00000005010 14764761403 0030125 0 ustar 00root root 0000000 0000000 import ast
from typing import Iterator
from .._change import Change
from .._change import Delete
from .._change import ListInsert
from .._change import Replace
from .._global_state import state
from .._sentinels import undefined
from .._utils import value_to_token
from .generic_value import GenericValue
from .generic_value import clone
from .generic_value import ignore_old_value
class CollectionValue(GenericValue):
_current_op = "x in snapshot"
def __contains__(self, item):
if self._old_value is undefined:
state().missing_values += 1
if self._new_value is undefined:
self._new_value = [clone(item)]
else:
if item not in self._new_value:
self._new_value.append(clone(item))
if ignore_old_value() or self._old_value is undefined:
return True
else:
return self._return(item in self._old_value)
def _new_code(self):
return self._file._value_to_code(self._new_value)
def _get_changes(self) -> Iterator[Change]:
if self._ast_node is None:
elements = [None] * len(self._old_value)
else:
assert isinstance(self._ast_node, ast.List)
elements = self._ast_node.elts
for old_value, old_node in zip(self._old_value, elements):
if old_value not in self._new_value:
yield Delete(
flag="trim",
file=self._file,
node=old_node,
old_value=old_value,
)
continue
# check for update
new_token = value_to_token(old_value)
if (
old_node is not None
and self._file._token_of_node(old_node) != new_token
):
new_code = self._file._token_to_code(new_token)
yield Replace(
node=old_node,
file=self._file,
new_code=new_code,
flag="update",
old_value=old_value,
new_value=old_value,
)
new_values = [v for v in self._new_value if v not in self._old_value]
if new_values:
yield ListInsert(
flag="fix",
file=self._file,
node=self._ast_node,
position=len(self._old_value),
new_code=[self._file._value_to_code(v) for v in new_values],
new_values=new_values,
)
python-inline-snapshot-0.20.7/src/inline_snapshot/_snapshot/dict_value.py 0000664 0000000 0000000 00000006111 14764761403 0026720 0 ustar 00root root 0000000 0000000 import ast
from typing import Iterator
from .._adapter.adapter import AdapterContext
from .._change import Change
from .._change import Delete
from .._change import DictInsert
from .._global_state import state
from .._inline_snapshot import UndecidedValue
from .._sentinels import undefined
from .generic_value import GenericValue
class DictValue(GenericValue):
_current_op = "snapshot[key]"
def __getitem__(self, index):
if self._new_value is undefined:
self._new_value = {}
if index not in self._new_value:
old_value = self._old_value
if old_value is undefined:
state().missing_values += 1
old_value = {}
child_node = None
if self._ast_node is not None:
assert isinstance(self._ast_node, ast.Dict)
if index in old_value:
pos = list(old_value.keys()).index(index)
child_node = self._ast_node.values[pos]
self._new_value[index] = UndecidedValue(
old_value.get(index, undefined), child_node, self._context
)
return self._new_value[index]
def _re_eval(self, value, context: AdapterContext):
super()._re_eval(value, context)
if self._new_value is not undefined and self._old_value is not undefined:
for key, s in self._new_value.items():
if key in self._old_value:
s._re_eval(self._old_value[key], context)
def _new_code(self):
return (
"{"
+ ", ".join(
[
f"{self._file._value_to_code(k)}: {v._new_code()}"
for k, v in self._new_value.items()
if not isinstance(v, UndecidedValue)
]
)
+ "}"
)
def _get_changes(self) -> Iterator[Change]:
assert self._old_value is not undefined
if self._ast_node is None:
values = [None] * len(self._old_value)
else:
assert isinstance(self._ast_node, ast.Dict)
values = self._ast_node.values
for key, node in zip(self._old_value.keys(), values):
if key in self._new_value:
# check values with same keys
yield from self._new_value[key]._get_changes()
else:
# delete entries
yield Delete("trim", self._file, node, self._old_value[key])
to_insert = []
for key, new_value_element in self._new_value.items():
if key not in self._old_value and not isinstance(
new_value_element, UndecidedValue
):
# add new values
to_insert.append((key, new_value_element._new_code()))
if to_insert:
new_code = [(self._file._value_to_code(k), v) for k, v in to_insert]
yield DictInsert(
"create",
self._file,
self._ast_node,
len(self._old_value),
new_code,
to_insert,
)
python-inline-snapshot-0.20.7/src/inline_snapshot/_snapshot/eq_value.py 0000664 0000000 0000000 00000002315 14764761403 0026404 0 ustar 00root root 0000000 0000000 from typing import Iterator
from typing import List
from inline_snapshot._adapter.adapter import Adapter
from .._change import Change
from .._compare_context import compare_only
from .._global_state import state
from .._sentinels import undefined
from .generic_value import GenericValue
from .generic_value import clone
class EqValue(GenericValue):
_current_op = "x == snapshot"
_changes: List[Change]
def __eq__(self, other):
if self._old_value is undefined:
state().missing_values += 1
if not compare_only() and self._new_value is undefined:
self._changes = []
adapter = Adapter(self._context).get_adapter(self._old_value, other)
it = iter(adapter.assign(self._old_value, self._ast_node, clone(other)))
while True:
try:
self._changes.append(next(it))
except StopIteration as ex:
self._new_value = ex.value
break
return self._return(self._visible_value() == other)
def _new_code(self):
return self._file._value_to_code(self._new_value)
def _get_changes(self) -> Iterator[Change]:
return iter(self._changes)
python-inline-snapshot-0.20.7/src/inline_snapshot/_snapshot/generic_value.py 0000664 0000000 0000000 00000007427 14764761403 0027424 0 ustar 00root root 0000000 0000000 import ast
import copy
from typing import Any
from typing import Iterator
from .._adapter.adapter import AdapterContext
from .._adapter.adapter import get_adapter_type
from .._change import Change
from .._code_repr import code_repr
from .._exceptions import UsageError
from .._global_state import state
from .._sentinels import undefined
from .._types import Snapshot
from .._unmanaged import Unmanaged
from .._unmanaged import update_allowed
def clone(obj):
new = copy.deepcopy(obj)
if not obj == new:
raise UsageError(
f"""\
inline-snapshot uses `copy.deepcopy` to copy objects,
but the copied object is not equal to the original one:
value = {code_repr(obj)}
copied_value = copy.deepcopy(value)
assert value == copied_value
Please fix the way your object is copied or your __eq__ implementation.
"""
)
return new
def ignore_old_value():
return state().update_flags.fix or state().update_flags.update
class GenericValue(Snapshot):
_new_value: Any
_old_value: Any
_current_op = "undefined"
_ast_node: ast.Expr
_context: AdapterContext
@staticmethod
def _return(result):
if not result:
state().incorrect_values += 1
return result
@property
def _file(self):
return self._context.file
def get_adapter(self, value):
return get_adapter_type(value)(self._context)
def _re_eval(self, value, context: AdapterContext):
self._context = context
def re_eval(old_value, node, value):
if isinstance(old_value, Unmanaged):
old_value.value = value
return
assert type(old_value) is type(value)
adapter = self.get_adapter(old_value)
if adapter is not None and hasattr(adapter, "items"):
old_items = adapter.items(old_value, node)
new_items = adapter.items(value, node)
assert len(old_items) == len(new_items)
for old_item, new_item in zip(old_items, new_items):
re_eval(old_item.value, old_item.node, new_item.value)
else:
if update_allowed(old_value):
if not old_value == value:
raise UsageError(
"snapshot value should not change. Use Is(...) for dynamic snapshot parts."
)
else:
assert False, "old_value should be converted to Unmanaged"
re_eval(self._old_value, self._ast_node, value)
def _ignore_old(self):
return (
state().update_flags.fix
or state().update_flags.update
or state().update_flags.create
or self._old_value is undefined
)
def _visible_value(self):
if self._ignore_old():
return self._new_value
else:
return self._old_value
def _get_changes(self) -> Iterator[Change]:
raise NotImplementedError()
def _new_code(self):
raise NotImplementedError()
def __repr__(self):
return repr(self._visible_value())
def _type_error(self, op):
__tracebackhide__ = True
raise TypeError(
f"This snapshot cannot be use with `{op}`, because it was previously used with `{self._current_op}`"
)
def __eq__(self, _other):
__tracebackhide__ = True
self._type_error("==")
def __le__(self, _other):
__tracebackhide__ = True
self._type_error("<=")
def __ge__(self, _other):
__tracebackhide__ = True
self._type_error(">=")
def __contains__(self, _other):
__tracebackhide__ = True
self._type_error("in")
def __getitem__(self, _item):
__tracebackhide__ = True
self._type_error("snapshot[key]")
python-inline-snapshot-0.20.7/src/inline_snapshot/_snapshot/min_max_value.py 0000664 0000000 0000000 00000004507 14764761403 0027434 0 ustar 00root root 0000000 0000000 from typing import Iterator
from .._change import Change
from .._change import Replace
from .._global_state import state
from .._sentinels import undefined
from .._utils import value_to_token
from .generic_value import GenericValue
from .generic_value import clone
from .generic_value import ignore_old_value
class MinMaxValue(GenericValue):
"""Generic implementation for <=, >="""
@staticmethod
def cmp(a, b):
raise NotImplementedError
def _generic_cmp(self, other):
if self._old_value is undefined:
state().missing_values += 1
if self._new_value is undefined:
self._new_value = clone(other)
if self._old_value is undefined or ignore_old_value():
return True
return self._return(self.cmp(self._old_value, other))
else:
if not self.cmp(self._new_value, other):
self._new_value = clone(other)
return self._return(self.cmp(self._visible_value(), other))
def _new_code(self):
return self._file._value_to_code(self._new_value)
def _get_changes(self) -> Iterator[Change]:
new_token = value_to_token(self._new_value)
if not self.cmp(self._old_value, self._new_value):
flag = "fix"
elif not self.cmp(self._new_value, self._old_value):
flag = "trim"
elif (
self._ast_node is not None
and self._file._token_of_node(self._ast_node) != new_token
):
flag = "update"
else:
return
new_code = self._file._token_to_code(new_token)
yield Replace(
node=self._ast_node,
file=self._file,
new_code=new_code,
flag=flag,
old_value=self._old_value,
new_value=self._new_value,
)
class MinValue(MinMaxValue):
"""
handles:
>>> snapshot(5) <= 6
True
>>> 6 >= snapshot(5)
True
"""
_current_op = "x >= snapshot"
@staticmethod
def cmp(a, b):
return a <= b
__le__ = MinMaxValue._generic_cmp
class MaxValue(MinMaxValue):
"""
handles:
>>> snapshot(5) >= 4
True
>>> 4 <= snapshot(5)
True
"""
_current_op = "x <= snapshot"
@staticmethod
def cmp(a, b):
return a >= b
__ge__ = MinMaxValue._generic_cmp
python-inline-snapshot-0.20.7/src/inline_snapshot/_snapshot/undecided_value.py 0000664 0000000 0000000 00000005116 14764761403 0027725 0 ustar 00root root 0000000 0000000 from typing import Iterator
from inline_snapshot._adapter.adapter import adapter_map
from .._adapter.adapter import AdapterContext
from .._adapter.adapter import get_adapter_type
from .._change import Change
from .._change import Replace
from .._sentinels import undefined
from .._unmanaged import Unmanaged
from .._unmanaged import map_unmanaged
from .._utils import value_to_token
from .generic_value import GenericValue
class UndecidedValue(GenericValue):
def __init__(self, old_value, ast_node, context: AdapterContext):
old_value = adapter_map(old_value, map_unmanaged)
self._old_value = old_value
self._new_value = undefined
self._ast_node = ast_node
self._context = context
def _change(self, cls):
self.__class__ = cls
def _new_code(self):
assert False
def _get_changes(self) -> Iterator[Change]:
def handle(node, obj):
adapter = get_adapter_type(obj)
if adapter is not None and hasattr(adapter, "items"):
for item in adapter.items(obj, node):
yield from handle(item.node, item.value)
return
if not isinstance(obj, Unmanaged) and node is not None:
new_token = value_to_token(obj)
if self._file._token_of_node(node) != new_token:
new_code = self._file._token_to_code(new_token)
yield Replace(
node=self._ast_node,
file=self._file,
new_code=new_code,
flag="update",
old_value=self._old_value,
new_value=self._old_value,
)
if self._file._source is not None:
yield from handle(self._ast_node, self._old_value)
# functions which determine the type
def __eq__(self, other):
from .._snapshot.eq_value import EqValue
self._change(EqValue)
return self == other
def __le__(self, other):
from .._snapshot.min_max_value import MinValue
self._change(MinValue)
return self <= other
def __ge__(self, other):
from .._snapshot.min_max_value import MaxValue
self._change(MaxValue)
return self >= other
def __contains__(self, other):
from .._snapshot.collection_value import CollectionValue
self._change(CollectionValue)
return other in self
def __getitem__(self, item):
from .._snapshot.dict_value import DictValue
self._change(DictValue)
return self[item]
python-inline-snapshot-0.20.7/src/inline_snapshot/_source_file.py 0000664 0000000 0000000 00000002440 14764761403 0025242 0 ustar 00root root 0000000 0000000 import tokenize
from pathlib import Path
from executing import Source
from inline_snapshot._format import enforce_formatting
from inline_snapshot._format import format_code
from inline_snapshot._utils import normalize
from inline_snapshot._utils import simple_token
from inline_snapshot._utils import value_to_token
from ._utils import ignore_tokens
class SourceFile:
_source: Source
def __init__(self, source: Source):
self._source = source
@property
def filename(self):
return self._source.filename
def _format(self, text):
if self._source is None or enforce_formatting():
return text
else:
return format_code(text, Path(self._source.filename))
def asttokens(self):
return self._source.asttokens()
def _token_to_code(self, tokens):
return self._format(tokenize.untokenize(tokens)).strip()
def _value_to_code(self, value):
return self._token_to_code(value_to_token(value))
def _token_of_node(self, node):
return list(
normalize(
[
simple_token(t.type, t.string)
for t in self._source.asttokens().get_tokens(node)
if t.type not in ignore_tokens
]
)
)
python-inline-snapshot-0.20.7/src/inline_snapshot/_types.py 0000664 0000000 0000000 00000003615 14764761403 0024114 0 ustar 00root root 0000000 0000000 """The following types are for type checking only."""
from typing import TYPE_CHECKING
from typing import Generic
from typing import Literal
from typing import TypeVar
if TYPE_CHECKING:
from typing_extensions import TypeAlias
T = TypeVar("T")
Snapshot: TypeAlias = T
else:
T = TypeVar("T")
class Snapshot(Generic[T]):
"""Can be used to annotate function arguments which accept snapshot
values.
You can annotate function arguments with `Snapshot[T]` to declare that a snapshot-value can be passed as function argument.
`Snapshot[T]` is a type alias for `T`, which allows you to pass `int` values instead of `int` snapshots.
Example:
``` python
from typing import Optional
from inline_snapshot import snapshot, Snapshot
# required snapshots
def check_in_bounds(value, lower: Snapshot[int], upper: Snapshot[int]):
assert lower <= value <= upper
def test_numbers():
for c in "hello world":
check_in_bounds(ord(c), snapshot(32), snapshot(119))
# use with normal values
check_in_bounds(5, 0, 10)
# optional snapshots
def check_container(
value,
*,
value_repr: Optional[Snapshot[str]] = None,
length: Optional[Snapshot[int]] = None
):
if value_repr is not None:
assert repr(value) == value_repr
if length is not None:
assert len(value) == length
def test_container():
check_container([1, 2], value_repr=snapshot("[1, 2]"), length=snapshot(2))
check_container({1, 1}, length=snapshot(1))
```
"""
Category = Literal["update", "fix", "create", "trim"]
"""See [categories](categories.md)"""
python-inline-snapshot-0.20.7/src/inline_snapshot/_unmanaged.py 0000664 0000000 0000000 00000002127 14764761403 0024704 0 ustar 00root root 0000000 0000000 from ._is import Is
from ._types import Snapshot
try:
import dirty_equals # type: ignore
except ImportError: # pragma: no cover
def is_dirty_equal(value):
return False
else:
def is_dirty_equal(value):
return isinstance(value, dirty_equals.DirtyEquals) or (
isinstance(value, type) and issubclass(value, dirty_equals.DirtyEquals)
)
def update_allowed(value):
global unmanaged_types
return not (is_dirty_equal(value) or isinstance(value, tuple(unmanaged_types))) # type: ignore
unmanaged_types = [Is, Snapshot]
def is_unmanaged(value):
return not update_allowed(value)
def declare_unmanaged(typ):
global unmanaged_types
unmanaged_types.append(typ)
return typ
class Unmanaged:
def __init__(self, value):
self.value = value
def __eq__(self, other):
assert not isinstance(other, Unmanaged)
return self.value == other
def __repr__(self):
return repr(self.value)
def map_unmanaged(value):
if is_unmanaged(value):
return Unmanaged(value)
else:
return value
python-inline-snapshot-0.20.7/src/inline_snapshot/_utils.py 0000664 0000000 0000000 00000011121 14764761403 0024077 0 ustar 00root root 0000000 0000000 import ast
import io
import token
import tokenize
from collections import namedtuple
from ._code_repr import code_repr
def normalize_strings(token_sequence):
"""Normalize string concattenanion.
"a" "b" -> "ab"
"""
current_string = None
for t in token_sequence:
if (
t.type == token.STRING
and not t.string.startswith(("'''", '"""', "b'''", 'b"""'))
and t.string.startswith(("'", '"', "b'", 'b"'))
):
if current_string is None:
current_string = ast.literal_eval(t.string)
else:
current_string += ast.literal_eval(t.string)
continue
if current_string is not None:
yield simple_token(token.STRING, repr(current_string))
current_string = None
yield t
if current_string is not None:
yield simple_token(token.STRING, repr(current_string))
def skip_trailing_comma(token_sequence):
token_sequence = list(token_sequence)
for index, token in enumerate(token_sequence):
if index + 1 < len(token_sequence):
next_token = token_sequence[index + 1]
if token.string == "," and next_token.string in ("]", ")", "}"):
continue
yield token
def normalize(token_sequence):
return skip_trailing_comma(normalize_strings(token_sequence))
ignore_tokens = (token.NEWLINE, token.ENDMARKER, token.NL)
# based on ast.unparse
def _str_literal_helper(string, *, quote_types):
"""Helper for writing string literals, minimizing escapes.
Returns the tuple (string literal to write, possible quote types).
"""
def escape_char(c):
# \n and \t are non-printable, but we only escape them if
# escape_special_whitespace is True
if c in "\n\t":
return c
# Always escape backslashes and other non-printable characters
if c == "\\" or not c.isprintable():
return c.encode("unicode_escape").decode("ascii")
if c == extra:
return "\\" + c
return c
extra = ""
if "'''" in string and '"""' in string:
extra = '"' if string.count("'") >= string.count('"') else "'"
escaped_string = "".join(map(escape_char, string))
possible_quotes = [q for q in quote_types if q not in escaped_string]
if escaped_string:
# Sort so that we prefer '''"''' over """\""""
possible_quotes.sort(key=lambda q: q[0] == escaped_string[-1])
# If we're using triple quotes and we'd need to escape a final
# quote, escape it
if possible_quotes[0][0] == escaped_string[-1]:
assert len(possible_quotes[0]) == 3
escaped_string = escaped_string[:-1] + "\\" + escaped_string[-1]
return escaped_string, possible_quotes
def triple_quote(string):
"""Write string literal value with a best effort attempt to avoid
backslashes."""
string, quote_types = _str_literal_helper(string, quote_types=['"""', "'''"])
quote_type = quote_types[0]
string = "\\\n" + string
if not string.endswith("\n"):
string = string + "\\\n"
return f"{quote_type}{string}{quote_type}"
class simple_token(namedtuple("simple_token", "type,string")):
def __eq__(self, other):
if self.type == other.type == 3:
if any(
s.startswith(suffix)
for s in (self.string, other.string)
for suffix in ("f", "rf", "Rf", "F", "rF", "RF")
):
return False
return ast.literal_eval(self.string) == ast.literal_eval(
other.string
) and self.string.replace("'", '"') == other.string.replace("'", '"')
else:
return super().__eq__(other)
def value_to_token(value):
input = io.StringIO(code_repr(value))
def map_string(tok):
"""Convert strings with newlines in triple quoted strings."""
if tok.type == token.STRING:
s = ast.literal_eval(tok.string)
if isinstance(s, str) and (
("\n" in s and s[-1] != "\n") or s.count("\n") > 1
):
# unparse creates a triple quoted string here,
# because it thinks that the string should be a docstring
tripple_quoted_string = triple_quote(s)
assert ast.literal_eval(tripple_quoted_string) == s
return simple_token(tok.type, tripple_quoted_string)
return simple_token(tok.type, tok.string)
return [
map_string(t)
for t in tokenize.generate_tokens(input.readline)
if t.type not in ignore_tokens
]
python-inline-snapshot-0.20.7/src/inline_snapshot/extra.py 0000664 0000000 0000000 00000014003 14764761403 0023725 0 ustar 00root root 0000000 0000000 """The following functions are build on top of inline-snapshot and could also
be implemented in an extra library.
They are part of inline-snapshot because they are general useful and do
not depend on other libraries.
"""
import contextlib
import io
import warnings
from contextlib import redirect_stderr
from contextlib import redirect_stdout
from typing import List
from typing import Tuple
from typing import Union
from inline_snapshot._types import Snapshot
@contextlib.contextmanager
def raises(exception: Snapshot[str]):
"""Check that an exception is raised.
Parameters:
exception: snapshot which is compared with `#!python f"{type}: {message}"` if an exception occured or `#!python ""` if no exception was raised.
=== "original"
``` python
from inline_snapshot import snapshot
from inline_snapshot.extra import raises
def test_raises():
with raises(snapshot()):
1 / 0
```
=== "--inline-snapshot=create"
``` python hl_lines="6"
from inline_snapshot import snapshot
from inline_snapshot.extra import raises
def test_raises():
with raises(snapshot("ZeroDivisionError: division by zero")):
1 / 0
```
"""
try:
yield
except Exception as ex:
msg = str(ex)
if "\n" in msg:
assert f"{type(ex).__name__}:\n{ex}" == exception
else:
assert f"{type(ex).__name__}: {ex}" == exception
else:
assert "" == exception
@contextlib.contextmanager
def prints(*, stdout: Snapshot[str] = "", stderr: Snapshot[str] = ""):
"""Uses `contextlib.redirect_stderr/stdout` to capture the output and
compare it with the snapshots. `dirty_equals.IsStr` can be used to ignore
the output if needed.
Parameters:
stdout: snapshot which is compared to the recorded output
stderr: snapshot which is compared to the recorded error output
=== "original"
``` python
from inline_snapshot import snapshot
from inline_snapshot.extra import prints
import sys
def test_prints():
with prints(stdout=snapshot(), stderr=snapshot()):
print("hello world")
print("some error", file=sys.stderr)
```
=== "--inline-snapshot=create"
``` python hl_lines="7 8 9"
from inline_snapshot import snapshot
from inline_snapshot.extra import prints
import sys
def test_prints():
with prints(
stdout=snapshot("hello world\\n"), stderr=snapshot("some error\\n")
):
print("hello world")
print("some error", file=sys.stderr)
```
=== "ignore stdout"
``` python hl_lines="3 9 10"
from inline_snapshot import snapshot
from inline_snapshot.extra import prints
from dirty_equals import IsStr
import sys
def test_prints():
with prints(
stdout=IsStr(),
stderr=snapshot("some error\\n"),
):
print("hello world")
print("some error", file=sys.stderr)
```
"""
with redirect_stdout(io.StringIO()) as stdout_io:
with redirect_stderr(io.StringIO()) as stderr_io:
yield
assert stderr_io.getvalue() == stderr
assert stdout_io.getvalue() == stdout
Warning = Union[str, Tuple[int, str], Tuple[str, str], Tuple[str, int, str]]
@contextlib.contextmanager
def warns(
expected_warnings: Snapshot[List[Warning]],
/,
include_line: bool = False,
include_file: bool = False,
):
"""
Captures warnings with `warnings.catch_warnings` and compares them against expected warnings.
Parameters:
expected_warnings: Snapshot containing a list of expected warnings.
include_line: If `True`, each expected warning is a tuple `(linenumber, message)`.
include_file: If `True`, each expected warning is a tuple `(filename, message)`.
The format of the expected warning:
- `(filename, linenumber, message)` if both `include_line` and `include_file` are `True`.
- `(linenumber, message)` if only `include_line` is `True`.
- `(filename, message)` if only `include_file` is `True`.
- A string `message` if both are `False`.
=== "original"
``` python
from inline_snapshot import snapshot
from inline_snapshot.extra import warns
from warnings import warn
def test_warns():
with warns(snapshot(), include_line=True):
warn("some problem")
```
=== "--inline-snapshot=create"
``` python hl_lines="7"
from inline_snapshot import snapshot
from inline_snapshot.extra import warns
from warnings import warn
def test_warns():
with warns(snapshot([(8, "UserWarning: some problem")]), include_line=True):
warn("some problem")
```
"""
with warnings.catch_warnings(record=True) as result:
warnings.simplefilter("always")
yield
def make_warning(w):
message = f"{w.category.__name__}: {w.message}"
if not include_line and not include_file:
return message
message = (message,)
if include_line:
message = (w.lineno, *message)
if include_file:
message = (w.filename, *message)
return message
assert [make_warning(w) for w in result] == expected_warnings
python-inline-snapshot-0.20.7/src/inline_snapshot/py.typed 0000664 0000000 0000000 00000000000 14764761403 0023717 0 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/src/inline_snapshot/pydantic_fix.py 0000664 0000000 0000000 00000001130 14764761403 0025260 0 ustar 00root root 0000000 0000000 from ._types import Snapshot
is_fixed = False
def pydantic_fix():
global is_fixed
if is_fixed:
return # pragma: no cover
is_fixed = True
try:
from pydantic import BaseModel
except ImportError: # pragma: no cover
return
import pydantic
if not pydantic.version.VERSION.startswith("1."):
return
origin_eq = BaseModel.__eq__
def new_eq(self, other):
if isinstance(other, Snapshot): # type: ignore
return other == self
else:
return origin_eq(self, other)
BaseModel.__eq__ = new_eq
python-inline-snapshot-0.20.7/src/inline_snapshot/pytest_plugin.py 0000664 0000000 0000000 00000031615 14764761403 0025520 0 ustar 00root root 0000000 0000000 import ast
import os
import sys
from pathlib import Path
import pytest
from executing import is_pytest_compatible
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Confirm
from rich.syntax import Syntax
from inline_snapshot._problems import report_problems
from inline_snapshot.pydantic_fix import pydantic_fix
from . import _config
from . import _external
from . import _find_external
from ._change import apply_all
from ._code_repr import used_hasrepr
from ._find_external import ensure_import
from ._flags import Flags
from ._global_state import snapshot_env
from ._global_state import state
from ._inline_snapshot import used_externals
from ._rewrite_code import ChangeRecorder
from ._snapshot.generic_value import GenericValue
pytest.register_assert_rewrite("inline_snapshot.extra")
pytest.register_assert_rewrite("inline_snapshot.testing._example")
if sys.version_info >= (3, 13):
# fixes #186
try:
import readline # noqa
except ModuleNotFoundError: # pragma: no cover
# should fix #189
pass
def pytest_addoption(parser, pluginmanager):
group = parser.getgroup("inline-snapshot")
group.addoption(
"--inline-snapshot",
metavar="(disable,short-report,report,review,create,update,trim,fix)*",
dest="inline_snapshot",
help="update specific snapshot values:\n"
"disable: disable the snapshot logic\n"
"short-report: show a short report\n"
"report: show a longer report with a diff report\n"
"review: allow to approve the changes interactive\n"
"create: creates snapshots which are currently not defined\n"
"update: update snapshots even if they are defined\n"
"trim: changes the snapshot in a way which will make the snapshot more precise.\n"
"fix: change snapshots which currently break your tests\n",
)
config_path = Path("pyproject.toml")
if config_path.exists():
config = _config.read_config(config_path)
for name, value in config.shortcuts.items():
value = ",".join(value)
group.addoption(
f"--{name}",
action="store_const",
const=value,
dest="inline_snapshot",
help=f'shortcut for "--inline-snapshot={value}"',
)
categories = Flags.all().to_set()
flags = set()
def xdist_running(config):
return (
hasattr(config.option, "numprocesses")
and config.option.numprocesses is not None
)
def is_implementation_supported():
return sys.implementation.name == "cpython"
def pytest_configure(config):
global flags
directory = config.rootpath
while not (pyproject := directory / "pyproject.toml").exists():
if directory == directory.parent:
break
directory = directory.parent
_config.config = _config.read_config(pyproject)
if config.option.inline_snapshot is None:
flags = set(_config.config.default_flags)
else:
flags = config.option.inline_snapshot.split(",")
flags = {flag for flag in flags if flag}
if xdist_running(config) and flags - {"disable"}:
raise pytest.UsageError(
f"--inline-snapshot={','.join(flags)} can not be combined with xdist"
)
unknown_flags = flags - categories - {"disable", "review", "report", "short-report"}
if unknown_flags:
raise pytest.UsageError(
f"--inline-snapshot={','.join(sorted(unknown_flags))} is a unknown flag"
)
if "disable" in flags and flags != {"disable"}:
raise pytest.UsageError(
f"--inline-snapshot=disable can not be combined with other flags ({', '.join(flags-{'disable'})})"
)
if xdist_running(config) or not is_implementation_supported():
state().active = False
elif flags & {"review"}:
state().active = True
state().update_flags = Flags.all()
else:
state().active = "disable" not in flags
state().update_flags = Flags(flags & categories)
external_storage = (
_config.config.storage_dir or config.rootpath / ".inline-snapshot"
) / "external"
state().storage = _external.DiscStorage(external_storage)
if flags - {"short-report", "disable"} and not is_pytest_compatible():
# hack to disable the assertion rewriting
# I found no other way because the hook gets installed early
sys.meta_path = [
e for e in sys.meta_path if type(e).__name__ != "AssertionRewritingHook"
]
pydantic_fix()
state().storage.prune_new_files()
def is_xfail(request):
if not "xfail" in request.keywords:
return False
xfail = request.keywords["xfail"]
if xfail.args and xfail.args[0] == False:
return False
return True
@pytest.fixture(autouse=True)
def snapshot_check(request):
state().missing_values = 0
state().incorrect_values = 0
if is_xfail(request):
with snapshot_env() as local_state:
local_state.active = False
yield
return
else:
yield
missing_values = state().missing_values
incorrect_values = state().incorrect_values
if missing_values != 0:
pytest.fail(
(
"your snapshot is missing one value."
if missing_values == 1
else f"your snapshot is missing {missing_values} values."
),
pytrace=False,
)
if incorrect_values != 0:
pytest.fail(
"some snapshots in this test have incorrect values.",
pytrace=False,
)
def pytest_assertrepr_compare(config, op, left, right):
results = []
if isinstance(left, GenericValue):
results = config.hook.pytest_assertrepr_compare(
config=config, op=op, left=left._visible_value(), right=right
)
if isinstance(right, GenericValue):
results = config.hook.pytest_assertrepr_compare(
config=config, op=op, left=left, right=right._visible_value()
)
external_used = False
if isinstance(right, _external.external):
external_used = True
if right._suffix == ".txt":
right = right._load_value().decode()
else:
right = right._load_value()
if isinstance(left, _external.external):
external_used = True
if left._suffix == ".txt":
left = left._load_value().decode()
else:
left = left._load_value()
if external_used:
results = config.hook.pytest_assertrepr_compare(
config=config, op=op, left=left, right=right
)
if results:
return results[0]
def pytest_terminal_summary(terminalreporter, exitstatus, config):
if xdist_running(config):
if flags != {"disable"}:
terminalreporter.section("inline snapshot")
terminalreporter.write(
"INFO: inline-snapshot was disabled because you used xdist\n"
)
return
if not is_implementation_supported():
if flags != {"disable"}:
terminalreporter.section("inline snapshot")
terminalreporter.write(
f"INFO: inline-snapshot was disabled because {sys.implementation.name} is not supported\n"
)
return
if not state().active:
return
terminalreporter.section("inline snapshot")
capture = config.pluginmanager.getplugin("capturemanager")
# --inline-snapshot
def apply_changes(flag):
if flag in flags:
console.print(
f"These changes will be applied, because you used [b]--inline-snapshot={flag}[/]",
highlight=False,
)
console.print()
return True
if "review" in flags:
result = Confirm.ask(
f"[bold]do you want to [blue]{flag}[/] these snapshots?[/]",
default=False,
)
console.print()
return result
else:
console.print("These changes are not applied.")
console.print(
f"Use [b]--inline-snapshot={flag}[/] to apply them, or use the interactive mode with [b]--inline-snapshot=review[/]",
highlight=False,
)
console.print()
return False
# auto mode
changes = {f: [] for f in Flags.all()}
snapshot_changes = {f: 0 for f in Flags.all()}
for snapshot in state().snapshots.values():
all_categories = set()
for change in snapshot._changes():
changes[change.flag].append(change)
all_categories.add(change.flag)
for category in all_categories:
snapshot_changes[category] += 1
capture.suspend_global_capture(in_=True)
try:
console = Console(
highlight=False,
)
if "short-report" in flags:
def report(flag, message, message_n):
num = snapshot_changes[flag]
if num and not getattr(state().update_flags, flag):
console.print(
message if num == 1 else message.format(num=num),
highlight=False,
)
report(
"fix",
"Error: one snapshot has incorrect values ([b]--inline-snapshot=fix[/])",
"Error: {num} snapshots have incorrect values ([b]--inline-snapshot=fix[/])",
)
report(
"trim",
"Info: one snapshot can be trimmed ([b]--inline-snapshot=trim[/])",
"Info: {num} snapshots can be trimmed ([b]--inline-snapshot=trim[/])",
)
report(
"create",
"Error: one snapshot is missing a value ([b]--inline-snapshot=create[/])",
"Error: {num} snapshots are missing values ([b]--inline-snapshot=create[/])",
)
report(
"update",
"Info: one snapshot changed its representation ([b]--inline-snapshot=update[/])",
"Info: {num} snapshots changed their representation ([b]--inline-snapshot=update[/])",
)
if sum(snapshot_changes.values()) != 0:
console.print(
"\nYou can also use [b]--inline-snapshot=review[/] to approve the changes interactively",
highlight=False,
)
return
if not is_pytest_compatible():
assert not any(
type(e).__name__ == "AssertionRewritingHook" for e in sys.meta_path
)
used_changes = []
for flag in Flags.all():
if not changes[flag]:
continue
if not {"review", "report", flag} & flags:
continue
console.rule(f"[yellow bold]{flag.capitalize()} snapshots")
cr = ChangeRecorder()
apply_all(used_changes, cr)
cr.virtual_write()
apply_all(changes[flag], cr)
for file in cr.files():
diff = file.diff()
if diff:
name = file.filename.relative_to(Path.cwd())
console.print(
Panel(
Syntax(diff, "diff", theme="ansi_light"),
title=str(name),
box=(
box.ASCII
if os.environ.get("TERM", "") == "unknown"
else box.ROUNDED
),
)
)
if apply_changes(flag):
used_changes += changes[flag]
report_problems(console)
if used_changes:
cr = ChangeRecorder()
apply_all(used_changes, cr)
for test_file in cr.files():
tree = ast.parse(test_file.new_code())
used = used_externals(tree)
required_imports = []
if used:
required_imports.append("external")
if used_hasrepr(tree):
required_imports.append("HasRepr")
if required_imports:
ensure_import(
test_file.filename,
{"inline_snapshot": required_imports},
cr,
)
for external_name in used:
state().storage.persist(external_name)
cr.fix_all()
unused_externals = _find_external.unused_externals()
if unused_externals and state().update_flags.trim:
for name in unused_externals:
assert state().storage
state().storage.remove(name)
terminalreporter.write(
f"removed {len(unused_externals)} unused externals\n"
)
finally:
capture.resume_global_capture()
return
python-inline-snapshot-0.20.7/src/inline_snapshot/syntax_warnings.py 0000664 0000000 0000000 00000000143 14764761403 0026040 0 ustar 00root root 0000000 0000000 class InlineSnapshotSyntaxWarning(Warning):
pass
class InlineSnapshotInfo(Warning):
pass
python-inline-snapshot-0.20.7/src/inline_snapshot/testing/ 0000775 0000000 0000000 00000000000 14764761403 0023707 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/src/inline_snapshot/testing/__init__.py 0000664 0000000 0000000 00000000066 14764761403 0026022 0 ustar 00root root 0000000 0000000 from ._example import Example
__all__ = ("Example",)
python-inline-snapshot-0.20.7/src/inline_snapshot/testing/_example.py 0000664 0000000 0000000 00000025153 14764761403 0026061 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import os
import platform
import re
import subprocess as sp
import sys
import traceback
from argparse import ArgumentParser
from io import StringIO
from pathlib import Path
from pathlib import PurePosixPath
from tempfile import TemporaryDirectory
from typing import Any
from rich.console import Console
from inline_snapshot._exceptions import UsageError
from inline_snapshot._external import DiscStorage
from inline_snapshot._problems import report_problems
from .._change import apply_all
from .._flags import Flags
from .._global_state import snapshot_env
from .._rewrite_code import ChangeRecorder
from .._types import Category
from .._types import Snapshot
ansi_escape = re.compile(
r"""
\x1B # ESC
(?: # 7-bit C1 Fe (except CSI)
[@-Z\\-_]
| # or [ for CSI, followed by a control sequence
\[
[0-?]* # Parameter bytes
[ -/]* # Intermediate bytes
[@-~] # Final byte
)
""",
re.VERBOSE,
)
def normalize(text):
text = ansi_escape.sub("", text)
# fix windows problems
text = text.replace("\u2500", "-")
text = text.replace("\r", "")
text = text.replace(" \n", " ⏎\n")
return text
class Example:
def __init__(self, files: str | dict[str, str]):
"""
Parameters:
files: a collecton of files where inline-snapshot opperates on,
or just a string which will be saved as *test_something.py*.
"""
if isinstance(files, str):
files = {"test_something.py": files}
self.files = files
self.dump_files()
def dump_files(self):
for name, content in self.files.items():
print(f"file: {name}")
print(content)
print()
def _write_files(self, dir: Path):
for name, content in self.files.items():
filename = dir / name
filename.parent.mkdir(exist_ok=True, parents=True)
filename.write_text(content)
def _read_files(self, dir: Path):
return {
str(p.relative_to(dir)): p.read_text()
for p in [*dir.iterdir(), *dir.rglob("*.py")]
if p.is_file()
}
def run_inline(
self,
args: list[str] = [],
*,
reported_categories: Snapshot[list[Category]] | None = None,
changed_files: Snapshot[dict[str, str]] | None = None,
report: Snapshot[str] | None = None,
raises: Snapshot[str] | None = None,
) -> Example:
"""Execute the example files in process and run every `test_*`
function.
This is useful for fast test execution.
Parameters:
args: inline-snapshot arguments (supports only "--inline-snapshot=fix|create|..." ).
reported_categories: snapshot of categories which inline-snapshot thinks could be applied.
changed_files: snapshot of files which are changed by this run.
raises: snapshot of the exception which is raised during the test execution.
It is required if your code raises an exception.
Returns:
A new Example instance which contains the changed files.
"""
parser = ArgumentParser()
parser.add_argument(
"--inline-snapshot",
metavar="(disable,short-report,report,review,create,update,trim,fix)*",
dest="inline_snapshot",
help="update specific snapshot values:\n"
"disable: disable the snapshot logic\n"
"short-report: show a short report\n"
"report: show a longer report with a diff report\n"
"review: allow to approve the changes interactive\n"
"create: creates snapshots which are currently not defined\n"
"update: update snapshots even if they are defined\n"
"trim: changes the snapshot in a way which will make the snapshot more precise.\n"
"fix: change snapshots which currently break your tests\n",
)
parsed_args = parser.parse_args(args)
flags = (parsed_args.inline_snapshot or "").split(",")
with TemporaryDirectory() as dir:
tmp_path = Path(dir)
self._write_files(tmp_path)
raised_exception = []
with snapshot_env() as state:
recorder = ChangeRecorder()
state.update_flags = Flags({*flags})
state.storage = DiscStorage(tmp_path / ".storage")
try:
tests_found = False
for filename in tmp_path.glob("*.py"):
globals: dict[str, Any] = {}
print("run> pytest", filename)
exec(
compile(filename.read_text("utf-8"), filename, "exec"),
globals,
)
# run all test_* functions
tests = [
v
for k, v in globals.items()
if (k.startswith("test_") or k == "test") and callable(v)
]
tests_found |= len(tests) != 0
for v in tests:
try:
v()
except Exception as e:
traceback.print_exc()
raised_exception.append(e)
if not tests_found:
raise UsageError("no test_*() functions in the example")
finally:
state.active = False
changes = []
for snapshot in state.snapshots.values():
changes += snapshot._changes()
snapshot_flags = {change.flag for change in changes}
apply_all(
[
change
for change in changes
if change.flag in state.update_flags.to_set()
],
recorder,
)
recorder.fix_all()
report_output = StringIO()
console = Console(file=report_output, width=80)
# TODO: add all the report output here
report_problems(console)
if reported_categories is not None:
assert sorted(snapshot_flags) == reported_categories
if raised_exception:
if raises is None:
raise raised_exception[0]
assert raises == "\n".join(
f"{type(e).__name__}:\n" + str(e) for e in raised_exception
)
else:
assert raises == None
if changed_files is not None:
current_files = {}
for name, content in sorted(self._read_files(tmp_path).items()):
if name not in self.files or self.files[name] != content:
current_files[name] = content
assert changed_files == current_files
if report is not None:
assert report == normalize(report_output.getvalue())
return Example(self._read_files(tmp_path))
def run_pytest(
self,
args: list[str] = [],
*,
term_columns=80,
env: dict[str, str] = {},
changed_files: Snapshot[dict[str, str]] | None = None,
report: Snapshot[str] | None = None,
stderr: Snapshot[str] | None = None,
returncode: Snapshot[int] | None = None,
) -> Example:
"""Run pytest with the given args and env variables in an seperate
process.
It can be used to test the interaction between your code and pytest, but it is a bit slower than `run_inline`
Parameters:
args: pytest arguments like "--inline-snapshot=fix"
env: dict of environment variables
changed_files: snapshot of files which are changed by this run.
report: snapshot of the report at the end of the pytest run.
stderr: pytest stderr output
returncode: snapshot of the pytest returncode.
Returns:
A new Example instance which contains the changed files.
"""
with TemporaryDirectory() as dir:
tmp_path = Path(dir)
self._write_files(tmp_path)
cmd = [sys.executable, "-m", "pytest", *args]
command_env = dict(os.environ)
command_env["TERM"] = "unknown"
command_env["COLUMNS"] = str(
term_columns + 1 if platform.system() == "Windows" else term_columns
)
command_env.pop("CI", None)
command_env.update(env)
result = sp.run(cmd, cwd=tmp_path, capture_output=True, env=command_env)
print("run>", *cmd)
print("stdout:")
print(result.stdout.decode())
print("stderr:")
print(result.stderr.decode())
if returncode is not None:
assert result.returncode == returncode
if stderr is not None:
original = result.stderr.decode().splitlines()
lines = [
line
for line in original
if not any(
s in line
for s in [
'No entry for terminal type "unknown"',
"using dumb terminal settings.",
]
)
]
assert "\n".join(lines) == stderr
if report is not None:
report_list = []
record = False
for line in result.stdout.decode().splitlines():
line = line.strip()
if line.startswith("===="):
record = False
if record and line:
report_list.append(line)
if line.startswith("====") and "inline snapshot" in line:
record = True
report_str = "\n".join(report_list)
assert normalize(report_str) == report, repr(report_str)
if changed_files is not None:
current_files = {}
for name, content in sorted(self._read_files(tmp_path).items()):
if name not in self.files or self.files[name] != content:
current_files[str(PurePosixPath(*Path(name).parts))] = content
assert changed_files == current_files
return Example(self._read_files(tmp_path))
python-inline-snapshot-0.20.7/testing/ 0000775 0000000 0000000 00000000000 14764761403 0017723 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/testing/generate_tests.py 0000664 0000000 0000000 00000006176 14764761403 0023323 0 ustar 00root root 0000000 0000000 import ast
import random
import sys
from pysource_minimize import minimize
from inline_snapshot import UsageError
from inline_snapshot.testing import Example
header = """
from dataclasses import dataclass
@dataclass
class A:
a:int
b:int=5
c:int=5
def f(v):
return v
"""
def chose(l):
return random.choice(l)
def gen_new_value(values):
kind = chose(["dataclass", "dict", "tuple", "list", "f"])
if kind == "dataclass":
typ = chose(["A"])
num_args = chose(range(1, 3))
num_pos_args = chose(range(num_args))
assert num_pos_args <= num_args
args_names = ["a", "b", "c"]
random.shuffle(args_names)
args = [
*[chose(values) for _ in range(num_pos_args)],
*[
f"{args_names.pop()}={chose(values)}"
for _ in range(num_args - num_pos_args)
],
]
return f"{typ}({', '.join(args)})"
if kind == "tuple":
return "(" + ", ".join(chose(values) for _ in range(3)) + ")"
if kind == "list":
return "[" + ", ".join(chose(values) for _ in range(3)) + "]"
if kind == "dict":
return (
"[" + ", ".join(f"{chose(values)}:{chose(values)}" for _ in range(3)) + "]"
)
if kind == "f":
return f"f({chose(values)})"
context = dict()
exec(header, context, context)
def is_valid_value(v):
try:
eval(v, context, {})
except:
return False
return True
def value_of(v):
return eval(v, context, {})
def gen_values():
values = [
"1",
"'abc'",
"True",
"list()",
"dict()",
"set()",
"tuple()",
"[]",
"{}",
"()",
"{*()}",
]
while len(values) <= 500:
new_value = gen_new_value([v for v in values if len(v) < 50])
if is_valid_value(new_value):
values.append(new_value)
return values
def fmt(code):
return ast.unparse(ast.parse(code))
class Store:
def __eq__(self, other):
self.value = other
return True
def gen_test(values):
va = chose(values)
vb = chose(values)
test = f"""
from inline_snapshot import snapshot
{header}
def test_a():
assert {va} == snapshot({vb})
assert {vb} == snapshot({va})
def test_b():
for _ in [1,2,3]:
assert {va} == snapshot({vb})
assert {vb} == snapshot({va})
def test_c():
snapshot({vb})
snapshot({va})
"""
test = fmt(test)
if value_of(va) == value_of(vb):
return
def contains_bug(code):
try:
Example({"test.py": code}).run_inline(["--inline-snapshot=fix"])
except UsageError:
return False
except KeyboardInterrupt:
raise
except AssertionError:
return True
except:
return True
return False
if not contains_bug(test):
return
test = minimize(test, checker=contains_bug)
print("minimal code:")
print("=" * 20)
print(test)
sys.exit()
if __name__ == "__main__":
values = gen_values()
for i in range(100000):
gen_test(values)
python-inline-snapshot-0.20.7/tests/ 0000775 0000000 0000000 00000000000 14764761403 0017410 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/tests/__init__.py 0000664 0000000 0000000 00000000000 14764761403 0021507 0 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/tests/_is_normalized.py 0000664 0000000 0000000 00000001062 14764761403 0022757 0 ustar 00root root 0000000 0000000 from inline_snapshot._unmanaged import declare_unmanaged
@declare_unmanaged
class IsNormalized:
def __init__(self, func, value) -> None:
self._func = func
self._value = value
self._last_value = None
def __eq__(self, other) -> bool:
self._last_value = self._func(other)
return self._last_value == self._value
def __repr__(self):
return f"IsNormalized({self._value}, should_be={self._last_value!r})"
def normalization(func):
def f(value):
return IsNormalized(func, value)
return f
python-inline-snapshot-0.20.7/tests/adapter/ 0000775 0000000 0000000 00000000000 14764761403 0021030 5 ustar 00root root 0000000 0000000 python-inline-snapshot-0.20.7/tests/adapter/test_change_types.py 0000664 0000000 0000000 00000002025 14764761403 0025111 0 ustar 00root root 0000000 0000000 import pytest
from inline_snapshot.testing._example import Example
values = ["1", '"2\'"', "[5]", "{1: 2}", "F(i=5)", "F.make1('2')", "f(7)"]
@pytest.mark.parametrize("a", values)
@pytest.mark.parametrize("b", values + ["F.make2(Is(5))"])
def test_change_types(a, b):
context = """\
from inline_snapshot import snapshot, Is
from dataclasses import dataclass
@dataclass
class F:
i: int
@staticmethod
def make1(s):
return F(i=int(s))
@staticmethod
def make2(s):
return F(i=s)
def f(v):
return v
"""
def code_repr(v):
g = {}
exec(context + f"r=repr({a})", g)
return g["r"]
def code(a, b):
return f"""\
{context}
def test_change():
for _ in [1,2]:
assert {a} == snapshot({b})
"""
print(a, b)
print(code_repr(a), code_repr(b))
Example(code(a, b)).run_inline(
["--inline-snapshot=fix,update"],
changed_files=(
{"test_something.py": code(a, code_repr(a))} if code_repr(a) != b else {}
),
)
python-inline-snapshot-0.20.7/tests/adapter/test_dataclass.py 0000664 0000000 0000000 00000030421 14764761403 0024400 0 ustar 00root root 0000000 0000000 from inline_snapshot import snapshot
from inline_snapshot.extra import warns
from inline_snapshot.testing._example import Example
def test_unmanaged():
Example(
"""\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass
@dataclass
class A:
a:int
b:int
def test_something():
assert A(a=2,b=4) == snapshot(A(a=1,b=Is(1))), "not equal"
"""
).run_inline(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass
@dataclass
class A:
a:int
b:int
def test_something():
assert A(a=2,b=4) == snapshot(A(a=2,b=Is(1))), "not equal"
"""
}
),
raises=snapshot(
"""\
AssertionError:
not equal\
"""
),
)
def test_reeval():
Example(
"""\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass
@dataclass
class A:
a:int
b:int
def test_something():
for c in "ab":
assert A(a=1,b=c) == snapshot(A(a=2,b=Is(c)))
"""
).run_inline(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass
@dataclass
class A:
a:int
b:int
def test_something():
for c in "ab":
assert A(a=1,b=c) == snapshot(A(a=1,b=Is(c)))
"""
}
),
)
def test_dataclass_default_value():
Example(
"""\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass,field
@dataclass
class A:
a:int
b:int=2
c:list=field(default_factory=list)
def test_something():
for _ in [1,2]:
assert A(a=1) == snapshot(A(a=1,b=2,c=[]))
"""
).run_inline(
["--inline-snapshot=update"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass,field
@dataclass
class A:
a:int
b:int=2
c:list=field(default_factory=list)
def test_something():
for _ in [1,2]:
assert A(a=1) == snapshot(A(a=1))
"""
}
),
)
def test_dataclass_positional_arguments():
Example(
"""\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass,field
@dataclass
class A:
a:int
b:int=2
c:list=field(default_factory=list)
def test_something():
for _ in [1,2]:
assert A(a=1) == snapshot(A(1,2,c=[]))
"""
).run_inline(
["--inline-snapshot=update"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass,field
@dataclass
class A:
a:int
b:int=2
c:list=field(default_factory=list)
def test_something():
for _ in [1,2]:
assert A(a=1) == snapshot(A(1,2))
"""
}
),
)
def test_attrs_default_value():
Example(
"""\
from inline_snapshot import snapshot,Is
import attrs
@attrs.define
class A:
a:int
b:int=2
c:list=attrs.field(factory=list)
d:int=attrs.field(default=attrs.Factory(lambda self:self.a+10,takes_self=True))
def test_something():
assert A(a=1) == snapshot(A(a=1,b=2,c=[],d=11))
assert A(a=2,b=3) == snapshot(A(a=1,b=2,c=[],d=11))
"""
).run_pytest(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
import attrs
@attrs.define
class A:
a:int
b:int=2
c:list=attrs.field(factory=list)
d:int=attrs.field(default=attrs.Factory(lambda self:self.a+10,takes_self=True))
def test_something():
assert A(a=1) == snapshot(A(a=1,b=2,c=[],d=11))
assert A(a=2,b=3) == snapshot(A(a=2,b=3,c=[],d=11))
"""
}
),
).run_pytest(
["--inline-snapshot=update"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
import attrs
@attrs.define
class A:
a:int
b:int=2
c:list=attrs.field(factory=list)
d:int=attrs.field(default=attrs.Factory(lambda self:self.a+10,takes_self=True))
def test_something():
assert A(a=1) == snapshot(A(a=1))
assert A(a=2,b=3) == snapshot(A(a=2,b=3))
"""
}
),
)
def test_attrs_field_repr():
Example(
"""\
from inline_snapshot import snapshot
import attrs
@attrs.define
class container:
a: int
b: int = attrs.field(default=5,repr=False)
assert container(a=1,b=5) == snapshot()
"""
).run_pytest(
["--inline-snapshot=create"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
import attrs
@attrs.define
class container:
a: int
b: int = attrs.field(default=5,repr=False)
assert container(a=1,b=5) == snapshot(container(a=1))
"""
}
),
).run_pytest()
def test_attrs_unmanaged():
Example(
"""\
import datetime as dt
import uuid
import attrs
from dirty_equals import IsDatetime
from inline_snapshot import Is, snapshot
@attrs.define
class Attrs:
ts: dt.datetime
id: uuid.UUID
def test():
id = uuid.uuid4()
assert snapshot(Attrs(ts=IsDatetime(), id=Is(id))) == Attrs(
dt.datetime.now(), id
)
"""
).run_pytest(
["--inline-snapshot=create,fix"],
changed_files=snapshot({}),
).run_pytest()
def test_disabled(executing_used):
Example(
"""\
from inline_snapshot import snapshot
from dataclasses import dataclass
@dataclass
class A:
a:int
def test_something():
assert A(a=3) == snapshot(A(a=5)),"not equal"
"""
).run_inline(
changed_files=snapshot({}),
raises=snapshot(
"""\
AssertionError:
not equal\
"""
),
)
def test_starred_warns():
with warns(
snapshot(
[
(
10,
"InlineSnapshotSyntaxWarning: star-expressions are not supported inside snapshots",
)
]
),
include_line=True,
):
Example(
"""
from inline_snapshot import snapshot
from dataclasses import dataclass
@dataclass
class A:
a:int
def test_something():
assert A(a=3) == snapshot(A(**{"a":5})),"not equal"
"""
).run_inline(
["--inline-snapshot=fix"],
raises=snapshot(
"""\
AssertionError:
not equal\
"""
),
)
def test_add_argument():
Example(
"""\
from inline_snapshot import snapshot
from dataclasses import dataclass
@dataclass
class A:
a:int=0
b:int=0
c:int=0
def test_something():
assert A(a=3,b=3,c=3) == snapshot(A(b=3)),"not equal"
"""
).run_inline(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
from dataclasses import dataclass
@dataclass
class A:
a:int=0
b:int=0
c:int=0
def test_something():
assert A(a=3,b=3,c=3) == snapshot(A(a = 3, b=3, c = 3)),"not equal"
"""
}
),
raises=snapshot(None),
)
def test_positional_star_args():
with warns(
snapshot(
[
"InlineSnapshotSyntaxWarning: star-expressions are not supported inside snapshots"
]
)
):
Example(
"""\
from inline_snapshot import snapshot
from dataclasses import dataclass
@dataclass
class A:
a:int
def test_something():
assert A(a=3) == snapshot(A(*[],a=3)),"not equal"
"""
).run_inline(
["--inline-snapshot=report"],
)
def test_remove_positional_argument():
Example(
"""\
from inline_snapshot import snapshot
from inline_snapshot._adapter.generic_call_adapter import GenericCallAdapter,Argument
class L:
def __init__(self,*l):
self.l=l
def __eq__(self,other):
if not isinstance(other,L):
return NotImplemented
return other.l==self.l
class LAdapter(GenericCallAdapter):
@classmethod
def check_type(cls, typ):
return issubclass(typ,L)
@classmethod
def arguments(cls, value):
return ([Argument(x) for x in value.l],{})
@classmethod
def argument(cls, value, pos_or_name):
assert isinstance(pos_or_name,int)
return value.l[pos_or_name]
def test_L1():
for _ in [1,2]:
assert L(1,2) == snapshot(L(1)), "not equal"
def test_L2():
for _ in [1,2]:
assert L(1,2) == snapshot(L(1, 2, 3)), "not equal"
def test_L3():
for _ in [1,2]:
assert L(1,2) == snapshot(L(1, 2)), "not equal"
"""
).run_pytest().run_pytest(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
from inline_snapshot._adapter.generic_call_adapter import GenericCallAdapter,Argument
class L:
def __init__(self,*l):
self.l=l
def __eq__(self,other):
if not isinstance(other,L):
return NotImplemented
return other.l==self.l
class LAdapter(GenericCallAdapter):
@classmethod
def check_type(cls, typ):
return issubclass(typ,L)
@classmethod
def arguments(cls, value):
return ([Argument(x) for x in value.l],{})
@classmethod
def argument(cls, value, pos_or_name):
assert isinstance(pos_or_name,int)
return value.l[pos_or_name]
def test_L1():
for _ in [1,2]:
assert L(1,2) == snapshot(L(1, 2)), "not equal"
def test_L2():
for _ in [1,2]:
assert L(1,2) == snapshot(L(1, 2)), "not equal"
def test_L3():
for _ in [1,2]:
assert L(1,2) == snapshot(L(1, 2)), "not equal"
"""
}
),
)
def test_namedtuple():
Example(
"""\
from inline_snapshot import snapshot
from collections import namedtuple
T=namedtuple("T","a,b")
def test_tuple():
assert T(a=1,b=2) == snapshot(T(a=1, b=3)), "not equal"
"""
).run_pytest(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
from collections import namedtuple
T=namedtuple("T","a,b")
def test_tuple():
assert T(a=1,b=2) == snapshot(T(a=1, b=2)), "not equal"
"""
}
),
)
def test_defaultdict():
Example(
"""\
from inline_snapshot import snapshot
from collections import defaultdict
def test_tuple():
d=defaultdict(list)
d[1].append(2)
assert d == snapshot(defaultdict(list, {1: [3]})), "not equal"
"""
).run_pytest(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
from collections import defaultdict
def test_tuple():
d=defaultdict(list)
d[1].append(2)
assert d == snapshot(defaultdict(list, {1: [2]})), "not equal"
"""
}
),
)
def test_dataclass_field_repr():
Example(
"""\
from inline_snapshot import snapshot
from dataclasses import dataclass,field
@dataclass
class container:
a: int
b: int = field(default=5,repr=False)
def test():
assert container(a=1,b=5) == snapshot()
"""
).run_inline(
["--inline-snapshot=create"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
from dataclasses import dataclass,field
@dataclass
class container:
a: int
b: int = field(default=5,repr=False)
def test():
assert container(a=1,b=5) == snapshot(container(a=1))
"""
}
),
).run_inline()
def test_dataclass_var():
Example(
"""\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass,field
@dataclass
class container:
a: int
def test_list():
l=container(5)
assert l == snapshot(l), "not equal"
"""
).run_inline(
["--inline-snapshot=update"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
from dataclasses import dataclass,field
@dataclass
class container:
a: int
def test_list():
l=container(5)
assert l == snapshot(container(a=5)), "not equal"
"""
}
),
)
python-inline-snapshot-0.20.7/tests/adapter/test_dict.py 0000664 0000000 0000000 00000001406 14764761403 0023365 0 ustar 00root root 0000000 0000000 from inline_snapshot import snapshot
from inline_snapshot.testing import Example
def test_dict_var():
Example(
"""\
from inline_snapshot import snapshot,Is
def test_list():
l={1:2}
assert l == snapshot(l), "not equal"
"""
).run_inline(
["--inline-snapshot=update"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
def test_list():
l={1:2}
assert l == snapshot({1: 2}), "not equal"
"""
}
),
)
def test_dict_constructor():
Example(
"""\
from inline_snapshot import snapshot
def test_dict():
snapshot(dict())
"""
).run_inline(
["--inline-snapshot=fix"],
changed_files=snapshot({}),
)
python-inline-snapshot-0.20.7/tests/adapter/test_general.py 0000664 0000000 0000000 00000001476 14764761403 0024066 0 ustar 00root root 0000000 0000000 from inline_snapshot import snapshot
from inline_snapshot.testing import Example
def test_adapter_mismatch():
Example(
"""\
from inline_snapshot import snapshot
def test_thing():
assert [1,2] == snapshot({1:2})
"""
).run_inline(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
def test_thing():
assert [1,2] == snapshot([1, 2])
\
"""
}
),
)
def test_reeval():
Example(
"""\
from inline_snapshot import snapshot,Is
def test_thing():
for i in (1,2):
assert {1:i} == snapshot({1:Is(i)})
assert [i] == [Is(i)]
assert (i,) == (Is(i),)
"""
).run_pytest(["--inline-snapshot=short-report"], report=snapshot(""))
python-inline-snapshot-0.20.7/tests/adapter/test_sequence.py 0000664 0000000 0000000 00000004475 14764761403 0024263 0 ustar 00root root 0000000 0000000 import pytest
from inline_snapshot._inline_snapshot import snapshot
from inline_snapshot.testing._example import Example
def test_list_adapter_create_inner_snapshot():
Example(
"""\
from inline_snapshot import snapshot
from dirty_equals import IsInt
def test_list():
assert [1,2,3,4] == snapshot([1,IsInt(),snapshot(),4]),"not equal"
"""
).run_inline(
["--inline-snapshot=create"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
from dirty_equals import IsInt
def test_list():
assert [1,2,3,4] == snapshot([1,IsInt(),snapshot(3),4]),"not equal"
"""
}
),
raises=snapshot(None),
)
def test_list_adapter_fix_inner_snapshot():
Example(
"""\
from inline_snapshot import snapshot
from dirty_equals import IsInt
def test_list():
assert [1,2,3,4] == snapshot([1,IsInt(),snapshot(8),4]),"not equal"
"""
).run_inline(
["--inline-snapshot=fix"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot
from dirty_equals import IsInt
def test_list():
assert [1,2,3,4] == snapshot([1,IsInt(),snapshot(3),4]),"not equal"
"""
}
),
raises=snapshot(None),
)
@pytest.mark.no_rewriting
def test_list_adapter_reeval(executing_used):
Example(
"""\
from inline_snapshot import snapshot,Is
def test_list():
for i in (1,2,3):
assert [1,i] == snapshot([1,Is(i)]),"not equal"
"""
).run_inline(
changed_files=snapshot({}),
raises=snapshot(None),
)
def test_list_var():
Example(
"""\
from inline_snapshot import snapshot,Is
def test_list():
l=[1]
assert l == snapshot(l), "not equal"
"""
).run_inline(
["--inline-snapshot=update"],
changed_files=snapshot(
{
"test_something.py": """\
from inline_snapshot import snapshot,Is
def test_list():
l=[1]
assert l == snapshot([1]), "not equal"
"""
}
),
)
def test_tuple_constructor():
Example(
"""\
from inline_snapshot import snapshot
def test_tuple():
snapshot(tuple()), "not equal"
"""
).run_inline(
["--inline-snapshot=fix"],
changed_files=snapshot({}),
)
python-inline-snapshot-0.20.7/tests/conftest.py 0000664 0000000 0000000 00000025436 14764761403 0021621 0 ustar 00root root 0000000 0000000 import os
import platform
import re
import shutil
import sys
import textwrap
import traceback
from dataclasses import dataclass
from dataclasses import field
from pathlib import Path
from types import SimpleNamespace
from typing import Set
from unittest import mock
import black
import executing
import pytest
import inline_snapshot._external
from inline_snapshot._change import apply_all
from inline_snapshot._flags import Flags
from inline_snapshot._format import format_code
from inline_snapshot._rewrite_code import ChangeRecorder
from inline_snapshot._types import Category
from inline_snapshot.testing._example import snapshot_env
pytest_plugins = "pytester"
pytest.register_assert_rewrite("tests.example")
black.files.find_project_root = black.files.find_project_root.__wrapped__ # type: ignore
@pytest.fixture(autouse=True)
def check_pypy(request):
implementation = sys.implementation.name
node = request.node
if implementation != "cpython" and node.get_closest_marker("no_rewriting") is None:
pytest.skip(f"{implementation} is not supported")
yield
@pytest.fixture()
def check_update(source):
def w(source_code, *, flags="", reported_flags=None, number=1):
s = source(source_code)
flags = {*flags.split(",")} - {""}
if reported_flags is None:
reported_flags = flags
else:
reported_flags = {*reported_flags.split(",")} - {""}
assert s.flags == reported_flags
assert s.number_snapshots == number
assert s.error == ("fix" in s.flags)
s2 = s.run(*flags)
return s2.source
return w
@pytest.fixture()
def source(tmp_path: Path):
filecount = 1
@dataclass
class Source:
source: str
flags: Set[str] = field(default_factory=set)
error: bool = False
number_snapshots: int = 0
number_changes: int = 0
def run(self, *flags_arg: Category):
flags = Flags({*flags_arg})
nonlocal filecount
filename: Path = tmp_path / f"test_{filecount}.py"
filecount += 1
prefix = """\"\"\"
PYTEST_DONT_REWRITE
\"\"\"
# äöß 🐍
from inline_snapshot import snapshot
from inline_snapshot import external
from inline_snapshot import outsource
"""
source = prefix + textwrap.dedent(self.source)
filename.write_text(source, "utf-8")
print()
print("input:")
print(textwrap.indent(source, " |", lambda line: True).rstrip())
with snapshot_env() as state:
recorder = ChangeRecorder()
state.update_flags = flags
state.storage = inline_snapshot._external.DiscStorage(
tmp_path / ".storage"
)
error = False
try:
exec(compile(filename.read_text("utf-8"), filename, "exec"), {})
except AssertionError:
traceback.print_exc()
error = True
finally:
state.active = False
number_snapshots = len(state.snapshots)
changes = []
for snapshot in state.snapshots.values():
changes += snapshot._changes()
snapshot_flags = {change.flag for change in changes}
apply_all(
[
change
for change in changes
if change.flag in state.update_flags.to_set()
],
recorder,
)
recorder.fix_all()
source = filename.read_text("utf-8")[len(prefix) :]
print("reported_flags:", snapshot_flags)
print(
f'run: pytest --inline-snapshot={",".join(flags.to_set())}'
if flags
else ""
)
print("output:")
print(textwrap.indent(source, " |", lambda line: True).rstrip())
return Source(
source=source,
flags=snapshot_flags,
error=error,
number_snapshots=number_snapshots,
number_changes=len(changes),
)
def w(source):
return Source(source=source).run()
return w
ansi_escape = re.compile(
r"""
\x1B # ESC
(?: # 7-bit C1 Fe (except CSI)
[@-Z\\-_]
| # or [ for CSI, followed by a control sequence
\[
[0-?]* # Parameter bytes
[ -/]* # Intermediate bytes
[@-~] # Final byte
)
""",
re.VERBOSE,
)
class RunResult:
def __init__(self, result):
self._result = result
def __getattr__(self, name):
return getattr(self._result, name)
@staticmethod
def _join_lines(lines):
text = "\n".join(lines).rstrip()
if "\n" in text:
return text + "\n"
else:
return text
@property
def report(self):
result = []
record = False
for line in self._result.stdout.lines:
line = line.strip()
if line.startswith("===="):
record = False
if record and line:
result.append(line)
if line.startswith("====") and "inline snapshot" in line:
record = True
result = self._join_lines(result)
result = ansi_escape.sub("", result)
# fix windows problems
result = result.replace("\u2500", "-")
result = result.replace("\r", "")
result = result.replace(" \n", " ⏎\n")
return result
@property
def errors(self):
result = []
record = False
for line in self._result.stdout.lines:
line = line.strip()
if line.startswith("====") and "ERRORS" in line:
record = True
if record and line:
result.append(line)
result = self._join_lines(result)
result = result.replace(" \n", " ⏎\n")
result = re.sub(r"\d+\.\d+s", "