Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-jsonschema-path for 
openSUSE:Factory checked in at 2026-07-01 16:36:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jsonschema-path (Old)
 and      /work/SRC/openSUSE:Factory/.python-jsonschema-path.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jsonschema-path"

Wed Jul  1 16:36:36 2026 rev:6 rq:1362605 version:0.5.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-jsonschema-path/python-jsonschema-path.changes
    2026-04-29 19:19:31.357207717 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-jsonschema-path.new.11887/python-jsonschema-path.changes
 2026-07-01 16:36:53.349585218 +0200
@@ -1,0 +2,10 @@
+Tue Jun 16 09:22:30 UTC 2026 - Jordi Massaguer <[email protected]>
+
+- update to 0.5.0 
+  * Resolved cache enabled and SchemaPath per-instance cache removed #259
+  * Resolved cache evolution rebind #260
+  * Per-accessor identity #262
+  * Resolved cache is enabled by default #259
+  * SchemaPath per-accessor __eq__/__hash__ #262
+
+-------------------------------------------------------------------

Old:
----
  jsonschema-path-0.4.6.tar.gz

New:
----
  jsonschema-path-0.5.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jsonschema-path.spec ++++++
--- /var/tmp/diff_new_pack.JFj49K/_old  2026-07-01 16:36:54.265617087 +0200
+++ /var/tmp/diff_new_pack.JFj49K/_new  2026-07-01 16:36:54.281617644 +0200
@@ -18,38 +18,37 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-jsonschema-path
-Version:        0.4.6
+Version:        0.5.0
 Release:        0
 Summary:        JSONSchema Spec with object-oriented paths
 License:        Apache-2.0
 URL:            https://github.com/p1c2u/jsonschema-path
 Source:         
https://github.com/p1c2u/jsonschema-path/archive/refs/tags/%{version}.tar.gz#/jsonschema-path-%{version}.tar.gz
-BuildRequires:  %{python_module base >= 3.10}
 BuildRequires:  %{python_module pip}
-BuildRequires:  %{python_module poetry-core >= 2}
+BuildRequires:  %{python_module poetry-core >= 1.0.0}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
+Requires:       python-PyYAML >= 5.1
+Requires:       python-attrs >= 22.2.0
+Requires:       python-pathable >= 0.6.0
+Requires:       python-referencing < 0.38.0
+BuildArch:      noarch
 # SECTION test requirements
 BuildRequires:  %{python_module PyYAML >= 5.1}
-BuildRequires:  %{python_module pathable >= 0.5}
+BuildRequires:  %{python_module attrs >= 22.2.0}
+BuildRequires:  %{python_module pathable >= 0.6.0}
 BuildRequires:  %{python_module pytest}
-BuildRequires:  %{python_module referencing}
+BuildRequires:  %{python_module referencing < 0.38.0}
 BuildRequires:  %{python_module requests >= 2.31.0}
-BuildRequires:  %{python_module responses >= 0.23.0}
+BuildRequires:  %{python_module responses >= 0.25}
 # /SECTION
-Requires:       python-PyYAML >= 5.1
-Requires:       python-pathable >= 0.5
-Requires:       python-referencing
-Suggests:       python-requests >= 2.31.0
-Provides:       python-jsonschema_path = %{version}-%{release}
-BuildArch:      noarch
 %python_subpackages
 
 %description
 JSONSchema Spec with object-oriented paths.
 
 %prep
-%setup -q -n jsonschema-path-%{version}
+%autosetup -p1 -n jsonschema-path-%{version}
 # unpin and hope for the best
 sed -i '/referencing/ s/,<0.30.0//' pyproject.toml
 # Remove need to pytest-cov

++++++ jsonschema-path-0.4.6.tar.gz -> jsonschema-path-0.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jsonschema-path-0.4.6/.github/workflows/python-test.yml 
new/jsonschema-path-0.5.0/.github/workflows/python-test.yml
--- old/jsonschema-path-0.4.6/.github/workflows/python-test.yml 2026-04-27 
20:56:09.000000000 +0200
+++ new/jsonschema-path-0.5.0/.github/workflows/python-test.yml 2026-05-19 
22:44:09.000000000 +0200
@@ -63,7 +63,7 @@
         run: poetry run deptry .
 
       - name: Upload coverage
-        uses: codecov/codecov-action@v5
+        uses: codecov/codecov-action@v6
 
   static-checks:
     name: "Static checks"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/README.rst 
new/jsonschema-path-0.5.0/README.rst
--- old/jsonschema-path-0.4.6/README.rst        2026-04-27 20:56:09.000000000 
+0200
+++ new/jsonschema-path-0.5.0/README.rst        2026-05-19 22:44:09.000000000 
+0200
@@ -89,21 +89,78 @@
    ...     print(contents)
    {'type': 'string', 'default': '1.0'}
 
+Identity and equality
+#####################
+
+Two ``SchemaPath`` instances are equal if they have the same ``parts``
+*and* point to the same ``SchemaAccessor``. ``SchemaAccessor`` identity
+is per-resource-handle: same wrapped dict (by reference), same
+``base_uri``, and same internal resolver instance. In practice:
+
+* Paths derived from the *same* accessor compare equal as expected:
+
+  .. code-block:: python
+
+     >>> accessor = SchemaAccessor.from_schema(d)
+     >>> SchemaPath(accessor) / "properties" == SchemaPath(accessor) / 
"properties"
+     True
+
+* Paths from *separate* ``from_dict`` or ``from_schema`` calls do **not**
+  compare equal even with identical arguments, because each call builds
+  its own accessor:
+
+  .. code-block:: python
+
+     >>> SchemaPath.from_dict(d) == SchemaPath.from_dict(d)
+     False
+
+* ``SchemaAccessor`` is hashable, so accessors and paths can be used as
+  set members and dict keys.
+
+This is also why the "build one accessor, reuse it" pattern below
+matters: it is both a caching optimisation and the contract you need
+for path equality to behave the way you expect.
 
 Resolved cache
 ##############
 
 The resolved-path cache is intended for repeated path lookups and may 
significantly improve
-``read_value``/membership hot paths. Cache entries are invalidated when the
-resolver registry evolves during reference resolution.
+``read_value``/membership hot paths. When the underlying ``referencing``
+registry grows (e.g. an external ``$ref`` pulls in a new document),
+cached entries are *rebound* to the new registry on read instead of
+being discarded. This relies on registries growing monotonically —
+resources are added, never replaced. Handlers that return drifting
+content for the same URI violate that assumption; disable caching with
+``resolved_cache_maxsize=0`` if you need to defend against it.
 
-This cache is optional and disabled by default
-(``resolved_cache_maxsize=0``). You can enable it when creating paths or
+This cache is enabled by default
+(``resolved_cache_maxsize=128``). You can disable it when creating paths or
 accessors, for example:
 
 .. code-block:: python
 
-   >>> path = SchemaPath.from_dict(d, resolved_cache_maxsize=64)
+   >>> path = SchemaPath.from_dict(d, resolved_cache_maxsize=0)
+
+Build **one** :code:`SchemaAccessor` per schema document and reuse it for
+every :code:`SchemaPath` you derive from that document. Treat the accessor
+as the long-lived handle to the resource; treat paths as cheap views over
+it.
+
+.. code-block:: python
+
+   >>> from jsonschema_path import SchemaAccessor, SchemaPath
+
+   >>> # Construct the accessor once, with caching enabled.
+   >>> accessor = SchemaAccessor.from_schema(d, resolved_cache_maxsize=128)
+
+   >>> # Derive as many paths as you like from the same accessor.
+   >>> root = SchemaPath(accessor)
+   >>> info = root / "properties" / "info"
+   >>> version = info / "properties" / "version"
+
+   >>> # Every path over `accessor` shares the same resolved-ref cache.
+   >>> with version.open() as contents:
+   ...     ...
 
 Benchmarks
 ##########
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/jsonschema_path/__init__.py 
new/jsonschema-path-0.5.0/jsonschema_path/__init__.py
--- old/jsonschema-path-0.4.6/jsonschema_path/__init__.py       2026-04-27 
20:56:09.000000000 +0200
+++ new/jsonschema-path-0.5.0/jsonschema_path/__init__.py       2026-05-19 
22:44:09.000000000 +0200
@@ -4,7 +4,7 @@
 
 __author__ = "Artur Maciag"
 __email__ = "[email protected]"
