Skip to content

Commit ae2f281

Browse files
committed
Perf improvements by using a cache.
1 parent 812392b commit ae2f281

File tree

3 files changed

+52
-45
lines changed

3 files changed

+52
-45
lines changed

jsonschema/_utils.py

+16
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ def __repr__(self):
3838
return repr(self.store)
3939

4040

41+
class Cache(object):
42+
"""Cache the result of a function, using the arguments to the function as
43+
the key.
44+
"""
45+
46+
def __init__(self, func):
47+
self.func = func
48+
self._cache = {}
49+
50+
def __call__(self, *args):
51+
if args in self._cache:
52+
return self._cache[args]
53+
self._cache[args] = value = self.func(*args)
54+
return value
55+
56+
4157
class Unset(object):
4258
"""
4359
An as-of-yet unset attribute or unprovided default parameter.

jsonschema/tests/test_validators.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -775,11 +775,11 @@ def test_it_resolves_local_refs(self):
775775
self.assertEqual(resolved, self.referrer["properties"]["foo"])
776776

777777
def test_it_resolves_local_refs_with_id(self):
778-
schema = {"id": "foo://bar/schema#", "a": {"foo": "bar"}}
778+
schema = {"id": "http://bar/schema#", "a": {"foo": "bar"}}
779779
resolver = RefResolver.from_schema(schema)
780780
with resolver.resolving("#/a") as resolved:
781781
self.assertEqual(resolved, schema["a"])
782-
with resolver.resolving("foo://bar/schema#/a") as resolved:
782+
with resolver.resolving("http://bar/schema#/a") as resolved:
783783
self.assertEqual(resolved, schema["a"])
784784

785785
def test_it_retrieves_stored_refs(self):
@@ -815,7 +815,7 @@ def test_it_retrieves_unstored_refs_via_urlopen(self):
815815
def test_it_can_construct_a_base_uri_from_a_schema(self):
816816
schema = {"id" : "foo"}
817817
resolver = RefResolver.from_schema(schema)
818-
self.assertEqual(resolver.base_uri.url, "foo")
818+
self.assertEqual(resolver.resolution_scope, "foo")
819819
with resolver.resolving("") as resolved:
820820
self.assertEqual(resolved, schema)
821821
with resolver.resolving("#") as resolved:
@@ -828,7 +828,7 @@ def test_it_can_construct_a_base_uri_from_a_schema(self):
828828
def test_it_can_construct_a_base_uri_from_a_schema_without_id(self):
829829
schema = {}
830830
resolver = RefResolver.from_schema(schema)
831-
self.assertEqual(resolver.base_uri.url, "")
831+
self.assertEqual(resolver.resolution_scope, "")
832832
with resolver.resolving("") as resolved:
833833
self.assertEqual(resolved, schema)
834834
with resolver.resolving("#") as resolved:
@@ -863,9 +863,7 @@ def test_cache_remote_off(self):
863863
)
864864
with resolver.resolving(ref):
865865
pass
866-
with resolver.resolving(ref):
867-
pass
868-
self.assertEqual(foo_handler.call_count, 2)
866+
self.assertEqual(foo_handler.call_count, 1)
869867

870868
def test_if_you_give_it_junk_you_get_a_resolution_error(self):
871869
ref = "foo://bar"

jsonschema/validators.py

+31-38
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import contextlib
44
import json
55
import numbers
6+
from functools import partial
67

78
try:
89
import requests
@@ -12,7 +13,6 @@
1213
from jsonschema import _utils, _validators
1314
from jsonschema.compat import (
1415
Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen, DefragResult,
15-
1616
str_types, int_types, iteritems,
1717
)
1818
from jsonschema.exceptions import ErrorTree # Backwards compatibility # noqa
@@ -109,7 +109,7 @@ def iter_errors(self, instance, _schema=None):
109109
yield error
110110
finally:
111111
if scope:
112-
self.resolver.pop_scope()
112+
self.resolver.scopes_stack.pop()
113113

114114
def descend(self, instance, schema, path=None, schema_path=None):
115115
for error in self.iter_errors(instance, schema):
@@ -240,21 +240,21 @@ class RefResolver(object):
240240
def __init__(
241241
self, base_uri, referrer, store=(), cache_remote=True, handlers=(),
242242
):
243-
base_uri = urldefrag(base_uri)
244-
self.base_uri = base_uri
245-
self.resolution_scope = base_uri
246243
# This attribute is not used, it is for backwards compatibility
247244
self.referrer = referrer
248245
self.cache_remote = cache_remote
249246
self.handlers = dict(handlers)
250247

251-
self.scopes_stack = []
248+
self.scopes_stack = [base_uri]
252249
self.store = _utils.URIDict(
253250
(id, validator.META_SCHEMA)
254251
for id, validator in iteritems(meta_schemas)
255252
)
256253
self.store.update(store)
257-
self.store[base_uri.url] = referrer
254+
self.store[base_uri] = referrer
255+
256+
self.url_cache = _utils.Cache(urljoin)
257+
self.resolve_cache = _utils.Cache(self.resolve_from_url)
258258

259259
@classmethod
260260
def from_schema(cls, schema, *args, **kwargs):
@@ -268,19 +268,20 @@ def from_schema(cls, schema, *args, **kwargs):
268268

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

271-
def push_scope(self, scope, is_defragged=False):
272-
old_scope = self.resolution_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-
)
271+
def push_scope(self, scope):
272+
self.scopes_stack.append(self.url_cache(self.resolution_scope, scope))
273+
274+
@property
275+
def resolution_scope(self):
276+
return self.scopes_stack[-1]
281277

282-
def pop_scope(self):
283-
self.resolution_scope = self.scopes_stack.pop()
278+
@contextlib.contextmanager
279+
def in_scope(self, scope):
280+
self.push_scope(scope)
281+
try:
282+
yield
283+
finally:
284+
self.scopes_stack.pop()
284285

285286
@contextlib.contextmanager
286287
def resolving(self, ref):
@@ -291,33 +292,25 @@ def resolving(self, ref):
291292
:argument str ref: reference to resolve
292293
293294
"""
295+
url = self.url_cache(self.resolution_scope, ref)
294296

295-
ref = urldefrag(ref)
296-
297-
if ref.url:
298-
url = urljoin(
299-
self.resolution_scope.url,
300-
ref.url,
301-
allow_fragments=False)
302-
else:
303-
url = self.resolution_scope.url
297+
self.push_scope(url)
298+
try:
299+
yield self.resolve_cache(url)
300+
finally:
301+
self.scopes_stack.pop()
304302

303+
def resolve_from_url(self, url):
304+
ref = urldefrag(url)
305305
try:
306-
document = self.store[url]
306+
document = self.store[ref.url]
307307
except KeyError:
308308
try:
309-
document = self.resolve_remote(url)
309+
document = self.resolve_remote(ref.url)
310310
except Exception as exc:
311311
raise RefResolutionError(exc)
312312

313-
uri = DefragResult(url, ref.fragment)
314-
old_base_uri, self.base_uri = self.base_uri, uri
315-
self.push_scope(uri, is_defragged=True)
316-
try:
317-
yield self.resolve_fragment(document, ref.fragment)
318-
finally:
319-
self.pop_scope()
320-
self.base_uri = old_base_uri
313+
return self.resolve_fragment(document, ref.fragment)
321314

322315
def resolve_fragment(self, document, fragment):
323316
"""

0 commit comments

Comments
 (0)