Skip to content

Commit e7fc2c5

Browse files
committed
Define Registry.__rmatmul__ for adding resources.
I can't resist a bit of over-the-top fun. This is a (useful) shortcut for adding a resource (or multiple resources) to a registry where the URI you want to use is specifically the resource's internal ID. It *must* have one to use this, otherwise you get an error, which is a benefit over using `with_resource` -- which will happily not error if you pass it `resource.id()` but that returned None. That being said, Registries already have 3 methods named with_<foo> for adding resource-like things to themselves, so I didn't want to add a fourth and risk users getting even more confused as to which they should use. Using @ seems like the sort of thing that "pOwEr UsErS" will find, and more casual users who just want to figure out something working will not be distracted by.
1 parent e4b215c commit e7fc2c5

File tree

4 files changed

+101
-16
lines changed

4 files changed

+101
-16
lines changed

docs/api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ API Reference
55
:members:
66
:undoc-members:
77
:imported-members:
8-
:special-members: __iter__, __getitem__, __len__
8+
:special-members: __iter__, __getitem__, __len__, __rmatmul__
99

1010

1111
.. autoclass:: referencing._core.Resolved

referencing/_core.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,45 @@ def __len__(self) -> int:
276276
"""
277277
return len(self._resources)
278278

279+
def __rmatmul__(self, new: Resource[D] | Iterable[Resource[D]]):
280+
"""
281+
Add resource(s) to a new registry, using their internal IDs.
282+
283+
Resources must have a internal IDs (e.g. the ``$id`` keyword in modern
284+
JSON Schema versions), otherwise an error will be raised.
285+
286+
Use this via:
287+
288+
* ``resource @ registry`` or
289+
290+
* ``[iterable, of, multiple, resources] @ registry``
291+
292+
which -- again, assuming the resources have internal IDs -- is
293+
equivalent to calling `Registry.with_resources` as such:
294+
295+
.. code:: python
296+
297+
registry.with_resources(
298+
(resource.id(), resource) for resource in new_resources
299+
)
300+
"""
301+
if isinstance(new, Resource):
302+
new = (new,)
303+
304+
resources = self._resources.evolver()
305+
uncrawled = self._uncrawled.evolver()
306+
for resource in new:
307+
id = resource.id()
308+
if id is None:
309+
raise exceptions.NoInternalID(resource=resource)
310+
uncrawled.add(id)
311+
resources.set(id, resource)
312+
return evolve(
313+
self,
314+
resources=resources.persistent(),
315+
uncrawled=uncrawled.persistent(),
316+
)
317+
279318
def __repr__(self) -> str:
280319
size = len(self)
281320
pluralized = "resource" if size == 1 else "resources"

referencing/exceptions.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
"""
22
Errors, oh no!
33
"""
4-
from typing import Any
4+
from __future__ import annotations
5+
6+
from typing import TYPE_CHECKING, Any
57

68
import attrs
79

810
from referencing._attrs import frozen
911
from referencing.typing import URI
1012

13+
if TYPE_CHECKING:
14+
from referencing import Resource
15+
1116

1217
@frozen
1318
class NoSuchResource(KeyError):
@@ -23,6 +28,25 @@ def __eq__(self, other: Any) -> bool:
2328
return attrs.astuple(self) == attrs.astuple(other)
2429

2530

31+
@frozen
32+
class NoInternalID(Exception):
33+
"""
34+
A resource has no internal ID, but one is needed.
35+
36+
E.g. in modern JSON Schema drafts, this is the ``$id`` keyword.
37+
38+
One might be needed if a resource was to-be added to a registry but no
39+
other URI is available, and the resource doesn't declare its canonical URI.
40+
"""
41+
42+
resource: Resource[Any]
43+
44+
def __eq__(self, other: Any) -> bool:
45+
if self.__class__ is not other.__class__:
46+
return NotImplemented
47+
return attrs.astuple(self) == attrs.astuple(other)
48+
49+
2650
@frozen
2751
class Unretrievable(KeyError):
2852
"""
@@ -64,7 +88,7 @@ class PointerToNowhere(Unresolvable):
6488
A JSON Pointer leads to a part of a document that does not exist.
6589
"""
6690

67-
resource: Any
91+
resource: Resource[Any]
6892

6993
def __str__(self):
7094
return f"{self.ref!r} does not exist within {self.resource.contents!r}"
@@ -76,7 +100,7 @@ class NoSuchAnchor(Unresolvable):
76100
An anchor does not exist within a particular resource.
77101
"""
78102

79-
resource: Any
103+
resource: Resource[Any]
80104
anchor: str
81105

