From 0ce3cef1ad37071b7bf37a919fc24498af88c831 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 28 Dec 2022 10:48:43 -0500 Subject: [PATCH 01/33] Drop support for 3.7. Referencing happens to not support it at the minute. It's close to EOL, so rather than adding it, I suspect it's droppable. --- .github/workflows/ci.yml | 12 ------------ pyproject.toml | 3 +-- tox.ini | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efb163bcc..06208ab4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,18 +38,6 @@ jobs: toxenv: pypy3-formatnongpl-build - name: pypy-3.9 toxenv: pypy3-formatnongpl-tests - - name: 3.7 - toxenv: py37-noextra-build - - name: 3.7 - toxenv: py37-noextra-tests - - name: 3.7 - toxenv: py37-format-build - - name: 3.7 - toxenv: py37-format-tests - - name: 3.7 - toxenv: py37-formatnongpl-build - - name: 3.7 - toxenv: py37-formatnongpl-tests - name: 3.8 toxenv: py38-noextra-build - name: 3.8 diff --git a/pyproject.toml b/pyproject.toml index 71f6d34de..8a873eff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ source = "vcs" [project] name = "jsonschema" description = "An implementation of JSON Schema validation for Python" -requires-python = ">=3.7" +requires-python = ">=3.8" license = {text = "MIT"} keywords = ["validation", "data validation", "jsonschema", "json"] authors = [ @@ -21,7 +21,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/tox.ini b/tox.ini index 61e6af874..b1464e5e6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] min_version = 4.0.9 envlist = - py{37,38,39,310,311,py3}-{noextra,format,formatnongpl}-{build,tests} + py{38,39,310,311,py3}-{noextra,format,formatnongpl}-{build,tests} {noextra,format,formatnongpl}-audit readme secrets From bf94d57b2b6d77cc8057be295f077435f4d4020d Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 28 Dec 2022 09:43:16 -0500 Subject: [PATCH 02/33] Move to retrieving schemas from the jsonschema-specifications registry. Still will be tweaked as the referencing library's public API changes. --- docs/requirements.txt | 86 ++++++--- jsonschema/_utils.py | 18 -- jsonschema/schemas/draft2019-09.json | 42 ----- jsonschema/schemas/draft2020-12.json | 58 ------ jsonschema/schemas/draft3.json | 172 ------------------ jsonschema/schemas/draft4.json | 149 --------------- jsonschema/schemas/draft6.json | 153 ---------------- jsonschema/schemas/draft7.json | 166 ----------------- .../vocabularies/draft2019-09/applicator | 56 ------ .../schemas/vocabularies/draft2019-09/content | 17 -- .../schemas/vocabularies/draft2019-09/core | 57 ------ .../vocabularies/draft2019-09/meta-data | 37 ---- .../vocabularies/draft2019-09/validation | 98 ---------- .../vocabularies/draft2020-12/applicator | 48 ----- .../schemas/vocabularies/draft2020-12/content | 17 -- .../schemas/vocabularies/draft2020-12/core | 51 ------ .../schemas/vocabularies/draft2020-12/format | 14 -- .../draft2020-12/format-annotation | 14 -- .../draft2020-12/format-assertion | 14 -- .../vocabularies/draft2020-12/meta-data | 37 ---- .../vocabularies/draft2020-12/unevaluated | 15 -- .../vocabularies/draft2020-12/validation | 98 ---------- jsonschema/validators.py | 37 ++-- pyproject.toml | 1 + 24 files changed, 85 insertions(+), 1370 deletions(-) delete mode 100644 jsonschema/schemas/draft2019-09.json delete mode 100644 jsonschema/schemas/draft2020-12.json delete mode 100644 jsonschema/schemas/draft3.json delete mode 100644 jsonschema/schemas/draft4.json delete mode 100644 jsonschema/schemas/draft6.json delete mode 100644 jsonschema/schemas/draft7.json delete mode 100644 jsonschema/schemas/vocabularies/draft2019-09/applicator delete mode 100644 jsonschema/schemas/vocabularies/draft2019-09/content delete mode 100644 jsonschema/schemas/vocabularies/draft2019-09/core delete mode 100644 jsonschema/schemas/vocabularies/draft2019-09/meta-data delete mode 100644 jsonschema/schemas/vocabularies/draft2019-09/validation delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/applicator delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/content delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/core delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/format delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/format-annotation delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/format-assertion delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/meta-data delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/unevaluated delete mode 100644 jsonschema/schemas/vocabularies/draft2020-12/validation diff --git a/docs/requirements.txt b/docs/requirements.txt index 48ee19baa..140c4bffe 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,29 +1,39 @@ # -# This file is autogenerated by pip-compile with python 3.11 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: # # pip-compile docs/requirements.in # -alabaster==0.7.12 +alabaster==0.7.13 # via sphinx -astroid==2.12.13 +astroid==2.14.1 # via sphinx-autoapi attrs==22.2.0 - # via jsonschema + # via + # jsonschema + # referencing babel==2.11.0 # via sphinx -beautifulsoup4==4.11.1 +beautifulsoup4==4.11.2 # via furo certifi==2022.12.7 # via requests -charset-normalizer==2.1.1 +charset-normalizer==3.0.1 # via requests +contourpy==1.0.7 + # via matplotlib +cycler==0.11.0 + # via matplotlib docutils==0.19 # via sphinx +fonttools==4.38.0 + # via matplotlib furo==2022.12.7 # via -r docs/requirements.in idna==3.4 - # via requests + # via + # requests + # yarl imagesize==1.4.1 # via sphinx jinja2==3.1.2 @@ -32,35 +42,61 @@ jinja2==3.1.2 # sphinx-autoapi file:.#egg=jsonschema # via -r docs/requirements.in -lazy-object-proxy==1.8.0 +jsonschema-specifications==2023.2.4 + # via jsonschema +kiwisolver==1.4.4 + # via matplotlib +lazy-object-proxy==1.9.0 # via astroid lxml==4.9.2 # via # -r docs/requirements.in # sphinx-json-schema-spec -markupsafe==2.1.1 +markupsafe==2.1.2 # via jinja2 -packaging==22.0 - # via sphinx +matplotlib==3.6.3 + # via sphinxext-opengraph +multidict==6.0.4 + # via yarl +numpy==1.24.2 + # via + # contourpy + # matplotlib +packaging==23.0 + # via + # matplotlib + # sphinx +pillow==9.4.0 + # via matplotlib pyenchant==3.2.2 # via sphinxcontrib-spelling -pygments==2.13.0 +pygments==2.14.0 # via # furo # sphinx -pyrsistent==0.19.2 - # via jsonschema -pytz==2022.7 +pyparsing==3.0.9 + # via matplotlib +pyrsistent==0.19.3 + # via + # jsonschema + # referencing +python-dateutil==2.8.2 + # via matplotlib +pytz==2022.7.1 # via babel pyyaml==6.0 # via sphinx-autoapi -requests==2.28.1 +referencing==0.14.2 + # via jsonschema-specifications +requests==2.28.2 # via sphinx +six==1.16.0 + # via python-dateutil snowballstemmer==2.2.0 # via sphinx soupsieve==2.3.2.post1 # via beautifulsoup4 -sphinx==5.3.0 +sphinx==6.1.3 # via # -r docs/requirements.in # furo @@ -71,9 +107,9 @@ sphinx==5.3.0 # sphinx-json-schema-spec # sphinxcontrib-spelling # sphinxext-opengraph -sphinx-autoapi==2.0.0 +sphinx-autoapi==2.0.1 # via -r docs/requirements.in -sphinx-autodoc-typehints==1.19.5 +sphinx-autodoc-typehints==1.22 # via -r docs/requirements.in sphinx-basic-ng==1.0.0b1 # via furo @@ -81,11 +117,11 @@ sphinx-copybutton==0.5.1 # via -r docs/requirements.in sphinx-json-schema-spec==2.3.3 # via -r docs/requirements.in -sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-applehelp==1.0.4 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-htmlhelp==2.0.1 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx @@ -95,11 +131,13 @@ sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxcontrib-spelling==7.7.0 # via -r docs/requirements.in -sphinxext-opengraph==0.7.4 +sphinxext-opengraph==0.8.1 # via -r docs/requirements.in unidecode==1.3.6 # via sphinx-autoapi -urllib3==1.26.13 +urllib3==1.26.14 # via requests wrapt==1.14.1 # via astroid +yarl==1.8.2 + # via referencing diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index 418348ce1..c218b184e 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -1,15 +1,7 @@ from collections.abc import Mapping, MutableMapping, Sequence from urllib.parse import urlsplit import itertools -import json import re -import sys - -# The files() API was added in Python 3.9. -if sys.version_info >= (3, 9): # pragma: no cover - from importlib import resources -else: # pragma: no cover - import importlib_resources as resources # type: ignore class URIDict(MutableMapping): @@ -52,16 +44,6 @@ def __repr__(self): return "" -def load_schema(name): - """ - Load a schema from ./schemas/``name``.json and return it. - """ - - path = resources.files(__package__).joinpath(f"schemas/{name}.json") - data = path.read_text(encoding="utf-8") - return json.loads(data) - - def format_as_index(container, indices): """ Construct a single string containing indexing operations for the indices. diff --git a/jsonschema/schemas/draft2019-09.json b/jsonschema/schemas/draft2019-09.json deleted file mode 100644 index 2248a0c80..000000000 --- a/jsonschema/schemas/draft2019-09.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://json-schema.org/draft/2019-09/schema", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true, - "https://json-schema.org/draft/2019-09/vocab/applicator": true, - "https://json-schema.org/draft/2019-09/vocab/validation": true, - "https://json-schema.org/draft/2019-09/vocab/meta-data": true, - "https://json-schema.org/draft/2019-09/vocab/format": false, - "https://json-schema.org/draft/2019-09/vocab/content": true - }, - "$recursiveAnchor": true, - - "title": "Core and Validation specifications meta-schema", - "allOf": [ - {"$ref": "meta/core"}, - {"$ref": "meta/applicator"}, - {"$ref": "meta/validation"}, - {"$ref": "meta/meta-data"}, - {"$ref": "meta/format"}, - {"$ref": "meta/content"} - ], - "type": ["object", "boolean"], - "properties": { - "definitions": { - "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", - "type": "object", - "additionalProperties": { "$recursiveRef": "#" }, - "default": {} - }, - "dependencies": { - "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", - "type": "object", - "additionalProperties": { - "anyOf": [ - { "$recursiveRef": "#" }, - { "$ref": "meta/validation#/$defs/stringArray" } - ] - } - } - } -} diff --git a/jsonschema/schemas/draft2020-12.json b/jsonschema/schemas/draft2020-12.json deleted file mode 100644 index d5e2d31c3..000000000 --- a/jsonschema/schemas/draft2020-12.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/schema", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true, - "https://json-schema.org/draft/2020-12/vocab/applicator": true, - "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, - "https://json-schema.org/draft/2020-12/vocab/validation": true, - "https://json-schema.org/draft/2020-12/vocab/meta-data": true, - "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, - "https://json-schema.org/draft/2020-12/vocab/content": true - }, - "$dynamicAnchor": "meta", - - "title": "Core and Validation specifications meta-schema", - "allOf": [ - {"$ref": "meta/core"}, - {"$ref": "meta/applicator"}, - {"$ref": "meta/unevaluated"}, - {"$ref": "meta/validation"}, - {"$ref": "meta/meta-data"}, - {"$ref": "meta/format-annotation"}, - {"$ref": "meta/content"} - ], - "type": ["object", "boolean"], - "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", - "properties": { - "definitions": { - "$comment": "\"definitions\" has been replaced by \"$defs\".", - "type": "object", - "additionalProperties": { "$dynamicRef": "#meta" }, - "deprecated": true, - "default": {} - }, - "dependencies": { - "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", - "type": "object", - "additionalProperties": { - "anyOf": [ - { "$dynamicRef": "#meta" }, - { "$ref": "meta/validation#/$defs/stringArray" } - ] - }, - "deprecated": true, - "default": {} - }, - "$recursiveAnchor": { - "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", - "$ref": "meta/core#/$defs/anchorString", - "deprecated": true - }, - "$recursiveRef": { - "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", - "$ref": "meta/core#/$defs/uriReferenceString", - "deprecated": true - } - } -} diff --git a/jsonschema/schemas/draft3.json b/jsonschema/schemas/draft3.json deleted file mode 100644 index 8b26b1f89..000000000 --- a/jsonschema/schemas/draft3.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "$schema" : "http://json-schema.org/draft-03/schema#", - "id" : "http://json-schema.org/draft-03/schema#", - "type" : "object", - - "properties" : { - "type" : { - "type" : ["string", "array"], - "items" : { - "type" : ["string", {"$ref" : "#"}] - }, - "uniqueItems" : true, - "default" : "any" - }, - - "properties" : { - "type" : "object", - "additionalProperties" : {"$ref" : "#"}, - "default" : {} - }, - - "patternProperties" : { - "type" : "object", - "additionalProperties" : {"$ref" : "#"}, - "default" : {} - }, - - "additionalProperties" : { - "type" : [{"$ref" : "#"}, "boolean"], - "default" : {} - }, - - "items" : { - "type" : [{"$ref" : "#"}, "array"], - "items" : {"$ref" : "#"}, - "default" : {} - }, - - "additionalItems" : { - "type" : [{"$ref" : "#"}, "boolean"], - "default" : {} - }, - - "required" : { - "type" : "boolean", - "default" : false - }, - - "dependencies" : { - "type" : "object", - "additionalProperties" : { - "type" : ["string", "array", {"$ref" : "#"}], - "items" : { - "type" : "string" - } - }, - "default" : {} - }, - - "minimum" : { - "type" : "number" - }, - - "maximum" : { - "type" : "number" - }, - - "exclusiveMinimum" : { - "type" : "boolean", - "default" : false - }, - - "exclusiveMaximum" : { - "type" : "boolean", - "default" : false - }, - - "minItems" : { - "type" : "integer", - "minimum" : 0, - "default" : 0 - }, - - "maxItems" : { - "type" : "integer", - "minimum" : 0 - }, - - "uniqueItems" : { - "type" : "boolean", - "default" : false - }, - - "pattern" : { - "type" : "string", - "format" : "regex" - }, - - "minLength" : { - "type" : "integer", - "minimum" : 0, - "default" : 0 - }, - - "maxLength" : { - "type" : "integer" - }, - - "enum" : { - "type" : "array", - "minItems" : 1, - "uniqueItems" : true - }, - - "default" : { - "type" : "any" - }, - - "title" : { - "type" : "string" - }, - - "description" : { - "type" : "string" - }, - - "format" : { - "type" : "string" - }, - - "divisibleBy" : { - "type" : "number", - "minimum" : 0, - "exclusiveMinimum" : true, - "default" : 1 - }, - - "disallow" : { - "type" : ["string", "array"], - "items" : { - "type" : ["string", {"$ref" : "#"}] - }, - "uniqueItems" : true - }, - - "extends" : { - "type" : [{"$ref" : "#"}, "array"], - "items" : {"$ref" : "#"}, - "default" : {} - }, - - "id" : { - "type" : "string" - }, - - "$ref" : { - "type" : "string" - }, - - "$schema" : { - "type" : "string", - "format" : "uri" - } - }, - - "dependencies" : { - "exclusiveMinimum" : "minimum", - "exclusiveMaximum" : "maximum" - }, - - "default" : {} -} diff --git a/jsonschema/schemas/draft4.json b/jsonschema/schemas/draft4.json deleted file mode 100644 index bcbb84743..000000000 --- a/jsonschema/schemas/draft4.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "id": "http://json-schema.org/draft-04/schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#" } - }, - "positiveInteger": { - "type": "integer", - "minimum": 0 - }, - "positiveIntegerDefault0": { - "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] - }, - "simpleTypes": { - "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] - }, - "stringArray": { - "type": "array", - "items": { "type": "string" }, - "minItems": 1, - "uniqueItems": true - } - }, - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "$schema": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": {}, - "multipleOf": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "maxLength": { "$ref": "#/definitions/positiveInteger" }, - "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "anyOf": [ - { "type": "boolean" }, - { "$ref": "#" } - ], - "default": {} - }, - "items": { - "anyOf": [ - { "$ref": "#" }, - { "$ref": "#/definitions/schemaArray" } - ], - "default": {} - }, - "maxItems": { "$ref": "#/definitions/positiveInteger" }, - "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxProperties": { "$ref": "#/definitions/positiveInteger" }, - "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, - "required": { "$ref": "#/definitions/stringArray" }, - "additionalProperties": { - "anyOf": [ - { "type": "boolean" }, - { "$ref": "#" } - ], - "default": {} - }, - "definitions": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { "$ref": "#" }, - { "$ref": "#/definitions/stringArray" } - ] - } - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - { "$ref": "#/definitions/simpleTypes" }, - { - "type": "array", - "items": { "$ref": "#/definitions/simpleTypes" }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "format": { "type": "string" }, - "allOf": { "$ref": "#/definitions/schemaArray" }, - "anyOf": { "$ref": "#/definitions/schemaArray" }, - "oneOf": { "$ref": "#/definitions/schemaArray" }, - "not": { "$ref": "#" } - }, - "dependencies": { - "exclusiveMaximum": [ "maximum" ], - "exclusiveMinimum": [ "minimum" ] - }, - "default": {} -} diff --git a/jsonschema/schemas/draft6.json b/jsonschema/schemas/draft6.json deleted file mode 100644 index a0d2bf789..000000000 --- a/jsonschema/schemas/draft6.json +++ /dev/null @@ -1,153 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-06/schema#", - "$id": "http://json-schema.org/draft-06/schema#", - "title": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#" } - }, - "nonNegativeInteger": { - "type": "integer", - "minimum": 0 - }, - "nonNegativeIntegerDefault0": { - "allOf": [ - { "$ref": "#/definitions/nonNegativeInteger" }, - { "default": 0 } - ] - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { "type": "string" }, - "uniqueItems": true, - "default": [] - } - }, - "type": ["object", "boolean"], - "properties": { - "$id": { - "type": "string", - "format": "uri-reference" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "$ref": { - "type": "string", - "format": "uri-reference" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": {}, - "examples": { - "type": "array", - "items": {} - }, - "multipleOf": { - "type": "number", - "exclusiveMinimum": 0 - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "number" - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "number" - }, - "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, - "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { "$ref": "#" }, - "items": { - "anyOf": [ - { "$ref": "#" }, - { "$ref": "#/definitions/schemaArray" } - ], - "default": {} - }, - "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, - "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "contains": { "$ref": "#" }, - "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, - "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, - "required": { "$ref": "#/definitions/stringArray" }, - "additionalProperties": { "$ref": "#" }, - "definitions": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "propertyNames": { "format": "regex" }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { "$ref": "#" }, - { "$ref": "#/definitions/stringArray" } - ] - } - }, - "propertyNames": { "$ref": "#" }, - "const": {}, - "enum": { - "type": "array" - }, - "type": { - "anyOf": [ - { "$ref": "#/definitions/simpleTypes" }, - { - "type": "array", - "items": { "$ref": "#/definitions/simpleTypes" }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "format": { "type": "string" }, - "allOf": { "$ref": "#/definitions/schemaArray" }, - "anyOf": { "$ref": "#/definitions/schemaArray" }, - "oneOf": { "$ref": "#/definitions/schemaArray" }, - "not": { "$ref": "#" } - }, - "default": {} -} diff --git a/jsonschema/schemas/draft7.json b/jsonschema/schemas/draft7.json deleted file mode 100644 index 746cde969..000000000 --- a/jsonschema/schemas/draft7.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://json-schema.org/draft-07/schema#", - "title": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#" } - }, - "nonNegativeInteger": { - "type": "integer", - "minimum": 0 - }, - "nonNegativeIntegerDefault0": { - "allOf": [ - { "$ref": "#/definitions/nonNegativeInteger" }, - { "default": 0 } - ] - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { "type": "string" }, - "uniqueItems": true, - "default": [] - } - }, - "type": ["object", "boolean"], - "properties": { - "$id": { - "type": "string", - "format": "uri-reference" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "$ref": { - "type": "string", - "format": "uri-reference" - }, - "$comment": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": true, - "readOnly": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": true - }, - "multipleOf": { - "type": "number", - "exclusiveMinimum": 0 - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "number" - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "number" - }, - "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, - "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { "$ref": "#" }, - "items": { - "anyOf": [ - { "$ref": "#" }, - { "$ref": "#/definitions/schemaArray" } - ], - "default": true - }, - "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, - "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "contains": { "$ref": "#" }, - "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, - "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, - "required": { "$ref": "#/definitions/stringArray" }, - "additionalProperties": { "$ref": "#" }, - "definitions": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { "$ref": "#" }, - "propertyNames": { "format": "regex" }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { "$ref": "#" }, - { "$ref": "#/definitions/stringArray" } - ] - } - }, - "propertyNames": { "$ref": "#" }, - "const": true, - "enum": { - "type": "array", - "items": true - }, - "type": { - "anyOf": [ - { "$ref": "#/definitions/simpleTypes" }, - { - "type": "array", - "items": { "$ref": "#/definitions/simpleTypes" }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "format": { "type": "string" }, - "contentMediaType": { "type": "string" }, - "contentEncoding": { "type": "string" }, - "if": {"$ref": "#"}, - "then": {"$ref": "#"}, - "else": {"$ref": "#"}, - "allOf": { "$ref": "#/definitions/schemaArray" }, - "anyOf": { "$ref": "#/definitions/schemaArray" }, - "oneOf": { "$ref": "#/definitions/schemaArray" }, - "not": { "$ref": "#" } - }, - "default": true -} diff --git a/jsonschema/schemas/vocabularies/draft2019-09/applicator b/jsonschema/schemas/vocabularies/draft2019-09/applicator deleted file mode 100644 index 24a1cc4f4..000000000 --- a/jsonschema/schemas/vocabularies/draft2019-09/applicator +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://json-schema.org/draft/2019-09/meta/applicator", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/applicator": true - }, - "$recursiveAnchor": true, - - "title": "Applicator vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "additionalItems": { "$recursiveRef": "#" }, - "unevaluatedItems": { "$recursiveRef": "#" }, - "items": { - "anyOf": [ - { "$recursiveRef": "#" }, - { "$ref": "#/$defs/schemaArray" } - ] - }, - "contains": { "$recursiveRef": "#" }, - "additionalProperties": { "$recursiveRef": "#" }, - "unevaluatedProperties": { "$recursiveRef": "#" }, - "properties": { - "type": "object", - "additionalProperties": { "$recursiveRef": "#" }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { "$recursiveRef": "#" }, - "propertyNames": { "format": "regex" }, - "default": {} - }, - "dependentSchemas": { - "type": "object", - "additionalProperties": { - "$recursiveRef": "#" - } - }, - "propertyNames": { "$recursiveRef": "#" }, - "if": { "$recursiveRef": "#" }, - "then": { "$recursiveRef": "#" }, - "else": { "$recursiveRef": "#" }, - "allOf": { "$ref": "#/$defs/schemaArray" }, - "anyOf": { "$ref": "#/$defs/schemaArray" }, - "oneOf": { "$ref": "#/$defs/schemaArray" }, - "not": { "$recursiveRef": "#" } - }, - "$defs": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { "$recursiveRef": "#" } - } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2019-09/content b/jsonschema/schemas/vocabularies/draft2019-09/content deleted file mode 100644 index f6752a8ef..000000000 --- a/jsonschema/schemas/vocabularies/draft2019-09/content +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://json-schema.org/draft/2019-09/meta/content", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/content": true - }, - "$recursiveAnchor": true, - - "title": "Content vocabulary meta-schema", - - "type": ["object", "boolean"], - "properties": { - "contentMediaType": { "type": "string" }, - "contentEncoding": { "type": "string" }, - "contentSchema": { "$recursiveRef": "#" } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2019-09/core b/jsonschema/schemas/vocabularies/draft2019-09/core deleted file mode 100644 index eb708a560..000000000 --- a/jsonschema/schemas/vocabularies/draft2019-09/core +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, - "$recursiveAnchor": true, - - "title": "Core vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "$id": { - "type": "string", - "format": "uri-reference", - "$comment": "Non-empty fragments not allowed.", - "pattern": "^[^#]*#?$" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "$anchor": { - "type": "string", - "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" - }, - "$ref": { - "type": "string", - "format": "uri-reference" - }, - "$recursiveRef": { - "type": "string", - "format": "uri-reference" - }, - "$recursiveAnchor": { - "type": "boolean", - "default": false - }, - "$vocabulary": { - "type": "object", - "propertyNames": { - "type": "string", - "format": "uri" - }, - "additionalProperties": { - "type": "boolean" - } - }, - "$comment": { - "type": "string" - }, - "$defs": { - "type": "object", - "additionalProperties": { "$recursiveRef": "#" }, - "default": {} - } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2019-09/meta-data b/jsonschema/schemas/vocabularies/draft2019-09/meta-data deleted file mode 100644 index da04cff6d..000000000 --- a/jsonschema/schemas/vocabularies/draft2019-09/meta-data +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://json-schema.org/draft/2019-09/meta/meta-data", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/meta-data": true - }, - "$recursiveAnchor": true, - - "title": "Meta-data vocabulary meta-schema", - - "type": ["object", "boolean"], - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": true, - "deprecated": { - "type": "boolean", - "default": false - }, - "readOnly": { - "type": "boolean", - "default": false - }, - "writeOnly": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": true - } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2019-09/validation b/jsonschema/schemas/vocabularies/draft2019-09/validation deleted file mode 100644 index 9f59677b3..000000000 --- a/jsonschema/schemas/vocabularies/draft2019-09/validation +++ /dev/null @@ -1,98 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://json-schema.org/draft/2019-09/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/validation": true - }, - "$recursiveAnchor": true, - - "title": "Validation vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "multipleOf": { - "type": "number", - "exclusiveMinimum": 0 - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "number" - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "number" - }, - "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, - "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, - "pattern": { - "type": "string", - "format": "regex" - }, - "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, - "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, - "minContains": { - "$ref": "#/$defs/nonNegativeInteger", - "default": 1 - }, - "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, - "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, - "required": { "$ref": "#/$defs/stringArray" }, - "dependentRequired": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/stringArray" - } - }, - "const": true, - "enum": { - "type": "array", - "items": true - }, - "type": { - "anyOf": [ - { "$ref": "#/$defs/simpleTypes" }, - { - "type": "array", - "items": { "$ref": "#/$defs/simpleTypes" }, - "minItems": 1, - "uniqueItems": true - } - ] - } - }, - "$defs": { - "nonNegativeInteger": { - "type": "integer", - "minimum": 0 - }, - "nonNegativeIntegerDefault0": { - "$ref": "#/$defs/nonNegativeInteger", - "default": 0 - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { "type": "string" }, - "uniqueItems": true, - "default": [] - } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/applicator b/jsonschema/schemas/vocabularies/draft2020-12/applicator deleted file mode 100644 index ca6992309..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/applicator +++ /dev/null @@ -1,48 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/applicator", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/applicator": true - }, - "$dynamicAnchor": "meta", - - "title": "Applicator vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "prefixItems": { "$ref": "#/$defs/schemaArray" }, - "items": { "$dynamicRef": "#meta" }, - "contains": { "$dynamicRef": "#meta" }, - "additionalProperties": { "$dynamicRef": "#meta" }, - "properties": { - "type": "object", - "additionalProperties": { "$dynamicRef": "#meta" }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { "$dynamicRef": "#meta" }, - "propertyNames": { "format": "regex" }, - "default": {} - }, - "dependentSchemas": { - "type": "object", - "additionalProperties": { "$dynamicRef": "#meta" }, - "default": {} - }, - "propertyNames": { "$dynamicRef": "#meta" }, - "if": { "$dynamicRef": "#meta" }, - "then": { "$dynamicRef": "#meta" }, - "else": { "$dynamicRef": "#meta" }, - "allOf": { "$ref": "#/$defs/schemaArray" }, - "anyOf": { "$ref": "#/$defs/schemaArray" }, - "oneOf": { "$ref": "#/$defs/schemaArray" }, - "not": { "$dynamicRef": "#meta" } - }, - "$defs": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { "$dynamicRef": "#meta" } - } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/content b/jsonschema/schemas/vocabularies/draft2020-12/content deleted file mode 100644 index 2f6e056a9..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/content +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/content", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/content": true - }, - "$dynamicAnchor": "meta", - - "title": "Content vocabulary meta-schema", - - "type": ["object", "boolean"], - "properties": { - "contentEncoding": { "type": "string" }, - "contentMediaType": { "type": "string" }, - "contentSchema": { "$dynamicRef": "#meta" } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/core b/jsonschema/schemas/vocabularies/draft2020-12/core deleted file mode 100644 index dfc092d96..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/core +++ /dev/null @@ -1,51 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, - "$dynamicAnchor": "meta", - - "title": "Core vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "$id": { - "$ref": "#/$defs/uriReferenceString", - "$comment": "Non-empty fragments not allowed.", - "pattern": "^[^#]*#?$" - }, - "$schema": { "$ref": "#/$defs/uriString" }, - "$ref": { "$ref": "#/$defs/uriReferenceString" }, - "$anchor": { "$ref": "#/$defs/anchorString" }, - "$dynamicRef": { "$ref": "#/$defs/uriReferenceString" }, - "$dynamicAnchor": { "$ref": "#/$defs/anchorString" }, - "$vocabulary": { - "type": "object", - "propertyNames": { "$ref": "#/$defs/uriString" }, - "additionalProperties": { - "type": "boolean" - } - }, - "$comment": { - "type": "string" - }, - "$defs": { - "type": "object", - "additionalProperties": { "$dynamicRef": "#meta" } - } - }, - "$defs": { - "anchorString": { - "type": "string", - "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$" - }, - "uriString": { - "type": "string", - "format": "uri" - }, - "uriReferenceString": { - "type": "string", - "format": "uri-reference" - } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/format b/jsonschema/schemas/vocabularies/draft2020-12/format deleted file mode 100644 index 09bbfdda9..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/format +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://json-schema.org/draft/2019-09/meta/format", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/format": true - }, - "$recursiveAnchor": true, - - "title": "Format vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "format": { "type": "string" } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/format-annotation b/jsonschema/schemas/vocabularies/draft2020-12/format-annotation deleted file mode 100644 index 51ef7ea11..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/format-annotation +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/format-annotation": true - }, - "$dynamicAnchor": "meta", - - "title": "Format vocabulary meta-schema for annotation results", - "type": ["object", "boolean"], - "properties": { - "format": { "type": "string" } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/format-assertion b/jsonschema/schemas/vocabularies/draft2020-12/format-assertion deleted file mode 100644 index 5e73fd757..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/format-assertion +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/format-assertion", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/format-assertion": true - }, - "$dynamicAnchor": "meta", - - "title": "Format vocabulary meta-schema for assertion results", - "type": ["object", "boolean"], - "properties": { - "format": { "type": "string" } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/meta-data b/jsonschema/schemas/vocabularies/draft2020-12/meta-data deleted file mode 100644 index 05cbc22af..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/meta-data +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/meta-data", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/meta-data": true - }, - "$dynamicAnchor": "meta", - - "title": "Meta-data vocabulary meta-schema", - - "type": ["object", "boolean"], - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": true, - "deprecated": { - "type": "boolean", - "default": false - }, - "readOnly": { - "type": "boolean", - "default": false - }, - "writeOnly": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": true - } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/unevaluated b/jsonschema/schemas/vocabularies/draft2020-12/unevaluated deleted file mode 100644 index 5f62a3ffa..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/unevaluated +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/unevaluated": true - }, - "$dynamicAnchor": "meta", - - "title": "Unevaluated applicator vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "unevaluatedItems": { "$dynamicRef": "#meta" }, - "unevaluatedProperties": { "$dynamicRef": "#meta" } - } -} diff --git a/jsonschema/schemas/vocabularies/draft2020-12/validation b/jsonschema/schemas/vocabularies/draft2020-12/validation deleted file mode 100644 index 606b87ba2..000000000 --- a/jsonschema/schemas/vocabularies/draft2020-12/validation +++ /dev/null @@ -1,98 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, - "$dynamicAnchor": "meta", - - "title": "Validation vocabulary meta-schema", - "type": ["object", "boolean"], - "properties": { - "type": { - "anyOf": [ - { "$ref": "#/$defs/simpleTypes" }, - { - "type": "array", - "items": { "$ref": "#/$defs/simpleTypes" }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "const": true, - "enum": { - "type": "array", - "items": true - }, - "multipleOf": { - "type": "number", - "exclusiveMinimum": 0 - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "number" - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "number" - }, - "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, - "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, - "pattern": { - "type": "string", - "format": "regex" - }, - "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, - "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, - "minContains": { - "$ref": "#/$defs/nonNegativeInteger", - "default": 1 - }, - "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, - "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, - "required": { "$ref": "#/$defs/stringArray" }, - "dependentRequired": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/stringArray" - } - } - }, - "$defs": { - "nonNegativeInteger": { - "type": "integer", - "minimum": 0 - }, - "nonNegativeIntegerDefault0": { - "$ref": "#/$defs/nonNegativeInteger", - "default": 0 - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { "type": "string" }, - "uniqueItems": true, - "default": [] - } - } -} diff --git a/jsonschema/validators.py b/jsonschema/validators.py index d370dc5b6..a9769599c 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -13,9 +13,9 @@ import contextlib import json import reprlib -import typing import warnings +from jsonschema_specifications import REGISTRY as SPECIFICATIONS from pyrsistent import m import attr @@ -33,7 +33,6 @@ _VALIDATORS: dict[str, Validator] = {} _META_SCHEMAS = _utils.URIDict() -_VOCABULARIES: list[tuple[str, typing.Any]] = [] def __getattr__(name): @@ -103,15 +102,11 @@ def _id_of(schema): def _store_schema_list(): - if not _VOCABULARIES: - package = _utils.resources.files(__package__) - for version in package.joinpath("schemas", "vocabularies").iterdir(): - for path in version.iterdir(): - vocabulary = json.loads(path.read_text()) - _VOCABULARIES.append((vocabulary["$id"], vocabulary)) return [ + (uri, each.contents) for uri, each in SPECIFICATIONS.items() + ] + [ (id, validator.META_SCHEMA) for id, validator in _META_SCHEMAS.items() - ] + _VOCABULARIES + ] def create( @@ -430,7 +425,9 @@ def extend( Draft3Validator = create( - meta_schema=_utils.load_schema("draft3"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-03/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -462,7 +459,9 @@ def extend( ) Draft4Validator = create( - meta_schema=_utils.load_schema("draft4"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-04/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -499,7 +498,9 @@ def extend( ) Draft6Validator = create( - meta_schema=_utils.load_schema("draft6"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-06/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -541,7 +542,9 @@ def extend( ) Draft7Validator = create( - meta_schema=_utils.load_schema("draft7"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-07/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -584,7 +587,9 @@ def extend( ) Draft201909Validator = create( - meta_schema=_utils.load_schema("draft2019-09"), + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2019-09/schema", + ), validators={ "$recursiveRef": _legacy_validators.recursiveRef, "$ref": _validators.ref, @@ -629,7 +634,9 @@ def extend( ) Draft202012Validator = create( - meta_schema=_utils.load_schema("draft2020-12"), + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2020-12/schema", + ), validators={ "$dynamicRef": _validators.dynamicRef, "$ref": _validators.ref, diff --git a/pyproject.toml b/pyproject.toml index 8a873eff3..a5ce83256 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dynamic = ["version", "readme"] dependencies = [ "attrs>=17.4.0", "pyrsistent>=0.14.0,!=0.17.0,!=0.17.1,!=0.17.2", + "jsonschema-specifications>=2023.02.4", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From 238e7111ecb012c4674b55a02a3ad54d4ae25a36 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 29 Dec 2022 10:58:11 -0500 Subject: [PATCH 03/33] Deprecate jsonschema.RefResolver from both places it is importable. Internal uses of it will be removed, replaced with referencing's resolution APIs, though RefResolver will continue to function during its deprecation period. --- CHANGELOG.rst | 7 ++++++ docs/api/jsonschema/validators/index.rst | 1 + docs/faq.rst | 9 +++---- jsonschema/__init__.py | 9 ++++++- jsonschema/cli.py | 4 +-- jsonschema/protocols.py | 2 +- jsonschema/tests/_suite.py | 4 +-- jsonschema/tests/test_deprecations.py | 2 +- jsonschema/tests/test_validators.py | 32 ++++++++++++------------ jsonschema/validators.py | 26 ++++++++++++++++--- 10 files changed, 64 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 69edbd4a2..66d689df6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,10 @@ +v4.18.0 +======= + +* ``jsonschema.RefResolver`` is now deprecated in favor of the new `referencing library `_. + ``referencing`` will begin in beta, but already is more compliant than the existing ``$ref`` support. + Please file issues on the ``referencing`` tracker if there is functionality missing from it. + v4.17.3 ======= diff --git a/docs/api/jsonschema/validators/index.rst b/docs/api/jsonschema/validators/index.rst index f5cf82ca6..13a9991ce 100644 --- a/docs/api/jsonschema/validators/index.rst +++ b/docs/api/jsonschema/validators/index.rst @@ -4,3 +4,4 @@ .. automodule:: jsonschema.validators :members: :undoc-members: + :private-members: _RefResolver diff --git a/docs/faq.rst b/docs/faq.rst index 4dc1c5b61..2236390c2 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -85,7 +85,7 @@ The JSON object ``{}`` is simply the Python `dict` ``{}``, and a JSON Schema lik The :kw:`$ref` keyword is a single notable exception. - Specifically, in the case where `jsonschema` is asked to `resolve a remote reference `, it has no choice but to assume that the remote reference is serialized as JSON, and to deserialize it using the `json` module. + Specifically, in the case where `jsonschema` is asked to resolve a remote reference, it has no choice but to assume that the remote reference is serialized as JSON, and to deserialize it using the `json` module. One cannot today therefore reference some remote piece of YAML and have it deserialized into Python objects by this library without doing some additional work. @@ -104,12 +104,9 @@ How do I configure a base URI for $ref resolution using local files? `jsonschema` supports loading schemas from the filesystem. -The most common mistake when configuring a `jsonschema.validators.RefResolver` -to retrieve schemas from the local filesystem is to give it a base URI -which points to a directory, but forget to add a trailing slash. +The most common mistake when configuring reference resolution to retrieve schemas from the local filesystem is to specify a base URI which points to a directory, but forget to add a trailing slash. -For example, given a directory ``/tmp/foo/`` with ``bar/schema.json`` -within it, you should use something like: +For example, given a directory ``/tmp/foo/`` with ``bar/schema.json`` within it, you should use something like: .. code-block:: python diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py index 6628fc7eb..8f6b0a499 100644 --- a/jsonschema/__init__.py +++ b/jsonschema/__init__.py @@ -27,7 +27,6 @@ Draft7Validator, Draft201909Validator, Draft202012Validator, - RefResolver, validate, ) @@ -48,6 +47,14 @@ def __getattr__(name): import importlib_metadata as metadata return metadata.version("jsonschema") + elif name == "RefResolver": + from jsonschema.validators import _RefResolver + warnings.warn( + _RefResolver._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolver format_checkers = { "draft3_format_checker": Draft3Validator, diff --git a/jsonschema/cli.py b/jsonschema/cli.py index 1292b1af8..d6b9ad98e 100644 --- a/jsonschema/cli.py +++ b/jsonschema/cli.py @@ -23,7 +23,7 @@ import attr from jsonschema.exceptions import SchemaError -from jsonschema.validators import RefResolver, validator_for +from jsonschema.validators import _RefResolver, validator_for warnings.warn( ( @@ -277,7 +277,7 @@ def load(_): raise _CannotLoadFile() instances = [""] - resolver = RefResolver( + resolver = _RefResolver( base_uri=arguments["base_uri"], referrer=schema, ) if arguments["base_uri"] is not None else None diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index 5f52166fa..c9c71dc09 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -108,7 +108,7 @@ class Validator(Protocol): def __init__( self, schema: Mapping | bool, - resolver: jsonschema.validators.RefResolver | None = None, + resolver: jsonschema.validators._RefResolver | None = None, format_checker: jsonschema.FormatChecker | None = None, ) -> None: ... diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py index c598e22fc..d425e4008 100644 --- a/jsonschema/tests/_suite.py +++ b/jsonschema/tests/_suite.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: import pyperf -from jsonschema.validators import _VALIDATORS +from jsonschema.validators import _VALIDATORS, _RefResolver import jsonschema _DELIMITERS = re.compile(r"[\W\- ]+") @@ -218,7 +218,7 @@ def fn(this): def validate(self, Validator, **kwargs): Validator.check_schema(self.schema) - resolver = jsonschema.RefResolver.from_schema( + resolver = _RefResolver.from_schema( schema=self.schema, store=self._remotes, id_of=Validator.ID_OF, diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py index 3e8a9cc47..898a792f7 100644 --- a/jsonschema/tests/test_deprecations.py +++ b/jsonschema/tests/test_deprecations.py @@ -77,7 +77,7 @@ def test_RefResolver_in_scope(self): As of v4.0.0, RefResolver.in_scope is deprecated. """ - resolver = validators.RefResolver.from_schema({}) + resolver = validators._RefResolver.from_schema({}) with self.assertWarns(DeprecationWarning) as w: with resolver.in_scope("foo"): pass diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 0bc60deaf..6e37f7b94 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1189,7 +1189,7 @@ def test_ref(self): ref, schema = "someRef", {"additionalProperties": {"type": "integer"}} validator = validators.Draft7Validator( {"$ref": ref}, - resolver=validators.RefResolver("", {}, store={ref: schema}), + resolver=validators._RefResolver("", {}, store={ref: schema}), ) error, = validator.iter_errors({"foo": "notAnInteger"}) @@ -1542,12 +1542,12 @@ def test_non_existent_properties_are_ignored(self): def test_it_creates_a_ref_resolver_if_not_provided(self): self.assertIsInstance( self.Validator({}).resolver, - validators.RefResolver, + validators._RefResolver, ) def test_it_delegates_to_a_ref_resolver(self): ref, schema = "someCoolRef", {"type": "integer"} - resolver = validators.RefResolver("", {}, store={ref: schema}) + resolver = validators._RefResolver("", {}, store={ref: schema}) validator = self.Validator({"$ref": ref}, resolver=resolver) with self.assertRaises(exceptions.ValidationError): @@ -1555,7 +1555,7 @@ def test_it_delegates_to_a_ref_resolver(self): def test_evolve(self): ref, schema = "someCoolRef", {"type": "integer"} - resolver = validators.RefResolver("", {}, store={ref: schema}) + resolver = validators._RefResolver("", {}, store={ref: schema}) validator = self.Validator(schema, resolver=resolver) new = validator.evolve(schema={"type": "string"}) @@ -1784,14 +1784,14 @@ def test_False_is_not_a_schema(self): @unittest.skip(bug(523)) def test_True_is_not_a_schema_even_if_you_forget_to_check(self): - resolver = validators.RefResolver("", {}) + resolver = validators._RefResolver("", {}) with self.assertRaises(Exception) as e: self.Validator(True, resolver=resolver).validate(12) self.assertNotIsInstance(e.exception, exceptions.ValidationError) @unittest.skip(bug(523)) def test_False_is_not_a_schema_even_if_you_forget_to_check(self): - resolver = validators.RefResolver("", {}) + resolver = validators._RefResolver("", {}) with self.assertRaises(Exception) as e: self.Validator(False, resolver=resolver).validate(12) self.assertNotIsInstance(e.exception, exceptions.ValidationError) @@ -1867,7 +1867,7 @@ class TestLatestValidator(TestCase): def test_ref_resolvers_may_have_boolean_schemas_stored(self): ref = "someCoolRef" schema = {"$ref": ref} - resolver = validators.RefResolver("", {}, store={ref: False}) + resolver = validators._RefResolver("", {}, store={ref: False}) validator = validators._LATEST_VERSION(schema, resolver=resolver) with self.assertRaises(exceptions.ValidationError): @@ -2123,7 +2123,7 @@ class TestRefResolver(TestCase): def setUp(self): self.referrer = {} self.store = {self.stored_uri: self.stored_schema} - self.resolver = validators.RefResolver( + self.resolver = validators._RefResolver( self.base_uri, self.referrer, self.store, ) @@ -2143,7 +2143,7 @@ def test_it_resolves_local_refs(self): def test_it_resolves_local_refs_with_id(self): schema = {"id": "http://bar/schema#", "a": {"foo": "bar"}} - resolver = validators.RefResolver.from_schema( + resolver = validators._RefResolver.from_schema( schema, id_of=lambda schema: schema.get("id", ""), ) @@ -2206,7 +2206,7 @@ def test_it_retrieves_local_refs_via_urlopen(self): def test_it_can_construct_a_base_uri_from_a_schema(self): schema = {"id": "foo"} - resolver = validators.RefResolver.from_schema( + resolver = validators._RefResolver.from_schema( schema, id_of=lambda schema: schema.get("id", ""), ) @@ -2223,7 +2223,7 @@ def test_it_can_construct_a_base_uri_from_a_schema(self): def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): schema = {} - resolver = validators.RefResolver.from_schema(schema) + resolver = validators._RefResolver.from_schema(schema) self.assertEqual(resolver.base_uri, "") self.assertEqual(resolver.resolution_scope, "") with resolver.resolving("") as resolved: @@ -2238,7 +2238,7 @@ def handler(url): schema = {"foo": "bar"} ref = "foo://bar" - resolver = validators.RefResolver("", {}, handlers={"foo": handler}) + resolver = validators._RefResolver("", {}, handlers={"foo": handler}) with resolver.resolving(ref) as resolved: self.assertEqual(resolved, schema) @@ -2252,7 +2252,7 @@ def handler(url): self.fail("Response must not have been cached!") ref = "foo://bar" - resolver = validators.RefResolver( + resolver = validators._RefResolver( "", {}, cache_remote=True, handlers={"foo": handler}, ) with resolver.resolving(ref): @@ -2270,7 +2270,7 @@ def handler(url): self.fail("Handler called twice!") ref = "foo://bar" - resolver = validators.RefResolver( + resolver = validators._RefResolver( "", {}, cache_remote=False, handlers={"foo": handler}, ) with resolver.resolving(ref): @@ -2283,14 +2283,14 @@ def handler(url): raise error ref = "foo://bar" - resolver = validators.RefResolver("", {}, handlers={"foo": handler}) + resolver = validators._RefResolver("", {}, handlers={"foo": handler}) with self.assertRaises(exceptions.RefResolutionError) as err: with resolver.resolving(ref): self.fail("Shouldn't get this far!") # pragma: no cover self.assertEqual(err.exception, exceptions.RefResolutionError(error)) def test_helpful_error_message_on_failed_pop_scope(self): - resolver = validators.RefResolver("", {}) + resolver = validators._RefResolver("", {}) resolver.pop_scope() with self.assertRaises(exceptions.RefResolutionError) as exc: resolver.pop_scope() diff --git a/jsonschema/validators.py b/jsonschema/validators.py index a9769599c..83864bcf6 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -61,6 +61,13 @@ def __getattr__(name): stacklevel=2, ) return _META_SCHEMAS + elif name == "RefResolver": + warnings.warn( + _RefResolver._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolver raise AttributeError(f"module {__name__} has no attribute {name}") @@ -209,7 +216,7 @@ def __init_subclass__(cls): def __attrs_post_init__(self): if self.resolver is None: - self.resolver = RefResolver.from_schema( + self.resolver = _RefResolver.from_schema( self.schema, id_of=id_of, ) @@ -684,7 +691,7 @@ def extend( _LATEST_VERSION = Draft202012Validator -class RefResolver: +class _RefResolver: """ Resolve JSON References. @@ -726,8 +733,21 @@ class RefResolver: cache_remote (bool): Whether remote refs should be cached after first resolution + + .. deprecated:: v4.18.0 + + `RefResolver` has been deprecated in favor of `referencing`. """ + _DEPRECATION_MESSAGE = ( + "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing library, which " + "provides more compliant referencing behavior as well as more " + "flexible APIs for customization. A future release will remove " + "RefResolver. Please file a feature request (on referencing) if you " + "are missing an API for the kind of customization you need." + ) + def __init__( self, base_uri, @@ -774,7 +794,7 @@ def from_schema(cls, schema, id_of=_id_of, *args, **kwargs): Returns: - `RefResolver` + `_RefResolver` """ return cls(base_uri=id_of(schema), referrer=schema, *args, **kwargs) # noqa: B026, E501 From f6aa0530357062dd704ec9ed187641dab21a88f2 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 9 Feb 2023 15:48:59 +0100 Subject: [PATCH 04/33] Load the test suite into a referencing.Registry for running tests. --- jsonschema/tests/_suite.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py index d425e4008..5fdae775c 100644 --- a/jsonschema/tests/_suite.py +++ b/jsonschema/tests/_suite.py @@ -15,6 +15,8 @@ import unittest from attrs import field, frozen +from referencing import Registry +import referencing.jsonschema if TYPE_CHECKING: import pyperf @@ -47,13 +49,38 @@ def _find_suite(): class Suite: _root: Path = field(factory=_find_suite) - _remotes: Mapping[str, Mapping[str, Any] | bool] = field(init=False) + _remotes: Registry = field(init=False) def __attrs_post_init__(self): jsonschema_suite = self._root.joinpath("bin", "jsonschema_suite") argv = [sys.executable, str(jsonschema_suite), "remotes"] remotes = subprocess.check_output(argv).decode("utf-8") - object.__setattr__(self, "_remotes", json.loads(remotes)) + + resources = json.loads(remotes) + + li = "http://localhost:1234/locationIndependentIdentifierPre2019.json" + li4 = "http://localhost:1234/locationIndependentIdentifierDraft4.json" + + registry = Registry().with_resources( + [ + ( + li, + referencing.jsonschema.DRAFT7.create_resource( + contents=resources.pop(li), + ), + ), + ( + li4, + referencing.jsonschema.DRAFT4.create_resource( + contents=resources.pop(li4), + ), + ), + ], + ).with_contents( + resources.items(), + default_specification=referencing.jsonschema.DRAFT202012, + ) + object.__setattr__(self, "_remotes", registry) def benchmark(self, runner: pyperf.Runner): # pragma: no cover for name, Validator in _VALIDATORS.items(): @@ -74,7 +101,7 @@ def version(self, name) -> Version: class Version: _path: Path - _remotes: Mapping[str, Mapping[str, Any] | bool] + _remotes: Registry name: str @@ -173,7 +200,7 @@ class _Test: valid: bool - _remotes: Mapping[str, Mapping[str, Any] | bool] + _remotes: Registry comment: str | None = None @@ -220,7 +247,7 @@ def validate(self, Validator, **kwargs): Validator.check_schema(self.schema) resolver = _RefResolver.from_schema( schema=self.schema, - store=self._remotes, + store={k: v.contents for k, v in self._remotes.items()}, id_of=Validator.ID_OF, ) From 2c8f643bf19a952eabf68e0c4250e5ae5646c035 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 9 Feb 2023 17:40:14 +0100 Subject: [PATCH 05/33] Minor regrouping of some to-be-modified/deprecated RefResolver tests. --- jsonschema/tests/test_validators.py | 64 ++++++++++++++--------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 6e37f7b94..f94b69bfc 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1539,20 +1539,6 @@ def test_invalid_instances_are_not_valid(self): def test_non_existent_properties_are_ignored(self): self.Validator({object(): object()}).validate(instance=object()) - def test_it_creates_a_ref_resolver_if_not_provided(self): - self.assertIsInstance( - self.Validator({}).resolver, - validators._RefResolver, - ) - - def test_it_delegates_to_a_ref_resolver(self): - ref, schema = "someCoolRef", {"type": "integer"} - resolver = validators._RefResolver("", {}, store={ref: schema}) - validator = self.Validator({"$ref": ref}, resolver=resolver) - - with self.assertRaises(exceptions.ValidationError): - validator.validate(None) - def test_evolve(self): ref, schema = "someCoolRef", {"type": "integer"} resolver = validators._RefResolver("", {}, store={ref: schema}) @@ -1588,24 +1574,6 @@ class OhNo(self.Validator): self.assertEqual(new.foo, [1, 2, 3]) self.assertEqual(new._bar, 12) - def test_it_delegates_to_a_legacy_ref_resolver(self): - """ - Legacy RefResolvers support only the context manager form of - resolution. - """ - - class LegacyRefResolver: - @contextmanager - def resolving(this, ref): - self.assertEqual(ref, "the ref") - yield {"type": "integer"} - - resolver = LegacyRefResolver() - schema = {"$ref": "the ref"} - - with self.assertRaises(exceptions.ValidationError): - self.Validator(schema, resolver=resolver).validate(None) - def test_is_type_is_true_for_valid_type(self): self.assertTrue(self.Validator({}).is_type("foo", "string")) @@ -1766,6 +1734,38 @@ def test_check_redefined_sequence(self): with self.assertRaises(exceptions.ValidationError): validator.validate(instance) + def test_it_creates_a_ref_resolver_if_not_provided(self): + self.assertIsInstance( + self.Validator({}).resolver, + validators._RefResolver, + ) + + def test_it_upconverts_from_deprecated_RefResolvers(self): + ref, schema = "someCoolRef", {"type": "integer"} + resolver = validators._RefResolver("", {}, store={ref: schema}) + validator = self.Validator({"$ref": ref}, resolver=resolver) + + with self.assertRaises(exceptions.ValidationError): + validator.validate(None) + + def test_it_upconverts_from_yet_older_deprecated_legacy_RefResolvers(self): + """ + Legacy RefResolvers support only the context manager form of + resolution. + """ + + class LegacyRefResolver: + @contextmanager + def resolving(this, ref): + self.assertEqual(ref, "the ref") + yield {"type": "integer"} + + resolver = LegacyRefResolver() + schema = {"$ref": "the ref"} + + with self.assertRaises(exceptions.ValidationError): + self.Validator(schema, resolver=resolver).validate(None) + class AntiDraft6LeakMixin: """ From 52340d7820a32336c7835d14ff1886dbae3c0e75 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Tue, 14 Feb 2023 16:03:59 +0100 Subject: [PATCH 06/33] Offload a small initial bit of id-related code to referencing. --- jsonschema/_legacy_validators.py | 13 ------------- jsonschema/validators.py | 30 ++++++++++++++---------------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index cc5e3f44c..fa9daf47b 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -2,19 +2,6 @@ from jsonschema.exceptions import ValidationError -def id_of_ignore_ref(property="$id"): - def id_of(schema): - """ - Ignore an ``$id`` sibling of ``$ref`` if it is present. - - Otherwise, return the ID of the given schema. - """ - if schema is True or schema is False or "$ref" in schema: - return "" - return schema.get(property, "") - return id_of - - def ignore_ref_siblings(schema): """ Ignore siblings of ``$ref`` if it is present. diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 83864bcf6..333855148 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -18,6 +18,7 @@ from jsonschema_specifications import REGISTRY as SPECIFICATIONS from pyrsistent import m import attr +import referencing.jsonschema from jsonschema import ( _format, @@ -99,15 +100,6 @@ def _validates(cls): return _validates -def _id_of(schema): - """ - Return the ID of a schema for recent JSON Schema drafts. - """ - if schema is True or schema is False: - return "" - return schema.get("$id", "") - - def _store_schema_list(): return [ (uri, each.contents) for uri, each in SPECIFICATIONS.items() @@ -122,7 +114,7 @@ def create( version=None, type_checker=_types.draft202012_type_checker, format_checker=_format.draft202012_format_checker, - id_of=_id_of, + id_of=referencing.jsonschema.DRAFT202012.id_of, applicable_validators=methodcaller("items"), ): """ @@ -461,7 +453,7 @@ def extend( type_checker=_types.draft3_type_checker, format_checker=_format.draft3_format_checker, version="draft3", - id_of=_legacy_validators.id_of_ignore_ref(property="id"), + id_of=referencing.jsonschema.DRAFT3.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) @@ -500,7 +492,7 @@ def extend( type_checker=_types.draft4_type_checker, format_checker=_format.draft4_format_checker, version="draft4", - id_of=_legacy_validators.id_of_ignore_ref(property="id"), + id_of=referencing.jsonschema.DRAFT4.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) @@ -544,7 +536,7 @@ def extend( type_checker=_types.draft6_type_checker, format_checker=_format.draft6_format_checker, version="draft6", - id_of=_legacy_validators.id_of_ignore_ref(), + id_of=referencing.jsonschema.DRAFT6.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) @@ -589,7 +581,7 @@ def extend( type_checker=_types.draft7_type_checker, format_checker=_format.draft7_format_checker, version="draft7", - id_of=_legacy_validators.id_of_ignore_ref(), + id_of=referencing.jsonschema.DRAFT7.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) @@ -782,7 +774,13 @@ def __init__( self._remote_cache = remote_cache @classmethod - def from_schema(cls, schema, id_of=_id_of, *args, **kwargs): + def from_schema( + cls, + schema, + id_of=referencing.jsonschema.DRAFT202012.id_of, + *args, + **kwargs, + ): """ Construct a resolver from a JSON schema object. @@ -797,7 +795,7 @@ def from_schema(cls, schema, id_of=_id_of, *args, **kwargs): `_RefResolver` """ - return cls(base_uri=id_of(schema), referrer=schema, *args, **kwargs) # noqa: B026, E501 + return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501 def push_scope(self, scope): """ From 4ec24ab4627adac1bdfb65028642b56ebf7c4c26 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 15 Feb 2023 11:28:08 +0200 Subject: [PATCH 07/33] Inline a function that will be RefResolver specific. --- jsonschema/validators.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 333855148..b86f376ef 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -100,14 +100,6 @@ def _validates(cls): return _validates -def _store_schema_list(): - return [ - (uri, each.contents) for uri, each in SPECIFICATIONS.items() - ] + [ - (id, validator.META_SCHEMA) for id, validator in _META_SCHEMAS.items() - ] - - def create( meta_schema, validators=(), @@ -761,7 +753,12 @@ def __init__( self._scopes_stack = [base_uri] - self.store = _utils.URIDict(_store_schema_list()) + self.store = _utils.URIDict( + (uri, each.contents) for uri, each in SPECIFICATIONS.items() + ) + self.store.update( + (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items() + ) self.store.update(store) self.store.update( (schema["$id"], schema) From 9da55dfa02c54b8e638a35362fb99755e5d46ce5 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 15 Feb 2023 12:25:58 +0200 Subject: [PATCH 08/33] Move reference resolution to a (private) Validator method. This will make it easier to swap over to referencing's Resolver (while preserving backwards compat for anyone who passes a RefResolver to a Validator). At some point in the future this method may become public (which will make it easier for external dialects to resolve references) but let's keep it private for a bit until it's clear that the interface is stable -- a future draft might do crazy things like have adjacent properties to $ref affect the resolution behavior, which would make this method need to take more than just the $ref value. --- jsonschema/_validators.py | 27 +--------- .../tests/test_jsonschema_test_suite.py | 52 +++++++++++++------ jsonschema/validators.py | 34 +++++++++--- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 8542a879c..7351c480a 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -1,5 +1,4 @@ from fractions import Fraction -from urllib.parse import urldefrag, urljoin import re from jsonschema._utils import ( @@ -286,33 +285,11 @@ def enum(validator, enums, instance, schema): def ref(validator, ref, instance, schema): - resolve = getattr(validator.resolver, "resolve", None) - if resolve is None: - with validator.resolver.resolving(ref) as resolved: - yield from validator.descend(instance, resolved) - else: - scope, resolved = validator.resolver.resolve(ref) - validator.resolver.push_scope(scope) - - try: - yield from validator.descend(instance, resolved) - finally: - validator.resolver.pop_scope() + yield from validator._validate_reference(ref=ref, instance=instance) def dynamicRef(validator, dynamicRef, instance, schema): - _, fragment = urldefrag(dynamicRef) - - for url in validator.resolver._scopes_stack: - lookup_url = urljoin(url, dynamicRef) - with validator.resolver.resolving(lookup_url) as subschema: - if ("$dynamicAnchor" in subschema - and fragment == subschema["$dynamicAnchor"]): - yield from validator.descend(instance, subschema) - break - else: - with validator.resolver.resolving(dynamicRef) as subschema: - yield from validator.descend(instance, subschema) + yield from validator._validate_reference(ref=dynamicRef, instance=instance) def type(validator, types, instance, schema): diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index 5ebd7ed03..3b602aa6f 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -419,23 +419,6 @@ def leap_second(test): "$defs first" ), )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="correct extended schema", - case_description=( - "$ref and $dynamicAnchor are independent of order - " - "$defs first" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="correct extended schema", - case_description=( - "$ref and $dynamicAnchor are independent of order - $ref first" - ), - )(test) or skip( message="dynamicRef support isn't fully working yet.", subject="dynamicRef", @@ -482,6 +465,41 @@ def leap_second(test): subject="anchor", case_description="same $anchor with different base uri", )(test) + or skip( + message="dynamicRef support isn't fully working yet.", + subject="dynamicRef", + description="instance with misspelled field", + case_description=( + "strict-tree schema, guards against misspelled properties" + ), + )(test) + or skip( + message="dynamicRef support isn't fully working yet.", + subject="dynamicRef", + description="An array containing non-strings is invalid", + case_description=( + "A $dynamicRef resolves to the first $dynamicAnchor still " + "in scope that is encountered when the schema is evaluated" + ), + )(test) + or skip( + message="dynamicRef support isn't fully working yet.", + subject="dynamicRef", + description="An array containing non-strings is invalid", + case_description=( + "A $dynamicRef with intermediate scopes that don't include a " + "matching $dynamicAnchor does not affect dynamic scope " + "resolution" + ), + )(test) + or skip( + message="dynamicRef support isn't fully working yet.", + subject="dynamicRef", + description="incorrect extended schema", + case_description=( + "tests for implementation dynamic anchor and reference link" + ), + )(test) or skip( message="Vocabulary support is still in-progress.", subject="vocabulary", diff --git a/jsonschema/validators.py b/jsonschema/validators.py index b86f376ef..be5ff1816 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -180,7 +180,7 @@ class Validator: ID_OF = staticmethod(id_of) schema = attr.ib(repr=reprlib.repr) - resolver = attr.ib(default=None, repr=False) + _resolver = attr.ib(default=None, repr=False) format_checker = attr.ib(default=None) def __init_subclass__(cls): @@ -198,13 +198,6 @@ def __init_subclass__(cls): stacklevel=2, ) - def __attrs_post_init__(self): - if self.resolver is None: - self.resolver = _RefResolver.from_schema( - self.schema, - id_of=id_of, - ) - @classmethod def check_schema(cls, schema, format_checker=_UNSET): Validator = validator_for(cls.META_SCHEMA, default=cls) @@ -217,6 +210,15 @@ def check_schema(cls, schema, format_checker=_UNSET): for error in validator.iter_errors(schema): raise exceptions.SchemaError.create_from(error) + @property + def resolver(self): + if self._resolver is None: + self._resolver = _RefResolver.from_schema( + self.schema, + id_of=id_of, + ) + return self._resolver + def evolve(self, **changes): # Essentially reproduces attr.evolve, but may involve instantiating # a different class than this one. @@ -262,6 +264,8 @@ def iter_errors(self, instance, _schema=None): ) return + # Temporarily needed to eagerly create a resolver... + self.resolver scope = id_of(_schema) if scope: self.resolver.push_scope(scope) @@ -306,6 +310,20 @@ def is_type(self, instance, type): except exceptions.UndefinedTypeCheck: raise exceptions.UnknownType(type, instance, self.schema) + def _validate_reference(self, ref, instance): + resolve = getattr(self.resolver, "resolve", None) + if resolve is None: + with self.resolver.resolving(ref) as resolved: + yield from self.descend(instance, resolved) + else: + scope, resolved = self.resolver.resolve(ref) + self.resolver.push_scope(scope) + + try: + yield from self.descend(instance, resolved) + finally: + self.resolver.pop_scope() + def is_valid(self, instance, _schema=None): if _schema is not None: warnings.warn( From 635dc13c9167f7b2c85ed1f2a8d11339d8b1096e Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 15 Feb 2023 14:04:49 +0200 Subject: [PATCH 09/33] Deprecate Validator.resolver. It will imminently be replaced by referencing.Registry-based resolution. --- jsonschema/protocols.py | 6 +++++- jsonschema/tests/test_deprecations.py | 16 ++++++++++++++++ jsonschema/tests/test_validators.py | 7 +++---- jsonschema/validators.py | 16 +++++++++++++++- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index c9c71dc09..eb674448e 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -65,6 +65,11 @@ class Validator(Protocol): a resolver that will be used to resolve :kw:`$ref` properties (JSON references). If unprovided, one will be created. + .. deprecated:: v4.18.0 + + `RefResolver` has been deprecated in favor of `referencing`, + and with it, this argument. + format_checker: if provided, a checker which will be used to assert about @@ -108,7 +113,6 @@ class Validator(Protocol): def __init__( self, schema: Mapping | bool, - resolver: jsonschema.validators._RefResolver | None = None, format_checker: jsonschema.FormatChecker | None = None, ) -> None: ... diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py index 898a792f7..d21065fae 100644 --- a/jsonschema/tests/test_deprecations.py +++ b/jsonschema/tests/test_deprecations.py @@ -125,6 +125,22 @@ def test_Validator_iter_errors_two_arguments(self): ), ) + def test_Validator_resolver(self): + """ + As of v4.18.0, accessing Validator.resolver is deprecated. + """ + + validator = validators.Draft7Validator({}) + with self.assertWarns(DeprecationWarning) as w: + self.assertIsInstance(validator.resolver, validators._RefResolver) + + self.assertEqual(w.filename, __file__) + self.assertTrue( + str(w.warning).startswith( + "Accessing Draft7Validator.resolver is ", + ), + ) + def test_Validator_subclassing(self): """ As of v4.12.0, subclassing a validator class produces an explicit diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index f94b69bfc..a76b247f6 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1735,10 +1735,9 @@ def test_check_redefined_sequence(self): validator.validate(instance) def test_it_creates_a_ref_resolver_if_not_provided(self): - self.assertIsInstance( - self.Validator({}).resolver, - validators._RefResolver, - ) + with self.assertWarns(DeprecationWarning): + resolver = self.Validator({}).resolver + self.assertIsInstance(resolver, validators._RefResolver) def test_it_upconverts_from_deprecated_RefResolvers(self): ref, schema = "someCoolRef", {"type": "integer"} diff --git a/jsonschema/validators.py b/jsonschema/validators.py index be5ff1816..8abec84fd 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -212,6 +212,18 @@ def check_schema(cls, schema, format_checker=_UNSET): @property def resolver(self): + warnings.warn( + ( + f"Accessing {self.__class__.__name__}.resolver is " + "deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing " + "library, which provides more compliant referencing " + "behavior as well as more flexible APIs for " + "customization." + ), + DeprecationWarning, + stacklevel=2, + ) if self._resolver is None: self._resolver = _RefResolver.from_schema( self.schema, @@ -265,7 +277,9 @@ def iter_errors(self, instance, _schema=None): return # Temporarily needed to eagerly create a resolver... - self.resolver + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.resolver scope = id_of(_schema) if scope: self.resolver.push_scope(scope) From 69f3899c0770472f19898d80aea2b7a51fd5ec6b Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 16 Feb 2023 11:43:35 +0200 Subject: [PATCH 10/33] Actually depend on referencing and update docs requirements. --- docs/requirements.txt | 24 ++++++++++-------------- pyproject.toml | 3 ++- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 140c4bffe..af3fcb5be 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile docs/requirements.in +# pip-compile --resolver=backtracking docs/requirements.in # alabaster==0.7.13 # via sphinx -astroid==2.14.1 +astroid==2.14.2 # via sphinx-autoapi attrs==22.2.0 # via @@ -31,9 +31,7 @@ fonttools==4.38.0 furo==2022.12.7 # via -r docs/requirements.in idna==3.4 - # via - # requests - # yarl + # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.2 @@ -42,7 +40,7 @@ jinja2==3.1.2 # sphinx-autoapi file:.#egg=jsonschema # via -r docs/requirements.in -jsonschema-specifications==2023.2.4 +jsonschema-specifications==2023.3.1 # via jsonschema kiwisolver==1.4.4 # via matplotlib @@ -54,10 +52,8 @@ lxml==4.9.2 # sphinx-json-schema-spec markupsafe==2.1.2 # via jinja2 -matplotlib==3.6.3 +matplotlib==3.7.0 # via sphinxext-opengraph -multidict==6.0.4 - # via yarl numpy==1.24.2 # via # contourpy @@ -86,15 +82,17 @@ pytz==2022.7.1 # via babel pyyaml==6.0 # via sphinx-autoapi -referencing==0.14.2 - # via jsonschema-specifications +referencing==0.17.1 + # via + # jsonschema + # jsonschema-specifications requests==2.28.2 # via sphinx six==1.16.0 # via python-dateutil snowballstemmer==2.2.0 # via sphinx -soupsieve==2.3.2.post1 +soupsieve==2.4 # via beautifulsoup4 sphinx==6.1.3 # via @@ -139,5 +137,3 @@ urllib3==1.26.14 # via requests wrapt==1.14.1 # via astroid -yarl==1.8.2 - # via referencing diff --git a/pyproject.toml b/pyproject.toml index a5ce83256..f13eb3f3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,8 @@ dynamic = ["version", "readme"] dependencies = [ "attrs>=17.4.0", "pyrsistent>=0.14.0,!=0.17.0,!=0.17.1,!=0.17.2", - "jsonschema-specifications>=2023.02.4", + "jsonschema-specifications>=2023.03.1", + "referencing>=0.17.1", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From 2889feb9fd2aa290ee97fde5ff105e491ec5143d Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 16 Feb 2023 11:47:05 +0200 Subject: [PATCH 11/33] Make an evolve test not refer to reference resolution. Just avoids a deprecation warning when we switch over. --- jsonschema/tests/test_validators.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index a76b247f6..8553cefab 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1540,16 +1540,23 @@ def test_non_existent_properties_are_ignored(self): self.Validator({object(): object()}).validate(instance=object()) def test_evolve(self): - ref, schema = "someCoolRef", {"type": "integer"} - resolver = validators._RefResolver("", {}, store={ref: schema}) - - validator = self.Validator(schema, resolver=resolver) - new = validator.evolve(schema={"type": "string"}) + schema, format_checker = {"type": "integer"}, FormatChecker() + original = self.Validator( + schema, + format_checker=format_checker, + ) + new = original.evolve( + schema={"type": "string"}, + format_checker=self.Validator.FORMAT_CHECKER, + ) - expected = self.Validator({"type": "string"}, resolver=resolver) + expected = self.Validator( + {"type": "string"}, + format_checker=self.Validator.FORMAT_CHECKER, + ) self.assertEqual(new, expected) - self.assertNotEqual(new, validator) + self.assertNotEqual(new, original) def test_evolve_with_subclass(self): """ From a39e5c953a559b287c753bd604e3e11d218c29cf Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 16 Feb 2023 12:07:08 +0200 Subject: [PATCH 12/33] Move Validator._resolver to _ref_resolver. Makes way for our newer resolver to live in the shorter name. (This should have no effect on the public API, where we have Validator.resolver returning this attribute after emitting a deprecation warning.) Also bumps the minimum attrs version we depend on, as we need the alias functionality. --- jsonschema/validators.py | 34 +++++++++++++++++----------------- pyproject.toml | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 8abec84fd..5a0cd43bf 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -180,7 +180,7 @@ class Validator: ID_OF = staticmethod(id_of) schema = attr.ib(repr=reprlib.repr) - _resolver = attr.ib(default=None, repr=False) + _ref_resolver = attr.ib(default=None, repr=False, alias="resolver") format_checker = attr.ib(default=None) def __init_subclass__(cls): @@ -224,12 +224,12 @@ def resolver(self): DeprecationWarning, stacklevel=2, ) - if self._resolver is None: - self._resolver = _RefResolver.from_schema( + if self._ref_resolver is None: + self._ref_resolver = _RefResolver.from_schema( self.schema, id_of=id_of, ) - return self._resolver + return self._ref_resolver def evolve(self, **changes): # Essentially reproduces attr.evolve, but may involve instantiating @@ -243,7 +243,7 @@ def evolve(self, **changes): if not field.init: continue attr_name = field.name # To deal with private attributes. - init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + init_name = field.alias if init_name not in changes: changes[init_name] = getattr(self, attr_name) @@ -325,18 +325,18 @@ def is_type(self, instance, type): raise exceptions.UnknownType(type, instance, self.schema) def _validate_reference(self, ref, instance): - resolve = getattr(self.resolver, "resolve", None) - if resolve is None: - with self.resolver.resolving(ref) as resolved: - yield from self.descend(instance, resolved) - else: - scope, resolved = self.resolver.resolve(ref) - self.resolver.push_scope(scope) - - try: - yield from self.descend(instance, resolved) - finally: - self.resolver.pop_scope() + resolve = getattr(self._ref_resolver, "resolve", None) + if resolve is None: + with self._ref_resolver.resolving(ref) as resolved: + return self.descend(instance, resolved) + else: + scope, resolved = resolve(ref) + self._ref_resolver.push_scope(scope) + + try: + return self.descend(instance, resolved) + finally: + self._ref_resolver.pop_scope() def is_valid(self, instance, _schema=None): if _schema is not None: diff --git a/pyproject.toml b/pyproject.toml index f13eb3f3d..c7c0a97df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ dynamic = ["version", "readme"] dependencies = [ - "attrs>=17.4.0", + "attrs>=22.2.0", "pyrsistent>=0.14.0,!=0.17.0,!=0.17.1,!=0.17.2", "jsonschema-specifications>=2023.03.1", "referencing>=0.17.1", From e8266294408521daf38d879ba35c45a4b0ef5180 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 16 Feb 2023 12:32:00 +0200 Subject: [PATCH 13/33] Resolve $ref using the referencing library. Passes all the remaining referencing tests across all drafts, hooray! Makes Validators take a referencing.Registry argument which users should use to customize preloaded schemas, or to configure remote reference retrieval. This fully obsoletes jsonschema.RefResolver, which has already been deprecated in a previous commit. Users should move to instead loading schemas into referencing.Registry objects. See the referencing documentation at https://referencing.rtfd.io/ for details (with more jsonschema-specific information to be added shortly). Note that the interface for resolving references on a Validator is not yet public (and hidden behind _resolver and _validate_reference attributes). One or both of these are likely to become public after some period of stabilization. Feedback is of course welcome! --- jsonschema/_legacy_validators.py | 44 ++- jsonschema/_utils.py | 40 +-- jsonschema/protocols.py | 7 + jsonschema/tests/_suite.py | 17 +- .../tests/test_jsonschema_test_suite.py | 306 +----------------- jsonschema/tests/test_validators.py | 1 + jsonschema/validators.py | 98 ++++-- 7 files changed, 131 insertions(+), 382 deletions(-) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index fa9daf47b..aebf1b97b 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -1,3 +1,5 @@ +from referencing.jsonschema import lookup_recursive_ref + from jsonschema import _utils from jsonschema.exceptions import ValidationError @@ -210,22 +212,12 @@ def contains_draft6_draft7(validator, contains, instance, schema): def recursiveRef(validator, recursiveRef, instance, schema): - lookup_url, target = validator.resolver.resolution_scope, validator.schema - - for each in reversed(validator.resolver._scopes_stack[1:]): - lookup_url, next_target = validator.resolver.resolve(each) - if next_target.get("$recursiveAnchor"): - target = next_target - else: - break - - fragment = recursiveRef.lstrip("#") - subschema = validator.resolver.resolve_fragment(target, fragment) - # FIXME: This is gutted (and not calling .descend) because it can trigger - # recursion errors, so there's a bug here. Re-enable the tests to - # see it. - subschema - return [] + resolved = lookup_recursive_ref(validator._resolver) + yield from validator.descend( + instance, + resolved.contents, + resolver=resolved.resolver, + ) def find_evaluated_item_indexes_by_schema(validator, instance, schema): @@ -243,15 +235,17 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): return list(range(0, len(instance))) if "$ref" in schema: - scope, resolved = validator.resolver.resolve(schema["$ref"]) - validator.resolver.push_scope(scope) - - try: - evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, resolved, - ) - finally: - validator.resolver.pop_scope() + resolved = validator._resolver.lookup(schema["$ref"]) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) if "items" in schema: if validator.is_type(schema["items"], "object"): diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index c218b184e..b14b70ea2 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -201,15 +201,17 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): return list(range(0, len(instance))) if "$ref" in schema: - scope, resolved = validator.resolver.resolve(schema["$ref"]) - validator.resolver.push_scope(scope) - - try: - evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, resolved, - ) - finally: - validator.resolver.pop_scope() + resolved = validator._resolver.lookup(schema["$ref"]) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) if "prefixItems" in schema: evaluated_indexes += list(range(0, len(schema["prefixItems"]))) @@ -260,15 +262,17 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): evaluated_keys = [] if "$ref" in schema: - scope, resolved = validator.resolver.resolve(schema["$ref"]) - validator.resolver.push_scope(scope) - - try: - evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, resolved, - ) - finally: - validator.resolver.pop_scope() + resolved = validator._resolver.lookup(schema["$ref"]) + evaluated_keys.extend( + find_evaluated_property_keys_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) for keyword in [ "properties", "additionalProperties", "unevaluatedProperties", diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index eb674448e..9d34f61c0 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -11,6 +11,8 @@ from typing import TYPE_CHECKING, Any, ClassVar, Iterable import sys +from referencing.jsonschema import SchemaRegistry + # doing these imports with `try ... except ImportError` doesn't pass mypy # checking because mypy sees `typing._SpecialForm` and # `typing_extensions._SpecialForm` as incompatible @@ -60,6 +62,10 @@ class Validator(Protocol): an invalid schema can lead to undefined behavior. See `Validator.check_schema` to validate a schema first. + registry: + + a schema registry that will be used for looking up JSON references + resolver: a resolver that will be used to resolve :kw:`$ref` @@ -113,6 +119,7 @@ class Validator(Protocol): def __init__( self, schema: Mapping | bool, + registry: SchemaRegistry, format_checker: jsonschema.FormatChecker | None = None, ) -> None: ... diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py index 5fdae775c..7f14ca5f1 100644 --- a/jsonschema/tests/_suite.py +++ b/jsonschema/tests/_suite.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: import pyperf -from jsonschema.validators import _VALIDATORS, _RefResolver +from jsonschema.validators import _VALIDATORS import jsonschema _DELIMITERS = re.compile(r"[\W\- ]+") @@ -245,20 +245,11 @@ def fn(this): def validate(self, Validator, **kwargs): Validator.check_schema(self.schema) - resolver = _RefResolver.from_schema( + validator = Validator( schema=self.schema, - store={k: v.contents for k, v in self._remotes.items()}, - id_of=Validator.ID_OF, + registry=self._remotes, + **kwargs, ) - - # XXX: #693 asks to improve the public API for this, since yeah, it's - # bad. Figures that since it's hard for end-users, we experience - # the pain internally here too. - def prevent_network_access(uri): - raise RuntimeError(f"Tried to access the network: {uri}") - resolver.resolve_remote = prevent_network_access - - validator = Validator(schema=self.schema, resolver=resolver, **kwargs) if os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": breakpoint() validator.validate(instance=self.data) diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index 3b602aa6f..fd2c49917 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -8,7 +8,6 @@ import sys -from jsonschema.tests._helpers import bug from jsonschema.tests._suite import Suite import jsonschema @@ -135,13 +134,6 @@ def leap_second(test): skip=lambda test: ( missing_format(jsonschema.Draft3Validator)(test) or complex_email_validation(test) - or skip( - message=bug(), - subject="ref", - case_description=( - "$ref prevents a sibling id from changing the base uri" - ), - )(test) ), ) @@ -160,49 +152,6 @@ def leap_second(test): or leap_second(test) or missing_format(jsonschema.Draft4Validator)(test) or complex_email_validation(test) - or skip( - message=bug(), - subject="ref", - case_description="Recursive references between schemas", - )(test) - or skip( - message=bug(), - subject="ref", - case_description=( - "Location-independent identifier with " - "base URI change in subschema" - ), - )(test) - or skip( - message=bug(), - subject="ref", - case_description=( - "$ref prevents a sibling id from changing the base uri" - ), - )(test) - or skip( - message=bug(), - subject="id", - description="match $ref to id", - )(test) - or skip( - message=bug(), - subject="id", - description="no match on enum or $ref to id", - )(test) - or skip( - message=bug(), - subject="refRemote", - case_description="base URI change - change folder in subschema", - )(test) - or skip( - message=bug(), - subject="ref", - case_description=( - "id must be resolved against nearest parent, " - "not just immediate parent" - ), - )(test) ), ) @@ -220,11 +169,6 @@ def leap_second(test): or leap_second(test) or missing_format(jsonschema.Draft6Validator)(test) or complex_email_validation(test) - or skip( - message=bug(), - subject="refRemote", - case_description="base URI change - change folder in subschema", - )(test) ), ) @@ -243,19 +187,6 @@ def leap_second(test): or leap_second(test) or missing_format(jsonschema.Draft7Validator)(test) or complex_email_validation(test) - or skip( - message=bug(), - subject="refRemote", - case_description="base URI change - change folder in subschema", - )(test) - or skip( - message=bug(), - subject="ref", - case_description=( - "$id must be resolved against nearest parent, " - "not just immediate parent" - ), - )(test) ), ) @@ -268,115 +199,12 @@ def leap_second(test): DRAFT201909.optional_cases_of(name="non-bmp-regex"), DRAFT201909.optional_cases_of(name="refOfUnknownKeyword"), Validator=jsonschema.Draft201909Validator, - skip=lambda test: ( - skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - case_description=( - "$recursiveRef with no $recursiveAnchor in " - "the initial target schema resource" - ), - description=( - "leaf node does not match: recursion uses the inner schema" - ), - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - description="leaf node matches: recursion uses the inner schema", - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - case_description=( - "dynamic $recursiveRef destination (not predictable " - "at schema compile time)" - ), - description="integer node", - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - case_description=( - "multiple dynamic paths to the $recursiveRef keyword" - ), - description="recurse to integerNode - floats are not allowed", - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - description="integer does not match as a property value", - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - description=( - "leaf node does not match: " - "recursion only uses inner schema" - ), - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - description=( - "leaf node matches: " - "recursion only uses inner schema" - ), - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - description=( - "two levels, integer does not match as a property value" - ), - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - description="recursive mismatch", - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="recursiveRef", - description="two levels, no match", - )(test) - or skip( - message="recursiveRef support isn't working yet.", - subject="id", - case_description=( - "Invalid use of fragments in location-independent $id" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="defs", - description="invalid definition schema", - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="anchor", - case_description="same $anchor with different base uri", - )(test) - or skip( - message="Vocabulary support is still in-progress.", - subject="vocabulary", - description=( - "no validation: invalid number, but it still validates" - ), - )(test) - or skip( - message=bug(), - subject="ref", - case_description=( - "$id must be resolved against nearest parent, " - "not just immediate parent" - ), - )(test) - or skip( - message=bug(), - subject="refRemote", - case_description="remote HTTP ref with nested absolute ref", - )(test) + skip=skip( + message="Vocabulary support is still in-progress.", + subject="vocabulary", + description=( + "no validation: invalid number, but it still validates" + ), ), ) @@ -404,122 +232,12 @@ def leap_second(test): DRAFT202012.optional_cases_of(name="non-bmp-regex"), DRAFT202012.optional_cases_of(name="refOfUnknownKeyword"), Validator=jsonschema.Draft202012Validator, - skip=lambda test: ( - skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="The recursive part is not valid against the root", - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="incorrect extended schema", - case_description=( - "$ref and $dynamicAnchor are independent of order - " - "$defs first" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="incorrect extended schema", - case_description=( - "$ref and $dynamicAnchor are independent of order - $ref first" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description=( - "/then/$defs/thingy is the final stop for the $dynamicRef" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description=( - "string matches /$defs/thingy, but the $dynamicRef " - "does not stop here" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description=( - "string matches /$defs/thingy, but the $dynamicRef " - "does not stop here" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="recurse to integerNode - floats are not allowed", - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="defs", - description="invalid definition schema", - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="anchor", - case_description="same $anchor with different base uri", - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="instance with misspelled field", - case_description=( - "strict-tree schema, guards against misspelled properties" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="An array containing non-strings is invalid", - case_description=( - "A $dynamicRef resolves to the first $dynamicAnchor still " - "in scope that is encountered when the schema is evaluated" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="An array containing non-strings is invalid", - case_description=( - "A $dynamicRef with intermediate scopes that don't include a " - "matching $dynamicAnchor does not affect dynamic scope " - "resolution" - ), - )(test) - or skip( - message="dynamicRef support isn't fully working yet.", - subject="dynamicRef", - description="incorrect extended schema", - case_description=( - "tests for implementation dynamic anchor and reference link" - ), - )(test) - or skip( - message="Vocabulary support is still in-progress.", - subject="vocabulary", - description=( - "no validation: invalid number, but it still validates" - ), - )(test) - or skip( - message=bug(), - subject="ref", - case_description=( - "$id must be resolved against nearest parent, " - "not just immediate parent" - ), - )(test) - or skip( - message=bug(), - subject="refRemote", - case_description="remote HTTP ref with nested absolute ref", - )(test) + skip=skip( + message="Vocabulary support is still in-progress.", + subject="vocabulary", + description=( + "no validation: invalid number, but it still validates" + ), ), ) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 8553cefab..c62498bf0 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1553,6 +1553,7 @@ def test_evolve(self): expected = self.Validator( {"type": "string"}, format_checker=self.Validator.FORMAT_CHECKER, + _resolver=new._resolver, ) self.assertEqual(new, expected) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 5a0cd43bf..b80285686 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -17,6 +17,7 @@ from jsonschema_specifications import REGISTRY as SPECIFICATIONS from pyrsistent import m +from referencing import Specification import attr import referencing.jsonschema @@ -170,6 +171,11 @@ def create( # preemptively don't shadow the `Validator.format_checker` local format_checker_arg = format_checker + specification = referencing.jsonschema.specification_with( + dialect_id=id_of(meta_schema), + default=Specification.OPAQUE, + ) + @attr.s class Validator: @@ -182,6 +188,19 @@ class Validator: schema = attr.ib(repr=reprlib.repr) _ref_resolver = attr.ib(default=None, repr=False, alias="resolver") format_checker = attr.ib(default=None) + # TODO: include new meta-schemas added at runtime + _registry = attr.ib( + default=SPECIFICATIONS, + converter=SPECIFICATIONS.combine, # type: ignore[misc] + kw_only=True, + repr=False, + ) + _resolver = attr.ib( + alias="_resolver", + default=None, + kw_only=True, + repr=False, + ) def __init_subclass__(cls): warnings.warn( @@ -198,6 +217,12 @@ def __init_subclass__(cls): stacklevel=2, ) + def __attrs_post_init__(self): + if self._resolver is None: + self._resolver = self._registry.resolver_with_root( + resource=specification.create_resource(self.schema), + ) + @classmethod def check_schema(cls, schema, format_checker=_UNSET): Validator = validator_for(cls.META_SCHEMA, default=cls) @@ -276,38 +301,39 @@ def iter_errors(self, instance, _schema=None): ) return - # Temporarily needed to eagerly create a resolver... - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.resolver - scope = id_of(_schema) - if scope: - self.resolver.push_scope(scope) - try: - for k, v in applicable_validators(_schema): - validator = self.VALIDATORS.get(k) - if validator is None: - continue - - errors = validator(self, v, instance, _schema) or () - for error in errors: - # set details if not already set by the called fn - error._set( - validator=k, - validator_value=v, - instance=instance, - schema=_schema, - type_checker=self.TYPE_CHECKER, - ) - if k not in {"if", "$ref"}: - error.schema_path.appendleft(k) - yield error - finally: - if scope: - self.resolver.pop_scope() - - def descend(self, instance, schema, path=None, schema_path=None): - for error in self.evolve(schema=schema).iter_errors(instance): + for k, v in applicable_validators(_schema): + validator = self.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(self, v, instance, _schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=_schema, + type_checker=self.TYPE_CHECKER, + ) + if k not in {"if", "$ref"}: + error.schema_path.appendleft(k) + yield error + + def descend( + self, + instance, + schema, + path=None, + schema_path=None, + resolver=None, + ): + if resolver is None: + resolver = self._resolver.in_subresource( + specification.create_resource(schema), + ) + validator = self.evolve(schema=schema, _resolver=resolver) + for error in validator.iter_errors(instance): if path is not None: error.path.appendleft(path) if schema_path is not None: @@ -325,6 +351,14 @@ def is_type(self, instance, type): raise exceptions.UnknownType(type, instance, self.schema) def _validate_reference(self, ref, instance): + if self._ref_resolver is None: + resolved = self._resolver.lookup(ref) + return self.descend( + instance, + resolved.contents, + resolver=resolved.resolver, + ) + else: resolve = getattr(self._ref_resolver, "resolve", None) if resolve is None: with self._ref_resolver.resolving(ref) as resolved: From a93e88befbda92ac953f1c4f8de0b07e546b852f Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 16 Feb 2023 12:35:25 +0200 Subject: [PATCH 14/33] Claim full support now that we pass all referencing tests. --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 29cdb670b..ea97daa4b 100644 --- a/README.rst +++ b/README.rst @@ -59,8 +59,7 @@ It can also be used from the command line by installing `check-jsonschema `_ and `Draft 2019-09 `_, except for ``dynamicRef`` / ``recursiveRef`` and ``$vocabulary`` (in-progress). - Full support for `Draft 7 `_, `Draft 6 `_, `Draft 4 `_ and `Draft 3 `_ +* Full support for `Draft 2020-12 `_, `Draft 2019-09 `_, `Draft 7 `_, `Draft 6 `_, `Draft 4 `_ and `Draft 3 `_ * `Lazy validation `_ that can iteratively report *all* validation errors. From bdda72350321cca37b2eb64276d388fe441ca622 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 16 Feb 2023 14:38:33 +0200 Subject: [PATCH 15/33] Flail to get Sphinx to find references again. --- docs/conf.py | 28 ++++++++++++++++++---------- jsonschema/protocols.py | 9 ++++----- jsonschema/validators.py | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 06d4a6401..6b1fd0acb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,29 +38,36 @@ # See sphinx-doc/sphinx#10785 _TYPE_ALIASES = { - "jsonschema._format._F", # format checkers + "jsonschema._format._F": ("data", "_F"), } -def _resolve_type_aliases(app, env, node, contnode): - if ( - node["refdomain"] == "py" - and node["reftype"] == "class" - and node["reftarget"] in _TYPE_ALIASES - ): +def _resolve_broken_refs(app, env, node, contnode): + if node["refdomain"] != "py": + return + + if node["reftarget"].startswith("referencing."): # :( :( :( :( :( + node["reftype"] = "data" + from sphinx.ext import intersphinx + return intersphinx.resolve_reference_in_inventory( + env, "referencing", node, contnode, + ) + + 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, - "data", - node["reftarget"], + kind, + target, node, contnode, ) def setup(app): - app.connect("missing-reference", _resolve_type_aliases) + app.connect("missing-reference", _resolve_broken_refs) # = Builders = @@ -116,6 +123,7 @@ def entire_domain(host): intersphinx_mapping = { "python": ("https://docs.python.org/3", None), + "referencing": ("https://referencing.readthedocs.io/en/stable/", None), "ujs": ("https://json-schema.org/understanding-json-schema/", None), } diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index 9d34f61c0..65f58989a 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -11,8 +11,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, Iterable import sys -from referencing.jsonschema import SchemaRegistry - # doing these imports with `try ... except ImportError` doesn't pass mypy # checking because mypy sees `typing._SpecialForm` and # `typing_extensions._SpecialForm` as incompatible @@ -32,6 +30,7 @@ if TYPE_CHECKING: import jsonschema import jsonschema.validators + import referencing.jsonschema from jsonschema.exceptions import ValidationError @@ -73,8 +72,8 @@ class Validator(Protocol): .. deprecated:: v4.18.0 - `RefResolver` has been deprecated in favor of `referencing`, - and with it, this argument. + `RefResolver <_RefResolver>` has been deprecated in favor of + `referencing`, and with it, this argument. format_checker: @@ -119,7 +118,7 @@ class Validator(Protocol): def __init__( self, schema: Mapping | bool, - registry: SchemaRegistry, + registry: referencing.jsonschema.SchemaRegistry, format_checker: jsonschema.FormatChecker | None = None, ) -> None: ... diff --git a/jsonschema/validators.py b/jsonschema/validators.py index b80285686..8ed275bfa 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -786,7 +786,7 @@ class _RefResolver: .. deprecated:: v4.18.0 - `RefResolver` has been deprecated in favor of `referencing`. + ``RefResolver`` has been deprecated in favor of `referencing`. """ _DEPRECATION_MESSAGE = ( From a42bbd95e240772c461b45f564dcaa0a95b0c401 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 16 Feb 2023 20:01:58 +0200 Subject: [PATCH 16/33] Fix the benchmark to pass the right type for remotes again. --- jsonschema/benchmarks/issue232.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonschema/benchmarks/issue232.py b/jsonschema/benchmarks/issue232.py index bf357e911..efd071548 100644 --- a/jsonschema/benchmarks/issue232.py +++ b/jsonschema/benchmarks/issue232.py @@ -6,14 +6,14 @@ from pathlib import Path from pyperf import Runner -from pyrsistent import m +from referencing import Registry from jsonschema.tests._suite import Version import jsonschema issue232 = Version( path=Path(__file__).parent / "issue232", - remotes=m(), + remotes=Registry(), name="issue232", ) diff --git a/pyproject.toml b/pyproject.toml index c7c0a97df..c772881fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "attrs>=22.2.0", "pyrsistent>=0.14.0,!=0.17.0,!=0.17.1,!=0.17.2", "jsonschema-specifications>=2023.03.1", - "referencing>=0.17.1", + "referencing>=0.18.1", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From bd6a7d08d083b77b33961088680fb1d5a22a585b Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 19 Feb 2023 12:43:28 +0200 Subject: [PATCH 17/33] Update docs requirements. --- docs/requirements.txt | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index af3fcb5be..4dc78ede4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -82,7 +82,7 @@ pytz==2022.7.1 # via babel pyyaml==6.0 # via sphinx-autoapi -referencing==0.17.1 +referencing==0.18.6 # via # jsonschema # jsonschema-specifications @@ -127,7 +127,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sphinxcontrib-spelling==7.7.0 +sphinxcontrib-spelling==8.0.0 # via -r docs/requirements.in sphinxext-opengraph==0.8.1 # via -r docs/requirements.in diff --git a/pyproject.toml b/pyproject.toml index c772881fe..e0f9ce0fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "attrs>=22.2.0", "pyrsistent>=0.14.0,!=0.17.0,!=0.17.1,!=0.17.2", "jsonschema-specifications>=2023.03.1", - "referencing>=0.18.1", + "referencing>=0.18.6", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From 33e28824ab6b5f9d29a181382ed731b3faf2f686 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Tue, 21 Feb 2023 13:40:38 +0200 Subject: [PATCH 18/33] Pin to newer pyrsistent. <19.3 can have performance problems on 3.11 where there aren't wheels (and where I think it's falling back to using the pure- Python implementation even on CPython). Here it's ~2x slower. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e0f9ce0fa..2cb3ea577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dynamic = ["version", "readme"] dependencies = [ "attrs>=22.2.0", - "pyrsistent>=0.14.0,!=0.17.0,!=0.17.1,!=0.17.2", + "pyrsistent>=0.19.3", "jsonschema-specifications>=2023.03.1", "referencing>=0.18.6", From fcea5ad0472b6141b1f1b686200fb9f26c0af326 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Tue, 21 Feb 2023 13:52:59 +0200 Subject: [PATCH 19/33] Tighten up a type in the tests. --- jsonschema/tests/_suite.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py index 7f14ca5f1..6be05bed9 100644 --- a/jsonschema/tests/_suite.py +++ b/jsonschema/tests/_suite.py @@ -49,7 +49,7 @@ def _find_suite(): class Suite: _root: Path = field(factory=_find_suite) - _remotes: Registry = field(init=False) + _remotes: referencing.jsonschema.SchemaRegistry = field(init=False) def __attrs_post_init__(self): jsonschema_suite = self._root.joinpath("bin", "jsonschema_suite") @@ -101,7 +101,7 @@ def version(self, name) -> Version: class Version: _path: Path - _remotes: Registry + _remotes: referencing.jsonschema.SchemaRegistry name: str @@ -200,7 +200,7 @@ class _Test: valid: bool - _remotes: Registry + _remotes: referencing.jsonschema.SchemaRegistry comment: str | None = None From 34d19dc706a1d57640e6f84333500e5f734dbf9a Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 22 Feb 2023 14:00:02 +0200 Subject: [PATCH 20/33] Add some prose documentation on the new referencing API. --- docs/faq.rst | 52 +----- docs/index.rst | 1 + docs/referencing.rst | 314 +++++++++++++++++++++++++++++++++++++ docs/requirements.txt | 4 +- docs/spelling-wordlist.txt | 3 + pyproject.toml | 2 +- 6 files changed, 322 insertions(+), 54 deletions(-) create mode 100644 docs/referencing.rst diff --git a/docs/faq.rst b/docs/faq.rst index 2236390c2..5ae3e62e4 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -88,6 +88,7 @@ The JSON object ``{}`` is simply the Python `dict` ``{}``, and a JSON Schema lik Specifically, in the case where `jsonschema` is asked to resolve a remote reference, it has no choice but to assume that the remote reference is serialized as JSON, and to deserialize it using the `json` module. One cannot today therefore reference some remote piece of YAML and have it deserialized into Python objects by this library without doing some additional work. + See `Resolving References to Schemas Written in YAML ` for details. In practice what this means for JSON-like formats like YAML and TOML is that indeed one can generally schematize and then validate them exactly as if they were JSON by simply first deserializing them using libraries like ``PyYAML`` or the like, and passing the resulting Python objects into functions within this library. @@ -99,57 +100,6 @@ In such cases one is recommended to first pre-process the data such that the res In the previous example, if the desired behavior is to transparently coerce numeric properties to strings, as Javascript might, then do the conversion explicitly before passing data to this library. -How do I configure a base URI for $ref resolution using local files? --------------------------------------------------------------------- - -`jsonschema` supports loading schemas from the filesystem. - -The most common mistake when configuring reference resolution to retrieve schemas from the local filesystem is to specify a base URI which points to a directory, but forget to add a trailing slash. - -For example, given a directory ``/tmp/foo/`` with ``bar/schema.json`` within it, you should use something like: - -.. code-block:: python - - from pathlib import Path - - import jsonschema.validators - - path = Path("/tmp/foo") - resolver = jsonschema.validators.RefResolver( - base_uri=f"{path.as_uri()}/", - referrer=True, - ) - jsonschema.validate( - instance={}, - schema={"$ref": "bar/schema.json"}, - resolver=resolver, - ) - -where note: - - * the base URI has a trailing slash, even though - `pathlib.PurePath.as_uri` does not add it! - * any relative refs are now given relative to the provided directory - -If you forget the trailing slash, you'll find references are resolved a -directory too high. - -You're likely familiar with this behavior from your browser. If you -visit a page at ``https://example.com/foo``, then links on it like -```` take you to ``https://example.com/bar``, not -``https://example.com/foo/bar``. For this reason many sites will -redirect ``https://example.com/foo`` to ``https://example.com/foo/``, -i.e. add the trailing slash, so that relative links on the page will keep the -last path component. - -There are, in summary, 2 ways to do this properly: - -* Remember to include a trailing slash, so your base URI is - ``file:///foo/bar/`` rather than ``file:///foo/bar``, as shown above -* Use a file within the directory as your base URI rather than the - directory itself, i.e. ``file://foo/bar/baz.json``, which will of course - cause ``baz.json`` to be removed while resolving relative URIs - Why doesn't my schema's default property set the default on my instance? ------------------------------------------------------------------------ diff --git a/docs/index.rst b/docs/index.rst index d66aa8b96..949ab448e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,7 @@ Contents validate errors + referencing creating faq api/index diff --git a/docs/referencing.rst b/docs/referencing.rst new file mode 100644 index 000000000..20a1ca91a --- /dev/null +++ b/docs/referencing.rst @@ -0,0 +1,314 @@ +========================= +JSON (Schema) Referencing +========================= + +The JSON Schema :kw:`$ref` and :kw:`$dynamicRef` keywords allow schema authors to combine multiple schemas (or subschemas) together for reuse or deduplication. + +The `referencing ` library was written in order to provide a simple, well-behaved and well-tested implementation of this kind of reference resolution [1]_. +It has its own documentation, but this page serves as a quick introduction which is tailored more specifically to JSON Schema, and even more specifically to how to configure `referencing ` for use with `Validator` objects in order to customize the behavior of :kw:`$ref` and friends in your schemas. + +Configuring `jsonschema` for custom referencing behavior is essentially a two step process: + + * Create a `referencing.Registry` object that behaves the way you wish + + * Pass the `referencing.Registry` to your `Validator` when instantiating it + +The examples below essentially follow these two steps. + +.. [1] One that in fact is independent of this `jsonschema` library itself, and may some day be used by other tools or implementations. + + +Common Scenarios +---------------- + +.. _in-memory-schemas: + +Making Additional In-Memory Schemas Available +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The most common scenario one is likely to encounter is the desire to include a small number of additional in-memory schemas, making them available for use during validation. + +For instance, imagine the below schema for non-negative integers: + +.. code:: json + + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "minimum": 0 + } + +We may wish to have other schemas we write be able to make use of this schema, and refer to it as ``http://example.com/nonneg-int-schema`` and/or as ``urn:nonneg-integer-schema``. + +To do so we make use of APIs from the referencing library to create a `referencing.Registry` which maps the URIs above to this schema: + +.. code:: python + + from referencing import Registry, Resource + schema = Resource.from_contents( + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "minimum": 0, + }, + ) + registry = Registry().with_resources( + [ + ("http://example.com/nonneg-int-schema", schema), + ("urn:nonneg-integer-schema", schema), + ], + ) + +What's above is likely mostly self-explanatory, other than the presence of the `referencing.Resource.from_contents` function. +Its purpose is to convert a piece of "opaque" JSON (or really a Python `dict` containing deserialized JSON) into an object which indicates what *version* of JSON Schema the schema is meant to be interpreted under. +Calling it will inspect a :kw:`$schema` keyword present in the given schema and use that to associate the JSON with an appropriate `specification `. +If your schemas do not contain ``$schema`` dialect identifiers, and you intend for them to be interpreted always under a specific dialect -- say Draft 2020-12 of JSON Schema -- you may instead use e.g.: + +.. code:: python + + from referencing import Registry, Resource + from referencing.jsonschema import DRAFT2020212 + schema = DRAFT202012.create_resource({"type": "integer", "minimum": 0}) + registry = Registry().with_resources( + [ + ("http://example.com/nonneg-int-schema", schema), + ("urn:nonneg-integer-schema", schema), + ], + ) + +which has the same functional effect. + +You can now pass this registry to your `Validator`, which allows a schema passed to it to make use of the aforementioned URIs to refer to our non-negative integer schema. +Here for instance is an example which validates that instances are JSON objects with non-negative integral values: + +.. code:: python + + from jsonschema import Draft202012Validator + validator = Draft202012Validator( + { + "type": "object", + "additionalProperties": {"$ref": "urn:nonneg-integer-schema"}, + }, + registry=registry, # the critical argument, our registry from above + ) + validator.validate({"foo": 37}) + validator.validate({"foo": -37}) # Uh oh! + +.. _ref-filesystem: + +Resolving References from the File System +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another common request from schema authors is to be able to map URIs to the file system, perhaps while developing a set of schemas in different local files. +The referencing library supports doing so dynamically by configuring a callable which can be used to retrieve any schema which is *not* already pre-loaded in the manner described `above `. + +Here we resolve any schema beginning with ``http://localhost`` to a directory ``/tmp/schemas`` on the local filesystem (note of course that this will not work if run directly unless you have populated that directory with some schemas): + +.. code:: python + + from pathlib import Path + import json + + from referencing import Registry, Resource + from referencing.exceptions import NoSuchResource + + SCHEMAS = Path("/tmp/schemas") + + def retrieve_from_filesystem(uri: str): + if not uri.startswith("http://localhost/"): + raise NoSuchResource(ref=uri) + path = SCHEMAS / Path(uri.removeprefix("http://localhost/")) + contents = json.loads(path.read_text()) + return Resource.from_contents(contents) + + registry = Registry(retrieve=retrieve_from_filesystem) + +Such a registry can then be used with `Validator` objects in the same way shown above, and any such references to URIs which are not already in-memory will be retrieved from the configured directory. + +We can mix the two examples above if we wish for some in-memory schemas to be available in addition to the filesystem schemas, e.g.: + +.. code:: python + + from referencing.jsonschema import DRAFT7 + registry = Registry(retrieve=retrieve_from_filesystem).with_resource( + "urn:non-empty-array", DRAFT7.create_resource({"type": "array", "minItems": 1}), + ) + +where we've made use of the similar `referencing.Registry.with_resource` function to add a single additional resource. + +Resolving References to Schemas Written in YAML +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Generalizing slightly, the retrieval function provided need not even assume that it is retrieving JSON. +As long as you deserialize what you have retrieved into Python objects, you may equally be retrieving references to YAML documents or any other format. + +Here for instance we retrieve YAML documents in a way similar to the `above ` using PyYAML: + +.. code:: python + + from pathlib import Path + import yaml + + from referencing import Registry, Resource + from referencing.exceptions import NoSuchResource + + SCHEMAS = Path("/tmp/yaml-schemas") + + def retrieve_yaml(uri: str): + if not uri.startswith("http://localhost/"): + raise NoSuchResource(ref=uri) + path = SCHEMAS / Path(uri.removeprefix("http://localhost/")) + contents = yaml.safe_load(path.read_text()) + return Resource.from_contents(contents) + + registry = Registry(retrieve=retrieve_yaml) + +.. note:: + + Not all YAML fits within the JSON data model. + + JSON Schema is defined specifically for JSON, and has well-defined behavior strictly for Python objects which could have possibly existed as JSON. + + If you stick to the subset of YAML for which this is the case then you shouldn't have issue, but if you pass schemas (or instances) around whose structure could never have possibly existed as JSON (e.g. a mapping whose keys are not strings), all bets are off. + +One could similarly imagine a retrieval function which switches on whether to call ``yaml.safe_load`` or ``json.loads`` by file extension (or some more reliable mechanism) and thereby support retrieving references of various different file formats. + +.. _http: + +Automatically Retrieving Resources Over HTTP +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the general case, the JSON Schema specifications tend to `discourage `_ implementations (like this one) from automatically retrieving references over the network, or even assuming such a thing is feasible (as schemas may be identified by URIs which are strictly identifiers, and not necessarily downloadable from the URI even when such a thing is sensical). + +However, if you as a schema author are in a situation where you indeed do wish to do so for convenience (and understand the implications of doing so), you may do so by making use of the ``retrieve`` argument to `referencing.Registry`. + +Here is how one would configure a registry to automatically retrieve schemas from the `JSON Schema Store `_ on the fly using the `httpx `_: + +.. code:: python + + from referencing import Registry, Resource + import httpx + + def retrieve_via_httpx(uri: str): + response = httpx.get(uri) + return Resource.from_contents(response.json()) + + registry = Registry(retrieve=retrieve_via_httpx) + +Given such a registry, we can now, for instance, validate instances against schemas from the schema store by passing the ``registry`` we configured to our `Validator` as in previous examples: + +.. code:: python + + from jsonschema import Draft202012Validator + Draft202012Validator( + {"$ref": "https://json.schemastore.org/pyproject.json"}, + registry=registry, + ).validate({"project": {"name": 12}}) + +which should in this case indicate the example data is invalid: + +.. code:: python + + Traceback (most recent call last): + File "example.py", line 14, in + ).validate({"project": {"name": 12}}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "jsonschema/validators.py", line 345, in validate + raise error + jsonschema.exceptions.ValidationError: 12 is not of type 'string' + + Failed validating 'type' in schema['properties']['project']['properties']['name']: + {'pattern': '^([a-zA-Z\\d]|[a-zA-Z\\d][\\w.-]*[a-zA-Z\\d])$', + 'title': 'Project name', + 'type': 'string'} + + On instance['project']['name']: + 12 + +Retrieving resources from a SQLite database or some other network-accessible resource should be more or less similar, replacing the HTTP client with one for your database of course. + +.. warning:: + + Be sure you understand the security implications of the reference resolution you configure. + And if you accept untrusted schemas, doubly sure! + + You wouldn't want a user causing your machine to go off and retrieve giant files off the network by passing it a ``$ref`` to some huge blob, or exploiting similar vulnerabilities in your setup. + + +Migrating From ``RefResolver`` +------------------------------ + +Older versions of `jsonschema` used a different object -- `_RefResolver` -- for reference resolution, which you a schema author may already be configuring for your own use. + +`_RefResolver` is now fully deprecated and replaced by the use of `referencing.Registry` as shown in examples above. + +If you are not already constructing your own `_RefResolver`, this change should be transparent to you (or even recognizably improved, as the point of the migration was to improve the quality of the referencing implementation and enable some new functionality). + +If you *were* configuring your own `_RefResolver`, here's how to migrate to the newer APIs: + +The ``store`` argument +~~~~~~~~~~~~~~~~~~~~~~ + +`_RefResolver`\ 's ``store`` argument was essentially the equivalent of `referencing.Registry`\ 's in-memory schema storage. + +If you currently pass a set of schemas via e.g.: + +.. code:: python + + from jsonschema import Draft202012Validator, RefResolver + resolver = RefResolver.from_schema( + schema={"title": "my schema"}, + store={"http://example.com": {"type": "integer"}}, + ) + validator = Draft202012Validator( + {"$ref": "http://example.com"}, + resolver=resolver, + ) + validator.validate("foo") + +you should be able to simply move to something like: + +.. code:: python + + from referencing import Registry + from referencing.jsonschema import DRAFT202012 + + from jsonschema import Draft202012Validator + + registry = Registry().with_resource( + "http://example.com", + DRAFT202012.create_resource({"type": "integer"}), + ) + validator = Draft202012Validator( + {"$ref": "http://example.com"}, + registry=registry, + ) + validator.validate("foo") + +Handlers +~~~~~~~~ + +The ``handlers`` functionality from `_RefResolver` was a way to support additional HTTP schemes for schema retrieval. + +Here you should move to a custom ``retrieve`` function which does whatever you'd like. +E.g. in pseudocode: + +.. code:: python + + from urllib.parse import urlsplit + + def retrieve(uri: str): + parsed = urlsplit(uri) + if parsed.scheme == "file": + ... + elif parsed.scheme == "custom": + ... + + registry = Registry(retrieve=retrieve) + + +Other Key Functional Differences +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whilst `_RefResolver` *did* automatically retrieve remote references (against the recommendation of the spec, and in a way which therefore could lead to questionable security concerns when combined with untrusted schemas), `referencing.Registry` does *not* do so. +If you rely on this behavior, you should follow the `above example of retrieving resources over HTTP `. diff --git a/docs/requirements.txt b/docs/requirements.txt index 4dc78ede4..1df64d85e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -82,7 +82,7 @@ pytz==2022.7.1 # via babel pyyaml==6.0 # via sphinx-autoapi -referencing==0.18.6 +referencing==0.20.0 # via # jsonschema # jsonschema-specifications @@ -113,7 +113,7 @@ sphinx-basic-ng==1.0.0b1 # via furo sphinx-copybutton==0.5.1 # via -r docs/requirements.in -sphinx-json-schema-spec==2.3.3 +sphinx-json-schema-spec==2023.2.2 # via -r docs/requirements.in sphinxcontrib-applehelp==1.0.4 # via sphinx diff --git a/docs/spelling-wordlist.txt b/docs/spelling-wordlist.txt index a2c7d5649..38eb5371f 100644 --- a/docs/spelling-wordlist.txt +++ b/docs/spelling-wordlist.txt @@ -10,6 +10,7 @@ callables # non-codeblocked cls from autoapi cls deque +deduplication dereferences deserialize deserialized @@ -30,6 +31,7 @@ online outputter pre programmatically +pseudocode recurses regex repr @@ -41,6 +43,7 @@ submodules subschema subschemas subscopes +untrusted uri validator validators diff --git a/pyproject.toml b/pyproject.toml index 2cb3ea577..7003071a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "attrs>=22.2.0", "pyrsistent>=0.19.3", "jsonschema-specifications>=2023.03.1", - "referencing>=0.18.6", + "referencing>=0.20.0", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From 8c4cd7c52ef783c3f0aa1c15342e5c8c9d4360f5 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 22 Feb 2023 15:42:38 +0200 Subject: [PATCH 21/33] These pass now actually. Closes: #523 --- jsonschema/tests/test_validators.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index c62498bf0..c21d71f8c 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1789,18 +1789,14 @@ def test_False_is_not_a_schema(self): self.Validator.check_schema(False) self.assertIn("False is not of type", str(e.exception)) - @unittest.skip(bug(523)) def test_True_is_not_a_schema_even_if_you_forget_to_check(self): - resolver = validators._RefResolver("", {}) with self.assertRaises(Exception) as e: - self.Validator(True, resolver=resolver).validate(12) + self.Validator(True).validate(12) self.assertNotIsInstance(e.exception, exceptions.ValidationError) - @unittest.skip(bug(523)) def test_False_is_not_a_schema_even_if_you_forget_to_check(self): - resolver = validators._RefResolver("", {}) with self.assertRaises(Exception) as e: - self.Validator(False, resolver=resolver).validate(12) + self.Validator(False).validate(12) self.assertNotIsInstance(e.exception, exceptions.ValidationError) From e922e795de8f331b08664ab211ff8a30e6490942 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 22 Feb 2023 16:21:37 +0200 Subject: [PATCH 22/33] Style --- jsonschema/tests/test_validators.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index c21d71f8c..2d50b7196 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -10,7 +10,6 @@ import os import sys import tempfile -import unittest import warnings import attr @@ -22,7 +21,6 @@ protocols, validators, ) -from jsonschema.tests._helpers import bug def fail(validator, errors, instance, schema): From 25f40e56590135dffb82a2cb69eb8f4ac2876f22 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 23 Feb 2023 10:58:19 +0200 Subject: [PATCH 23/33] Again bump the referencing version. --- docs/requirements.txt | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 1df64d85e..87e289489 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -82,7 +82,7 @@ pytz==2022.7.1 # via babel pyyaml==6.0 # via sphinx-autoapi -referencing==0.20.0 +referencing==0.21.0 # via # jsonschema # jsonschema-specifications diff --git a/pyproject.toml b/pyproject.toml index 7003071a6..19ba652a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "attrs>=22.2.0", "pyrsistent>=0.19.3", "jsonschema-specifications>=2023.03.1", - "referencing>=0.20.0", + "referencing>=0.21.0", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From 787dbc9a143b0631024999a2a3dd56619d42b39d Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 23 Feb 2023 14:55:45 +0200 Subject: [PATCH 24/33] Re-add the direct test of RefResolver's deprecation. --- jsonschema/tests/test_deprecations.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py index d21065fae..537059967 100644 --- a/jsonschema/tests/test_deprecations.py +++ b/jsonschema/tests/test_deprecations.py @@ -141,6 +141,20 @@ def test_Validator_resolver(self): ), ) + def test_RefResolver(self): + """ + As of v4.18.0, RefResolver is fully deprecated. + """ + + message = "jsonschema.RefResolver is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import RefResolver # noqa: F401 + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.validators import RefResolver # noqa: F401, F811 + self.assertEqual(w.filename, __file__) + def test_Validator_subclassing(self): """ As of v4.12.0, subclassing a validator class produces an explicit From 3801d9aea0e044ff8b190bff750594aa18dee67e Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 23 Feb 2023 15:01:46 +0200 Subject: [PATCH 25/33] Improve error messages for deprecation tests when they fail. --- jsonschema/tests/test_deprecations.py | 144 +++++++------------------- 1 file changed, 37 insertions(+), 107 deletions(-) diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py index 537059967..224f45989 100644 --- a/jsonschema/tests/test_deprecations.py +++ b/jsonschema/tests/test_deprecations.py @@ -12,15 +12,11 @@ def test_version(self): As of v4.0.0, __version__ is deprecated in favor of importlib.metadata. """ - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.__version__ is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema import __version__ # noqa self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.__version__ is deprecated", - ), - ) def test_validators_ErrorTree(self): """ @@ -28,15 +24,11 @@ def test_validators_ErrorTree(self): deprecated in favor of doing so from jsonschema.exceptions. """ - with self.assertWarns(DeprecationWarning) as w: + message = "Importing ErrorTree from jsonschema.validators is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema.validators import ErrorTree # noqa self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Importing ErrorTree from jsonschema.validators is deprecated", - ), - ) def test_validators_validators(self): """ @@ -44,16 +36,12 @@ def test_validators_validators(self): deprecated. """ - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.validators.validators is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: value = validators.validators - self.assertEqual(value, validators._VALIDATORS) + self.assertEqual(value, validators._VALIDATORS) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.validators.validators is deprecated", - ), - ) def test_validators_meta_schemas(self): """ @@ -61,16 +49,12 @@ def test_validators_meta_schemas(self): deprecated. """ - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.validators.meta_schemas is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: value = validators.meta_schemas - self.assertEqual(value, validators._META_SCHEMAS) + self.assertEqual(value, validators._META_SCHEMAS) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.validators.meta_schemas is deprecated", - ), - ) def test_RefResolver_in_scope(self): """ @@ -78,16 +62,12 @@ def test_RefResolver_in_scope(self): """ resolver = validators._RefResolver.from_schema({}) - with self.assertWarns(DeprecationWarning) as w: + message = "jsonschema.RefResolver.in_scope is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: with resolver.in_scope("foo"): pass self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "jsonschema.RefResolver.in_scope is deprecated ", - ), - ) def test_Validator_is_valid_two_arguments(self): """ @@ -96,16 +76,12 @@ def test_Validator_is_valid_two_arguments(self): """ validator = validators.Draft7Validator({}) - with self.assertWarns(DeprecationWarning) as w: + message = "Passing a schema to Validator.is_valid is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: result = validator.is_valid("foo", {"type": "number"}) self.assertFalse(result) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Passing a schema to Validator.is_valid is deprecated ", - ), - ) def test_Validator_iter_errors_two_arguments(self): """ @@ -114,16 +90,12 @@ def test_Validator_iter_errors_two_arguments(self): """ validator = validators.Draft7Validator({}) - with self.assertWarns(DeprecationWarning) as w: + message = "Passing a schema to Validator.iter_errors is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: error, = validator.iter_errors("foo", {"type": "number"}) self.assertEqual(error.validator, "type") self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Passing a schema to Validator.iter_errors is deprecated ", - ), - ) def test_Validator_resolver(self): """ @@ -131,15 +103,11 @@ def test_Validator_resolver(self): """ validator = validators.Draft7Validator({}) - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing Draft7Validator.resolver is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: self.assertIsInstance(validator.resolver, validators._RefResolver) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing Draft7Validator.resolver is ", - ), - ) def test_RefResolver(self): """ @@ -167,16 +135,14 @@ def test_Validator_subclassing(self): A future version will explicitly raise an error. """ - with self.assertWarns(DeprecationWarning) as w: + message = "Subclassing validator classes is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: class Subclass(validators.Draft202012Validator): pass self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith("Subclassing validator classes is "), - ) - with self.assertWarns(DeprecationWarning) as w: + with self.assertWarnsRegex(DeprecationWarning, message) as w: class AnotherSubclass(validators.create(meta_schema={})): pass @@ -188,13 +154,11 @@ def test_FormatChecker_cls_checks(self): self.addCleanup(FormatChecker.checkers.pop, "boom", None) - with self.assertWarns(DeprecationWarning) as w: + message = "FormatChecker.cls_checks " + with self.assertWarnsRegex(DeprecationWarning, message) as w: FormatChecker.cls_checks("boom") self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith("FormatChecker.cls_checks "), - ) def test_draftN_format_checker(self): """ @@ -202,7 +166,8 @@ def test_draftN_format_checker(self): in favor of Validator.FORMAT_CHECKER. """ - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.draft202012_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema import draft202012_format_checker # noqa self.assertIs( @@ -210,14 +175,9 @@ def test_draftN_format_checker(self): validators.Draft202012Validator.FORMAT_CHECKER, ) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.draft202012_format_checker is ", - ), - msg=w.warning, - ) - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.draft201909_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema import draft201909_format_checker # noqa self.assertIs( @@ -225,14 +185,9 @@ def test_draftN_format_checker(self): validators.Draft201909Validator.FORMAT_CHECKER, ) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.draft201909_format_checker is ", - ), - msg=w.warning, - ) - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.draft7_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema import draft7_format_checker # noqa self.assertIs( @@ -240,14 +195,9 @@ def test_draftN_format_checker(self): validators.Draft7Validator.FORMAT_CHECKER, ) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.draft7_format_checker is ", - ), - msg=w.warning, - ) - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.draft6_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema import draft6_format_checker # noqa self.assertIs( @@ -255,14 +205,9 @@ def test_draftN_format_checker(self): validators.Draft6Validator.FORMAT_CHECKER, ) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.draft6_format_checker is ", - ), - msg=w.warning, - ) - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.draft4_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema import draft4_format_checker # noqa self.assertIs( @@ -270,14 +215,9 @@ def test_draftN_format_checker(self): validators.Draft4Validator.FORMAT_CHECKER, ) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.draft4_format_checker is ", - ), - msg=w.warning, - ) - with self.assertWarns(DeprecationWarning) as w: + message = "Accessing jsonschema.draft3_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema import draft3_format_checker # noqa self.assertIs( @@ -285,12 +225,6 @@ def test_draftN_format_checker(self): validators.Draft3Validator.FORMAT_CHECKER, ) self.assertEqual(w.filename, __file__) - self.assertTrue( - str(w.warning).startswith( - "Accessing jsonschema.draft3_format_checker is ", - ), - msg=w.warning, - ) with self.assertRaises(ImportError): from jsonschema import draft1234_format_checker # noqa @@ -300,16 +234,12 @@ def test_import_cli(self): As of v4.17.0, importing jsonschema.cli is deprecated. """ - with self.assertWarns(DeprecationWarning) as w: + message = "The jsonschema CLI is deprecated and will be removed " + with self.assertWarnsRegex(DeprecationWarning, message) as w: import jsonschema.cli importlib.reload(jsonschema.cli) self.assertEqual(w.filename, importlib.__file__) - self.assertTrue( - str(w.warning).startswith( - "The jsonschema CLI is deprecated and will be removed ", - ), - ) def test_cli(self): """ From 19bdf6191a8e209584b18e7a3d4ec55cad9037e6 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 23 Feb 2023 15:35:44 +0200 Subject: [PATCH 26/33] Three more exception-related deprecations. * RefResolutionError is deprecated entirely. Use referencing.Registry-based APIs, and catch referencing.exceptions.Unresolvable if you really want to ignore referencing related issues. * FormatError should now be imported from jsonschema.exceptions only, not from the package root. * ErrorTree should now be imported from jsonschema.exceptions only, not from the package root. --- docs/errors.rst | 1 + jsonschema/__init__.py | 36 ++++++++++++++++---- jsonschema/exceptions.py | 20 ++++++++++- jsonschema/tests/test_cli.py | 6 ++-- jsonschema/tests/test_deprecations.py | 48 ++++++++++++++++++++++++++- jsonschema/tests/test_format.py | 3 +- jsonschema/tests/test_validators.py | 6 ++-- jsonschema/validators.py | 6 ++-- 8 files changed, 107 insertions(+), 19 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 7cfb502bf..5b0230f7e 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -273,6 +273,7 @@ error objects. .. testcode:: + from jsonschema.exceptions import ErrorTree tree = ErrorTree(v.iter_errors(instance)) As you can see, `jsonschema.exceptions.ErrorTree` takes an diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py index 8f6b0a499..ad7affc89 100644 --- a/jsonschema/__init__.py +++ b/jsonschema/__init__.py @@ -12,13 +12,7 @@ from jsonschema._format import FormatChecker from jsonschema._types import TypeChecker -from jsonschema.exceptions import ( - ErrorTree, - FormatError, - RefResolutionError, - SchemaError, - ValidationError, -) +from jsonschema.exceptions import SchemaError, ValidationError from jsonschema.protocols import Validator from jsonschema.validators import ( Draft3Validator, @@ -55,6 +49,34 @@ def __getattr__(name): stacklevel=2, ) return _RefResolver + elif name == "ErrorTree": + warnings.warn( + "Importing ErrorTree directly from the jsonschema package " + "is deprecated and will become an ImportError. Import it from " + "jsonschema.exceptions instead.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import ErrorTree + return ErrorTree + elif name == "FormatError": + warnings.warn( + "Importing FormatError directly from the jsonschema package " + "is deprecated and will become an ImportError. Import it from " + "jsonschema.exceptions instead.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import FormatError + return FormatError + elif name == "RefResolutionError": + from jsonschema.exceptions import _RefResolutionError + warnings.warn( + _RefResolutionError._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolutionError format_checkers = { "draft3_format_checker": Draft3Validator, diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 149d83890..5e88d7465 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -9,6 +9,7 @@ from typing import ClassVar import heapq import itertools +import warnings import attr @@ -20,6 +21,17 @@ _unset = _utils.Unset() +def __getattr__(name): + if name == "RefResolutionError": + warnings.warn( + _RefResolutionError._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolutionError + raise AttributeError(f"module {__name__} has no attribute {name}") + + class _Error(Exception): _word_for_schema_in_error_message: ClassVar[str] @@ -181,11 +193,17 @@ class SchemaError(_Error): @attr.s(hash=True) -class RefResolutionError(Exception): +class _RefResolutionError(Exception): """ A ref could not be resolved. """ + _DEPRECATION_MESSAGE = ( + "jsonschema.exceptions.RefResolutionError is deprecated as of version " + "4.18.0. If you wish to catch potential reference resolution errors, " + "directly catch referencing.exceptions.Unresolvable." + ) + _cause = attr.ib() def __str__(self): diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py index 84f8812fe..6d5873aae 100644 --- a/jsonschema/tests/test_cli.py +++ b/jsonschema/tests/test_cli.py @@ -20,9 +20,9 @@ from jsonschema import Draft4Validator, Draft202012Validator from jsonschema.exceptions import ( - RefResolutionError, SchemaError, ValidationError, + _RefResolutionError, ) from jsonschema.validators import _LATEST_VERSION, validate @@ -747,7 +747,7 @@ def test_nonexistent_file_with_explicit_base_uri(self): schema = '{"$ref": "someNonexistentFile.json#definitions/num"}' instance = "1" - with self.assertRaises(RefResolutionError) as e: + with self.assertRaises(_RefResolutionError) as e: self.assertOutputs( files=dict( some_schema=schema, @@ -766,7 +766,7 @@ def test_invalid_explicit_base_uri(self): schema = '{"$ref": "foo.json#definitions/num"}' instance = "1" - with self.assertRaises(RefResolutionError) as e: + with self.assertRaises(_RefResolutionError) as e: self.assertOutputs( files=dict( some_schema=schema, diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py index 224f45989..2beb02ff6 100644 --- a/jsonschema/tests/test_deprecations.py +++ b/jsonschema/tests/test_deprecations.py @@ -3,7 +3,7 @@ import subprocess import sys -from jsonschema import FormatChecker, validators +from jsonschema import FormatChecker, exceptions, validators class TestDeprecations(TestCase): @@ -28,6 +28,33 @@ def test_validators_ErrorTree(self): with self.assertWarnsRegex(DeprecationWarning, message) as w: from jsonschema.validators import ErrorTree # noqa + self.assertEqual(ErrorTree, exceptions.ErrorTree) + self.assertEqual(w.filename, __file__) + + def test_import_ErrorTree(self): + """ + As of v4.18.0, importing ErrorTree from the package root is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing ErrorTree directly from the jsonschema package " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import ErrorTree # noqa + + self.assertEqual(ErrorTree, exceptions.ErrorTree) + self.assertEqual(w.filename, __file__) + + def test_import_FormatError(self): + """ + As of v4.18.0, importing FormatError from the package root is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing FormatError directly from the jsonschema package " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import FormatError # noqa + + self.assertEqual(FormatError, exceptions.FormatError) self.assertEqual(w.filename, __file__) def test_validators_validators(self): @@ -123,6 +150,25 @@ def test_RefResolver(self): from jsonschema.validators import RefResolver # noqa: F401, F811 self.assertEqual(w.filename, __file__) + def test_RefResolutionError(self): + """ + As of v4.18.0, RefResolutionError is deprecated in favor of directly + catching errors from the referencing library. + """ + + message = "jsonschema.exceptions.RefResolutionError is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import RefResolutionError # noqa: F401 + + self.assertEqual(RefResolutionError, exceptions._RefResolutionError) + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.exceptions import RefResolutionError # noqa + + self.assertEqual(RefResolutionError, exceptions._RefResolutionError) + self.assertEqual(w.filename, __file__) + def test_Validator_subclassing(self): """ As of v4.12.0, subclassing a validator class produces an explicit diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py index 5dd06cfaf..a5a1d0c4f 100644 --- a/jsonschema/tests/test_format.py +++ b/jsonschema/tests/test_format.py @@ -4,7 +4,8 @@ from unittest import TestCase -from jsonschema import FormatChecker, FormatError, ValidationError +from jsonschema import FormatChecker, ValidationError +from jsonschema.exceptions import FormatError from jsonschema.validators import Draft4Validator BOOM = ValueError("Boom!") diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 2d50b7196..6a5756b2b 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -2285,15 +2285,15 @@ def handler(url): ref = "foo://bar" resolver = validators._RefResolver("", {}, handlers={"foo": handler}) - with self.assertRaises(exceptions.RefResolutionError) as err: + with self.assertRaises(exceptions._RefResolutionError) as err: with resolver.resolving(ref): self.fail("Shouldn't get this far!") # pragma: no cover - self.assertEqual(err.exception, exceptions.RefResolutionError(error)) + self.assertEqual(err.exception, exceptions._RefResolutionError(error)) def test_helpful_error_message_on_failed_pop_scope(self): resolver = validators._RefResolver("", {}) resolver.pop_scope() - with self.assertRaises(exceptions.RefResolutionError) as exc: + with self.assertRaises(exceptions._RefResolutionError) as exc: resolver.pop_scope() self.assertIn("Failed to pop the scope", str(exc.exception)) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 8ed275bfa..bd135444a 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -884,7 +884,7 @@ def pop_scope(self): try: self._scopes_stack.pop() except IndexError: - raise exceptions.RefResolutionError( + raise exceptions._RefResolutionError( "Failed to pop the scope from an empty stack. " "`pop_scope()` should only be called once for every " "`push_scope()`", @@ -1000,7 +1000,7 @@ def resolve_from_url(self, url): try: document = self.resolve_remote(url) except Exception as exc: - raise exceptions.RefResolutionError(exc) + raise exceptions._RefResolutionError(exc) return self.resolve_fragment(document, fragment) @@ -1054,7 +1054,7 @@ def find(key): try: document = document[part] except (TypeError, LookupError): - raise exceptions.RefResolutionError( + raise exceptions._RefResolutionError( f"Unresolvable JSON pointer: {fragment!r}", ) From 84199e984aba5f2c6bf5b121eb95faedc53951fc Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Mon, 27 Feb 2023 11:37:40 +0200 Subject: [PATCH 27/33] Elaborate a bit more in the referencing doc --- docs/referencing.rst | 61 +++++++++++++++++++++++++++++++++++++- docs/requirements.txt | 8 ++--- docs/spelling-wordlist.txt | 1 + 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/docs/referencing.rst b/docs/referencing.rst index 20a1ca91a..fc95e0dab 100644 --- a/docs/referencing.rst +++ b/docs/referencing.rst @@ -18,6 +18,53 @@ The examples below essentially follow these two steps. .. [1] One that in fact is independent of this `jsonschema` library itself, and may some day be used by other tools or implementations. +Introduction to the `referencing ` API +--------------------------------------------------------- + +There are 3 main objects to be aware of in the `referencing` API: + + * `referencing.Registry`, which represents a specific immutable set of JSON Schemas (either in-memory or retrievable) + * `referencing.Specification`, which represents a specific *version* of the JSON Schema specification, which can have differing referencing behavior. + JSON Schema-specific specifications live in the `referencing.jsonschema` module and are named like `referencing.jsonschema.DRAFT202012`. + * `referencing.Resource`, which represents a specific JSON Schema (often a Python `dict`) *along* with a specific `referencing.Specification` it is to be interpreted under. + +As a concrete example, the simple 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`. + +.. seealso:: the JSON Schema :kw:`$schema` keyword + + Which should generally be used to remove all ambiguity and identify *internally* to the schema what version it is written for. + +A schema may be identified via one or more URIs, either because they contain an :kw:`$id` keyword (in suitable versions of the JSON Schema specification) which indicates their canonical URI, or simply because you wish to externally associate a URI with the schema, regardless of whether it contains an ``$id`` keyword. +You could add the aforementioned simple schema 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 + schema = Resource(contents={"type": "integer"}, specification=DRAFT202012) + registry = Registry().with_resource(uri="http://example.com/my/schema", resource=schema) + print(registry) + +.. testoutput:: + + + +.. note:: + + `referencing.Registry` is an entirely immutable object. + All of its methods which add schemas (resources) to itself return *new* registry objects containing the added schemas. + +You could also confirm your schema 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/schema")) + +.. testoutput:: + + {'type': 'integer'} + Common Scenarios ---------------- @@ -244,7 +291,19 @@ Older versions of `jsonschema` used a different object -- `_RefResolver` -- for If you are not already constructing your own `_RefResolver`, this change should be transparent to you (or even recognizably improved, as the point of the migration was to improve the quality of the referencing implementation and enable some new functionality). -If you *were* configuring your own `_RefResolver`, here's how to migrate to the newer APIs: +.. table:: Rough equivalence between `_RefResolver` and `referencing.Registry` APIs + :widths: auto + + =========================================================== ===================================================================================================================== + Old API New API + =========================================================== ===================================================================================================================== + ``RefResolver.from_schema({"$id": "urn:example:foo", ...}`` ``Registry().with_resource(uri="urn:example:foo", resource=Resource.from_contents({"$id": "urn:example:foo", ...}))`` + Overriding ``RefResolver.resolve_from_url`` Passing a callable to `referencing.Registry`\ 's ``retrieve`` argument + ``DraftNValidator(..., resolver=_RefResolver(...))`` `` DraftNValidator(..., registry=Registry().with_resources(...))`` + =========================================================== ===================================================================================================================== + + +Here are some more specifics on how to migrate to the newer APIs: The ``store`` argument ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/requirements.txt b/docs/requirements.txt index 87e289489..25bf5b94a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -40,7 +40,7 @@ jinja2==3.1.2 # sphinx-autoapi file:.#egg=jsonschema # via -r docs/requirements.in -jsonschema-specifications==2023.3.1 +jsonschema-specifications==2023.3.2 # via jsonschema kiwisolver==1.4.4 # via matplotlib @@ -82,7 +82,7 @@ pytz==2022.7.1 # via babel pyyaml==6.0 # via sphinx-autoapi -referencing==0.21.0 +referencing==0.21.1 # via # jsonschema # jsonschema-specifications @@ -113,7 +113,7 @@ sphinx-basic-ng==1.0.0b1 # via furo sphinx-copybutton==0.5.1 # via -r docs/requirements.in -sphinx-json-schema-spec==2023.2.2 +sphinx-json-schema-spec==2023.2.4 # via -r docs/requirements.in sphinxcontrib-applehelp==1.0.4 # via sphinx @@ -135,5 +135,5 @@ unidecode==1.3.6 # via sphinx-autoapi urllib3==1.26.14 # via requests -wrapt==1.14.1 +wrapt==1.15.0 # via astroid diff --git a/docs/spelling-wordlist.txt b/docs/spelling-wordlist.txt index 38eb5371f..640d56f73 100644 --- a/docs/spelling-wordlist.txt +++ b/docs/spelling-wordlist.txt @@ -6,6 +6,7 @@ ValidationError # 0th, sigh... th +amongst callables # non-codeblocked cls from autoapi cls From eb004479645a4e1f0d842e4434b909f476569dcc Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 5 Mar 2023 08:26:30 +0200 Subject: [PATCH 28/33] Replace the other usages of pyrsistent with rpds. --- jsonschema/_types.py | 26 +++++++++++--------------- jsonschema/tests/test_cli.py | 6 ++---- jsonschema/validators.py | 4 ++-- pyproject.toml | 2 +- tox.ini | 1 - 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 5b543f71b..9f8dfa007 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -1,26 +1,22 @@ from __future__ import annotations +from typing import Any, Callable, Mapping import numbers -import typing -from pyrsistent import pmap -from pyrsistent.typing import PMap +from rpds import HashTrieMap import attr from jsonschema.exceptions import UndefinedTypeCheck -# unfortunately, the type of pmap is generic, and if used as the attr.ib +# unfortunately, the type of HashTrieMap is generic, and if used as the attr.ib # converter, the generic type is presented to mypy, which then fails to match # the concrete type of a type checker mapping # this "do nothing" wrapper presents the correct information to mypy -def _typed_pmap_converter( - init_val: typing.Mapping[ - str, - typing.Callable[["TypeChecker", typing.Any], bool], - ], -) -> PMap[str, typing.Callable[["TypeChecker", typing.Any], bool]]: - return pmap(init_val) +def _typed_map_converter( + init_val: Mapping[str, Callable[["TypeChecker", Any], bool]], +) -> HashTrieMap[str, Callable[["TypeChecker", Any], bool]]: + return HashTrieMap.convert(init_val) def is_array(checker, instance): @@ -82,11 +78,11 @@ class TypeChecker: The initial mapping of types to their checking functions. """ - _type_checkers: PMap[ - str, typing.Callable[["TypeChecker", typing.Any], bool], + _type_checkers: HashTrieMap[ + str, Callable[["TypeChecker", Any], bool], ] = attr.ib( - default=pmap(), - converter=_typed_pmap_converter, + default=HashTrieMap(), + converter=_typed_map_converter, ) def __repr__(self): diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py index 6d5873aae..c5b0b2ed2 100644 --- a/jsonschema/tests/test_cli.py +++ b/jsonschema/tests/test_cli.py @@ -16,8 +16,6 @@ except ImportError: # pragma: no cover import importlib_metadata as metadata # type: ignore -from pyrsistent import m - from jsonschema import Draft4Validator, Draft202012Validator from jsonschema.exceptions import ( SchemaError, @@ -70,13 +68,13 @@ def _message_for(non_json): class TestCLI(TestCase): def run_cli( - self, argv, files=m(), stdin=StringIO(), exit_code=0, **override, + self, argv, files=None, stdin=StringIO(), exit_code=0, **override, ): arguments = cli.parse_args(argv) arguments.update(override) self.assertFalse(hasattr(cli, "open")) - cli.open = fake_open(files) + cli.open = fake_open(files or {}) try: stdout, stderr = StringIO(), StringIO() actual_exit_code = cli.run( diff --git a/jsonschema/validators.py b/jsonschema/validators.py index bd135444a..3182d1385 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -16,8 +16,8 @@ import warnings from jsonschema_specifications import REGISTRY as SPECIFICATIONS -from pyrsistent import m from referencing import Specification +from rpds import HashTrieMap import attr import referencing.jsonschema @@ -802,7 +802,7 @@ def __init__( self, base_uri, referrer, - store=m(), + store=HashTrieMap(), cache_remote=True, handlers=(), urljoin_cache=None, diff --git a/pyproject.toml b/pyproject.toml index 19ba652a6..ad3152b97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,9 @@ dynamic = ["version", "readme"] dependencies = [ "attrs>=22.2.0", - "pyrsistent>=0.19.3", "jsonschema-specifications>=2023.03.1", "referencing>=0.21.0", + "rpds-py>=0.4.1", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", diff --git a/tox.ini b/tox.ini index b1464e5e6..4a2e871ab 100644 --- a/tox.ini +++ b/tox.ini @@ -92,7 +92,6 @@ deps = # FIXME: Why are we repeating dependencies here? attrs mypy - pyrsistent types-requests commands = {envpython} -m mypy --config {toxinidir}/pyproject.toml {posargs} {toxinidir}/jsonschema From 2b547c7085ed5171a9319837c275b87e9aa3bde8 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Mon, 6 Mar 2023 08:10:38 +0200 Subject: [PATCH 29/33] Avoid whatever nonsense pkg_resources error. Just use virtue in CI. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 4a2e871ab..83bf9c24b 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ commands = build: {envpython} -m build {toxinidir} --outdir {envtmpdir}/dist - tests,coverage,ghcoverage: {envpython} -Werror -m {env:MAYBE_COVERAGE:} twisted.trial {posargs:jsonschema} + tests,coverage,ghcoverage: {envpython} -Werror -m {env:MAYBE_COVERAGE:} virtue {posargs:jsonschema} tests: {envpython} -m doctest {toxinidir}/README.rst coverage: {envpython} -m coverage report --show-missing @@ -53,7 +53,7 @@ deps = perf: pyperf - tests,coverage,ghcoverage: twisted + tests,coverage,ghcoverage: virtue coverage,ghcoverage: coverage>=7.0.0b1 From 94c60e4170b68811aeac0bbc6bc9442d29f073eb Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Mon, 6 Mar 2023 13:18:27 +0200 Subject: [PATCH 30/33] Speed up Validator.evolve by pre-computing fields. We're not a general class, so we know what fields we need ahead of time. This seems to give ~15% speedup on Validator evolution, which happens often as part of walking up and down schemas. --- jsonschema/validators.py | 78 +++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 3182d1385..62e86dfb2 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -217,6 +217,23 @@ def __init_subclass__(cls): stacklevel=2, ) + def evolve(self, **changes): + cls = self.__class__ + schema = changes.setdefault("schema", self.schema) + NewValidator = validator_for(schema, default=cls) + + for field in attr.fields(cls): + if not field.init: + continue + attr_name = field.name + init_name = field.alias + if init_name not in changes: + changes[init_name] = getattr(self, attr_name) + + return NewValidator(**changes) + + cls.evolve = evolve + def __attrs_post_init__(self): if self._resolver is None: self._resolver = self._registry.resolver_with_root( @@ -257,18 +274,10 @@ def resolver(self): return self._ref_resolver def evolve(self, **changes): - # Essentially reproduces attr.evolve, but may involve instantiating - # a different class than this one. - cls = self.__class__ - schema = changes.setdefault("schema", self.schema) - NewValidator = validator_for(schema, default=cls) + NewValidator = validator_for(schema, default=self.__class__) - for field in attr.fields(cls): - if not field.init: - continue - attr_name = field.name # To deal with private attributes. - init_name = field.alias + for (attr_name, init_name) in evolve_fields: if init_name not in changes: changes[init_name] = getattr(self, attr_name) @@ -328,17 +337,46 @@ def descend( schema_path=None, resolver=None, ): + if schema is True: + return + elif schema is False: + yield exceptions.ValidationError( + f"False schema does not allow {instance!r}", + validator=None, + validator_value=None, + instance=instance, + schema=schema, + ) + return + if resolver is None: resolver = self._resolver.in_subresource( specification.create_resource(schema), ) - validator = self.evolve(schema=schema, _resolver=resolver) - for error in validator.iter_errors(instance): - if path is not None: - error.path.appendleft(path) - if schema_path is not None: - error.schema_path.appendleft(schema_path) - yield error + evolved = self.evolve(schema=schema, _resolver=resolver) + + for k, v in applicable_validators(schema): + validator = evolved.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(evolved, v, instance, schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=schema, + type_checker=evolved.TYPE_CHECKER, + ) + if k not in {"if", "$ref"}: + error.schema_path.appendleft(k) + if path is not None: + error.path.appendleft(path) + if schema_path is not None: + error.schema_path.appendleft(schema_path) + yield error def validate(self, *args, **kwargs): for error in self.iter_errors(*args, **kwargs): @@ -389,6 +427,12 @@ def is_valid(self, instance, _schema=None): error = next(self.iter_errors(instance), None) return error is None + evolve_fields = [ + (field.name, field.alias) + for field in attr.fields(Validator) + if field.init + ] + if version is not None: safe = version.title().replace(" ", "").replace("-", "") Validator.__name__ = Validator.__qualname__ = f"{safe}Validator" From 1854b26916d79845673211f731a7fe6dde932232 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 8 Mar 2023 15:31:48 -0500 Subject: [PATCH 31/33] Bump requirements. --- docs/requirements.txt | 30 +++++++++++++++--------------- pyproject.toml | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 25bf5b94a..3305990a5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,24 +1,24 @@ # -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: +# This file is autogenerated by pip-compile with python 3.10 +# To update, run: # # pip-compile --resolver=backtracking docs/requirements.in # alabaster==0.7.13 # via sphinx -astroid==2.14.2 +astroid==2.15.0 # via sphinx-autoapi attrs==22.2.0 # via # jsonschema # referencing -babel==2.11.0 +babel==2.12.1 # via sphinx beautifulsoup4==4.11.2 # via furo certifi==2022.12.7 # via requests -charset-normalizer==3.0.1 +charset-normalizer==3.1.0 # via requests contourpy==1.0.7 # via matplotlib @@ -26,7 +26,7 @@ cycler==0.11.0 # via matplotlib docutils==0.19 # via sphinx -fonttools==4.38.0 +fonttools==4.39.0 # via matplotlib furo==2022.12.7 # via -r docs/requirements.in @@ -40,7 +40,7 @@ jinja2==3.1.2 # sphinx-autoapi file:.#egg=jsonschema # via -r docs/requirements.in -jsonschema-specifications==2023.3.2 +jsonschema-specifications==2023.3.3 # via jsonschema kiwisolver==1.4.4 # via matplotlib @@ -52,7 +52,7 @@ lxml==4.9.2 # sphinx-json-schema-spec markupsafe==2.1.2 # via jinja2 -matplotlib==3.7.0 +matplotlib==3.7.1 # via sphinxext-opengraph numpy==1.24.2 # via @@ -72,22 +72,20 @@ pygments==2.14.0 # sphinx pyparsing==3.0.9 # via matplotlib -pyrsistent==0.19.3 - # via - # jsonschema - # referencing python-dateutil==2.8.2 # via matplotlib -pytz==2022.7.1 - # via babel pyyaml==6.0 # via sphinx-autoapi -referencing==0.21.1 +referencing==0.24.2 # via # jsonschema # jsonschema-specifications requests==2.28.2 # via sphinx +rpds-py==0.5.2 + # via + # jsonschema + # referencing six==1.16.0 # via python-dateutil snowballstemmer==2.2.0 @@ -131,6 +129,8 @@ sphinxcontrib-spelling==8.0.0 # via -r docs/requirements.in sphinxext-opengraph==0.8.1 # via -r docs/requirements.in +typing-extensions==4.5.0 + # via astroid unidecode==1.3.6 # via sphinx-autoapi urllib3==1.26.14 diff --git a/pyproject.toml b/pyproject.toml index ad3152b97..d821e34cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,8 @@ dynamic = ["version", "readme"] dependencies = [ "attrs>=22.2.0", "jsonschema-specifications>=2023.03.1", - "referencing>=0.21.0", - "rpds-py>=0.4.1", + "referencing>=0.24.2", + "rpds-py>=0.5.2", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From 4d6bff9498ecc9767ef0b8c3ba87a1157c6edd26 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 9 Mar 2023 15:29:51 -0500 Subject: [PATCH 32/33] Link to the new referencing doc page. --- docs/referencing.rst | 4 +++- docs/requirements.txt | 10 ++++------ pyproject.toml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/referencing.rst b/docs/referencing.rst index fc95e0dab..e3f0e7f45 100644 --- a/docs/referencing.rst +++ b/docs/referencing.rst @@ -5,7 +5,7 @@ JSON (Schema) Referencing The JSON Schema :kw:`$ref` and :kw:`$dynamicRef` keywords allow schema authors to combine multiple schemas (or subschemas) together for reuse or deduplication. The `referencing ` library was written in order to provide a simple, well-behaved and well-tested implementation of this kind of reference resolution [1]_. -It has its own documentation, but this page serves as a quick introduction which is tailored more specifically to JSON Schema, and even more specifically to how to configure `referencing ` for use with `Validator` objects in order to customize the behavior of :kw:`$ref` and friends in your schemas. +It has its `own documentation which is worth reviewing `, but this page serves as an introduction which is tailored specifically to JSON Schema, and even more specifically to how to configure `referencing ` for use with `Validator` objects in order to customize the behavior of the :kw:`$ref` keyword and friends in your schemas. Configuring `jsonschema` for custom referencing behavior is essentially a two step process: @@ -65,6 +65,8 @@ You could also confirm your schema is in the registry if you'd like, via `refere {'type': 'integer'} +For further details, see the `referencing documentation `. + Common Scenarios ---------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 3305990a5..7c64b9fc3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.10 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: # # pip-compile --resolver=backtracking docs/requirements.in # @@ -76,13 +76,13 @@ python-dateutil==2.8.2 # via matplotlib pyyaml==6.0 # via sphinx-autoapi -referencing==0.24.2 +referencing==0.24.3 # via # jsonschema # jsonschema-specifications requests==2.28.2 # via sphinx -rpds-py==0.5.2 +rpds-py==0.6.1 # via # jsonschema # referencing @@ -129,8 +129,6 @@ sphinxcontrib-spelling==8.0.0 # via -r docs/requirements.in sphinxext-opengraph==0.8.1 # via -r docs/requirements.in -typing-extensions==4.5.0 - # via astroid unidecode==1.3.6 # via sphinx-autoapi urllib3==1.26.14 diff --git a/pyproject.toml b/pyproject.toml index d821e34cd..579bc7a08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,9 @@ dynamic = ["version", "readme"] dependencies = [ "attrs>=22.2.0", - "jsonschema-specifications>=2023.03.1", - "referencing>=0.24.2", - "rpds-py>=0.5.2", + "jsonschema-specifications>=2023.03.3", + "referencing>=0.24.3", + "rpds-py>=0.6.1", "importlib_metadata;python_version<'3.8'", "typing_extensions;python_version<'3.8'", From fcbeced3a52ed12ad07c3670a84ba608d9c1339c Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Mon, 13 Mar 2023 15:48:50 -0400 Subject: [PATCH 33/33] Another version bump. --- docs/requirements.txt | 6 +++--- pyproject.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 7c64b9fc3..0db48a347 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -40,7 +40,7 @@ jinja2==3.1.2 # sphinx-autoapi file:.#egg=jsonschema # via -r docs/requirements.in -jsonschema-specifications==2023.3.3 +jsonschema-specifications==2023.3.4 # via jsonschema kiwisolver==1.4.4 # via matplotlib @@ -76,7 +76,7 @@ python-dateutil==2.8.2 # via matplotlib pyyaml==6.0 # via sphinx-autoapi -referencing==0.24.3 +referencing==0.24.4 # via # jsonschema # jsonschema-specifications @@ -131,7 +131,7 @@ sphinxext-opengraph==0.8.1 # via -r docs/requirements.in unidecode==1.3.6 # via sphinx-autoapi -urllib3==1.26.14 +urllib3==1.26.15 # via requests wrapt==1.15.0 # via astroid diff --git a/pyproject.toml b/pyproject.toml index 579bc7a08..f709c9f76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,8 +32,8 @@ dynamic = ["version", "readme"] dependencies = [ "attrs>=22.2.0", - "jsonschema-specifications>=2023.03.3", - "referencing>=0.24.3", + "jsonschema-specifications>=2023.03.4", + "referencing>=0.24.4", "rpds-py>=0.6.1", "importlib_metadata;python_version<'3.8'",