Skip to content

Commit 7225b76

Browse files
committed
issue python-jsonschema#158: Use try-finally to ensure resolver scopes_stack empty when
iteration breaks (no detectable performance penalty). * Replace non-python-2.6 DefragResult with named-tuple. * Add test-case checking scopes_stack empty.
1 parent cd88c19 commit 7225b76

File tree

3 files changed

+63
-139
lines changed

3 files changed

+63
-139
lines changed

jsonschema/compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import unicode_literals
22

3+
from collections import namedtuple
34
import operator
45
import sys
56

@@ -16,7 +17,6 @@
1617
from io import StringIO
1718
from urllib.parse import (
1819
unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit,
19-
DefragResult
2020
)
2121
from urllib.request import urlopen
2222
str_types = str,
@@ -27,7 +27,6 @@
2727
from StringIO import StringIO
2828
from urlparse import (
2929
urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit, # noqa
30-
DefragResult
3130
)
3231
from urllib import unquote # noqa
3332
from urllib2 import urlopen # noqa
@@ -44,6 +43,7 @@ def urlsplit(url):
4443
return SplitResult(scheme, netloc, path, query, fragment)
4544

4645

46+
DefragResult = namedtuple('DefragResult', 'url fragment')
4747
def urldefrag(url):
4848
if "#" in url:
4949
s, n, p, q, frag = urlsplit(url)

jsonschema/tests/test_validators.py

Lines changed: 21 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -425,106 +425,6 @@ def test_multiple_nesting(self):
425425
self.assertEqual(e5.validator, "minItems")
426426
self.assertEqual(e6.validator, "enum")
427427

