diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py index 16c9843..e23168d 100644 --- a/jsonschema/__init__.py +++ b/jsonschema/__init__.py @@ -19,8 +19,6 @@ from jsonschema.validators import ( Draft3Validator, Draft4Validator, RefResolver, validate ) - -__version__ = "2.5.0-dev" - +from jsonschema.version import __version__ # flake8: noqa diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index c13711c..ae7e2b5 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -38,22 +38,6 @@ class URIDict(MutableMapping): return repr(self.store) -class Cache(object): - """Cache the result of a function, using the arguments to the function as - the key. - """ - - def __init__(self, func): - self.func = func - self._cache = {} - - def __call__(self, *args): - if args in self._cache: - return self._cache[args] - self._cache[args] = value = self.func(*args) - return value - - class Unset(object): """ An as-of-yet unset attribute or unprovided default parameter. diff --git a/jsonschema/compat.py b/jsonschema/compat.py index b3156f9..9f52ded 100644 --- a/jsonschema/compat.py +++ b/jsonschema/compat.py @@ -13,6 +13,7 @@ PY3 = sys.version_info[0] >= 3 if PY3: zip = zip + from functools import lru_cache from io import StringIO from urllib.parse import ( unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit @@ -23,6 +24,7 @@ if PY3: iteritems = operator.methodcaller("items") else: from itertools import izip as zip # noqa + from repoze.lru import lru_cache from StringIO import StringIO from urlparse import ( urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index f3bb854..1f03294 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1,5 +1,4 @@ from collections import deque -from contextlib import contextmanager import json from jsonschema import FormatChecker, ValidationError @@ -870,6 +869,13 @@ class TestRefResolver(unittest.TestCase): pass self.assertEqual(str(err.exception), "Oh no! What's this?") + def test_helpful_error_message_on_failed_pop_scope(self): + resolver = RefResolver("", {}) + resolver.pop_scope() + with self.assertRaises(RefResolutionError) as exc: + resolver.pop_scope() + self.assertIn("Failed to pop the scope", str(exc.exception)) + def sorted_errors(errors): def key(error): diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 2343908..c84a3db 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -12,7 +12,7 @@ except ImportError: from jsonschema import _utils, _validators from jsonschema.compat import ( Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen, - str_types, int_types, iteritems, + str_types, int_types, iteritems, lru_cache, ) from jsonschema.exceptions import ErrorTree # Backwards compatibility # noqa from jsonschema.exceptions import RefResolutionError, SchemaError, UnknownType @@ -233,18 +233,22 @@ class RefResolver(object): first resolution :argument dict handlers: a mapping from URI schemes to functions that should be used to retrieve them - + :arguments callable cache_func: a function decorator used to cache + expensive calls. Should support the `functools.lru_cache` interface. + :argument int cache_maxsize: number of items to store in the cache. Set + this to 0 to disable caching. Defaults to 1000. """ def __init__( self, base_uri, referrer, store=(), cache_remote=True, handlers=(), + cache_func=lru_cache, cache_maxsize=1000, ): # This attribute is not used, it is for backwards compatibility self.referrer = referrer self.cache_remote = cache_remote self.handlers = dict(handlers) - self.scopes_stack = [base_uri] + self._scopes_stack = [base_uri] self.store = _utils.URIDict( (id, validator.META_SCHEMA) for id, validator in iteritems(meta_schemas) @@ -252,8 +256,8 @@ class RefResolver(object): self.store.update(store) self.store[base_uri] = referrer - self.urljoin_cache = _utils.Cache(urljoin) - self.resolve_cache = _utils.Cache(self.resolve_from_url) + self._urljoin_cache = cache_func(cache_maxsize)(urljoin) + self._resolve_cache = cache_func(cache_maxsize)(self.resolve_from_url) @classmethod def from_schema(cls, schema, *args, **kwargs): @@ -268,15 +272,21 @@ class RefResolver(object): return cls(schema.get(u"id", u""), schema, *args, **kwargs) def push_scope(self, scope): - self.scopes_stack.append( - self.urljoin_cache(self.resolution_scope, scope)) + self._scopes_stack.append( + self._urljoin_cache(self.resolution_scope, scope)) def pop_scope(self): - self.scopes_stack.pop() + try: + self._scopes_stack.pop() + except IndexError: + raise RefResolutionError( + "Failed to pop the scope from an empty stack. " + "`pop_scope()` should only be called once for every " + "`push_scope()`") @property def resolution_scope(self): - return self.scopes_stack[-1] + return self._scopes_stack[-1] # Deprecated, this function is no longer used, but is preserved for @@ -308,8 +318,8 @@ class RefResolver(object): :argument str ref: reference to resolve """ - url = self.urljoin_cache(self.resolution_scope, ref) - return url, self.resolve_cache(url) + url = self._urljoin_cache(self.resolution_scope, ref) + return url, self._resolve_cache(url) def resolve_from_url(self, url): url, fragment = urldefrag(url) diff --git a/jsonschema/version.py b/jsonschema/version.py new file mode 100644 index 0000000..9509105 --- /dev/null +++ b/jsonschema/version.py @@ -0,0 +1 @@ +__version__ = "2.5.0-dev" diff --git a/setup.py b/setup.py index 6b47714..ffd61f1 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,12 @@ +import os.path from setuptools import setup +import sys -from jsonschema import __version__ - +# Load __version__ info globals without importing anything +with open( + os.path.join(os.path.dirname(__file__), 'jsonschema', 'version.py') +) as fh: + exec(fh.read()) with open("README.rst") as readme: long_description = readme.read() @@ -21,6 +26,11 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] +install_requires = [] + +if sys.version_info < (3, 2): + install_requires.append('repoze.lru >= 0.6') + setup( name="jsonschema", version=__version__, @@ -34,4 +44,5 @@ setup( long_description=long_description, url="http://github.com/Julian/jsonschema", entry_points={"console_scripts": ["jsonschema = jsonschema.cli:main"]}, + install_requires=install_requires, )