Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Changelog
=========

Next
----

* Fix dynamic anchor resolution so that relative references after a dynamic
override use the URI of the matched dynamic scope.

v0.37.0
-------

Expand Down
4 changes: 3 additions & 1 deletion referencing/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,9 @@ def _evolve(self, base_uri: URI, **kwargs: Any):
Evolve, appending to the dynamic scope.
"""
previous = self._previous
if self._base_uri and (not previous or base_uri != self._base_uri):
if self._base_uri and (
not previous or next(iter(previous), None) != self._base_uri
):
previous = previous.push_front(self._base_uri)
return evolve(self, base_uri=base_uri, previous=previous, **kwargs)

Expand Down
9 changes: 9 additions & 0 deletions referencing/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from collections.abc import Iterable, Sequence, Set
from typing import Any

from attrs import evolve

from referencing import Anchor, Registry, Resource, Specification, exceptions
from referencing._attrs import frozen
from referencing._core import (
Expand Down Expand Up @@ -606,13 +608,20 @@ def resolve(self, resolver: _Resolver[Schema]) -> _Resolved[Schema]:
Resolve this anchor dynamically.
"""
last = self.resource
last_uri: URI | None = None
for uri, registry in resolver.dynamic_scope():
try:
anchor = registry.anchor(uri, self.name).value
except exceptions.NoSuchAnchor:
continue
if isinstance(anchor, DynamicAnchor):
last = anchor.resource
last_uri = uri
if last_uri is not None:
return _Resolved(
contents=last.contents,
resolver=evolve(resolver, base_uri=last_uri),
)
return _Resolved(
contents=last.contents,
resolver=resolver.in_subresource(last),
Expand Down
17 changes: 17 additions & 0 deletions referencing/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,23 @@ def retrieve(uri):
resolved = resolved.resolver.lookup("#")
assert resolved.contents == resource.contents

def test_repeated_scope_does_not_duplicate_same_caller(self):
first_uri = "http://example.com/first"
second_uri = "http://example.com/second"
registry = Registry().with_resources(
[
(first_uri, Resource.opaque({"value": 1})),
(second_uri, Resource.opaque({"value": 2})),
],
)

first = registry.resolver(base_uri=first_uri).lookup(first_uri)
second = first.resolver.lookup(second_uri)

assert [uri for uri, _ in second.resolver.dynamic_scope()] == [
first_uri,
]

# FIXME: The tests below aren't really representable in the current
# suite, though we should probably think of ways to do so.

Expand Down
156 changes: 156 additions & 0 deletions referencing/tests/test_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,162 @@ def test_multiple_lookup_trivial_dynamic_ref():
assert fourth.contents == root.contents


def test_dynamic_anchor_uses_matching_scope_uri_for_followup_lookup():
"""
A dynamic override also supplies the base for relative follow-up refs.

The two public lookup paths deliberately have different dynamic scopes:
looking up ``A#x`` from ``Cam`` starts at ``Cam``, while entering ``A``
first makes ``A`` the nearest scope for a later ``#x`` lookup.
"""
a = {
"$id": "https://example.test/A",
"$dynamicAnchor": "x",
"$defs": {
"y": {"$dynamicAnchor": "y", "const": "A.y"},
},
}
cam = {
"$id": "https://example.test/Cam",
"$defs": {"x": {"$dynamicAnchor": "x", "const": "Cam.x"}},
}
b = {
"$id": "https://example.test/B",
"$dynamicAnchor": "y",
"const": "B.y",
}
registry = Registry().with_resources(
[
(a["$id"], referencing.jsonschema.DRAFT202012.create_resource(a)),
(
cam["$id"],
referencing.jsonschema.DRAFT202012.create_resource(cam),
),
(b["$id"], referencing.jsonschema.DRAFT202012.create_resource(b)),
],
)
caller = registry.resolver(base_uri="https://example.test/Cam")

foreign = caller.lookup("https://example.test/A#x")
entered = caller.lookup("https://example.test/A")
current = entered.resolver.lookup("#x")

assert foreign.contents["const"] == "Cam.x"
assert current.contents["const"] == "Cam.x"
assert (
foreign.resolver.lookup("https://example.test/B#y").contents["const"]
== "B.y"
)
assert (
current.resolver.lookup("https://example.test/B#y").contents["const"]
== "A.y"
)
assert foreign.resolver._base_uri == "https://example.test/Cam"
assert current.resolver._base_uri == "https://example.test/Cam"
assert (
next(iter(foreign.resolver.dynamic_scope()))[0]
== "https://example.test/Cam"
)
assert [uri for uri, _ in current.resolver.dynamic_scope()] == [
"https://example.test/A",
"https://example.test/Cam",
]
for resolved, expected_scope in (
(foreign, ["https://example.test/Cam"]),
(
current,
[
"https://example.test/Cam",
"https://example.test/A",
"https://example.test/Cam",
],
),
):
third = resolved.resolver.lookup("#x")
fourth = third.resolver.lookup("#x")
assert [uri for uri, _ in fourth.resolver.dynamic_scope()] == (
expected_scope
)


def test_dynamic_anchor_nested_override_uses_camera_resource_for_pointer():
base_uri = "https://example.test/base.json"
camera_uri = "https://example.test/camera.json"
base = referencing.jsonschema.DRAFT202012.create_resource(
{
"$id": base_uri,
"$dynamicAnchor": "concreteData",
},
)
camera = referencing.jsonschema.DRAFT202012.create_resource(
{
"$id": camera_uri,
"$ref": base_uri,
"$defs": {
"override": {"$dynamicAnchor": "concreteData"},
"size": {"type": "integer"},
},
},
)
registry = Registry().with_resources(
[(base_uri, base), (camera_uri, camera)],
)

entered = registry.resolver(base_uri=camera_uri).lookup(base_uri)
override = entered.resolver.lookup("#concreteData")
size = override.resolver.lookup("#/$defs/size")

assert size.contents == {"type": "integer"}
assert override.resolver._base_uri == camera_uri


def test_dynamic_anchor_relative_id_does_not_join_against_target_uri():
base_uri = "https://example.test/base/root.json"
camera_uri = "https://example.test/camera/dir/root.json"
override_seed_uri = "https://example.test/camera/dir/override.json"
override_uri = "https://example.test/camera/override.json"
base = referencing.jsonschema.DRAFT202012.create_resource(
{"$id": base_uri, "$dynamicAnchor": "x"},
)
override = referencing.jsonschema.DRAFT202012.create_resource(
{"$id": "../override.json", "$dynamicAnchor": "x"},
)
camera = referencing.jsonschema.DRAFT202012.create_resource({})
registry = Registry().with_resources(
[
(base_uri, base),
(camera_uri, camera),
(override_seed_uri, override),
],
)

entered_override = registry.resolver(base_uri=camera_uri).lookup(
"../override.json",
)
entered_base = entered_override.resolver.lookup(base_uri)
resolved = entered_base.resolver.lookup("#x")

assert resolved.resolver._base_uri == override_uri


def test_dynamic_anchor_without_match_keeps_subresource_base():
resource = referencing.jsonschema.DRAFT202012.create_resource(
{"$id": "https://example.test/root", "$dynamicAnchor": "x"},
)
resolver = (
Registry()
.with_resource(
"https://example.test/root",
resource,
)
.resolver()
)

resolved = resolver.lookup("https://example.test/root#x")

assert resolved.resolver._base_uri == "https://example.test/root"


def test_multiple_lookup_dynamic_ref_to_nondynamic_ref():
one = referencing.jsonschema.DRAFT202012.create_resource(
{"$anchor": "fooAnchor"},
Expand Down