pax_global_header 0000666 0000000 0000000 00000000064 14524736324 0014524 g ustar 00root root 0000000 0000000 52 comment=fdc8ab0116c82622a1ed0cd642e51237788ad1eb
referencing-0.31.0/ 0000775 0000000 0000000 00000000000 14524736324 0014074 5 ustar 00root root 0000000 0000000 referencing-0.31.0/.github/ 0000775 0000000 0000000 00000000000 14524736324 0015434 5 ustar 00root root 0000000 0000000 referencing-0.31.0/.github/FUNDING.yml 0000664 0000000 0000000 00000000100 14524736324 0017240 0 ustar 00root root 0000000 0000000 # These are supported funding model platforms
github: "Julian"
referencing-0.31.0/.github/SECURITY.md 0000664 0000000 0000000 00000001170 14524736324 0017224 0 ustar 00root root 0000000 0000000 # Security Policy
## Supported Versions
In general, only the latest released `referencing` version is supported and will receive updates.
## Reporting a Vulnerability
To report a security vulnerability, please send an email to `Julian+Security` at `GrayVines.com` with subject line `SECURITY (referencing)`.
I will do my best to respond within 48 hours to acknowledge the message and discuss further steps.
If the vulnerability is accepted, an advisory will be sent out via GitHub's security advisory functionality.
For non-sensitive discussion related to this policy itself, feel free to open an issue on the issue tracker.
referencing-0.31.0/.github/dependabot.yml 0000664 0000000 0000000 00000000612 14524736324 0020263 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/docs"
schedule:
interval: "weekly"
referencing-0.31.0/.github/release.yml 0000664 0000000 0000000 00000000114 14524736324 0017573 0 ustar 00root root 0000000 0000000 changelog:
exclude:
authors:
- dependabot
- pre-commit-ci
referencing-0.31.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14524736324 0017471 5 ustar 00root root 0000000 0000000 referencing-0.31.0/.github/workflows/ci.yml 0000664 0000000 0000000 00000005421 14524736324 0020611 0 ustar 00root root 0000000 0000000 name: CI
on:
push:
pull_request:
release:
types: [published]
schedule:
# Daily at 8:33
- cron: "33 8 * * *"
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_PYTHON_VERSION_WARNING: "1"
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- uses: pre-commit/action@v3.0.0
list:
runs-on: ubuntu-latest
outputs:
noxenvs: ${{ steps.noxenvs-matrix.outputs.noxenvs }}
steps:
- uses: actions/checkout@v4
- name: Set up nox
uses: wntrblm/nox@2023.04.22
- id: noxenvs-matrix
run: |
echo >>$GITHUB_OUTPUT noxenvs=$(
nox --list-sessions --json | jq '[.[].session]'
)
ci:
needs: list
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
noxenv: ${{ fromJson(needs.list.outputs.noxenvs) }}
posargs: [""]
include:
- noxenv: tests-3.11
posargs: coverage github
steps:
- uses: actions/checkout@v4
with:
submodules: "recursive"
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libenchant-2-dev
if: runner.os == 'Linux' && startsWith(matrix.noxenv, 'docs')
- name: Install dependencies
run: brew install enchant
if: runner.os == 'macOS' && startsWith(matrix.noxenv, 'docs')
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: |
3.8
3.9
3.10
3.11
3.12
pypy3.10
allow-prereleases: true
- name: Set up nox
uses: wntrblm/nox@2023.04.22
- name: Run nox
run: nox -s "${{ matrix.noxenv }}" -- ${{ matrix.posargs }}
packaging:
needs: ci
runs-on: ubuntu-latest
environment:
name: PyPI
url: https://pypi.org/p/referencing
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
submodules: "recursive"
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install dependencies
run: python -m pip install build
- name: Create packages
run: python -m build .
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
- name: Create a Release
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
uses: softprops/action-gh-release@v1
with:
files: |
dist/*
generate_release_notes: true
referencing-0.31.0/.gitignore 0000664 0000000 0000000 00000005361 14524736324 0016071 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dirhtml/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# User defined
_cache
TODO
referencing-0.31.0/.gitmodules 0000664 0000000 0000000 00000000140 14524736324 0016244 0 ustar 00root root 0000000 0000000 [submodule "suite"]
path = suite
url = https://github.com/python-jsonschema/referencing-suite
referencing-0.31.0/.pre-commit-config.yaml 0000664 0000000 0000000 00000001530 14524736324 0020354 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-ast
- id: check-docstring-first
- id: check-toml
- id: check-vcs-permalinks
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
args: [--fix, lf]
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.1.5"
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.11.0
hooks:
- name: black
id: black
args: ["--line-length", "79"]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.1.0"
hooks:
- id: prettier
referencing-0.31.0/.readthedocs.yml 0000664 0000000 0000000 00000000340 14524736324 0017157 0 ustar 00root root 0000000 0000000 version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
builder: dirhtml
configuration: docs/conf.py
fail_on_warning: true
formats: all
python:
install:
- requirements: docs/requirements.txt
referencing-0.31.0/CHANGELOG.rst 0000777 0000000 0000000 00000000000 14524736324 0021202 2docs/changes.rst ustar 00root root 0000000 0000000 referencing-0.31.0/COPYING 0000664 0000000 0000000 00000002041 14524736324 0015124 0 ustar 00root root 0000000 0000000 Copyright (c) 2022 Julian Berman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
referencing-0.31.0/README.rst 0000664 0000000 0000000 00000002213 14524736324 0015561 0 ustar 00root root 0000000 0000000 ===============
``referencing``
===============
|PyPI| |Pythons| |CI| |ReadTheDocs| |pre-commit|
.. |PyPI| image:: https://img.shields.io/pypi/v/referencing.svg
:alt: PyPI version
:target: https://pypi.org/project/referencing/
.. |Pythons| image:: https://img.shields.io/pypi/pyversions/referencing.svg
:alt: Supported Python versions
:target: https://pypi.org/project/referencing/
.. |CI| image:: https://github.com/python-jsonschema/referencing/workflows/CI/badge.svg
:alt: Build status
:target: https://github.com/python-jsonschema/referencing/actions?query=workflow%3ACI
.. |ReadTheDocs| image:: https://readthedocs.org/projects/referencing/badge/?version=stable&style=flat
:alt: ReadTheDocs status
:target: https://referencing.readthedocs.io/en/stable/
.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/python-jsonschema/referencing/main.svg
:alt: pre-commit.ci status
:target: https://results.pre-commit.ci/latest/github/python-jsonschema/referencing/main
An implementation-agnostic implementation of JSON reference resolution.
See `the documentation `_ for more details.
referencing-0.31.0/docs/ 0000775 0000000 0000000 00000000000 14524736324 0015024 5 ustar 00root root 0000000 0000000 referencing-0.31.0/docs/Makefile 0000664 0000000 0000000 00000001172 14524736324 0016465 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
referencing-0.31.0/docs/api.rst 0000664 0000000 0000000 00000003142 14524736324 0016327 0 ustar 00root root 0000000 0000000 API Reference
=============
.. automodule:: referencing
:members:
:undoc-members:
:imported-members:
:special-members: __iter__, __getitem__, __len__, __rmatmul__
Private Objects
---------------
The following objects are private in the sense that constructing or importing them is not part of the `referencing` public API, as their name indicates (by virtue of beginning with an underscore).
They are however public in the sense that other public API functions may return objects of these types.
Plainly then, you may rely on their methods and attributes not changing in backwards incompatible ways once `referencing` itself is stable, but may not rely on importing or constructing them yourself.
.. autoclass:: referencing._core.Resolved
:members:
:undoc-members:
.. autoclass:: referencing._core.Retrieved
:members:
:undoc-members:
.. autoclass:: referencing._core.AnchorOrResource
.. autoclass:: referencing._core.Resolver
:members:
:undoc-members:
.. autoclass:: referencing._core._MaybeInSubresource
:members:
:undoc-members:
Submodules
----------
referencing.jsonschema
^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: referencing.jsonschema
:members:
:undoc-members:
referencing.exceptions
^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: referencing.exceptions
:members:
:show-inheritance:
:undoc-members:
referencing.retrieval
^^^^^^^^^^^^^^^^^^^^^
.. automodule:: referencing.retrieval
:members:
:undoc-members:
.. autoclass:: referencing.retrieval._T
referencing.typing
^^^^^^^^^^^^^^^^^^
.. automodule:: referencing.typing
:members:
:undoc-members:
referencing-0.31.0/docs/changes.rst 0000664 0000000 0000000 00000007151 14524736324 0017172 0 ustar 00root root 0000000 0000000 =========
Changelog
=========
v0.31.0
-------
* Add ``referencing.jsonschema.EMPTY_REGISTRY`` (which simply has a convenient type annotation, but otherwise is just ``Registry()``).
v0.30.2
-------
* Minor docs improvement.
v0.30.1
-------
* Ensure that an ``sdist`` contains the test suite JSON files.
v0.30.0
-------
* Declare support for Python 3.12.
v0.29.3
-------
* Documentation fix.
v0.29.2
-------
* Improve the hashability of exceptions when they contain hashable data.
v0.29.1
-------
* Minor docs improvement.
v0.29.0
-------
* Add ``referencing.retrieval.to_cached_resource``, a simple caching decorator useful when writing a retrieval function turning JSON text into resources without repeatedly hitting the network, filesystem, etc.
v0.28.6
-------
* No user-facing changes.
v0.28.5
-------
* Fix a type annotation and fill in some missing test coverage.
v0.28.4
-------
* Fix a type annotation.
v0.28.3
-------
* No user-facing changes.
v0.28.2
-------
* Added some additional packaging trove classifiers.
v0.28.1
-------
* More minor documentation improvements
v0.28.0
-------
* Minor documentation improvement
v0.27.4
-------
* Minor simplification to the docs structure.
v0.27.3
-------
* Also strip fragments when using ``__getitem__`` on URIs with empty fragments.
v0.27.2
-------
* Another fix for looking up anchors from non-canonical URIs, now when they're inside a subresource which has a relative ``$id``.
v0.27.1
-------
* Improve a small number of docstrings.
v0.27.0
-------
* Support looking up anchors from non-canonical URIs.
In other words, if you add a resource at the URI ``http://example.com``, then looking up the anchor ``http://example.com#foo`` now works even if the resource has some internal ``$id`` saying its canonical URI is ``http://somethingelse.example.com``.
v0.26.4
-------
* Further API documentation.
v0.26.3
-------
* Add some documentation on ``referencing`` public and non-public API.
v0.26.2
-------
* Also suggest a proper JSON Pointer for users who accidentally use ``#/`` and intend to refer to the entire resource.
v0.26.1
-------
* No changes.
v0.26.0
-------
* Attempt to suggest a correction if someone uses '#foo/bar', which is neither a valid plain name anchor (as it contains a slash) nor a valid JSON pointer (as it doesn't start with a slash)
v0.25.3
-------
* Normalize the ID of JSON Schema resources with empty fragments (by removing the fragment).
Having a schema with an ID with empty fragment is discouraged, and newer versions of the spec may flat-out make it an error, but older meta-schemas indeed used IDs with empty fragments, so some extra normalization was needed and useful here even beyond what was previously done.
TBD on whether this is exactly right if/when another referencing spec defines differing behavior.
v0.25.2
-------
* Minor tweaks to the package keywords and description.
v0.25.1
-------
* Minor internal tweaks to the docs configuration.
v0.25.0
-------
* Bump the minimum version of ``rpds.py`` used, enabling registries to be used from multiple threads.
v0.24.4
-------
* Fix handling of IDs with empty fragments (which are equivalent to URIs with no fragment)
v0.24.3
-------
* Further intro documentation
v0.24.2
-------
* Fix handling of ``additionalProperties`` with boolean value on Draft 4 (where the boolean isn't a schema, it's a special allowed value)
v0.24.1
-------
* Add a bit of intro documentation
v0.24.0
-------
* ``pyrsistent`` was replaced with ``rpds.py`` (Python bindings to the Rust rpds crate), which seems to be quite a bit faster.
No user-facing changes really should be expected here.
referencing-0.31.0/docs/compatibility.rst 0000664 0000000 0000000 00000006531 14524736324 0020434 0 ustar 00root root 0000000 0000000 =============
Compatibility
=============
``referencing`` is currently in beta so long as version numbers begin with a ``0``, meaning its public interface may change if issues are uncovered, though not typically without reason.
Once it seems clear that the interfaces look correct (likely after ``referencing`` is in use for some period of time) versioning will move to `CalVer `_ and interfaces will not change in backwards-incompatible ways without deprecation periods.
.. note::
Backwards compatibility is always defined relative to the specifications we implement.
Changing a behavior which is incorrect according to the relevant referencing specifications is not considered a backwards-incompatible change -- on the contrary, it's considered a bug fix.
In the spirit of `having some explicit detail on referencing's public interfaces `, here is a non-exhaustive list of things which are *not* part of the ``referencing`` public interface, and therefore which may change without warning, even once no longer in beta:
* All commonly understood indicators of privacy in Python -- in particular, (sub)packages, modules and identifiers beginning with a single underscore.
In the case of modules or packages, this includes *all* of their contents recursively, regardless of their naming.
* All contents in the ``referencing.tests`` package unless explicitly indicated otherwise
* The precise contents and wording of exception messages raised by any callable, private *or* public.
* The precise contents of the ``__repr__`` of any type defined in the package.
* The ability to *instantiate* exceptions defined in `referencing.exceptions`, with the sole exception of those explicitly indicating they are publicly instantiable (notably `referencing.exceptions.NoSuchResource`).
* The instantiation of any type with no public identifier, even if instances of it are returned by other public API.
E.g., `referencing._core.Resolver` is not publicly exposed, and it is not public API to instantiate it in ways other than by calling `referencing.Registry.resolver` or an equivalent.
All of its public attributes are of course public, however.
* The concrete types within the signature of a callable whenever they differ from their documented types.
In other words, if a function documents that it returns an argument of type ``Mapping[int, Sequence[str]]``, this is the promised return type, not whatever concrete type is returned which may be richer or have additional attributes and methods.
Changes to the signature will continue to guarantee this return type (or a broader one) but indeed are free to change the concrete type.
* Any identifiers in any modules which are imported from other modules.
In other words, if ``referencing.foo`` imports ``bar`` from ``referencing.quux``, it is *not* public API to use ``referencing.foo.bar``; only ``referencing.quux.bar`` is public API.
This does not apply to any objects exposed directly on the ``referencing`` package (e.g. `referencing.Resource`), which are indeed public.
* Subclassing of any class defined throughout the package.
Doing so is not supported for any object, and in general most types will raise exceptions to remind downstream users not to do so.
If any API usage may be questionable, feel free to open a discussion (or issue if appropriate) to clarify.
referencing-0.31.0/docs/conf.py 0000664 0000000 0000000 00000005541 14524736324 0016330 0 ustar 00root root 0000000 0000000 import importlib.metadata
import re
from url import URL
GITHUB = URL.parse("https://github.com/")
HOMEPAGE = GITHUB / "python-jsonschema/referencing"
project = "referencing"
author = "Julian Berman"
copyright = f"2022, {author}"
release = importlib.metadata.version("referencing")
version = release.partition("-")[0]
language = "en"
default_role = "any"
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.autosectionlabel",
"sphinx.ext.coverage",
"sphinx.ext.doctest",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"sphinx.ext.napoleon",
"sphinx.ext.todo",
"sphinx.ext.viewcode",
"sphinx_click",
"sphinx_copybutton",
"sphinx_json_schema_spec",
"sphinxcontrib.spelling",
"sphinxext.opengraph",
]
pygments_style = "lovelace"
pygments_dark_style = "one-dark"
html_theme = "furo"
# See sphinx-doc/sphinx#10785
_TYPE_ALIASES = dict(
AnchorType=("class", "Anchor"),
D=("data", "D"),
ObjectSchema=("data", "ObjectSchema"),
Schema=("data", "Schema"),
URI=("attr", "URI"), # ?!?!?! Sphinx...
)
def _resolve_broken_refs(app, env, node, contnode):
if node["refdomain"] != "py":
return
if node["reftarget"].startswith("referencing.typing."):
kind, target = "data", node["reftarget"]
else:
kind, target = _TYPE_ALIASES.get(node["reftarget"], (None, None))
if kind is not None:
return app.env.get_domain("py").resolve_xref(
env,
node["refdoc"],
app.builder,
kind,
target,
node,
contnode,
)
def setup(app):
app.connect("missing-reference", _resolve_broken_refs)
def entire_domain(host):
return r"http.?://" + re.escape(host) + r"($|/.*)"
linkcheck_ignore = [
entire_domain("img.shields.io"),
f"{GITHUB}.*#.*",
str(HOMEPAGE / "actions"),
str(HOMEPAGE / "workflows/CI/badge.svg"),
]
# = Extensions =
# -- autodoc --
autodoc_default_options = {
"members": True,
"member-order": "bysource",
}
# -- autosectionlabel --
autosectionlabel_prefix_document = True
# -- intersphinx --
intersphinx_mapping = {
"hatch": ("https://hatch.pypa.io/latest/", None),
"jsonschema-specifications": (
"https://jsonschema-specifications.readthedocs.io/en/stable/",
None,
),
"regret": ("https://regret.readthedocs.io/en/stable/", None),
"python": ("https://docs.python.org/", None),
"setuptools": ("https://setuptools.pypa.io/en/stable/", None),
}
# -- extlinks --
extlinks = {
"gh": (str(HOMEPAGE) + "/%s", None),
"github": (str(GITHUB) + "/%s", None),
"hatch": ("https://hatch.pypa.io/latest/%s", None),
"httpx": ("https://www.python-httpx.org/%s", None),
}
extlinks_detect_hardcoded_links = True
# -- sphinxcontrib-spelling --
spelling_word_list_filename = "spelling-wordlist.txt"
spelling_show_suggestions = True
referencing-0.31.0/docs/index.rst 0000664 0000000 0000000 00000003473 14524736324 0016674 0 ustar 00root root 0000000 0000000 An implementation-agnostic implementation of JSON reference resolution.
In other words, a way for e.g. JSON Schema tooling to resolve the :kw:`$ref` keywords across all drafts without needing to implement support themselves.
This library is meant for use both by implementers of JSON referencing-related tooling -- like JSON Schema implementations supporting the :kw:`$ref` keyword -- as well as by end-users using said implementations who wish to then configure sets of resources (like schemas) for use at runtime.
The simplest example of populating a registry (typically done by end-users) and then looking up a resource from it (typically done by something like a JSON Schema implementation) is:
.. testcode::
from referencing import Registry, Resource
import referencing.jsonschema
schema = Resource.from_contents( # Parse some contents into a 2020-12 JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:example:a-202012-schema",
"$defs": {
"nonNegativeInteger": {
"$anchor": "nonNegativeInteger",
"type": "integer",
"minimum": 0,
},
},
}
)
registry = schema @ Registry() # Add the resource to a new registry
# From here forward, this would usually be done within a library wrapping this one,
# like a JSON Schema implementation
resolver = registry.resolver()
resolved = resolver.lookup("urn:example:a-202012-schema#nonNegativeInteger")
assert resolved.contents == {
"$anchor": "nonNegativeInteger",
"type": "integer",
"minimum": 0,
}
For fuller details, see the `intro`.
.. toctree::
:glob:
:hidden:
intro
schema-packages
compatibility
api
changes
referencing-0.31.0/docs/intro.rst 0000664 0000000 0000000 00000027634 14524736324 0016725 0 ustar 00root root 0000000 0000000 ============
Introduction
============
When authoring JSON documents, it is often useful to be able to reference other JSON documents, or to reference subsections of other JSON documents.
This kind of JSON referencing has historically been defined by various specifications, with slightly differing behavior.
The JSON Schema specifications, for instance, define :kw:`$ref` and :kw:`$dynamicRef` keywords to allow schema authors to combine multiple schemas together for reuse or deduplication as part of authoring JSON schemas.
The `referencing ` library was written in order to provide a simple, well-behaved and well-tested implementation of JSON reference resolution in a way which can be used across multiple specifications or specification versions.
Core Concepts
-------------
There are 3 main objects to be aware of:
* `referencing.Registry`, which represents a specific immutable set of resources (either in-memory or retrievable)
* `referencing.Specification`, which represents a specific specification, such as JSON Schema Draft 7, which can have differing referencing behavior from other specifications or even versions of JSON Schema.
JSON Schema-specific specifications live in the `referencing.jsonschema` module and are named like `referencing.jsonschema.DRAFT202012`.
* `referencing.Resource`, which represents a specific resource (often a Python `dict`) *along* with a specific `referencing.Specification` it is to be interpreted under.
As a concrete example, the simple JSON Schema ``{"type": "integer"}`` may be interpreted as a schema under either Draft 2020-12 or Draft 4 of the JSON Schema specification (amongst others); in draft 2020-12, the float ``2.0`` must be considered an integer, whereas in draft 4, it potentially is not.
If you mean the former (i.e. to associate this schema with draft 2020-12), you'd use ``referencing.Resource(contents={"type": "integer"}, specification=referencing.jsonschema.DRAFT202012)``, whereas for the latter you'd use `referencing.jsonschema.DRAFT4`.
A resource may be identified via one or more URIs, either because they identify themselves in a way proscribed by their specification (e.g. an :kw:`$id` keyword in suitable versions of the JSON Schema specification), or simply because you wish to externally associate a URI with the resource, regardless of a specification-specific way to refer to itself.
You could add the aforementioned simple JSON Schema resource to a `referencing.Registry` by creating an empty registry and then identifying it via some URI:
.. testcode::
from referencing import Registry, Resource
from referencing.jsonschema import DRAFT202012
resource = Resource(contents={"type": "integer"}, specification=DRAFT202012)
registry = Registry().with_resource(uri="http://example.com/my/resource", resource=resource)
print(registry)
.. testoutput::
.. note::
`referencing.Registry` is an entirely immutable object.
All of its methods which add resources to itself return *new* registry objects containing the added resource.
You could also confirm your resource is in the registry if you'd like, via `referencing.Registry.contents`, which will show you the contents of a resource at a given URI:
.. testcode::
print(registry.contents("http://example.com/my/resource"))
.. testoutput::
{'type': 'integer'}
Populating Registries
---------------------
There are a few different methods you can use to populate registries with resources.
Which one you want to use depends on things like:
* do you already have an instance of `referencing.Resource`, or are you creating one out of some loaded JSON?
If not, does the JSON have some sort of identifier that can be used to determine which specification it belongs to (e.g. the JSON Schema :kw:`$schema` keyword)?
* does your resource have an internal ID (e.g. the JSON Schema :kw:`$id` keyword)?
* do you have additional (external) URIs you want to refer to the same resource as well?
* do you have one resource to add or many?
We'll assume for example's sake that we're dealing with JSON Schema resources for the following examples, and we'll furthermore assume you have some initial `referencing.Registry` to add them to, perhaps an empty one:
.. testcode::
from referencing import Registry
initial_registry = Registry()
Recall that registries are immutable, so we'll be "adding" our resources by creating new registries containing the additional resource(s) we add.
In the ideal case, you have a JSON Schema with an internal ID, and which also identifies itself for a specific version of JSON Schema e.g.:
.. code:: json
{
"$id": "urn:example:my-schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer"
}
If you have such a schema in some JSON text, and wish to add a resource to our registry and be able to identify it using its internal ID (``urn:example:my-schema``) you can simply use:
.. testcode::
import json
loaded = json.loads(
"""
{
"$id": "urn:example:my-schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer"
}
""",
)
resource = Resource.from_contents(loaded)
registry = resource @ initial_registry
which will give you a registry with our resource added to it.
Let's check by using `Registry.contents`, which takes a URI and should show us the contents of our resource:
.. testcode::
print(registry.contents("urn:example:my-schema"))
.. testoutput::
{'$id': 'urn:example:my-schema', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'type': 'integer'}
If your schema did *not* have a :kw:`$schema` keyword, you'd get an error:
.. testcode::
another = json.loads(
"""
{
"$id": "urn:example:my-second-schema",
"type": "integer"
}
""",
)
print(Resource.from_contents(another))
.. testoutput::
Traceback (most recent call last):
...
referencing.exceptions.CannotDetermineSpecification: {'$id': 'urn:example:my-second-schema', 'type': 'integer'}
which is telling you that the resource you've tried to create is ambiguous -- there's no way to know which version of JSON Schema you intend it to be written for.
You can of course instead directly create a `Resource`, instead of using `Resource.from_contents`, which will allow you to specify which version of JSON Schema you're intending your schema to be written for:
.. testcode::
import referencing.jsonschema
second = Resource(contents=another, specification=referencing.jsonschema.DRAFT202012)
and now of course can add it as above:
.. testcode::
registry = second @ registry
print(registry.contents("urn:example:my-second-schema"))
.. testoutput::
{'$id': 'urn:example:my-second-schema', 'type': 'integer'}
As a shorthand, you can also use `Specification.create_resource` to create a `Resource` slightly more tersely.
E.g., an equivalent way to create the above resource is:
.. testcode::
second_again = referencing.jsonschema.DRAFT202012.create_resource(another)
print(second_again == second)
.. testoutput::
True
If your resource doesn't contain an :kw:`$id` keyword, you'll get a different error if you attempt to add it to a registry:
.. testcode::
third = Resource(
contents=json.loads("""{"type": "integer"}"""),
specification=referencing.jsonschema.DRAFT202012,
)
registry = third @ registry
.. testoutput::
Traceback (most recent call last):
...
referencing.exceptions.NoInternalID: Resource(contents={'type': 'integer'}, _specification=)
which is now saying that there's no way to add this resource to a registry directly, as it has no ``$id`` -- you must provide whatever URI you intend to use to refer to this resource to be able to add it.
You can do so using `referencing.Registry.with_resource` instead of the `@ operator ` which we have used thus far, and which takes the explicit URI you wish to use as an argument:
.. testcode::
registry = registry.with_resource(uri="urn:example:my-third-schema", resource=third)
which now allows us to use the URI we associated with our third resource to retrieve it:
.. testcode::
print(registry.contents("urn:example:my-third-schema"))
.. testoutput::
{'type': 'integer'}
If you have more than one resource to add, you can use `Registry.with_resources` (with an ``s``) to add many at once, or, if they meet the criteria to use ``@``, you can use ``[one, two, three] @ registry`` to add all three resources at once.
You may also want to have a look at `Registry.with_contents` for a further method to add resources to a registry without constructing a `Resource` object yourself.
Dynamically Retrieving Resources
--------------------------------
Sometimes one wishes to dynamically retrieve or construct `Resource`\ s which *don't* already live in-memory within a `Registry`.
This might be resources retrieved dynamically from a database, from files somewhere on disk, from some arbitrary place over the internet, or from the like.
We'll refer to such resources not present in-memory as *external resources*.
The ``retrieve`` argument to ``Registry`` objects can be used to configure a callable which will be used anytime a requested URI is *not* present in the registry, thereby allowing you to retrieve it from whichever location it lives in.
Here's an example of automatically retrieving external references by downloading them via :httpx:`httpx >`, illustrated by then automatically retrieving one of the JSON Schema metaschemas from the network:
.. code:: python
from referencing import Registry, Resource
import httpx
def retrieve_via_httpx(uri):
response = httpx.get(uri)
return Resource.from_contents(response.json())
registry = Registry(retrieve=retrieve_via_httpx)
resolver = registry.resolver()
print(resolver.lookup("https://json-schema.org/draft/2020-12/schema"))
.. note::
In the case of JSON Schema, the specifications generally discourage implementations from automatically retrieving these sorts of external resources over the network due to potential security implications.
See :kw:`schema-references` in particular.
`referencing` will of course therefore not do any such thing automatically, and this section generally assumes that you have personally considered the security implications for your own use case.
Caching
^^^^^^^
A common concern in these situations is also to *cache* the resulting resource such that repeated lookups of the same URI do not repeatedly call your retrieval function and thereby make network calls, hit the filesystem, etc.
You are of course free to use whatever caching mechanism is convenient even if it uses caching functionality entirely unrelated to this library (e.g. one specific to ``httpx`` in the above example, or one using `functools.lru_cache` internally).
Nonetheless, because it is so common to retrieve a JSON string and construct a resource from it, `referencing.retrieval.to_cached_resource` is a decorator which can help.
If you use it, your retrieval callable should return a `str`, not a `Resource`, as the decorator will handle deserializing your response and constructing a `Resource` from it (this is mostly because otherwise, deserialized JSON is generally not hashable if it ends up being a Python `dict`).
The above example would be written:
.. code:: python
from referencing import Registry, Resource
import httpx
import referencing.retrieval
@referencing.retrieval.to_cached_resource()
def cached_retrieve_via_httpx(uri):
return httpx.get(uri).text
registry = Registry(retrieve=cached_retrieve_via_httpx)
resolver = registry.resolver()
print(resolver.lookup("https://json-schema.org/draft/2020-12/schema"))
It is otherwise functionally equivalent to the above, other than that retrieval will not repeatedly make a web request.
referencing-0.31.0/docs/requirements.in 0000664 0000000 0000000 00000000247 14524736324 0020102 0 ustar 00root root 0000000 0000000 file:.#egg=referencing
furo
pygments-github-lexers
sphinx-click
sphinx-copybutton
sphinx-json-schema-spec
sphinx>5
sphinxcontrib-spelling>5
sphinxext-opengraph
url.py
referencing-0.31.0/docs/requirements.txt 0000664 0000000 0000000 00000004301 14524736324 0020306 0 ustar 00root root 0000000 0000000 #
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --strip-extras docs/requirements.in
#
alabaster==0.7.13
# via sphinx
attrs==23.1.0
# via referencing
babel==2.13.1
# via sphinx
beautifulsoup4==4.12.2
# via furo
certifi==2023.7.22
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via sphinx-click
docutils==0.20.1
# via
# sphinx
# sphinx-click
furo==2023.9.10
# via -r docs/requirements.in
idna==3.4
# via requests
imagesize==1.4.1
# via sphinx
jinja2==3.1.2
# via sphinx
lxml==4.9.3
# via sphinx-json-schema-spec
markupsafe==2.1.3
# via jinja2
packaging==23.2
# via sphinx
pyenchant==3.2.2
# via sphinxcontrib-spelling
pygments==2.16.1
# via
# furo
# pygments-github-lexers
# sphinx
pygments-github-lexers==0.0.5
# via -r docs/requirements.in
file:.#egg=referencing
# via -r docs/requirements.in
requests==2.31.0
# via sphinx
rpds-py==0.12.0
# via referencing
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.5
# via beautifulsoup4
sphinx==7.2.6
# via
# -r docs/requirements.in
# furo
# sphinx-basic-ng
# sphinx-click
# sphinx-copybutton
# sphinx-json-schema-spec
# sphinxcontrib-applehelp
# sphinxcontrib-devhelp
# sphinxcontrib-htmlhelp
# sphinxcontrib-qthelp
# sphinxcontrib-serializinghtml
# sphinxcontrib-spelling
# sphinxext-opengraph
sphinx-basic-ng==1.0.0b2
# via furo
sphinx-click==5.0.1
# via -r docs/requirements.in
sphinx-copybutton==0.5.2
# via -r docs/requirements.in
sphinx-json-schema-spec==2023.8.1
# via -r docs/requirements.in
sphinxcontrib-applehelp==1.0.7
# via sphinx
sphinxcontrib-devhelp==1.0.5
# via sphinx
sphinxcontrib-htmlhelp==2.0.4
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.6
# via sphinx
sphinxcontrib-serializinghtml==1.1.9
# via sphinx
sphinxcontrib-spelling==8.0.0
# via -r docs/requirements.in
sphinxext-opengraph==0.9.0
# via -r docs/requirements.in
url-py==0.9.2
# via -r docs/requirements.in
urllib3==2.1.0
# via requests
referencing-0.31.0/docs/schema-packages.rst 0000664 0000000 0000000 00000001763 14524736324 0020601 0 ustar 00root root 0000000 0000000 ===============
Schema Packages
===============
The `Registry` object is a useful way to ship Python packages which essentially bundle a set of JSON Schemas for use at runtime from Python.
In order to do so, you likely will want to:
* Collect together the JSON files you wish to ship
* Put them inside a Python package (one you possibly tag with the ``jsonschema`` keyword for easy discoverability).
Remember to ensure you have configured your build tool to include the JSON files in your built package distribution -- for e.g. :hatch:`hatch >` this is likely automatic, but for `setuptools ` may involve creating a suitable :file:`MANIFEST.in`.
* Instantiate a `Registry` object somewhere globally within the package
* Call `Registry.crawl` at import-time, such that users of your package get a "fully ready" registry to use
For an example of such a package, see `jsonschema-specifications `, which bundles the JSON Schema "official" schemas for use.
referencing-0.31.0/docs/spelling-wordlist.txt 0000664 0000000 0000000 00000000503 14524736324 0021245 0 ustar 00root root 0000000 0000000 amongst
autodetecting
boolean
changelog
deduplication
dereferenced
deserialized
deserializing
discoverability
docstrings
filesystem
hashable
hashability
implementers
instantiable
instantiation
iterable
lookups
metaschemas
referenceable
resolvers
runtime
schemas
subclassing
submodules
subresource
subresources
unresolvable
referencing-0.31.0/noxfile.py 0000664 0000000 0000000 00000007554 14524736324 0016125 0 ustar 00root root 0000000 0000000 from pathlib import Path
from tempfile import TemporaryDirectory
import os
import nox
ROOT = Path(__file__).parent
PYPROJECT = ROOT / "pyproject.toml"
DOCS = ROOT / "docs"
REFERENCING = ROOT / "referencing"
nox.options.sessions = []
def session(default=True, **kwargs): # noqa: D103
def _session(fn):
if default:
nox.options.sessions.append(kwargs.get("name", fn.__name__))
return nox.session(**kwargs)(fn)
return _session
@session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3"])
def tests(session):
"""
Run the test suite with a corresponding Python version.
"""
session.install("-r", ROOT / "test-requirements.txt")
if session.posargs and session.posargs[0] == "coverage":
if len(session.posargs) > 1 and session.posargs[1] == "github":
github = os.environ["GITHUB_STEP_SUMMARY"]
else:
github = None
session.install("coverage[toml]")
session.run("coverage", "run", "-m", "pytest", REFERENCING)
if github is None:
session.run("coverage", "report")
else:
with open(github, "a") as summary:
summary.write("### Coverage\n\n")
summary.flush() # without a flush, output seems out of order.
session.run(
"coverage",
"report",
"--format=markdown",
stdout=summary,
)
else:
session.run("pytest", *session.posargs, REFERENCING)
@session()
def audit(session):
"""
Audit dependencies for vulnerabilities.
"""
session.install("pip-audit", ROOT)
session.run("python", "-m", "pip_audit")
@session(tags=["build"])
def build(session):
"""
Build a distribution suitable for PyPI and check its validity.
"""
session.install("build", "twine")
with TemporaryDirectory() as tmpdir:
session.run("python", "-m", "build", ROOT, "--outdir", tmpdir)
session.run("twine", "check", "--strict", tmpdir + "/*")
@session(tags=["style"])
def style(session):
"""
Check Python code style.
"""
session.install("ruff")
session.run("ruff", "check", ROOT)
@session()
def typing(session):
"""
Check static typing.
"""
session.install("pyright", ROOT)
session.run("pyright", REFERENCING)
@session(tags=["docs"])
@nox.parametrize(
"builder",
[
nox.param(name, id=name)
for name in [
"dirhtml",
"doctest",
"linkcheck",
"man",
"spelling",
]
],
)
def docs(session, builder):
"""
Build the documentation using a specific Sphinx builder.
"""
session.install("-r", DOCS / "requirements.txt")
with TemporaryDirectory() as tmpdir_str:
tmpdir = Path(tmpdir_str)
argv = ["-n", "-T", "-W"]
if builder != "spelling":
argv += ["-q"]
posargs = session.posargs or [tmpdir / builder]
session.run(
"python",
"-m",
"sphinx",
"-b",
builder,
DOCS,
*argv,
*posargs,
)
@session(tags=["docs", "style"], name="docs(style)")
def docs_style(session):
"""
Check the documentation style.
"""
session.install(
"doc8",
"pygments",
"pygments-github-lexers",
)
session.run("python", "-m", "doc8", "--config", PYPROJECT, DOCS)
@session(default=False)
def requirements(session):
"""
Update the project's pinned requirements. Commit the result.
"""
session.install("pip-tools")
for each in [DOCS / "requirements.in", ROOT / "test-requirements.in"]:
session.run(
"pip-compile",
"--resolver",
"backtracking",
"--strip-extras",
"-U",
each.relative_to(ROOT),
)
referencing-0.31.0/pyproject.toml 0000664 0000000 0000000 00000006776 14524736324 0017030 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
[project]
name = "referencing"
description = "JSON Referencing + Python"
readme = "README.rst"
license = {text = "MIT"}
requires-python = ">=3.8"
keywords = ["json", "referencing", "jsonschema", "openapi", "asyncapi"]
authors = [
{email = "Julian+referencing@GrayVines.com"},
{name = "Julian Berman"},
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: File Formats :: JSON",
"Topic :: File Formats :: JSON :: JSON Schema",
]
dynamic = ["version"]
dependencies = [
"attrs>=22.2.0",
"rpds-py>=0.7.0",
]
[project.urls]
Documentation = "https://referencing.readthedocs.io/"
Homepage = "https://github.com/python-jsonschema/referencing"
Issues = "https://github.com/python-jsonschema/referencing/issues/"
Funding = "https://github.com/sponsors/Julian"
Source = "https://github.com/python-jsonschema/referencing"
[tool.coverage.html]
show_contexts = true
skip_covered = false
[tool.coverage.run]
branch = true
source = ["referencing"]
dynamic_context = "test_function"
[tool.coverage.report]
exclude_also = [
"if TYPE_CHECKING:",
"\\s*\\.\\.\\.\\s*",
]
fail_under = 100
show_missing = true
skip_covered = true
[tool.doc8]
ignore = [
"D001", # one sentence per line, so max length doesn't make sense
]
[tool.isort]
combine_as_imports = true
ensure_newline_before_comments = true
from_first = true
include_trailing_comma = true
multi_line_output = 3
use_parentheses = true
[tool.pyright]
strict = ["**/*"]
exclude = [
"**/tests/__init__.py",
"**/tests/test_*.py",
]
[tool.ruff]
line-length = 79
select = [
"ANN", "B", "D", "D204", "E", "F", "Q", "RUF", "SIM", "TCH", "UP", "W",
]
ignore = [
# Wat, type annotations for self and cls, why is this a thing?
"ANN101",
"ANN102",
# Private annotations are fine to leave out.
"ANN202",
# I don't know how to more properly annotate "pass along all arguments".
"ANN401",
# It's totally OK to call functions for default arguments.
"B008",
# raise SomeException(...) is fine.
"B904",
# There's no need for explicit strict, this is simply zip's default behavior.
"B905",
# It's fine to not have docstrings for magic methods.
"D105",
# __init__ especially doesn't need a docstring
"D107",
# This rule makes diffs uglier when expanding docstrings (and it's uglier)
"D200",
# No blank lines before docstrings.
"D203",
# Start docstrings on the second line.
"D212",
# This rule misses sassy docstrings ending with ! or ?.
"D400",
# Section headers should end with a colon not a newline
"D406",
# Underlines aren't needed
"D407",
# Plz spaces after section headers
"D412",
# Not sure what heuristic this uses, but it seems easy for it to be wrong.
"SIM300",
# We support 3.8 + 3.9
"UP007",
]
extend-exclude = ["suite"]
[tool.ruff.flake8-quotes]
docstring-quotes = "double"
[tool.ruff.per-file-ignores]
"noxfile.py" = ["ANN", "D100"]
"docs/*" = ["ANN", "D"]
"referencing/tests/*" = ["ANN", "D", "RUF012"]
referencing-0.31.0/referencing/ 0000775 0000000 0000000 00000000000 14524736324 0016363 5 ustar 00root root 0000000 0000000 referencing-0.31.0/referencing/__init__.py 0000664 0000000 0000000 00000000316 14524736324 0020474 0 ustar 00root root 0000000 0000000 """
Cross-specification, implementation-agnostic JSON referencing.
"""
from referencing._core import Anchor, Registry, Resource, Specification
__all__ = ["Anchor", "Registry", "Resource", "Specification"]
referencing-0.31.0/referencing/_attrs.py 0000664 0000000 0000000 00000001350 14524736324 0020230 0 ustar 00root root 0000000 0000000 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):
pass
@staticmethod
def _do_not_subclass() -> NoReturn: # pragma: no cover
raise UnsupportedSubclassing(
"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.",
)
referencing-0.31.0/referencing/_attrs.pyi 0000664 0000000 0000000 00000001057 14524736324 0020405 0 ustar 00root root 0000000 0000000 from typing import Any, Callable, TypeVar, Union
from attr import attrib, field
class UnsupportedSubclassing(Exception): ...
_T = TypeVar("_T")
def __dataclass_transform__(
*,
frozen_default: bool = False,
field_descriptors: tuple[Union[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]: ...
referencing-0.31.0/referencing/_core.py 0000664 0000000 0000000 00000053531 14524736324 0020033 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from collections.abc import Iterable, Iterator, Sequence
from typing import Any, Callable, ClassVar, Generic, Protocol, TypeVar
from urllib.parse import unquote, urldefrag, urljoin
from attrs import evolve, field
from rpds import HashTrieMap, HashTrieSet, List
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 _MaybeInSubresource(Protocol[D]):
def __call__(
self,
segments: Sequence[int | str],
resolver: Resolver[D],
subresource: Resource[D],
) -> Resolver[D]:
...
@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]]
def __repr__(self) -> str:
return f""
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: Specification[D] = None, # type: ignore[reportGeneralTypeIssues]
) -> Resource[D]:
"""
Attempt to discern which specification applies to the given 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
if isinstance(contents, Mapping):
jsonschema_dialect_id = contents.get("$schema") # type: ignore[reportUnknownMemberType]
if jsonschema_dialect_id is not None:
from referencing.jsonschema import specification_with
specification = specification_with(
jsonschema_dialect_id, # type: ignore[reportUnknownArgumentType]
default=default_specification,
)
if specification is None: # type: ignore[reportUnnecessaryComparison]
raise exceptions.CannotDetermineSpecification(contents)
return cls(contents=contents, specification=specification) # type: ignore[reportUnknownArgumentType]
@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
"""
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:
raise exceptions.PointerToNowhere(ref=pointer, resource=self)
segments.append(segment)
last = resolver
resolver = self._specification.maybe_in_subresource(
segments=segments,
resolver=resolver,
subresource=self._specification.create_resource(contents), # type: ignore[reportUnknownArgumentType]
)
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() # type: ignore[reportGeneralTypeIssues]
_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)
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""
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:
raise exceptions.Unretrievable(ref=uri)
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.
"""
# Empty fragment URIs are equivalent to URIs without the fragment.
# TODO: Is this true for non JSON Schema resources? Probably not.
return self._resources[uri.rstrip("#")].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) # type: ignore[reportUnknownMemberType]
anchors = anchors.update(registry._anchors) # type: ignore[reportUnknownMemberType]
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(
"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])
@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:
raise exceptions.Unresolvable(ref=ref)
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)
referencing-0.31.0/referencing/exceptions.py 0000664 0000000 0000000 00000010100 14524736324 0021106 0 ustar 00root root 0000000 0000000 """
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: Any) -> 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: Any) -> 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: Any) -> 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: Any) -> 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: Any) -> 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."
)
referencing-0.31.0/referencing/jsonschema.py 0000664 0000000 0000000 00000044413 14524736324 0021075 0 ustar 00root root 0000000 0000000 """
Referencing implementations for JSON Schema specs (historic & current).
"""
from __future__ import annotations
from collections.abc import Sequence, Set
from typing import Any, Iterable, Union
from referencing import Anchor, Registry, Resource, Specification, exceptions
from referencing._attrs import frozen
from referencing._core import Resolved as _Resolved, Resolver as _Resolver
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 = Union[bool, ObjectSchema]
#: 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 == "items" or segment == "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",
"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",
"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",
"dependentSchemas",
"patternProperties",
"properties",
},
),
anchors_in=_anchor_2019, # type: ignore[reportGeneralTypeIssues] TODO: check whether this is real
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",
"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, # type: ignore[reportGeneralTypeIssues] TODO: check whether this is real
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, # type: ignore[reportGeneralTypeIssues] TODO: check whether this is real
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(
{ # type: ignore[reportGeneralTypeIssues] # :/ internal vs external types
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] = None, # type: ignore[reportGeneralTypeIssues]
) -> 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 None: # type: ignore[reportUnnecessaryComparison]
raise UnknownDialect(dialect_id)
return default
@frozen
class DynamicAnchor:
"""
Dynamic anchors, introduced in draft 2020.
"""
name: str
resource: Resource[Schema]
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
referencing-0.31.0/referencing/py.typed 0000664 0000000 0000000 00000000000 14524736324 0020050 0 ustar 00root root 0000000 0000000 referencing-0.31.0/referencing/retrieval.py 0000664 0000000 0000000 00000005015 14524736324 0020733 0 ustar 00root root 0000000 0000000 """
Helpers related to (dynamic) resource retrieval.
"""
from __future__ import annotations
from functools import lru_cache
from typing import TYPE_CHECKING, Callable, TypeVar
import json
from referencing import Resource
if TYPE_CHECKING:
from referencing.typing import URI, D, Retrieve
#: A serialized document (e.g. a JSON string)
_T = TypeVar("_T")
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
referencing-0.31.0/referencing/tests/ 0000775 0000000 0000000 00000000000 14524736324 0017525 5 ustar 00root root 0000000 0000000 referencing-0.31.0/referencing/tests/__init__.py 0000664 0000000 0000000 00000000000 14524736324 0021624 0 ustar 00root root 0000000 0000000 referencing-0.31.0/referencing/tests/test_core.py 0000664 0000000 0000000 00000106106 14524736324 0022072 0 ustar 00root root 0000000 0000000 from rpds import HashTrieMap
import pytest
from referencing import Anchor, Registry, Resource, Specification, exceptions
from referencing.jsonschema import DRAFT202012
ID_AND_CHILDREN = Specification(
name="id-and-children",
id_of=lambda contents: contents.get("ID"),
subresources_of=lambda contents: contents.get("children", []),
anchors_in=lambda specification, contents: [
Anchor(
name=name,
resource=specification.create_resource(contents=each),
)
for name, each in contents.get("anchors", {}).items()
],
maybe_in_subresource=lambda segments, resolver, subresource: (
resolver.in_subresource(subresource)
if not len(segments) % 2
and all(each == "children" for each in segments[::2])
else resolver
),
)
def blow_up(uri): # pragma: no cover
"""
A retriever suitable for use in tests which expect it never to be used.
"""
raise RuntimeError("This retrieve function expects to never be called!")
class TestRegistry:
def test_with_resource(self):
"""
Adding a resource to the registry then allows re-retrieving it.
"""
resource = Resource.opaque(contents={"foo": "bar"})
uri = "urn:example"
registry = Registry().with_resource(uri=uri, resource=resource)
assert registry[uri] is resource
def test_with_resources(self):
"""
Adding multiple resources to the registry is like adding each one.
"""
one = Resource.opaque(contents={})
two = Resource(contents={"foo": "bar"}, specification=ID_AND_CHILDREN)
registry = Registry().with_resources(
[
("http://example.com/1", one),
("http://example.com/foo/bar", two),
],
)
assert registry == Registry().with_resource(
uri="http://example.com/1",
resource=one,
).with_resource(
uri="http://example.com/foo/bar",
resource=two,
)
def test_matmul_resource(self):
uri = "urn:example:resource"
resource = ID_AND_CHILDREN.create_resource({"ID": uri, "foo": 12})
registry = resource @ Registry()
assert registry == Registry().with_resource(uri, resource)
def test_matmul_many_resources(self):
one_uri = "urn:example:one"
one = ID_AND_CHILDREN.create_resource({"ID": one_uri, "foo": 12})
two_uri = "urn:example:two"
two = ID_AND_CHILDREN.create_resource({"ID": two_uri, "foo": 12})
registry = [one, two] @ Registry()
assert registry == Registry().with_resources(
[(one_uri, one), (two_uri, two)],
)
def test_matmul_resource_without_id(self):
resource = Resource.opaque(contents={"foo": "bar"})
with pytest.raises(exceptions.NoInternalID) as e:
resource @ Registry()
assert e.value == exceptions.NoInternalID(resource=resource)
def test_with_contents_from_json_schema(self):
uri = "urn:example"
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
registry = Registry().with_contents([(uri, schema)])
expected = Resource(contents=schema, specification=DRAFT202012)
assert registry[uri] == expected
def test_with_contents_and_default_specification(self):
uri = "urn:example"
registry = Registry().with_contents(
[(uri, {"foo": "bar"})],
default_specification=Specification.OPAQUE,
)
assert registry[uri] == Resource.opaque({"foo": "bar"})
def test_len(self):
total = 5
registry = Registry().with_contents(
[(str(i), {"foo": "bar"}) for i in range(total)],
default_specification=Specification.OPAQUE,
)
assert len(registry) == total
def test_bool_empty(self):
assert not Registry()
def test_bool_not_empty(self):
registry = Registry().with_contents(
[(str(i), {"foo": "bar"}) for i in range(3)],
default_specification=Specification.OPAQUE,
)
assert registry
def test_iter(self):
registry = Registry().with_contents(
[(str(i), {"foo": "bar"}) for i in range(8)],
default_specification=Specification.OPAQUE,
)
assert set(registry) == {str(i) for i in range(8)}
def test_crawl_still_has_top_level_resource(self):
resource = Resource.opaque({"foo": "bar"})
uri = "urn:example"
registry = Registry({uri: resource}).crawl()
assert registry[uri] is resource
def test_crawl_finds_a_subresource(self):
child_id = "urn:child"
root = ID_AND_CHILDREN.create_resource(
{"ID": "urn:root", "children": [{"ID": child_id, "foo": 12}]},
)
registry = root @ Registry()
with pytest.raises(LookupError):
registry[child_id]
expected = ID_AND_CHILDREN.create_resource({"ID": child_id, "foo": 12})
assert registry.crawl()[child_id] == expected
def test_crawl_finds_anchors_with_id(self):
resource = ID_AND_CHILDREN.create_resource(
{"ID": "urn:bar", "anchors": {"foo": 12}},
)
registry = resource @ Registry()
assert registry.crawl().anchor(resource.id(), "foo").value == Anchor(
name="foo",
resource=ID_AND_CHILDREN.create_resource(12),
)
def test_crawl_finds_anchors_no_id(self):
resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
registry = Registry().with_resource("urn:root", resource)
assert registry.crawl().anchor("urn:root", "foo").value == Anchor(
name="foo",
resource=ID_AND_CHILDREN.create_resource(12),
)
def test_contents(self):
resource = Resource.opaque({"foo": "bar"})
uri = "urn:example"
registry = Registry().with_resource(uri, resource)
assert registry.contents(uri) == {"foo": "bar"}
def test_getitem_strips_empty_fragments(self):
uri = "http://example.com/"
resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
registry = resource @ Registry()
assert registry[uri] == registry[uri + "#"] == resource
def test_contents_strips_empty_fragments(self):
uri = "http://example.com/"
resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
registry = resource @ Registry()
assert (
registry.contents(uri)
== registry.contents(uri + "#")
== {"ID": uri + "#"}
)
def test_crawled_anchor(self):
resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}})
registry = Registry().with_resource("urn:example", resource)
retrieved = registry.anchor("urn:example", "foo")
assert retrieved.value == Anchor(
name="foo",
resource=ID_AND_CHILDREN.create_resource("bar"),
)
assert retrieved.registry == registry.crawl()
def test_anchor_in_nonexistent_resource(self):
registry = Registry()
with pytest.raises(exceptions.NoSuchResource) as e:
registry.anchor("urn:example", "foo")
assert e.value == exceptions.NoSuchResource(ref="urn:example")
def test_init(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
registry = Registry(
{
"http://example.com/1": one,
"http://example.com/foo/bar": two,
},
)
assert (
registry
== Registry()
.with_resources(
[
("http://example.com/1", one),
("http://example.com/foo/bar", two),
],
)
.crawl()
)
def test_dict_conversion(self):
"""
Passing a `dict` to `Registry` gets converted to a `HashTrieMap`.
So continuing to use the registry works.
"""
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
registry = Registry(
{"http://example.com/1": one},
).with_resource("http://example.com/foo/bar", two)
assert (
registry.crawl()
== Registry()
.with_resources(
[
("http://example.com/1", one),
("http://example.com/foo/bar", two),
],
)
.crawl()
)
def test_no_such_resource(self):
registry = Registry()
with pytest.raises(exceptions.NoSuchResource) as e:
registry["urn:bigboom"]
assert e.value == exceptions.NoSuchResource(ref="urn:bigboom")
def test_combine(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
four = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
first = Registry({"http://example.com/1": one})
second = Registry().with_resource("http://example.com/foo/bar", two)
third = Registry(
{
"http://example.com/1": one,
"http://example.com/baz": three,
},
)
fourth = (
Registry()
.with_resource(
"http://example.com/foo/quux",
four,
)
.crawl()
)
assert first.combine(second, third, fourth) == Registry(
[
("http://example.com/1", one),
("http://example.com/baz", three),
("http://example.com/foo/quux", four),
],
anchors=HashTrieMap(
{
("http://example.com/foo/quux", "foo"): Anchor(
name="foo",
resource=ID_AND_CHILDREN.create_resource(12),
),
},
),
).with_resource("http://example.com/foo/bar", two)
def test_combine_self(self):
"""
Combining a registry with itself short-circuits.
This is a performance optimization -- otherwise we do lots more work
(in jsonschema this seems to correspond to making the test suite take
*3x* longer).
"""
registry = Registry({"urn:foo": "bar"})
assert registry.combine(registry) is registry
def test_combine_with_uncrawled_resources(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
first = Registry().with_resource("http://example.com/1", one)
second = Registry().with_resource("http://example.com/foo/bar", two)
third = Registry(
{
"http://example.com/1": one,
"http://example.com/baz": three,
},
)
expected = Registry(
[
("http://example.com/1", one),
("http://example.com/foo/bar", two),
("http://example.com/baz", three),
],
)
combined = first.combine(second, third)
assert combined != expected
assert combined.crawl() == expected
def test_combine_with_single_retrieve(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
def retrieve(uri): # pragma: no cover
pass
first = Registry().with_resource("http://example.com/1", one)
second = Registry(
retrieve=retrieve,
).with_resource("http://example.com/2", two)
third = Registry().with_resource("http://example.com/3", three)
assert first.combine(second, third) == Registry(
retrieve=retrieve,
).with_resources(
[
("http://example.com/1", one),
("http://example.com/2", two),
("http://example.com/3", three),
],
)
assert second.combine(first, third) == Registry(
retrieve=retrieve,
).with_resources(
[
("http://example.com/1", one),
("http://example.com/2", two),
("http://example.com/3", three),
],
)
def test_combine_with_common_retrieve(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
def retrieve(uri): # pragma: no cover
pass
first = Registry(retrieve=retrieve).with_resource(
"http://example.com/1",
one,
)
second = Registry(
retrieve=retrieve,
).with_resource("http://example.com/2", two)
third = Registry(retrieve=retrieve).with_resource(
"http://example.com/3",
three,
)
assert first.combine(second, third) == Registry(
retrieve=retrieve,
).with_resources(
[
("http://example.com/1", one),
("http://example.com/2", two),
("http://example.com/3", three),
],
)
assert second.combine(first, third) == Registry(
retrieve=retrieve,
).with_resources(
[
("http://example.com/1", one),
("http://example.com/2", two),
("http://example.com/3", three),
],
)
def test_combine_conflicting_retrieve(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
def foo_retrieve(uri): # pragma: no cover
pass
def bar_retrieve(uri): # pragma: no cover
pass
first = Registry(retrieve=foo_retrieve).with_resource(
"http://example.com/1",
one,
)
second = Registry().with_resource("http://example.com/2", two)
third = Registry(retrieve=bar_retrieve).with_resource(
"http://example.com/3",
three,
)
with pytest.raises(Exception, match="conflict.*retriev"):
first.combine(second, third)
def test_remove(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
registry = Registry({"urn:foo": one, "urn:bar": two})
assert registry.remove("urn:foo") == Registry({"urn:bar": two})
def test_remove_uncrawled(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
registry = Registry().with_resources(
[("urn:foo", one), ("urn:bar", two)],
)
assert registry.remove("urn:foo") == Registry().with_resource(
"urn:bar",
two,
)
def test_remove_with_anchors(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}})
registry = (
Registry()
.with_resources(
[("urn:foo", one), ("urn:bar", two)],
)
.crawl()
)
assert (
registry.remove("urn:bar")
== Registry()
.with_resource(
"urn:foo",
one,
)
.crawl()
)
def test_remove_nonexistent_uri(self):
with pytest.raises(exceptions.NoSuchResource) as e:
Registry().remove("urn:doesNotExist")
assert e.value == exceptions.NoSuchResource(ref="urn:doesNotExist")
def test_retrieve(self):
foo = Resource.opaque({"foo": "bar"})
registry = Registry(retrieve=lambda uri: foo)
assert registry.get_or_retrieve("urn:example").value == foo
def test_retrieve_arbitrary_exception(self):
foo = Resource.opaque({"foo": "bar"})
def retrieve(uri):
if uri == "urn:succeed":
return foo
raise Exception("Oh no!")
registry = Registry(retrieve=retrieve)
assert registry.get_or_retrieve("urn:succeed").value == foo
with pytest.raises(exceptions.Unretrievable):
registry.get_or_retrieve("urn:uhoh")
def test_retrieve_no_such_resource(self):
foo = Resource.opaque({"foo": "bar"})
def retrieve(uri):
if uri == "urn:succeed":
return foo
raise exceptions.NoSuchResource(ref=uri)
registry = Registry(retrieve=retrieve)
assert registry.get_or_retrieve("urn:succeed").value == foo
with pytest.raises(exceptions.NoSuchResource):
registry.get_or_retrieve("urn:uhoh")
def test_retrieve_cannot_determine_specification(self):
def retrieve(uri):
return Resource.from_contents({})
registry = Registry(retrieve=retrieve)
with pytest.raises(exceptions.CannotDetermineSpecification):
registry.get_or_retrieve("urn:uhoh")
def test_retrieve_already_available_resource(self):
foo = Resource.opaque({"foo": "bar"})
registry = Registry({"urn:example": foo}, retrieve=blow_up)
assert registry["urn:example"] == foo
assert registry.get_or_retrieve("urn:example").value == foo
def test_retrieve_first_checks_crawlable_resource(self):
child = ID_AND_CHILDREN.create_resource({"ID": "urn:child", "foo": 12})
root = ID_AND_CHILDREN.create_resource({"children": [child.contents]})
registry = Registry(retrieve=blow_up).with_resource("urn:root", root)
assert registry.crawl()["urn:child"] == child
def test_resolver(self):
one = Resource.opaque(contents={})
registry = Registry({"http://example.com": one})
resolver = registry.resolver(base_uri="http://example.com")
assert resolver.lookup("#").contents == {}
def test_resolver_with_root_identified(self):
root = ID_AND_CHILDREN.create_resource({"ID": "http://example.com"})
resolver = Registry().resolver_with_root(root)
assert resolver.lookup("http://example.com").contents == root.contents
assert resolver.lookup("#").contents == root.contents
def test_resolver_with_root_unidentified(self):
root = Resource.opaque(contents={})
resolver = Registry().resolver_with_root(root)
assert resolver.lookup("#").contents == root.contents
def test_repr(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
registry = Registry().with_resources(
[
("http://example.com/1", one),
("http://example.com/foo/bar", two),
],
)
assert repr(registry) == ""
assert repr(registry.crawl()) == ""
def test_repr_mixed_crawled(self):
one = Resource.opaque(contents={})
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
registry = (
Registry(
{"http://example.com/1": one},
)
.crawl()
.with_resource(uri="http://example.com/foo/bar", resource=two)
)
assert repr(registry) == ""
def test_repr_one_resource(self):
registry = Registry().with_resource(
uri="http://example.com/1",
resource=Resource.opaque(contents={}),
)
assert repr(registry) == ""
def test_repr_empty(self):
assert repr(Registry()) == ""
class TestResource:
def test_from_contents_from_json_schema(self):
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
resource = Resource.from_contents(schema)
assert resource == Resource(contents=schema, specification=DRAFT202012)
def test_from_contents_with_no_discernible_information(self):
"""
Creating a resource with no discernible way to see what
specification it belongs to (e.g. no ``$schema`` keyword for JSON
Schema) raises an error.
"""
with pytest.raises(exceptions.CannotDetermineSpecification):
Resource.from_contents({"foo": "bar"})
def test_from_contents_with_no_discernible_information_and_default(self):
resource = Resource.from_contents(
{"foo": "bar"},
default_specification=Specification.OPAQUE,
)
assert resource == Resource.opaque(contents={"foo": "bar"})
def test_from_contents_unneeded_default(self):
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
resource = Resource.from_contents(
schema,
default_specification=Specification.OPAQUE,
)
assert resource == Resource(
contents=schema,
specification=DRAFT202012,
)
def test_non_mapping_from_contents(self):
resource = Resource.from_contents(
True,
default_specification=ID_AND_CHILDREN,
)
assert resource == Resource(
contents=True,
specification=ID_AND_CHILDREN,
)
def test_from_contents_with_fallback(self):
resource = Resource.from_contents(
{"foo": "bar"},
default_specification=Specification.OPAQUE,
)
assert resource == Resource.opaque(contents={"foo": "bar"})
def test_id_delegates_to_specification(self):
specification = Specification(
name="",
id_of=lambda contents: "urn:fixedID",
subresources_of=lambda contents: [],
anchors_in=lambda specification, contents: [],
maybe_in_subresource=(
lambda segments, resolver, subresource: resolver
),
)
resource = Resource(
contents={"foo": "baz"},
specification=specification,
)
assert resource.id() == "urn:fixedID"
def test_id_strips_empty_fragment(self):
uri = "http://example.com/"
root = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
assert root.id() == uri
def test_subresources_delegates_to_specification(self):
resource = ID_AND_CHILDREN.create_resource({"children": [{}, 12]})
assert list(resource.subresources()) == [
ID_AND_CHILDREN.create_resource(each) for each in [{}, 12]
]
def test_subresource_with_different_specification(self):
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
resource = ID_AND_CHILDREN.create_resource({"children": [schema]})
assert list(resource.subresources()) == [
DRAFT202012.create_resource(schema),
]
def test_anchors_delegates_to_specification(self):
resource = ID_AND_CHILDREN.create_resource(
{"anchors": {"foo": {}, "bar": 1, "baz": ""}},
)
assert list(resource.anchors()) == [
Anchor(name="foo", resource=ID_AND_CHILDREN.create_resource({})),
Anchor(name="bar", resource=ID_AND_CHILDREN.create_resource(1)),
Anchor(name="baz", resource=ID_AND_CHILDREN.create_resource("")),
]
def test_pointer_to_mapping(self):
resource = Resource.opaque(contents={"foo": "baz"})
resolver = Registry().resolver()
assert resource.pointer("/foo", resolver=resolver).contents == "baz"
def test_pointer_to_array(self):
resource = Resource.opaque(contents={"foo": {"bar": [3]}})
resolver = Registry().resolver()
assert resource.pointer("/foo/bar/0", resolver=resolver).contents == 3
def test_opaque(self):
contents = {"foo": "bar"}
assert Resource.opaque(contents) == Resource(
contents=contents,
specification=Specification.OPAQUE,
)
class TestResolver:
def test_lookup_exact_uri(self):
resource = Resource.opaque(contents={"foo": "baz"})
resolver = Registry({"http://example.com/1": resource}).resolver()
resolved = resolver.lookup("http://example.com/1")
assert resolved.contents == resource.contents
def test_lookup_subresource(self):
root = ID_AND_CHILDREN.create_resource(
{
"ID": "http://example.com/",
"children": [
{"ID": "http://example.com/a", "foo": 12},
],
},
)
registry = root @ Registry()
resolved = registry.resolver().lookup("http://example.com/a")
assert resolved.contents == {"ID": "http://example.com/a", "foo": 12}
def test_lookup_anchor_with_id(self):
root = ID_AND_CHILDREN.create_resource(
{
"ID": "http://example.com/",
"anchors": {"foo": 12},
},
)
registry = root @ Registry()
resolved = registry.resolver().lookup("http://example.com/#foo")
assert resolved.contents == 12
def test_lookup_anchor_without_id(self):
root = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
resolver = Registry().with_resource("urn:example", root).resolver()
resolved = resolver.lookup("urn:example#foo")
assert resolved.contents == 12
def test_lookup_unknown_reference(self):
resolver = Registry().resolver()
ref = "http://example.com/does/not/exist"
with pytest.raises(exceptions.Unresolvable) as e:
resolver.lookup(ref)
assert e.value == exceptions.Unresolvable(ref=ref)
def test_lookup_non_existent_pointer(self):
resource = Resource.opaque({"foo": {}})
resolver = Registry({"http://example.com/1": resource}).resolver()
ref = "http://example.com/1#/foo/bar"
with pytest.raises(exceptions.Unresolvable) as e:
resolver.lookup(ref)
assert e.value == exceptions.PointerToNowhere(
ref="/foo/bar",
resource=resource,
)
assert str(e.value) == "'/foo/bar' does not exist within {'foo': {}}"
def test_lookup_non_existent_pointer_to_array_index(self):
resource = Resource.opaque([1, 2, 4, 8])
resolver = Registry({"http://example.com/1": resource}).resolver()
ref = "http://example.com/1#/10"
with pytest.raises(exceptions.Unresolvable) as e:
resolver.lookup(ref)
assert e.value == exceptions.PointerToNowhere(
ref="/10",
resource=resource,
)
def test_lookup_pointer_to_empty_string(self):
resolver = Registry().resolver_with_root(Resource.opaque({"": {}}))
assert resolver.lookup("#/").contents == {}
def test_lookup_non_existent_pointer_to_empty_string(self):
resource = Resource.opaque({"foo": {}})
resolver = Registry().resolver_with_root(resource)
with pytest.raises(
exceptions.Unresolvable,
match="^'/' does not exist within {'foo': {}}.*'#'",
) as e:
resolver.lookup("#/")
assert e.value == exceptions.PointerToNowhere(
ref="/",
resource=resource,
)
def test_lookup_non_existent_anchor(self):
root = ID_AND_CHILDREN.create_resource({"anchors": {}})
resolver = Registry().with_resource("urn:example", root).resolver()
resolved = resolver.lookup("urn:example")
assert resolved.contents == root.contents
ref = "urn:example#noSuchAnchor"
with pytest.raises(exceptions.Unresolvable) as e:
resolver.lookup(ref)
assert "'noSuchAnchor' does not exist" in str(e.value)
assert e.value == exceptions.NoSuchAnchor(
ref="urn:example",
resource=root,
anchor="noSuchAnchor",
)
def test_lookup_invalid_JSON_pointerish_anchor(self):
resolver = Registry().resolver_with_root(
ID_AND_CHILDREN.create_resource(
{
"ID": "http://example.com/",
"foo": {"bar": 12},
},
),
)
valid = resolver.lookup("#/foo/bar")
assert valid.contents == 12
with pytest.raises(exceptions.InvalidAnchor) as e:
resolver.lookup("#foo/bar")
assert " '#/foo/bar'" in str(e.value)
def test_lookup_retrieved_resource(self):
resource = Resource.opaque(contents={"foo": "baz"})
resolver = Registry(retrieve=lambda uri: resource).resolver()
resolved = resolver.lookup("http://example.com/")
assert resolved.contents == resource.contents
def test_lookup_failed_retrieved_resource(self):
"""
Unretrievable exceptions are also wrapped in Unresolvable.
"""
uri = "http://example.com/"
registry = Registry(retrieve=blow_up)
with pytest.raises(exceptions.Unretrievable):
registry.get_or_retrieve(uri)
resolver = registry.resolver()
with pytest.raises(exceptions.Unresolvable):
resolver.lookup(uri)
def test_repeated_lookup_from_retrieved_resource(self):
"""
A (custom-)retrieved resource is added to the registry returned by
looking it up.
"""
resource = Resource.opaque(contents={"foo": "baz"})
once = [resource]
def retrieve(uri):
return once.pop()
resolver = Registry(retrieve=retrieve).resolver()
resolved = resolver.lookup("http://example.com/")
assert resolved.contents == resource.contents
resolved = resolved.resolver.lookup("http://example.com/")
assert resolved.contents == resource.contents
def test_repeated_anchor_lookup_from_retrieved_resource(self):
resource = Resource.opaque(contents={"foo": "baz"})
once = [resource]
def retrieve(uri):
return once.pop()
resolver = Registry(retrieve=retrieve).resolver()
resolved = resolver.lookup("http://example.com/")
assert resolved.contents == resource.contents
resolved = resolved.resolver.lookup("#")
assert resolved.contents == resource.contents
# FIXME: The tests below aren't really representable in the current
# suite, though we should probably think of ways to do so.
def test_in_subresource(self):
root = ID_AND_CHILDREN.create_resource(
{
"ID": "http://example.com/",
"children": [
{
"ID": "child/",
"children": [{"ID": "grandchild"}],
},
],
},
)
registry = root @ Registry()
resolver = registry.resolver()
first = resolver.lookup("http://example.com/")
assert first.contents == root.contents
with pytest.raises(exceptions.Unresolvable):
first.resolver.lookup("grandchild")
sub = first.resolver.in_subresource(
ID_AND_CHILDREN.create_resource(first.contents["children"][0]),
)
second = sub.lookup("grandchild")
assert second.contents == {"ID": "grandchild"}
def test_in_pointer_subresource(self):
root = ID_AND_CHILDREN.create_resource(
{
"ID": "http://example.com/",
"children": [
{
"ID": "child/",
"children": [{"ID": "grandchild"}],
},
],
},
)
registry = root @ Registry()
resolver = registry.resolver()
first = resolver.lookup("http://example.com/")
assert first.contents == root.contents
with pytest.raises(exceptions.Unresolvable):
first.resolver.lookup("grandchild")
second = first.resolver.lookup("#/children/0")
third = second.resolver.lookup("grandchild")
assert third.contents == {"ID": "grandchild"}
def test_dynamic_scope(self):
one = ID_AND_CHILDREN.create_resource(
{
"ID": "http://example.com/",
"children": [
{
"ID": "child/",
"children": [{"ID": "grandchild"}],
},
],
},
)
two = ID_AND_CHILDREN.create_resource(
{
"ID": "http://example.com/two",
"children": [{"ID": "two-child/"}],
},
)
registry = [one, two] @ Registry()
resolver = registry.resolver()
first = resolver.lookup("http://example.com/")
second = first.resolver.lookup("#/children/0")
third = second.resolver.lookup("grandchild")
fourth = third.resolver.lookup("http://example.com/two")
assert list(fourth.resolver.dynamic_scope()) == [
("http://example.com/child/grandchild", fourth.resolver._registry),
("http://example.com/child/", fourth.resolver._registry),
("http://example.com/", fourth.resolver._registry),
]
assert list(third.resolver.dynamic_scope()) == [
("http://example.com/child/", third.resolver._registry),
("http://example.com/", third.resolver._registry),
]
assert list(second.resolver.dynamic_scope()) == [
("http://example.com/", second.resolver._registry),
]
assert list(first.resolver.dynamic_scope()) == []
class TestSpecification:
def test_create_resource(self):
specification = Specification(
name="",
id_of=lambda contents: "urn:fixedID",
subresources_of=lambda contents: [],
anchors_in=lambda specification, contents: [],
maybe_in_subresource=(
lambda segments, resolver, subresource: resolver
),
)
resource = specification.create_resource(contents={"foo": "baz"})
assert resource == Resource(
contents={"foo": "baz"},
specification=specification,
)
assert resource.id() == "urn:fixedID"
def test_repr(self):
assert (
repr(ID_AND_CHILDREN) == ""
)
class TestOpaqueSpecification:
THINGS = [{"foo": "bar"}, True, 37, "foo", object()]
@pytest.mark.parametrize("thing", THINGS)
def test_no_id(self, thing):
"""
An arbitrary thing has no ID.
"""
assert Specification.OPAQUE.id_of(thing) is None
@pytest.mark.parametrize("thing", THINGS)
def test_no_subresources(self, thing):
"""
An arbitrary thing has no subresources.
"""
assert list(Specification.OPAQUE.subresources_of(thing)) == []
@pytest.mark.parametrize("thing", THINGS)
def test_no_anchors(self, thing):
"""
An arbitrary thing has no anchors.
"""
assert list(Specification.OPAQUE.anchors_in(thing)) == []
@pytest.mark.parametrize(
"cls",
[Anchor, Registry, Resource, Specification, exceptions.PointerToNowhere],
)
def test_nonsubclassable(cls):
with pytest.raises(Exception, match="(?i)subclassing"):
class Boom(cls): # pragma: no cover
pass
referencing-0.31.0/referencing/tests/test_exceptions.py 0000664 0000000 0000000 00000001646 14524736324 0023326 0 ustar 00root root 0000000 0000000 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()}
referencing-0.31.0/referencing/tests/test_jsonschema.py 0000664 0000000 0000000 00000026647 14524736324 0023307 0 ustar 00root root 0000000 0000000 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()
referencing-0.31.0/referencing/tests/test_referencing_suite.py 0000664 0000000 0000000 00000004240 14524736324 0024636 0 ustar 00root root 0000000 0000000 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):
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")
referencing-0.31.0/referencing/tests/test_retrieval.py 0000664 0000000 0000000 00000007207 14524736324 0023141 0 ustar 00root root 0000000 0000000 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")
referencing-0.31.0/referencing/typing.py 0000664 0000000 0000000 00000002626 14524736324 0020255 0 ustar 00root root 0000000 0000000 """
Type-annotation related support for the referencing library.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Protocol, TypeVar
try:
from collections.abc import Mapping as Mapping
Mapping[str, str]
except TypeError: # pragma: no cover
from typing import Mapping as Mapping
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")
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.
"""
...
referencing-0.31.0/suite/ 0000775 0000000 0000000 00000000000 14524736324 0015225 5 ustar 00root root 0000000 0000000 referencing-0.31.0/test-requirements.in 0000664 0000000 0000000 00000000056 14524736324 0020125 0 ustar 00root root 0000000 0000000 file:.#egg=referencing
pytest
pytest-subtests
referencing-0.31.0/test-requirements.txt 0000664 0000000 0000000 00000001043 14524736324 0020333 0 ustar 00root root 0000000 0000000 #
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --strip-extras test-requirements.in
#
attrs==23.1.0
# via
# pytest-subtests
# referencing
iniconfig==2.0.0
# via pytest
packaging==23.2
# via pytest
pluggy==1.3.0
# via pytest
pytest==7.4.3
# via
# -r test-requirements.in
# pytest-subtests
pytest-subtests==0.11.0
# via -r test-requirements.in
file:.#egg=referencing
# via -r test-requirements.in
rpds-py==0.12.0
# via referencing