Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ref resolution scope #68

Merged
merged 4 commits into from Feb 22, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 41 additions & 19 deletions jsonschema.py
Expand Up @@ -12,6 +12,7 @@
from __future__ import division, unicode_literals

import collections
import contextlib
import datetime
import itertools
import json
Expand Down Expand Up @@ -138,18 +139,21 @@ def iter_errors(self, instance, _schema=None):
if _schema is None:
_schema = self.schema

for k, v in iteritems(_schema):
validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None)
with self.resolver.in_scope(_schema.get("id", "")):
for k, v in iteritems(_schema):
validator = getattr(self,
"validate_%s" % (k.lstrip("$"),), None)

if validator is None:
continue
if validator is None:
continue

errors = validator(v, instance, _schema) or ()
for error in errors:
# set the validator if it wasn't already set by the called fn
if error.validator is None:
error.validator = k
yield error
errors = validator(v, instance, _schema) or ()
for error in errors:
# set the validator if it wasn't already set by the
# called function
if error.validator is None:
error.validator = k
yield error

def validate(self, *args, **kwargs):
for error in self.iter_errors(*args, **kwargs):
Expand Down Expand Up @@ -357,9 +361,9 @@ def validate_extends(self, extends, instance, schema):
yield error

def validate_ref(self, ref, instance, schema):
resolved = self.resolver.resolve(ref)
for error in self.iter_errors(instance, resolved):
yield error
with self.resolver.resolving(ref) as resolved:
for error in self.iter_errors(instance, resolved):
yield error


