Skip to content

Commit d5d9f63

Browse files
committed
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.
1 parent 3d78861 commit d5d9f63

File tree

9 files changed

+180
-84
lines changed

9 files changed

+180
-84
lines changed

python_jsonschema_objects/__init__.py

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
import inflection
99
import jsonschema
1010
import six
11-
from jsonschema import Draft4Validator
12-
1311
import python_jsonschema_objects.classbuilder as classbuilder
1412
import python_jsonschema_objects.markdown_support
1513
import python_jsonschema_objects.util
1614
from python_jsonschema_objects.validators import ValidationError
15+
from typing import Optional
16+
17+
from jsonschema_specifications import REGISTRY as SPECIFICATIONS
18+
from referencing import Registry, Resource
19+
import referencing.typing
20+
import referencing.jsonschema
21+
import referencing.retrieval
1722

1823

1924
logger = logging.getLogger(__name__)
@@ -23,15 +28,20 @@
2328
FILE = __file__
2429

2530
SUPPORTED_VERSIONS = (
26-
"http://json-schema.org/draft-03/schema#",
27-
"http://json-schema.org/draft-04/schema#",
31+
"http://json-schema.org/draft-03/schema",
32+
"http://json-schema.org/draft-04/schema",
2833
)
2934

3035

3136
class ObjectBuilder(object):
32-
def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None):
33-
self.mem_resolved = resolved
34-
37+
def __init__(
38+
self,
39+
schema_uri,
40+
resolved={},
41+
registry: Optional[referencing.Registry] = None,
42+
resolver: Optional[referencing.typing.Retrieve] = None,
43+
specification_uri: str = "http://json-schema.org/draft-04/schema",
44+
):
3545
if isinstance(schema_uri, six.string_types):
3646
uri = os.path.normpath(schema_uri)
3747
self.basedir = os.path.dirname(uri)
@@ -44,7 +54,7 @@ def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None):
4454

4555
if (
4656
"$schema" in self.schema
47-
and self.schema["$schema"] not in SUPPORTED_VERSIONS
57+
and self.schema["$schema"].rstrip("#") not in SUPPORTED_VERSIONS
4858
):
4959
warnings.warn(
5060
"Schema version {} not recognized. Some "
@@ -53,15 +63,78 @@ def __init__(self, schema_uri, resolved={}, resolver=None, validatorClass=None):
5363
)
5464
)
5565

56-
self.resolver = resolver or jsonschema.RefResolver.from_schema(self.schema)
57-
self.resolver.handlers.update(
58-
{"file": self.relative_file_resolver, "memory": self.memory_resolver}
59-
)
66+
if registry is not None:
67+
if not isinstance(registry, referencing.Registry):
68+
raise TypeError("registry must be a Registry instance")
69+
70+
if resolver is not None:
71+
raise AttributeError(
72+
"Cannot specify both registry and resolver. If you provide your own registry, pass the resolver directly to that"
73+
)
74+
self.registry = registry
75+
else:
76+
if resolver is not None:
77+
78+
def file_and_memory_handler(uri):
79+
if uri.startswith("file:"):
80+
return Resource.from_contents(self.relative_file_resolver(uri))
81+
return resolver(uri)
82+
83+
self.registry = Registry(retrieve=file_and_memory_handler)
84+
else:
85+
86+
def file_and_memory_handler(uri):
87+
if uri.startswith("file:"):
88+
return Resource.from_contents(self.relative_file_resolver(uri))
89+
raise RuntimeError(
90+
"No remote resource resolver provided. Cannot resolve {}".format(
91+
uri
92+
)
93+
)
6094

61-
validatorClass = validatorClass or Draft4Validator
62-
meta_validator = validatorClass(validatorClass.META_SCHEMA)
95+
self.registry = Registry(retrieve=file_and_memory_handler)
96+
97+
if len(resolved) > 0:
98+
warnings.warn(
99+
"Use of 'memory:' URIs is deprecated. Provide a registry with properly resolved references "
100+
"if you want to resolve items externally.",
101+
DeprecationWarning,
102+
)
103+
for uri, contents in resolved.items():
104+
self.registry = self.registry.with_resource(
105+
"memory:" + uri,
106+
referencing.Resource.from_contents(contents, specification_uri),
107+
)
108+
109+
if "$schema" not in self.schema:
110+
warnings.warn("Schema version not specified. Defaulting to draft4")
111+
updated = {"$schema": specification_uri}
112+
updated.update(self.schema)
113+
self.schema = updated
114+
115+
schema = Resource.from_contents(self.schema)
116+
if schema.id() is None:
117+
warnings.warn("Schema id not specified. Defaulting to 'self'")
118+
updated = {"$id": "self", "id": "self"}
119+
updated.update(self.schema)
120+
self.schema = updated
121+
schema = Resource.from_contents(self.schema)
122+
123+
self.registry = self.registry.with_resource("", schema)
124+
self.resolver = self.registry.resolver()
125+
126+
if specification_uri is not None:
127+
validatorClass = jsonschema.validators.validator_for(
128+
{"$schema": specification_uri}
129+
)
130+
else:
131+
validatorClass = jsonschema.validators.validator_for(self.schema)
132+
133+
meta_validator = validatorClass(
134+
validatorClass.META_SCHEMA, registry=self.registry
135+
)
63136
meta_validator.validate(self.schema)
64-
self.validator = validatorClass(self.schema, resolver=self.resolver)
137+
self.validator = validatorClass(self.schema, registry=self.registry)
65138

