Skip to content

Customize#246

Merged
15r10nk merged 72 commits intomainfrom
customize
Feb 13, 2026
Merged

Customize#246
15r10nk merged 72 commits intomainfrom
customize

Conversation

@15r10nk
Copy link
Owner

@15r10nk 15r10nk commented May 28, 2025

#177

test with:

pip install git+https://github.com/15r10nk/inline-snapshot.git@customize-stable

or like this if you want to use the latest branch:

pip install git+https://github.com/15r10nk/inline-snapshot.git@customize

@customize allows you to teach inline-snapshot how you want to create your snapshots.

mini howto:

from inline_snapshot import snapshot,customize
from dirty_equals import IsStr

@customize
def handler(value,local_vars):

    for v in local_vars:
        if v.value==value:
            return v

    if isinstance(value,str) and len(value)>=5:
        return IsStr(min_length=5)

                                           
def test_change():                         

    a="gflncfgega"

    assert [5,a,"lgfengfa","q"] == snapshot([5, a, IsStr(min_length=5), "q"])

You can also have multiple @customize handler.

You will mostly create custom call expressions like this:

@customize
def path_handler(value, builder: Builder):
    if isinstance(value, Path):
        return builder.create_call(Path, [value.as_posix()])

    if isinstance(value, PurePath):
        return builder.create_call(PurePath, [value.as_posix()])

@15r10nk 15r10nk force-pushed the customize branch 3 times, most recently from 97764a1 to 6d5681c Compare January 24, 2026 19:54
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new plugin-based customization system for snapshot generation, refactors much of the internal adapter/codegen pipeline to use Builder/Custom* abstractions, and extends external storage and dirty-equals support, along with comprehensive tests and documentation updates.