Draft3Validator.META_SCHEMA = {
Expand Down Expand Up @@ -642,6 +646,7 @@ class RefResolver(object):
def __init__(self, base_uri, referrer, store=(), cache_remote=True,
handlers=()):
self.base_uri = base_uri
self.resolution_scope = base_uri
self.referrer = referrer
self.store = dict(store, **_meta_schemas())
self.cache_remote = cache_remote
Expand All @@ -659,17 +664,27 @@ def from_schema(cls, schema, *args, **kwargs):

return cls(schema.get("id", ""), schema, *args, **kwargs)

def resolve(self, ref):
@contextlib.contextmanager
def in_scope(self, scope):
old_scope = self.resolution_scope
self.resolution_scope = urlparse.urljoin(old_scope, scope)
try:
yield
finally:
self.resolution_scope = old_scope

@contextlib.contextmanager
def resolving(self, ref):
"""
Resolve a JSON ``ref``.
Context manager which resolves a JSON ``ref`` and enters the
resolution scope of this ref.

:argument str ref: reference to resolve
:returns: the referrant document

"""

base_uri = self.base_uri
uri, fragment = urlparse.urldefrag(urlparse.urljoin(base_uri, ref))
full_uri = urlparse.urljoin(self.resolution_scope, ref)
uri, fragment = urlparse.urldefrag(full_uri)

if uri in self.store:
document = self.store[uri]
Expand All @@ -678,7 +693,13 @@ def resolve(self, ref):
else:
document = self.resolve_remote(uri)

return self.resolve_fragment(document, fragment.lstrip("/"))
old_base_uri, old_referrer = self.base_uri, self.referrer
self.base_uri, self.referrer = uri, document
try:
with self.in_scope(uri):
yield self.resolve_fragment(document, fragment)
finally:
self.base_uri, self.referrer = old_base_uri, old_referrer

def resolve_fragment(self, document, fragment):
"""
Expand All @@ -689,6 +710,7 @@ def resolve_fragment(self, document, fragment):

"""

fragment = fragment.lstrip("/")
parts = unquote(fragment).split("/") if fragment else []

for part in parts:
Expand Down
55 changes: 31 additions & 24 deletions tests.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
from decimal import Decimal
import contextlib
import glob
import os
import re
Expand Down Expand Up @@ -404,14 +405,19 @@ def test_it_creates_a_ref_resolver_if_not_provided(self):
self.assertIsInstance(self.validator.resolver, RefResolver)

def test_it_delegates_to_a_ref_resolver(self):
resolver = mock.Mock()
resolver.resolve.return_value = {"type" : "integer"}
resolver = RefResolver("", {})
schema = {"$ref" : mock.Mock()}

with self.assertRaises(ValidationError):
Draft3Validator(schema, resolver=resolver).validate(None)
@contextlib.contextmanager
def resolving():
yield {"type": "integer"}

with mock.patch.object(resolver, "resolving") as resolve:
resolve.return_value = resolving()
with self.assertRaises(ValidationError):
Draft3Validator(schema, resolver=resolver).validate(None)

resolver.resolve.assert_called_once_with(schema["$ref"])
resolve.assert_called_once_with(schema["$ref"])

def test_is_type_is_true_for_valid_type(self):
self.assertTrue(self.validator.is_type("foo", "string"))
Expand Down Expand Up @@ -445,31 +451,29 @@ def setUp(self):
def test_it_does_not_retrieve_schema_urls_from_the_network(self):
ref = Draft3Validator.META_SCHEMA["id"]
with mock.patch.object(self.resolver, "resolve_remote") as remote:
resolved = self.resolver.resolve(ref)

self.assertEqual(resolved, Draft3Validator.META_SCHEMA)
with self.resolver.resolving(ref) as resolved:
self.assertEqual(resolved, Draft3Validator.META_SCHEMA)
self.assertFalse(remote.called)

def test_it_resolves_local_refs(self):
ref = "#/properties/foo"
self.referrer["properties"] = {"foo" : object()}
resolved = self.resolver.resolve(ref)
self.assertEqual(resolved, self.referrer["properties"]["foo"])
with self.resolver.resolving(ref) as resolved:
self.assertEqual(resolved, self.referrer["properties"]["foo"])

def test_it_retrieves_stored_refs(self):
self.resolver.store["cached_ref"] = {"foo" : 12}
resolved = self.resolver.resolve("cached_ref#/foo")
self.assertEqual(resolved, 12)
with self.resolver.resolving("cached_ref#/foo") as resolved:
self.assertEqual(resolved, 12)

def test_it_retrieves_unstored_refs_via_requests(self):
ref = "http://bar#baz"
schema = {"baz" : 12}

with mock.patch("jsonschema.requests") as requests:
requests.get.return_value.json.return_value = schema
resolved = self.resolver.resolve(ref)

self.assertEqual(resolved, 12)
with self.resolver.resolving(ref) as resolved:
self.assertEqual(resolved, 12)
requests.get.assert_called_once_with("http://bar")

def test_it_retrieves_unstored_refs_via_urlopen(self):
Expand All @@ -480,9 +484,8 @@ def test_it_retrieves_unstored_refs_via_urlopen(self):
with mock.patch("jsonschema.urlopen") as urlopen:
urlopen.return_value.read.return_value = (
json.dumps(schema).encode("utf8"))
resolved = self.resolver.resolve(ref)

self.assertEqual(resolved, 12)
with self.resolver.resolving(ref) as resolved:
self.assertEqual(resolved, 12)
urlopen.assert_called_once_with("http://bar")

def test_it_can_construct_a_base_uri_from_a_schema(self):
Expand All @@ -502,26 +505,30 @@ def test_custom_uri_scheme_handlers(self):
ref = "foo://bar"
foo_handler = mock.Mock(return_value=schema)
resolver = RefResolver("", {}, handlers={"foo": foo_handler})
resolved = resolver.resolve(ref)
self.assertEqual(resolved, schema)
with resolver.resolving(ref) as resolved:
self.assertEqual(resolved, schema)
foo_handler.assert_called_once_with(ref)

def test_cache_remote_on(self):
ref = "foo://bar"
foo_handler = mock.Mock()
resolver = RefResolver("", {}, cache_remote=True,
handlers={"foo": foo_handler})
resolver.resolve(ref)
resolver.resolve(ref)
with resolver.resolving(ref):
pass
with resolver.resolving(ref):
pass
foo_handler.assert_called_once_with(ref)

def test_cache_remote_off(self):
ref = "foo://bar"
foo_handler = mock.Mock()
resolver = RefResolver("", {}, cache_remote=False,
handlers={"foo": foo_handler})
resolver.resolve(ref)
resolver.resolve(ref)
with resolver.resolving(ref):
pass
with resolver.resolving(ref):
pass
self.assertEqual(foo_handler.call_count, 2)


Expand Down