-__version__ = "0.4.6"
+__version__ = "0.5.0"
 __url__ = "https://github.com/p1c2u/jsonschema-path";
 __license__ = "Apache-2.0"
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jsonschema-path-0.4.6/jsonschema_path/_referencing_compat.py 
new/jsonschema-path-0.5.0/jsonschema_path/_referencing_compat.py
--- old/jsonschema-path-0.4.6/jsonschema_path/_referencing_compat.py    
1970-01-01 01:00:00.000000000 +0100
+++ new/jsonschema-path-0.5.0/jsonschema_path/_referencing_compat.py    
2026-05-19 22:44:09.000000000 +0200
@@ -0,0 +1,122 @@
+"""Compatibility shim for the ``referencing`` library's private API.
+
+This module is the *only* place in jsonschema-path that touches
+``referencing._core`` internals (the ``_base_uri``, ``_registry``, and
+``_previous`` attributes on ``Resolver``). Every other module must go
+through the helpers here.
+
+The motivation is firewalling: if a future ``referencing`` release
+reshapes those internals, the breakage is contained to this file. The
+``assert_referencing_layout`` call at import time also converts what
+would otherwise be silent wrong-results into a loud ``ImportError`` that
+names the issue, so version-skew bugs surface immediately.
+
+The helpers intentionally use ``Any``-typed generics because
+``referencing`` does not expose ``Resolver`` / ``Resolved`` as
+``attrs``-typed classes to type checkers, and ``Resolver._evolve``
+itself is declared ``**kwargs: Any``. The runtime invariants are
+enforced by ``assert_referencing_layout``; ``mypy`` is asked to trust
+this single module.
+
+Supported ``referencing`` versions: the upper bound is pinned in
+``pyproject.toml``; the lower bound is implied by these internals being
+``attrs`` fields named ``_base_uri``, ``_registry``, and ``_previous``.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Union
+
+import attrs
+from referencing import Registry
+from referencing._core import Resolved
+from referencing._core import Resolver
+
+ResolvedOrResolver = Union[Resolved[Any], Resolver[Any]]
+
+_REQUIRED_RESOLVER_FIELDS = frozenset({"_base_uri", "_registry", "_previous"})
+
+
+def assert_referencing_layout() -> None:
+    """Verify ``referencing.Resolver`` exposes the attrs fields we rebind.
+
+    Called once at import time. Raises ``ImportError`` if the installed
+    ``referencing`` is incompatible, instead of allowing rebind operations
+    to silently produce wrong results.
+    """
+    fields = {
+        field.name
+        for field in attrs.fields(Resolver)  # type: ignore[arg-type]
+    }
+    missing = _REQUIRED_RESOLVER_FIELDS - fields
+    if missing:
+        raise ImportError(
+            "jsonschema-path is incompatible with the installed version "
+            "of `referencing`. Expected `Resolver` attrs fields to "
+            f"include {sorted(_REQUIRED_RESOLVER_FIELDS)}; missing "
+            f"{sorted(missing)}. Pin `referencing` to a supported "
+            "version (see jsonschema_path/_referencing_compat.py)."
+        )
+
+
+def rebind_registry(
+    resolver: Resolver[Any],
+    registry: Registry[Any],
+) -> Resolver[Any]:
+    """Return a new resolver identical to *resolver* but with *registry*.
+
+    ``referencing.Registry`` instances are immutable and grow
+    monotonically via ``with_resource``. A cached ``Resolver`` captured
+    before the registry grew can be cheaply rebound to the latest
+    registry without re-walking the schema, because:
+
+    * The resolver's ``base_uri`` describes a document scope that the
+      newer registry, being a superset, still contains.
+    * ``Resolver._previous`` holds URIs (not resolver snapshots), and
+      ``Resolver.dynamic_scope`` re-uses ``self._registry`` for every
+      frame, so swapping the top resolver's registry rebinds the entire
+      dynamic scope in one shot.
+
+    Uses ``attrs.evolve`` rather than ``Resolver._evolve`` because the
+    latter pushes onto the dynamic-scope stack when called with a
+    differing ``base_uri``. We want a pure field replacement.
+    """
+    return attrs.evolve(resolver, registry=registry)  # type: ignore[misc]
+
+
+def rebind_resolved(
+    resolved: Resolved[Any],
+    registry: Registry[Any],
+) -> Resolved[Any]:
+    """Return a new ``Resolved`` with the same ``contents`` but a
+    resolver rebound to *registry*.
+
+    Centralizing the ``Resolved(...)`` construction here keeps the
+    ``call-arg`` type-ignore confined to a single line — production
+    callers (``SchemaAccessor.get_resolved`` and
+    ``CachedPathResolver._resolve_with_prefix_cache``) work in terms of
+    this helper and don't have to construct ``Resolved`` themselves.
+    """
+    return Resolved(  # type: ignore[call-arg]
+        contents=resolved.contents,
+        resolver=rebind_registry(resolved.resolver, registry),
+    )
+
+
+def registry_of(target: ResolvedOrResolver) -> Registry[Any]:
+    """Return the registry backing a Resolved or Resolver."""
+    if isinstance(target, Resolved):
+        return target.resolver._registry
+    return target._registry
+
+
+def base_uri_of(target: ResolvedOrResolver) -> str:
+    """Return the base URI of a Resolved or Resolver."""
+    if isinstance(target, Resolved):
+        return target.resolver._base_uri
+    return target._base_uri
+
+
+# Verify referencing layout at import time so version-skew fails loud.
+assert_referencing_layout()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/jsonschema_path/accessors.py 
new/jsonschema-path-0.5.0/jsonschema_path/accessors.py
--- old/jsonschema-path-0.4.6/jsonschema_path/accessors.py      2026-04-27 
20:56:09.000000000 +0200
+++ new/jsonschema-path-0.5.0/jsonschema_path/accessors.py      2026-05-19 
22:44:09.000000000 +0200
@@ -18,6 +18,7 @@
 from referencing._core import Resolver
 from referencing.jsonschema import DRAFT202012
 
+from jsonschema_path._referencing_compat import rebind_resolved
 from jsonschema_path.caches import FullPathResolvedCache
 from jsonschema_path.handlers import default_handlers
 from jsonschema_path.resolvers import CachedPathResolver
@@ -27,11 +28,27 @@
 
 
 class SchemaAccessor(LookupAccessor):
+    """Resource handle binding a schema document to its resolver.
+
+    Identity contract: a `SchemaAccessor` is its own identity token,
+    discriminated by the wrapped node (by reference) and the
+    `_path_resolver` instance (by reference). Both are set in
+    `__init__` and never reassigned, so equality and hash are stable
+    for the accessor's lifetime even though the inner registry
+    evolves as `$ref`s are resolved.
+
+    Consequence: two `from_schema(doc, ...)` calls produce non-equal
+    accessors even with identical arguments, because each call builds
+    its own `_path_resolver`. Build one accessor per schema document
+    and reuse it across all derived `SchemaPath`s — see "Identity and
+    equality" and "Recommended usage" in the README.
+    """
+
     def __init__(
         self,
         schema: Schema,
         resolver: Resolver[Schema],
-        resolved_cache_maxsize: int = 0,
+        resolved_cache_maxsize: int = 128,
     ):
         if resolved_cache_maxsize < 0:
             raise ValueError("resolved_cache_maxsize must be >= 0")
@@ -45,6 +62,31 @@
             maxsize=resolved_cache_maxsize
         )
 
+    def __eq__(self, other: object) -> Any:
+        if not isinstance(other, SchemaAccessor):
+            return NotImplemented
+        # See the class docstring for the identity contract. Both
+        # discriminators are reference-stable: `_node` is the
+        # constructor argument and `_path_resolver` is constructed
+        # once in `__init__` and never reassigned (only its inner
+        # `resolver` field is swapped when the registry evolves).
+        return (
+            type(self) is type(other)
+            and self._node is other._node
+            and self._path_resolver is other._path_resolver
+        )
+
+    def __hash__(self) -> int:
+        # Reference-stable inputs only — does not depend on the schema
+        # dict being hashable or on the mutating registry.
+        return hash(
+            (
+                type(self),
+                id(self._node),
+                id(self._path_resolver),
+            )
+        )
+
     @classmethod
     def from_schema(
         cls,
@@ -176,12 +218,25 @@
     def get_resolved(self, parts: Sequence[LookupKey]) -> Resolved[LookupNode]:
         cached_resolved = self._resolved_cache.get(parts)
         if cached_resolved is not None:
-            return cached_resolved
+            # Read `_registry` directly: it is a stable attrs-backed
+            # attribute on every supported referencing version (the
+            # import-time `assert_referencing_layout` guarantees this)
+            # and a plain attribute access is ~30ns vs ~100ns through a
+            # helper with isinstance dispatch. The cold-path field
+            # *write* still goes through `rebind_resolved`.
+            current_registry = self._path_resolver.resolver._registry
+            if cached_resolved.resolver._registry is current_registry:
+                return cached_resolved
+            # Rebind to the current registry rather than discard. Safe
+            # under monotonic registry growth (see caches.py docstring).
+            rebound = cast(
+                Resolved[LookupNode],
+                rebind_resolved(cached_resolved, current_registry),
+            )
+            self._resolved_cache.set(parts, rebound)
+            return rebound
 
         result = self._path_resolver.resolve(self.node, parts)
-        if result.registry_changed:
-            self._resolved_cache.invalidate()
-
         self._resolved_cache.set(parts, result.resolved)
 
         return result.resolved
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/jsonschema_path/caches.py 
new/jsonschema-path-0.5.0/jsonschema_path/caches.py
--- old/jsonschema-path-0.4.6/jsonschema_path/caches.py 2026-04-27 
20:56:09.000000000 +0200
+++ new/jsonschema-path-0.5.0/jsonschema_path/caches.py 2026-05-19 
22:44:09.000000000 +0200
@@ -1,4 +1,15 @@
-"""JSONSchema path caches module."""
+"""JSONSchema path caches module.
+
+Both caches store ``Resolved`` values keyed on hashable schema paths.
+Staleness across ``referencing.Registry`` growth is *not* handled by
+invalidation here; the callers rebind cached ``Resolved`` values to the
+current registry on read (see
+``jsonschema_path._referencing_compat.rebind_registry``). This relies on
+the assumption that registries grow monotonically — resources are added,
+never replaced. Handlers that return drifting content for the same URI
+violate that assumption; users who need to defend against that should
+disable caching with ``resolved_cache_maxsize=0``.
+"""
 
 from collections import OrderedDict
 from collections.abc import Sequence
@@ -11,16 +22,15 @@
 class FullPathResolvedCache:
     def __init__(self, maxsize: int):
         self._maxsize = maxsize
-        self._generation = 0
         self._cache: OrderedDict[
-            tuple[tuple[LookupKey, ...], int],
+            tuple[LookupKey, ...],
             Resolved[LookupNode],
         ] = OrderedDict()
 
     def _make_key(
         self,
         parts: Sequence[LookupKey],
-    ) -> tuple[tuple[LookupKey, ...], int] | None:
+    ) -> tuple[LookupKey, ...] | None:
         if self._maxsize <= 0:
             return None
 
@@ -30,7 +40,7 @@
         except TypeError:
             return None
 
-        return (parts_tuple, self._generation)
+        return parts_tuple
 
     def get(
         self,
@@ -61,10 +71,6 @@
         if len(self._cache) > self._maxsize:
             self._cache.popitem(last=False)
 
-    def invalidate(self) -> None:
-        self._generation += 1
-        self._cache.clear()
-
 
 class PrefixResolvedCache:
     def __init__(self) -> None:
@@ -89,6 +95,19 @@
 
         return None
 
+    def replace(
+        self,
+        parts: tuple[LookupKey, ...],
+        index: int,
+        resolved: Resolved[LookupNode],
+    ) -> None:
+        """Overwrite an existing prefix entry (used after a rebind)."""
+        prefix = parts[:index]
+        try:
+            self._cache[prefix] = resolved
+        except TypeError:
+            pass
+
     def store_intermediate(
         self,
         parts: tuple[LookupKey, ...],
@@ -103,6 +122,3 @@
             self._cache[prefix] = resolved
         except TypeError:
             pass
-
-    def invalidate(self) -> None:
-        self._cache.clear()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/jsonschema_path/paths.py 
new/jsonschema-path-0.5.0/jsonschema_path/paths.py
--- old/jsonschema-path-0.4.6/jsonschema_path/paths.py  2026-04-27 
20:56:09.000000000 +0200
+++ new/jsonschema-path-0.5.0/jsonschema_path/paths.py  2026-05-19 
22:44:09.000000000 +0200
@@ -7,7 +7,6 @@
 from collections.abc import Iterator
 from collections.abc import Sequence
 from contextlib import contextmanager
-from functools import cached_property
 from pathlib import Path
 from typing import Any
 from typing import TypeVar
@@ -260,18 +259,12 @@
     @contextmanager
     def open(self) -> Any:
         """Open the path."""
-        # Cached path content
         with self.resolve() as resolved:
             yield resolved.contents
 
     @contextmanager
     def resolve(self) -> Iterator[Resolved[SchemaNode]]:
         """Resolve the path."""
-        # Cached path content
-        yield self._get_resolved
-
-    @cached_property
-    def _get_resolved(self) -> Resolved[SchemaNode]:
         assert isinstance(self.accessor, SchemaAccessor)
         with self.accessor.resolve(self.parts) as resolved:
-            return resolved
+            yield resolved
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/jsonschema_path/resolvers.py 
new/jsonschema-path-0.5.0/jsonschema_path/resolvers.py
--- old/jsonschema-path-0.4.6/jsonschema_path/resolvers.py      2026-04-27 
20:56:09.000000000 +0200
+++ new/jsonschema-path-0.5.0/jsonschema_path/resolvers.py      2026-05-19 
22:44:09.000000000 +0200
@@ -8,6 +8,8 @@
 from referencing._core import Resolved
 from referencing._core import Resolver
 
+from jsonschema_path._referencing_compat import rebind_registry
+from jsonschema_path._referencing_compat import rebind_resolved
 from jsonschema_path.caches import PrefixResolvedCache
 from jsonschema_path.nodes import SchemaNode
 from jsonschema_path.typing import Schema
@@ -55,7 +57,20 @@
             start = 0
             self.prefix_cache.seed_root(resolved)
         else:
-            start, resolved = cached_prefix
+            start, cached_resolved = cached_prefix
+            # Rebind to the current registry if it grew since this prefix
+            # was cached, then refresh the stored entry so subsequent hits
+            # skip the check. Reads of `_registry` go direct (cheap, plain
+            # attribute access on an attrs class); the cold-path write
+            # still goes through `rebind_resolved`.
+            current_registry = self.resolver._registry
+            if cached_resolved.resolver._registry is not current_registry:
+                cached_resolved = cast(
+                    Resolved[LookupNode],
+                    rebind_resolved(cached_resolved, current_registry),
+                )
+                self.prefix_cache.replace(parts_tuple, start, cached_resolved)
+            resolved = cached_resolved
             current_node = resolved.contents
             current_resolver = cast(Resolver[Schema], resolved.resolver)
 
@@ -83,9 +98,10 @@
         if registry is self.resolver._registry:
             return False
 
-        self.resolver = self.resolver._evolve(
-            self.resolver._base_uri,
-            registry=registry,
+        # Rebind self.resolver so subsequent fresh resolutions start from
+        # the latest registry. The prefix cache is *not* invalidated; its
+        # entries are rebound on read in _resolve_with_prefix_cache above.
+        self.resolver = cast(
+            Resolver[Schema], rebind_registry(self.resolver, registry)
         )
-        self.prefix_cache.invalidate()
         return True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/poetry.lock 
new/jsonschema-path-0.5.0/poetry.lock
--- old/jsonschema-path-0.4.6/poetry.lock       2026-04-27 20:56:09.000000000 
+0200
+++ new/jsonschema-path-0.5.0/poetry.lock       2026-05-19 22:44:09.000000000 
+0200
@@ -38,39 +38,39 @@
 
 [[package]]
 name = "black"
-version = "26.1.0"
+version = "26.3.1"
 description = "The uncompromising code formatter."
 optional = false
 python-versions = ">=3.10"
 groups = ["dev"]
 files = [
-    {file = "black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = 
"sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168"},
-    {file = "black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = 
"sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d"},
-    {file = 
"black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0"},
-    {file = "black-26.1.0-cp310-cp310-win_amd64.whl", hash = 
"sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24"},
-    {file = "black-26.1.0-cp310-cp310-win_arm64.whl", hash = 
"sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89"},
-    {file = "black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = 
"sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5"},
-    {file = "black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = 
"sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68"},
-    {file = 
"black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14"},
-    {file = "black-26.1.0-cp311-cp311-win_amd64.whl", hash = 
"sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c"},
-    {file = "black-26.1.0-cp311-cp311-win_arm64.whl", hash = 
"sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4"},
-    {file = "black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = 
"sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f"},
-    {file = "black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = 
"sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6"},
-    {file = 
"black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a"},
-    {file = "black-26.1.0-cp312-cp312-win_amd64.whl", hash = 
"sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791"},
-    {file = "black-26.1.0-cp312-cp312-win_arm64.whl", hash = 
"sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954"},
-    {file = "black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = 
"sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304"},
-    {file = "black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = 
"sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9"},
-    {file = 
"black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b"},
-    {file = "black-26.1.0-cp313-cp313-win_amd64.whl", hash = 
"sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b"},
-    {file = "black-26.1.0-cp313-cp313-win_arm64.whl", hash = 
"sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca"},
-    {file = "black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = 
"sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115"},
-    {file = "black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = 
"sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79"},
-    {file = 
"black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af"},
-    {file = "black-26.1.0-cp314-cp314-win_amd64.whl", hash = 
"sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f"},
-    {file = "black-26.1.0-cp314-cp314-win_arm64.whl", hash = 
"sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0"},
-    {file = "black-26.1.0-py3-none-any.whl", hash = 
"sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede"},
-    {file = "black-26.1.0.tar.gz", hash = 
"sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58"},
+    {file = "black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = 
"sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"},
+    {file = "black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = 
"sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"},
+    {file = 
"black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac"},
+    {file = "black-26.3.1-cp310-cp310-win_amd64.whl", hash = 
"sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a"},
+    {file = "black-26.3.1-cp310-cp310-win_arm64.whl", hash = 
"sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a"},
+    {file = "black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = 
"sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff"},
+    {file = "black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = 
"sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c"},
+    {file = 
"black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5"},
+    {file = "black-26.3.1-cp311-cp311-win_amd64.whl", hash = 
"sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e"},
+    {file = "black-26.3.1-cp311-cp311-win_arm64.whl", hash = 
"sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5"},
+    {file = "black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = 
"sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1"},
+    {file = "black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = 
"sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f"},
+    {file = 
"black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7"},
+    {file = "black-26.3.1-cp312-cp312-win_amd64.whl", hash = 
"sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983"},
+    {file = "black-26.3.1-cp312-cp312-win_arm64.whl", hash = 
"sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb"},
+    {file = "black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = 
"sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54"},
+    {file = "black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = 
"sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f"},
+    {file = 
"black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56"},
+    {file = "black-26.3.1-cp313-cp313-win_amd64.whl", hash = 
"sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839"},
+    {file = "black-26.3.1-cp313-cp313-win_arm64.whl", hash = 
"sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2"},
+    {file = "black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = 
"sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78"},
+    {file = "black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = 
"sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568"},
+    {file = 
"black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
 hash = 
"sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f"},
+    {file = "black-26.3.1-cp314-cp314-win_amd64.whl", hash = 
"sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c"},
+    {file = "black-26.3.1-cp314-cp314-win_arm64.whl", hash = 
"sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1"},
+    {file = "black-26.3.1-py3-none-any.whl", hash = 
"sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b"},
+    {file = "black-26.3.1.tar.gz", hash = 
"sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07"},
 ]
 
 [package.dependencies]
@@ -79,7 +79,7 @@
 packaging = ">=22.0"
 pathspec = ">=1.0.0"
 platformdirs = ">=2"
-pytokens = ">=0.3.0"
+pytokens = ">=0.4.0,<0.5.0"
 tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
 typing-extensions = {version = ">=4.0.1", markers = "python_version < 
\"3.11\""}
 
@@ -87,7 +87,7 @@
 colorama = ["colorama (>=0.4.3)"]
 d = ["aiohttp (>=3.10)"]
 jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-uvloop = ["uvloop (>=0.15.2)"]
+uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) 
; sys_platform == \"win32\""]
 
 [[package]]
 name = "certifi"
@@ -354,28 +354,28 @@
 
 [[package]]
 name = "deptry"
-version = "0.23.1"
+version = "0.25.1"
 description = "A command line utility to check for unused, missing and 
transitive dependencies in a Python project."
 optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
 groups = ["dev"]
 files = [
-    {file = "deptry-0.23.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = 
"sha256:f0b231d098fb5b48d8973c9f192c353ffdd395770063424969fa7f15ddfea7d8"},
-    {file = "deptry-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = 
"sha256:bf057f514bb2fa18a2b192a7f7372bd14577ff46b11486933e8383dfef461983"},
-    {file = 
"deptry-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", 
hash = 
"sha256:1ee3f5663bb1c048e2aaf25a4d9e6d09cc1f3b3396ee248980878c6a6c9c0e21"},
-    {file = 
"deptry-0.23.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash 
= "sha256:ae0366dc5f50a5fb29cf90de1110c5e368513de6c1b2dac439f2817f3f752616"},
-    {file = "deptry-0.23.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = 
"sha256:ab156a90a9eda5819aeb1c1da585dd4d5ec509029399a38771a49e78f40db90f"},
-    {file = "deptry-0.23.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = 
"sha256:651c7eb168233755152fcc468713c024d64a03069645187edb4a17ba61ce6133"},
-    {file = "deptry-0.23.1-cp39-abi3-win_amd64.whl", hash = 
"sha256:8da1e8f70e7086ebc228f3a4a3cfb5aa127b09b5eef60d694503d6bb79809025"},
-    {file = "deptry-0.23.1-cp39-abi3-win_arm64.whl", hash = 
"sha256:f589497a5809717db4dcf2aa840f2847c0a4c489331608e538850b6a9ab1c30b"},
-    {file = "deptry-0.23.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = 
"sha256:6af91d86380ef703adb6ae65f273d88e3cca7fd315c4c309da857a0cfa728244"},
-    {file = "deptry-0.23.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = 
"sha256:42a249d317c3128c286035a1f7aaa41a0c3c967f17848817c2e07ca50d5ed450"},
-    {file = 
"deptry-0.23.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:d988c7c75201997970bae1e8d564b4c7a14d350556c4f7c269fd33f3b081c314"},
-    {file = 
"deptry-0.23.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
 hash = 
"sha256:ae13d8e65ae88b77632c45edb4038301a6f9efcac06715abfde9a029e5879698"},
-    {file = "deptry-0.23.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash 
= "sha256:40058a7a3fe9dacb745668897ee992e58daf5aac406b668ff2eaaf0f6f586550"},
-    {file = "deptry-0.23.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash 
= "sha256:d111cf4261eeadbdb20051d8d542f04deb3cfced0cb280ece8d654f7f6055921"},
-    {file = "deptry-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = 
"sha256:9f9bbb92f95ada9ccfa5ecefee05ba3c39cfa0734b5483a3a1a3c4eeb9c99054"},
-    {file = "deptry-0.23.1.tar.gz", hash = 
"sha256:5d23e0ef25f3c56405c05383a476edda55944563c5c47a3e9249ed3ec860d382"},
+    {file = "deptry-0.25.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = 
"sha256:a4dd1148db24a1ddacfa8b840836c6019c2f864fcb7579dd089fd217606338c8"},
+    {file = "deptry-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = 
"sha256:c67c666d916ef12013c0772e40d78be0f21577a495d8d99ec5fcb18c332d393d"},
+    {file = 
"deptry-0.25.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", 
hash = 
"sha256:58d39279828dbf4efc1abb40bf50a71b21499c36759bed5a8d8a3c0e3149b091"},
+    {file = 
"deptry-0.25.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash 
= "sha256:14bfcc28b4326ed8c6abb30691b19077d4ef8613cfba6c37ef5b1f471775bf6f"},
+    {file = "deptry-0.25.1-cp310-abi3-musllinux_1_1_aarch64.whl", hash = 
"sha256:555f5f9a487899ec9bf301eecba1745e14d212c4b354f4d3a5fd691e907366d3"},
+    {file = "deptry-0.25.1-cp310-abi3-musllinux_1_1_x86_64.whl", hash = 
"sha256:18d21b3545ab2bfec53f3f45c6f5f201d55f713323327f8d12674505469ae6b7"},
+    {file = "deptry-0.25.1-cp310-abi3-win_amd64.whl", hash = 
"sha256:b59a560cb7dffb21832a98bb80d33d614cfb5630ea36ce21833eabf4eae3df99"},
+    {file = "deptry-0.25.1-cp310-abi3-win_arm64.whl", hash = 
"sha256:6efffd8116fb9d2c45a251382ce4ce1c38dbb17179f581ec9231ed5390f7fc12"},
+    {file = "deptry-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = 
"sha256:30d64d4df1c08bc69de56cb0b4ec1f4cd9fa2e42582347d5b1eb25fd0e401745"},
+    {file = "deptry-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = 
"sha256:87bcd90f99a98bb059c7580bc315c3f87d97fe2db725530030bc974176834735"},
+    {file = 
"deptry-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
 hash = 
"sha256:80f31eb5c520651b102568dd91f738222b250a3e44c9e95d4941322109b8d40a"},
+    {file = 
"deptry-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
 hash = 
"sha256:df88952a2bab7517ef23cb304b979199b28449e5d9db2e9ba9bc27a286ac852b"},
+    {file = "deptry-0.25.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash 
= "sha256:e6f7b8fa72932e51e86799b10dcd29381b2132dc799c790dca3b28ab08dffb28"},
+    {file = "deptry-0.25.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash 
= "sha256:e3fa3321078e11cd1ac3f10ce3ff0547731c53f9253b87c757a8749c76fe8fa9"},
+    {file = "deptry-0.25.1-pp311-pypy311_pp73-win_amd64.whl", hash = 
"sha256:03c032c32492fde434736954fbcaff09c02bf207b0f793b77e9040300e34b344"},
+    {file = "deptry-0.25.1.tar.gz", hash = 
"sha256:45c8cd982c85cd4faae573ddff6920de7eec735336db6973f26a765ae7950f7d"},
 ]
 
 [package.dependencies]
@@ -383,7 +383,7 @@
 colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
 packaging = ">=23.2"
 requirements-parser = ">=0.11.0,<1"
-tomli = {version = ">=2.0.1", markers = "python_full_version < \"3.11.0\""}
+tomli = {version = ">=2.0.1", markers = "python_full_version < \"3.15.0\""}
 
 [[package]]
 name = "distlib"
@@ -506,19 +506,19 @@
 
 [[package]]
 name = "idna"
-version = "3.10"
+version = "3.15"
 description = "Internationalized Domain Names in Applications (IDNA)"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 groups = ["main", "dev"]
 files = [
-    {file = "idna-3.10-py3-none-any.whl", hash = 
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
-    {file = "idna-3.10.tar.gz", hash = 
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+    {file = "idna-3.15-py3-none-any.whl", hash = 
"sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"},
+    {file = "idna-3.15.tar.gz", hash = 
"sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"},
 ]
 markers = {main = "extra == \"requests\""}
 
 [package.extras]
-all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff 
(>=0.6.2)"]
+all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
 
 [[package]]
 name = "iniconfig"
@@ -588,14 +588,14 @@
 
 [[package]]
 name = "isort"
-version = "8.0.0"
+version = "8.0.1"
 description = "A Python utility / library to sort Python imports."
 optional = false
 python-versions = ">=3.10.0"
 groups = ["dev"]
 files = [
-    {file = "isort-8.0.0-py3-none-any.whl", hash = 
"sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9"},
-    {file = "isort-8.0.0.tar.gz", hash = 
"sha256:fddea59202f231e170e52e71e3510b99c373b6e571b55d9c7b31b679c0fed47c"},
+    {file = "isort-8.0.1-py3-none-any.whl", hash = 
"sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"},
+    {file = "isort-8.0.1.tar.gz", hash = 
"sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"},
 ]
 
 [package.extras]
@@ -763,14 +763,14 @@
 
 [[package]]
 name = "pathable"
-version = "0.5.0"
+version = "0.6.0"
 description = "Object-oriented paths"
 optional = false
 python-versions = "<4.0,>=3.10"
 groups = ["main"]
 files = [
-    {file = "pathable-0.5.0-py3-none-any.whl", hash = 
"sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6"},
-    {file = "pathable-0.5.0.tar.gz", hash = 
"sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1"},
+    {file = "pathable-0.6.0-py3-none-any.whl", hash = 
"sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566"},
+    {file = "pathable-0.6.0.tar.gz", hash = 
"sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58"},
 ]
 
 [[package]]
@@ -928,14 +928,14 @@
 
 [[package]]
 name = "pygments"
-version = "2.19.1"
+version = "2.20.0"
 description = "Pygments is a syntax highlighting package written in Python."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 groups = ["dev"]
 files = [
-    {file = "pygments-2.19.1-py3-none-any.whl", hash = 
"sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
-    {file = "pygments-2.19.1.tar.gz", hash = 
"sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
+    {file = "pygments-2.20.0-py3-none-any.whl", hash = 
"sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
+    {file = "pygments-2.20.0.tar.gz", hash = 
"sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
 ]
 
 [package.extras]
@@ -943,21 +943,21 @@
 
 [[package]]
 name = "pytest"
-version = "8.4.0"
+version = "9.0.3"
 description = "pytest: simple powerful testing with Python"
 optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
 groups = ["dev"]
 files = [
-    {file = "pytest-8.4.0-py3-none-any.whl", hash = 
"sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"},
-    {file = "pytest-8.4.0.tar.gz", hash = 
"sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"},
+    {file = "pytest-9.0.3-py3-none-any.whl", hash = 
"sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
+    {file = "pytest-9.0.3.tar.gz", hash = 
"sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
 ]
 
 [package.dependencies]
 colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
 exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
-iniconfig = ">=1"
-packaging = ">=20"
+iniconfig = ">=1.0.1"
+packaging = ">=22"
 pluggy = ">=1.5,<2"
 pygments = ">=2.7.2"
 tomli = {version = ">=1", markers = "python_version < \"3.11\""}
@@ -1165,26 +1165,27 @@
 
 [[package]]
 name = "requests"
-version = "2.32.5"
+version = "2.33.0"
 description = "Python HTTP for Humans."
 optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
 groups = ["main", "dev"]
 files = [
-    {file = "requests-2.32.5-py3-none-any.whl", hash = 
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
-    {file = "requests-2.32.5.tar.gz", hash = 
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
+    {file = "requests-2.33.0-py3-none-any.whl", hash = 
"sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"},
+    {file = "requests-2.33.0.tar.gz", hash = 
"sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"},
 ]
 markers = {main = "extra == \"requests\""}
 
 [package.dependencies]
-certifi = ">=2017.4.17"
+certifi = ">=2023.5.7"
 charset_normalizer = ">=2,<4"
 idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<3"
+urllib3 = ">=1.26,<3"
 
 [package.extras]
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", 
"pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
 
 [[package]]
 name = "requirements-parser"
@@ -1420,7 +1421,7 @@
 optional = false
 python-versions = ">=3.8"
 groups = ["dev"]
-markers = "python_full_version <= \"3.11.0a6\""
+markers = "python_version < \"3.15\""
 files = [
     {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = 
"sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
     {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = 
"sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -1486,26 +1487,26 @@
 
 [[package]]
 name = "types-pyyaml"
-version = "6.0.12.20250516"
+version = "6.0.12.20250915"
 description = "Typing stubs for PyYAML"
 optional = false
 python-versions = ">=3.9"
 groups = ["dev"]
 files = [
-    {file = "types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = 
"sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530"},
-    {file = "types_pyyaml-6.0.12.20250516.tar.gz", hash = 
"sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba"},
+    {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = 
"sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"},
+    {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = 
"sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"},
 ]
 
 [[package]]
 name = "types-requests"
-version = "2.32.4.20250913"
+version = "2.32.4.20260107"
 description = "Typing stubs for requests"
 optional = false
 python-versions = ">=3.9"
 groups = ["dev"]
 files = [
-    {file = "types_requests-2.32.4.20250913-py3-none-any.whl", hash = 
"sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1"},
-    {file = "types_requests-2.32.4.20250913.tar.gz", hash = 
"sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d"},
+    {file = "types_requests-2.32.4.20260107-py3-none-any.whl", hash = 
"sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d"},
+    {file = "types_requests-2.32.4.20260107.tar.gz", hash = 
"sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f"},
 ]
 
 [package.dependencies]
@@ -1538,14 +1539,14 @@
 
 [[package]]
 name = "urllib3"
-version = "2.6.3"
+version = "2.7.0"
 description = "HTTP library with thread-safe connection pooling, file post, 
and more."
 optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
 groups = ["main", "dev"]
 files = [
-    {file = "urllib3-2.6.3-py3-none-any.whl", hash = 
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
-    {file = "urllib3-2.6.3.tar.gz", hash = 
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
+    {file = "urllib3-2.7.0-py3-none-any.whl", hash = 
"sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
+    {file = "urllib3-2.7.0.tar.gz", hash = 
"sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
 ]
 markers = {main = "extra == \"requests\""}
 
@@ -1595,4 +1596,4 @@
 [metadata]
 lock-version = "2.1"
 python-versions = ">=3.10,<4.0.0"
-content-hash = 
"7827ad3deef0b8ae31b769b44e6d894dbcae39b518d70490fec10884b7965bde"
+content-hash = 
"c5d97114b2c1e7bbf99c37320647a097c7de60b803a48d45bc9fb180522a871a"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/pyproject.toml 
new/jsonschema-path-0.5.0/pyproject.toml
--- old/jsonschema-path-0.4.6/pyproject.toml    2026-04-27 20:56:09.000000000 
+0200
+++ new/jsonschema-path-0.5.0/pyproject.toml    2026-05-19 22:44:09.000000000 
+0200
@@ -19,7 +19,7 @@
 
 [tool.poetry]
 name = "jsonschema-path"
-version = "0.4.6"
+version = "0.5.0"
 description = "JSONSchema Spec with object-oriented paths"
 authors = ["Artur Maciag <[email protected]>"]
 license = "Apache-2.0"
@@ -41,7 +41,8 @@
 ]
 
 [tool.poetry.dependencies]
-pathable = "^0.5.0"
+attrs = ">=22.2.0"
+pathable = "^0.6.0"
 python = ">=3.10,<4.0.0"
 PyYAML = ">=5.1"
 requests = {version = "^2.31.0", optional = true}
@@ -50,7 +51,7 @@
 [tool.poetry.group.dev.dependencies]
 tbump = "^6.11.0"
 pre-commit = "*"
-pytest = "^8.2.1"
+pytest = ">=8.2.1,<10.0.0"
 pytest-flake8 = "=1.3.0"
 pytest-cov = ">=5,<7"
 isort = ">=5.13.2,<9.0.0"
@@ -60,7 +61,7 @@
 types-PyYAML = "^6.0.12"
 types-requests = "^2.31.0"
 responses = ">=0.25,<0.27"
-deptry = ">=0.19.1,<0.24.0"
+deptry = ">=0.19.1,<0.26.0"
 pyflakes = ">=2.5,<4.0"
 ipdb = "^0.13.13"
 
@@ -93,7 +94,7 @@
 tag_template = "{new_version}"
 
 [tool.tbump.version]
-current = "0.4.6"
+current = "0.5.0"
 regex = '''
   (?P<major>\d+)
   \.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonschema-path-0.4.6/tests/unit/test_accessors.py 
new/jsonschema-path-0.5.0/tests/unit/test_accessors.py
--- old/jsonschema-path-0.4.6/tests/unit/test_accessors.py      2026-04-27 
20:56:09.000000000 +0200
+++ new/jsonschema-path-0.5.0/tests/unit/test_accessors.py      2026-05-19 
22:44:09.000000000 +0200
@@ -2,9 +2,14 @@
 from unittest.mock import patch
 
 import pytest
+from referencing import Registry
+from referencing.jsonschema import DRAFT202012
 
+from jsonschema_path import SchemaPath
 from jsonschema_path.accessors import SchemaAccessor
+from jsonschema_path.handlers import default_handlers
 from jsonschema_path.nodes import SchemaNode
+from jsonschema_path.retrievers import SchemaRetriever
 
 
 class TestSchemaAccessorOpen:
@@ -229,7 +234,7 @@
 
         assert first_a is not second_a
 
-    def test_registry_evolution_invalidates_previous_generation(self):
+    def test_registry_evolution_rebinds_cached_resolved(self):
         retrieve = Mock(side_effect=[{"value": 1}, {"value": 2}])
         accessor = SchemaAccessor.from_schema(
             {
@@ -246,16 +251,48 @@
 
         first_one = accessor.get_resolved(["one", "value"])
         assert first_one.contents == 1
+        registry_after_first = accessor._path_resolver.resolver._registry
+        assert first_one.resolver._registry is registry_after_first
 
         second_two = accessor.get_resolved(["two", "value"])
         assert second_two.contents == 2
+        registry_after_two = accessor._path_resolver.resolver._registry
+        assert registry_after_two is not registry_after_first
 
+        # Cache hit rebinds to the current registry instead of
+        # re-resolving from scratch: same underlying contents object,
+        # new Resolved wrapper carrying the up-to-date registry.
         second_one = accessor.get_resolved(["one", "value"])
         assert second_one.contents == 1
+        assert second_one.contents is first_one.contents
+        assert second_one.resolver._registry is registry_after_two
         assert second_one is not first_one
 
+        # No re-retrieval — each $ref target is loaded exactly once.
         assert retrieve.call_count == 2
 
+    def test_rebound_cache_hit_preserves_base_uri_and_previous(self):
+        retrieve = Mock(side_effect=[{"value": 1}, {"value": 2}])
+        accessor = SchemaAccessor.from_schema(
+            {
+                "one": {"$ref": "x://one"},
+                "two": {"$ref": "x://two"},
+            },
+            handlers={"x": retrieve},
+            resolved_cache_maxsize=8,
+        )
+
+        first_one = accessor.get_resolved(["one", "value"])
+        _ = accessor.get_resolved(["two", "value"])
+        rebound = accessor.get_resolved(["one", "value"])
+
+        # Rebind is a pure registry field swap. base_uri and the
+        # dynamic-scope `_previous` chain must survive intact, otherwise
+        # subsequent $ref resolution under the rebound Resolved would
+        # use the wrong scope.
+        assert rebound.resolver._base_uri == first_one.resolver._base_uri
+        assert rebound.resolver._previous == first_one.resolver._previous
+
 
 class TestSchemaAccessorPrefixCache:
     def test_reuses_longest_resolved_prefix_for_siblings(self):
@@ -300,7 +337,7 @@
 
         assert first is not second
 
-    def test_prefix_cache_is_cleared_when_registry_evolves(self):
+    def test_prefix_cache_is_preserved_when_registry_evolves(self):
         retrieve = Mock(return_value={"value": "tested"})
         accessor = SchemaAccessor.from_schema(
             {
@@ -314,5 +351,172 @@
 
         _ = accessor.get_resolved(["one", "value"])
 
+        # Prefix cache used to be wiped on registry growth. With the
+        # rebind-on-read strategy it retains its intermediate entry.
         assert accessor._path_resolver.prefix_cache is prefix_cache
-        assert accessor._path_resolver.prefix_cache._cache == {}
+        assert ("one",) in prefix_cache._cache
+
+    def test_prefix_cache_rebound_avoids_redundant_retrieval(self):
+        payloads = {
+            "x://one": {
+                "fast": "shallow",
+                "deep": {"$ref": "x://later"},
+            },
+            "x://primer": {"primed": {"$ref": "x://later"}},
+            "x://later": {"value": "found"},
+        }
+        retrieve = Mock(side_effect=lambda uri: payloads[uri])
+        accessor = SchemaAccessor.from_schema(
+            {
+                "one": {"$ref": "x://one"},
+                "primer": {"$ref": "x://primer"},
+            },
+            handlers={"x": retrieve},
+        )
+
+        # Populate the prefix cache for ("one",) under a registry that
+        # only knows x://one. Walking through "fast" avoids loading
+        # x://later at this point.
+        assert accessor.read(["one", "fast"]) == "shallow"
+
+        # Grow the registry through a different branch, loading
+        # x://primer and x://later.
+        assert accessor.read(["primer", "primed", "value"]) == "found"
+
+        # Re-enter through the cached ("one",) prefix and follow a
+        # $ref into x://later. Without rebind this would either fail
+        # (resource unknown to the stale registry) or trigger a
+        # second retrieve. With rebind it should reuse the already
+        # loaded x://later resource.
+        assert accessor.read(["one", "deep", "value"]) == "found"
+
+        calls = sorted(c.args[0] for c in retrieve.call_args_list)
+        assert calls == ["x://later", "x://one", "x://primer"]
+
+
+class TestSchemaAccessorIdentity:
+    """Locks in the per-resource-handle identity model.
+
+    SchemaAccessor identity is the accessor instance itself, with
+    discrimination on `_node` (by reference) and `_path_resolver`
+    (by reference) — not a value tuple of its inputs. This forces the
+    recommended lifecycle: construct one SchemaAccessor per schema
+    document and reuse it across all derived SchemaPaths.
+    """
+
+    def test_same_instance_compares_equal_and_hashes_equal(self):
+        accessor = SchemaAccessor.from_schema({"a": 1})
+
+        assert accessor == accessor
+        assert hash(accessor) == hash(accessor)
+
+    def test_accessor_is_hashable(self):
+        accessor = SchemaAccessor.from_schema({"a": 1})
+
+        # Would raise TypeError before this PR (defining __eq__
+        # without __hash__ silently makes instances unhashable).
+        assert hash(accessor) == hash(accessor)
+        {accessor}  # constructable as a set element
+
+    def test_distinct_from_schema_calls_not_equal(self):
+        # Each from_schema() call builds its own _path_resolver, so
+        # the resulting accessors are distinct resource handles even
+        # with identical arguments. This is the "reuse the accessor"
+        # assertion: callers must hold onto the accessor instance,
+        # not reconstruct it on demand.
+        doc = {"a": 1}
+
+        acc1 = SchemaAccessor.from_schema(doc)
+        acc2 = SchemaAccessor.from_schema(doc)
+
+        assert acc1 != acc2
+        # Hashes are allowed to collide but are very unlikely to here.
+
+    def test_distinct_dicts_not_equal(self):
+        # Value-equal but distinct dict objects are distinct resources:
+        # `_node is other._node` is false. Included for clarity even
+        # though the `_path_resolver` check would also catch it.
+        acc1 = SchemaAccessor.from_schema({"a": 1})
+        acc2 = SchemaAccessor.from_schema({"a": 1})
+
+        assert acc1 != acc2
+
+    def test_different_base_uri_not_equal(self):
+        # Same schema dict by reference, different base_uri → different
+        # resources, because $ref resolution differs.
+        doc = {"a": 1}
+
+        acc1 = SchemaAccessor.from_schema(doc, base_uri="https://a/";)
+        acc2 = SchemaAccessor.from_schema(doc, base_uri="https://b/";)
+
+        assert acc1 != acc2
+
+    def test_path_equality_follows_accessor_equality(self):
+        accessor = SchemaAccessor.from_schema({"a": {"b": 1}})
+
+        p1 = SchemaPath(accessor) / "a"
+        p2 = SchemaPath(accessor) / "a"
+
+        # Same accessor instance + same parts → equal paths and
+        # equal hashes (delegated to pathable's AccessorPath identity).
+        assert p1 == p2
+        assert hash(p1) == hash(p2)
+
+    def test_path_inequality_across_distinct_accessors(self):
+        doc = {"a": {"b": 1}}
+        acc1 = SchemaAccessor.from_schema(doc)
+        acc2 = SchemaAccessor.from_schema(doc)
+
+        p1 = SchemaPath(acc1) / "a"
+        p2 = SchemaPath(acc2) / "a"
+
+        # Distinct accessor instances → distinct resources → unequal
+        # paths even though parts and underlying dict reference match.
+        assert p1 != p2
+
+    def test_resolved_cache_shared_when_accessor_reused(self):
+        # Two paths over the same accessor hit the same resolved cache.
+        # If a future refactor reintroduces per-path caching, this test
+        # fails because the second .get_resolved would return a fresh
+        # object instead of the cached one.
+        accessor = SchemaAccessor.from_schema(
+            {"a": {"b": 1}},
+            resolved_cache_maxsize=8,
+        )
+
+        p1 = SchemaPath(accessor) / "a" / "b"
+        p2 = SchemaPath(accessor) / "a" / "b"
+
+        with p1.resolve() as r1:
+            with p2.resolve() as r2:
+                assert r1 is r2
+
+    def test_hash_stable_across_registry_evolution(self):
+        # The inner `_path_resolver.resolver` is reassigned when the
+        # registry evolves (see CachedPathResolver._sync_registry).
+        # The accessor's hash must NOT read through that swappable
+        # field; otherwise set/dict membership corrupts mid-life.
+        # This is a tripwire: if a future refactor reintroduces a
+        # derived field (e.g. base_uri via the resolver) into
+        # __hash__, this test fails.
+
+        accessor = SchemaAccessor.from_schema({"a": 1})
+
+        h_before = hash(accessor)
+        bucket = {accessor}
+
+        # Force a registry swap by handing the path resolver a fresh
+        # registry instance. `_sync_registry` will rebind
+        # `_path_resolver.resolver`, but `_path_resolver` itself stays
+        # the same instance.
+        path_resolver = accessor._path_resolver
+        retriever = SchemaRetriever(default_handlers, DRAFT202012)
+        new_registry: Registry = Registry(retrieve=retriever)  # type: ignore
+        new_registry = new_registry.with_resource(
+            "", DRAFT202012.create_resource({"a": 1})
+        )
+        changed = path_resolver._sync_registry(new_registry)
+
+        assert changed, "registry swap precondition not met"
+        assert hash(accessor) == h_before
+        assert accessor in bucket
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jsonschema-path-0.4.6/tests/unit/test_referencing_compat.py 
new/jsonschema-path-0.5.0/tests/unit/test_referencing_compat.py
--- old/jsonschema-path-0.4.6/tests/unit/test_referencing_compat.py     
1970-01-01 01:00:00.000000000 +0100
+++ new/jsonschema-path-0.5.0/tests/unit/test_referencing_compat.py     
2026-05-19 22:44:09.000000000 +0200
@@ -0,0 +1,135 @@
+"""Tests for the referencing compatibility shim.
+
+The shim is the single firewall against ``referencing`` private-API
+drift. These tests pin the behavior we depend on so that a future
+``referencing`` release that changes it produces a meaningful failure
+here rather than mysterious cache bugs elsewhere.
+"""
+
+from unittest.mock import patch
+
+import attrs
+import pytest
+from referencing import Registry
+from referencing._core import Resolved
+from referencing._core import Resolver
+from referencing.jsonschema import DRAFT202012
+
+from jsonschema_path._referencing_compat import assert_referencing_layout
+from jsonschema_path._referencing_compat import base_uri_of
+from jsonschema_path._referencing_compat import rebind_registry
+from jsonschema_path._referencing_compat import rebind_resolved
+from jsonschema_path._referencing_compat import registry_of
+
+
+def _build_resolver(resources):
+    registry = Registry()
+    for uri, doc in resources.items():
+        registry = registry.with_resource(
+            uri, DRAFT202012.create_resource(doc)
+        )
+    base_uri = next(iter(resources)) if resources else ""
+    return registry.resolver(base_uri=base_uri), registry
+
+
+class TestRebindRegistry:
+    def test_swaps_registry_only(self):
+        resolver, reg1 = _build_resolver({"a://1": {"v": 1}})
+        reg2 = reg1.with_resource(
+            "a://2", DRAFT202012.create_resource({"v": 2})
+        )
+
+        new_resolver = rebind_registry(resolver, reg2)
+
+        assert new_resolver._registry is reg2
+        # base_uri and dynamic-scope previous chain must be untouched
+        # — that is the *whole point* of using attrs.evolve rather than
+        # Resolver._evolve (which would push to _previous on a
+        # differing base_uri).
+        assert new_resolver._base_uri == resolver._base_uri
+        assert new_resolver._previous == resolver._previous
+
+    def test_rebound_resolver_sees_new_resources(self):
+        resolver, reg1 = _build_resolver({"a://1": {"v": 1}})
+        reg2 = reg1.with_resource(
+            "a://2", DRAFT202012.create_resource({"v": 2})
+        )
+
+        rebound = rebind_registry(resolver, reg2)
+        looked_up = rebound.lookup("a://2")
+
+        assert looked_up.contents == {"v": 2}
+
+    def test_original_resolver_unchanged(self):
+        resolver, reg1 = _build_resolver({"a://1": {"v": 1}})
+        reg2 = reg1.with_resource(
+            "a://2", DRAFT202012.create_resource({"v": 2})
+        )
+
+        _ = rebind_registry(resolver, reg2)
+
+        # attrs.evolve must not mutate the source.
+        assert resolver._registry is reg1
+
+
+class TestRebindResolved:
+    def test_preserves_contents_swaps_registry(self):
+        resolver, reg1 = _build_resolver({"a://1": {"v": 1}})
+        resolved = resolver.lookup("a://1")
+        reg2 = reg1.with_resource(
+            "a://2", DRAFT202012.create_resource({"v": 2})
+        )
+
+        rebound = rebind_resolved(resolved, reg2)
+
+        assert rebound.contents is resolved.contents
+        assert rebound.resolver._registry is reg2
+        # Source must not be mutated.
+        assert resolved.resolver._registry is reg1
+
+
+class TestRegistryOf:
+    def test_for_resolver(self):
+        resolver, reg = _build_resolver({"a://1": {"v": 1}})
+
+        assert registry_of(resolver) is reg
+
+    def test_for_resolved(self):
+        resolver, reg = _build_resolver({"a://1": {"v": 1}})
+        resolved = resolver.lookup("a://1")
+
+        assert isinstance(resolved, Resolved)
+        assert registry_of(resolved) is resolved.resolver._registry
+
+
+class TestBaseUriOf:
+    def test_for_resolver(self):
+        resolver, _ = _build_resolver({"a://1": {"v": 1}})
+
+        assert base_uri_of(resolver) == "a://1"
+
+    def test_for_resolved(self):
+        resolver, _ = _build_resolver({"a://1": {"v": 1}})
+        resolved = resolver.lookup("a://1")
+
+        assert base_uri_of(resolved) == resolved.resolver._base_uri
+
+
+class TestAssertReferencingLayout:
+    def test_passes_with_current_referencing(self):
+        # Must not raise under the supported referencing range.
+        assert_referencing_layout()
+
+    def test_raises_when_required_field_missing(self):
+        # Simulate a future referencing that renames _registry. The
+        # shim must surface a clear ImportError rather than letting
+        # silent wrong-results escape into production.
+        fake_fields = tuple(
+            f for f in attrs.fields(Resolver) if f.name != "_registry"
+        )
+        with patch(
+            "jsonschema_path._referencing_compat.attrs.fields",
+            return_value=fake_fields,
+        ):
+            with pytest.raises(ImportError, match="referencing"):
+                assert_referencing_layout()

Reply via email to