pax_global_header00006660000000000000000000000064145247363240014524gustar00rootroot0000000000000052 comment=fdc8ab0116c82622a1ed0cd642e51237788ad1eb referencing-0.31.0/000077500000000000000000000000001452473632400140745ustar00rootroot00000000000000referencing-0.31.0/.github/000077500000000000000000000000001452473632400154345ustar00rootroot00000000000000referencing-0.31.0/.github/FUNDING.yml000066400000000000000000000001001452473632400172400ustar00rootroot00000000000000# These are supported funding model platforms github: "Julian" referencing-0.31.0/.github/SECURITY.md000066400000000000000000000011701452473632400172240ustar00rootroot00000000000000# 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.yml000066400000000000000000000006121452473632400202630ustar00rootroot00000000000000version: 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.yml000066400000000000000000000001141452473632400175730ustar00rootroot00000000000000changelog: exclude: authors: - dependabot - pre-commit-ci referencing-0.31.0/.github/workflows/000077500000000000000000000000001452473632400174715ustar00rootroot00000000000000referencing-0.31.0/.github/workflows/ci.yml000066400000000000000000000054211452473632400206110ustar00rootroot00000000000000name: 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/.gitignore000066400000000000000000000053611452473632400160710ustar00rootroot00000000000000# 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/.gitmodules000066400000000000000000000001401452473632400162440ustar00rootroot00000000000000[submodule "suite"] path = suite url = https://github.com/python-jsonschema/referencing-suite referencing-0.31.0/.pre-commit-config.yaml000066400000000000000000000015301452473632400203540ustar00rootroot00000000000000repos: - 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.yml000066400000000000000000000003401452473632400171570ustar00rootroot00000000000000version: 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.rst000077700000000000000000000000001452473632400212022docs/changes.rstustar00rootroot00000000000000referencing-0.31.0/COPYING000066400000000000000000000020411452473632400151240ustar00rootroot00000000000000Copyright (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.rst000066400000000000000000000022131452473632400155610ustar00rootroot00000000000000=============== ``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/000077500000000000000000000000001452473632400150245ustar00rootroot00000000000000referencing-0.31.0/docs/Makefile000066400000000000000000000011721452473632400164650ustar00rootroot00000000000000# 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.rst000066400000000000000000000031421452473632400163270ustar00rootroot00000000000000API 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.rst000066400000000000000000000071511452473632400171720ustar00rootroot00000000000000========= 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.rst000066400000000000000000000065311452473632400204340ustar00rootroot00000000000000============= 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.py000066400000000000000000000055411452473632400163300ustar00rootroot00000000000000import 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.rst000066400000000000000000000034731452473632400166740ustar00rootroot00000000000000An 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.rst000066400000000000000000000276341452473632400167250ustar00rootroot00000000000000============ 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.in000066400000000000000000000002471452473632400201020ustar00rootroot00000000000000file:.#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.txt000066400000000000000000000043011452473632400203060ustar00rootroot00000000000000# # 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.rst000066400000000000000000000017631452473632400206010ustar00rootroot00000000000000=============== 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.txt000066400000000000000000000005031452473632400212450ustar00rootroot00000000000000amongst 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.py000066400000000000000000000075541452473632400161250ustar00rootroot00000000000000from 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.toml000066400000000000000000000067761452473632400170300ustar00rootroot00000000000000[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/000077500000000000000000000000001452473632400163635ustar00rootroot00000000000000referencing-0.31.0/referencing/__init__.py000066400000000000000000000003161452473632400204740ustar00rootroot00000000000000""" 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.py000066400000000000000000000013501452473632400202300ustar00rootroot00000000000000from __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.pyi000066400000000000000000000010571452473632400204050ustar00rootroot00000000000000from 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.py000066400000000000000000000535311452473632400200330ustar00rootroot00000000000000from __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.py000066400000000000000000000101001452473632400211060ustar00rootroot00000000000000""" 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.py000066400000000000000000000444131452473632400210750ustar00rootroot00000000000000""" 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.typed000066400000000000000000000000001452473632400200500ustar00rootroot00000000000000referencing-0.31.0/referencing/retrieval.py000066400000000000000000000050151452473632400207330ustar00rootroot00000000000000""" 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/000077500000000000000000000000001452473632400175255ustar00rootroot00000000000000referencing-0.31.0/referencing/tests/__init__.py000066400000000000000000000000001452473632400216240ustar00rootroot00000000000000referencing-0.31.0/referencing/tests/test_core.py000066400000000000000000001061061452473632400220720ustar00rootroot00000000000000from 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.py000066400000000000000000000016461452473632400233260ustar00rootroot00000000000000import 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.py000066400000000000000000000266471452473632400233070ustar00rootroot00000000000000import 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.py000066400000000000000000000042401452473632400246360ustar00rootroot00000000000000from 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.py000066400000000000000000000072071452473632400231410ustar00rootroot00000000000000from 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.py000066400000000000000000000026261452473632400202550ustar00rootroot00000000000000""" 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/000077500000000000000000000000001452473632400152255ustar00rootroot00000000000000referencing-0.31.0/test-requirements.in000066400000000000000000000000561452473632400201250ustar00rootroot00000000000000file:.#egg=referencing pytest pytest-subtests referencing-0.31.0/test-requirements.txt000066400000000000000000000010431452473632400203330ustar00rootroot00000000000000# # 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