chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Cross-specification, implementation-agnostic JSON referencing.
|
||||
"""
|
||||
|
||||
from referencing._core import Anchor, Registry, Resource, Specification
|
||||
|
||||
__all__ = ["Anchor", "Registry", "Resource", "Specification"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NoReturn, TypeVar
|
||||
|
||||
from attrs import define as _define, frozen as _frozen
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def define(cls: type[_T]) -> type[_T]: # pragma: no cover
|
||||
cls.__init_subclass__ = _do_not_subclass
|
||||
return _define(cls)
|
||||
|
||||
|
||||
def frozen(cls: type[_T]) -> type[_T]:
|
||||
cls.__init_subclass__ = _do_not_subclass
|
||||
return _frozen(cls)
|
||||
|
||||
|
||||
class UnsupportedSubclassing(Exception):
|
||||
def __str__(self):
|
||||
return (
|
||||
"Subclassing is not part of referencing's public API. "
|
||||
"If no other suitable API exists for what you're trying to do, "
|
||||
"feel free to file an issue asking for one."
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _do_not_subclass() -> NoReturn: # pragma: no cover
|
||||
raise UnsupportedSubclassing()
|
||||
@@ -0,0 +1,21 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from attr import attrib, field
|
||||
|
||||
class UnsupportedSubclassing(Exception): ...
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
def __dataclass_transform__(
|
||||
*,
|
||||
frozen_default: bool = False,
|
||||
field_descriptors: tuple[type | Callable[..., Any], ...] = ...,
|
||||
) -> Callable[[_T], _T]: ...
|
||||
@__dataclass_transform__(field_descriptors=(attrib, field))
|
||||
def define(cls: type[_T]) -> type[_T]: ...
|
||||
@__dataclass_transform__(
|
||||
frozen_default=True,
|
||||
field_descriptors=(attrib, field),
|
||||
)
|
||||
def frozen(cls: type[_T]) -> type[_T]: ...
|
||||
@@ -0,0 +1,739 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar, Generic, Protocol
|
||||
from urllib.parse import unquote, urldefrag, urljoin
|
||||
|
||||
from attrs import evolve, field
|
||||
from rpds import HashTrieMap, HashTrieSet, List
|
||||
|
||||
try:
|
||||
from typing_extensions import TypeVar
|
||||
except ImportError: # pragma: no cover
|
||||
from typing import TypeVar
|
||||
|
||||
from referencing import exceptions
|
||||
from referencing._attrs import frozen
|
||||
from referencing.typing import URI, Anchor as AnchorType, D, Mapping, Retrieve
|
||||
|
||||
EMPTY_UNCRAWLED: HashTrieSet[URI] = HashTrieSet()
|
||||
EMPTY_PREVIOUS_RESOLVERS: List[URI] = List()
|
||||
|
||||
|
||||
class _Unset(Enum):
|
||||
"""
|
||||
What sillyness...
|
||||
"""
|
||||
|
||||
SENTINEL = 1
|
||||
|
||||
|
||||
_UNSET = _Unset.SENTINEL
|
||||
|
||||
|
||||
class _MaybeInSubresource(Protocol[D]):
|
||||
def __call__(
|
||||
self,
|
||||
segments: Sequence[int | str],
|
||||
resolver: Resolver[D],
|
||||
subresource: Resource[D],
|
||||
) -> Resolver[D]: ...
|
||||
|
||||
|
||||
def _detect_or_error(contents: D) -> Specification[D]:
|
||||
if not isinstance(contents, Mapping):
|
||||
raise exceptions.CannotDetermineSpecification(contents)
|
||||
|
||||
jsonschema_dialect_id = contents.get("$schema") # type: ignore[reportUnknownMemberType]
|
||||
if not isinstance(jsonschema_dialect_id, str):
|
||||
raise exceptions.CannotDetermineSpecification(contents)
|
||||
|
||||
from referencing.jsonschema import specification_with
|
||||
|
||||
return specification_with(jsonschema_dialect_id)
|
||||
|
||||
|
||||
def _detect_or_default(
|
||||
default: Specification[D],
|
||||
) -> Callable[[D], Specification[D]]:
|
||||
def _detect(contents: D) -> Specification[D]:
|
||||
if not isinstance(contents, Mapping):
|
||||
return default
|
||||
|
||||
jsonschema_dialect_id = contents.get("$schema") # type: ignore[reportUnknownMemberType]
|
||||
if jsonschema_dialect_id is None:
|
||||
return default
|
||||
|
||||
from referencing.jsonschema import specification_with
|
||||
|
||||
return specification_with(
|
||||
jsonschema_dialect_id, # type: ignore[reportUnknownArgumentType]
|
||||
default=default,
|
||||
)
|
||||
|
||||
return _detect
|
||||
|
||||
|
||||
class _SpecificationDetector:
|
||||
def __get__(
|
||||
self,
|
||||
instance: Specification[D] | None,
|
||||
cls: type[Specification[D]],
|
||||
) -> Callable[[D], Specification[D]]:
|
||||
if instance is None:
|
||||
return _detect_or_error
|
||||
else:
|
||||
return _detect_or_default(instance)
|
||||
|
||||
|
||||
@frozen
|
||||
class Specification(Generic[D]):
|
||||
"""
|
||||
A specification which defines referencing behavior.
|
||||
|
||||
The various methods of a `Specification` allow for varying referencing
|
||||
behavior across JSON Schema specification versions, etc.
|
||||
"""
|
||||
|
||||
#: A short human-readable name for the specification, used for debugging.
|
||||
name: str
|
||||
|
||||
#: Find the ID of a given document.
|
||||
id_of: Callable[[D], URI | None]
|
||||
|
||||
#: Retrieve the subresources of the given document (without traversing into
|
||||
#: the subresources themselves).
|
||||
subresources_of: Callable[[D], Iterable[D]]
|
||||
|
||||
#: While resolving a JSON pointer, conditionally enter a subresource
|
||||
#: (if e.g. we have just entered a keyword whose value is a subresource)
|
||||
maybe_in_subresource: _MaybeInSubresource[D]
|
||||
|
||||
#: Retrieve the anchors contained in the given document.
|
||||
_anchors_in: Callable[
|
||||
[Specification[D], D],
|
||||
Iterable[AnchorType[D]],
|
||||
] = field(alias="anchors_in")
|
||||
|
||||
#: An opaque specification where resources have no subresources
|
||||
#: nor internal identifiers.
|
||||
OPAQUE: ClassVar[Specification[Any]]
|
||||
|
||||
#: Attempt to discern which specification applies to the given contents.
|
||||
#:
|
||||
#: May be called either as an instance method or as a class method, with
|
||||
#: slightly different behavior in the following case:
|
||||
#:
|
||||
#: Recall that not all contents contains enough internal information about
|
||||
#: which specification it is written for -- the JSON Schema ``{}``,
|
||||
#: for instance, is valid under many different dialects and may be
|
||||
#: interpreted as any one of them.
|
||||
#:
|
||||
#: When this method is used as an instance method (i.e. called on a
|
||||
#: specific specification), that specification is used as the default
|
||||
#: if the given contents are unidentifiable.
|
||||
#:
|
||||
#: On the other hand when called as a class method, an error is raised.
|
||||
#:
|
||||
#: To reiterate, ``DRAFT202012.detect({})`` will return ``DRAFT202012``
|
||||
#: whereas the class method ``Specification.detect({})`` will raise an
|
||||
#: error.
|
||||
#:
|
||||
#: (Note that of course ``DRAFT202012.detect(...)`` may return some other
|
||||
#: specification when given a schema which *does* identify as being for
|
||||
#: another version).
|
||||
#:
|
||||
#: Raises:
|
||||
#:
|
||||
#: `CannotDetermineSpecification`
|
||||
#:
|
||||
#: if the given contents don't have any discernible
|
||||
#: information which could be used to guess which
|
||||
#: specification they identify as
|
||||
detect = _SpecificationDetector()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Specification name={self.name!r}>"
|
||||
|
||||
def anchors_in(self, contents: D):
|
||||
"""
|
||||
Retrieve the anchors contained in the given document.
|
||||
"""
|
||||
return self._anchors_in(self, contents)
|
||||
|
||||
def create_resource(self, contents: D) -> Resource[D]:
|
||||
"""
|
||||
Create a resource which is interpreted using this specification.
|
||||
"""
|
||||
return Resource(contents=contents, specification=self)
|
||||
|
||||
|
||||
Specification.OPAQUE = Specification(
|
||||
name="opaque",
|
||||
id_of=lambda contents: None,
|
||||
subresources_of=lambda contents: [],
|
||||
anchors_in=lambda specification, contents: [],
|
||||
maybe_in_subresource=lambda segments, resolver, subresource: resolver,
|
||||
)
|
||||
|
||||
|
||||
@frozen
|
||||
class Resource(Generic[D]):
|
||||
r"""
|
||||
A document (deserialized JSON) with a concrete interpretation under a spec.
|
||||
|
||||
In other words, a Python object, along with an instance of `Specification`
|
||||
which describes how the document interacts with referencing -- both
|
||||
internally (how it refers to other `Resource`\ s) and externally (how it
|
||||
should be identified such that it is referenceable by other documents).
|
||||
"""
|
||||
|
||||
contents: D
|
||||
_specification: Specification[D] = field(alias="specification")
|
||||
|
||||
@classmethod
|
||||
def from_contents(
|
||||
cls,
|
||||
contents: D,
|
||||
default_specification: (
|
||||
type[Specification[D]] | Specification[D]
|
||||
) = Specification,
|
||||
) -> Resource[D]:
|
||||
"""
|
||||
Create a resource guessing which specification applies to the contents.
|
||||
|
||||
Raises:
|
||||
|
||||
`CannotDetermineSpecification`
|
||||
|
||||
if the given contents don't have any discernible
|
||||
information which could be used to guess which
|
||||
specification they identify as
|
||||
|
||||
"""
|
||||
specification = default_specification.detect(contents)
|
||||
return specification.create_resource(contents=contents)
|
||||
|
||||
@classmethod
|
||||
def opaque(cls, contents: D) -> Resource[D]:
|
||||
"""
|
||||
Create an opaque `Resource` -- i.e. one with opaque specification.
|
||||
|
||||
See `Specification.OPAQUE` for details.
|
||||
"""
|
||||
return Specification.OPAQUE.create_resource(contents=contents)
|
||||
|
||||
def id(self) -> URI | None:
|
||||
"""
|
||||
Retrieve this resource's (specification-specific) identifier.
|
||||
"""
|
||||
id = self._specification.id_of(self.contents)
|
||||
if id is None:
|
||||
return
|
||||
return id.rstrip("#")
|
||||
|
||||
def subresources(self) -> Iterable[Resource[D]]:
|
||||
"""
|
||||
Retrieve this resource's subresources.
|
||||
"""
|
||||
return (
|
||||
Resource.from_contents(
|
||||
each,
|
||||
default_specification=self._specification,
|
||||
)
|
||||
for each in self._specification.subresources_of(self.contents)
|
||||
)
|
||||
|
||||
def anchors(self) -> Iterable[AnchorType[D]]:
|
||||
"""
|
||||
Retrieve this resource's (specification-specific) identifier.
|
||||
"""
|
||||
return self._specification.anchors_in(self.contents)
|
||||
|
||||
def pointer(self, pointer: str, resolver: Resolver[D]) -> Resolved[D]:
|
||||
"""
|
||||
Resolve the given JSON pointer.
|
||||
|
||||
Raises:
|
||||
|
||||
`exceptions.PointerToNowhere`
|
||||
|
||||
if the pointer points to a location not present in the document
|
||||
|
||||
"""
|
||||
if not pointer:
|
||||
return Resolved(contents=self.contents, resolver=resolver)
|
||||
|
||||
contents = self.contents
|
||||
segments: list[int | str] = []
|
||||
for segment in unquote(pointer[1:]).split("/"):
|
||||
if isinstance(contents, Sequence):
|
||||
segment = int(segment)
|
||||
else:
|
||||
segment = segment.replace("~1", "/").replace("~0", "~")
|
||||
try:
|
||||
contents = contents[segment] # type: ignore[reportUnknownArgumentType]
|
||||
except LookupError as lookup_error:
|
||||
error = exceptions.PointerToNowhere(ref=pointer, resource=self)
|
||||
raise error from lookup_error
|
||||
|
||||
segments.append(segment)
|
||||
last = resolver
|
||||
resolver = self._specification.maybe_in_subresource(
|
||||
segments=segments,
|
||||
resolver=resolver,
|
||||
subresource=self._specification.create_resource(contents),
|
||||
)
|
||||
if resolver is not last:
|
||||
segments = []
|
||||
return Resolved(contents=contents, resolver=resolver) # type: ignore[reportUnknownArgumentType]
|
||||
|
||||
|
||||
def _fail_to_retrieve(uri: URI):
|
||||
raise exceptions.NoSuchResource(ref=uri)
|
||||
|
||||
|
||||
@frozen
|
||||
class Registry(Mapping[URI, Resource[D]]):
|
||||
r"""
|
||||
A registry of `Resource`\ s, each identified by their canonical URIs.
|
||||
|
||||
Registries store a collection of in-memory resources, and optionally
|
||||
enable additional resources which may be stored elsewhere (e.g. in a
|
||||
database, a separate set of files, over the network, etc.).
|
||||
|
||||
They also lazily walk their known resources, looking for subresources
|
||||
within them. In other words, subresources contained within any added
|
||||
resources will be retrievable via their own IDs (though this discovery of
|
||||
subresources will be delayed until necessary).
|
||||
|
||||
Registries are immutable, and their methods return new instances of the
|
||||
registry with the additional resources added to them.
|
||||
|
||||
The ``retrieve`` argument can be used to configure retrieval of resources
|
||||
dynamically, either over the network, from a database, or the like.
|
||||
Pass it a callable which will be called if any URI not present in the
|
||||
registry is accessed. It must either return a `Resource` or else raise a
|
||||
`NoSuchResource` exception indicating that the resource does not exist
|
||||
even according to the retrieval logic.
|
||||
"""
|
||||
|
||||
_resources: HashTrieMap[URI, Resource[D]] = field(
|
||||
default=HashTrieMap(),
|
||||
converter=HashTrieMap.convert, # type: ignore[reportGeneralTypeIssues]
|
||||
alias="resources",
|
||||
)
|
||||
_anchors: HashTrieMap[tuple[URI, str], AnchorType[D]] = HashTrieMap()
|
||||
_uncrawled: HashTrieSet[URI] = EMPTY_UNCRAWLED
|
||||
_retrieve: Retrieve[D] = field(default=_fail_to_retrieve, alias="retrieve")
|
||||
|
||||
def __getitem__(self, uri: URI) -> Resource[D]:
|
||||
"""
|
||||
Return the (already crawled) `Resource` identified by the given URI.
|
||||
"""
|
||||
try:
|
||||
return self._resources[uri.rstrip("#")]
|
||||
except KeyError:
|
||||
raise exceptions.NoSuchResource(ref=uri) from None
|
||||
|
||||
def __iter__(self) -> Iterator[URI]:
|
||||
"""
|
||||
Iterate over all crawled URIs in the registry.
|
||||
"""
|
||||
return iter(self._resources)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Count the total number of fully crawled resources in this registry.
|
||||
"""
|
||||
return len(self._resources)
|
||||
|
||||
def __rmatmul__(
|
||||
self,
|
||||
new: Resource[D] | Iterable[Resource[D]],
|
||||
) -> Registry[D]:
|
||||
"""
|
||||
Create a new registry with resource(s) added using their internal IDs.
|
||||
|
||||
Resources must have a internal IDs (e.g. the :kw:`$id` keyword in
|
||||
modern JSON Schema versions), otherwise an error will be raised.
|
||||
|
||||
Both a single resource as well as an iterable of resources works, i.e.:
|
||||
|
||||
* ``resource @ registry`` or
|
||||
|
||||
* ``[iterable, of, multiple, resources] @ registry``
|
||||
|
||||
which -- again, assuming the resources have internal IDs -- is
|
||||
equivalent to calling `Registry.with_resources` as such:
|
||||
|
||||
.. code:: python
|
||||
|
||||
registry.with_resources(
|
||||
(resource.id(), resource) for resource in new_resources
|
||||
)
|
||||
|
||||
Raises:
|
||||
|
||||
`NoInternalID`
|
||||
|
||||
if the resource(s) in fact do not have IDs
|
||||
|
||||
"""
|
||||
if isinstance(new, Resource):
|
||||
new = (new,)
|
||||
|
||||
resources = self._resources
|
||||
uncrawled = self._uncrawled
|
||||
for resource in new:
|
||||
id = resource.id()
|
||||
if id is None:
|
||||
raise exceptions.NoInternalID(resource=resource)
|
||||
uncrawled = uncrawled.insert(id)
|
||||
resources = resources.insert(id, resource)
|
||||
return evolve(self, resources=resources, uncrawled=uncrawled)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
size = len(self)
|
||||
pluralized = "resource" if size == 1 else "resources"
|
||||
if self._uncrawled:
|
||||
uncrawled = len(self._uncrawled)
|
||||
if uncrawled == size:
|
||||
summary = f"uncrawled {pluralized}"
|
||||
else:
|
||||
summary = f"{pluralized}, {uncrawled} uncrawled"
|
||||
else:
|
||||
summary = f"{pluralized}"
|
||||
return f"<Registry ({size} {summary})>"
|
||||
|
||||
def get_or_retrieve(self, uri: URI) -> Retrieved[D, Resource[D]]:
|
||||
"""
|
||||
Get a resource from the registry, crawling or retrieving if necessary.
|
||||
|
||||
May involve crawling to find the given URI if it is not already known,
|
||||
so the returned object is a `Retrieved` object which contains both the
|
||||
resource value as well as the registry which ultimately contained it.
|
||||
"""
|
||||
resource = self._resources.get(uri)
|
||||
if resource is not None:
|
||||
return Retrieved(registry=self, value=resource)
|
||||
|
||||
registry = self.crawl()
|
||||
resource = registry._resources.get(uri)
|
||||
if resource is not None:
|
||||
return Retrieved(registry=registry, value=resource)
|
||||
|
||||
try:
|
||||
resource = registry._retrieve(uri)
|
||||
except (
|
||||
exceptions.CannotDetermineSpecification,
|
||||
exceptions.NoSuchResource,
|
||||
):
|
||||
raise
|
||||
except Exception as error:
|
||||
raise exceptions.Unretrievable(ref=uri) from error
|
||||
else:
|
||||
registry = registry.with_resource(uri, resource)
|
||||
return Retrieved(registry=registry, value=resource)
|
||||
|
||||
def remove(self, uri: URI):
|
||||
"""
|
||||
Return a registry with the resource identified by a given URI removed.
|
||||
"""
|
||||
if uri not in self._resources:
|
||||
raise exceptions.NoSuchResource(ref=uri)
|
||||
|
||||
return evolve(
|
||||
self,
|
||||
resources=self._resources.remove(uri),
|
||||
uncrawled=self._uncrawled.discard(uri),
|
||||
anchors=HashTrieMap(
|
||||
(k, v) for k, v in self._anchors.items() if k[0] != uri
|
||||
),
|
||||
)
|
||||
|
||||
def anchor(self, uri: URI, name: str):
|
||||
"""
|
||||
Retrieve a given anchor from a resource which must already be crawled.
|
||||
"""
|
||||
value = self._anchors.get((uri, name))
|
||||
if value is not None:
|
||||
return Retrieved(value=value, registry=self)
|
||||
|
||||
registry = self.crawl()
|
||||
value = registry._anchors.get((uri, name))
|
||||
if value is not None:
|
||||
return Retrieved(value=value, registry=registry)
|
||||
|
||||
resource = self[uri]
|
||||
canonical_uri = resource.id()
|
||||
if canonical_uri is not None:
|
||||
value = registry._anchors.get((canonical_uri, name))
|
||||
if value is not None:
|
||||
return Retrieved(value=value, registry=registry)
|
||||
|
||||
if "/" in name:
|
||||
raise exceptions.InvalidAnchor(
|
||||
ref=uri,
|
||||
resource=resource,
|
||||
anchor=name,
|
||||
)
|
||||
raise exceptions.NoSuchAnchor(ref=uri, resource=resource, anchor=name)
|
||||
|
||||
def contents(self, uri: URI) -> D:
|
||||
"""
|
||||
Retrieve the (already crawled) contents identified by the given URI.
|
||||
"""
|
||||
return self[uri].contents
|
||||
|
||||
def crawl(self) -> Registry[D]:
|
||||
"""
|
||||
Crawl all added resources, discovering subresources.
|
||||
"""
|
||||
resources = self._resources
|
||||
anchors = self._anchors
|
||||
uncrawled = [(uri, resources[uri]) for uri in self._uncrawled]
|
||||
while uncrawled:
|
||||
uri, resource = uncrawled.pop()
|
||||
|
||||
id = resource.id()
|
||||
if id is not None:
|
||||
uri = urljoin(uri, id)
|
||||
resources = resources.insert(uri, resource)
|
||||
for each in resource.anchors():
|
||||
anchors = anchors.insert((uri, each.name), each)
|
||||
uncrawled.extend((uri, each) for each in resource.subresources())
|
||||
return evolve(
|
||||
self,
|
||||
resources=resources,
|
||||
anchors=anchors,
|
||||
uncrawled=EMPTY_UNCRAWLED,
|
||||
)
|
||||
|
||||
def with_resource(self, uri: URI, resource: Resource[D]):
|
||||
"""
|
||||
Add the given `Resource` to the registry, without crawling it.
|
||||
"""
|
||||
return self.with_resources([(uri, resource)])
|
||||
|
||||
def with_resources(
|
||||
self,
|
||||
pairs: Iterable[tuple[URI, Resource[D]]],
|
||||
) -> Registry[D]:
|
||||
r"""
|
||||
Add the given `Resource`\ s to the registry, without crawling them.
|
||||
"""
|
||||
resources = self._resources
|
||||
uncrawled = self._uncrawled
|
||||
for uri, resource in pairs:
|
||||
# Empty fragment URIs are equivalent to URIs without the fragment.
|
||||
# TODO: Is this true for non JSON Schema resources? Probably not.
|
||||
uri = uri.rstrip("#")
|
||||
uncrawled = uncrawled.insert(uri)
|
||||
resources = resources.insert(uri, resource)
|
||||
return evolve(self, resources=resources, uncrawled=uncrawled)
|
||||
|
||||
def with_contents(
|
||||
self,
|
||||
pairs: Iterable[tuple[URI, D]],
|
||||
**kwargs: Any,
|
||||
) -> Registry[D]:
|
||||
r"""
|
||||
Add the given contents to the registry, autodetecting when necessary.
|
||||
"""
|
||||
return self.with_resources(
|
||||
(uri, Resource.from_contents(each, **kwargs))
|
||||
for uri, each in pairs
|
||||
)
|
||||
|
||||
def combine(self, *registries: Registry[D]) -> Registry[D]:
|
||||
"""
|
||||
Combine together one or more other registries, producing a unified one.
|
||||
"""
|
||||
if registries == (self,):
|
||||
return self
|
||||
resources = self._resources
|
||||
anchors = self._anchors
|
||||
uncrawled = self._uncrawled
|
||||
retrieve = self._retrieve
|
||||
for registry in registries:
|
||||
resources = resources.update(registry._resources)
|
||||
anchors = anchors.update(registry._anchors)
|
||||
uncrawled = uncrawled.update(registry._uncrawled)
|
||||
|
||||
if registry._retrieve is not _fail_to_retrieve:
|
||||
if registry._retrieve is not retrieve is not _fail_to_retrieve:
|
||||
raise ValueError( # noqa: TRY003
|
||||
"Cannot combine registries with conflicting retrieval "
|
||||
"functions.",
|
||||
)
|
||||
retrieve = registry._retrieve
|
||||
return evolve(
|
||||
self,
|
||||
anchors=anchors,
|
||||
resources=resources,
|
||||
uncrawled=uncrawled,
|
||||
retrieve=retrieve,
|
||||
)
|
||||
|
||||
def resolver(self, base_uri: URI = "") -> Resolver[D]:
|
||||
"""
|
||||
Return a `Resolver` which resolves references against this registry.
|
||||
"""
|
||||
return Resolver(base_uri=base_uri, registry=self)
|
||||
|
||||
def resolver_with_root(self, resource: Resource[D]) -> Resolver[D]:
|
||||
"""
|
||||
Return a `Resolver` with a specific root resource.
|
||||
"""
|
||||
uri = resource.id() or ""
|
||||
return Resolver(
|
||||
base_uri=uri,
|
||||
registry=self.with_resource(uri, resource),
|
||||
)
|
||||
|
||||
|
||||
#: An anchor or resource.
|
||||
AnchorOrResource = TypeVar(
|
||||
"AnchorOrResource",
|
||||
AnchorType[Any],
|
||||
Resource[Any],
|
||||
default=Resource[Any],
|
||||
)
|
||||
|
||||
|
||||
@frozen
|
||||
class Retrieved(Generic[D, AnchorOrResource]):
|
||||
"""
|
||||
A value retrieved from a `Registry`.
|
||||
"""
|
||||
|
||||
value: AnchorOrResource
|
||||
registry: Registry[D]
|
||||
|
||||
|
||||
@frozen
|
||||
class Resolved(Generic[D]):
|
||||
"""
|
||||
A reference resolved to its contents by a `Resolver`.
|
||||
"""
|
||||
|
||||
contents: D
|
||||
resolver: Resolver[D]
|
||||
|
||||
|
||||
@frozen
|
||||
class Resolver(Generic[D]):
|
||||
"""
|
||||
A reference resolver.
|
||||
|
||||
Resolvers help resolve references (including relative ones) by
|
||||
pairing a fixed base URI with a `Registry`.
|
||||
|
||||
This object, under normal circumstances, is expected to be used by
|
||||
*implementers of libraries* built on top of `referencing` (e.g. JSON Schema
|
||||
implementations or other libraries resolving JSON references),
|
||||
not directly by end-users populating registries or while writing
|
||||
schemas or other resources.
|
||||
|
||||
References are resolved against the base URI, and the combined URI
|
||||
is then looked up within the registry.
|
||||
|
||||
The process of resolving a reference may itself involve calculating
|
||||
a *new* base URI for future reference resolution (e.g. if an
|
||||
intermediate resource sets a new base URI), or may involve encountering
|
||||
additional subresources and adding them to a new registry.
|
||||
"""
|
||||
|
||||
_base_uri: URI = field(alias="base_uri")
|
||||
_registry: Registry[D] = field(alias="registry")
|
||||
_previous: List[URI] = field(default=List(), repr=False, alias="previous")
|
||||
|
||||
def lookup(self, ref: URI) -> Resolved[D]:
|
||||
"""
|
||||
Resolve the given reference to the resource it points to.
|
||||
|
||||
Raises:
|
||||
|
||||
`exceptions.Unresolvable`
|
||||
|
||||
or a subclass thereof (see below) if the reference isn't
|
||||
resolvable
|
||||
|
||||
`exceptions.NoSuchAnchor`
|
||||
|
||||
if the reference is to a URI where a resource exists but
|
||||
contains a plain name fragment which does not exist within
|
||||
the resource
|
||||
|
||||
`exceptions.PointerToNowhere`
|
||||
|
||||
if the reference is to a URI where a resource exists but
|
||||
contains a JSON pointer to a location within the resource
|
||||
that does not exist
|
||||
|
||||
"""
|
||||
if ref.startswith("#"):
|
||||
uri, fragment = self._base_uri, ref[1:]
|
||||
else:
|
||||
uri, fragment = urldefrag(urljoin(self._base_uri, ref))
|
||||
try:
|
||||
retrieved = self._registry.get_or_retrieve(uri)
|
||||
except exceptions.NoSuchResource:
|
||||
raise exceptions.Unresolvable(ref=ref) from None
|
||||
except exceptions.Unretrievable as error:
|
||||
raise exceptions.Unresolvable(ref=ref) from error
|
||||
|
||||
if fragment.startswith("/"):
|
||||
resolver = self._evolve(registry=retrieved.registry, base_uri=uri)
|
||||
return retrieved.value.pointer(pointer=fragment, resolver=resolver)
|
||||
|
||||
if fragment:
|
||||
retrieved = retrieved.registry.anchor(uri, fragment)
|
||||
resolver = self._evolve(registry=retrieved.registry, base_uri=uri)
|
||||
return retrieved.value.resolve(resolver=resolver)
|
||||
|
||||
resolver = self._evolve(registry=retrieved.registry, base_uri=uri)
|
||||
return Resolved(contents=retrieved.value.contents, resolver=resolver)
|
||||
|
||||
def in_subresource(self, subresource: Resource[D]) -> Resolver[D]:
|
||||
"""
|
||||
Create a resolver for a subresource (which may have a new base URI).
|
||||
"""
|
||||
id = subresource.id()
|
||||
if id is None:
|
||||
return self
|
||||
return evolve(self, base_uri=urljoin(self._base_uri, id))
|
||||
|
||||
def dynamic_scope(self) -> Iterable[tuple[URI, Registry[D]]]:
|
||||
"""
|
||||
In specs with such a notion, return the URIs in the dynamic scope.
|
||||
"""
|
||||
for uri in self._previous:
|
||||
yield uri, self._registry
|
||||
|
||||
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):
|
||||
previous = previous.push_front(self._base_uri)
|
||||
return evolve(self, base_uri=base_uri, previous=previous, **kwargs)
|
||||
|
||||
|
||||
@frozen
|
||||
class Anchor(Generic[D]):
|
||||
"""
|
||||
A simple anchor in a `Resource`.
|
||||
"""
|
||||
|
||||
name: str
|
||||
resource: Resource[D]
|
||||
|
||||
def resolve(self, resolver: Resolver[D]):
|
||||
"""
|
||||
Return the resource for this anchor.
|
||||
"""
|
||||
return Resolved(contents=self.resource.contents, resolver=resolver)
|
||||
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Errors, oh no!
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import attrs
|
||||
|
||||
from referencing._attrs import frozen
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from referencing import Resource
|
||||
from referencing.typing import URI
|
||||
|
||||
|
||||
@frozen
|
||||
class NoSuchResource(KeyError):
|
||||
"""
|
||||
The given URI is not present in a registry.
|
||||
|
||||
Unlike most exceptions, this class *is* intended to be publicly
|
||||
instantiable and *is* part of the public API of the package.
|
||||
"""
|
||||
|
||||
ref: URI
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return NotImplemented
|
||||
return attrs.astuple(self) == attrs.astuple(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(attrs.astuple(self))
|
||||
|
||||
|
||||
@frozen
|
||||
class NoInternalID(Exception):
|
||||
"""
|
||||
A resource has no internal ID, but one is needed.
|
||||
|
||||
E.g. in modern JSON Schema drafts, this is the :kw:`$id` keyword.
|
||||
|
||||
One might be needed if a resource was to-be added to a registry but no
|
||||
other URI is available, and the resource doesn't declare its canonical URI.
|
||||
"""
|
||||
|
||||
resource: Resource[Any]
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return NotImplemented
|
||||
return attrs.astuple(self) == attrs.astuple(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(attrs.astuple(self))
|
||||
|
||||
|
||||
@frozen
|
||||
class Unretrievable(KeyError):
|
||||
"""
|
||||
The given URI is not present in a registry, and retrieving it failed.
|
||||
"""
|
||||
|
||||
ref: URI
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return NotImplemented
|
||||
return attrs.astuple(self) == attrs.astuple(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(attrs.astuple(self))
|
||||
|
||||
|
||||
@frozen
|
||||
class CannotDetermineSpecification(Exception):
|
||||
"""
|
||||
Attempting to detect the appropriate `Specification` failed.
|
||||
|
||||
This happens if no discernible information is found in the contents of the
|
||||
new resource which would help identify it.
|
||||
"""
|
||||
|
||||
contents: Any
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return NotImplemented
|
||||
return attrs.astuple(self) == attrs.astuple(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(attrs.astuple(self))
|
||||
|
||||
|
||||
@attrs.frozen # Because here we allow subclassing below.
|
||||
class Unresolvable(Exception):
|
||||
"""
|
||||
A reference was unresolvable.
|
||||
"""
|
||||
|
||||
ref: URI
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return NotImplemented
|
||||
return attrs.astuple(self) == attrs.astuple(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(attrs.astuple(self))
|
||||
|
||||
|
||||
@frozen
|
||||
class PointerToNowhere(Unresolvable):
|
||||
"""
|
||||
A JSON Pointer leads to a part of a document that does not exist.
|
||||
"""
|
||||
|
||||
resource: Resource[Any]
|
||||
|
||||
def __str__(self) -> str:
|
||||
msg = f"{self.ref!r} does not exist within {self.resource.contents!r}"
|
||||
if self.ref == "/":
|
||||
msg += (
|
||||
". The pointer '/' is a valid JSON Pointer but it points to "
|
||||
"an empty string property ''. If you intended to point "
|
||||
"to the entire resource, you should use '#'."
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
@frozen
|
||||
class NoSuchAnchor(Unresolvable):
|
||||
"""
|
||||
An anchor does not exist within a particular resource.
|
||||
"""
|
||||
|
||||
resource: Resource[Any]
|
||||
anchor: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"{self.anchor!r} does not exist within {self.resource.contents!r}"
|
||||
)
|
||||
|
||||
|
||||
@frozen
|
||||
class InvalidAnchor(Unresolvable):
|
||||
"""
|
||||
An anchor which could never exist in a resource was dereferenced.
|
||||
|
||||
It is somehow syntactically invalid.
|
||||
"""
|
||||
|
||||
resource: Resource[Any]
|
||||
anchor: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"'#{self.anchor}' is not a valid anchor, neither as a "
|
||||
"plain name anchor nor as a JSON Pointer. You may have intended "
|
||||
f"to use '#/{self.anchor}', as the slash is required *before each "
|
||||
"segment* of a JSON pointer."
|
||||
)
|
||||
@@ -0,0 +1,642 @@
|
||||
"""
|
||||
Referencing implementations for JSON Schema specs (historic & current).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Sequence, Set
|
||||
from typing import Any
|
||||
|
||||
from referencing import Anchor, Registry, Resource, Specification, exceptions
|
||||
from referencing._attrs import frozen
|
||||
from referencing._core import (
|
||||
_UNSET, # type: ignore[reportPrivateUsage]
|
||||
Resolved as _Resolved,
|
||||
Resolver as _Resolver,
|
||||
_Unset, # type: ignore[reportPrivateUsage]
|
||||
)
|
||||
from referencing.typing import URI, Anchor as AnchorType, Mapping
|
||||
|
||||
#: A JSON Schema which is a JSON object
|
||||
ObjectSchema = Mapping[str, Any]
|
||||
|
||||
#: A JSON Schema of any kind
|
||||
Schema = bool | ObjectSchema
|
||||
|
||||
#: A Resource whose contents are JSON Schemas
|
||||
SchemaResource = Resource[Schema]
|
||||
|
||||
#: A JSON Schema Registry
|
||||
SchemaRegistry = Registry[Schema]
|
||||
|
||||
#: The empty JSON Schema Registry
|
||||
EMPTY_REGISTRY: SchemaRegistry = Registry()
|
||||
|
||||
|
||||
@frozen
|
||||
class UnknownDialect(Exception):
|
||||
"""
|
||||
A dialect identifier was found for a dialect unknown by this library.
|
||||
|
||||
If it's a custom ("unofficial") dialect, be sure you've registered it.
|
||||
"""
|
||||
|
||||
uri: URI
|
||||
|
||||
|
||||
def _dollar_id(contents: Schema) -> URI | None:
|
||||
if isinstance(contents, bool):
|
||||
return
|
||||
return contents.get("$id")
|
||||
|
||||
|
||||
def _legacy_dollar_id(contents: Schema) -> URI | None:
|
||||
if isinstance(contents, bool) or "$ref" in contents:
|
||||
return
|
||||
id = contents.get("$id")
|
||||
if id is not None and not id.startswith("#"):
|
||||
return id
|
||||
|
||||
|
||||
def _legacy_id(contents: ObjectSchema) -> URI | None:
|
||||
if "$ref" in contents:
|
||||
return
|
||||
id = contents.get("id")
|
||||
if id is not None and not id.startswith("#"):
|
||||
return id
|
||||
|
||||
|
||||
def _anchor(
|
||||
specification: Specification[Schema],
|
||||
contents: Schema,
|
||||
) -> Iterable[AnchorType[Schema]]:
|
||||
if isinstance(contents, bool):
|
||||
return
|
||||
anchor = contents.get("$anchor")
|
||||
if anchor is not None:
|
||||
yield Anchor(
|
||||
name=anchor,
|
||||
resource=specification.create_resource(contents),
|
||||
)
|
||||
|
||||
dynamic_anchor = contents.get("$dynamicAnchor")
|
||||
if dynamic_anchor is not None:
|
||||
yield DynamicAnchor(
|
||||
name=dynamic_anchor,
|
||||
resource=specification.create_resource(contents),
|
||||
)
|
||||
|
||||
|
||||
def _anchor_2019(
|
||||
specification: Specification[Schema],
|
||||
contents: Schema,
|
||||
) -> Iterable[Anchor[Schema]]:
|
||||
if isinstance(contents, bool):
|
||||
return []
|
||||
anchor = contents.get("$anchor")
|
||||
if anchor is None:
|
||||
return []
|
||||
return [
|
||||
Anchor(
|
||||
name=anchor,
|
||||
resource=specification.create_resource(contents),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _legacy_anchor_in_dollar_id(
|
||||
specification: Specification[Schema],
|
||||
contents: Schema,
|
||||
) -> Iterable[Anchor[Schema]]:
|
||||
if isinstance(contents, bool):
|
||||
return []
|
||||
id = contents.get("$id", "")
|
||||
if not id.startswith("#"):
|
||||
return []
|
||||
return [
|
||||
Anchor(
|
||||
name=id[1:],
|
||||
resource=specification.create_resource(contents),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _legacy_anchor_in_id(
|
||||
specification: Specification[ObjectSchema],
|
||||
contents: ObjectSchema,
|
||||
) -> Iterable[Anchor[ObjectSchema]]:
|
||||
id = contents.get("id", "")
|
||||
if not id.startswith("#"):
|
||||
return []
|
||||
return [
|
||||
Anchor(
|
||||
name=id[1:],
|
||||
resource=specification.create_resource(contents),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _subresources_of(
|
||||
in_value: Set[str] = frozenset(),
|
||||
in_subvalues: Set[str] = frozenset(),
|
||||
in_subarray: Set[str] = frozenset(),
|
||||
):
|
||||
"""
|
||||
Create a callable returning JSON Schema specification-style subschemas.
|
||||
|
||||
Relies on specifying the set of keywords containing subschemas in their
|
||||
values, in a subobject's values, or in a subarray.
|
||||
"""
|
||||
|
||||
def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
|
||||
if isinstance(contents, bool):
|
||||
return
|
||||
for each in in_value:
|
||||
if each in contents:
|
||||
yield contents[each]
|
||||
for each in in_subarray:
|
||||
if each in contents:
|
||||
yield from contents[each]
|
||||
for each in in_subvalues:
|
||||
if each in contents:
|
||||
yield from contents[each].values()
|
||||
|
||||
return subresources_of
|
||||
|
||||
|
||||
def _subresources_of_with_crazy_items(
|
||||
in_value: Set[str] = frozenset(),
|
||||
in_subvalues: Set[str] = frozenset(),
|
||||
in_subarray: Set[str] = frozenset(),
|
||||
):
|
||||
"""
|
||||
Specifically handle older drafts where there are some funky keywords.
|
||||
"""
|
||||
|
||||
def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
|
||||
if isinstance(contents, bool):
|
||||
return
|
||||
for each in in_value:
|
||||
if each in contents:
|
||||
yield contents[each]
|
||||
for each in in_subarray:
|
||||
if each in contents:
|
||||
yield from contents[each]
|
||||
for each in in_subvalues:
|
||||
if each in contents:
|
||||
yield from contents[each].values()
|
||||
|
||||
items = contents.get("items")
|
||||
if items is not None:
|
||||
if isinstance(items, Sequence):
|
||||
yield from items
|
||||
else:
|
||||
yield items
|
||||
|
||||
return subresources_of
|
||||
|
||||
|
||||
def _subresources_of_with_crazy_items_dependencies(
|
||||
in_value: Set[str] = frozenset(),
|
||||
in_subvalues: Set[str] = frozenset(),
|
||||
in_subarray: Set[str] = frozenset(),
|
||||
):
|
||||
"""
|
||||
Specifically handle older drafts where there are some funky keywords.
|
||||
"""
|
||||
|
||||
def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
|
||||
if isinstance(contents, bool):
|
||||
return
|
||||
for each in in_value:
|
||||
if each in contents:
|
||||
yield contents[each]
|
||||
for each in in_subarray:
|
||||
if each in contents:
|
||||
yield from contents[each]
|
||||
for each in in_subvalues:
|
||||
if each in contents:
|
||||
yield from contents[each].values()
|
||||
|
||||
items = contents.get("items")
|
||||
if items is not None:
|
||||
if isinstance(items, Sequence):
|
||||
yield from items
|
||||
else:
|
||||
yield items
|
||||
dependencies = contents.get("dependencies")
|
||||
if dependencies is not None:
|
||||
values = iter(dependencies.values())
|
||||
value = next(values, None)
|
||||
if isinstance(value, Mapping):
|
||||
yield value
|
||||
yield from values
|
||||
|
||||
return subresources_of
|
||||
|
||||
|
||||
def _subresources_of_with_crazy_aP_items_dependencies(
|
||||
in_value: Set[str] = frozenset(),
|
||||
in_subvalues: Set[str] = frozenset(),
|
||||
in_subarray: Set[str] = frozenset(),
|
||||
):
|
||||
"""
|
||||
Specifically handle even older drafts where there are some funky keywords.
|
||||
"""
|
||||
|
||||
def subresources_of(contents: ObjectSchema) -> Iterable[ObjectSchema]:
|
||||
for each in in_value:
|
||||
if each in contents:
|
||||
yield contents[each]
|
||||
for each in in_subarray:
|
||||
if each in contents:
|
||||
yield from contents[each]
|
||||
for each in in_subvalues:
|
||||
if each in contents:
|
||||
yield from contents[each].values()
|
||||
|
||||
items = contents.get("items")
|
||||
if items is not None:
|
||||
if isinstance(items, Sequence):
|
||||
yield from items
|
||||
else:
|
||||
yield items
|
||||
dependencies = contents.get("dependencies")
|
||||
if dependencies is not None:
|
||||
values = iter(dependencies.values())
|
||||
value = next(values, None)
|
||||
if isinstance(value, Mapping):
|
||||
yield value
|
||||
yield from values
|
||||
|
||||
for each in "additionalItems", "additionalProperties":
|
||||
value = contents.get(each)
|
||||
if isinstance(value, Mapping):
|
||||
yield value
|
||||
|
||||
return subresources_of
|
||||
|
||||
|
||||
def _maybe_in_subresource(
|
||||
in_value: Set[str] = frozenset(),
|
||||
in_subvalues: Set[str] = frozenset(),
|
||||
in_subarray: Set[str] = frozenset(),
|
||||
):
|
||||
in_child = in_subvalues | in_subarray
|
||||
|
||||
def maybe_in_subresource(
|
||||
segments: Sequence[int | str],
|
||||
resolver: _Resolver[Any],
|
||||
subresource: Resource[Any],
|
||||
) -> _Resolver[Any]:
|
||||
_segments = iter(segments)
|
||||
for segment in _segments:
|
||||
if segment not in in_value and (
|
||||
segment not in in_child or next(_segments, None) is None
|
||||
):
|
||||
return resolver
|
||||
return resolver.in_subresource(subresource)
|
||||
|
||||
return maybe_in_subresource
|
||||
|
||||
|
||||
def _maybe_in_subresource_crazy_items(
|
||||
in_value: Set[str] = frozenset(),
|
||||
in_subvalues: Set[str] = frozenset(),
|
||||
in_subarray: Set[str] = frozenset(),
|
||||
):
|
||||
in_child = in_subvalues | in_subarray
|
||||
|
||||
def maybe_in_subresource(
|
||||
segments: Sequence[int | str],
|
||||
resolver: _Resolver[Any],
|
||||
subresource: Resource[Any],
|
||||
) -> _Resolver[Any]:
|
||||
_segments = iter(segments)
|
||||
for segment in _segments:
|
||||
if segment == "items" and isinstance(
|
||||
subresource.contents,
|
||||
Mapping,
|
||||
):
|
||||
return resolver.in_subresource(subresource)
|
||||
if segment not in in_value and (
|
||||
segment not in in_child or next(_segments, None) is None
|
||||
):
|
||||
return resolver
|
||||
return resolver.in_subresource(subresource)
|
||||
|
||||
return maybe_in_subresource
|
||||
|
||||
|
||||
def _maybe_in_subresource_crazy_items_dependencies(
|
||||
in_value: Set[str] = frozenset(),
|
||||
in_subvalues: Set[str] = frozenset(),
|
||||
in_subarray: Set[str] = frozenset(),
|
||||
):
|
||||
in_child = in_subvalues | in_subarray
|
||||
|
||||
def maybe_in_subresource(
|
||||
segments: Sequence[int | str],
|
||||
resolver: _Resolver[Any],
|
||||
subresource: Resource[Any],
|
||||
) -> _Resolver[Any]:
|
||||
_segments = iter(segments)
|
||||
for segment in _segments:
|
||||
if segment in {"items", "dependencies"} and isinstance(
|
||||
subresource.contents,
|
||||
Mapping,
|
||||
):
|
||||
return resolver.in_subresource(subresource)
|
||||
if segment not in in_value and (
|
||||
segment not in in_child or next(_segments, None) is None
|
||||
):
|
||||
return resolver
|
||||
return resolver.in_subresource(subresource)
|
||||
|
||||
return maybe_in_subresource
|
||||
|
||||
|
||||
#: JSON Schema draft 2020-12
|
||||
DRAFT202012 = Specification(
|
||||
name="draft2020-12",
|
||||
id_of=_dollar_id,
|
||||
subresources_of=_subresources_of(
|
||||
in_value={
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"contentSchema",
|
||||
"else",
|
||||
"if",
|
||||
"items",
|
||||
"not",
|
||||
"propertyNames",
|
||||
"then",
|
||||
"unevaluatedItems",
|
||||
"unevaluatedProperties",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"},
|
||||
in_subvalues={
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
},
|
||||
),
|
||||
anchors_in=_anchor,
|
||||
maybe_in_subresource=_maybe_in_subresource(
|
||||
in_value={
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"contentSchema",
|
||||
"else",
|
||||
"if",
|
||||
"items",
|
||||
"not",
|
||||
"propertyNames",
|
||||
"then",
|
||||
"unevaluatedItems",
|
||||
"unevaluatedProperties",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"},
|
||||
in_subvalues={
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
},
|
||||
),
|
||||
)
|
||||
#: JSON Schema draft 2019-09
|
||||
DRAFT201909 = Specification(
|
||||
name="draft2019-09",
|
||||
id_of=_dollar_id,
|
||||
subresources_of=_subresources_of_with_crazy_items(
|
||||
in_value={
|
||||
"additionalItems",
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"contentSchema",
|
||||
"else",
|
||||
"if",
|
||||
"not",
|
||||
"propertyNames",
|
||||
"then",
|
||||
"unevaluatedItems",
|
||||
"unevaluatedProperties",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
},
|
||||
),
|
||||
anchors_in=_anchor_2019,
|
||||
maybe_in_subresource=_maybe_in_subresource_crazy_items(
|
||||
in_value={
|
||||
"additionalItems",
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"contentSchema",
|
||||
"else",
|
||||
"if",
|
||||
"not",
|
||||
"propertyNames",
|
||||
"then",
|
||||
"unevaluatedItems",
|
||||
"unevaluatedProperties",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
},
|
||||
),
|
||||
)
|
||||
#: JSON Schema draft 7
|
||||
DRAFT7 = Specification(
|
||||
name="draft-07",
|
||||
id_of=_legacy_dollar_id,
|
||||
subresources_of=_subresources_of_with_crazy_items_dependencies(
|
||||
in_value={
|
||||
"additionalItems",
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"else",
|
||||
"if",
|
||||
"not",
|
||||
"propertyNames",
|
||||
"then",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
anchors_in=_legacy_anchor_in_dollar_id,
|
||||
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
||||
in_value={
|
||||
"additionalItems",
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"else",
|
||||
"if",
|
||||
"not",
|
||||
"propertyNames",
|
||||
"then",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
)
|
||||
#: JSON Schema draft 6
|
||||
DRAFT6 = Specification(
|
||||
name="draft-06",
|
||||
id_of=_legacy_dollar_id,
|
||||
subresources_of=_subresources_of_with_crazy_items_dependencies(
|
||||
in_value={
|
||||
"additionalItems",
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"not",
|
||||
"propertyNames",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
anchors_in=_legacy_anchor_in_dollar_id,
|
||||
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
||||
in_value={
|
||||
"additionalItems",
|
||||
"additionalProperties",
|
||||
"contains",
|
||||
"not",
|
||||
"propertyNames",
|
||||
},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
)
|
||||
#: JSON Schema draft 4
|
||||
DRAFT4 = Specification(
|
||||
name="draft-04",
|
||||
id_of=_legacy_id,
|
||||
subresources_of=_subresources_of_with_crazy_aP_items_dependencies(
|
||||
in_value={"not"},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
anchors_in=_legacy_anchor_in_id,
|
||||
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
||||
in_value={"additionalItems", "additionalProperties", "not"},
|
||||
in_subarray={"allOf", "anyOf", "oneOf"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
)
|
||||
#: JSON Schema draft 3
|
||||
DRAFT3 = Specification(
|
||||
name="draft-03",
|
||||
id_of=_legacy_id,
|
||||
subresources_of=_subresources_of_with_crazy_aP_items_dependencies(
|
||||
in_subarray={"extends"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
anchors_in=_legacy_anchor_in_id,
|
||||
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
||||
in_value={"additionalItems", "additionalProperties"},
|
||||
in_subarray={"extends"},
|
||||
in_subvalues={"definitions", "patternProperties", "properties"},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_SPECIFICATIONS: Registry[Specification[Schema]] = Registry(
|
||||
{
|
||||
dialect_id: Resource.opaque(specification)
|
||||
for dialect_id, specification in [
|
||||
("https://json-schema.org/draft/2020-12/schema", DRAFT202012),
|
||||
("https://json-schema.org/draft/2019-09/schema", DRAFT201909),
|
||||
("http://json-schema.org/draft-07/schema", DRAFT7),
|
||||
("http://json-schema.org/draft-06/schema", DRAFT6),
|
||||
("http://json-schema.org/draft-04/schema", DRAFT4),
|
||||
("http://json-schema.org/draft-03/schema", DRAFT3),
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def specification_with(
|
||||
dialect_id: URI,
|
||||
default: Specification[Any] | _Unset = _UNSET,
|
||||
) -> Specification[Any]:
|
||||
"""
|
||||
Retrieve the `Specification` with the given dialect identifier.
|
||||
|
||||
Raises:
|
||||
|
||||
`UnknownDialect`
|
||||
|
||||
if the given ``dialect_id`` isn't known
|
||||
|
||||
"""
|
||||
resource = _SPECIFICATIONS.get(dialect_id.rstrip("#"))
|
||||
if resource is not None:
|
||||
return resource.contents
|
||||
if default is _UNSET:
|
||||
raise UnknownDialect(dialect_id)
|
||||
return default
|
||||
|
||||
|
||||
@frozen
|
||||
class DynamicAnchor:
|
||||
"""
|
||||
Dynamic anchors, introduced in draft 2020.
|
||||
"""
|
||||
|
||||
name: str
|
||||
resource: SchemaResource
|
||||
|
||||
def resolve(self, resolver: _Resolver[Schema]) -> _Resolved[Schema]:
|
||||
"""
|
||||
Resolve this anchor dynamically.
|
||||
"""
|
||||
last = self.resource
|
||||
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
|
||||
return _Resolved(
|
||||
contents=last.contents,
|
||||
resolver=resolver.in_subresource(last),
|
||||
)
|
||||
|
||||
|
||||
def lookup_recursive_ref(resolver: _Resolver[Schema]) -> _Resolved[Schema]:
|
||||
"""
|
||||
Recursive references (via recursive anchors), present only in draft 2019.
|
||||
|
||||
As per the 2019 specification (§ 8.2.4.2.1), only the ``#`` recursive
|
||||
reference is supported (and is therefore assumed to be the relevant
|
||||
reference).
|
||||
"""
|
||||
resolved = resolver.lookup("#")
|
||||
if isinstance(resolved.contents, Mapping) and resolved.contents.get(
|
||||
"$recursiveAnchor",
|
||||
):
|
||||
for uri, _ in resolver.dynamic_scope():
|
||||
next_resolved = resolver.lookup(uri)
|
||||
if not isinstance(
|
||||
next_resolved.contents,
|
||||
Mapping,
|
||||
) or not next_resolved.contents.get("$recursiveAnchor"):
|
||||
break
|
||||
resolved = next_resolved
|
||||
return resolved
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Helpers related to (dynamic) resource retrieval.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING
|
||||
import json
|
||||
|
||||
try:
|
||||
from typing_extensions import TypeVar
|
||||
except ImportError: # pragma: no cover
|
||||
from typing import TypeVar
|
||||
|
||||
from referencing import Resource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from referencing.typing import URI, D, Retrieve
|
||||
|
||||
#: A serialized document (e.g. a JSON string)
|
||||
_T = TypeVar("_T", default=str)
|
||||
|
||||
|
||||
def to_cached_resource(
|
||||
cache: Callable[[Retrieve[D]], Retrieve[D]] | None = None,
|
||||
loads: Callable[[_T], D] = json.loads,
|
||||
from_contents: Callable[[D], Resource[D]] = Resource.from_contents,
|
||||
) -> Callable[[Callable[[URI], _T]], Retrieve[D]]:
|
||||
"""
|
||||
Create a retriever which caches its return values from a simpler callable.
|
||||
|
||||
Takes a function which returns things like serialized JSON (strings) and
|
||||
returns something suitable for passing to `Registry` as a retrieve
|
||||
function.
|
||||
|
||||
This decorator both reduces a small bit of boilerplate for a common case
|
||||
(deserializing JSON from strings and creating `Resource` objects from the
|
||||
result) as well as makes the probable need for caching a bit easier.
|
||||
Retrievers which otherwise do expensive operations (like hitting the
|
||||
network) might otherwise be called repeatedly.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. testcode::
|
||||
|
||||
from referencing import Registry
|
||||
from referencing.typing import URI
|
||||
import referencing.retrieval
|
||||
|
||||
|
||||
@referencing.retrieval.to_cached_resource()
|
||||
def retrieve(uri: URI):
|
||||
print(f"Retrieved {uri}")
|
||||
|
||||
# Normally, go get some expensive JSON from the network, a file ...
|
||||
return '''
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"foo": "bar"
|
||||
}
|
||||
'''
|
||||
|
||||
one = Registry(retrieve=retrieve).get_or_retrieve("urn:example:foo")
|
||||
print(one.value.contents["foo"])
|
||||
|
||||
# Retrieving the same URI again reuses the same value (and thus doesn't
|
||||
# print another retrieval message here)
|
||||
two = Registry(retrieve=retrieve).get_or_retrieve("urn:example:foo")
|
||||
print(two.value.contents["foo"])
|
||||
|
||||
.. testoutput::
|
||||
|
||||
Retrieved urn:example:foo
|
||||
bar
|
||||
bar
|
||||
|
||||
"""
|
||||
if cache is None:
|
||||
cache = lru_cache(maxsize=None)
|
||||
|
||||
def decorator(retrieve: Callable[[URI], _T]):
|
||||
@cache
|
||||
def cached_retrieve(uri: URI):
|
||||
response = retrieve(uri)
|
||||
contents = loads(response)
|
||||
return from_contents(contents)
|
||||
|
||||
return cached_retrieve
|
||||
|
||||
return decorator
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
from referencing import Resource, exceptions
|
||||
|
||||
|
||||
def pairs(choices):
|
||||
return itertools.combinations(choices, 2)
|
||||
|
||||
|
||||
TRUE = Resource.opaque(True)
|
||||
|
||||
|
||||
thunks = (
|
||||
lambda: exceptions.CannotDetermineSpecification(TRUE),
|
||||
lambda: exceptions.NoSuchResource("urn:example:foo"),
|
||||
lambda: exceptions.NoInternalID(TRUE),
|
||||
lambda: exceptions.InvalidAnchor(resource=TRUE, anchor="foo", ref="a#b"),
|
||||
lambda: exceptions.NoSuchAnchor(resource=TRUE, anchor="foo", ref="a#b"),
|
||||
lambda: exceptions.PointerToNowhere(resource=TRUE, ref="urn:example:foo"),
|
||||
lambda: exceptions.Unresolvable("urn:example:foo"),
|
||||
lambda: exceptions.Unretrievable("urn:example:foo"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("one, two", pairs(each() for each in thunks))
|
||||
def test_eq_incompatible_types(one, two):
|
||||
assert one != two
|
||||
|
||||
|
||||
@pytest.mark.parametrize("thunk", thunks)
|
||||
def test_hash(thunk):
|
||||
assert thunk() in {thunk()}
|
||||
@@ -0,0 +1,382 @@
|
||||
import pytest
|
||||
|
||||
from referencing import Registry, Resource, Specification
|
||||
import referencing.jsonschema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri, expected",
|
||||
[
|
||||
(
|
||||
"https://json-schema.org/draft/2020-12/schema",
|
||||
referencing.jsonschema.DRAFT202012,
|
||||
),
|
||||
(
|
||||
"https://json-schema.org/draft/2019-09/schema",
|
||||
referencing.jsonschema.DRAFT201909,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-07/schema#",
|
||||
referencing.jsonschema.DRAFT7,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-06/schema#",
|
||||
referencing.jsonschema.DRAFT6,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-04/schema#",
|
||||
referencing.jsonschema.DRAFT4,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-03/schema#",
|
||||
referencing.jsonschema.DRAFT3,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_schemas_with_explicit_schema_keywords_are_detected(uri, expected):
|
||||
"""
|
||||
The $schema keyword in JSON Schema is a dialect identifier.
|
||||
"""
|
||||
contents = {"$schema": uri}
|
||||
resource = Resource.from_contents(contents)
|
||||
assert resource == Resource(contents=contents, specification=expected)
|
||||
|
||||
|
||||
def test_unknown_dialect():
|
||||
dialect_id = "http://example.com/unknown-json-schema-dialect-id"
|
||||
with pytest.raises(referencing.jsonschema.UnknownDialect) as excinfo:
|
||||
Resource.from_contents({"$schema": dialect_id})
|
||||
assert excinfo.value.uri == dialect_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"id, specification",
|
||||
[
|
||||
("$id", referencing.jsonschema.DRAFT202012),
|
||||
("$id", referencing.jsonschema.DRAFT201909),
|
||||
("$id", referencing.jsonschema.DRAFT7),
|
||||
("$id", referencing.jsonschema.DRAFT6),
|
||||
("id", referencing.jsonschema.DRAFT4),
|
||||
("id", referencing.jsonschema.DRAFT3),
|
||||
],
|
||||
)
|
||||
def test_id_of_mapping(id, specification):
|
||||
uri = "http://example.com/some-schema"
|
||||
assert specification.id_of({id: uri}) == uri
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"specification",
|
||||
[
|
||||
referencing.jsonschema.DRAFT202012,
|
||||
referencing.jsonschema.DRAFT201909,
|
||||
referencing.jsonschema.DRAFT7,
|
||||
referencing.jsonschema.DRAFT6,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
def test_id_of_bool(specification, value):
|
||||
assert specification.id_of(value) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"specification",
|
||||
[
|
||||
referencing.jsonschema.DRAFT202012,
|
||||
referencing.jsonschema.DRAFT201909,
|
||||
referencing.jsonschema.DRAFT7,
|
||||
referencing.jsonschema.DRAFT6,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
def test_anchors_in_bool(specification, value):
|
||||
assert list(specification.anchors_in(value)) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"specification",
|
||||
[
|
||||
referencing.jsonschema.DRAFT202012,
|
||||
referencing.jsonschema.DRAFT201909,
|
||||
referencing.jsonschema.DRAFT7,
|
||||
referencing.jsonschema.DRAFT6,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
def test_subresources_of_bool(specification, value):
|
||||
assert list(specification.subresources_of(value)) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri, expected",
|
||||
[
|
||||
(
|
||||
"https://json-schema.org/draft/2020-12/schema",
|
||||
referencing.jsonschema.DRAFT202012,
|
||||
),
|
||||
(
|
||||
"https://json-schema.org/draft/2019-09/schema",
|
||||
referencing.jsonschema.DRAFT201909,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-07/schema#",
|
||||
referencing.jsonschema.DRAFT7,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-06/schema#",
|
||||
referencing.jsonschema.DRAFT6,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-04/schema#",
|
||||
referencing.jsonschema.DRAFT4,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-03/schema#",
|
||||
referencing.jsonschema.DRAFT3,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_specification_with(uri, expected):
|
||||
assert referencing.jsonschema.specification_with(uri) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri, expected",
|
||||
[
|
||||
(
|
||||
"http://json-schema.org/draft-07/schema",
|
||||
referencing.jsonschema.DRAFT7,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-06/schema",
|
||||
referencing.jsonschema.DRAFT6,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-04/schema",
|
||||
referencing.jsonschema.DRAFT4,
|
||||
),
|
||||
(
|
||||
"http://json-schema.org/draft-03/schema",
|
||||
referencing.jsonschema.DRAFT3,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_specification_with_no_empty_fragment(uri, expected):
|
||||
assert referencing.jsonschema.specification_with(uri) == expected
|
||||
|
||||
|
||||
def test_specification_with_unknown_dialect():
|
||||
dialect_id = "http://example.com/unknown-json-schema-dialect-id"
|
||||
with pytest.raises(referencing.jsonschema.UnknownDialect) as excinfo:
|
||||
referencing.jsonschema.specification_with(dialect_id)
|
||||
assert excinfo.value.uri == dialect_id
|
||||
|
||||
|
||||
def test_specification_with_default():
|
||||
dialect_id = "http://example.com/unknown-json-schema-dialect-id"
|
||||
specification = referencing.jsonschema.specification_with(
|
||||
dialect_id,
|
||||
default=Specification.OPAQUE,
|
||||
)
|
||||
assert specification is Specification.OPAQUE
|
||||
|
||||
|
||||
# FIXME: The tests below should move to the referencing suite but I haven't yet
|
||||
# figured out how to represent dynamic (& recursive) ref lookups in it.
|
||||
def test_lookup_trivial_dynamic_ref():
|
||||
one = referencing.jsonschema.DRAFT202012.create_resource(
|
||||
{"$dynamicAnchor": "foo"},
|
||||
)
|
||||
resolver = Registry().with_resource("http://example.com", one).resolver()
|
||||
resolved = resolver.lookup("http://example.com#foo")
|
||||
assert resolved.contents == one.contents
|
||||
|
||||
|
||||
def test_multiple_lookup_trivial_dynamic_ref():
|
||||
TRUE = referencing.jsonschema.DRAFT202012.create_resource(True)
|
||||
root = referencing.jsonschema.DRAFT202012.create_resource(
|
||||
{
|
||||
"$id": "http://example.com",
|
||||
"$dynamicAnchor": "fooAnchor",
|
||||
"$defs": {
|
||||
"foo": {
|
||||
"$id": "foo",
|
||||
"$dynamicAnchor": "fooAnchor",
|
||||
"$defs": {
|
||||
"bar": True,
|
||||
"baz": {
|
||||
"$dynamicAnchor": "fooAnchor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
resolver = (
|
||||
Registry()
|
||||
.with_resources(
|
||||
[
|
||||
("http://example.com", root),
|
||||
("http://example.com/foo/", TRUE),
|
||||
("http://example.com/foo/bar", root),
|
||||
],
|
||||
)
|
||||
.resolver()
|
||||
)
|
||||
|
||||
first = resolver.lookup("http://example.com")
|
||||
second = first.resolver.lookup("foo/")
|
||||
resolver = second.resolver.lookup("bar").resolver
|
||||
fourth = resolver.lookup("#fooAnchor")
|
||||
assert fourth.contents == root.contents
|
||||
|
||||
|
||||
def test_multiple_lookup_dynamic_ref_to_nondynamic_ref():
|
||||
one = referencing.jsonschema.DRAFT202012.create_resource(
|
||||
{"$anchor": "fooAnchor"},
|
||||
)
|
||||
two = referencing.jsonschema.DRAFT202012.create_resource(
|
||||
{
|
||||
"$id": "http://example.com",
|
||||
"$dynamicAnchor": "fooAnchor",
|
||||
"$defs": {
|
||||
"foo": {
|
||||
"$id": "foo",
|
||||
"$dynamicAnchor": "fooAnchor",
|
||||
"$defs": {
|
||||
"bar": True,
|
||||
"baz": {
|
||||
"$dynamicAnchor": "fooAnchor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
resolver = (
|
||||
Registry()
|
||||
.with_resources(
|
||||
[
|
||||
("http://example.com", two),
|
||||
("http://example.com/foo/", one),
|
||||
("http://example.com/foo/bar", two),
|
||||
],
|
||||
)
|
||||
.resolver()
|
||||
)
|
||||
|
||||
first = resolver.lookup("http://example.com")
|
||||
second = first.resolver.lookup("foo/")
|
||||
resolver = second.resolver.lookup("bar").resolver
|
||||
fourth = resolver.lookup("#fooAnchor")
|
||||
assert fourth.contents == two.contents
|
||||
|
||||
|
||||
def test_lookup_trivial_recursive_ref():
|
||||
one = referencing.jsonschema.DRAFT201909.create_resource(
|
||||
{"$recursiveAnchor": True},
|
||||
)
|
||||
resolver = Registry().with_resource("http://example.com", one).resolver()
|
||||
first = resolver.lookup("http://example.com")
|
||||
resolved = referencing.jsonschema.lookup_recursive_ref(
|
||||
resolver=first.resolver,
|
||||
)
|
||||
assert resolved.contents == one.contents
|
||||
|
||||
|
||||
def test_lookup_recursive_ref_to_bool():
|
||||
TRUE = referencing.jsonschema.DRAFT201909.create_resource(True)
|
||||
registry = Registry({"http://example.com": TRUE})
|
||||
resolved = referencing.jsonschema.lookup_recursive_ref(
|
||||
resolver=registry.resolver(base_uri="http://example.com"),
|
||||
)
|
||||
assert resolved.contents == TRUE.contents
|
||||
|
||||
|
||||
def test_multiple_lookup_recursive_ref_to_bool():
|
||||
TRUE = referencing.jsonschema.DRAFT201909.create_resource(True)
|
||||
root = referencing.jsonschema.DRAFT201909.create_resource(
|
||||
{
|
||||
"$id": "http://example.com",
|
||||
"$recursiveAnchor": True,
|
||||
"$defs": {
|
||||
"foo": {
|
||||
"$id": "foo",
|
||||
"$recursiveAnchor": True,
|
||||
"$defs": {
|
||||
"bar": True,
|
||||
"baz": {
|
||||
"$recursiveAnchor": True,
|
||||
"$anchor": "fooAnchor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
resolver = (
|
||||
Registry()
|
||||
.with_resources(
|
||||
[
|
||||
("http://example.com", root),
|
||||
("http://example.com/foo/", TRUE),
|
||||
("http://example.com/foo/bar", root),
|
||||
],
|
||||
)
|
||||
.resolver()
|
||||
)
|
||||
|
||||
first = resolver.lookup("http://example.com")
|
||||
second = first.resolver.lookup("foo/")
|
||||
resolver = second.resolver.lookup("bar").resolver
|
||||
fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver)
|
||||
assert fourth.contents == root.contents
|
||||
|
||||
|
||||
def test_multiple_lookup_recursive_ref_with_nonrecursive_ref():
|
||||
one = referencing.jsonschema.DRAFT201909.create_resource(
|
||||
{"$recursiveAnchor": True},
|
||||
)
|
||||
two = referencing.jsonschema.DRAFT201909.create_resource(
|
||||
{
|
||||
"$id": "http://example.com",
|
||||
"$recursiveAnchor": True,
|
||||
"$defs": {
|
||||
"foo": {
|
||||
"$id": "foo",
|
||||
"$recursiveAnchor": True,
|
||||
"$defs": {
|
||||
"bar": True,
|
||||
"baz": {
|
||||
"$recursiveAnchor": True,
|
||||
"$anchor": "fooAnchor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
three = referencing.jsonschema.DRAFT201909.create_resource(
|
||||
{"$recursiveAnchor": False},
|
||||
)
|
||||
resolver = (
|
||||
Registry()
|
||||
.with_resources(
|
||||
[
|
||||
("http://example.com", three),
|
||||
("http://example.com/foo/", two),
|
||||
("http://example.com/foo/bar", one),
|
||||
],
|
||||
)
|
||||
.resolver()
|
||||
)
|
||||
|
||||
first = resolver.lookup("http://example.com")
|
||||
second = first.resolver.lookup("foo/")
|
||||
resolver = second.resolver.lookup("bar").resolver
|
||||
fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver)
|
||||
assert fourth.contents == two.contents
|
||||
|
||||
|
||||
def test_empty_registry():
|
||||
assert referencing.jsonschema.EMPTY_REGISTRY == Registry()
|
||||
@@ -0,0 +1,66 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from referencing import Registry
|
||||
from referencing.exceptions import Unresolvable
|
||||
import referencing.jsonschema
|
||||
|
||||
|
||||
class SuiteNotFound(Exception):
|
||||
def __str__(self): # pragma: no cover
|
||||
return (
|
||||
"Cannot find the referencing suite. "
|
||||
"Set the REFERENCING_SUITE environment variable to the path to "
|
||||
"the suite, or run the test suite from alongside a full checkout "
|
||||
"of the git repository."
|
||||
)
|
||||
|
||||
|
||||
if "REFERENCING_SUITE" in os.environ: # pragma: no cover
|
||||
SUITE = Path(os.environ["REFERENCING_SUITE"]) / "tests"
|
||||
else:
|
||||
SUITE = Path(__file__).parent.parent.parent / "suite/tests"
|
||||
if not SUITE.is_dir(): # pragma: no cover
|
||||
raise SuiteNotFound()
|
||||
DIALECT_IDS = json.loads(SUITE.joinpath("specifications.json").read_text())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_path",
|
||||
[
|
||||
pytest.param(each, id=f"{each.parent.name}-{each.stem}")
|
||||
for each in SUITE.glob("*/**/*.json")
|
||||
],
|
||||
)
|
||||
def test_referencing_suite(test_path, subtests):
|
||||
dialect_id = DIALECT_IDS[test_path.relative_to(SUITE).parts[0]]
|
||||
specification = referencing.jsonschema.specification_with(dialect_id)
|
||||
loaded = json.loads(test_path.read_text())
|
||||
registry = loaded["registry"]
|
||||
registry = Registry().with_resources(
|
||||
(uri, specification.create_resource(contents))
|
||||
for uri, contents in loaded["registry"].items()
|
||||
)
|
||||
for test in loaded["tests"]:
|
||||
with subtests.test(test=test):
|
||||
if "normalization" in test_path.stem:
|
||||
pytest.xfail("APIs need to change for proper URL support.")
|
||||
|
||||
resolver = registry.resolver(base_uri=test.get("base_uri", ""))
|
||||
|
||||
if test.get("error"):
|
||||
with pytest.raises(Unresolvable):
|
||||
resolver.lookup(test["ref"])
|
||||
else:
|
||||
resolved = resolver.lookup(test["ref"])
|
||||
assert resolved.contents == test["target"]
|
||||
|
||||
then = test.get("then")
|
||||
while then: # pragma: no cover
|
||||
with subtests.test(test=test, then=then):
|
||||
resolved = resolved.resolver.lookup(then["ref"])
|
||||
assert resolved.contents == then["target"]
|
||||
then = then.get("then")
|
||||
@@ -0,0 +1,106 @@
|
||||
from functools import lru_cache
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from referencing import Registry, Resource, exceptions
|
||||
from referencing.jsonschema import DRAFT202012
|
||||
from referencing.retrieval import to_cached_resource
|
||||
|
||||
|
||||
class TestToCachedResource:
|
||||
def test_it_caches_retrieved_resources(self):
|
||||
contents = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
||||
stack = [json.dumps(contents)]
|
||||
|
||||
@to_cached_resource()
|
||||
def retrieve(uri):
|
||||
return stack.pop()
|
||||
|
||||
registry = Registry(retrieve=retrieve)
|
||||
|
||||
expected = Resource.from_contents(contents)
|
||||
|
||||
got = registry.get_or_retrieve("urn:example:schema")
|
||||
assert got.value == expected
|
||||
|
||||
# And a second time we get the same value.
|
||||
again = registry.get_or_retrieve("urn:example:schema")
|
||||
assert again.value is got.value
|
||||
|
||||
def test_custom_loader(self):
|
||||
contents = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
||||
stack = [json.dumps(contents)[::-1]]
|
||||
|
||||
@to_cached_resource(loads=lambda s: json.loads(s[::-1]))
|
||||
def retrieve(uri):
|
||||
return stack.pop()
|
||||
|
||||
registry = Registry(retrieve=retrieve)
|
||||
|
||||
expected = Resource.from_contents(contents)
|
||||
|
||||
got = registry.get_or_retrieve("urn:example:schema")
|
||||
assert got.value == expected
|
||||
|
||||
# And a second time we get the same value.
|
||||
again = registry.get_or_retrieve("urn:example:schema")
|
||||
assert again.value is got.value
|
||||
|
||||
def test_custom_from_contents(self):
|
||||
contents = {}
|
||||
stack = [json.dumps(contents)]
|
||||
|
||||
@to_cached_resource(from_contents=DRAFT202012.create_resource)
|
||||
def retrieve(uri):
|
||||
return stack.pop()
|
||||
|
||||
registry = Registry(retrieve=retrieve)
|
||||
|
||||
expected = DRAFT202012.create_resource(contents)
|
||||
|
||||
got = registry.get_or_retrieve("urn:example:schema")
|
||||
assert got.value == expected
|
||||
|
||||
# And a second time we get the same value.
|
||||
again = registry.get_or_retrieve("urn:example:schema")
|
||||
assert again.value is got.value
|
||||
|
||||
def test_custom_cache(self):
|
||||
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
||||
mapping = {
|
||||
"urn:example:1": dict(schema, foo=1),
|
||||
"urn:example:2": dict(schema, foo=2),
|
||||
"urn:example:3": dict(schema, foo=3),
|
||||
}
|
||||
|
||||
resources = {
|
||||
uri: Resource.from_contents(contents)
|
||||
for uri, contents in mapping.items()
|
||||
}
|
||||
|
||||
@to_cached_resource(cache=lru_cache(maxsize=2))
|
||||
def retrieve(uri):
|
||||
return json.dumps(mapping.pop(uri))
|
||||
|
||||
registry = Registry(retrieve=retrieve)
|
||||
|
||||
got = registry.get_or_retrieve("urn:example:1")
|
||||
assert got.value == resources["urn:example:1"]
|
||||
assert registry.get_or_retrieve("urn:example:1").value is got.value
|
||||
assert registry.get_or_retrieve("urn:example:1").value is got.value
|
||||
|
||||
got = registry.get_or_retrieve("urn:example:2")
|
||||
assert got.value == resources["urn:example:2"]
|
||||
assert registry.get_or_retrieve("urn:example:2").value is got.value
|
||||
assert registry.get_or_retrieve("urn:example:2").value is got.value
|
||||
|
||||
# This still succeeds, but evicts the first URI
|
||||
got = registry.get_or_retrieve("urn:example:3")
|
||||
assert got.value == resources["urn:example:3"]
|
||||
assert registry.get_or_retrieve("urn:example:3").value is got.value
|
||||
assert registry.get_or_retrieve("urn:example:3").value is got.value
|
||||
|
||||
# And now this fails (as we popped the value out of `mapping`)
|
||||
with pytest.raises(exceptions.Unretrievable):
|
||||
registry.get_or_retrieve("urn:example:1")
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Type-annotation related support for the referencing library.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping as Mapping
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
try:
|
||||
from typing_extensions import TypeVar
|
||||
except ImportError: # pragma: no cover
|
||||
from typing import TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from referencing._core import Resolved, Resolver, Resource
|
||||
|
||||
#: A URI which identifies a `Resource`.
|
||||
URI = str
|
||||
|
||||
#: The type of documents within a registry.
|
||||
D = TypeVar("D", default=Any)
|
||||
|
||||
|
||||
class Retrieve(Protocol[D]):
|
||||
"""
|
||||
A retrieval callable, usable within a `Registry` for resource retrieval.
|
||||
|
||||
Does not make assumptions about where the resource might be coming from.
|
||||
"""
|
||||
|
||||
def __call__(self, uri: URI) -> Resource[D]:
|
||||
"""
|
||||
Retrieve the resource with the given URI.
|
||||
|
||||
Raise `referencing.exceptions.NoSuchResource` if you wish to indicate
|
||||
the retriever cannot lookup the given URI.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class Anchor(Protocol[D]):
|
||||
"""
|
||||
An anchor within a `Resource`.
|
||||
|
||||
Beyond "simple" anchors, some specifications like JSON Schema's 2020
|
||||
version have dynamic anchors.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""
|
||||
Return the name of this anchor.
|
||||
"""
|
||||
...
|
||||
|
||||
def resolve(self, resolver: Resolver[D]) -> Resolved[D]:
|
||||
"""
|
||||
Return the resource for this anchor.
|
||||
"""
|
||||
...
|
||||
Reference in New Issue
Block a user