Skip to content

Commit 8f66819

Browse files
committed
Revamp RefResolver.
Now properly should resolve both local and remote uris and fragments within them.
1 parent 2b48e71 commit 8f66819

File tree

2 files changed

+121
-60
lines changed

2 files changed

+121
-60
lines changed

jsonschema.py

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,36 @@
2121

2222
__version__ = "0.8dev"
2323

24-
FLOAT_TOLERANCE = 10 ** -15
2524
PY3 = sys.version_info[0] >= 3
2625

2726
if PY3:
2827
basestring = unicode = str
2928
iteritems = operator.methodcaller("items")
29+
from urllib import parse as urlparse
3030
from urllib.parse import unquote
3131
from urllib.request import urlopen
3232
else:
3333
from itertools import izip as zip
3434
iteritems = operator.methodcaller("iteritems")
3535
from urllib import unquote
3636
from urllib2 import urlopen
37+
import urlparse
38+
39+
40+
FLOAT_TOLERANCE = 10 ** -15
41+
validators = {}
42+
43+
44+
def validator(version):
45+
"""
46+
Register a validator for a ``version`` of the specification.
47+
48+
"""
49+
50+
def _validator(cls):
51+
validators[version] = cls
52+
return cls
53+
return _validator
3754

3855

3956
class UnknownType(Exception):
@@ -86,6 +103,7 @@ def __init__(self, message, validator=None, path=()):
86103
self.validator = validator
87104

88105

106+
@validator("draft3")
89107
class Draft3Validator(object):
90108
"""
91109
A validator for JSON Schema draft 3.
@@ -123,7 +141,7 @@ def __init__(self, schema, types=(), resolver=None):
123141
self._types["any"] = tuple(self._types.values())
124142

125143
if resolver is None:
126-
resolver = RefResolver()
144+
resolver = RefResolver.from_schema(schema)
127145

128146
self.resolver = resolver
129147
self.schema = schema
@@ -412,7 +430,7 @@ def validate_extends(self, extends, instance, schema):
412430
yield error
413431

414432
def validate_ref(self, ref, instance, schema):
415-
resolved = self.resolver.resolve(self.schema, ref)
433+
resolved = self.resolver.resolve(ref)
416434
for error in self.iter_errors(instance, resolved):
417435
yield error
418436

@@ -503,50 +521,69 @@ def validate_ref(self, ref, instance, schema):
503521

504522
class RefResolver(object):
505523
"""
506-
Resolve JSON Schema refs.
524+
Resolve JSON References.
507525
508526
"""
509527

510-
def __init__(self, store=None, get_page=urlopen):
511-
if store is None:
512-
store = {}
528+
def __init__(self, base_uri, referrer, store=()):
529+
self.base_uri = base_uri
530+
self.referrer = referrer
531+
self.store = collections.defaultdict(dict, store, **_meta_schemas())
532+
533+
@classmethod
534+
def from_schema(cls, schema, *args, **kwargs):
535+
"""
536+
Construct a resolver from a JSON schema object.
537+
538+
"""
513539

514-
self.get_page = get_page
515-
self.store = store
540+
return cls(schema.get("id", ""), schema, *args, **kwargs)
516541

517-
def resolve(self, root_schema, ref):
542+
def resolve(self, ref):
518543
"""
519-
Resolve a ``ref`` within the context of the ``root_schema``.
544+
Resolve a JSON ``ref``.
520545
521546
"""
522547

523-
if ref in self.store:
524-
return self.store[ref]
525-
elif ref.startswith("#"):
526-
return self.resolve_relative(root_schema, ref)
548+
base_uri = self.base_uri
549+
uri, fragment = urlparse.urldefrag(urlparse.urljoin(base_uri, ref))
550+
551+
if uri in self.store:
552+
document = self.store[uri]
553+
elif not uri or uri == self.base_uri:
554+
document = self.referrer
527555
else:
528-
return json.load(self.get_page(ref))
556+
document = self.resolve_remote(uri)
557+
558+
return self.resolve_fragment(document, fragment.lstrip("/"))
529559

530-
def resolve_relative(self, schema, ref):
560+
def resolve_fragment(self, document, fragment):
531561
"""
532-
Resolve a relative ``ref`` within the given ``schema``.
562+
Resolve a ``fragment`` within the referenced ``document``.
533563
534564
"""
535565

536-
if ref == "#":
537-
return schema
566+
parts = unquote(fragment).split("/") if fragment else []
538567

539-
parts = ref.lstrip("#/").split("/")
540-
parts = map(unquote, parts)
541-
parts = [part.replace('~1', '/').replace('~0', '~') for part in parts]
568+
for part in parts:
569+
part = part.replace("~1", "/").replace("~0", "~")
542570

543-
try:
544-
for part in parts:
545-
schema = schema[part]
546-
except KeyError:
547-
raise InvalidRef("Unresolvable json-pointer %r" % ref)
548-
else:
549-
return schema
571+
if part not in document:
572+
raise InvalidRef("Unresolvable JSON pointer: %r" % fragment)
573+
574+
document = document[part]
575+
576+
return document
577+
578+
def resolve_remote(self, uri):
579+
"""
580+
Resolve a remote ``uri``.
581+
582+
Does not check the store first.
583+
584+
"""
585+
586+
return json.load(urlopen(uri))
550587

551588

552589
class ErrorTree(object):
@@ -599,6 +636,16 @@ def total_errors(self):
599636
return len(self.errors) + child_errors
600637

601638

639+
def _meta_schemas():
640+
"""
641+
Collect the urls and meta schemas from each known validator.
642+
643+
"""
644+
645+
meta_schemas = (v.META_SCHEMA for v in validators.values())
646+
return dict((urlparse.urldefrag(m["id"])[0], m) for m in meta_schemas)
647+
648+
602649
def _find_additional_properties(instance, schema):
603650
"""
604651
Return the set of additional properties for the given ``instance``.

