1212from jsonschema import _utils , _validators
1313from jsonschema .compat import (
1414 Sequence , urljoin , urlsplit , urldefrag , unquote , urlopen ,
15- str_types , int_types , iteritems ,
15+ str_types , int_types , iteritems , lru_cache ,
1616)
1717from jsonschema .exceptions import ErrorTree # Backwards compatibility # noqa
1818from jsonschema .exceptions import RefResolutionError , SchemaError , UnknownType
@@ -79,7 +79,10 @@ def iter_errors(self, instance, _schema=None):
7979 if _schema is None :
8080 _schema = self .schema
8181
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 :
8386 ref = _schema .get (u"$ref" )
8487 if ref is not None :
8588 validators = [(u"$ref" , ref )]
@@ -103,6 +106,9 @@ def iter_errors(self, instance, _schema=None):
103106 if k != u"$ref" :
104107 error .schema_path .appendleft (k )
105108 yield error
109+ finally :
110+ if scope :
111+ self .resolver .pop_scope ()
106112
107113 def descend (self , instance , schema , path = None , schema_path = None ):
108114 for error in self .iter_errors (instance , schema ):
@@ -227,26 +233,32 @@ class RefResolver(object):
227233 first resolution
228234 :argument dict handlers: a mapping from URI schemes to functions that
229235 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.
231240 """
232241
233242 def __init__ (
234243 self , base_uri , referrer , store = (), cache_remote = True , handlers = (),
244+ cache_func = lru_cache , cache_maxsize = 1000 ,
235245 ):
236- self .base_uri = base_uri
237- 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 = [base_uri ]
243252 self .store = _utils .URIDict (
244253 (id , validator .META_SCHEMA )
245254 for id , validator in iteritems (meta_schemas )
246255 )
247256 self .store .update (store )
248257 self .store [base_uri ] = referrer
249258
259+ self ._urljoin_cache = cache_func (cache_maxsize )(urljoin )
260+ self ._resolve_cache = cache_func (cache_maxsize )(self .resolve_from_url )
261+
250262 @classmethod
251263 def from_schema (cls , schema , * args , ** kwargs ):
252264 """
@@ -259,44 +271,67 @@ def from_schema(cls, schema, *args, **kwargs):
259271
260272 return cls (schema .get (u"id" , u"" ), schema , * args , ** kwargs )
261273
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
262294 @contextlib .contextmanager
263295 def in_scope (self , scope ):
264- old_scope = self .resolution_scope
265- self .resolution_scope = urljoin (old_scope , scope )
296+ self .push_scope (scope )
266297 try :
267298 yield
268299 finally :
269- self .resolution_scope = old_scope
300+ self .pop_scope ()
270301
302+ # Deprecated, this function is no longer used, but is preserved for
303+ # backwards compatibility
271304 @contextlib .contextmanager
272305 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 ):
273314 """
274315 Context manager which resolves a JSON ``ref`` and enters the
275316 resolution scope of this ref.
276317
277318 :argument str ref: reference to resolve
278319
279320 """
321+ url = self ._urljoin_cache (self .resolution_scope , ref )
322+ return url , self ._resolve_cache (url )
280323
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 :
289329 try :
290- document = self .resolve_remote (uri )
330+ document = self .resolve_remote (url )
291331 except Exception as exc :
292332 raise RefResolutionError (exc )
293333
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 )
300335
301336 def resolve_fragment (self , document , fragment ):
302337 """
0 commit comments