Skip to content

Commit d652f4f

Browse files
authored
Merge pull request #264 from cwacek/fix/218
Migrate to jsonschema 4.18
2 parents 82b1fb4 + 26da6d3 commit d652f4f

File tree

11 files changed

+207
-101
lines changed

11 files changed

+207
-101
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
fail-fast: true
3939
matrix:
4040
os: [ubuntu, macos, windows]
41-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
41+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
4242
include:
4343
- experimental: false
4444
- python-version: "3.12"

python_jsonschema_objects/__init__.py

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,43 @@
66
import logging
77
import os.path
88
import warnings
9+
from typing import Optional
10+
import typing
911

1012
import inflection
1113
import jsonschema
14+
import referencing.jsonschema
15+
import referencing.retrieval
16+
import referencing._core
1217
import six
13-
from jsonschema import Draft4Validator
18+
from referencing import Registry, Resource
1419

15-
from python_jsonschema_objects import classbuilder, markdown_support, util
20+
import python_jsonschema_objects.classbuilder as classbuilder
21+
import python_jsonschema_objects.markdown_support
22+
import python_jsonschema_objects.util
1623
from python_jsonschema_objects.validators import ValidationError
1724

1825
logger = logging.getLogger(__name__)
1926

27+
__all__ = ["ObjectBuilder", "markdown_support", "ValidationError"]
28+
2029
FILE = __file__
2130

2231
SUPPORTED_VERSIONS = (
23-
"http://json-schema.org/draft-03/schema#",
24-
"http://json-schema.org/draft-04/schema#",
32+
"http://json-schema.org/draft-03/schema",
33+
"http://json-schema.org/draft-04/schema",
2534
)
2635

2736

2837
class ObjectBuilder(object):
29-
def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None):
30-
self.mem_resolved = resolved
31-
38+
def __init__(
39+
self,
40+
schema_uri: typing.Union[typing.AnyStr, typing.Mapping],
41+
resolved: typing.Dict[typing.AnyStr, typing.Mapping] = {},
42+
registry: Optional[referencing.Registry] = None,
43+
resolver: Optional[referencing.typing.Retrieve] = None,
44+
specification_uri: Optional[str] = None,
45+
):
3246
if isinstance(schema_uri, six.string_types):
3347
uri = os.path.normpath(schema_uri)
3448
self.basedir = os.path.dirname(uri)
@@ -41,7 +55,7 @@ def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None):
4155

4256
if (
4357
"$schema" in self.schema
44-
and self.schema["$schema"] not in SUPPORTED_VERSIONS
58+
and self.schema["$schema"].rstrip("#") not in SUPPORTED_VERSIONS
4559
):
4660
warnings.warn(
4761
"Schema version {} not recognized. Some "
@@ -50,19 +64,91 @@ def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None):
5064
)
5165
)
5266

