diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18dd28c6..a8e38607 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,6 @@ ci: repos: - hooks: - - id: check-yaml - id: check-ast - id: check-merge-conflict - id: trailing-whitespace diff --git a/README.md b/README.md index b7030ae4..df3153ca 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Docs](https://img.shields.io/badge/docs-mkdocs-green)](https://15r10nk.github.io/inline-snapshot/latest/) [![pypi version](https://img.shields.io/pypi/v/inline-snapshot.svg)](https://pypi.org/project/inline-snapshot/) ![Python Versions](https://img.shields.io/pypi/pyversions/inline-snapshot) -![PyPI - Downloads](https://img.shields.io/pypi/dw/inline-snapshot) +[![PyPI - Downloads](https://img.shields.io/pypi/dw/inline-snapshot)](https://pypacktrends.com/?packages=inline-snapshot&time_range=2years) [![coverage](https://img.shields.io/badge/coverage-100%25-blue)](https://15r10nk.github.io/inline-snapshot/latest/contributing/#coverage) [![GitHub Sponsors](https://img.shields.io/github/sponsors/15r10nk)](https://github.com/sponsors/15r10nk) diff --git a/changelog.d/20250131_085021_15r10nk-git_preparations.md b/changelog.d/20250131_085021_15r10nk-git_preparations.md new file mode 100644 index 00000000..4854db63 --- /dev/null +++ b/changelog.d/20250131_085021_15r10nk-git_preparations.md @@ -0,0 +1,4 @@ +### Fixed + +- fixed an issue where --inline-snapshot=review discarded the user input and never formatted + the code if you used cpython 3.13. diff --git a/changelog.d/20250201_085056_15r10nk-git_preparations.md b/changelog.d/20250201_085056_15r10nk-git_preparations.md new file mode 100644 index 00000000..73cbc2f2 --- /dev/null +++ b/changelog.d/20250201_085056_15r10nk-git_preparations.md @@ -0,0 +1,23 @@ +### Changed + +- pytest assert rewriting works now together with inline-snapshot if you use `cpython>=3.11` + +--> + + + diff --git a/docs/code_generation.md b/docs/code_generation.md index fd76d618..be380a0c 100644 --- a/docs/code_generation.md +++ b/docs/code_generation.md @@ -62,7 +62,10 @@ It might be necessary to import the right modules to match the `repr()` output. 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)) +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 diff --git a/mkdocs.yml b/mkdocs.yml index 87cf55d9..1e9ff7e9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,8 @@ theme: features: - toc.follow - content.code.annotate + - navigation.tabs + palette: - media: (prefers-color-scheme) @@ -34,7 +36,13 @@ watch: - src/inline_snapshot nav: -- Introduction: index.md +- Home: + - Introduction: index.md + - Configuration: configuration.md + - pytest integration: pytest.md + - Categories: categories.md + - Code generation: code_generation.md + - Limitations: limitations.md - Core: - x == snapshot(): eq_snapshot.md - x <= snapshot(): cmp_snapshot.md @@ -46,14 +54,10 @@ nav: - Extensions: - first-party (extra): extra.md - third-party: third_party.md -- pytest integration: pytest.md -- Categories: categories.md -- Configuration: configuration.md -- Code generation: code_generation.md -- Testing: testing.md -- Limitations: limitations.md -- Contributing: contributing.md -- Changelog: changelog.md +- Development: + - Testing: testing.md + - Contributing: contributing.md + - Changelog: changelog.md @@ -73,6 +77,9 @@ markdown_extensions: - 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: diff --git a/pyproject.toml b/pyproject.toml index 4af94662..863a2c38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ "asttokens>=2.0.5", - "executing>=2.1.0", + "executing>=2.2.0", "rich>=13.7.1", "tomli>=2.0.0; python_version < '3.11'" ] diff --git a/src/inline_snapshot/_adapter/value_adapter.py b/src/inline_snapshot/_adapter/value_adapter.py index 5527ce6f..a47e38eb 100644 --- a/src/inline_snapshot/_adapter/value_adapter.py +++ b/src/inline_snapshot/_adapter/value_adapter.py @@ -3,13 +3,13 @@ import ast import warnings -from inline_snapshot._code_repr import value_code_repr -from inline_snapshot._unmanaged import Unmanaged -from inline_snapshot._unmanaged import update_allowed -from inline_snapshot._utils import value_to_token -from inline_snapshot.syntax_warnings import InlineSnapshotInfo - 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 @@ -46,7 +46,10 @@ def assign(self, old_value, old_node, new_value): return old_value if not old_value == new_value: - flag = "fix" + if old_value is undefined: + flag = "create" + else: + flag = "fix" elif ( old_node is not None and update_allowed(old_value) diff --git a/src/inline_snapshot/_flags.py b/src/inline_snapshot/_flags.py index 06f9e6fc..e753ea6d 100644 --- a/src/inline_snapshot/_flags.py +++ b/src/inline_snapshot/_flags.py @@ -1,4 +1,7 @@ +from __future__ import annotations + from typing import Set +from typing import cast from ._types import Category @@ -11,14 +14,21 @@ class Flags: 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.fix = "fix" in flags - self.update = "update" in flags + 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 to_set(self): - return {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"}) diff --git a/src/inline_snapshot/_inline_snapshot.py b/src/inline_snapshot/_inline_snapshot.py index 67c5f5c4..716e47d3 100644 --- a/src/inline_snapshot/_inline_snapshot.py +++ b/src/inline_snapshot/_inline_snapshot.py @@ -116,7 +116,11 @@ def __init__(self, value, expr, context: AdapterContext): def _changes(self): - if self._value._old_value is undefined: + 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 diff --git a/src/inline_snapshot/_sentinels.py b/src/inline_snapshot/_sentinels.py index 8c0d8e18..1fae4b69 100644 --- a/src/inline_snapshot/_sentinels.py +++ b/src/inline_snapshot/_sentinels.py @@ -1,7 +1 @@ -# sentinels -class Undefined: - def __repr__(self): - return "undefined" - - -undefined = Undefined() +undefined = ... diff --git a/src/inline_snapshot/pytest_plugin.py b/src/inline_snapshot/pytest_plugin.py index 7195aa7a..c1753f62 100644 --- a/src/inline_snapshot/pytest_plugin.py +++ b/src/inline_snapshot/pytest_plugin.py @@ -4,6 +4,7 @@ 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 @@ -28,6 +29,10 @@ pytest.register_assert_rewrite("inline_snapshot.extra") pytest.register_assert_rewrite("inline_snapshot.testing._example") +if sys.version_info >= (3, 13): + # fixes #186 + import readline # noqa + def pytest_addoption(parser, pluginmanager): group = parser.getgroup("inline-snapshot") @@ -61,7 +66,7 @@ def pytest_addoption(parser, pluginmanager): ) -categories = {"create", "update", "trim", "fix"} +categories = Flags.all().to_set() flags = set() @@ -113,7 +118,7 @@ def pytest_configure(config): elif flags & {"review"}: state().active = True - state().update_flags = Flags({"fix", "create", "update", "trim"}) + state().update_flags = Flags.all() else: state().active = "disable" not in flags @@ -126,7 +131,7 @@ def pytest_configure(config): _external.storage = _external.DiscStorage(external_storage) - if flags - {"short-report", "disable"}: + 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 @@ -166,6 +171,7 @@ def snapshot_check(): def pytest_assertrepr_compare(config, op, left, right): + results = [] if isinstance(left, GenericValue): results = config.hook.pytest_assertrepr_compare( @@ -253,19 +259,9 @@ def apply_changes(flag): return False # auto mode - changes = { - "update": [], - "fix": [], - "trim": [], - "create": [], - } - - snapshot_changes = { - "update": 0, - "fix": 0, - "trim": 0, - "create": 0, - } + 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() @@ -324,12 +320,13 @@ def report(flag, message, message_n): return - assert not any( - type(e).__name__ == "AssertionRewritingHook" for e in sys.meta_path - ) + if not is_pytest_compatible(): + assert not any( + type(e).__name__ == "AssertionRewritingHook" for e in sys.meta_path + ) used_changes = [] - for flag in ("create", "fix", "trim", "update"): + for flag in Flags.all(): if not changes[flag]: continue diff --git a/src/inline_snapshot/testing/_example.py b/src/inline_snapshot/testing/_example.py index 3aa8b3db..1a747a77 100644 --- a/src/inline_snapshot/testing/_example.py +++ b/src/inline_snapshot/testing/_example.py @@ -132,14 +132,13 @@ def run_inline( self._write_files(tmp_path) - raised_exception = None + raised_exception = [] with snapshot_env() as state: with ChangeRecorder().activate() as recorder: state.update_flags = Flags({*flags}) inline_snapshot._external.storage = ( inline_snapshot._external.DiscStorage(tmp_path / ".storage") ) - try: for filename in tmp_path.glob("*.py"): globals: dict[str, Any] = {} @@ -152,11 +151,11 @@ def run_inline( # run all test_* functions for k, v in globals.items(): if k.startswith("test_") and callable(v): - v() - except Exception as e: - traceback.print_exc() - raised_exception = e - + try: + v() + except Exception as e: + traceback.print_exc() + raised_exception.append(e) finally: state.active = False @@ -184,9 +183,9 @@ def run_inline( if reported_categories is not None: assert sorted(snapshot_flags) == reported_categories - if raised_exception is not None: - assert raises == f"{type(raised_exception).__name__}:\n" + str( - raised_exception + if raised_exception: + assert raises == "\n".join( + f"{type(e).__name__}:\n" + str(e) for e in raised_exception ) else: assert raises == None @@ -259,7 +258,21 @@ def run_pytest( assert result.returncode == returncode if stderr is not None: - assert result.stderr.decode() == stderr + + 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: diff --git a/tests/conftest.py b/tests/conftest.py index 796443bb..2e3a71c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -235,6 +235,22 @@ def errors(self): result = re.sub(r"\d+\.\d+s", "