66139
self._classes = None
67140
self._resolved = None
@@ -88,9 +161,6 @@ def get_class(self, uri):
88161
self._classes = self.build_classes()
89162
return self._resolved.get(uri, None)
90163

91-
def memory_resolver(self, uri):
92-
return self.mem_resolved[uri[7:]]
93-
94164
def relative_file_resolver(self, uri):
95165
path = os.path.join(self.basedir, uri[8:])
96166
with codecs.open(path, "r", "utf-8") as fin:
@@ -129,10 +199,11 @@ def build_classes(self, strict=False, named_only=False, standardize_names=True):
129199
kw = {"strict": strict}
130200
builder = classbuilder.ClassBuilder(self.resolver)
131201
for nm, defn in six.iteritems(self.schema.get("definitions", {})):
202+
resolved = self.resolver.lookup("#/definitions/" + nm)
132203
uri = python_jsonschema_objects.util.resolve_ref_uri(
133-
self.resolver.resolution_scope, "#/definitions/" + nm
204+
self.resolver._base_uri, "#/definitions/" + nm
134205
)
135-
builder.construct(uri, defn, **kw)
206+
builder.construct(uri, resolved.contents, **kw)
136207

137208
if standardize_names:
138209
name_transform = lambda t: inflection.camelize(

python_jsonschema_objects/classbuilder.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import referencing._core
2+
13
import python_jsonschema_objects.util as util
24
import python_jsonschema_objects.validators as validators
35
import python_jsonschema_objects.pattern_properties as pattern_properties
@@ -444,7 +446,7 @@ def __call__(self, *a, **kw):
444446

445447

446448
class ClassBuilder(object):
447-
def __init__(self, resolver):
449+
def __init__(self, resolver: referencing._core.Resolver):
448450
self.resolver = resolver
449451
self.resolved = {}
450452
self.under_construction = set()
@@ -464,7 +466,7 @@ def expand_references(self, source_uri, iterable):
464466

465467
def resolve_type(self, ref, source):
466468
"""Return a resolved type for a URI, potentially constructing one if necessary"""
467-
uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref)
469+
uri = util.resolve_ref_uri(self.resolver._base_uri, ref)
468470
if uri in self.resolved:
469471
return self.resolved[uri]
470472

@@ -483,9 +485,9 @@ def resolve_type(self, ref, source):
483485
"Resolving direct reference object {0} -> {1}", source, uri
484486
)
485487
)
486-
with self.resolver.resolving(ref) as resolved:
487-
self.resolved[uri] = self.construct(uri, resolved, (ProtocolBase,))
488-
return self.resolved[uri]
488+
resolved = self.resolver.lookup(ref)
489+
self.resolved[uri] = self.construct(uri, resolved.contents, (ProtocolBase,))
490+
return self.resolved[uri]
489491

490492
def construct(self, uri, *args, **kw):
491493
"""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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
install_requires=[
4242
"inflection>=0.2",
4343
"Markdown>=2.4",
44-
"jsonschema>=2.3,<4.18",
44+
"jsonschema>=2.3",
4545
"six>=1.5.2",
4646
],
4747
cmdclass=versioneer.get_cmdclass(),
Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,52 @@
11
import pytest # noqa
2-
from jsonschema import RefResolver, Draft3Validator
3-
from jsonschema._utils import load_schema, URIDict
2+
import referencing
3+
import json
4+
import referencing.jsonschema
5+
import referencing.exceptions
46
import python_jsonschema_objects as pjo
57

68

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)
14-
15-
# 'Other' schema should be valid with draft3
9+
def test_custom_spec_validator(markdown_examples):
10+
# This schema shouldn't be valid under DRAFT-03
11+
schema = {
12+
"$schema": "http://json-schema.org/draft-04/schema",
13+
"title": "other",
14+
"oneOf": [{"type": "string"}, {"type": "number"}],
15+
}
1616
builder = pjo.ObjectBuilder(
17-
markdown_examples["Other"],
18-
resolver=resolver_with_store,
19-
validatorClass=Draft3Validator,
17+
schema,
18+
specification_uri="http://json-schema.org/draft-03/schema",
2019
resolved=markdown_examples,
2120
)
2221
klasses = builder.build_classes()
23-
a = klasses.Other(MyAddress="where I live")
24-
assert a.MyAddress == "where I live"
22+
a = klasses.Other("foo")
23+
assert a == "foo"
24+
25+
26+
def test_non_default_resolver_finds_refs():
27+
registry = referencing.Registry()
28+
29+
remote_schema = {
30+
"$schema": "http://json-schema.org/draft-04/schema",
31+
"type": "number",
32+
}
33+
registry = registry.with_resource(
34+
"https://example.org/schema/example",
35+
referencing.Resource.from_contents(remote_schema),
36+
)
37+
38+
schema = {
39+
"$schema": "http://json-schema.org/draft-04/schema",
40+
"title": "other",
41+
"type": "object",
42+
"properties": {
43+
"local": {"type": "string"},
44+
"remote": {"$ref": "https://example.org/schema/example"},
45+
},
46+
}
47+
48+
builder = pjo.ObjectBuilder(
49+
schema,
50+
registry=registry,
51+
)
52+
builder.build_classes()

0 commit comments

Comments
 (0)