53-
self.resolver = resolver or jsonschema.RefResolver.from_schema(self.schema)
54-
self.resolver.handlers.update(
55-
{"file": self.relative_file_resolver, "memory": self.memory_resolver}
67+
if registry is not None:
68+
if not isinstance(registry, referencing.Registry):
69+
raise TypeError("registry must be a Registry instance")
70+
71+
if resolver is not None:
72+
raise AttributeError(
73+
"Cannot specify both registry and resolver. If you provide your own registry, pass the resolver "
74+
"directly to that"
75+
)
76+
self.registry = registry
77+
else:
78+
if resolver is not None:
79+
80+
def file_and_memory_handler(uri):
81+
if uri.startswith("file:"):
82+
return Resource.from_contents(self.relative_file_resolver(uri))
83+
return resolver(uri)
84+
85+
self.registry = Registry(retrieve=file_and_memory_handler)
86+
else:
87+
88+
def file_and_memory_handler(uri):
89+
if uri.startswith("file:"):
90+
return Resource.from_contents(self.relative_file_resolver(uri))
91+
raise RuntimeError(
92+
"No remote resource resolver provided. Cannot resolve {}".format(
93+
uri
94+
)
95+
)
96+
97+
self.registry = Registry(retrieve=file_and_memory_handler)
98+
99+
if "$schema" not in self.schema:
100+
warnings.warn(
101+
"Schema version not specified. Defaulting to {}".format(
102+
specification_uri or "http://json-schema.org/draft-04/schema"
103+
)
104+
)
105+
updated = {
106+
"$schema": specification_uri or "http://json-schema.org/draft-04/schema"
107+
}
108+
updated.update(self.schema)
109+
self.schema = updated
110+
111+
schema = Resource.from_contents(self.schema)
112+
if schema.id() is None:
113+
warnings.warn("Schema id not specified. Defaulting to 'self'")
114+
updated = {"$id": "self", "id": "self"}
115+
updated.update(self.schema)
116+
self.schema = updated
117+
schema = Resource.from_contents(self.schema)
118+
119+
self.registry = self.registry.with_resource("", schema)
120+
121+
if len(resolved) > 0:
122+
warnings.warn(
123+
"Use of 'memory:' URIs is deprecated. Provide a registry with properly resolved references "
124+
"if you want to resolve items externally.",
125+
DeprecationWarning,
126+
)
127+
for uri, contents in resolved.items():
128+
self.registry = self.registry.with_resource(
129+
"memory:" + uri,
130+
referencing.Resource.from_contents(
131+
contents, specification_uri or self.schema["$schema"]
132+
),
133+
)
134+
135+
validatorClass = jsonschema.validators.validator_for(
136+
{"$schema": specification_uri or self.schema["$schema"]}
56137
)
57138

58-
validatorClass = validatorClass or Draft4Validator
59-
meta_validator = validatorClass(validatorClass.META_SCHEMA)
139+
meta_validator = validatorClass(
140+
validatorClass.META_SCHEMA, registry=self.registry
141+
)
60142
meta_validator.validate(self.schema)
61-
self.validator = validatorClass(self.schema, resolver=self.resolver)
143+
self.validator = validatorClass(self.schema, registry=self.registry)
62144

63145
self._classes = None
64146
self._resolved = None
65147

148+
@property
149+
def resolver(self) -> referencing._core.Resolver:
150+
return self.registry.resolver()
151+
66152
@property
67153
def schema(self):
68154
try:
@@ -85,9 +171,6 @@ def get_class(self, uri):
85171
self._classes = self.build_classes()
86172
return self._resolved.get(uri, None)
87173

88-
def memory_resolver(self, uri):
89-
return self.mem_resolved[uri[7:]]
90-
91174
def relative_file_resolver(self, uri):
92175
path = os.path.join(self.basedir, uri[8:])
93176
with codecs.open(path, "r", "utf-8") as fin:
@@ -126,10 +209,11 @@ def build_classes(self, strict=False, named_only=False, standardize_names=True):
126209
kw = {"strict": strict}
127210
builder = classbuilder.ClassBuilder(self.resolver)
128211
for nm, defn in six.iteritems(self.schema.get("definitions", {})):
129-
uri = util.resolve_ref_uri(
130-
self.resolver.resolution_scope, "#/definitions/" + nm
212+
resolved = self.resolver.lookup("#/definitions/" + nm)
213+
uri = python_jsonschema_objects.util.resolve_ref_uri(
214+
self.resolver._base_uri, "#/definitions/" + nm
131215
)
132-
builder.construct(uri, defn, **kw)
216+
builder.construct(uri, resolved.contents, **kw)
133217

134218
if standardize_names:
135219
name_transform = lambda t: inflection.camelize(
@@ -152,7 +236,7 @@ def build_classes(self, strict=False, named_only=False, standardize_names=True):
152236
elif not named_only:
153237
classes[name_transform(uri.split("/")[-1])] = klass
154238

155-
return util.Namespace.from_mapping(classes)
239+
return python_jsonschema_objects.util.Namespace.from_mapping(classes)
156240

157241

158242
if __name__ == "__main__":

python_jsonschema_objects/classbuilder.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import collections
1+
import collections.abc
22
import copy
33
import itertools
44
import logging
55
import sys
66

7+
import referencing._core
78
import six
89

910
from python_jsonschema_objects import (
@@ -443,7 +444,7 @@ def __call__(self, *a, **kw):
443444

444445

445446
class ClassBuilder(object):
446-
def __init__(self, resolver):
447+
def __init__(self, resolver: referencing._core.Resolver):
447448
self.resolver = resolver
448449
self.resolved = {}
449450
self.under_construction = set()
@@ -462,10 +463,8 @@ def expand_references(self, source_uri, iterable):
462463
return pp
463464

464465
def resolve_type(self, ref, source):
465-
"""Return a resolved type for a URI, potentially constructing one if
466-
necessary.
467-
"""
468-
uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref)
466+
"""Return a resolved type for a URI, potentially constructing one if necessary"""
467+
uri = util.resolve_ref_uri(self.resolver._base_uri, ref)
469468
if uri in self.resolved:
470469
return self.resolved[uri]
471470

@@ -484,9 +483,9 @@ def resolve_type(self, ref, source):
484483
"Resolving direct reference object {0} -> {1}", source, uri
485484
)
486485
)
487-
with self.resolver.resolving(ref) as resolved:
488-
self.resolved[uri] = self.construct(uri, resolved, (ProtocolBase,))
489-
return self.resolved[uri]
486+
resolved = self.resolver.lookup(ref)
487+
self.resolved[uri] = self.construct(uri, resolved.contents, (ProtocolBase,))
488+
return self.resolved[uri]
490489

491490
def construct(self, uri, *args, **kw):
492491
"""Wrapper to debug things"""

python_jsonschema_objects/examples/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ The schema and code example below show how this works.
219219

220220
The `$ref` operator is supported in nearly all locations, and
221221
dispatches the actual reference resolution to the
222-
`jsonschema.RefResolver`.
222+
`referencing.Registry` resolver.
223223

224224
This example shows using the memory URI (described in more detail
225225
below) to create a wrapper object that is just a string literal.
@@ -298,6 +298,9 @@ ValidationError: '[u'author']' are required attributes for B
298298

299299
#### The "memory:" URI
300300

301+
**"memory:" URIs are deprecated (although they still work). Load resources into a
302+
`referencing.Registry` instead and pass those in**
303+
301304
The ObjectBuilder can be passed a dictionary specifying
302305
'memory' schemas when instantiated. This will allow it to
303306
resolve references where the referenced schemas are retrieved

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@
4141
install_requires=[
4242
"inflection>=0.2",
4343
"Markdown>=2.4",
44-
"jsonschema>=2.3,<4.18",
44+
"jsonschema>=4.18",
4545
"six>=1.5.2",
4646
],
47+
python_requires=">=3.8",
4748
cmdclass=versioneer.get_cmdclass(),
4849
classifiers=[
4950
"Programming Language :: Python :: 3",
50-
"Programming Language :: Python :: 3.7",
5151
"Programming Language :: Python :: 3.8",
5252
"Programming Language :: Python :: 3.9",
5353
"Programming Language :: Python :: 3.10",
Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,61 @@
1-
from jsonschema import Draft3Validator, RefResolver
2-
from jsonschema._utils import URIDict, load_schema
1+
import jsonschema.exceptions
2+
import pytest # noqa
3+
import referencing
4+
import referencing.exceptions
5+
import referencing.jsonschema
36

7+
import python_jsonschema_objects
48
import python_jsonschema_objects as pjo
59

610

7-
def test_non_default_resolver_validator(markdown_examples):
8-
ms = URIDict()
9-
draft3 = load_schema("draft3")
10-
draft4 = load_schema("draft4")
11-
ms[draft3["id"]] = draft3
12-
ms[draft4["id"]] = draft4
13-
resolver_with_store = RefResolver(draft3["id"], draft3, ms)
11+
def test_custom_spec_validator(markdown_examples):
12+
# This schema shouldn't be valid under DRAFT-03
13+
schema = {
14+
"$schema": "http://json-schema.org/draft-03/schema",
15+
"title": "other",
16+
"type": "any", # this wasn't valid starting in 04
17+
}
18+
pjo.ObjectBuilder(
19+
schema,
20+
resolved=markdown_examples,
21+
)
22+
23+
with pytest.raises(jsonschema.exceptions.ValidationError):
24+
pjo.ObjectBuilder(
25+
schema,
26+
specification_uri="http://json-schema.org/draft-04/schema",
27+
resolved=markdown_examples,
28+
)
29+
30+
31+
def test_non_default_resolver_finds_refs():
32+
registry = referencing.Registry()
33+
34+
remote_schema = {
35+
"$schema": "http://json-schema.org/draft-04/schema",
36+
"type": "number",
37+
}
38+
registry = registry.with_resource(
39+
"https://example.org/schema/example",
40+
referencing.Resource.from_contents(remote_schema),
41+
)
42+
43+
schema = {
44+
"$schema": "http://json-schema.org/draft-04/schema",
45+
"title": "other",
46+
"type": "object",
47+
"properties": {
48+
"local": {"type": "string"},
49+
"remote": {"$ref": "https://example.org/schema/example"},
50+
},
51+
}
1452

15-
# 'Other' schema should be valid with draft3
1653
builder = pjo.ObjectBuilder(
17-
markdown_examples["Other"],
18-
resolver=resolver_with_store,
19-
validatorClass=Draft3Validator,
20-
resolved=markdown_examples,
54+
schema,
55+
registry=registry,
2156
)
22-
klasses = builder.build_classes()
23-
a = klasses.Other(MyAddress="where I live")
24-
assert a.MyAddress == "where I live"
57+
ns = builder.build_classes()
58+
59+
thing = ns.Other(local="foo", remote=1)
60+
with pytest.raises(python_jsonschema_objects.ValidationError):
61+
thing = ns.Other(local="foo", remote="NaN")

0 commit comments

Comments
 (0)