tests.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from __future__ import unicode_literals
22
from decimal import Decimal
3-
from functools import wraps
4-
from io import StringIO
3+
from io import BytesIO
54
import glob
65
import os
76
import re
87
import sys
9-
import warnings
108
import json
119

1210
if sys.version_info[:2] < (2, 7): # pragma: no cover
@@ -21,7 +19,7 @@
2119

2220
from jsonschema import (
2321
PY3, SchemaError, UnknownType, ValidationError, ErrorTree,
24-
Draft3Validator, RefResolver, urlopen, validate
22+
Draft3Validator, RefResolver, validate
2523
)
2624

2725

@@ -349,7 +347,7 @@ def test_it_delegates_to_a_ref_resolver(self):
349347
with self.assertRaises(ValidationError):
350348
Draft3Validator(schema, resolver=resolver).validate(None)
351349

352-
resolver.resolve.assert_called_once_with(schema, schema["$ref"])
350+
resolver.resolve.assert_called_once_with(schema["$ref"])
353351

354352
def test_is_type_is_true_for_valid_type(self):
355353
self.assertTrue(self.validator.is_type("foo", "string"))
@@ -372,36 +370,52 @@ def test_is_type_raises_exception_for_unknown_type(self):
372370

373371
class TestRefResolver(TestCase):
374372
def setUp(self):
375-
self.resolver = RefResolver()
376-
self.schema = mock.MagicMock()
373+
self.base_uri = ""
374+
self.referrer = {}
375+
self.store = {}
376+
self.resolver = RefResolver(self.base_uri, self.referrer, self.store)
377377

378-
def test_it_resolves_local_refs(self):
379-
ref = "#/properties/foo"
380-
resolved = self.resolver.resolve(self.schema, ref)
381-
self.assertEqual(resolved, self.schema["properties"]["foo"])
382-
383-
def test_it_retrieves_non_local_refs(self):
384-
schema = '{"type" : "integer"}'
385-
get_page = mock.Mock(return_value=StringIO(schema))
386-
resolver = RefResolver(get_page=get_page)
387-
388-
url = "http://example.com/schema"
389-
resolved = resolver.resolve(mock.Mock(), url)
378+
def test_it_does_not_retrieve_schema_urls_from_the_network(self):
379+
ref = Draft3Validator.META_SCHEMA["id"]
380+
with mock.patch.object(self.resolver, "resolve_remote") as remote:
381+
resolved = self.resolver.resolve(ref)
390382

391-
self.assertEqual(resolved, json.loads(schema))
392-
get_page.assert_called_once_with(url)
383+
self.assertEqual(resolved, Draft3Validator.META_SCHEMA)
384+
self.assertFalse(remote.called)
393385

394-
def test_it_uses_urlopen_by_default_for_nonlocal_refs(self):
395-
self.assertEqual(self.resolver.get_page, urlopen)
396-
397-
def test_it_accepts_a_ref_store(self):
398-
store = mock.Mock()
399-
self.assertEqual(RefResolver(store).store, store)
386+
def test_it_resolves_local_refs(self):
387+
ref = "#/properties/foo"
388+
self.referrer["properties"] = {"foo" : object()}
389+
resolved = self.resolver.resolve(ref)
390+
self.assertEqual(resolved, self.referrer["properties"]["foo"])
400391

401392
def test_it_retrieves_stored_refs(self):
402-
ref = self.resolver.store["cached_ref"] = mock.Mock()
403-
resolved = self.resolver.resolve(self.schema, "cached_ref")
404-
self.assertEqual(resolved, ref)
393+
ref = self.resolver.store["cached_ref"] = {"foo" : 12}
394+
resolved = self.resolver.resolve("cached_ref#/foo")
395+
self.assertEqual(resolved, 12)
396+
397+
def test_it_retrieves_unstored_refs_via_urlopen(self):
398+
ref = "http://bar#baz"
399+
schema = {"baz" : 12}
400+
401+
with mock.patch("jsonschema.urlopen") as urlopen:
402+
urlopen.return_value.read.return_value = json.dumps(schema)
403+
resolved = self.resolver.resolve(ref)
404+
405+
self.assertEqual(resolved, 12)
406+
urlopen.assert_called_once_with("http://bar")
407+
408+
def test_it_can_construct_a_base_uri_from_a_schema(self):
409+
schema = {"id" : "foo"}
410+
resolver = RefResolver.from_schema(schema)
411+
self.assertEqual(resolver.base_uri, "foo")
412+
self.assertEqual(resolver.referrer, schema)
413+
414+
def test_it_can_construct_a_base_uri_from_a_schema_without_id(self):
415+
schema = {}
416+
resolver = RefResolver.from_schema(schema)
417+
self.assertEqual(resolver.base_uri, "")
418+
self.assertEqual(resolver.referrer, schema)
405419

406420

407421
def sorted_errors(errors):

0 commit comments

Comments
 (0)