12
12
from jsonschema import _utils , _validators
13
13
from jsonschema .compat import (
14
14
Sequence , urljoin , urlsplit , urldefrag , unquote , urlopen ,
15
- str_types , int_types , iteritems ,
15
+ str_types , int_types , iteritems , lru_cache ,
16
16
)
17
17
from jsonschema .exceptions import ErrorTree # Backwards compatibility # noqa
18
18
from jsonschema .exceptions import RefResolutionError , SchemaError , UnknownType
@@ -79,7 +79,10 @@ def iter_errors(self, instance, _schema=None):
79
79
if _schema is None :
80
80
_schema = self .schema
81
81
82
- with self .resolver .in_scope (_schema .get (u"id" , u"" )):
82
+ scope = _schema .get (u"id" )
83
+ if scope :
84
+ self .resolver .push_scope (scope )
85
+ try :
83
86
ref = _schema .get (u"$ref" )
84
87
if ref is not None :
85
88
validators = [(u"$ref" , ref )]
@@ -103,6 +106,9 @@ def iter_errors(self, instance, _schema=None):
103
106
if k != u"$ref" :
104
107
error .schema_path .appendleft (k )
105
108
yield error
109
+ finally :
110
+ if scope :
111
+ self .resolver .pop_scope ()
106
112
107
113
def descend (self , instance , schema , path = None , schema_path = None ):
108
114
for error in self .iter_errors (instance , schema ):
@@ -227,26 +233,32 @@ class RefResolver(object):
227
233
first resolution
228
234
:argument dict handlers: a mapping from URI schemes to functions that
229
235
should be used to retrieve them
230
-
236
+ :arguments callable cache_func: a function decorator used to cache
237
+ expensive calls. Should support the `functools.lru_cache` interface.
238
+ :argument int cache_maxsize: number of items to store in the cache. Set
239
+ this to 0 to disable caching. Defaults to 1000.
231
240
"""
232
241
233
242
def __init__ (
234
243
self , base_uri , referrer , store = (), cache_remote = True , handlers = (),
244
+ cache_func = lru_cache , cache_maxsize = 1000 ,
235
245
):
236
- self .base_uri = base_uri
237
- self .resolution_scope = base_uri
238
246
# This attribute is not used, it is for backwards compatibility
239
247
self .referrer = referrer
240
248
self .cache_remote = cache_remote
241
249
self .handlers = dict (handlers )
242
250
251
+ self ._scopes_stack = [base_uri ]
243
252
self .store = _utils .URIDict (
244
253
(id , validator .META_SCHEMA )
245
254
for id , validator in iteritems (meta_schemas )
246
255
)
247
256
self .store .update (store )
248
257
self .store [base_uri ] = referrer
249
258
259
+ self ._urljoin_cache = cache_func (cache_maxsize )(urljoin )
260
+ self ._resolve_cache = cache_func (cache_maxsize )(self .resolve_from_url )
261
+
250
262
@classmethod
251
263
def from_schema (cls , schema , * args , ** kwargs ):
252
264
"""
@@ -259,44 +271,67 @@ def from_schema(cls, schema, *args, **kwargs):
259
271
260
272
return cls (schema .get (u"id" , u"" ), schema , * args , ** kwargs )
261
273
274
+ def push_scope (self , scope ):
275
+ self ._scopes_stack .append (
276
+ self ._urljoin_cache (self .resolution_scope , scope ))
277
+
278
+ def pop_scope (self ):
279
+ try :
280
+ self ._scopes_stack .pop ()
281
+ except IndexError :
282
+ raise RefResolutionError (
283
+ "Failed to pop the scope from an empty stack. "
284
+ "`pop_scope()` should only be called once for every "
285
+ "`push_scope()`" )
286
+
287
+ @property
288
+ def resolution_scope (self ):
289
+ return self ._scopes_stack [- 1 ]
290
+
291
+
292
+ # Deprecated, this function is no longer used, but is preserved for
293
+ # backwards compatibility
262
294
@contextlib .contextmanager
263
295
def in_scope (self , scope ):
264
- old_scope = self .resolution_scope
265
- self .resolution_scope = urljoin (old_scope , scope )
296
+ self .push_scope (scope )
266
297
try :
267
298
yield
268
299
finally :
269
- self .resolution_scope = old_scope
300
+ self .pop_scope ()
270
301
302
+ # Deprecated, this function is no longer used, but is preserved for
303
+ # backwards compatibility
271
304
@contextlib .contextmanager
272
305
def resolving (self , ref ):
306
+ url , resolved = self .resolve (ref )
307
+ self .push_scope (url )
308
+ try :
309
+ yield resolved
310
+ finally :
311
+ self .pop_scope ()
312
+
313
+ def resolve (self , ref ):
273
314
"""
274
315
Context manager which resolves a JSON ``ref`` and enters the
275
316
resolution scope of this ref.
276
317
277
318
:argument str ref: reference to resolve
278
319
279
320
"""
321
+ url = self ._urljoin_cache (self .resolution_scope , ref )
322
+ return url , self ._resolve_cache (url )
280
323
281
- full_uri = urljoin (self .resolution_scope , ref )
282
- uri , fragment = urldefrag (full_uri )
283
- if not uri :
284
- uri = self .base_uri
285
-
286
- if uri in self .store :
287
- document = self .store [uri ]
288
- else :
324
+ def resolve_from_url (self , url ):
325
+ url , fragment = urldefrag (url )
326
+ try :
327
+ document = self .store [url ]
328
+ except KeyError :
289
329
try :
290
- document = self .resolve_remote (uri )
330
+ document = self .resolve_remote (url )
291
331
except Exception as exc :
292
332
raise RefResolutionError (exc )
293
333
294
- old_base_uri , self .base_uri = self .base_uri , uri
295
- try :
296
- with self .in_scope (uri ):
297
- yield self .resolve_fragment (document , fragment )
298
- finally :
299
- self .base_uri = old_base_uri
334
+ return self .resolve_fragment (document , fragment )
300
335
301
336
def resolve_fragment (self , document , fragment ):
302
337
"""
0 commit comments