Skip to content

Commit 2fda155

Browse files
ankostisdnephin
authored andcommitted
issue python-jsonschema#158: TRY to speed-up scope & $ref url-handling by keeping
fragments separated from URL (and avoid redunant frag/defrag). Conflicts: jsonschema/tests/test_benchmarks.py 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. Conflicts: jsonschema/tests/test_validators.py jsonschema/validators.py
1 parent a38eac9 commit 2fda155

File tree

2 files changed

+45
-23
lines changed

2 files changed

+45
-23
lines changed

jsonschema/compat.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import unicode_literals
2-
import sys
2+
3+
from collections import namedtuple
34
import operator
5+
import sys
6+
47

58
try:
69
from collections import MutableMapping, Sequence # noqa
@@ -40,14 +43,17 @@ def urlsplit(url):
4043
return SplitResult(scheme, netloc, path, query, fragment)
4144

4245

46+
DefragResult = namedtuple('DefragResult', 'url fragment')
47+
48+
4349
def urldefrag(url):
4450
if "#" in url:
4551
s, n, p, q, frag = urlsplit(url)
4652
defrag = urlunsplit((s, n, p, q, ''))
4753
else:
4854
defrag = url
4955
frag = ''
50-
return defrag, frag
56+
return DefragResult(defrag, frag)
5157

5258

5359
# flake8: noqa

jsonschema/validators.py

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
from jsonschema import _utils, _validators
1313
from jsonschema.compat import (
14-
Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen,
14+
Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen, DefragResult,
15+
1516
str_types, int_types, iteritems,
1617
)
1718
from jsonschema.exceptions import ErrorTree # Backwards compatibility # noqa
@@ -79,7 +80,10 @@ def iter_errors(self, instance, _schema=None):
7980
if _schema is None:
8081
_schema = self.schema
8182

82-
with self.resolver.in_scope(_schema.get(u"id", u"")):
83+
scope = _schema.get(u"id")
84+
if scope:
85+
self.resolver.push_scope(scope)
86+
try:
8387
ref = _schema.get(u"$ref")
8488
if ref is not None:
8589
validators = [(u"$ref", ref)]
@@ -103,6 +107,9 @@ def iter_errors(self, instance, _schema=None):
103107
if k != u"$ref":
104108
error.schema_path.appendleft(k)
105109
yield error
110+
finally:
111+
if scope:
112+
self.resolver.pop_scope()
106113

107114
def descend(self, instance, schema, path=None, schema_path=None):
108115
for error in self.iter_errors(instance, schema):
@@ -222,7 +229,7 @@ class RefResolver(object):
222229
223230
:argument str base_uri: URI of the referring document
224231
:argument referrer: the actual referring document
225-
:argument dict store: a mapping from URIs to documents to cache
232+
:argument dict store: a mapping from URIs (without fragments!) to documents to cache
226233
:argument bool cache_remote: whether remote refs should be cached after
227234
first resolution
228235
:argument dict handlers: a mapping from URI schemes to functions that
@@ -233,19 +240,21 @@ class RefResolver(object):
233240
def __init__(
234241
self, base_uri, referrer, store=(), cache_remote=True, handlers=(),
235242
):
243+
base_uri = urldefrag(base_uri)
236244
self.base_uri = base_uri
237245
self.resolution_scope = base_uri
238246
# This attribute is not used, it is for backwards compatibility
239247
self.referrer = referrer
240248
self.cache_remote = cache_remote
241249
self.handlers = dict(handlers)
242250

251+
self.scopes_stack = []
243252
self.store = _utils.URIDict(
244253
(id, validator.META_SCHEMA)
245254
for id, validator in iteritems(meta_schemas)
246255
)
247256
self.store.update(store)
248-
self.store[base_uri] = referrer
257+
self.store[base_uri.url] = referrer
249258

250259
@classmethod
251260
def from_schema(cls, schema, *args, **kwargs):
@@ -259,14 +268,19 @@ def from_schema(cls, schema, *args, **kwargs):
259268

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

262-
@contextlib.contextmanager
263-
def in_scope(self, scope):
271+
def push_scope(self, scope, is_defragged=False):
264272
old_scope = self.resolution_scope
265-
self.resolution_scope = urljoin(old_scope, scope)
266-
try:
267-
yield
268-
finally:
269-
self.resolution_scope = old_scope
273+
self.scopes_stack.append(old_scope)
274+
if not is_defragged:
275+
scope = urldefrag(scope)
276+
self.resolution_scope = DefragResult(
277+
urljoin(old_scope.url, scope.url, allow_fragments=False)
278+
if scope.url else old_scope.url,
279+
scope.fragment
280+
)
281+
282+
def pop_scope(self):
283+
self.resolution_scope = self.scopes_stack.pop()
270284

271285
@contextlib.contextmanager
272286
def resolving(self, ref):
@@ -278,24 +292,26 @@ def resolving(self, ref):
278292
279293
"""
280294

281-
full_uri = urljoin(self.resolution_scope, ref)
282-
uri, fragment = urldefrag(full_uri)
283-
if not uri:
284-
uri = self.base_uri
295+
ref = urldefrag(ref)
285296

286-
if uri in self.store:
287-
document = self.store[uri]
288-
else:
297+
url = urljoin(self.resolution_scope.url, ref.url, allow_fragments=False) \
298+
if ref.url else self.resolution_scope.url
299+
300+
try:
301+
document = self.store[url]
302+
except KeyError:
289303
try:
290-
document = self.resolve_remote(uri)
304+
document = self.resolve_remote(url)
291305
except Exception as exc:
292306
raise RefResolutionError(exc)
293307

308+
uri = DefragResult(url, ref.fragment)
294309
old_base_uri, self.base_uri = self.base_uri, uri
310+
self.push_scope(uri, is_defragged=True)
295311
try:
296-
with self.in_scope(uri):
297-
yield self.resolve_fragment(document, fragment)
312+
yield self.resolve_fragment(document, ref.fragment)
298313
finally:
314+
self.pop_scope()
299315
self.base_uri = old_base_uri
300316

301317
def resolve_fragment(self, document, fragment):

0 commit comments

Comments
 (0)