Skip to content

Commit 7ad0104

Browse files
committed
Draft 7, passing all but 4 (refRemote) tests.
1 parent 5a4f899 commit 7ad0104

File tree

3 files changed

+110
-51
lines changed

3 files changed

+110
-51
lines changed

referencing/_core.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from __future__ import annotations
22

3-
from collections.abc import Sequence
3+
from collections.abc import Iterable, Sequence
44
from urllib.parse import unquote, urldefrag, urljoin
55

66
from attrs import evolve, field
77
from pyrsistent import m, plist, s
88
from pyrsistent.typing import PList, PMap, PSet
99

1010
from referencing._attrs import define, frozen
11-
from referencing.jsonschema import DynamicAnchor, id_of
1211
from referencing.typing import Anchor as AnchorType, Schema, Specification
1312

1413

@@ -19,11 +18,10 @@ class UnidentifiedResource(Exception):
1918
@frozen
2019
class Anchor:
2120

22-
uri: str
2321
name: str
2422
resource: Schema
2523

26-
def resolve(self, dynamic_scope, uri) -> tuple[Schema, str]:
24+
def resolve(self, resolver, uri) -> tuple[Schema, str]:
2725
return self.resource, uri
2826

2927

@@ -34,7 +32,15 @@ class OpaqueSpecification:
3432
In particular, they have no subresources.
3533
"""
3634

37-
def subresources_of(self, resource: Schema):
35+
def id_of(self, resource):
36+
if resource is True or resource is False:
37+
return
38+
return resource.get("$id") # REMOVEME
39+
40+
def anchors_in(self, resource):
41+
return ()
42+
43+
def subresources_of(self, resource):
3844
return ()
3945

4046

@@ -45,7 +51,7 @@ class Registry:
4551
default=m(),
4652
repr=lambda value: f"({len(value)} entries)",
4753
)
48-
_uncrawled: PSet[str] = s()
54+
_uncrawled: PSet[str] = field(default=s(), repr=False)
4955
_specification: Specification = OpaqueSpecification()
5056

5157
def update(self, *registries: Registry) -> Registry:
@@ -58,7 +64,7 @@ def update(self, *registries: Registry) -> Registry:
5864
)
5965

6066
def with_resource(self, resource) -> Registry:
61-
uri = id_of(resource)
67+
uri = self._specification.id_of(resource)
6268
if uri is None:
6369
raise UnidentifiedResource(resource)
6470
return self.with_identified_resource(uri=uri, resource=resource)
@@ -77,17 +83,23 @@ def with_resources(self, pairs) -> Registry:
7783
), (uri, self._contents[uri], resource)
7884
contents = contents.set(uri, (resource, m()))
7985

80-
id = id_of(resource)
86+
id = self._specification.id_of(resource)
8187
if id is not None:
8288
contents = contents.set(id, (resource, m()))
8389

8490
uncrawled = uncrawled.add(uri)
8591
return evolve(self, contents=contents, uncrawled=uncrawled)
8692

87-
def with_anchor(self, anchor: AnchorType) -> Registry:
88-
resource, anchors = self._contents[anchor.uri]
89-
new = resource, anchors.set(anchor.name, anchor)
90-
return evolve(self, contents=self._contents.set(anchor.uri, new))
93+
def with_anchors(
94+
self,
95+
uri: str,
96+
anchors: Iterable[AnchorType],
97+
) -> Registry:
98+
assert "#" not in uri, uri
99+
resource, old = self._contents[uri]
100+
new = old.update({anchor.name: anchor for anchor in anchors})
101+
contents = self._contents.set(uri, (resource, new))
102+
return evolve(self, contents=contents)
91103

92104
def resource_at(self, uri: str) -> tuple[Schema, Registry]:
93105
at_uri = self._contents.get(uri)
@@ -108,28 +120,16 @@ def _crawl(self) -> Registry:
108120
if resource is True or resource is False:
109121
continue
110122

111-
uri = urljoin(base_uri, resource.get("$id", ""))
123+
uri = urljoin(base_uri, self._specification.id_of(resource) or "")
112124
if uri != base_uri:
113125
registry = registry.with_identified_resource(
114126
uri=uri,
115127
resource=resource,
116128
)
117129

118-
anchor = resource.get("$anchor")
119-
if anchor is not None:
120-
registry = registry.with_anchor(
121-
Anchor(uri=uri, name=anchor, resource=resource),
122-
)
130+
anchors = self._specification.anchors_in(resource)
131+
registry = registry.with_anchors(uri=uri, anchors=anchors)
123132

124-
dynamic_anchor = resource.get("$dynamicAnchor")
125-
if dynamic_anchor is not None:
126-
registry = registry.with_anchor(
127-
DynamicAnchor(
128-
uri=uri,
129-
name=dynamic_anchor,
130-
resource=resource,
131-
),
132-
)
133133
resources.extend(
134134
(uri, each)
135135
for each in self._specification.subresources_of(resource)
@@ -138,7 +138,7 @@ def _crawl(self) -> Registry:
138138
return evolve(registry, uncrawled=s())
139139

140140
def resolver(self, root, specification) -> Resolver:
141-
uri = id_of(root) or ""
141+
uri = self._specification.id_of(root) or ""
142142
registry = self.with_identified_resource(uri=uri, resource=root)
143143
registry = evolve(registry, specification=specification)
144144
return Resolver(base_uri=uri, registry=registry)
@@ -167,15 +167,12 @@ def lookup(self, ref: str) -> tuple[Schema, Resolver]:
167167
target = target[segment] # type: ignore # this can't be a bool
168168
elif fragment:
169169
anchor = registry.anchors_at(uri=uri)[fragment]
170-
target, uri = anchor.resolve(
171-
dynamic_scope=self.dynamic_scope(),
172-
uri=uri,
173-
)
170+
target, uri = anchor.resolve(resolver=self, uri=uri)
174171

175172
return target, self.evolve(base_uri=uri, registry=registry)
176173

177174
def with_root(self, root) -> Resolver:
178-
maybe_relative = id_of(root)
175+
maybe_relative = self._registry._specification.id_of(root)
179176
if maybe_relative is None:
180177
return self
181178

referencing/jsonschema.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import annotations
66

77
from referencing._attrs import frozen
8+
from referencing._core import Anchor
89
from referencing.typing import Schema
910

1011

@@ -14,6 +15,20 @@ class Draft202012:
1415
_SUBRESOURCE_ITEMS = {"allOf"}
1516
_SUBRESOURCE_VALUES = {"$defs", "properties"}
1617

18+
def id_of(self, resource):
19+
if resource is True or resource is False:
20+
return
21+
return resource.get("$id")
22+
23+
def anchors_in(self, resource):
24+
anchor = resource.get("$anchor")
25+
if anchor is not None:
26+
yield Anchor(name=anchor, resource=resource)
27+
28+
dynamic_anchor = resource.get("$dynamicAnchor")
29+
if dynamic_anchor is not None:
30+
yield DynamicAnchor(name=dynamic_anchor, resource=resource)
31+
1732
def subresources_of(self, resource):
1833
for each in self._SUBRESOURCE:
1934
if each in resource:
@@ -29,19 +44,19 @@ def subresources_of(self, resource):
2944
@frozen
3045
class DynamicAnchor:
3146

32-
uri: str
3347
name: str
3448
resource: Schema
3549

36-
def resolve(self, dynamic_scope, uri):
50+
def resolve(self, resolver, uri):
3751
last = self.resource
38-
for resource, anchors in dynamic_scope:
52+
for resource, anchors in resolver.dynamic_scope():
3953
anchor = anchors.get(self.name)
4054
if isinstance(anchor, DynamicAnchor):
4155
last = anchor.resource
4256
elif "$ref" not in resource:
4357
break
44-
return last, id_of(last) or "" # FIXME: consider when this can be None
58+
id = resolver._registry._specification.id_of(last) or "" # FIXME
59+
return last, id
4560

4661

4762
class Draft201909:
@@ -50,6 +65,16 @@ class Draft201909:
5065
_SUBRESOURCE_ITEMS = {"allOf"}
5166
_SUBRESOURCE_VALUES = {"$defs", "properties"}
5267

68+
def id_of(self, resource):
69+
if resource is True or resource is False:
70+
return
71+
return resource.get("$id")
72+
73+
def anchors_in(self, resource):
74+
anchor = resource.get("$anchor")
75+
if anchor is not None:
76+
yield Anchor(name=anchor, resource=resource)
77+
5378
def subresources_of(self, resource):
5479
for each in self._SUBRESOURCE:
5580
if each in resource:
@@ -70,7 +95,39 @@ def subresources_of(self, resource):
7095
yield items
7196

7297

73-
def id_of(resource) -> str | None:
74-
if resource is True or resource is False:
75-
return None
76-
return resource.get("$id")
98+
class Draft7:
99+
100+
_SUBRESOURCE = {"not"}
101+
_SUBRESOURCE_ITEMS = {"allOf"}
102+
_SUBRESOURCE_VALUES = {"definitions", "properties"}
103+
104+
def id_of(self, resource):
105+
if resource is True or resource is False or "$ref" in resource:
106+
return None
107+
id = resource.get("$id")
108+
if id is not None and not id.startswith("#"):
109+
return id
110+
111+
def anchors_in(self, resource):
112+
anchor = resource.get("$id", "")
113+
if anchor.startswith("#"):
114+
yield Anchor(name=anchor[1:], resource=resource)
115+
116+
def subresources_of(self, resource):
117+
for each in self._SUBRESOURCE:
118+
if each in resource:
119+
yield resource[each]
120+
for each in self._SUBRESOURCE_ITEMS:
121+
if each in resource:
122+
yield from resource[each]
123+
for each in self._SUBRESOURCE_VALUES:
124+
if each in resource:
125+
yield from resource[each].values()
126+
127+
items = resource.get("items")
128+
if items is None:
129+
return
130+
elif isinstance(items, list):
131+
yield from items
132+
else:
133+
yield items

referencing/typing.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any, Iterable, Protocol, Union
3+
from typing import TYPE_CHECKING, Any, Iterable, Protocol, Union
44

55
try:
66
from collections.abc import Mapping
@@ -9,29 +9,34 @@
99
except TypeError:
1010
from typing import Mapping
1111

12+
if TYPE_CHECKING:
13+
from referencing._core import Resolver
14+
1215

1316
ObjectSchema = Mapping[str, Any]
1417
Schema = Union[bool, ObjectSchema]
1518

1619

1720
class Anchor(Protocol):
18-
@property
19-
def uri(self) -> str:
20-
...
21-
2221
@property
2322
def name(self) -> str:
2423
...
2524

26-
def resolve(
27-
self,
28-
dynamic_scope: Iterable[tuple[Schema, Anchor]],
29-
uri: str,
30-
) -> tuple[Schema, str]:
25+
def resolve(self, resolver: Resolver, uri: str) -> tuple[Schema, str]:
3126
pass
3227

3328

3429
class Specification(Protocol):
30+
def id_of(self, resource: Schema) -> str | None:
31+
"""
32+
The URI ID of the given resource.
33+
"""
34+
35+
def anchors_in(self, resource: ObjectSchema) -> Iterable[Anchor]:
36+
"""
37+
All (non-recursively nested) anchors inside the given resource.
38+
"""
39+
3540
def subresources_of(self, resource: ObjectSchema) -> Iterable[Schema]:
3641
"""
3742
All (non-recursively nested) resources inside the given resource.

0 commit comments

Comments
 (0)