Skip to content

Commit

Permalink
Merge pull request #264 from cwacek/fix/218
Browse files Browse the repository at this point in the history
Migrate to jsonschema 4.18
  • Loading branch information
cwacek authored Sep 20, 2023
2 parents 82b1fb4 + 26da6d3 commit d652f4f
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 101 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
126 changes: 105 additions & 21 deletions python_jsonschema_objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,43 @@
import logging
import os.path
import warnings
from typing import Optional
import typing

import inflection
import jsonschema
import referencing.jsonschema
import referencing.retrieval
import referencing._core
import six
from jsonschema import Draft4Validator
from referencing import Registry, Resource

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

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: 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,
):
if isinstance(schema_uri, six.string_types):
uri = os.path.normpath(schema_uri)
self.basedir = os.path.dirname(uri)
Expand All @@ -41,7 +55,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 "
Expand All @@ -50,19 +64,91 @@ 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:

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 "$schema" not in self.schema:
warnings.warn(
"Schema version not specified. Defaulting to {}".format(
specification_uri or "http://json-schema.org/draft-04/schema"
)
)
updated = {
"$schema": specification_uri or "http://json-schema.org/draft-04/schema"
}
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)

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 or self.schema["$schema"]
),
)

validatorClass = jsonschema.validators.validator_for(
{"$schema": specification_uri or self.schema["$schema"]}
)

validatorClass = validatorClass or Draft4Validator
meta_validator = validatorClass(validatorClass.META_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

@property
def resolver(self) -> referencing._core.Resolver:
return self.registry.resolver()

@property
def schema(self):
try:
Expand All @@ -85,9 +171,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:
Expand Down Expand Up @@ -126,10 +209,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(
Expand All @@ -152,7 +236,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__":
Expand Down
17 changes: 8 additions & 9 deletions python_jsonschema_objects/classbuilder.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import collections
import collections.abc
import copy
import itertools
import logging
import sys

import referencing._core
import six

from python_jsonschema_objects import (
Expand Down Expand Up @@ -443,7 +444,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()
Expand All @@ -462,10 +463,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]

Expand All @@ -484,9 +483,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"""
Expand Down
5 changes: 4 additions & 1 deletion python_jsonschema_objects/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@
install_requires=[
"inflection>=0.2",
"Markdown>=2.4",
"jsonschema>=2.3,<4.18",
"jsonschema>=4.18",
"six>=1.5.2",
],
python_requires=">=3.8",
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",
Expand Down
71 changes: 54 additions & 17 deletions test/test_nondefault_resolver_validator.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,61 @@
from jsonschema import Draft3Validator, RefResolver
from jsonschema._utils import URIDict, load_schema
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_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)
def test_custom_spec_validator(markdown_examples):
# This schema shouldn't be valid under DRAFT-03
schema = {
"$schema": "http://json-schema.org/draft-03/schema",
"title": "other",
"type": "any", # this wasn't valid starting in 04
}
pjo.ObjectBuilder(
schema,
resolved=markdown_examples,
)

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():
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"},
},
}

# 'Other' schema should be valid with draft3
builder = pjo.ObjectBuilder(
markdown_examples["Other"],
resolver=resolver_with_store,
validatorClass=Draft3Validator,
resolved=markdown_examples,
schema,
registry=registry,
)
klasses = builder.build_classes()
a = klasses.Other(MyAddress="where I live")
assert a.MyAddress == "where I live"
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")
Loading

0 comments on commit d652f4f

Please sign in to comment.