428-
def test_recursive(self):
429-
schema = {
430-
"definitions": {
431-
"node": {
432-
"anyOf": [{
433-
"type": "object",
434-
"required": ["name", "children"],
435-
"properties": {
436-
"name": {
437-
"type": "string",
438-
},
439-
"children": {
440-
"type": "object",
441-
"patternProperties": {
442-
"^.*$": {
443-
"$ref": "#/definitions/node",
444-
},
445-
},
446-
},
447-
},
448-
}],
449-
},
450-
},
451-
"type": "object",
452-
"required": ["root"],
453-
"properties": {
454-
"root": {"$ref": "#/definitions/node"},
455-
}
456-
}
457-
458-
instance = {
459-
"root": {
460-
"name": "root",
461-
"children": {
462-
"a": {
463-
"name": "a",
464-
"children": {
465-
"ab": {
466-
"name": "ab",
467-
# missing "children"
468-
}
469-
}
470-
},
471-
},
472-
},
473-
}
474-
validator = Draft4Validator(schema)
475-
476-
e, = validator.iter_errors(instance)
477-
self.assertEqual(e.absolute_path, deque(["root"]))
478-
self.assertEqual(
479-
e.absolute_schema_path, deque(["properties", "root", "anyOf"]),
480-
)
481-
482-
e1, = e.context
483-
self.assertEqual(e1.absolute_path, deque(["root", "children", "a"]))
484-
self.assertEqual(
485-
e1.absolute_schema_path, deque(
486-
[
487-
"properties",
488-
"root",
489-
"anyOf",
490-
0,
491-
"properties",
492-
"children",
493-
"patternProperties",
494-
"^.*$",
495-
"anyOf",
496-
],
497-
),
498-
)
499-
500-
e2, = e1.context
501-
self.assertEqual(
502-
e2.absolute_path, deque(
503-
["root", "children", "a", "children", "ab"],
504-
),
505-
)
506-
self.assertEqual(
507-
e2.absolute_schema_path, deque(
508-
[
509-
"properties",
510-
"root",
511-
"anyOf",
512-
0,
513-
"properties",
514-
"children",
515-
"patternProperties",
516-
"^.*$",
517-
"anyOf",
518-
0,
519-
"properties",
520-
"children",
521-
"patternProperties",
522-
"^.*$",
523-
"anyOf"
524-
],
525-
),
526-
)
527-
528428
def test_additionalProperties(self):
529429
instance = {"bar": "bar", "foo": 2}
530430
schema = {
@@ -658,6 +558,25 @@ def test_is_type_raises_exception_for_unknown_type(self):
658558
with self.assertRaises(UnknownType):
659559
self.validator.is_type("foo", object())
660560

561+
def test_resolver_scope_stack_is_empty_after_exception(self):
562+
instance ={
563+
'foo': 1
564+
}
565+
schema = {
566+
'id':'/a/b',
567+
'type': ['object'],
568+
'properties': {
569+
'foo': {
570+
'id':'c',
571+
'type': ['string']
572+
}
573+
}
574+
}
575+
with self.assertRaises(ValidationError):
576+
self.validator.validate(instance, schema)
577+
578+
self.assertEqual(len(self.validator.resolver.scopes_stack), 0)
579+
661580

662581
class TestDraft3Validator(ValidatorTestMixin, unittest.TestCase):
663582
validator_class = Draft3Validator
@@ -815,7 +734,7 @@ def test_it_retrieves_unstored_refs_via_urlopen(self):
815734
def test_it_can_construct_a_base_uri_from_a_schema(self):
816735
schema = {"id" : "foo"}
817736
resolver = RefResolver.from_schema(schema)
818-
self.assertEqual(resolver.base_uri, "foo")
737+
self.assertEqual(resolver.base_uri.url, "foo")
819738
with resolver.resolving("") as resolved:
820739
self.assertEqual(resolved, schema)
821740
with resolver.resolving("#") as resolved:
@@ -828,7 +747,7 @@ def test_it_can_construct_a_base_uri_from_a_schema(self):
828747
def test_it_can_construct_a_base_uri_from_a_schema_without_id(self):
829748
schema = {}
830749
resolver = RefResolver.from_schema(schema)
831-
self.assertEqual(resolver.base_uri, "")
750+
self.assertEqual(resolver.base_uri.url, "")
832751
with resolver.resolving("") as resolved:
833752
self.assertEqual(resolved, schema)
834753
with resolver.resolving("#") as resolved:

jsonschema/validators.py

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,33 @@ def iter_errors(self, instance, _schema=None):
8484
has_scope = scope
8585
if has_scope:
8686
self.resolver.push_scope(scope)
87-
88-
89-
ref = _schema.get(u"$ref")
90-
if ref is not None:
91-
validators = [(u"$ref", ref)]
92-
else:
93-
validators = iteritems(_schema)
94-
95-
for k, v in validators:
96-
validator = self.VALIDATORS.get(k)
97-
if validator is None:
98-
continue
99-
100-
errors = validator(self, v, instance, _schema) or ()
101-
for error in errors:
102-
# set details if not already set by the called fn
103-
error._set(
104-
validator=k,
105-
validator_value=v,
106-
instance=instance,
107-
schema=_schema,
108-
)
109-
if k != u"$ref":
110-
error.schema_path.appendleft(k)
111-
yield error
112-
113-
if has_scope:
114-
self.resolver.pop_scope()
87+
try:
88+
ref = _schema.get(u"$ref")
89+
if ref is None:
90+
validators = iteritems(_schema)
91+
else:
92+
validators = [(u"$ref", ref)]
93+
94+
for k, v in validators:
95+
validator = self.VALIDATORS.get(k)
96+
if validator is None:
97+
continue
98+
99+
errors = validator(self, v, instance, _schema) or ()
100+
for error in errors:
101+
# set details if not already set by the called fn
102+
error._set(
103+
validator=k,
104+
validator_value=v,
105+
instance=instance,
106+
schema=_schema,
107+
)
108+
if k != u"$ref":
109+
error.schema_path.appendleft(k)
110+
yield error
111+
finally:
112+
if scope:
113+
self.resolver.pop_scope()
115114

116115
def descend(self, instance, schema, path=None, schema_path=None):
117116
for error in self.iter_errors(instance, schema):
@@ -251,7 +250,7 @@ def __init__(
251250
self.handlers = dict(handlers)
252251

253252

254-
self.old_scopes = []
253+
self.scopes_stack = []
255254
self.store = _utils.URIDict(
256255
(id, validator.META_SCHEMA) ## IDs assumed pure urls (no fragments).
257256
for id, validator in iteritems(meta_schemas)
@@ -273,11 +272,17 @@ def from_schema(cls, schema, *args, **kwargs):
273272

274273
def push_scope(self, resoultion_scope):
275274
old_scope = self.resolution_scope
276-
self.old_scopes.append(old_scope)
277-
self.resolution_scope = urljoin(old_scope, resoultion_scope)
275+
self.scopes_stack.append(old_scope)
276+
if not is_defragged:
277+
scope = urldefrag(scope)
278+
self.resolution_scope = DefragResult(
279+
urljoin(old_scope.url, scope.url, allow_fragments=False)
280+
if scope.url else old_scope.url,
281+
scope.fragment
282+
)
278283

279284
def pop_scope(self):
280-
self.resolution_scope = self.old_scopes.pop()
285+
self.resolution_scope = self.scopes_stack.pop()
281286

282287
@contextlib.contextmanager
283288
def resolving(self, ref):
@@ -304,11 +309,11 @@ def resolving(self, ref):
304309

305310
uri = DefragResult(url, ref.fragment)
306311
old_base_uri, self.base_uri = self.base_uri, uri
312+
self.push_scope(uri, is_defragged=True)
307313
try:
308-
self.push_scope(uri)
309-
yield self.resolve_fragment(document, fragment)
310-
self.pop_scope()
314+
yield self.resolve_fragment(document, ref.fragment)
311315
finally:
316+
self.pop_scope()
312317
self.base_uri = old_base_uri
313318

314319
def resolve_fragment(self, document, fragment):

0 commit comments

Comments
 (0)