82106
def __str__(self):

referencing/tests/test_core.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ def test_with_resources(self):
5656
resource=two,
5757
)
5858

59+
def test_matmul_resource(self):
60+
uri = "urn:example:resource"
61+
resource = ID_AND_CHILDREN.create_resource({"ID": uri, "foo": 12})
62+
registry = resource @ Registry()
63+
assert registry == Registry().with_resource(uri, resource)
64+
65+
def test_matmul_many_resources(self):
66+
one_uri = "urn:example:one"
67+
one = ID_AND_CHILDREN.create_resource({"ID": one_uri, "foo": 12})
68+
69+
two_uri = "urn:example:two"
70+
two = ID_AND_CHILDREN.create_resource({"ID": two_uri, "foo": 12})
71+
72+
registry = [one, two] @ Registry()
73+
assert registry == Registry().with_resources(
74+
[(one_uri, one), (two_uri, two)],
75+
)
76+
77+
def test_matmul_resource_without_id(self):
78+
resource = Resource.opaque(contents={"foo": "bar"})
79+
with pytest.raises(exceptions.NoInternalID) as e:
80+
resource @ Registry()
81+
assert e.value == exceptions.NoInternalID(resource=resource)
82+
5983
def test_with_contents_from_json_schema(self):
6084
uri = "urn:example"
6185
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
@@ -98,7 +122,7 @@ def test_crawl_finds_a_subresource(self):
98122
root = ID_AND_CHILDREN.create_resource(
99123
{"ID": "urn:root", "children": [{"ID": child_id, "foo": 12}]},
100124
)
101-
registry = Registry().with_resource(root.id(), root)
125+
registry = root @ Registry()
102126
with pytest.raises(LookupError):
103127
registry[child_id]
104128

@@ -109,7 +133,7 @@ def test_crawl_finds_anchors_with_id(self):
109133
resource = ID_AND_CHILDREN.create_resource(
110134
{"ID": "urn:bar", "anchors": {"foo": 12}},
111135
)
112-
registry = Registry().with_resource(resource.id(), resource)
136+
registry = resource @ Registry()
113137

114138
assert registry.crawl().anchor(resource.id(), "foo").value == Anchor(
115139
name="foo",
@@ -173,7 +197,7 @@ def test_dict_conversion(self):
173197
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
174198
registry = Registry(
175199
{"http://example.com/1": one},
176-
).with_resources([("http://example.com/foo/bar", two)])
200+
).with_resource("http://example.com/foo/bar", two)
177201
assert (
178202
registry.crawl()
179203
== Registry()
@@ -637,8 +661,8 @@ def test_lookup_subresource(self):
637661
],
638662
},
639663
)
640-
resolver = Registry().with_resource(root.id(), root).resolver()
641-
resolved = resolver.lookup("http://example.com/a")
664+
registry = root @ Registry()
665+
resolved = registry.resolver().lookup("http://example.com/a")
642666
assert resolved.contents == {"ID": "http://example.com/a", "foo": 12}
643667

644668
def test_lookup_anchor_with_id(self):
@@ -648,8 +672,8 @@ def test_lookup_anchor_with_id(self):
648672
"anchors": {"foo": 12},
649673
},
650674
)
651-
resolver = Registry().with_resource(root.id(), root).resolver()
652-
resolved = resolver.lookup("http://example.com/#foo")
675+
registry = root @ Registry()
676+
resolved = registry.resolver().lookup("http://example.com/#foo")
653677
assert resolved.contents == 12
654678

655679
def test_lookup_anchor_without_id(self):
@@ -756,7 +780,7 @@ def test_in_subresource(self):
756780
],
757781
},
758782
)
759-
registry = Registry().with_resource(root.id(), root)
783+
registry = root @ Registry()
760784

761785
resolver = registry.resolver()
762786
first = resolver.lookup("http://example.com/")
@@ -783,7 +807,7 @@ def test_in_pointer_subresource(self):
783807
],
784808
},
785809
)
786-
registry = Registry().with_resource(root.id(), root)
810+
registry = root @ Registry()
787811

788812
resolver = registry.resolver()
789813
first = resolver.lookup("http://example.com/")
@@ -814,9 +838,7 @@ def test_dynamic_scope(self):
814838
"children": [{"ID": "two-child/"}],
815839
},
816840
)
817-
registry = Registry().with_resources(
818-
[(one.id(), one), (two.id(), two)],
819-
)
841+
registry = [one, two] @ Registry()
820842

821843
resolver = registry.resolver()
822844
first = resolver.lookup("http://example.com/")

0 commit comments

Comments
 (0)