Changes:

  • Add a pluggy-based plugin API (inline_snapshot.plugin) with a @customize hook, default plugin implementations for core types (dataclasses, attrs, pydantic, dirty-equals, paths, externals), and automatic discovery from conftest.py and entry points.
  • Replace the old adapter/value-repr pipeline with a new Custom/Builder-based system, including NewAdapter, CustomCode, CustomCall, CustomDict, CustomSequence, CustomExternal, and CustomUndefined/CustomUnmanaged, and wire it through _inline_snapshot, _snapshot/*, _get_snapshot_value, _global_state, and _change.
  • Rework external snapshot handling (hash/UUID storage, outsource, external, import insertion) and add broad test coverage for customization, externals, namedtuple/dataclass behavior, docs rendering, and dirty-equals semantics.

Reviewed changes

Copilot reviewed 81 out of 83 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
tests/test_pytest_plugin.py Extends config tests to cover default-storage="hash" in pyproject.toml scenarios, validating new default storage behavior.
tests/test_preserve_values.py Adjusts a snapshot to use an expression (3 + 3) ensuring the new comparison/customization logic still preserves original source forms where expected.
tests/test_formatting.py Updates expected formatted snapshots to use canonical list formatting (consistent commas/spaces) under the new formatting/token handling.
tests/test_docs.py Adapts doc mapping tests to treat block_options as a dict, adds isort-based import normalization and align-based hl_lines computation to match new docs processing behavior.
tests/test_dirty_equals.py Adds tests around IsNow handling (removing approx and supporting custom delta) under the new dirty-equals customization.
tests/test_customize.py New test suite validating @customize and Builder behavior (dirty-equals, imports, global lookup, __file__, datetime types, error cases, etc.).
tests/test_code_repr.py Reworks enum/path/HasRepr tests to run through the new Example/inline execution flow and new path/customization behavior.
tests/test_builder.py New tests for Builder.with_default rejecting custom values (ensuring UsageError for invalid defaults).
tests/external/test_module_name.py New tests that exercise module_name_of and import behavior when snapshots live in __init__.py inside a package.
tests/external/test_external.py Updates many external-storage tests for the new hash/uuid behavior, removal of legacy hash storage expectations, new collision and not-found handling, and import insertion signature changes.
tests/external/storage/test_hash.py Adds a test ensuring repeated lookups of the same hash produce stable external references under the new interface.
tests/conftest.py Extends the Example testing environment to use importlib for module execution, deterministic UUIDs for externals, and the new plugin/customization model for freezegun’s FakeDatetime/FakeDate.
tests/adapter/test_namedtuple.py New tests that verify the new namedtuple handling (defaults, positional args, typing.NamedTuple, nested/mixed args) via the new adapter/custom pipeline.
tests/adapter/test_general.py Adds a test ensuring unmanaged values now raise UsageError under the new customization/unmanaged handling.
tests/adapter/test_dataclass.py Adds/adjusts dataclass tests (extra args, positional handling, plugin-based L adapter, custom __init__ semantics) for the new builder/custom pipeline.
tests/adapter/test_change_types.py Fixes a bug in the test helper (code_repr(v) now uses the v parameter instead of an undefined a), aligning with new adapter behavior.
src/inline_snapshot/testing/_example.py Major refactor of the Example runner: switch to importlib-based loading, maintain sys.path, explicitly load conftest.py and register customize hooks via SnapshotSession.register_customize_hooks_from_module, and restore sys.modules/sys.path after runs.
src/inline_snapshot/pytest_plugin.py Extends the pytest plugin to register @customize hooks from already-loaded and newly-registered conftest.py plugins, and maintains the unwrap-based assertrepr compare behavior under the new pipeline.
src/inline_snapshot/plugin/_spec.py Defines the pluggy hook spec/impl markers and the InlineSnapshotPluginSpec.customize hook (with Builder, local/global vars) plus a convenience @customize decorator.
src/inline_snapshot/plugin/_default_plugin.py Implements the default plugin(s) covering lists/tuples/dicts, strings, Counter, functions/types, datetime/timedelta, Path/PurePath, sets/frozensets, Enums/Flags, dataclasses, namedtuples, defaultdict, unmanaged/undefined, Outsourced, and optional attrs/pydantic/dirty-equals behavior.
src/inline_snapshot/plugin/init.py Public plugin API surface re-exporting InlineSnapshotPluginSpec, customize, hookimpl, Builder, Custom, Import, and ImportFrom.
src/inline_snapshot/fix_pytest_diff.py Removes the Unmanaged pretty-printer adapter, now that unmanaged values are represented via CustomUnmanaged.
src/inline_snapshot/extra.py Updates embedded docs/examples to new import ordering and highlight line numbers, in line with the new formatting and hl_lines behavior.
src/inline_snapshot/_utils.py Replaces the old tokenization/value-to-token helpers with a new clone function (now deep-copy based with UsageError on unequal copies, using real_repr), and tweaks string token equality to mark an uncovered branch as pragma: no cover.
src/inline_snapshot/_unmanaged.py Simplifies dirty-equals type detection (__mro__ based), and removes the Unmanaged wrapper and map_unmanaged in favor of CustomUnmanaged.
src/inline_snapshot/_types.py Minor doc/example import reordering (Snapshot before snapshot) to match formatting rules.
src/inline_snapshot/_source_file.py Reintroduces tokenization logic here (_token_of_code), adds format_expression helper, and a code_changed method that compares tokenized expressions to detect meaningful code changes.
src/inline_snapshot/_snapshot_session.py Adds plugin registration (register_customize_hooks_from_module using a small registry and SimpleNamespace) and adjusts storage-dir handling to respect configured storage_dir/default-storage.
src/inline_snapshot/_snapshot/undecided_value.py Completely rewrites UndecidedValue to work in terms of new Custom objects, including an AstToCustom converter for AST→Custom mapping and delegation to the new adapter pipeline.
src/inline_snapshot/_snapshot/min_max_value.py Refactors min/max snapshot behavior to operate on Custom values, computing new code via _code_repr and using SourceFile.code_changed instead of hand-rolled token comparison.
src/inline_snapshot/_snapshot/generic_value.py Central refactor: GenericValue now operates on Custom values, provides to_custom/value_to_custom (using Builder and mock_repr), re-evaluation via reeval, and updated type-error/operation dispatch semantics.
src/inline_snapshot/_snapshot/eq_value.py Equality snapshots now use NewAdapter.compare over Custom values, capturing changes via a generator (split_gen) and computing new code via _code_repr.
src/inline_snapshot/_snapshot/dict_value.py Dict snapshots are reimplemented on top of CustomDict and UndecidedValue, with custom key/value code generation and DictInsert that now carries both code and value tuples.
src/inline_snapshot/_snapshot/collection_value.py List/tuple/collection snapshots now rely on CustomList and SourceFile.code_changed, emitting ListInsert and Replace operations based on new _code_repr output.
src/inline_snapshot/_snapshot/init.py (Present but unchanged content in diff; serves as package marker for snapshot modules under the new architecture.)
src/inline_snapshot/_new_adapter.py New core adapter: implements warn_star_expression, reeval across Custom* types, and NewAdapter.compare for CustomCode, CustomSequence, CustomDict, and CustomCall, emitting ChangeBase instances (Delete/ListInsert/DictInsert/CallArg/Replace).
src/inline_snapshot/_inline_snapshot.py Wires the new AdapterContext, CustomUndefined, and generator-based _new_code/_changes into the snapshot call machinery, including flagging create changes via with_flag.
src/inline_snapshot/_global_state.py Introduces a per-context pluggy PluginManager (pm), seeds it with default plugins and setuptools entry points in enter_snapshot_context, and ensures a fresh plugin environment per snapshot context.
src/inline_snapshot/_get_snapshot_value.py Reimplements unwrap logic to operate via GenericValue._visible_value()._map, treats Outsourced specially, and uses StorageLookupError.files to distinguish lookup failures when unwrapping External/ExternalFile.
src/inline_snapshot/_generator_utils.py New helpers for working with change generators: split_gen, only_value, gen_map, with_flag, and make_gen_map.
src/inline_snapshot/_external/_storage/_uuid.py Updates UuidStorage to raise StorageLookupError(location, files=[]) on lookup failure, aligning with the new error interface.
src/inline_snapshot/_external/_storage/_protocol.py Extends StorageLookupError to carry a files attribute (list of candidate files) while still subclassing Exception.
src/inline_snapshot/_external/_storage/_hash.py Adapts hash storage to pass files into StorageLookupError for collisions/not-found, enabling richer error handling in callers.
src/inline_snapshot/_external/_storage/init.py Returns a storage dict with both "hash" and "uuid" storages for a given storage_dir.
src/inline_snapshot/_external/_outsource.py Redefines Outsourced as a dataclass carrying data, optional suffix, and optional storage, and changes outsource() to validate formats and return Outsourced without immediate storage writes.
src/inline_snapshot/_external/_find_external.py Enhances external detection and import insertion: adds module_name_of, extends ensure_import to handle both from and import statements, obey module docstrings/future imports, and skip self/builtins imports; also introduces an unconditional print() (see below).
src/inline_snapshot/_external/_external_location.py Switches to using the new AdapterContext type, aligning external-location evaluation with the new adapter pipeline.
src/inline_snapshot/_external/_external_base.py Adjusts ExternalBase.__eq__ to understand Outsourced and to react to StorageLookupError(files=...), auto-filling missing externals in fix mode while preserving error signaling when appropriate.
src/inline_snapshot/_external/_external.py Declares _original_location/_location types explicitly and uses AdapterContext for the external helper, integrating with the new comparison pipeline.
src/inline_snapshot/_customize/_custom_unmanaged.py Introduces CustomUnmanaged as the new representation of unmanaged values within the Custom pipeline.
src/inline_snapshot/_customize/_custom_undefined.py Introduces CustomUndefined as the Custom-level sentinel for undefined snapshot parts, with a code representation of "...".
src/inline_snapshot/_customize/_custom_sequence.py Implements CustomSequence plus CustomList/CustomTuple with brace/trailing-comma semantics and per-element _code_repr.
src/inline_snapshot/_customize/_custom_external.py Implements CustomExternal that emits ExternalChange and RequiredImport changes and generates external("...") code using the configured storage and format handlers.
src/inline_snapshot/_customize/_custom_dict.py Implements CustomDict with dict-based _map and _code_repr supporting per-key/value Custom conversion.
src/inline_snapshot/_customize/_custom_code.py Implements CustomCode, Import, ImportFrom, and _simplify_module_path, handling cloning, representation fallback via HasRepr, and emitting RequiredImport changes for needed imports.
src/inline_snapshot/_customize/_custom_call.py Implements CustomCall/CustomDefault, argument lookup, argument defaulting, and call code generation while keeping defaults out of generated code where appropriate.
src/inline_snapshot/_customize/_custom.py Base Custom ABC defining _map, _code_repr, _eval, and node_type/original_value semantics for all custom nodes.
src/inline_snapshot/_customize/_builder.py Core Builder implementation for plugins: provides create_list/tuple/dict/call/code/external, with_default, and access to local/global/import variables, and orchestrates the pm.hook.customize pipeline and mismatch checking.
src/inline_snapshot/_customize/init.py Package marker for the new customization subsystem.
src/inline_snapshot/_compare_context.py Makes compare_context robust with a try/finally to restore the global flag on exceptions.
src/inline_snapshot/_code_repr.py Refactors repr handling to use mock_repr and the new Custom pipeline, deprecates @customize_repr in favor of @customize, and removes the old built-in type-specific repr hooks (now handled by the plugin system).
src/inline_snapshot/_change.py Introduces RequiredImport and integrates import collection with ensure_import, normalizes new/replace code via SourceFile.format_expression, and generalizes list/dict/call-arg insertions to work with the new Custom pipeline.
src/inline_snapshot/_adapter_context.py New small data structures (FrameContext, AdapterContext) encapsulating the current file/frame/qualname and providing an eval helper for AST expressions.
src/inline_snapshot/_adapter/value_adapter.py (Removed legacy adapter implementation; now superseded by NewAdapter/Custom pipeline.)
src/inline_snapshot/_adapter/sequence_adapter.py (Removed legacy list/tuple adapter in favor of CustomSequence and NewAdapter.)
src/inline_snapshot/_adapter/generic_call_adapter.py (Removed legacy generic-call adapter; call handling now lives in CustomCall/NewAdapter.)
src/inline_snapshot/_adapter/dict_adapter.py (Removed legacy dict adapter in favor of CustomDict/NewAdapter.)
src/inline_snapshot/_adapter/adapter.py (Removed legacy adapter core; responsibilities are now split between AdapterContext, NewAdapter, and Builder/Customs.)
src/inline_snapshot/_adapter/init.py (Emptied legacy adapter export; new customization is via inline_snapshot.plugin.)
pyproject.toml Adds typing-extensions and isort, adjusts coverage (ignore_errors, pattern for "..."), makes docs env scripts forward args, and simplifies Hatch env dependencies to use dependency-groups.
mkdocs.yml Adds a new “Plugin” page to the docs nav.
docs/testing.md Reorders imports in testing examples to conform to new style/formatting.
docs/plugin.md New documentation page describing the plugin architecture, @customize hook, Builder API, and several practical examples.
docs/external/register_format.md Minor example import reorder to match new style.
docs/external/outsource.md Updates outsource docs to reflect UUID-based external references instead of hash-based names.
docs/external/external.md Reorders imports in examples and aligns behavior with new external handling.
docs/eq_snapshot.md Updates dirty-equals examples to use IsNow and tweaks surrounding narrative and imports accordingly.
docs/customize_repr.md Marks @customize_repr as deprecated in favor of @customize, with guidance and updated examples.
docs/code_generation.md Minor import order updates in code generation docs.
docs/categories.md Clarifies the update category (why it is hidden by default, and relation to #177) and fixes a small grammar issue.
changelog.d/20251201_200505_15r10nk-git_customize.md Notes new Path/PurePath snapshot behavior (avoiding platform-specific PosixPath/WindowsPath snapshots).
README.md Updates README examples for new import order and adds outcome-errors in the inline-snapshot directive.
.github/workflows/ci.yml Tightens coverage reporting (skip-covered/skip-empty, branch coverage) and stores a textual coverage report in htmlcov/report.txt.
Comments suppressed due to low confidence (1)

src/inline_snapshot/_snapshot/min_max_value.py:13

  • The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.
    The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@15r10nk
Copy link
Owner Author

15r10nk commented Jan 27, 2026

status update:

  • The API should be stable now.
  • I'm doing some last tests by using the new version with logfire and pydantic-ai
  • There is some test pollution happening in pydantic-ai which looks like it is caused by inline-snapshot

pydantic/pydantic-ai#4108
pydantic/logfire#1664

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 83 out of 85 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/inline_snapshot/_snapshot/min_max_value.py:13

  • The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.
    The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@qdewaghe
Copy link

status update:

  • The API should be stable now.
  • I'm doing some last tests by using the new version with logfire and pydantic-ai
  • There is some test pollution happening in pydantic-ai which looks like it is caused by inline-snapshot

pydantic/pydantic-ai#4108
pydantic/logfire#1664

Hey, thanks for providing an update. Do you think it's usable in prod or should I wait a few more weeks? :)

@15r10nk
Copy link
Owner Author

15r10nk commented Feb 10, 2026

It's ready to go. I've fixed all the issues I found while testing with pydantic-ai and logfire.
And I'm aiming to release it this week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants