Skip to content

Commit 4c4da0d

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 179656d commit 4c4da0d

File tree

3 files changed

+53
-35
lines changed

3 files changed

+53
-35
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,25 @@ def test_is_type_raises_exception_for_unknown_type(self):
558558
with self.assertRaises(UnknownType):
559559
self.validator.is_type("foo", object())
560560

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+
561580

562581
class TestDraft3Validator(ValidatorTestMixin, unittest.TestCase):
563582
validator_class = Draft3Validator

jsonschema/validators.py

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

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

252251

253-
self.old_scopes = []
252+
self.scopes_stack = []
254253
self.store = _utils.URIDict(
255254
(id, validator.META_SCHEMA) ## IDs assumed pure urls (no fragments).
256255
for id, validator in iteritems(meta_schemas)
@@ -272,7 +271,7 @@ def from_schema(cls, schema, *args, **kwargs):
272271

273272
def push_scope(self, scope, is_defragged=False):
274273
old_scope = self.resolution_scope
275-
self.old_scopes.append(old_scope)
274+
self.scopes_stack.append(old_scope)
276275
if not is_defragged:
277276
scope = urldefrag(scope)
278277
self.resolution_scope = DefragResult(
@@ -282,7 +281,7 @@ def push_scope(self, scope, is_defragged=False):
282281
)
283282

284283
def pop_scope(self):
285-
self.resolution_scope = self.old_scopes.pop()
284+
self.resolution_scope = self.scopes_stack.pop()
286285

287286
@contextlib.contextmanager
288287
def resolving(self, ref):
@@ -309,11 +308,11 @@ def resolving(self, ref):
309308

310309
uri = DefragResult(url, ref.fragment)
311310
old_base_uri, self.base_uri = self.base_uri, uri
311+
self.push_scope(uri, is_defragged=True)
312312
try:
313-
self.push_scope(uri, is_defragged=True)
314313
yield self.resolve_fragment(document, ref.fragment)
315-
self.pop_scope()
316314
finally:
315+
self.pop_scope()
317316
self.base_uri = old_base_uri
318317

319318
def resolve_fragment(self, document, fragment):

0 commit comments

Comments
 (0)