From e6e4f8f50ee789dbde309024bd987a403d791b59 Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Mon, 18 Sep 2023 14:18:25 -0400 Subject: [PATCH 1/8] dependencies: Resolve #218 by adopting the new resolver interface. + Deprecates "memory:" URIs in favor of the registry behavior. + Fixes a bunch of tests that referenced completely invalid specifications. --- python_jsonschema_objects/__init__.py | 120 +++++++++++++++---- python_jsonschema_objects/classbuilder.py | 21 ++-- python_jsonschema_objects/examples/README.md | 5 +- setup.py | 2 +- test/test_nondefault_resolver_validator.py | 62 +++++++--- test/test_pytest.py | 55 +++++---- test/test_regression_232.py | 16 --- test/thing-one.json | 2 +- test/thing-two.json | 2 +- 9 files changed, 195 insertions(+), 90 deletions(-) diff --git a/python_jsonschema_objects/__init__.py b/python_jsonschema_objects/__init__.py index 3290b33..3d6a481 100644 --- a/python_jsonschema_objects/__init__.py +++ b/python_jsonschema_objects/__init__.py @@ -10,25 +10,40 @@ import inflection import jsonschema import six -from jsonschema import Draft4Validator - -from python_jsonschema_objects import classbuilder, markdown_support, util +import python_jsonschema_objects.classbuilder as classbuilder +import python_jsonschema_objects.markdown_support +import python_jsonschema_objects.util from python_jsonschema_objects.validators import ValidationError +from typing import Optional + +from jsonschema_specifications import REGISTRY as SPECIFICATIONS +from referencing import Registry, Resource +import referencing.typing +import referencing.jsonschema +import referencing.retrieval + logger = logging.getLogger(__name__) +__all__ = ["ObjectBuilder", "markdown_support", "ValidationError"] + FILE = __file__ SUPPORTED_VERSIONS = ( - "http://json-schema.org/draft-03/schema#", - "http://json-schema.org/draft-04/schema#", + "http://json-schema.org/draft-03/schema", + "http://json-schema.org/draft-04/schema", ) class ObjectBuilder(object): - def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None): - self.mem_resolved = resolved - + def __init__( + self, + schema_uri, + resolved={}, + registry: Optional[referencing.Registry] = None, + resolver: Optional[referencing.typing.Retrieve] = None, + specification_uri: str = "http://json-schema.org/draft-04/schema", + ): if isinstance(schema_uri, six.string_types): uri = os.path.normpath(schema_uri) self.basedir = os.path.dirname(uri) @@ -41,7 +56,7 @@ def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None): if ( "$schema" in self.schema - and self.schema["$schema"] not in SUPPORTED_VERSIONS + and self.schema["$schema"].rstrip("#") not in SUPPORTED_VERSIONS ): warnings.warn( "Schema version {} not recognized. Some " @@ -50,15 +65,78 @@ def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None): ) ) - self.resolver = resolver or jsonschema.RefResolver.from_schema(self.schema) - self.resolver.handlers.update( - {"file": self.relative_file_resolver, "memory": self.memory_resolver} - ) + if registry is not None: + if not isinstance(registry, referencing.Registry): + raise TypeError("registry must be a Registry instance") + + if resolver is not None: + raise AttributeError( + "Cannot specify both registry and resolver. If you provide your own registry, pass the resolver directly to that" + ) + self.registry = registry + else: + if resolver is not None: + + def file_and_memory_handler(uri): + if uri.startswith("file:"): + return Resource.from_contents(self.relative_file_resolver(uri)) + return resolver(uri) + + self.registry = Registry(retrieve=file_and_memory_handler) + else: - validatorClass = validatorClass or Draft4Validator - meta_validator = validatorClass(validatorClass.META_SCHEMA) + def file_and_memory_handler(uri): + if uri.startswith("file:"): + return Resource.from_contents(self.relative_file_resolver(uri)) + raise RuntimeError( + "No remote resource resolver provided. Cannot resolve {}".format( + uri + ) + ) + + self.registry = Registry(retrieve=file_and_memory_handler) + + if len(resolved) > 0: + warnings.warn( + "Use of 'memory:' URIs is deprecated. Provide a registry with properly resolved references " + "if you want to resolve items externally.", + DeprecationWarning, + ) + for uri, contents in resolved.items(): + self.registry = self.registry.with_resource( + "memory:" + uri, + referencing.Resource.from_contents(contents, specification_uri), + ) + + if "$schema" not in self.schema: + warnings.warn("Schema version not specified. Defaulting to draft4") + updated = {"$schema": specification_uri} + updated.update(self.schema) + self.schema = updated + + schema = Resource.from_contents(self.schema) + if schema.id() is None: + warnings.warn("Schema id not specified. Defaulting to 'self'") + updated = {"$id": "self", "id": "self"} + updated.update(self.schema) + self.schema = updated + schema = Resource.from_contents(self.schema) + + self.registry = self.registry.with_resource("", schema) + self.resolver = self.registry.resolver() + + if specification_uri is not None: + validatorClass = jsonschema.validators.validator_for( + {"$schema": specification_uri} + ) + else: + validatorClass = jsonschema.validators.validator_for(self.schema) + + meta_validator = validatorClass( + validatorClass.META_SCHEMA, registry=self.registry + ) meta_validator.validate(self.schema) - self.validator = validatorClass(self.schema, resolver=self.resolver) + self.validator = validatorClass(self.schema, registry=self.registry) self._classes = None self._resolved = None @@ -85,9 +163,6 @@ def get_class(self, uri): self._classes = self.build_classes() return self._resolved.get(uri, None) - def memory_resolver(self, uri): - return self.mem_resolved[uri[7:]] - def relative_file_resolver(self, uri): path = os.path.join(self.basedir, uri[8:]) with codecs.open(path, "r", "utf-8") as fin: @@ -126,10 +201,11 @@ def build_classes(self, strict=False, named_only=False, standardize_names=True): kw = {"strict": strict} builder = classbuilder.ClassBuilder(self.resolver) for nm, defn in six.iteritems(self.schema.get("definitions", {})): - uri = util.resolve_ref_uri( - self.resolver.resolution_scope, "#/definitions/" + nm + resolved = self.resolver.lookup("#/definitions/" + nm) + uri = python_jsonschema_objects.util.resolve_ref_uri( + self.resolver._base_uri, "#/definitions/" + nm ) - builder.construct(uri, defn, **kw) + builder.construct(uri, resolved.contents, **kw) if standardize_names: name_transform = lambda t: inflection.camelize( diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py index 9ae08c3..103ee53 100644 --- a/python_jsonschema_objects/classbuilder.py +++ b/python_jsonschema_objects/classbuilder.py @@ -1,3 +1,10 @@ +import referencing._core + +import python_jsonschema_objects.util as util +import python_jsonschema_objects.validators as validators +import python_jsonschema_objects.pattern_properties as pattern_properties +from python_jsonschema_objects.literals import LiteralValue + import collections import copy import itertools @@ -443,7 +450,7 @@ def __call__(self, *a, **kw): class ClassBuilder(object): - def __init__(self, resolver): + def __init__(self, resolver: referencing._core.Resolver): self.resolver = resolver self.resolved = {} self.under_construction = set() @@ -462,10 +469,8 @@ def expand_references(self, source_uri, iterable): return pp def resolve_type(self, ref, source): - """Return a resolved type for a URI, potentially constructing one if - necessary. - """ - uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref) + """Return a resolved type for a URI, potentially constructing one if necessary""" + uri = util.resolve_ref_uri(self.resolver._base_uri, ref) if uri in self.resolved: return self.resolved[uri] @@ -484,9 +489,9 @@ def resolve_type(self, ref, source): "Resolving direct reference object {0} -> {1}", source, uri ) ) - with self.resolver.resolving(ref) as resolved: - self.resolved[uri] = self.construct(uri, resolved, (ProtocolBase,)) - return self.resolved[uri] + resolved = self.resolver.lookup(ref) + self.resolved[uri] = self.construct(uri, resolved.contents, (ProtocolBase,)) + return self.resolved[uri] def construct(self, uri, *args, **kw): """Wrapper to debug things""" diff --git a/python_jsonschema_objects/examples/README.md b/python_jsonschema_objects/examples/README.md index 254582d..aca8dae 100644 --- a/python_jsonschema_objects/examples/README.md +++ b/python_jsonschema_objects/examples/README.md @@ -219,7 +219,7 @@ The schema and code example below show how this works. The `$ref` operator is supported in nearly all locations, and dispatches the actual reference resolution to the -`jsonschema.RefResolver`. +`referencing.Registry` resolver. This example shows using the memory URI (described in more detail below) to create a wrapper object that is just a string literal. @@ -298,6 +298,9 @@ ValidationError: '[u'author']' are required attributes for B #### The "memory:" URI +**"memory:" URIs are deprecated (although they still work). Load resources into a +`referencing.Registry` instead and pass those in** + The ObjectBuilder can be passed a dictionary specifying 'memory' schemas when instantiated. This will allow it to resolve references where the referenced schemas are retrieved diff --git a/setup.py b/setup.py index 7a66678..d892325 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ install_requires=[ "inflection>=0.2", "Markdown>=2.4", - "jsonschema>=2.3,<4.18", + "jsonschema>=2.3", "six>=1.5.2", ], cmdclass=versioneer.get_cmdclass(), diff --git a/test/test_nondefault_resolver_validator.py b/test/test_nondefault_resolver_validator.py index ef18bdf..18a7fd3 100644 --- a/test/test_nondefault_resolver_validator.py +++ b/test/test_nondefault_resolver_validator.py @@ -1,24 +1,52 @@ -from jsonschema import Draft3Validator, RefResolver -from jsonschema._utils import URIDict, load_schema - +import pytest # noqa +import referencing +import json +import referencing.jsonschema +import referencing.exceptions import python_jsonschema_objects as pjo -def test_non_default_resolver_validator(markdown_examples): - ms = URIDict() - draft3 = load_schema("draft3") - draft4 = load_schema("draft4") - ms[draft3["id"]] = draft3 - ms[draft4["id"]] = draft4 - resolver_with_store = RefResolver(draft3["id"], draft3, ms) - - # 'Other' schema should be valid with draft3 +def test_custom_spec_validator(markdown_examples): + # This schema shouldn't be valid under DRAFT-03 + schema = { + "$schema": "http://json-schema.org/draft-04/schema", + "title": "other", + "oneOf": [{"type": "string"}, {"type": "number"}], + } builder = pjo.ObjectBuilder( - markdown_examples["Other"], - resolver=resolver_with_store, - validatorClass=Draft3Validator, + schema, + specification_uri="http://json-schema.org/draft-03/schema", resolved=markdown_examples, ) klasses = builder.build_classes() - a = klasses.Other(MyAddress="where I live") - assert a.MyAddress == "where I live" + a = klasses.Other("foo") + assert a == "foo" + + +def test_non_default_resolver_finds_refs(): + registry = referencing.Registry() + + remote_schema = { + "$schema": "http://json-schema.org/draft-04/schema", + "type": "number", + } + registry = registry.with_resource( + "https://example.org/schema/example", + referencing.Resource.from_contents(remote_schema), + ) + + schema = { + "$schema": "http://json-schema.org/draft-04/schema", + "title": "other", + "type": "object", + "properties": { + "local": {"type": "string"}, + "remote": {"$ref": "https://example.org/schema/example"}, + }, + } + + builder = pjo.ObjectBuilder( + schema, + registry=registry, + ) + builder.build_classes() diff --git a/test/test_pytest.py b/test/test_pytest.py index 0e315d8..a7ec6af 100644 --- a/test/test_pytest.py +++ b/test/test_pytest.py @@ -1,4 +1,7 @@ import json + +import referencing.jsonschema +import six import logging import warnings @@ -12,32 +15,38 @@ @pytest.mark.parametrize( - "version, warn", + "version, warn, error", [ - ("http://json-schema.org/schema#", True), - ("http://json-schema.org/draft-03/schema#", False), - ("http://json-schema.org/draft-04/schema#", False), - ("http://json-schema.org/draft-06/schema#", True), - ("http://json-schema.org/draft-07/schema#", True), + ("http://json-schema.org/schema#", True, True), + ("http://json-schema.org/draft-03/schema#", False, False), + ("http://json-schema.org/draft-04/schema#", False, False), + ("http://json-schema.org/draft-06/schema#", True, False), + ("http://json-schema.org/draft-07/schema#", True, False), ], ) -def test_warnings_on_schema_version(version, warn): +def test_warnings_on_schema_version(version, warn, error): schema = {"$schema": version, "$id": "test", "type": "object", "properties": {}} with warnings.catch_warnings(record=True) as w: - pjs.ObjectBuilder(schema) - - if warn: - assert len(w) == 1 - assert "Schema version %s not recognized" % version in str(w[-1].message) + try: + pjs.ObjectBuilder(schema) + except Exception as e: + assert error == True else: - assert len(w) == 0, w[-1].message + warn_msgs = [str(m.message) for m in w] + present = [ + "Schema version %s not recognized" % version in msg for msg in warn_msgs + ] + if warn: + assert any(present) + else: + assert not any(present) def test_schema_validation(): """Test that the ObjectBuilder validates the schema itself.""" schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "$id": "test", "type": "object", "properties": { @@ -52,7 +61,7 @@ def test_schema_validation(): def test_regression_9(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "$id": "test", "type": "object", "properties": { @@ -67,7 +76,7 @@ def test_regression_9(): def test_build_classes_is_idempotent(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "title": "test", "type": "object", "properties": { @@ -87,7 +96,7 @@ def test_build_classes_is_idempotent(): def test_underscore_properties(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "title": "AggregateQuery", "type": "object", "properties": {"group": {"type": "object", "properties": {}}}, @@ -108,7 +117,7 @@ def test_underscore_properties(): def test_array_regressions(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "$id": "test", "type": "object", "properties": { @@ -140,7 +149,7 @@ def test_array_regressions(): def test_arrays_can_have_reffed_items_of_mixed_type(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "$id": "test", "type": "object", "properties": { @@ -472,7 +481,7 @@ def test_dictionary_transformation(Person, pdict): def test_strict_mode(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": {"firstName": {"type": "string"}, "lastName": {"type": "string"}}, "$id": "test", @@ -492,7 +501,7 @@ def test_strict_mode(): def test_boolean_in_child_object(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "$id": "test", "type": "object", "properties": {"data": {"type": "object", "additionalProperties": True}}, @@ -514,7 +523,7 @@ def test_boolean_in_child_object(): def test_default_values(default): default = json.loads(default) schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "$id": "test", "type": "object", "properties": {"sample": default}, @@ -537,7 +546,7 @@ def test_justareference_example(markdown_examples): def test_number_multiple_of_validation(): schema = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "$id": "test", "type": "object", "title": "Base", diff --git a/test/test_regression_232.py b/test/test_regression_232.py index ef05635..61d2325 100644 --- a/test/test_regression_232.py +++ b/test/test_regression_232.py @@ -71,15 +71,9 @@ def test_nested_oneof_with_different_types(schema_json): builder = pjo.ObjectBuilder(schema_json) ns = builder.build_classes() - resolver = jsonschema.RefResolver.from_schema(schema_json) - main_obj = schema_json["definitions"]["MainObject"] - test1 = {"location": 12345} test2 = {"location": {"type": "Location"}} test3 = {"location": "unique:12"} - jsonschema.validate(test1, main_obj, resolver=resolver) - jsonschema.validate(test2, main_obj, resolver=resolver) - jsonschema.validate(test3, main_obj, resolver=resolver) obj1 = ns.MainObject(**test1) obj2 = ns.MainObject(**test2) @@ -94,13 +88,8 @@ def test_nested_oneof_with_different_types_by_reference(schema_json): builder = pjo.ObjectBuilder(schema_json) ns = builder.build_classes() - resolver = jsonschema.RefResolver.from_schema(schema_json) - ref_obj = schema_json["definitions"]["RefObject"] - test1 = {"location": 12345} test2 = {"location": {"type": "Location"}} - jsonschema.validate(test1, ref_obj, resolver=resolver) - jsonschema.validate(test2, ref_obj, resolver=resolver) obj1 = ns.RefObject(**test1) obj2 = ns.RefObject(**test2) @@ -113,15 +102,10 @@ def test_nested_oneof_with_different_types_in_additional_properties(schema_json) builder = pjo.ObjectBuilder(schema_json) ns = builder.build_classes() - resolver = jsonschema.RefResolver.from_schema(schema_json) - map_obj = schema_json["definitions"]["MapObject"] - x_prop_name = "location-id" test1 = {x_prop_name: 12345} test2 = {x_prop_name: {"type": "Location"}} - jsonschema.validate(test1, map_obj, resolver=resolver) - jsonschema.validate(test2, map_obj, resolver=resolver) obj1 = ns.MapObject(**test1) obj2 = ns.MapObject(**test2) diff --git a/test/thing-one.json b/test/thing-one.json index e335564..ae53726 100644 --- a/test/thing-one.json +++ b/test/thing-one.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "id": "thing_one", "title": "thing_one", "description": "The first thing.", diff --git a/test/thing-two.json b/test/thing-two.json index 58a9420..d473490 100644 --- a/test/thing-two.json +++ b/test/thing-two.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "id": "thing_two", "title": "thing_two", "description": "The second thing.", From 8c738cef1707fcc450b70615f3a3987db4a1f7ea Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Mon, 18 Sep 2023 14:22:11 -0400 Subject: [PATCH 2/8] deps: Drop support and tests for jsonschema below 4.18 --- setup.py | 2 +- tox.ini | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index d892325..ac86ce9 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ install_requires=[ "inflection>=0.2", "Markdown>=2.4", - "jsonschema>=2.3", + "jsonschema>=4.18", "six>=1.5.2", ], cmdclass=versioneer.get_cmdclass(), diff --git a/tox.ini b/tox.ini index f8bb557..09b4b64 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{37,38,39,310,311,312}-jsonschema{23,24,25,26,30,40}-markdown{2,3} +envlist = py{37,38,39,310,311,312}-jsonschema{40}-markdown{2,3} skip_missing_interpreters = true [gh-actions] @@ -19,11 +19,6 @@ deps = coverage pytest pytest-mock - jsonschema23: jsonschema~=2.3.0 - jsonschema24: jsonschema~=2.4.0 - jsonschema25: jsonschema~=2.5.0 - jsonschema26: jsonschema~=2.6.0 - jsonschema30: jsonschema~=3.0.0 - jsonschema40: jsonschema~=4.0 + jsonschema40: jsonschema>=4.18 markdown2: Markdown~=2.4 markdown3: Markdown~=3.0 From b1e753aca8a4a9b3dde6605059abe249cfe242a7 Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Mon, 18 Sep 2023 20:35:50 -0400 Subject: [PATCH 3/8] lint: Fix flake8 errors --- python_jsonschema_objects/__init__.py | 19 +++++++++---------- python_jsonschema_objects/classbuilder.py | 10 ++-------- test/test_nondefault_resolver_validator.py | 4 ++-- test/test_pytest.py | 7 ++----- test/test_regression_232.py | 1 - 5 files changed, 15 insertions(+), 26 deletions(-) diff --git a/python_jsonschema_objects/__init__.py b/python_jsonschema_objects/__init__.py index 3d6a481..861fd5f 100644 --- a/python_jsonschema_objects/__init__.py +++ b/python_jsonschema_objects/__init__.py @@ -6,22 +6,20 @@ import logging import os.path import warnings +from typing import Optional import inflection import jsonschema +import referencing.jsonschema +import referencing.retrieval +import referencing.typing import six +from referencing import Registry, Resource + import python_jsonschema_objects.classbuilder as classbuilder import python_jsonschema_objects.markdown_support import python_jsonschema_objects.util from python_jsonschema_objects.validators import ValidationError -from typing import Optional - -from jsonschema_specifications import REGISTRY as SPECIFICATIONS -from referencing import Registry, Resource -import referencing.typing -import referencing.jsonschema -import referencing.retrieval - logger = logging.getLogger(__name__) @@ -71,7 +69,8 @@ def __init__( if resolver is not None: raise AttributeError( - "Cannot specify both registry and resolver. If you provide your own registry, pass the resolver directly to that" + "Cannot specify both registry and resolver. If you provide your own registry, pass the resolver " + "directly to that" ) self.registry = registry else: @@ -228,7 +227,7 @@ def build_classes(self, strict=False, named_only=False, standardize_names=True): elif not named_only: classes[name_transform(uri.split("/")[-1])] = klass - return util.Namespace.from_mapping(classes) + return python_jsonschema_objects.util.Namespace.from_mapping(classes) if __name__ == "__main__": diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py index 103ee53..b099f2c 100644 --- a/python_jsonschema_objects/classbuilder.py +++ b/python_jsonschema_objects/classbuilder.py @@ -1,16 +1,10 @@ -import referencing._core - -import python_jsonschema_objects.util as util -import python_jsonschema_objects.validators as validators -import python_jsonschema_objects.pattern_properties as pattern_properties -from python_jsonschema_objects.literals import LiteralValue - -import collections +import collections.abc import copy import itertools import logging import sys +import referencing._core import six from python_jsonschema_objects import ( diff --git a/test/test_nondefault_resolver_validator.py b/test/test_nondefault_resolver_validator.py index 18a7fd3..312c1f2 100644 --- a/test/test_nondefault_resolver_validator.py +++ b/test/test_nondefault_resolver_validator.py @@ -1,8 +1,8 @@ import pytest # noqa import referencing -import json -import referencing.jsonschema import referencing.exceptions +import referencing.jsonschema + import python_jsonschema_objects as pjo diff --git a/test/test_pytest.py b/test/test_pytest.py index a7ec6af..080fd26 100644 --- a/test/test_pytest.py +++ b/test/test_pytest.py @@ -1,7 +1,4 @@ import json - -import referencing.jsonschema -import six import logging import warnings @@ -30,8 +27,8 @@ def test_warnings_on_schema_version(version, warn, error): with warnings.catch_warnings(record=True) as w: try: pjs.ObjectBuilder(schema) - except Exception as e: - assert error == True + except Exception: + assert error == True # noqa else: warn_msgs = [str(m.message) for m in w] present = [ diff --git a/test/test_regression_232.py b/test/test_regression_232.py index 61d2325..7ff9e7a 100644 --- a/test/test_regression_232.py +++ b/test/test_regression_232.py @@ -1,4 +1,3 @@ -import jsonschema import pytest import python_jsonschema_objects as pjo From 601f7a76275f59dc53c630c178ab9d4cbe48da51 Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Mon, 18 Sep 2023 20:40:23 -0400 Subject: [PATCH 4/8] deps: Remove support for Python 3.7 + jsonschema 4.18 doesn't support Python below 3.8, so we can't either --- setup.py | 1 - tox.ini | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ac86ce9..781cb84 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,6 @@ cmdclass=versioneer.get_cmdclass(), classifiers=[ "Programming Language :: Python :: 3", - "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 09b4b64..9218c20 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,9 @@ [tox] -envlist = py{37,38,39,310,311,312}-jsonschema{40}-markdown{2,3} +envlist = py{38,39,310,311,312}-jsonschema{40}-markdown{2,3} skip_missing_interpreters = true [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 From 819f61605401d818058bdce0d925f8a49f280b18 Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Mon, 18 Sep 2023 20:43:30 -0400 Subject: [PATCH 5/8] deps: Remove Python 3.7 from actions matrix --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index b4d4943..196f548 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -38,7 +38,7 @@ jobs: fail-fast: true matrix: os: [ubuntu, macos, windows] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] include: - experimental: false - python-version: "3.12" From 09e68d74ae93d7ee1850015de8fb6af6d5acbbb2 Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Mon, 18 Sep 2023 21:24:01 -0400 Subject: [PATCH 6/8] bugfix: Fix handling of alternate specifications. + Also fix the tests so that they actually test using a different validator (using the `any` keyword which was removed in DRAFT-04). Signed-off-by: Chris Wacek --- python_jsonschema_objects/__init__.py | 43 ++++++++++++---------- test/test_nondefault_resolver_validator.py | 25 +++++++++---- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/python_jsonschema_objects/__init__.py b/python_jsonschema_objects/__init__.py index 861fd5f..d14ed2a 100644 --- a/python_jsonschema_objects/__init__.py +++ b/python_jsonschema_objects/__init__.py @@ -40,7 +40,7 @@ def __init__( resolved={}, registry: Optional[referencing.Registry] = None, resolver: Optional[referencing.typing.Retrieve] = None, - specification_uri: str = "http://json-schema.org/draft-04/schema", + specification_uri: Optional[str] = None, ): if isinstance(schema_uri, six.string_types): uri = os.path.normpath(schema_uri) @@ -95,21 +95,15 @@ def file_and_memory_handler(uri): self.registry = Registry(retrieve=file_and_memory_handler) - if len(resolved) > 0: + if "$schema" not in self.schema: warnings.warn( - "Use of 'memory:' URIs is deprecated. Provide a registry with properly resolved references " - "if you want to resolve items externally.", - DeprecationWarning, - ) - for uri, contents in resolved.items(): - self.registry = self.registry.with_resource( - "memory:" + uri, - referencing.Resource.from_contents(contents, specification_uri), + "Schema version not specified. Defaulting to {}".format( + specification_uri or "http://json-schema.org/draft-04/schema" + ) ) - - if "$schema" not in self.schema: - warnings.warn("Schema version not specified. Defaulting to draft4") - updated = {"$schema": specification_uri} + updated = { + "$schema": specification_uri or "http://json-schema.org/draft-04/schema" + } updated.update(self.schema) self.schema = updated @@ -124,12 +118,23 @@ def file_and_memory_handler(uri): self.registry = self.registry.with_resource("", schema) self.resolver = self.registry.resolver() - if specification_uri is not None: - validatorClass = jsonschema.validators.validator_for( - {"$schema": specification_uri} + if len(resolved) > 0: + warnings.warn( + "Use of 'memory:' URIs is deprecated. Provide a registry with properly resolved references " + "if you want to resolve items externally.", + DeprecationWarning, ) - else: - validatorClass = jsonschema.validators.validator_for(self.schema) + for uri, contents in resolved.items(): + self.registry = self.registry.with_resource( + "memory:" + uri, + referencing.Resource.from_contents( + contents, specification_uri or self.schema["$schema"] + ), + ) + + validatorClass = jsonschema.validators.validator_for( + {"$schema": specification_uri or self.schema["$schema"]} + ) meta_validator = validatorClass( validatorClass.META_SCHEMA, registry=self.registry diff --git a/test/test_nondefault_resolver_validator.py b/test/test_nondefault_resolver_validator.py index 312c1f2..afe4215 100644 --- a/test/test_nondefault_resolver_validator.py +++ b/test/test_nondefault_resolver_validator.py @@ -1,26 +1,31 @@ +import jsonschema.exceptions import pytest # noqa import referencing import referencing.exceptions import referencing.jsonschema +import python_jsonschema_objects import python_jsonschema_objects as pjo def test_custom_spec_validator(markdown_examples): # This schema shouldn't be valid under DRAFT-03 schema = { - "$schema": "http://json-schema.org/draft-04/schema", + "$schema": "http://json-schema.org/draft-03/schema", "title": "other", - "oneOf": [{"type": "string"}, {"type": "number"}], + "type": "any", # this wasn't valid starting in 04 } - builder = pjo.ObjectBuilder( + pjo.ObjectBuilder( schema, - specification_uri="http://json-schema.org/draft-03/schema", resolved=markdown_examples, ) - klasses = builder.build_classes() - a = klasses.Other("foo") - assert a == "foo" + + with pytest.raises(jsonschema.exceptions.ValidationError): + pjo.ObjectBuilder( + schema, + specification_uri="http://json-schema.org/draft-04/schema", + resolved=markdown_examples, + ) def test_non_default_resolver_finds_refs(): @@ -49,4 +54,8 @@ def test_non_default_resolver_finds_refs(): schema, registry=registry, ) - builder.build_classes() + ns = builder.build_classes() + + thing = ns.Other(local="foo", remote=1) + with pytest.raises(python_jsonschema_objects.ValidationError): + thing = ns.Other(local="foo", remote="NaN") From 2f65818c7c2337b7cf79def3a8f35bb55eb5d3c1 Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Mon, 18 Sep 2023 21:41:25 -0400 Subject: [PATCH 7/8] bugfix: Fix handling of alternate specifications. + Also fix the tests so that they actually test using a different validator (using the `any` keyword which was removed in DRAFT-04). Signed-off-by: Chris Wacek --- python_jsonschema_objects/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/python_jsonschema_objects/__init__.py b/python_jsonschema_objects/__init__.py index d14ed2a..a463ca2 100644 --- a/python_jsonschema_objects/__init__.py +++ b/python_jsonschema_objects/__init__.py @@ -7,12 +7,13 @@ import os.path import warnings from typing import Optional +import typing import inflection import jsonschema import referencing.jsonschema import referencing.retrieval -import referencing.typing +import referencing._core import six from referencing import Registry, Resource @@ -36,8 +37,8 @@ class ObjectBuilder(object): def __init__( self, - schema_uri, - resolved={}, + schema_uri: typing.Union[typing.AnyStr, typing.Mapping], + resolved: typing.Dict[typing.AnyStr, typing.Mapping] = {}, registry: Optional[referencing.Registry] = None, resolver: Optional[referencing.typing.Retrieve] = None, specification_uri: Optional[str] = None, @@ -116,7 +117,6 @@ def file_and_memory_handler(uri): schema = Resource.from_contents(self.schema) self.registry = self.registry.with_resource("", schema) - self.resolver = self.registry.resolver() if len(resolved) > 0: warnings.warn( @@ -145,6 +145,10 @@ def file_and_memory_handler(uri): self._classes = None self._resolved = None + @property + def resolver(self) -> referencing._core.Resolver: + return self.registry.resolver() + @property def schema(self): try: From 26da6d36e8473ce6a93fca3602d08202f8448431 Mon Sep 17 00:00:00 2001 From: Chris Wacek Date: Tue, 19 Sep 2023 22:22:39 -0400 Subject: [PATCH 8/8] cleanup: Add python_requires specifier as well --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 781cb84..35e2313 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ "jsonschema>=4.18", "six>=1.5.2", ], + python_requires=">=3.8", cmdclass=versioneer.get_cmdclass(), classifiers=[ "Programming Language :: Python :: 3",