diff options
23 files changed, 1979 insertions, 2200 deletions
diff --git a/lib/spack/external/__init__.py b/lib/spack/external/__init__.py index 833aa47b8d..c4c4855fe0 100644 --- a/lib/spack/external/__init__.py +++ b/lib/spack/external/__init__.py @@ -65,11 +65,8 @@ jsonschema * Homepage: https://pypi.python.org/pypi/jsonschema * Usage: An implementation of JSON Schema for Python. -* Version: 2.4.0 (last version before functools32 dependency was added) -* Note: functools32 doesn't support Python 2.6 or 3.0, so jsonschema - cannot be upgraded any further until we drop 2.6. - Also, jsonschema/validators.py has been modified NOT to try to import - requests (see 7a1dd517b8). +* Version: 3.2.0 (last version before 2.7 and 3.6 support was dropped) +* Note: We don't include tests or benchmarks; just what Spack needs. markupsafe ---------- diff --git a/lib/spack/external/jsonschema/README.rst b/lib/spack/external/jsonschema/README.rst deleted file mode 100644 index 20c2fe6266..0000000000 --- a/lib/spack/external/jsonschema/README.rst +++ /dev/null @@ -1,104 +0,0 @@ -========== -jsonschema -========== - -``jsonschema`` is an implementation of `JSON Schema <http://json-schema.org>`_ -for Python (supporting 2.6+ including Python 3). - -.. code-block:: python - - >>> from jsonschema import validate - - >>> # A sample schema, like what we'd get from json.load() - >>> schema = { - ... "type" : "object", - ... "properties" : { - ... "price" : {"type" : "number"}, - ... "name" : {"type" : "string"}, - ... }, - ... } - - >>> # If no exception is raised by validate(), the instance is valid. - >>> validate({"name" : "Eggs", "price" : 34.99}, schema) - - >>> validate( - ... {"name" : "Eggs", "price" : "Invalid"}, schema - ... ) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValidationError: 'Invalid' is not of type 'number' - - -Features --------- - -* Full support for - `Draft 3 <https://python-jsonschema.readthedocs.org/en/latest/validate/#jsonschema.Draft3Validator>`_ - **and** `Draft 4 <https://python-jsonschema.readthedocs.org/en/latest/validate/#jsonschema.Draft4Validator>`_ - of the schema. - -* `Lazy validation <https://python-jsonschema.readthedocs.org/en/latest/validate/#jsonschema.IValidator.iter_errors>`_ - that can iteratively report *all* validation errors. - -* Small and extensible - -* `Programmatic querying <https://python-jsonschema.readthedocs.org/en/latest/errors/#module-jsonschema>`_ - of which properties or items failed validation. - - -Release Notes -------------- - -* A simple CLI was added for validation -* Validation errors now keep full absolute paths and absolute schema paths in - their ``absolute_path`` and ``absolute_schema_path`` attributes. The ``path`` - and ``schema_path`` attributes are deprecated in favor of ``relative_path`` - and ``relative_schema_path``\ . - -*Note:* Support for Python 3.2 was dropped in this release, and installation -now uses setuptools. - - -Running the Test Suite ----------------------- - -``jsonschema`` uses the wonderful `Tox <http://tox.readthedocs.org>`_ for its -test suite. (It really is wonderful, if for some reason you haven't heard of -it, you really should use it for your projects). - -Assuming you have ``tox`` installed (perhaps via ``pip install tox`` or your -package manager), just run ``tox`` in the directory of your source checkout to -run ``jsonschema``'s test suite on all of the versions of Python ``jsonschema`` -supports. Note that you'll need to have all of those versions installed in -order to run the tests on each of them, otherwise ``tox`` will skip (and fail) -the tests on that version. - -Of course you're also free to just run the tests on a single version with your -favorite test runner. The tests live in the ``jsonschema.tests`` package. - - -Community ---------- - -There's a `mailing list <https://groups.google.com/forum/#!forum/jsonschema>`_ -for this implementation on Google Groups. - -Please join, and feel free to send questions there. - - -Contributing ------------- - -I'm Julian Berman. - -``jsonschema`` is on `GitHub <http://github.com/Julian/jsonschema>`_. - -Get in touch, via GitHub or otherwise, if you've got something to contribute, -it'd be most welcome! - -You can also generally find me on Freenode (nick: ``tos9``) in various -channels, including ``#python``. - -If you feel overwhelmingly grateful, you can woo me with beer money on -`Gittip <https://www.gittip.com/Julian/>`_ or via Google Wallet with the email -in my GitHub profile. diff --git a/lib/spack/external/jsonschema/__init__.py b/lib/spack/external/jsonschema/__init__.py index 6c099f1d8b..6dfdb9419a 100644 --- a/lib/spack/external/jsonschema/__init__.py +++ b/lib/spack/external/jsonschema/__init__.py @@ -4,23 +4,34 @@ An implementation of JSON Schema for Python The main functionality is provided by the validator classes for each of the supported JSON Schema versions. -Most commonly, :func:`validate` is the quickest way to simply validate a given +Most commonly, `validate` is the quickest way to simply validate a given instance under a schema, and will create a validator for you. - """ from jsonschema.exceptions import ( ErrorTree, FormatError, RefResolutionError, SchemaError, ValidationError ) from jsonschema._format import ( - FormatChecker, draft3_format_checker, draft4_format_checker, + FormatChecker, + draft3_format_checker, + draft4_format_checker, + draft6_format_checker, + draft7_format_checker, ) +from jsonschema._types import TypeChecker from jsonschema.validators import ( - Draft3Validator, Draft4Validator, RefResolver, validate + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + RefResolver, + validate, ) - - -__version__ = "2.4.0" - - -# flake8: noqa +# try: +# from importlib import metadata +# except ImportError: # for Python<3.8 +# import importlib_metadata as metadata +# __version__ = metadata.version("jsonschema") +# set the version manually here, as we don't install dist-info or egg-info +# files for vendored spack externals. +__version__ = '3.2.0' diff --git a/lib/spack/external/jsonschema/_format.py b/lib/spack/external/jsonschema/_format.py index bb52d183ad..281a7cfcff 100644 --- a/lib/spack/external/jsonschema/_format.py +++ b/lib/spack/external/jsonschema/_format.py @@ -1,6 +1,7 @@ import datetime import re import socket +import struct from jsonschema.compat import str_types from jsonschema.exceptions import FormatError @@ -14,17 +15,19 @@ class FormatChecker(object): validation. If validation is desired however, instances of this class can be hooked into validators to enable format validation. - :class:`FormatChecker` objects always return ``True`` when asked about + `FormatChecker` objects always return ``True`` when asked about formats that they do not know how to validate. To check a custom format using a function that takes an instance and - returns a ``bool``, use the :meth:`FormatChecker.checks` or - :meth:`FormatChecker.cls_checks` decorators. + returns a ``bool``, use the `FormatChecker.checks` or + `FormatChecker.cls_checks` decorators. - :argument iterable formats: the known formats to validate. This argument - can be used to limit which formats will be used - during validation. + Arguments: + formats (~collections.Iterable): + + The known formats to validate. This argument can be used to + limit which formats will be used during validation. """ checkers = {} @@ -35,16 +38,27 @@ class FormatChecker(object): else: self.checkers = dict((k, self.checkers[k]) for k in formats) + def __repr__(self): + return "<FormatChecker checkers={}>".format(sorted(self.checkers)) + def checks(self, format, raises=()): """ Register a decorated function as validating a new format. - :argument str format: the format that the decorated function will check - :argument Exception raises: the exception(s) raised by the decorated - function when an invalid instance is found. The exception object - will be accessible as the :attr:`ValidationError.cause` attribute - of the resulting validation error. + Arguments: + + format (str): + + The format that the decorated function will check. + + raises (Exception): + The exception(s) raised by the decorated function when an + invalid instance is found. + + The exception object will be accessible as the + `jsonschema.exceptions.ValidationError.cause` attribute of the + resulting validation error. """ def _checks(func): @@ -58,11 +72,20 @@ class FormatChecker(object): """ Check whether the instance conforms to the given format. - :argument instance: the instance to check - :type: any primitive type (str, number, bool) - :argument str format: the format that instance should conform to - :raises: :exc:`FormatError` if instance does not conform to format + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + + format (str): + The format that instance should conform to + + + Raises: + + FormatError: if the instance does not conform to ``format`` """ if format not in self.checkers: @@ -83,11 +106,19 @@ class FormatChecker(object): """ Check whether the instance conforms to the given format. - :argument instance: the instance to check - :type: any primitive type (str, number, bool) - :argument str format: the format that instance should conform to - :rtype: bool + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + format (str): + + The format that instance should conform to + + Returns: + + bool: whether it conformed """ try: @@ -98,25 +129,55 @@ class FormatChecker(object): return True -_draft_checkers = {"draft3": [], "draft4": []} +draft3_format_checker = FormatChecker() +draft4_format_checker = FormatChecker() +draft6_format_checker = FormatChecker() +draft7_format_checker = FormatChecker() + +_draft_checkers = dict( + draft3=draft3_format_checker, + draft4=draft4_format_checker, + draft6=draft6_format_checker, + draft7=draft7_format_checker, +) -def _checks_drafts(both=None, draft3=None, draft4=None, raises=()): - draft3 = draft3 or both - draft4 = draft4 or both + +def _checks_drafts( + name=None, + draft3=None, + draft4=None, + draft6=None, + draft7=None, + raises=(), +): + draft3 = draft3 or name + draft4 = draft4 or name + draft6 = draft6 or name + draft7 = draft7 or name def wrap(func): if draft3: - _draft_checkers["draft3"].append(draft3) - func = FormatChecker.cls_checks(draft3, raises)(func) + func = _draft_checkers["draft3"].checks(draft3, raises)(func) if draft4: - _draft_checkers["draft4"].append(draft4) - func = FormatChecker.cls_checks(draft4, raises)(func) + func = _draft_checkers["draft4"].checks(draft4, raises)(func) + if draft6: + func = _draft_checkers["draft6"].checks(draft6, raises)(func) + if draft7: + func = _draft_checkers["draft7"].checks(draft7, raises)(func) + + # Oy. This is bad global state, but relied upon for now, until + # deprecation. See https://github.com/Julian/jsonschema/issues/519 + # and test_format_checkers_come_with_defaults + FormatChecker.cls_checks(draft7 or draft6 or draft4 or draft3, raises)( + func, + ) return func return wrap -@_checks_drafts("email") +@_checks_drafts(name="idn-email") +@_checks_drafts(name="email") def is_email(instance): if not isinstance(instance, str_types): return True @@ -125,7 +186,10 @@ def is_email(instance): _ipv4_re = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") -@_checks_drafts(draft3="ip-address", draft4="ipv4") + +@_checks_drafts( + draft3="ip-address", draft4="ipv4", draft6="ipv4", draft7="ipv4", +) def is_ipv4(instance): if not isinstance(instance, str_types): return True @@ -135,7 +199,11 @@ def is_ipv4(instance): if hasattr(socket, "inet_pton"): - @_checks_drafts("ipv6", raises=socket.error) + # FIXME: Really this only should raise struct.error, but see the sadness + # that is https://twistedmatrix.com/trac/ticket/9409 + @_checks_drafts( + name="ipv6", raises=(socket.error, struct.error, ValueError), + ) def is_ipv6(instance): if not isinstance(instance, str_types): return True @@ -144,7 +212,13 @@ if hasattr(socket, "inet_pton"): _host_name_re = re.compile(r"^[A-Za-z0-9][A-Za-z0-9\.\-]{1,255}$") -@_checks_drafts(draft3="host-name", draft4="hostname") + +@_checks_drafts( + draft3="host-name", + draft4="hostname", + draft6="hostname", + draft7="hostname", +) def is_host_name(instance): if not isinstance(instance, str_types): return True @@ -158,46 +232,103 @@ def is_host_name(instance): try: - import rfc3987 + # The built-in `idna` codec only implements RFC 3890, so we go elsewhere. + import idna except ImportError: pass else: - @_checks_drafts("uri", raises=ValueError) - def is_uri(instance): + @_checks_drafts(draft7="idn-hostname", raises=idna.IDNAError) + def is_idn_host_name(instance): if not isinstance(instance, str_types): return True - return rfc3987.parse(instance, rule="URI") + idna.encode(instance) + return True try: - import strict_rfc3339 + import rfc3987 except ImportError: try: - import isodate + from rfc3986_validator import validate_rfc3986 except ImportError: pass else: - @_checks_drafts("date-time", raises=(ValueError, isodate.ISO8601Error)) - def is_date(instance): + @_checks_drafts(name="uri") + def is_uri(instance): if not isinstance(instance, str_types): return True - return isodate.parse_datetime(instance) -else: - @_checks_drafts("date-time") - def is_date(instance): + return validate_rfc3986(instance, rule="URI") + + @_checks_drafts( + draft6="uri-reference", + draft7="uri-reference", + raises=ValueError, + ) + def is_uri_reference(instance): if not isinstance(instance, str_types): return True - return strict_rfc3339.validate_rfc3339(instance) + return validate_rfc3986(instance, rule="URI_reference") + +else: + @_checks_drafts(draft7="iri", raises=ValueError) + def is_iri(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="IRI") + + @_checks_drafts(draft7="iri-reference", raises=ValueError) + def is_iri_reference(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="IRI_reference") + + @_checks_drafts(name="uri", raises=ValueError) + def is_uri(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="URI") + + @_checks_drafts( + draft6="uri-reference", + draft7="uri-reference", + raises=ValueError, + ) + def is_uri_reference(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="URI_reference") + + +try: + from strict_rfc3339 import validate_rfc3339 +except ImportError: + try: + from rfc3339_validator import validate_rfc3339 + except ImportError: + validate_rfc3339 = None + +if validate_rfc3339: + @_checks_drafts(name="date-time") + def is_datetime(instance): + if not isinstance(instance, str_types): + return True + return validate_rfc3339(instance) + + @_checks_drafts(draft7="time") + def is_time(instance): + if not isinstance(instance, str_types): + return True + return is_datetime("1970-01-01T" + instance) -@_checks_drafts("regex", raises=re.error) +@_checks_drafts(name="regex", raises=re.error) def is_regex(instance): if not isinstance(instance, str_types): return True return re.compile(instance) -@_checks_drafts(draft3="date", raises=ValueError) +@_checks_drafts(draft3="date", draft7="date", raises=ValueError) def is_date(instance): if not isinstance(instance, str_types): return True @@ -205,7 +336,7 @@ def is_date(instance): @_checks_drafts(draft3="time", raises=ValueError) -def is_time(instance): +def is_draft3_time(instance): if not isinstance(instance, str_types): return True return datetime.datetime.strptime(instance, "%H:%M:%S") @@ -219,7 +350,6 @@ else: def is_css_color_code(instance): return webcolors.normalize_hex(instance) - @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) def is_css21_color(instance): if ( @@ -229,12 +359,67 @@ else: return True return is_css_color_code(instance) - def is_css3_color(instance): if instance.lower() in webcolors.css3_names_to_hex: return True return is_css_color_code(instance) -draft3_format_checker = FormatChecker(_draft_checkers["draft3"]) -draft4_format_checker = FormatChecker(_draft_checkers["draft4"]) +try: + import jsonpointer +except ImportError: + pass +else: + @_checks_drafts( + draft6="json-pointer", + draft7="json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_json_pointer(instance): + if not isinstance(instance, str_types): + return True + return jsonpointer.JsonPointer(instance) + + # TODO: I don't want to maintain this, so it + # needs to go either into jsonpointer (pending + # https://github.com/stefankoegl/python-json-pointer/issues/34) or + # into a new external library. + @_checks_drafts( + draft7="relative-json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_relative_json_pointer(instance): + # Definition taken from: + # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3 + if not isinstance(instance, str_types): + return True + non_negative_integer, rest = [], "" + for i, character in enumerate(instance): + if character.isdigit(): + non_negative_integer.append(character) + continue + + if not non_negative_integer: + return False + + rest = instance[i:] + break + return (rest == "#") or jsonpointer.JsonPointer(rest) + + +try: + import uritemplate.exceptions +except ImportError: + pass +else: + @_checks_drafts( + draft6="uri-template", + draft7="uri-template", + raises=uritemplate.exceptions.InvalidTemplate, + ) + def is_uri_template( + instance, + template_validator=uritemplate.Validator().force_balanced_braces(), + ): + template = uritemplate.URITemplate(instance) + return template_validator.validate(template) diff --git a/lib/spack/external/jsonschema/_legacy_validators.py b/lib/spack/external/jsonschema/_legacy_validators.py new file mode 100644 index 0000000000..264ff7d713 --- /dev/null +++ b/lib/spack/external/jsonschema/_legacy_validators.py @@ -0,0 +1,141 @@ +from jsonschema import _utils +from jsonschema.compat import iteritems +from jsonschema.exceptions import ValidationError + + +def dependencies_draft3(validator, dependencies, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in iteritems(dependencies): + if property not in instance: + continue + + if validator.is_type(dependency, "object"): + for error in validator.descend( + instance, dependency, schema_path=property, + ): + yield error + elif validator.is_type(dependency, "string"): + if dependency not in instance: + yield ValidationError( + "%r is a dependency of %r" % (dependency, property) + ) + else: + for each in dependency: + if each not in instance: + message = "%r is a dependency of %r" + yield ValidationError(message % (each, property)) + + +def disallow_draft3(validator, disallow, instance, schema): + for disallowed in _utils.ensure_list(disallow): + if validator.is_valid(instance, {"type": [disallowed]}): + yield ValidationError( + "%r is disallowed for %r" % (disallowed, instance) + ) + + +def extends_draft3(validator, extends, instance, schema): + if validator.is_type(extends, "object"): + for error in validator.descend(instance, extends): + yield error + return + for index, subschema in enumerate(extends): + for error in validator.descend(instance, subschema, schema_path=index): + yield error + + +def items_draft3_draft4(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "object"): + for index, item in enumerate(instance): + for error in validator.descend(item, items, path=index): + yield error + else: + for (index, item), subschema in zip(enumerate(instance), items): + for error in validator.descend( + item, subschema, path=index, schema_path=index, + ): + yield error + + +def minimum_draft3_draft4(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMinimum", False): + failed = instance <= minimum + cmp = "less than or equal to" + else: + failed = instance < minimum + cmp = "less than" + + if failed: + yield ValidationError( + "%r is %s the minimum of %r" % (instance, cmp, minimum) + ) + + +def maximum_draft3_draft4(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMaximum", False): + failed = instance >= maximum + cmp = "greater than or equal to" + else: + failed = instance > maximum + cmp = "greater than" + + if failed: + yield ValidationError( + "%r is %s the maximum of %r" % (instance, cmp, maximum) + ) + + +def properties_draft3(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in iteritems(properties): + if property in instance: + for error in validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ): + yield error + elif subschema.get("required", False): + error = ValidationError("%r is a required property" % property) + error._set( + validator="required", + validator_value=subschema["required"], + instance=instance, + schema=schema, + ) + error.path.appendleft(property) + error.schema_path.extend([property, "required"]) + yield error + + +def type_draft3(validator, types, instance, schema): + types = _utils.ensure_list(types) + + all_errors = [] + for index, type in enumerate(types): + if validator.is_type(type, "object"): + errors = list(validator.descend(instance, type, schema_path=index)) + if not errors: + return + all_errors.extend(errors) + else: + if validator.is_type(instance, type): + return + else: + yield ValidationError( + _utils.types_msg(instance, types), context=all_errors, + ) diff --git a/lib/spack/external/jsonschema/_types.py b/lib/spack/external/jsonschema/_types.py new file mode 100644 index 0000000000..a71a4e34bd --- /dev/null +++ b/lib/spack/external/jsonschema/_types.py @@ -0,0 +1,188 @@ +import numbers + +from pyrsistent import pmap +import attr + +from jsonschema.compat import int_types, str_types +from jsonschema.exceptions import UndefinedTypeCheck + + +def is_array(checker, instance): + return isinstance(instance, list) + + +def is_bool(checker, instance): + return isinstance(instance, bool) + + +def is_integer(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, int_types) + + +def is_null(checker, instance): + return instance is None + + +def is_number(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, numbers.Number) + + +def is_object(checker, instance): + return isinstance(instance, dict) + + +def is_string(checker, instance): + return isinstance(instance, str_types) + + +def is_any(checker, instance): + return True + + +@attr.s(frozen=True) +class TypeChecker(object): + """ + A ``type`` property checker. + + A `TypeChecker` performs type checking for an `IValidator`. Type + checks to perform are updated using `TypeChecker.redefine` or + `TypeChecker.redefine_many` and removed via `TypeChecker.remove`. + Each of these return a new `TypeChecker` object. + + Arguments: + + type_checkers (dict): + + The initial mapping of types to their checking functions. + """ + _type_checkers = attr.ib(default=pmap(), converter=pmap) + + def is_type(self, instance, type): + """ + Check if the instance is of the appropriate type. + + Arguments: + + instance (object): + + The instance to check + + type (str): + + The name of the type that is expected. + + Returns: + + bool: Whether it conformed. + + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + if type is unknown to this object. + """ + try: + fn = self._type_checkers[type] + except KeyError: + raise UndefinedTypeCheck(type) + + return fn(self, instance) + + def redefine(self, type, fn): + """ + Produce a new checker with the given type redefined. + + Arguments: + + type (str): + + The name of the type to check. + + fn (collections.Callable): + + A function taking exactly two parameters - the type + checker calling the function and the instance to check. + The function should return true if instance is of this + type and false otherwise. + + Returns: + + A new `TypeChecker` instance. + """ + return self.redefine_many({type: fn}) + + def redefine_many(self, definitions=()): + """ + Produce a new checker with the given types redefined. + + Arguments: + + definitions (dict): + + A dictionary mapping types to their checking functions. + + Returns: + + A new `TypeChecker` instance. + """ + return attr.evolve( + self, type_checkers=self._type_checkers.update(definitions), + ) + + def remove(self, *types): + """ + Produce a new checker with the given types forgotten. + + Arguments: + + types (~collections.Iterable): + + the names of the types to remove. + + Returns: + + A new `TypeChecker` instance + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + + if any given type is unknown to this object + """ + + checkers = self._type_checkers + for each in types: + try: + checkers = checkers.remove(each) + except KeyError: + raise UndefinedTypeCheck(each) + return attr.evolve(self, type_checkers=checkers) + + +draft3_type_checker = TypeChecker( + { + u"any": is_any, + u"array": is_array, + u"boolean": is_bool, + u"integer": is_integer, + u"object": is_object, + u"null": is_null, + u"number": is_number, + u"string": is_string, + }, +) +draft4_type_checker = draft3_type_checker.remove(u"any") +draft6_type_checker = draft4_type_checker.redefine( + u"integer", + lambda checker, instance: ( + is_integer(checker, instance) or + isinstance(instance, float) and instance.is_integer() + ), +) +draft7_type_checker = draft6_type_checker diff --git a/lib/spack/external/jsonschema/_utils.py b/lib/spack/external/jsonschema/_utils.py index 2262f3305d..ceb880198d 100644 --- a/lib/spack/external/jsonschema/_utils.py +++ b/lib/spack/external/jsonschema/_utils.py @@ -3,13 +3,12 @@ import json import pkgutil import re -from jsonschema.compat import str_types, MutableMapping, urlsplit +from jsonschema.compat import MutableMapping, str_types, urlsplit class URIDict(MutableMapping): """ Dictionary which uses normalized URIs as keys. - """ def normalize(self, uri): @@ -41,7 +40,6 @@ class URIDict(MutableMapping): class Unset(object): """ An as-of-yet unset attribute or unprovided default parameter. - """ def __repr__(self): @@ -51,17 +49,15 @@ class Unset(object): def load_schema(name): """ Load a schema from ./schemas/``name``.json and return it. - """ - data = pkgutil.get_data(__package__, "schemas/{0}.json".format(name)) + data = pkgutil.get_data("jsonschema", "schemas/{0}.json".format(name)) return json.loads(data.decode("utf-8")) def indent(string, times=1): """ - A dumb version of :func:`textwrap.indent` from Python 3.3. - + A dumb version of `textwrap.indent` from Python 3.3. """ return "\n".join(" " * (4 * times) + line for line in string.splitlines()) @@ -73,8 +69,11 @@ def format_as_index(indices): For example, [1, 2, "foo"] -> [1][2]["foo"] - :type indices: sequence + Arguments: + indices (sequence): + + The indices to format. """ if not indices: @@ -90,7 +89,6 @@ def find_additional_properties(instance, schema): / or ``patternProperties``. Assumes ``instance`` is dict-like already. - """ properties = schema.get("properties", {}) @@ -105,7 +103,6 @@ def find_additional_properties(instance, schema): def extras_msg(extras): """ Create an error message for extra items or properties. - """ if len(extras) == 1: @@ -123,7 +120,6 @@ def types_msg(instance, types): be considered to be a description of that object and used as its type. Otherwise the message is simply the reprs of the given ``types``. - """ reprs = [] @@ -143,7 +139,6 @@ def flatten(suitable_for_isinstance): * an arbitrary nested tree of tuples Return a flattened tuple of the given argument. - """ types = set() @@ -163,7 +158,6 @@ def ensure_list(thing): Wrap ``thing`` in a list if it's a single str. Otherwise, return it unchanged. - """ if isinstance(thing, str_types): @@ -171,10 +165,16 @@ def ensure_list(thing): return thing +def equal(one, two): + """ + Check if two things are equal, but evade booleans and ints being equal. + """ + return unbool(one) == unbool(two) + + def unbool(element, true=object(), false=object()): """ A hack to make True and 1 and False and 0 unique for ``uniq``. - """ if element is True: @@ -191,7 +191,6 @@ def uniq(container): Successively tries first to rely that the elements are hashable, then falls back on them being sortable, and finally falls back on brute force. - """ try: diff --git a/lib/spack/external/jsonschema/_validators.py b/lib/spack/external/jsonschema/_validators.py index c6e801ccb2..179fec09a9 100644 --- a/lib/spack/external/jsonschema/_validators.py +++ b/lib/spack/external/jsonschema/_validators.py @@ -1,13 +1,18 @@ import re -from jsonschema import _utils +from jsonschema._utils import ( + ensure_list, + equal, + extras_msg, + find_additional_properties, + types_msg, + unbool, + uniq, +) from jsonschema.exceptions import FormatError, ValidationError from jsonschema.compat import iteritems -FLOAT_TOLERANCE = 10 ** -15 - - def patternProperties(validator, patternProperties, instance, schema): if not validator.is_type(instance, "object"): return @@ -21,35 +26,60 @@ def patternProperties(validator, patternProperties, instance, schema): yield error +def propertyNames(validator, propertyNames, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property in instance: + for error in validator.descend( + instance=property, + schema=propertyNames, + ): + yield error + + def additionalProperties(validator, aP, instance, schema): if not validator.is_type(instance, "object"): return - extras = set(_utils.find_additional_properties(instance, schema)) + extras = set(find_additional_properties(instance, schema)) if validator.is_type(aP, "object"): for extra in extras: for error in validator.descend(instance[extra], aP, path=extra): yield error elif not aP and extras: - error = "Additional properties are not allowed (%s %s unexpected)" - yield ValidationError(error % _utils.extras_msg(extras)) + if "patternProperties" in schema: + patterns = sorted(schema["patternProperties"]) + if len(extras) == 1: + verb = "does" + else: + verb = "do" + error = "%s %s not match any of the regexes: %s" % ( + ", ".join(map(repr, sorted(extras))), + verb, + ", ".join(map(repr, patterns)), + ) + yield ValidationError(error) + else: + error = "Additional properties are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(extras)) def items(validator, items, instance, schema): if not validator.is_type(instance, "array"): return - if validator.is_type(items, "object"): - for index, item in enumerate(instance): - for error in validator.descend(item, items, path=index): - yield error - else: + if validator.is_type(items, "array"): for (index, item), subschema in zip(enumerate(instance), items): for error in validator.descend( item, subschema, path=index, schema_path=index, ): yield error + else: + for index, item in enumerate(instance): + for error in validator.descend(item, items, path=index): + yield error def additionalItems(validator, aI, instance, schema): @@ -68,41 +98,66 @@ def additionalItems(validator, aI, instance, schema): error = "Additional items are not allowed (%s %s unexpected)" yield ValidationError( error % - _utils.extras_msg(instance[len(schema.get("items", [])):]) + extras_msg(instance[len(schema.get("items", [])):]) ) -def minimum(validator, minimum, instance, schema): +def const(validator, const, instance, schema): + if not equal(instance, const): + yield ValidationError("%r was expected" % (const,)) + + +def contains(validator, contains, instance, schema): + if not validator.is_type(instance, "array"): + return + + if not any(validator.is_valid(element, contains) for element in instance): + yield ValidationError( + "None of %r are valid under the given schema" % (instance,) + ) + + +def exclusiveMinimum(validator, minimum, instance, schema): if not validator.is_type(instance, "number"): return - if schema.get("exclusiveMinimum", False): - failed = float(instance) <= minimum - cmp = "less than or equal to" - else: - failed = float(instance) < minimum - cmp = "less than" + if instance <= minimum: + yield ValidationError( + "%r is less than or equal to the minimum of %r" % ( + instance, minimum, + ), + ) - if failed: + +def exclusiveMaximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance >= maximum: yield ValidationError( - "%r is %s the minimum of %r" % (instance, cmp, minimum) + "%r is greater than or equal to the maximum of %r" % ( + instance, maximum, + ), ) -def maximum(validator, maximum, instance, schema): +def minimum(validator, minimum, instance, schema): if not validator.is_type(instance, "number"): return - if schema.get("exclusiveMaximum", False): - failed = instance >= maximum - cmp = "greater than or equal to" - else: - failed = instance > maximum - cmp = "greater than" + if instance < minimum: + yield ValidationError( + "%r is less than the minimum of %r" % (instance, minimum) + ) - if failed: + +def maximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance > maximum: yield ValidationError( - "%r is %s the maximum of %r" % (instance, cmp, maximum) + "%r is greater than the maximum of %r" % (instance, maximum) ) @@ -111,8 +166,8 @@ def multipleOf(validator, dB, instance, schema): return if isinstance(dB, float): - mod = instance % dB - failed = (mod > FLOAT_TOLERANCE) and (dB - mod) > FLOAT_TOLERANCE + quotient = instance / dB + failed = int(quotient) != quotient else: failed = instance % dB @@ -134,9 +189,9 @@ def uniqueItems(validator, uI, instance, schema): if ( uI and validator.is_type(instance, "array") and - not _utils.uniq(instance) + not uniq(instance) ): - yield ValidationError("%r has non-unique elements" % instance) + yield ValidationError("%r has non-unique elements" % (instance,)) def pattern(validator, patrn, instance, schema): @@ -173,104 +228,52 @@ def dependencies(validator, dependencies, instance, schema): if property not in instance: continue - if validator.is_type(dependency, "object"): + if validator.is_type(dependency, "array"): + for each in dependency: + if each not in instance: + message = "%r is a dependency of %r" + yield ValidationError(message % (each, property)) + else: for error in validator.descend( instance, dependency, schema_path=property, ): yield error - else: - dependencies = _utils.ensure_list(dependency) - for dependency in dependencies: - if dependency not in instance: - yield ValidationError( - "%r is a dependency of %r" % (dependency, property) - ) def enum(validator, enums, instance, schema): - if instance not in enums: + if instance == 0 or instance == 1: + unbooled = unbool(instance) + if all(unbooled != unbool(each) for each in enums): + yield ValidationError("%r is not one of %r" % (instance, enums)) + elif instance not in enums: yield ValidationError("%r is not one of %r" % (instance, enums)) def ref(validator, ref, instance, schema): - with validator.resolver.resolving(ref) as resolved: - for error in validator.descend(instance, resolved): - yield error - - -def type_draft3(validator, types, instance, schema): - types = _utils.ensure_list(types) - - all_errors = [] - for index, type in enumerate(types): - if type == "any": - return - if validator.is_type(type, "object"): - errors = list(validator.descend(instance, type, schema_path=index)) - if not errors: - return - all_errors.extend(errors) - else: - if validator.is_type(instance, type): - return + resolve = getattr(validator.resolver, "resolve", None) + if resolve is None: + with validator.resolver.resolving(ref) as resolved: + for error in validator.descend(instance, resolved): + yield error else: - yield ValidationError( - _utils.types_msg(instance, types), context=all_errors, - ) - - -def properties_draft3(validator, properties, instance, schema): - if not validator.is_type(instance, "object"): - return + scope, resolved = validator.resolver.resolve(ref) + validator.resolver.push_scope(scope) - for property, subschema in iteritems(properties): - if property in instance: - for error in validator.descend( - instance[property], - subschema, - path=property, - schema_path=property, - ): + try: + for error in validator.descend(instance, resolved): yield error - elif subschema.get("required", False): - error = ValidationError("%r is a required property" % property) - error._set( - validator="required", - validator_value=subschema["required"], - instance=instance, - schema=schema, - ) - error.path.appendleft(property) - error.schema_path.extend([property, "required"]) - yield error - - -def disallow_draft3(validator, disallow, instance, schema): - for disallowed in _utils.ensure_list(disallow): - if validator.is_valid(instance, {"type" : [disallowed]}): - yield ValidationError( - "%r is disallowed for %r" % (disallowed, instance) - ) + finally: + validator.resolver.pop_scope() -def extends_draft3(validator, extends, instance, schema): - if validator.is_type(extends, "object"): - for error in validator.descend(instance, extends): - yield error - return - for index, subschema in enumerate(extends): - for error in validator.descend(instance, subschema, schema_path=index): - yield error - - -def type_draft4(validator, types, instance, schema): - types = _utils.ensure_list(types) +def type(validator, types, instance, schema): + types = ensure_list(types) if not any(validator.is_type(instance, type) for type in types): - yield ValidationError(_utils.types_msg(instance, types)) + yield ValidationError(types_msg(instance, types)) -def properties_draft4(validator, properties, instance, schema): +def properties(validator, properties, instance, schema): if not validator.is_type(instance, "object"): return @@ -285,7 +288,7 @@ def properties_draft4(validator, properties, instance, schema): yield error -def required_draft4(validator, required, instance, schema): +def required(validator, required, instance, schema): if not validator.is_type(instance, "object"): return for property in required: @@ -293,33 +296,31 @@ def required_draft4(validator, required, instance, schema): yield ValidationError("%r is a required property" % property) -def minProperties_draft4(validator, mP, instance, schema): +def minProperties(validator, mP, instance, schema): if validator.is_type(instance, "object") and len(instance) < mP: yield ValidationError( "%r does not have enough properties" % (instance,) ) -def maxProperties_draft4(validator, mP, instance, schema): +def maxProperties(validator, mP, instance, schema): if not validator.is_type(instance, "object"): return if validator.is_type(instance, "object") and len(instance) > mP: yield ValidationError("%r has too many properties" % (instance,)) -def allOf_draft4(validator, allOf, instance, schema): +def allOf(validator, allOf, instance, schema): for index, subschema in enumerate(allOf): for error in validator.descend(instance, subschema, schema_path=index): yield error -def oneOf_draft4(validator, oneOf, instance, schema): - subschemas = enumerate(oneOf) +def anyOf(validator, anyOf, instance, schema): all_errors = [] - for index, subschema in subschemas: + for index, subschema in enumerate(anyOf): errs = list(validator.descend(instance, subschema, schema_path=index)) if not errs: - first_valid = subschema break all_errors.extend(errs) else: @@ -328,20 +329,14 @@ def oneOf_draft4(validator, oneOf, instance, schema): context=all_errors, ) - more_valid = [s for i, s in subschemas if validator.is_valid(instance, s)] - if more_valid: - more_valid.append(first_valid) - reprs = ", ".join(repr(schema) for schema in more_valid) - yield ValidationError( - "%r is valid under each of %s" % (instance, reprs) - ) - -def anyOf_draft4(validator, anyOf, instance, schema): +def oneOf(validator, oneOf, instance, schema): + subschemas = enumerate(oneOf) all_errors = [] - for index, subschema in enumerate(anyOf): + for index, subschema in subschemas: errs = list(validator.descend(instance, subschema, schema_path=index)) if not errs: + first_valid = subschema break all_errors.extend(errs) else: @@ -350,9 +345,29 @@ def anyOf_draft4(validator, anyOf, instance, schema): context=all_errors, ) + more_valid = [s for i, s in subschemas if validator.is_valid(instance, s)] + if more_valid: + more_valid.append(first_valid) + reprs = ", ".join(repr(schema) for schema in more_valid) + yield ValidationError( + "%r is valid under each of %s" % (instance, reprs) + ) + -def not_draft4(validator, not_schema, instance, schema): +def not_(validator, not_schema, instance, schema): if validator.is_valid(instance, not_schema): yield ValidationError( "%r is not allowed for %r" % (not_schema, instance) ) + + +def if_(validator, if_schema, instance, schema): + if validator.is_valid(instance, if_schema): + if u"then" in schema: + then = schema[u"then"] + for error in validator.descend(instance, then, schema_path="then"): + yield error + elif u"else" in schema: + else_ = schema[u"else"] + for error in validator.descend(instance, else_, schema_path="else"): + yield error diff --git a/lib/spack/external/jsonschema/cli.py b/lib/spack/external/jsonschema/cli.py index 0126564f46..ab3335b27c 100644 --- a/lib/spack/external/jsonschema/cli.py +++ b/lib/spack/external/jsonschema/cli.py @@ -1,8 +1,12 @@ +""" +The ``jsonschema`` command line. +""" from __future__ import absolute_import import argparse import json import sys +from jsonschema import __version__ from jsonschema._reflect import namedAny from jsonschema.validators import validator_for @@ -26,26 +30,37 @@ parser.add_argument( action="append", dest="instances", type=_json_file, - help="a path to a JSON instance to validate " - "(may be specified multiple times)", + help=( + "a path to a JSON instance (i.e. filename.json) " + "to validate (may be specified multiple times)" + ), ) parser.add_argument( "-F", "--error-format", default="{error.instance}: {error.message}\n", - help="the format to use for each error output message, specified in " - "a form suitable for passing to str.format, which will be called " - "with 'error' for each error", + help=( + "the format to use for each error output message, specified in " + "a form suitable for passing to str.format, which will be called " + "with 'error' for each error" + ), ) parser.add_argument( "-V", "--validator", type=_namedAnyWithDefault, - help="the fully qualified object name of a validator to use, or, for " - "validators that are registered with jsonschema, simply the name " - "of the class.", + help=( + "the fully qualified object name of a validator to use, or, for " + "validators that are registered with jsonschema, simply the name " + "of the class." + ), +) +parser.add_argument( + "--version", + action="version", + version=__version__, ) parser.add_argument( "schema", - help="the JSON Schema to validate with", + help="the JSON Schema to validate with (i.e. schema.json)", type=_json_file, ) @@ -64,6 +79,9 @@ def main(args=sys.argv[1:]): def run(arguments, stdout=sys.stdout, stderr=sys.stderr): error_format = arguments["error_format"] validator = arguments["validator"](schema=arguments["schema"]) + + validator.check_schema(arguments["schema"]) + errored = False for instance in arguments["instances"] or (): for error in validator.iter_errors(instance): diff --git a/lib/spack/external/jsonschema/compat.py b/lib/spack/external/jsonschema/compat.py index 6ca49ab6be..47e0980455 100644 --- a/lib/spack/external/jsonschema/compat.py +++ b/lib/spack/external/jsonschema/compat.py @@ -1,52 +1,54 @@ -from __future__ import unicode_literals -import sys +""" +Python 2/3 compatibility helpers. + +Note: This module is *not* public API. +""" +import contextlib import operator +import sys + try: - from collections import MutableMapping, Sequence # noqa -except ImportError: from collections.abc import MutableMapping, Sequence # noqa +except ImportError: + from collections import MutableMapping, Sequence # noqa PY3 = sys.version_info[0] >= 3 if PY3: zip = zip - from io import StringIO + from functools import lru_cache + from io import StringIO as NativeIO from urllib.parse import ( - unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit + unquote, urljoin, urlunsplit, SplitResult, urlsplit ) - from urllib.request import urlopen + from urllib.request import pathname2url, urlopen str_types = str, int_types = int, iteritems = operator.methodcaller("items") else: from itertools import izip as zip # noqa - from StringIO import StringIO - from urlparse import ( - urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa - ) - from urllib import unquote # noqa - from urllib2 import urlopen # noqa + from io import BytesIO as NativeIO + from urlparse import urljoin, urlunsplit, SplitResult, urlsplit + from urllib import pathname2url, unquote # noqa + import urllib2 # noqa + def urlopen(*args, **kwargs): + return contextlib.closing(urllib2.urlopen(*args, **kwargs)) + str_types = basestring int_types = int, long iteritems = operator.methodcaller("iteritems") - -# On python < 3.3 fragments are not handled properly with unknown schemes -def urlsplit(url): - scheme, netloc, path, query, fragment = _urlsplit(url) - if "#" in path: - path, fragment = path.split("#", 1) - return SplitResult(scheme, netloc, path, query, fragment) + from functools32 import lru_cache def urldefrag(url): if "#" in url: s, n, p, q, frag = urlsplit(url) - defrag = urlunsplit((s, n, p, q, '')) + defrag = urlunsplit((s, n, p, q, "")) else: defrag = url - frag = '' + frag = "" return defrag, frag diff --git a/lib/spack/external/jsonschema/exceptions.py b/lib/spack/external/jsonschema/exceptions.py index 478e59c531..691dcffe6c 100644 --- a/lib/spack/external/jsonschema/exceptions.py +++ b/lib/spack/external/jsonschema/exceptions.py @@ -1,8 +1,13 @@ +""" +Validation errors, and some surrounding helpers. +""" from collections import defaultdict, deque import itertools import pprint import textwrap +import attr + from jsonschema import _utils from jsonschema.compat import PY3, iteritems @@ -27,6 +32,18 @@ class _Error(Exception): schema_path=(), parent=None, ): + super(_Error, self).__init__( + message, + validator, + path, + cause, + context, + validator_value, + instance, + schema, + schema_path, + parent, + ) self.message = message self.path = self.relative_path = deque(path) self.schema_path = self.relative_schema_path = deque(schema_path) @@ -44,9 +61,6 @@ class _Error(Exception): def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, self.message) - def __str__(self): - return unicode(self).encode("utf-8") - def __unicode__(self): essential_for_verbose = ( self.validator, self.validator_value, self.instance, self.schema, @@ -58,22 +72,27 @@ class _Error(Exception): pinstance = pprint.pformat(self.instance, width=72) return self.message + textwrap.dedent(""" - Failed validating %r in schema%s: + Failed validating %r in %s%s: %s - On instance%s: + On %s%s: %s """.rstrip() ) % ( self.validator, + self._word_for_schema_in_error_message, _utils.format_as_index(list(self.relative_schema_path)[:-1]), _utils.indent(pschema), + self._word_for_instance_in_error_message, _utils.format_as_index(self.relative_path), _utils.indent(pinstance), ) if PY3: __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode("utf-8") @classmethod def create_from(cls, other): @@ -86,7 +105,7 @@ class _Error(Exception): return self.relative_path path = deque(self.relative_path) - path.extendleft(parent.absolute_path) + path.extendleft(reversed(parent.absolute_path)) return path @property @@ -96,7 +115,7 @@ class _Error(Exception): return self.relative_schema_path path = deque(self.relative_schema_path) - path.extendleft(parent.absolute_schema_path) + path.extendleft(reversed(parent.absolute_schema_path)) return path def _set(self, **kwargs): @@ -113,26 +132,63 @@ class _Error(Exception): class ValidationError(_Error): - pass + """ + An instance was invalid under a provided schema. + """ + + _word_for_schema_in_error_message = "schema" + _word_for_instance_in_error_message = "instance" class SchemaError(_Error): - pass + """ + A schema was invalid under its corresponding metaschema. + """ + + _word_for_schema_in_error_message = "metaschema" + _word_for_instance_in_error_message = "schema" +@attr.s(hash=True) class RefResolutionError(Exception): - pass + """ + A ref could not be resolved. + """ + + _cause = attr.ib() + + def __str__(self): + return str(self._cause) + + +class UndefinedTypeCheck(Exception): + """ + A type checker was asked to check a type it did not have registered. + """ + + def __init__(self, type): + self.type = type + + def __unicode__(self): + return "Type %r is unknown to this type checker" % self.type + + if PY3: + __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode("utf-8") class UnknownType(Exception): + """ + A validator was asked to validate an instance against an unknown type. + """ + def __init__(self, type, instance, schema): self.type = type self.instance = instance self.schema = schema - def __str__(self): - return unicode(self).encode("utf-8") - def __unicode__(self): pschema = pprint.pformat(self.schema, width=72) pinstance = pprint.pformat(self.instance, width=72) @@ -147,29 +203,34 @@ class UnknownType(Exception): if PY3: __str__ = __unicode__ - + else: + def __str__(self): + return unicode(self).encode("utf-8") class FormatError(Exception): + """ + Validating a format failed. + """ + def __init__(self, message, cause=None): super(FormatError, self).__init__(message, cause) self.message = message self.cause = self.__cause__ = cause - def __str__(self): - return self.message.encode("utf-8") - def __unicode__(self): return self.message if PY3: __str__ = __unicode__ + else: + def __str__(self): + return self.message.encode("utf-8") class ErrorTree(object): """ ErrorTrees make it easier to check which validations failed. - """ _instance = _unset @@ -184,12 +245,11 @@ class ErrorTree(object): container = container[element] container.errors[error.validator] = error - self._instance = error.instance + container._instance = error.instance def __contains__(self, index): """ Check whether ``instance[index]`` has any errors. - """ return index in self._contents @@ -201,8 +261,7 @@ class ErrorTree(object): If the index is not in the instance that this tree corresponds to and is not known by this tree, whatever error would be raised by ``instance.__getitem__`` will be propagated (usually this is some - subclass of :class:`LookupError`. - + subclass of `exceptions.LookupError`. """ if self._instance is not _unset and index not in self: @@ -210,22 +269,22 @@ class ErrorTree(object): return self._contents[index] def __setitem__(self, index, value): + """ + Add an error to the tree at the given ``index``. + """ self._contents[index] = value def __iter__(self): """ Iterate (non-recursively) over the indices in the instance with errors. - """ return iter(self._contents) def __len__(self): """ - Same as :attr:`total_errors`. - + Return the `total_errors`. """ - return self.total_errors def __repr__(self): @@ -235,7 +294,6 @@ class ErrorTree(object): def total_errors(self): """ The total number of errors in the entire tree, including children. - """ child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) @@ -243,6 +301,21 @@ class ErrorTree(object): def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): + """ + Create a key function that can be used to sort errors by relevance. + + Arguments: + weak (set): + a collection of validator names to consider to be "weak". + If there are two errors at the same level of the instance + and one is in the set of weak validator names, the other + error will take priority. By default, :validator:`anyOf` and + :validator:`oneOf` are considered weak validators and will + be superseded by other same-level validation errors. + + strong (set): + a collection of validator names to consider to be "strong" + """ def relevance(error): validator = error.validator return -len(error.path), validator not in weak, validator in strong @@ -253,6 +326,43 @@ relevance = by_relevance() def best_match(errors, key=relevance): + """ + Try to find an error that appears to be the best match among given errors. + + In general, errors that are higher up in the instance (i.e. for which + `ValidationError.path` is shorter) are considered better matches, + since they indicate "more" is wrong with the instance. + + If the resulting match is either :validator:`oneOf` or :validator:`anyOf`, + the *opposite* assumption is made -- i.e. the deepest error is picked, + since these validators only need to match once, and any other errors may + not be relevant. + + Arguments: + errors (collections.Iterable): + + the errors to select from. Do not provide a mixture of + errors from different validation attempts (i.e. from + different instances or schemas), since it won't produce + sensical output. + + key (collections.Callable): + + the key to use when sorting errors. See `relevance` and + transitively `by_relevance` for more details (the default is + to sort with the defaults of that function). Changing the + default is only useful if you want to change the function + that rates errors but still want the error context descent + done by this function. + + Returns: + the best matching error, or ``None`` if the iterable was empty + + .. note:: + + This function is a heuristic. Its return value may change for a given + set of inputs from version to version if better heuristics are added. + """ errors = iter(errors) best = next(errors, None) if best is None: diff --git a/lib/spack/external/jsonschema/schemas/draft3.json b/lib/spack/external/jsonschema/schemas/draft3.json index 5bcefe30d5..f8a09c563b 100644 --- a/lib/spack/external/jsonschema/schemas/draft3.json +++ b/lib/spack/external/jsonschema/schemas/draft3.json @@ -80,9 +80,7 @@ "type": "number" }, "enum": { - "minItems": 1, - "type": "array", - "uniqueItems": true + "type": "array" }, "exclusiveMaximum": { "default": false, diff --git a/lib/spack/external/jsonschema/schemas/draft4.json b/lib/spack/external/jsonschema/schemas/draft4.json index fead5cefab..9b666cff88 100644 --- a/lib/spack/external/jsonschema/schemas/draft4.json +++ b/lib/spack/external/jsonschema/schemas/draft4.json @@ -111,9 +111,7 @@ "type": "string" }, "enum": { - "minItems": 1, - "type": "array", - "uniqueItems": true + "type": "array" }, "exclusiveMaximum": { "default": false, @@ -123,6 +121,9 @@ "default": false, "type": "boolean" }, + "format": { + "type": "string" + }, "id": { "format": "uri", "type": "string" diff --git a/lib/spack/external/jsonschema/schemas/draft6.json b/lib/spack/external/jsonschema/schemas/draft6.json new file mode 100644 index 0000000000..a0d2bf7896 --- /dev/null +++ b/lib/spack/external/jsonschema/schemas/draft6.json @@ -0,0 +1,153 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": {}, + "enum": { + "type": "array" + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/lib/spack/external/jsonschema/schemas/draft7.json b/lib/spack/external/jsonschema/schemas/draft7.json new file mode 100644 index 0000000000..746cde9690 --- /dev/null +++ b/lib/spack/external/jsonschema/schemas/draft7.json @@ -0,0 +1,166 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": {"$ref": "#"}, + "then": {"$ref": "#"}, + "else": {"$ref": "#"}, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/lib/spack/external/jsonschema/tests/__init__.py b/lib/spack/external/jsonschema/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/lib/spack/external/jsonschema/tests/__init__.py +++ /dev/null diff --git a/lib/spack/external/jsonschema/tests/compat.py b/lib/spack/external/jsonschema/tests/compat.py deleted file mode 100644 index b37483f5dd..0000000000 --- a/lib/spack/external/jsonschema/tests/compat.py +++ /dev/null @@ -1,15 +0,0 @@ -import sys - - -if sys.version_info[:2] < (2, 7): # pragma: no cover - import unittest2 as unittest -else: - import unittest - -try: - from unittest import mock -except ImportError: - import mock - - -# flake8: noqa diff --git a/lib/spack/external/jsonschema/tests/test_cli.py b/lib/spack/external/jsonschema/tests/test_cli.py deleted file mode 100644 index f625ca989d..0000000000 --- a/lib/spack/external/jsonschema/tests/test_cli.py +++ /dev/null @@ -1,110 +0,0 @@ -from jsonschema import Draft4Validator, ValidationError, cli -from jsonschema.compat import StringIO -from jsonschema.tests.compat import mock, unittest - - -def fake_validator(*errors): - errors = list(reversed(errors)) - - class FakeValidator(object): - def __init__(self, *args, **kwargs): - pass - - def iter_errors(self, instance): - if errors: - return errors.pop() - return [] - return FakeValidator - - -class TestParser(unittest.TestCase): - FakeValidator = fake_validator() - - def setUp(self): - mock_open = mock.mock_open() - patch_open = mock.patch.object(cli, "open", mock_open, create=True) - patch_open.start() - self.addCleanup(patch_open.stop) - - mock_json_load = mock.Mock() - mock_json_load.return_value = {} - patch_json_load = mock.patch("json.load") - patch_json_load.start() - self.addCleanup(patch_json_load.stop) - - def test_find_validator_by_fully_qualified_object_name(self): - arguments = cli.parse_args( - [ - "--validator", - "jsonschema.tests.test_cli.TestParser.FakeValidator", - "--instance", "foo.json", - "schema.json", - ] - ) - self.assertIs(arguments["validator"], self.FakeValidator) - - def test_find_validator_in_jsonschema(self): - arguments = cli.parse_args( - [ - "--validator", "Draft4Validator", - "--instance", "foo.json", - "schema.json", - ] - ) - self.assertIs(arguments["validator"], Draft4Validator) - - -class TestCLI(unittest.TestCase): - def test_successful_validation(self): - stdout, stderr = StringIO(), StringIO() - exit_code = cli.run( - { - "validator": fake_validator(), - "schema": {}, - "instances": [1], - "error_format": "{error.message}", - }, - stdout=stdout, - stderr=stderr, - ) - self.assertFalse(stdout.getvalue()) - self.assertFalse(stderr.getvalue()) - self.assertEqual(exit_code, 0) - - def test_unsuccessful_validation(self): - error = ValidationError("I am an error!", instance=1) - stdout, stderr = StringIO(), StringIO() - exit_code = cli.run( - { - "validator": fake_validator([error]), - "schema": {}, - "instances": [1], - "error_format": "{error.instance} - {error.message}", - }, - stdout=stdout, - stderr=stderr, - ) - self.assertFalse(stdout.getvalue()) - self.assertEqual(stderr.getvalue(), "1 - I am an error!") - self.assertEqual(exit_code, 1) - - def test_unsuccessful_validation_multiple_instances(self): - first_errors = [ - ValidationError("9", instance=1), - ValidationError("8", instance=1), - ] - second_errors = [ValidationError("7", instance=2)] - stdout, stderr = StringIO(), StringIO() - exit_code = cli.run( - { - "validator": fake_validator(first_errors, second_errors), - "schema": {}, - "instances": [1, 2], - "error_format": "{error.instance} - {error.message}\t", - }, - stdout=stdout, - stderr=stderr, - ) - self.assertFalse(stdout.getvalue()) - self.assertEqual(stderr.getvalue(), "1 - 9\t1 - 8\t2 - 7\t") - self.assertEqual(exit_code, 1) diff --git a/lib/spack/external/jsonschema/tests/test_exceptions.py b/lib/spack/external/jsonschema/tests/test_exceptions.py deleted file mode 100644 index 9e5793c628..0000000000 --- a/lib/spack/external/jsonschema/tests/test_exceptions.py +++ /dev/null @@ -1,382 +0,0 @@ -import textwrap - -from jsonschema import Draft4Validator, exceptions -from jsonschema.compat import PY3 -from jsonschema.tests.compat import mock, unittest - - -class TestBestMatch(unittest.TestCase): - def best_match(self, errors): - errors = list(errors) - best = exceptions.best_match(errors) - reversed_best = exceptions.best_match(reversed(errors)) - self.assertEqual( - best, - reversed_best, - msg="Didn't return a consistent best match!\n" - "Got: {0}\n\nThen: {1}".format(best, reversed_best), - ) - return best - - def test_shallower_errors_are_better_matches(self): - validator = Draft4Validator( - { - "properties" : { - "foo" : { - "minProperties" : 2, - "properties" : {"bar" : {"type" : "object"}}, - } - } - } - ) - best = self.best_match(validator.iter_errors({"foo" : {"bar" : []}})) - self.assertEqual(best.validator, "minProperties") - - def test_oneOf_and_anyOf_are_weak_matches(self): - """ - A property you *must* match is probably better than one you have to - match a part of. - - """ - - validator = Draft4Validator( - { - "minProperties" : 2, - "anyOf" : [{"type" : "string"}, {"type" : "number"}], - "oneOf" : [{"type" : "string"}, {"type" : "number"}], - } - ) - best = self.best_match(validator.iter_errors({})) - self.assertEqual(best.validator, "minProperties") - - def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self): - """ - If the most relevant error is an anyOf, then we traverse its context - and select the otherwise *least* relevant error, since in this case - that means the most specific, deep, error inside the instance. - - I.e. since only one of the schemas must match, we look for the most - relevant one. - - """ - - validator = Draft4Validator( - { - "properties" : { - "foo" : { - "anyOf" : [ - {"type" : "string"}, - {"properties" : {"bar" : {"type" : "array"}}}, - ], - }, - }, - }, - ) - best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) - self.assertEqual(best.validator_value, "array") - - def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self): - """ - If the most relevant error is an oneOf, then we traverse its context - and select the otherwise *least* relevant error, since in this case - that means the most specific, deep, error inside the instance. - - I.e. since only one of the schemas must match, we look for the most - relevant one. - - """ - - validator = Draft4Validator( - { - "properties" : { - "foo" : { - "oneOf" : [ - {"type" : "string"}, - {"properties" : {"bar" : {"type" : "array"}}}, - ], - }, - }, - }, - ) - best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) - self.assertEqual(best.validator_value, "array") - - def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self): - """ - Now, if the error is allOf, we traverse but select the *most* relevant - error from the context, because all schemas here must match anyways. - - """ - - validator = Draft4Validator( - { - "properties" : { - "foo" : { - "allOf" : [ - {"type" : "string"}, - {"properties" : {"bar" : {"type" : "array"}}}, - ], - }, - }, - }, - ) - best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) - self.assertEqual(best.validator_value, "string") - - def test_nested_context_for_oneOf(self): - validator = Draft4Validator( - { - "properties" : { - "foo" : { - "oneOf" : [ - {"type" : "string"}, - { - "oneOf" : [ - {"type" : "string"}, - { - "properties" : { - "bar" : {"type" : "array"} - }, - }, - ], - }, - ], - }, - }, - }, - ) - best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) - self.assertEqual(best.validator_value, "array") - - def test_one_error(self): - validator = Draft4Validator({"minProperties" : 2}) - error, = validator.iter_errors({}) - self.assertEqual( - exceptions.best_match(validator.iter_errors({})).validator, - "minProperties", - ) - - def test_no_errors(self): - validator = Draft4Validator({}) - self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) - - -class TestByRelevance(unittest.TestCase): - def test_short_paths_are_better_matches(self): - shallow = exceptions.ValidationError("Oh no!", path=["baz"]) - deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"]) - match = max([shallow, deep], key=exceptions.relevance) - self.assertIs(match, shallow) - - match = max([deep, shallow], key=exceptions.relevance) - self.assertIs(match, shallow) - - def test_global_errors_are_even_better_matches(self): - shallow = exceptions.ValidationError("Oh no!", path=[]) - deep = exceptions.ValidationError("Oh yes!", path=["foo"]) - - errors = sorted([shallow, deep], key=exceptions.relevance) - self.assertEqual( - [list(error.path) for error in errors], - [["foo"], []], - ) - - errors = sorted([deep, shallow], key=exceptions.relevance) - self.assertEqual( - [list(error.path) for error in errors], - [["foo"], []], - ) - - def test_weak_validators_are_lower_priority(self): - weak = exceptions.ValidationError("Oh no!", path=[], validator="a") - normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") - - best_match = exceptions.by_relevance(weak="a") - - match = max([weak, normal], key=best_match) - self.assertIs(match, normal) - - match = max([normal, weak], key=best_match) - self.assertIs(match, normal) - - def test_strong_validators_are_higher_priority(self): - weak = exceptions.ValidationError("Oh no!", path=[], validator="a") - normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") - strong = exceptions.ValidationError("Oh fine!", path=[], validator="c") - - best_match = exceptions.by_relevance(weak="a", strong="c") - - match = max([weak, normal, strong], key=best_match) - self.assertIs(match, strong) - - match = max([strong, normal, weak], key=best_match) - self.assertIs(match, strong) - - -class TestErrorTree(unittest.TestCase): - def test_it_knows_how_many_total_errors_it_contains(self): - errors = [mock.MagicMock() for _ in range(8)] - tree = exceptions.ErrorTree(errors) - self.assertEqual(tree.total_errors, 8) - - def test_it_contains_an_item_if_the_item_had_an_error(self): - errors = [exceptions.ValidationError("a message", path=["bar"])] - tree = exceptions.ErrorTree(errors) - self.assertIn("bar", tree) - - def test_it_does_not_contain_an_item_if_the_item_had_no_error(self): - errors = [exceptions.ValidationError("a message", path=["bar"])] - tree = exceptions.ErrorTree(errors) - self.assertNotIn("foo", tree) - - def test_validators_that_failed_appear_in_errors_dict(self): - error = exceptions.ValidationError("a message", validator="foo") - tree = exceptions.ErrorTree([error]) - self.assertEqual(tree.errors, {"foo" : error}) - - def test_it_creates_a_child_tree_for_each_nested_path(self): - errors = [ - exceptions.ValidationError("a bar message", path=["bar"]), - exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]), - ] - tree = exceptions.ErrorTree(errors) - self.assertIn(0, tree["bar"]) - self.assertNotIn(1, tree["bar"]) - - def test_children_have_their_errors_dicts_built(self): - e1, e2 = ( - exceptions.ValidationError("1", validator="foo", path=["bar", 0]), - exceptions.ValidationError("2", validator="quux", path=["bar", 0]), - ) - tree = exceptions.ErrorTree([e1, e2]) - self.assertEqual(tree["bar"][0].errors, {"foo" : e1, "quux" : e2}) - - def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self): - error = exceptions.ValidationError("123", validator="foo", instance=[]) - tree = exceptions.ErrorTree([error]) - - with self.assertRaises(IndexError): - tree[0] - - def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self): - """ - If a validator is dumb (like :validator:`required` in draft 3) and - refers to a path that isn't in the instance, the tree still properly - returns a subtree for that path. - - """ - - error = exceptions.ValidationError( - "a message", validator="foo", instance={}, path=["foo"], - ) - tree = exceptions.ErrorTree([error]) - self.assertIsInstance(tree["foo"], exceptions.ErrorTree) - - -class TestErrorReprStr(unittest.TestCase): - def make_error(self, **kwargs): - defaults = dict( - message=u"hello", - validator=u"type", - validator_value=u"string", - instance=5, - schema={u"type": u"string"}, - ) - defaults.update(kwargs) - return exceptions.ValidationError(**defaults) - - def assertShows(self, expected, **kwargs): - if PY3: - expected = expected.replace("u'", "'") - expected = textwrap.dedent(expected).rstrip("\n") - - error = self.make_error(**kwargs) - message_line, _, rest = str(error).partition("\n") - self.assertEqual(message_line, error.message) - self.assertEqual(rest, expected) - - def test_repr(self): - self.assertEqual( - repr(exceptions.ValidationError(message="Hello!")), - "<ValidationError: %r>" % "Hello!", - ) - - def test_unset_error(self): - error = exceptions.ValidationError("message") - self.assertEqual(str(error), "message") - - kwargs = { - "validator": "type", - "validator_value": "string", - "instance": 5, - "schema": {"type": "string"} - } - # Just the message should show if any of the attributes are unset - for attr in kwargs: - k = dict(kwargs) - del k[attr] - error = exceptions.ValidationError("message", **k) - self.assertEqual(str(error), "message") - - def test_empty_paths(self): - self.assertShows( - """ - Failed validating u'type' in schema: - {u'type': u'string'} - - On instance: - 5 - """, - path=[], - schema_path=[], - ) - - def test_one_item_paths(self): - self.assertShows( - """ - Failed validating u'type' in schema: - {u'type': u'string'} - - On instance[0]: - 5 - """, - path=[0], - schema_path=["items"], - ) - - def test_multiple_item_paths(self): - self.assertShows( - """ - Failed validating u'type' in schema[u'items'][0]: - {u'type': u'string'} - - On instance[0][u'a']: - 5 - """, - path=[0, u"a"], - schema_path=[u"items", 0, 1], - ) - - def test_uses_pprint(self): - with mock.patch("pprint.pformat") as pformat: - str(self.make_error()) - self.assertEqual(pformat.call_count, 2) # schema + instance - - def test_str_works_with_instances_having_overriden_eq_operator(self): - """ - Check for https://github.com/Julian/jsonschema/issues/164 which - rendered exceptions unusable when a `ValidationError` involved - instances with an `__eq__` method that returned truthy values. - - """ - - instance = mock.MagicMock() - error = exceptions.ValidationError( - "a message", - validator="foo", - instance=instance, - validator_value="some", - schema="schema", - ) - str(error) - self.assertFalse(instance.__eq__.called) diff --git a/lib/spack/external/jsonschema/tests/test_format.py b/lib/spack/external/jsonschema/tests/test_format.py deleted file mode 100644 index 8392ca1de3..0000000000 --- a/lib/spack/external/jsonschema/tests/test_format.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Tests for the parts of jsonschema related to the :validator:`format` property. - -""" - -from jsonschema.tests.compat import mock, unittest - -from jsonschema import FormatError, ValidationError, FormatChecker -from jsonschema.validators import Draft4Validator - - -class TestFormatChecker(unittest.TestCase): - def setUp(self): - self.fn = mock.Mock() - - def test_it_can_validate_no_formats(self): - checker = FormatChecker(formats=()) - self.assertFalse(checker.checkers) - - def test_it_raises_a_key_error_for_unknown_formats(self): - with self.assertRaises(KeyError): - FormatChecker(formats=["o noes"]) - - def test_it_can_register_cls_checkers(self): - with mock.patch.dict(FormatChecker.checkers, clear=True): - FormatChecker.cls_checks("new")(self.fn) - self.assertEqual(FormatChecker.checkers, {"new" : (self.fn, ())}) - - def test_it_can_register_checkers(self): - checker = FormatChecker() - checker.checks("new")(self.fn) - self.assertEqual( - checker.checkers, - dict(FormatChecker.checkers, new=(self.fn, ())) - ) - - def test_it_catches_registered_errors(self): - checker = FormatChecker() - cause = self.fn.side_effect = ValueError() - - checker.checks("foo", raises=ValueError)(self.fn) - - with self.assertRaises(FormatError) as cm: - checker.check("bar", "foo") - - self.assertIs(cm.exception.cause, cause) - self.assertIs(cm.exception.__cause__, cause) - - # Unregistered errors should not be caught - self.fn.side_effect = AttributeError - with self.assertRaises(AttributeError): - checker.check("bar", "foo") - - def test_format_error_causes_become_validation_error_causes(self): - checker = FormatChecker() - checker.checks("foo", raises=ValueError)(self.fn) - cause = self.fn.side_effect = ValueError() - validator = Draft4Validator({"format" : "foo"}, format_checker=checker) - - with self.assertRaises(ValidationError) as cm: - validator.validate("bar") - - self.assertIs(cm.exception.__cause__, cause) diff --git a/lib/spack/external/jsonschema/tests/test_jsonschema_test_suite.py b/lib/spack/external/jsonschema/tests/test_jsonschema_test_suite.py deleted file mode 100644 index 75c6857bc0..0000000000 --- a/lib/spack/external/jsonschema/tests/test_jsonschema_test_suite.py +++ /dev/null @@ -1,290 +0,0 @@ -""" -Test runner for the JSON Schema official test suite - -Tests comprehensive correctness of each draft's validator. - -See https://github.com/json-schema/JSON-Schema-Test-Suite for details. - -""" - -from contextlib import closing -from decimal import Decimal -import glob -import json -import io -import itertools -import os -import re -import subprocess -import sys - -try: - from sys import pypy_version_info -except ImportError: - pypy_version_info = None - -from jsonschema import ( - FormatError, SchemaError, ValidationError, Draft3Validator, - Draft4Validator, FormatChecker, draft3_format_checker, - draft4_format_checker, validate, -) -from jsonschema.compat import PY3 -from jsonschema.tests.compat import mock, unittest -import jsonschema - - -REPO_ROOT = os.path.join(os.path.dirname(jsonschema.__file__), os.path.pardir) -SUITE = os.getenv("JSON_SCHEMA_TEST_SUITE", os.path.join(REPO_ROOT, "json")) - -if not os.path.isdir(SUITE): - raise ValueError( - "Can't find the JSON-Schema-Test-Suite directory. Set the " - "'JSON_SCHEMA_TEST_SUITE' environment variable or run the tests from " - "alongside a checkout of the suite." - ) - -TESTS_DIR = os.path.join(SUITE, "tests") -JSONSCHEMA_SUITE = os.path.join(SUITE, "bin", "jsonschema_suite") - -remotes_stdout = subprocess.Popen( - ["python", JSONSCHEMA_SUITE, "remotes"], stdout=subprocess.PIPE, -).stdout - -with closing(remotes_stdout): - if PY3: - remotes_stdout = io.TextIOWrapper(remotes_stdout) - REMOTES = json.load(remotes_stdout) - - -def make_case(schema, data, valid, name): - if valid: - def test_case(self): - kwargs = getattr(self, "validator_kwargs", {}) - validate(data, schema, cls=self.validator_class, **kwargs) - else: - def test_case(self): - kwargs = getattr(self, "validator_kwargs", {}) - with self.assertRaises(ValidationError): - validate(data, schema, cls=self.validator_class, **kwargs) - - if not PY3: - name = name.encode("utf-8") - test_case.__name__ = name - - return test_case - - -def maybe_skip(skip, test_case, case, test): - if skip is not None: - reason = skip(case, test) - if reason is not None: - test_case = unittest.skip(reason)(test_case) - return test_case - - -def load_json_cases(tests_glob, ignore_glob="", basedir=TESTS_DIR, skip=None): - if ignore_glob: - ignore_glob = os.path.join(basedir, ignore_glob) - - def add_test_methods(test_class): - ignored = set(glob.iglob(ignore_glob)) - - for filename in glob.iglob(os.path.join(basedir, tests_glob)): - if filename in ignored: - continue - - validating, _ = os.path.splitext(os.path.basename(filename)) - id = itertools.count(1) - - with open(filename) as test_file: - for case in json.load(test_file): - for test in case["tests"]: - name = "test_%s_%s_%s" % ( - validating, - next(id), - re.sub(r"[\W ]+", "_", test["description"]), - ) - assert not hasattr(test_class, name), name - - test_case = make_case( - data=test["data"], - schema=case["schema"], - valid=test["valid"], - name=name, - ) - test_case = maybe_skip(skip, test_case, case, test) - setattr(test_class, name, test_case) - - return test_class - return add_test_methods - - -class TypesMixin(object): - @unittest.skipIf(PY3, "In Python 3 json.load always produces unicode") - def test_string_a_bytestring_is_a_string(self): - self.validator_class({"type" : "string"}).validate(b"foo") - - -class DecimalMixin(object): - def test_it_can_validate_with_decimals(self): - schema = {"type" : "number"} - validator = self.validator_class( - schema, types={"number" : (int, float, Decimal)} - ) - - for valid in [1, 1.1, Decimal(1) / Decimal(8)]: - validator.validate(valid) - - for invalid in ["foo", {}, [], True, None]: - with self.assertRaises(ValidationError): - validator.validate(invalid) - - -def missing_format(checker): - def missing_format(case, test): - format = case["schema"].get("format") - if format not in checker.checkers: - return "Format checker {0!r} not found.".format(format) - elif ( - format == "date-time" and - pypy_version_info is not None and - pypy_version_info[:2] <= (1, 9) - ): - # datetime.datetime is overzealous about typechecking in <=1.9 - return "datetime.datetime is broken on this version of PyPy." - return missing_format - - -class FormatMixin(object): - def test_it_returns_true_for_formats_it_does_not_know_about(self): - validator = self.validator_class( - {"format" : "carrot"}, format_checker=FormatChecker(), - ) - validator.validate("bugs") - - def test_it_does_not_validate_formats_by_default(self): - validator = self.validator_class({}) - self.assertIsNone(validator.format_checker) - - def test_it_validates_formats_if_a_checker_is_provided(self): - checker = mock.Mock(spec=FormatChecker) - validator = self.validator_class( - {"format" : "foo"}, format_checker=checker, - ) - - validator.validate("bar") - - checker.check.assert_called_once_with("bar", "foo") - - cause = ValueError() - checker.check.side_effect = FormatError('aoeu', cause=cause) - - with self.assertRaises(ValidationError) as cm: - validator.validate("bar") - # Make sure original cause is attached - self.assertIs(cm.exception.cause, cause) - - def test_it_validates_formats_of_any_type(self): - checker = mock.Mock(spec=FormatChecker) - validator = self.validator_class( - {"format" : "foo"}, format_checker=checker, - ) - - validator.validate([1, 2, 3]) - - checker.check.assert_called_once_with([1, 2, 3], "foo") - - cause = ValueError() - checker.check.side_effect = FormatError('aoeu', cause=cause) - - with self.assertRaises(ValidationError) as cm: - validator.validate([1, 2, 3]) - # Make sure original cause is attached - self.assertIs(cm.exception.cause, cause) - - -if sys.maxunicode == 2 ** 16 - 1: # This is a narrow build. - def narrow_unicode_build(case, test): - if "supplementary Unicode" in test["description"]: - return "Not running surrogate Unicode case, this Python is narrow." -else: - def narrow_unicode_build(case, test): # This isn't, skip nothing. - return - - -@load_json_cases( - "draft3/*.json", - skip=narrow_unicode_build, - ignore_glob="draft3/refRemote.json", -) -@load_json_cases( - "draft3/optional/format.json", skip=missing_format(draft3_format_checker) -) -@load_json_cases("draft3/optional/bignum.json") -@load_json_cases("draft3/optional/zeroTerminatedFloats.json") -class TestDraft3(unittest.TestCase, TypesMixin, DecimalMixin, FormatMixin): - validator_class = Draft3Validator - validator_kwargs = {"format_checker" : draft3_format_checker} - - def test_any_type_is_valid_for_type_any(self): - validator = self.validator_class({"type" : "any"}) - validator.validate(mock.Mock()) - - # TODO: we're in need of more meta schema tests - def test_invalid_properties(self): - with self.assertRaises(SchemaError): - validate({}, {"properties": {"test": True}}, - cls=self.validator_class) - - def test_minItems_invalid_string(self): - with self.assertRaises(SchemaError): - # needs to be an integer - validate([1], {"minItems" : "1"}, cls=self.validator_class) - - -@load_json_cases( - "draft4/*.json", - skip=narrow_unicode_build, - ignore_glob="draft4/refRemote.json", -) -@load_json_cases( - "draft4/optional/format.json", skip=missing_format(draft4_format_checker) -) -@load_json_cases("draft4/optional/bignum.json") -@load_json_cases("draft4/optional/zeroTerminatedFloats.json") -class TestDraft4(unittest.TestCase, TypesMixin, DecimalMixin, FormatMixin): - validator_class = Draft4Validator - validator_kwargs = {"format_checker" : draft4_format_checker} - - # TODO: we're in need of more meta schema tests - def test_invalid_properties(self): - with self.assertRaises(SchemaError): - validate({}, {"properties": {"test": True}}, - cls=self.validator_class) - - def test_minItems_invalid_string(self): - with self.assertRaises(SchemaError): - # needs to be an integer - validate([1], {"minItems" : "1"}, cls=self.validator_class) - - -class RemoteRefResolutionMixin(object): - def setUp(self): - patch = mock.patch("jsonschema.validators.requests") - requests = patch.start() - requests.get.side_effect = self.resolve - self.addCleanup(patch.stop) - - def resolve(self, reference): - _, _, reference = reference.partition("http://localhost:1234/") - return mock.Mock(**{"json.return_value" : REMOTES.get(reference)}) - - -@load_json_cases("draft3/refRemote.json") -class Draft3RemoteResolution(RemoteRefResolutionMixin, unittest.TestCase): - validator_class = Draft3Validator - - -@load_json_cases("draft4/refRemote.json") -class Draft4RemoteResolution(RemoteRefResolutionMixin, unittest.TestCase): - validator_class = Draft4Validator diff --git a/lib/spack/external/jsonschema/tests/test_validators.py b/lib/spack/external/jsonschema/tests/test_validators.py deleted file mode 100644 index f8692388ea..0000000000 --- a/lib/spack/external/jsonschema/tests/test_validators.py +++ /dev/null @@ -1,786 +0,0 @@ -from collections import deque -from contextlib import contextmanager -import json - -from jsonschema import FormatChecker, ValidationError -from jsonschema.tests.compat import mock, unittest -from jsonschema.validators import ( - RefResolutionError, UnknownType, Draft3Validator, - Draft4Validator, RefResolver, create, extend, validator_for, validate, -) - - -class TestCreateAndExtend(unittest.TestCase): - def setUp(self): - self.meta_schema = {u"properties" : {u"smelly" : {}}} - self.smelly = mock.MagicMock() - self.validators = {u"smelly" : self.smelly} - self.types = {u"dict" : dict} - self.Validator = create( - meta_schema=self.meta_schema, - validators=self.validators, - default_types=self.types, - ) - - self.validator_value = 12 - self.schema = {u"smelly" : self.validator_value} - self.validator = self.Validator(self.schema) - - def test_attrs(self): - self.assertEqual(self.Validator.VALIDATORS, self.validators) - self.assertEqual(self.Validator.META_SCHEMA, self.meta_schema) - self.assertEqual(self.Validator.DEFAULT_TYPES, self.types) - - def test_init(self): - self.assertEqual(self.validator.schema, self.schema) - - def test_iter_errors(self): - instance = "hello" - - self.smelly.return_value = [] - self.assertEqual(list(self.validator.iter_errors(instance)), []) - - error = mock.Mock() - self.smelly.return_value = [error] - self.assertEqual(list(self.validator.iter_errors(instance)), [error]) - - self.smelly.assert_called_with( - self.validator, self.validator_value, instance, self.schema, - ) - - def test_if_a_version_is_provided_it_is_registered(self): - with mock.patch("jsonschema.validators.validates") as validates: - validates.side_effect = lambda version : lambda cls : cls - Validator = create(meta_schema={u"id" : ""}, version="my version") - validates.assert_called_once_with("my version") - self.assertEqual(Validator.__name__, "MyVersionValidator") - - def test_if_a_version_is_not_provided_it_is_not_registered(self): - with mock.patch("jsonschema.validators.validates") as validates: - create(meta_schema={u"id" : "id"}) - self.assertFalse(validates.called) - - def test_extend(self): - validators = dict(self.Validator.VALIDATORS) - new = mock.Mock() - - Extended = extend(self.Validator, validators={u"a new one" : new}) - - validators.update([(u"a new one", new)]) - self.assertEqual(Extended.VALIDATORS, validators) - self.assertNotIn(u"a new one", self.Validator.VALIDATORS) - - self.assertEqual(Extended.META_SCHEMA, self.Validator.META_SCHEMA) - self.assertEqual(Extended.DEFAULT_TYPES, self.Validator.DEFAULT_TYPES) - - -class TestIterErrors(unittest.TestCase): - def setUp(self): - self.validator = Draft3Validator({}) - - def test_iter_errors(self): - instance = [1, 2] - schema = { - u"disallow" : u"array", - u"enum" : [["a", "b", "c"], ["d", "e", "f"]], - u"minItems" : 3 - } - - got = (e.message for e in self.validator.iter_errors(instance, schema)) - expected = [ - "%r is disallowed for [1, 2]" % (schema["disallow"],), - "[1, 2] is too short", - "[1, 2] is not one of %r" % (schema["enum"],), - ] - self.assertEqual(sorted(got), sorted(expected)) - - def test_iter_errors_multiple_failures_one_validator(self): - instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"} - schema = { - u"properties" : { - "foo" : {u"type" : "string"}, - "bar" : {u"minItems" : 2}, - "baz" : {u"maximum" : 10, u"enum" : [2, 4, 6, 8]}, - } - } - - errors = list(self.validator.iter_errors(instance, schema)) - self.assertEqual(len(errors), 4) - - -class TestValidationErrorMessages(unittest.TestCase): - def message_for(self, instance, schema, *args, **kwargs): - kwargs.setdefault("cls", Draft3Validator) - with self.assertRaises(ValidationError) as e: - validate(instance, schema, *args, **kwargs) - return e.exception.message - - def test_single_type_failure(self): - message = self.message_for(instance=1, schema={u"type" : u"string"}) - self.assertEqual(message, "1 is not of type %r" % u"string") - - def test_single_type_list_failure(self): - message = self.message_for(instance=1, schema={u"type" : [u"string"]}) - self.assertEqual(message, "1 is not of type %r" % u"string") - - def test_multiple_type_failure(self): - types = u"string", u"object" - message = self.message_for(instance=1, schema={u"type" : list(types)}) - self.assertEqual(message, "1 is not of type %r, %r" % types) - - def test_object_without_title_type_failure(self): - type = {u"type" : [{u"minimum" : 3}]} - message = self.message_for(instance=1, schema={u"type" : [type]}) - self.assertEqual(message, "1 is not of type %r" % (type,)) - - def test_object_with_name_type_failure(self): - name = "Foo" - schema = {u"type" : [{u"name" : name, u"minimum" : 3}]} - message = self.message_for(instance=1, schema=schema) - self.assertEqual(message, "1 is not of type %r" % (name,)) - - def test_minimum(self): - message = self.message_for(instance=1, schema={"minimum" : 2}) - self.assertEqual(message, "1 is less than the minimum of 2") - - def test_maximum(self): - message = self.message_for(instance=1, schema={"maximum" : 0}) - self.assertEqual(message, "1 is greater than the maximum of 0") - - def test_dependencies_failure_has_single_element_not_list(self): - depend, on = "bar", "foo" - schema = {u"dependencies" : {depend : on}} - message = self.message_for({"bar" : 2}, schema) - self.assertEqual(message, "%r is a dependency of %r" % (on, depend)) - - def test_additionalItems_single_failure(self): - message = self.message_for( - [2], {u"items" : [], u"additionalItems" : False}, - ) - self.assertIn("(2 was unexpected)", message) - - def test_additionalItems_multiple_failures(self): - message = self.message_for( - [1, 2, 3], {u"items" : [], u"additionalItems" : False} - ) - self.assertIn("(1, 2, 3 were unexpected)", message) - - def test_additionalProperties_single_failure(self): - additional = "foo" - schema = {u"additionalProperties" : False} - message = self.message_for({additional : 2}, schema) - self.assertIn("(%r was unexpected)" % (additional,), message) - - def test_additionalProperties_multiple_failures(self): - schema = {u"additionalProperties" : False} - message = self.message_for(dict.fromkeys(["foo", "bar"]), schema) - - self.assertIn(repr("foo"), message) - self.assertIn(repr("bar"), message) - self.assertIn("were unexpected)", message) - - def test_invalid_format_default_message(self): - checker = FormatChecker(formats=()) - check_fn = mock.Mock(return_value=False) - checker.checks(u"thing")(check_fn) - - schema = {u"format" : u"thing"} - message = self.message_for("bla", schema, format_checker=checker) - - self.assertIn(repr("bla"), message) - self.assertIn(repr("thing"), message) - self.assertIn("is not a", message) - - -class TestValidationErrorDetails(unittest.TestCase): - # TODO: These really need unit tests for each individual validator, rather - # than just these higher level tests. - def test_anyOf(self): - instance = 5 - schema = { - "anyOf": [ - {"minimum": 20}, - {"type": "string"} - ] - } - - validator = Draft4Validator(schema) - errors = list(validator.iter_errors(instance)) - self.assertEqual(len(errors), 1) - e = errors[0] - - self.assertEqual(e.validator, "anyOf") - self.assertEqual(e.validator_value, schema["anyOf"]) - self.assertEqual(e.instance, instance) - self.assertEqual(e.schema, schema) - self.assertIsNone(e.parent) - - self.assertEqual(e.path, deque([])) - self.assertEqual(e.relative_path, deque([])) - self.assertEqual(e.absolute_path, deque([])) - - self.assertEqual(e.schema_path, deque(["anyOf"])) - self.assertEqual(e.relative_schema_path, deque(["anyOf"])) - self.assertEqual(e.absolute_schema_path, deque(["anyOf"])) - - self.assertEqual(len(e.context), 2) - - e1, e2 = sorted_errors(e.context) - - self.assertEqual(e1.validator, "minimum") - self.assertEqual(e1.validator_value, schema["anyOf"][0]["minimum"]) - self.assertEqual(e1.instance, instance) - self.assertEqual(e1.schema, schema["anyOf"][0]) - self.assertIs(e1.parent, e) - - self.assertEqual(e1.path, deque([])) - self.assertEqual(e1.absolute_path, deque([])) - self.assertEqual(e1.relative_path, deque([])) - - self.assertEqual(e1.schema_path, deque([0, "minimum"])) - self.assertEqual(e1.relative_schema_path, deque([0, "minimum"])) - self.assertEqual( - e1.absolute_schema_path, deque(["anyOf", 0, "minimum"]), - ) - - self.assertFalse(e1.context) - - self.assertEqual(e2.validator, "type") - self.assertEqual(e2.validator_value, schema["anyOf"][1]["type"]) - self.assertEqual(e2.instance, instance) - self.assertEqual(e2.schema, schema["anyOf"][1]) - self.assertIs(e2.parent, e) - - self.assertEqual(e2.path, deque([])) - self.assertEqual(e2.relative_path, deque([])) - self.assertEqual(e2.absolute_path, deque([])) - - self.assertEqual(e2.schema_path, deque([1, "type"])) - self.assertEqual(e2.relative_schema_path, deque([1, "type"])) - self.assertEqual(e2.absolute_schema_path, deque(["anyOf", 1, "type"])) - - self.assertEqual(len(e2.context), 0) - - def test_type(self): - instance = {"foo": 1} - schema = { - "type": [ - {"type": "integer"}, - { - "type": "object", - "properties": { - "foo": {"enum": [2]} - } - } - ] - } - - validator = Draft3Validator(schema) - errors = list(validator.iter_errors(instance)) - self.assertEqual(len(errors), 1) - e = errors[0] - - self.assertEqual(e.validator, "type") - self.assertEqual(e.validator_value, schema["type"]) - self.assertEqual(e.instance, instance) - self.assertEqual(e.schema, schema) - self.assertIsNone(e.parent) - - self.assertEqual(e.path, deque([])) - self.assertEqual(e.relative_path, deque([])) - self.assertEqual(e.absolute_path, deque([])) - - self.assertEqual(e.schema_path, deque(["type"])) - self.assertEqual(e.relative_schema_path, deque(["type"])) - self.assertEqual(e.absolute_schema_path, deque(["type"])) - - self.assertEqual(len(e.context), 2) - - e1, e2 = sorted_errors(e.context) - - self.assertEqual(e1.validator, "type") - self.assertEqual(e1.validator_value, schema["type"][0]["type"]) - self.assertEqual(e1.instance, instance) - self.assertEqual(e1.schema, schema["type"][0]) - self.assertIs(e1.parent, e) - - self.assertEqual(e1.path, deque([])) - self.assertEqual(e1.relative_path, deque([])) - self.assertEqual(e1.absolute_path, deque([])) - - self.assertEqual(e1.schema_path, deque([0, "type"])) - self.assertEqual(e1.relative_schema_path, deque([0, "type"])) - self.assertEqual(e1.absolute_schema_path, deque(["type", 0, "type"])) - - self.assertFalse(e1.context) - - self.assertEqual(e2.validator, "enum") - self.assertEqual(e2.validator_value, [2]) - self.assertEqual(e2.instance, 1) - self.assertEqual(e2.schema, {u"enum" : [2]}) - self.assertIs(e2.parent, e) - - self.assertEqual(e2.path, deque(["foo"])) - self.assertEqual(e2.relative_path, deque(["foo"])) - self.assertEqual(e2.absolute_path, deque(["foo"])) - - self.assertEqual( - e2.schema_path, deque([1, "properties", "foo", "enum"]), - ) - self.assertEqual( - e2.relative_schema_path, deque([1, "properties", "foo", "enum"]), - ) - self.assertEqual( - e2.absolute_schema_path, - deque(["type", 1, "properties", "foo", "enum"]), - ) - - self.assertFalse(e2.context) - - def test_single_nesting(self): - instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"} - schema = { - "properties" : { - "foo" : {"type" : "string"}, - "bar" : {"minItems" : 2}, - "baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]}, - } - } - - validator = Draft3Validator(schema) - errors = validator.iter_errors(instance) - e1, e2, e3, e4 = sorted_errors(errors) - - self.assertEqual(e1.path, deque(["bar"])) - self.assertEqual(e2.path, deque(["baz"])) - self.assertEqual(e3.path, deque(["baz"])) - self.assertEqual(e4.path, deque(["foo"])) - - self.assertEqual(e1.relative_path, deque(["bar"])) - self.assertEqual(e2.relative_path, deque(["baz"])) - self.assertEqual(e3.relative_path, deque(["baz"])) - self.assertEqual(e4.relative_path, deque(["foo"])) - - self.assertEqual(e1.absolute_path, deque(["bar"])) - self.assertEqual(e2.absolute_path, deque(["baz"])) - self.assertEqual(e3.absolute_path, deque(["baz"])) - self.assertEqual(e4.absolute_path, deque(["foo"])) - - self.assertEqual(e1.validator, "minItems") - self.assertEqual(e2.validator, "enum") - self.assertEqual(e3.validator, "maximum") - self.assertEqual(e4.validator, "type") - - def test_multiple_nesting(self): - instance = [1, {"foo" : 2, "bar" : {"baz" : [1]}}, "quux"] - schema = { - "type" : "string", - "items" : { - "type" : ["string", "object"], - "properties" : { - "foo" : {"enum" : [1, 3]}, - "bar" : { - "type" : "array", - "properties" : { - "bar" : {"required" : True}, - "baz" : {"minItems" : 2}, - } - } - } - } - } - - validator = Draft3Validator(schema) - errors = validator.iter_errors(instance) - e1, e2, e3, e4, e5, e6 = sorted_errors(errors) - - self.assertEqual(e1.path, deque([])) - self.assertEqual(e2.path, deque([0])) - self.assertEqual(e3.path, deque([1, "bar"])) - self.assertEqual(e4.path, deque([1, "bar", "bar"])) - self.assertEqual(e5.path, deque([1, "bar", "baz"])) - self.assertEqual(e6.path, deque([1, "foo"])) - - self.assertEqual(e1.schema_path, deque(["type"])) - self.assertEqual(e2.schema_path, deque(["items", "type"])) - self.assertEqual( - list(e3.schema_path), ["items", "properties", "bar", "type"], - ) - self.assertEqual( - list(e4.schema_path), - ["items", "properties", "bar", "properties", "bar", "required"], - ) - self.assertEqual( - list(e5.schema_path), - ["items", "properties", "bar", "properties", "baz", "minItems"] - ) - self.assertEqual( - list(e6.schema_path), ["items", "properties", "foo", "enum"], - ) - - self.assertEqual(e1.validator, "type") - self.assertEqual(e2.validator, "type") - self.assertEqual(e3.validator, "type") - self.assertEqual(e4.validator, "required") - self.assertEqual(e5.validator, "minItems") - self.assertEqual(e6.validator, "enum") - - def test_additionalProperties(self): - instance = {"bar": "bar", "foo": 2} - schema = { - "additionalProperties" : {"type": "integer", "minimum": 5} - } - - validator = Draft3Validator(schema) - errors = validator.iter_errors(instance) - e1, e2 = sorted_errors(errors) - - self.assertEqual(e1.path, deque(["bar"])) - self.assertEqual(e2.path, deque(["foo"])) - - self.assertEqual(e1.validator, "type") - self.assertEqual(e2.validator, "minimum") - - def test_patternProperties(self): - instance = {"bar": 1, "foo": 2} - schema = { - "patternProperties" : { - "bar": {"type": "string"}, - "foo": {"minimum": 5} - } - } - - validator = Draft3Validator(schema) - errors = validator.iter_errors(instance) - e1, e2 = sorted_errors(errors) - - self.assertEqual(e1.path, deque(["bar"])) - self.assertEqual(e2.path, deque(["foo"])) - - self.assertEqual(e1.validator, "type") - self.assertEqual(e2.validator, "minimum") - - def test_additionalItems(self): - instance = ["foo", 1] - schema = { - "items": [], - "additionalItems" : {"type": "integer", "minimum": 5} - } - - validator = Draft3Validator(schema) - errors = validator.iter_errors(instance) - e1, e2 = sorted_errors(errors) - - self.assertEqual(e1.path, deque([0])) - self.assertEqual(e2.path, deque([1])) - - self.assertEqual(e1.validator, "type") - self.assertEqual(e2.validator, "minimum") - - def test_additionalItems_with_items(self): - instance = ["foo", "bar", 1] - schema = { - "items": [{}], - "additionalItems" : {"type": "integer", "minimum": 5} - } - - validator = Draft3Validator(schema) - errors = validator.iter_errors(instance) - e1, e2 = sorted_errors(errors) - - self.assertEqual(e1.path, deque([1])) - self.assertEqual(e2.path, deque([2])) - - self.assertEqual(e1.validator, "type") - self.assertEqual(e2.validator, "minimum") - - -class ValidatorTestMixin(object): - def setUp(self): - self.instance = mock.Mock() - self.schema = {} - self.resolver = mock.Mock() - self.validator = self.validator_class(self.schema) - - def test_valid_instances_are_valid(self): - errors = iter([]) - - with mock.patch.object( - self.validator, "iter_errors", return_value=errors, - ): - self.assertTrue( - self.validator.is_valid(self.instance, self.schema) - ) - - def test_invalid_instances_are_not_valid(self): - errors = iter([mock.Mock()]) - - with mock.patch.object( - self.validator, "iter_errors", return_value=errors, - ): - self.assertFalse( - self.validator.is_valid(self.instance, self.schema) - ) - - def test_non_existent_properties_are_ignored(self): - instance, my_property, my_value = mock.Mock(), mock.Mock(), mock.Mock() - validate(instance=instance, schema={my_property : my_value}) - - def test_it_creates_a_ref_resolver_if_not_provided(self): - self.assertIsInstance(self.validator.resolver, RefResolver) - - def test_it_delegates_to_a_ref_resolver(self): - resolver = RefResolver("", {}) - schema = {"$ref" : mock.Mock()} - - @contextmanager - def resolving(): - yield {"type": "integer"} - - with mock.patch.object(resolver, "resolving") as resolve: - resolve.return_value = resolving() - with self.assertRaises(ValidationError): - self.validator_class(schema, resolver=resolver).validate(None) - - resolve.assert_called_once_with(schema["$ref"]) - - def test_is_type_is_true_for_valid_type(self): - self.assertTrue(self.validator.is_type("foo", "string")) - - def test_is_type_is_false_for_invalid_type(self): - self.assertFalse(self.validator.is_type("foo", "array")) - - def test_is_type_evades_bool_inheriting_from_int(self): - self.assertFalse(self.validator.is_type(True, "integer")) - self.assertFalse(self.validator.is_type(True, "number")) - - def test_is_type_raises_exception_for_unknown_type(self): - with self.assertRaises(UnknownType): - self.validator.is_type("foo", object()) - - -class TestDraft3Validator(ValidatorTestMixin, unittest.TestCase): - validator_class = Draft3Validator - - def test_is_type_is_true_for_any_type(self): - self.assertTrue(self.validator.is_valid(mock.Mock(), {"type": "any"})) - - def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): - self.assertTrue(self.validator.is_type(True, "boolean")) - self.assertTrue(self.validator.is_valid(True, {"type": "any"})) - - def test_non_string_custom_types(self): - schema = {'type': [None]} - cls = self.validator_class(schema, types={None: type(None)}) - cls.validate(None, schema) - - -class TestDraft4Validator(ValidatorTestMixin, unittest.TestCase): - validator_class = Draft4Validator - - -class TestBuiltinFormats(unittest.TestCase): - """ - The built-in (specification-defined) formats do not raise type errors. - - If an instance or value is not a string, it should be ignored. - - """ - - -for format in FormatChecker.checkers: - def test(self, format=format): - v = Draft4Validator({"format": format}, format_checker=FormatChecker()) - v.validate(123) - - name = "test_{0}_ignores_non_strings".format(format) - test.__name__ = name - setattr(TestBuiltinFormats, name, test) - del test # Ugh py.test. Stop discovering top level tests. - - -class TestValidatorFor(unittest.TestCase): - def test_draft_3(self): - schema = {"$schema" : "http://json-schema.org/draft-03/schema"} - self.assertIs(validator_for(schema), Draft3Validator) - - schema = {"$schema" : "http://json-schema.org/draft-03/schema#"} - self.assertIs(validator_for(schema), Draft3Validator) - - def test_draft_4(self): - schema = {"$schema" : "http://json-schema.org/draft-04/schema"} - self.assertIs(validator_for(schema), Draft4Validator) - - schema = {"$schema" : "http://json-schema.org/draft-04/schema#"} - self.assertIs(validator_for(schema), Draft4Validator) - - def test_custom_validator(self): - Validator = create(meta_schema={"id" : "meta schema id"}, version="12") - schema = {"$schema" : "meta schema id"} - self.assertIs(validator_for(schema), Validator) - - def test_validator_for_jsonschema_default(self): - self.assertIs(validator_for({}), Draft4Validator) - - def test_validator_for_custom_default(self): - self.assertIs(validator_for({}, default=None), None) - - -class TestValidate(unittest.TestCase): - def test_draft3_validator_is_chosen(self): - schema = {"$schema" : "http://json-schema.org/draft-03/schema#"} - with mock.patch.object(Draft3Validator, "check_schema") as chk_schema: - validate({}, schema) - chk_schema.assert_called_once_with(schema) - # Make sure it works without the empty fragment - schema = {"$schema" : "http://json-schema.org/draft-03/schema"} - with mock.patch.object(Draft3Validator, "check_schema") as chk_schema: - validate({}, schema) - chk_schema.assert_called_once_with(schema) - - def test_draft4_validator_is_chosen(self): - schema = {"$schema" : "http://json-schema.org/draft-04/schema#"} - with mock.patch.object(Draft4Validator, "check_schema") as chk_schema: - validate({}, schema) - chk_schema.assert_called_once_with(schema) - - def test_draft4_validator_is_the_default(self): - with mock.patch.object(Draft4Validator, "check_schema") as chk_schema: - validate({}, {}) - chk_schema.assert_called_once_with({}) - - -class TestRefResolver(unittest.TestCase): - - base_uri = "" - stored_uri = "foo://stored" - stored_schema = {"stored" : "schema"} - - def setUp(self): - self.referrer = {} - self.store = {self.stored_uri : self.stored_schema} - self.resolver = RefResolver(self.base_uri, self.referrer, self.store) - - def test_it_does_not_retrieve_schema_urls_from_the_network(self): - ref = Draft3Validator.META_SCHEMA["id"] - with mock.patch.object(self.resolver, "resolve_remote") as remote: - with self.resolver.resolving(ref) as resolved: - self.assertEqual(resolved, Draft3Validator.META_SCHEMA) - self.assertFalse(remote.called) - - def test_it_resolves_local_refs(self): - ref = "#/properties/foo" - self.referrer["properties"] = {"foo" : object()} - with self.resolver.resolving(ref) as resolved: - self.assertEqual(resolved, self.referrer["properties"]["foo"]) - - def test_it_resolves_local_refs_with_id(self): - schema = {"id": "foo://bar/schema#", "a": {"foo": "bar"}} - resolver = RefResolver.from_schema(schema) - with resolver.resolving("#/a") as resolved: - self.assertEqual(resolved, schema["a"]) - with resolver.resolving("foo://bar/schema#/a") as resolved: - self.assertEqual(resolved, schema["a"]) - - def test_it_retrieves_stored_refs(self): - with self.resolver.resolving(self.stored_uri) as resolved: - self.assertIs(resolved, self.stored_schema) - - self.resolver.store["cached_ref"] = {"foo" : 12} - with self.resolver.resolving("cached_ref#/foo") as resolved: - self.assertEqual(resolved, 12) - - def test_it_retrieves_unstored_refs_via_requests(self): - ref = "http://bar#baz" - schema = {"baz" : 12} - - with mock.patch("jsonschema.validators.requests") as requests: - requests.get.return_value.json.return_value = schema - with self.resolver.resolving(ref) as resolved: - self.assertEqual(resolved, 12) - requests.get.assert_called_once_with("http://bar") - - def test_it_retrieves_unstored_refs_via_urlopen(self): - ref = "http://bar#baz" - schema = {"baz" : 12} - - with mock.patch("jsonschema.validators.requests", None): - with mock.patch("jsonschema.validators.urlopen") as urlopen: - urlopen.return_value.read.return_value = ( - json.dumps(schema).encode("utf8")) - with self.resolver.resolving(ref) as resolved: - self.assertEqual(resolved, 12) - urlopen.assert_called_once_with("http://bar") - - def test_it_can_construct_a_base_uri_from_a_schema(self): - schema = {"id" : "foo"} - resolver = RefResolver.from_schema(schema) - self.assertEqual(resolver.base_uri, "foo") - with resolver.resolving("") as resolved: - self.assertEqual(resolved, schema) - with resolver.resolving("#") as resolved: - self.assertEqual(resolved, schema) - with resolver.resolving("foo") as resolved: - self.assertEqual(resolved, schema) - with resolver.resolving("foo#") as resolved: - self.assertEqual(resolved, schema) - - def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): - schema = {} - resolver = RefResolver.from_schema(schema) - self.assertEqual(resolver.base_uri, "") - with resolver.resolving("") as resolved: - self.assertEqual(resolved, schema) - with resolver.resolving("#") as resolved: - self.assertEqual(resolved, schema) - - def test_custom_uri_scheme_handlers(self): - schema = {"foo": "bar"} - ref = "foo://bar" - foo_handler = mock.Mock(return_value=schema) - resolver = RefResolver("", {}, handlers={"foo": foo_handler}) - with resolver.resolving(ref) as resolved: - self.assertEqual(resolved, schema) - foo_handler.assert_called_once_with(ref) - - def test_cache_remote_on(self): - ref = "foo://bar" - foo_handler = mock.Mock() - resolver = RefResolver( - "", {}, cache_remote=True, handlers={"foo" : foo_handler}, - ) - with resolver.resolving(ref): - pass - with resolver.resolving(ref): - pass - foo_handler.assert_called_once_with(ref) - - def test_cache_remote_off(self): - ref = "foo://bar" - foo_handler = mock.Mock() - resolver = RefResolver( - "", {}, cache_remote=False, handlers={"foo" : foo_handler}, - ) - with resolver.resolving(ref): - pass - with resolver.resolving(ref): - pass - self.assertEqual(foo_handler.call_count, 2) - - def test_if_you_give_it_junk_you_get_a_resolution_error(self): - ref = "foo://bar" - foo_handler = mock.Mock(side_effect=ValueError("Oh no! What's this?")) - resolver = RefResolver("", {}, handlers={"foo" : foo_handler}) - with self.assertRaises(RefResolutionError) as err: - with resolver.resolving(ref): - pass - self.assertEqual(str(err.exception), "Oh no! What's this?") - - -def sorted_errors(errors): - def key(error): - return ( - [str(e) for e in error.path], - [str(e) for e in error.schema_path] - ) - return sorted(errors, key=key) diff --git a/lib/spack/external/jsonschema/validators.py b/lib/spack/external/jsonschema/validators.py index 30c3515398..1dc420c70d 100644 --- a/lib/spack/external/jsonschema/validators.py +++ b/lib/spack/external/jsonschema/validators.py @@ -1,26 +1,107 @@ +""" +Creation and extension of validators, with implementations for existing drafts. +""" from __future__ import division +from warnings import warn import contextlib import json import numbers -requests = None +from six import add_metaclass -from jsonschema import _utils, _validators +from jsonschema import ( + _legacy_validators, + _types, + _utils, + _validators, + exceptions, +) from jsonschema.compat import ( - Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen, - str_types, int_types, iteritems, + Sequence, + int_types, + iteritems, + lru_cache, + str_types, + unquote, + urldefrag, + urljoin, + urlopen, + urlsplit, ) -from jsonschema.exceptions import ErrorTree # Backwards compatibility # noqa -from jsonschema.exceptions import RefResolutionError, SchemaError, UnknownType +# Sigh. https://gitlab.com/pycqa/flake8/issues/280 +# https://github.com/pyga/ebb-lint/issues/7 +# Imported for backwards compatibility. +from jsonschema.exceptions import ErrorTree +ErrorTree + + +class _DontDoThat(Exception): + """ + Raised when a Validators with non-default type checker is misused. + + Asking one for DEFAULT_TYPES doesn't make sense, since type checkers + exist for the unrepresentable cases where DEFAULT_TYPES can't + represent the type relationship. + """ + + def __str__(self): + return "DEFAULT_TYPES cannot be used on Validators using TypeCheckers" -_unset = _utils.Unset() validators = {} meta_schemas = _utils.URIDict() +def _generate_legacy_type_checks(types=()): + """ + Generate newer-style type checks out of JSON-type-name-to-type mappings. + + Arguments: + + types (dict): + + A mapping of type names to their Python types + + Returns: + + A dictionary of definitions to pass to `TypeChecker` + """ + types = dict(types) + + def gen_type_check(pytypes): + pytypes = _utils.flatten(pytypes) + + def type_check(checker, instance): + if isinstance(instance, bool): + if bool not in pytypes: + return False + return isinstance(instance, pytypes) + + return type_check + + definitions = {} + for typename, pytypes in iteritems(types): + definitions[typename] = gen_type_check(pytypes) + + return definitions + + +_DEPRECATED_DEFAULT_TYPES = { + u"array": list, + u"boolean": bool, + u"integer": int_types, + u"null": type(None), + u"number": numbers.Number, + u"object": dict, + u"string": str_types, +} +_TYPE_CHECKER_FOR_DEPRECATED_DEFAULT_TYPES = _types.TypeChecker( + type_checkers=_generate_legacy_type_checks(_DEPRECATED_DEFAULT_TYPES), +) + + def validates(version): """ Register the decorated validator for a ``version`` of the specification. @@ -28,40 +109,180 @@ def validates(version): Registered validators and their meta schemas will be considered when parsing ``$schema`` properties' URIs. - :argument str version: an identifier to use as the version's name - :returns: a class decorator to decorate the validator with the version + Arguments: + + version (str): + + An identifier to use as the version's name + + Returns: + + collections.Callable: + a class decorator to decorate the validator with the version """ def _validates(cls): validators[version] = cls - if u"id" in cls.META_SCHEMA: - meta_schemas[cls.META_SCHEMA[u"id"]] = cls + meta_schema_id = cls.ID_OF(cls.META_SCHEMA) + if meta_schema_id: + meta_schemas[meta_schema_id] = cls return cls return _validates -def create(meta_schema, validators=(), version=None, default_types=None): # noqa - if default_types is None: - default_types = { - u"array" : list, u"boolean" : bool, u"integer" : int_types, - u"null" : type(None), u"number" : numbers.Number, u"object" : dict, - u"string" : str_types, - } +def _DEFAULT_TYPES(self): + if self._CREATED_WITH_DEFAULT_TYPES is None: + raise _DontDoThat() + + warn( + ( + "The DEFAULT_TYPES attribute is deprecated. " + "See the type checker attached to this validator instead." + ), + DeprecationWarning, + stacklevel=2, + ) + return self._DEFAULT_TYPES + + +class _DefaultTypesDeprecatingMetaClass(type): + DEFAULT_TYPES = property(_DEFAULT_TYPES) + + +def _id_of(schema): + if schema is True or schema is False: + return u"" + return schema.get(u"$id", u"") + + +def create( + meta_schema, + validators=(), + version=None, + default_types=None, + type_checker=None, + id_of=_id_of, +): + """ + Create a new validator class. + + Arguments: + + meta_schema (collections.Mapping): + + the meta schema for the new validator class + + validators (collections.Mapping): + + a mapping from names to callables, where each callable will + validate the schema property with the given name. + + Each callable should take 4 arguments: + + 1. a validator instance, + 2. the value of the property being validated within the + instance + 3. the instance + 4. the schema + + version (str): + + an identifier for the version that this validator class will + validate. If provided, the returned validator class will + have its ``__name__`` set to include the version, and also + will have `jsonschema.validators.validates` automatically + called for the given version. + type_checker (jsonschema.TypeChecker): + + a type checker, used when applying the :validator:`type` validator. + + If unprovided, a `jsonschema.TypeChecker` will be created + with a set of default types typical of JSON Schema drafts. + + default_types (collections.Mapping): + + .. deprecated:: 3.0.0 + + Please use the type_checker argument instead. + + If set, it provides mappings of JSON types to Python types + that will be converted to functions and redefined in this + object's `jsonschema.TypeChecker`. + + id_of (collections.Callable): + + A function that given a schema, returns its ID. + + Returns: + + a new `jsonschema.IValidator` class + """ + + if default_types is not None: + if type_checker is not None: + raise TypeError( + "Do not specify default_types when providing a type checker.", + ) + _created_with_default_types = True + warn( + ( + "The default_types argument is deprecated. " + "Use the type_checker argument instead." + ), + DeprecationWarning, + stacklevel=2, + ) + type_checker = _types.TypeChecker( + type_checkers=_generate_legacy_type_checks(default_types), + ) + else: + default_types = _DEPRECATED_DEFAULT_TYPES + if type_checker is None: + _created_with_default_types = False + type_checker = _TYPE_CHECKER_FOR_DEPRECATED_DEFAULT_TYPES + elif type_checker is _TYPE_CHECKER_FOR_DEPRECATED_DEFAULT_TYPES: + _created_with_default_types = False + else: + _created_with_default_types = None + + @add_metaclass(_DefaultTypesDeprecatingMetaClass) class Validator(object): + VALIDATORS = dict(validators) META_SCHEMA = dict(meta_schema) - DEFAULT_TYPES = dict(default_types) + TYPE_CHECKER = type_checker + ID_OF = staticmethod(id_of) + + DEFAULT_TYPES = property(_DEFAULT_TYPES) + _DEFAULT_TYPES = dict(default_types) + _CREATED_WITH_DEFAULT_TYPES = _created_with_default_types def __init__( - self, schema, types=(), resolver=None, format_checker=None, + self, + schema, + types=(), + resolver=None, + format_checker=None, ): - self._types = dict(self.DEFAULT_TYPES) - self._types.update(types) + if types: + warn( + ( + "The types argument is deprecated. Provide " + "a type_checker to jsonschema.validators.extend " + "instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + self.TYPE_CHECKER = self.TYPE_CHECKER.redefine_many( + _generate_legacy_type_checks(types), + ) if resolver is None: - resolver = RefResolver.from_schema(schema) + resolver = RefResolver.from_schema(schema, id_of=id_of) self.resolver = resolver self.format_checker = format_checker @@ -70,13 +291,28 @@ def create(meta_schema, validators=(), version=None, default_types=None): # noq @classmethod def check_schema(cls, schema): for error in cls(cls.META_SCHEMA).iter_errors(schema): - raise SchemaError.create_from(error) + raise exceptions.SchemaError.create_from(error) def iter_errors(self, instance, _schema=None): if _schema is None: _schema = self.schema - with self.resolver.in_scope(_schema.get(u"id", u"")): + if _schema is True: + return + elif _schema is False: + yield exceptions.ValidationError( + "False schema does not allow %r" % (instance,), + validator=None, + validator_value=None, + instance=instance, + schema=_schema, + ) + return + + scope = id_of(_schema) + if scope: + self.resolver.push_scope(scope) + try: ref = _schema.get(u"$ref") if ref is not None: validators = [(u"$ref", ref)] @@ -100,6 +336,9 @@ def create(meta_schema, validators=(), version=None, default_types=None): # noq if k != u"$ref": error.schema_path.appendleft(k) yield error + finally: + if scope: + self.resolver.pop_scope() def descend(self, instance, schema, path=None, schema_path=None): for error in self.iter_errors(instance, schema): @@ -114,19 +353,10 @@ def create(meta_schema, validators=(), version=None, default_types=None): # noq raise error def is_type(self, instance, type): - if type not in self._types: - raise UnknownType(type, instance, self.schema) - pytypes = self._types[type] - - # bool inherits from int, so ensure bools aren't reported as ints - if isinstance(instance, bool): - pytypes = _utils.flatten(pytypes) - is_number = any( - issubclass(pytype, numbers.Number) for pytype in pytypes - ) - if is_number and bool not in pytypes: - return False - return isinstance(instance, pytypes) + try: + return self.TYPE_CHECKER.is_type(instance, type) + except exceptions.UndefinedTypeCheck: + raise exceptions.UnknownType(type, instance, self.schema) def is_valid(self, instance, _schema=None): error = next(self.iter_errors(instance, _schema), None) @@ -139,104 +369,290 @@ def create(meta_schema, validators=(), version=None, default_types=None): # noq return Validator -def extend(validator, validators, version=None): +def extend(validator, validators=(), version=None, type_checker=None): + """ + Create a new validator class by extending an existing one. + + Arguments: + + validator (jsonschema.IValidator): + + an existing validator class + + validators (collections.Mapping): + + a mapping of new validator callables to extend with, whose + structure is as in `create`. + + .. note:: + + Any validator callables with the same name as an + existing one will (silently) replace the old validator + callable entirely, effectively overriding any validation + done in the "parent" validator class. + + If you wish to instead extend the behavior of a parent's + validator callable, delegate and call it directly in + the new validator function by retrieving it using + ``OldValidator.VALIDATORS["validator_name"]``. + + version (str): + + a version for the new validator class + + type_checker (jsonschema.TypeChecker): + + a type checker, used when applying the :validator:`type` validator. + + If unprovided, the type checker of the extended + `jsonschema.IValidator` will be carried along.` + + Returns: + + a new `jsonschema.IValidator` class extending the one provided + + .. note:: Meta Schemas + + The new validator class will have its parent's meta schema. + + If you wish to change or extend the meta schema in the new + validator class, modify ``META_SCHEMA`` directly on the returned + class. Note that no implicit copying is done, so a copy should + likely be made before modifying it, in order to not affect the + old validator. + """ + all_validators = dict(validator.VALIDATORS) all_validators.update(validators) + + if type_checker is None: + type_checker = validator.TYPE_CHECKER + elif validator._CREATED_WITH_DEFAULT_TYPES: + raise TypeError( + "Cannot extend a validator created with default_types " + "with a type_checker. Update the validator to use a " + "type_checker when created." + ) return create( meta_schema=validator.META_SCHEMA, validators=all_validators, version=version, - default_types=validator.DEFAULT_TYPES, + type_checker=type_checker, + id_of=validator.ID_OF, ) Draft3Validator = create( meta_schema=_utils.load_schema("draft3"), validators={ - u"$ref" : _validators.ref, - u"additionalItems" : _validators.additionalItems, - u"additionalProperties" : _validators.additionalProperties, - u"dependencies" : _validators.dependencies, - u"disallow" : _validators.disallow_draft3, - u"divisibleBy" : _validators.multipleOf, - u"enum" : _validators.enum, - u"extends" : _validators.extends_draft3, - u"format" : _validators.format, - u"items" : _validators.items, - u"maxItems" : _validators.maxItems, - u"maxLength" : _validators.maxLength, - u"maximum" : _validators.maximum, - u"minItems" : _validators.minItems, - u"minLength" : _validators.minLength, - u"minimum" : _validators.minimum, - u"multipleOf" : _validators.multipleOf, - u"pattern" : _validators.pattern, - u"patternProperties" : _validators.patternProperties, - u"properties" : _validators.properties_draft3, - u"type" : _validators.type_draft3, - u"uniqueItems" : _validators.uniqueItems, + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"dependencies": _legacy_validators.dependencies_draft3, + u"disallow": _legacy_validators.disallow_draft3, + u"divisibleBy": _validators.multipleOf, + u"enum": _validators.enum, + u"extends": _legacy_validators.extends_draft3, + u"format": _validators.format, + u"items": _legacy_validators.items_draft3_draft4, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maximum": _legacy_validators.maximum_draft3_draft4, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minimum": _legacy_validators.minimum_draft3_draft4, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _legacy_validators.properties_draft3, + u"type": _legacy_validators.type_draft3, + u"uniqueItems": _validators.uniqueItems, }, + type_checker=_types.draft3_type_checker, version="draft3", + id_of=lambda schema: schema.get(u"id", ""), ) Draft4Validator = create( meta_schema=_utils.load_schema("draft4"), validators={ - u"$ref" : _validators.ref, - u"additionalItems" : _validators.additionalItems, - u"additionalProperties" : _validators.additionalProperties, - u"allOf" : _validators.allOf_draft4, - u"anyOf" : _validators.anyOf_draft4, - u"dependencies" : _validators.dependencies, - u"enum" : _validators.enum, - u"format" : _validators.format, - u"items" : _validators.items, - u"maxItems" : _validators.maxItems, - u"maxLength" : _validators.maxLength, - u"maxProperties" : _validators.maxProperties_draft4, - u"maximum" : _validators.maximum, - u"minItems" : _validators.minItems, - u"minLength" : _validators.minLength, - u"minProperties" : _validators.minProperties_draft4, - u"minimum" : _validators.minimum, - u"multipleOf" : _validators.multipleOf, - u"not" : _validators.not_draft4, - u"oneOf" : _validators.oneOf_draft4, - u"pattern" : _validators.pattern, - u"patternProperties" : _validators.patternProperties, - u"properties" : _validators.properties_draft4, - u"required" : _validators.required_draft4, - u"type" : _validators.type_draft4, - u"uniqueItems" : _validators.uniqueItems, + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"allOf": _validators.allOf, + u"anyOf": _validators.anyOf, + u"dependencies": _validators.dependencies, + u"enum": _validators.enum, + u"format": _validators.format, + u"items": _legacy_validators.items_draft3_draft4, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maxProperties": _validators.maxProperties, + u"maximum": _legacy_validators.maximum_draft3_draft4, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minProperties": _validators.minProperties, + u"minimum": _legacy_validators.minimum_draft3_draft4, + u"multipleOf": _validators.multipleOf, + u"not": _validators.not_, + u"oneOf": _validators.oneOf, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _validators.properties, + u"required": _validators.required, + u"type": _validators.type, + u"uniqueItems": _validators.uniqueItems, }, + type_checker=_types.draft4_type_checker, version="draft4", + id_of=lambda schema: schema.get(u"id", ""), +) + +Draft6Validator = create( + meta_schema=_utils.load_schema("draft6"), + validators={ + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"allOf": _validators.allOf, + u"anyOf": _validators.anyOf, + u"const": _validators.const, + u"contains": _validators.contains, + u"dependencies": _validators.dependencies, + u"enum": _validators.enum, + u"exclusiveMaximum": _validators.exclusiveMaximum, + u"exclusiveMinimum": _validators.exclusiveMinimum, + u"format": _validators.format, + u"items": _validators.items, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maxProperties": _validators.maxProperties, + u"maximum": _validators.maximum, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minProperties": _validators.minProperties, + u"minimum": _validators.minimum, + u"multipleOf": _validators.multipleOf, + u"not": _validators.not_, + u"oneOf": _validators.oneOf, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _validators.properties, + u"propertyNames": _validators.propertyNames, + u"required": _validators.required, + u"type": _validators.type, + u"uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft6_type_checker, + version="draft6", +) + +Draft7Validator = create( + meta_schema=_utils.load_schema("draft7"), + validators={ + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"allOf": _validators.allOf, + u"anyOf": _validators.anyOf, + u"const": _validators.const, + u"contains": _validators.contains, + u"dependencies": _validators.dependencies, + u"enum": _validators.enum, + u"exclusiveMaximum": _validators.exclusiveMaximum, + u"exclusiveMinimum": _validators.exclusiveMinimum, + u"format": _validators.format, + u"if": _validators.if_, + u"items": _validators.items, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maxProperties": _validators.maxProperties, + u"maximum": _validators.maximum, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minProperties": _validators.minProperties, + u"minimum": _validators.minimum, + u"multipleOf": _validators.multipleOf, + u"oneOf": _validators.oneOf, + u"not": _validators.not_, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _validators.properties, + u"propertyNames": _validators.propertyNames, + u"required": _validators.required, + u"type": _validators.type, + u"uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft7_type_checker, + version="draft7", ) +_LATEST_VERSION = Draft7Validator + class RefResolver(object): """ Resolve JSON References. - :argument str base_uri: URI of the referring document - :argument referrer: the actual referring document - :argument dict store: a mapping from URIs to documents to cache - :argument bool cache_remote: whether remote refs should be cached after - first resolution - :argument dict handlers: a mapping from URI schemes to functions that - should be used to retrieve them + Arguments: + + base_uri (str): + + The URI of the referring document + + referrer: + + The actual referring document + + store (dict): + + A mapping from URIs to documents to cache + + cache_remote (bool): + + Whether remote refs should be cached after first resolution + + handlers (dict): + + A mapping from URI schemes to functions that should be used + to retrieve them + + urljoin_cache (:func:`functools.lru_cache`): + A cache that will be used for caching the results of joining + the resolution scope to subscopes. + + remote_cache (:func:`functools.lru_cache`): + + A cache that will be used for caching the results of + resolved remote URLs. + + Attributes: + + cache_remote (bool): + + Whether remote refs should be cached after first resolution """ def __init__( - self, base_uri, referrer, store=(), cache_remote=True, handlers=(), + self, + base_uri, + referrer, + store=(), + cache_remote=True, + handlers=(), + urljoin_cache=None, + remote_cache=None, ): - self.base_uri = base_uri - self.resolution_scope = base_uri - # This attribute is not used, it is for backwards compatibility + if urljoin_cache is None: + urljoin_cache = lru_cache(1024)(urljoin) + if remote_cache is None: + remote_cache = lru_cache(1024)(self.resolve_from_url) + self.referrer = referrer self.cache_remote = cache_remote self.handlers = dict(handlers) + self._scopes_stack = [base_uri] self.store = _utils.URIDict( (id, validator.META_SCHEMA) for id, validator in iteritems(meta_schemas) @@ -244,64 +660,139 @@ class RefResolver(object): self.store.update(store) self.store[base_uri] = referrer + self._urljoin_cache = urljoin_cache + self._remote_cache = remote_cache + @classmethod - def from_schema(cls, schema, *args, **kwargs): + def from_schema(cls, schema, id_of=_id_of, *args, **kwargs): """ Construct a resolver from a JSON schema object. - :argument schema schema: the referring schema - :rtype: :class:`RefResolver` + Arguments: + + schema: + + the referring schema + + Returns: + + `RefResolver` + """ + + return cls(base_uri=id_of(schema), referrer=schema, *args, **kwargs) + + def push_scope(self, scope): + """ + Enter a given sub-scope. + + Treats further dereferences as being performed underneath the + given scope. + """ + self._scopes_stack.append( + self._urljoin_cache(self.resolution_scope, scope), + ) + def pop_scope(self): """ + Exit the most recent entered scope. - return cls(schema.get(u"id", u""), schema, *args, **kwargs) + Treats further dereferences as being performed underneath the + original scope. + + Don't call this method more times than `push_scope` has been + called. + """ + try: + self._scopes_stack.pop() + except IndexError: + raise exceptions.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): + """ + Retrieve the current resolution scope. + """ + return self._scopes_stack[-1] + + @property + def base_uri(self): + """ + Retrieve the current base URI, not including any fragment. + """ + uri, _ = urldefrag(self.resolution_scope) + return uri @contextlib.contextmanager def in_scope(self, scope): - old_scope = self.resolution_scope - self.resolution_scope = urljoin(old_scope, scope) + """ + Temporarily enter the given scope for the duration of the context. + """ + self.push_scope(scope) try: yield finally: - self.resolution_scope = old_scope + self.pop_scope() @contextlib.contextmanager def resolving(self, ref): """ - Context manager which resolves a JSON ``ref`` and enters the - resolution scope of this ref. + Resolve the given ``ref`` and enter its resolution scope. + + Exits the scope on exit of this context manager. + + Arguments: - :argument str ref: reference to resolve + ref (str): + The reference to resolve """ - full_uri = urljoin(self.resolution_scope, ref) - uri, fragment = urldefrag(full_uri) - if not uri: - uri = self.base_uri + url, resolved = self.resolve(ref) + self.push_scope(url) + try: + yield resolved + finally: + self.pop_scope() - if uri in self.store: - document = self.store[uri] - else: + def resolve(self, ref): + """ + Resolve the given reference. + """ + url = self._urljoin_cache(self.resolution_scope, ref) + return url, self._remote_cache(url) + + def resolve_from_url(self, url): + """ + Resolve the given remote URL. + """ + url, fragment = urldefrag(url) + try: + document = self.store[url] + except KeyError: try: - document = self.resolve_remote(uri) + document = self.resolve_remote(url) except Exception as exc: - raise RefResolutionError(exc) + raise exceptions.RefResolutionError(exc) - old_base_uri, self.base_uri = self.base_uri, uri - try: - with self.in_scope(uri): - yield self.resolve_fragment(document, fragment) - finally: - self.base_uri = old_base_uri + return self.resolve_fragment(document, fragment) def resolve_fragment(self, document, fragment): """ Resolve a ``fragment`` within the referenced ``document``. - :argument document: the referrant document - :argument str fragment: a URI fragment to resolve within it + Arguments: + + document: + The referent document + + fragment (str): + + a URI fragment to resolve within it """ fragment = fragment.lstrip(u"/") @@ -319,7 +810,7 @@ class RefResolver(object): try: document = document[part] except (TypeError, LookupError): - raise RefResolutionError( + raise exceptions.RefResolutionError( "Unresolvable JSON pointer: %r" % fragment ) @@ -329,8 +820,9 @@ class RefResolver(object): """ Resolve a remote ``uri``. - Does not check the store first, but stores the retrieved document in - the store if :attr:`RefResolver.cache_remote` is True. + If called directly, does not check the store first, but after + retrieving the document at the specified URI it will be saved in + the store if :attr:`cache_remote` is True. .. note:: @@ -341,85 +833,138 @@ class RefResolver(object): If it isn't, or if the scheme of the ``uri`` is not ``http`` or ``https``, UTF-8 is assumed. - :argument str uri: the URI to resolve - :returns: the retrieved document + Arguments: + + uri (str): - .. _requests: http://pypi.python.org/pypi/requests/ + The URI to resolve + Returns: + + The retrieved document + + .. _requests: https://pypi.org/project/requests/ """ + try: + import requests + except ImportError: + requests = None scheme = urlsplit(uri).scheme if scheme in self.handlers: result = self.handlers[scheme](uri) - elif ( - scheme in [u"http", u"https"] and - requests and - getattr(requests.Response, "json", None) is not None - ): + elif scheme in [u"http", u"https"] and requests: # Requests has support for detecting the correct encoding of # json over http - if callable(requests.Response.json): - result = requests.get(uri).json() - else: - result = requests.get(uri).json + result = requests.get(uri).json() else: # Otherwise, pass off to urllib and assume utf-8 - result = json.loads(urlopen(uri).read().decode("utf-8")) + with urlopen(uri) as url: + result = json.loads(url.read().decode("utf-8")) if self.cache_remote: self.store[uri] = result return result -def validator_for(schema, default=_unset): - if default is _unset: - default = Draft4Validator - return meta_schemas.get(schema.get(u"$schema", u""), default) - - def validate(instance, schema, cls=None, *args, **kwargs): """ Validate an instance under the given schema. - >>> validate([2, 3, 4], {"maxItems" : 2}) + >>> validate([2, 3, 4], {"maxItems": 2}) Traceback (most recent call last): ... ValidationError: [2, 3, 4] is too long - :func:`validate` will first verify that the provided schema is itself - valid, since not doing so can lead to less obvious error messages and fail - in less obvious or consistent ways. If you know you have a valid schema - already or don't care, you might prefer using the - :meth:`~IValidator.validate` method directly on a specific validator - (e.g. :meth:`Draft4Validator.validate`). + :func:`validate` will first verify that the provided schema is + itself valid, since not doing so can lead to less obvious error + messages and fail in less obvious or consistent ways. + + If you know you have a valid schema already, especially if you + intend to validate multiple instances with the same schema, you + likely would prefer using the `IValidator.validate` method directly + on a specific validator (e.g. ``Draft7Validator.validate``). + + + Arguments: + instance: - :argument instance: the instance to validate - :argument schema: the schema to validate with - :argument cls: an :class:`IValidator` class that will be used to validate - the instance. + The instance to validate - If the ``cls`` argument is not provided, two things will happen in - accordance with the specification. First, if the schema has a - :validator:`$schema` property containing a known meta-schema [#]_ then the - proper validator will be used. The specification recommends that all - schemas contain :validator:`$schema` properties for this reason. If no - :validator:`$schema` property is found, the default validator class is - :class:`Draft4Validator`. + schema: - Any other provided positional and keyword arguments will be passed on when - instantiating the ``cls``. + The schema to validate with - :raises: - :exc:`ValidationError` if the instance is invalid + cls (IValidator): - :exc:`SchemaError` if the schema itself is invalid + The class that will be used to validate the instance. + + If the ``cls`` argument is not provided, two things will happen + in accordance with the specification. First, if the schema has a + :validator:`$schema` property containing a known meta-schema [#]_ + then the proper validator will be used. The specification recommends + that all schemas contain :validator:`$schema` properties for this + reason. If no :validator:`$schema` property is found, the default + validator class is the latest released draft. + + Any other provided positional and keyword arguments will be passed + on when instantiating the ``cls``. + + Raises: + + `jsonschema.exceptions.ValidationError` if the instance + is invalid + + `jsonschema.exceptions.SchemaError` if the schema itself + is invalid .. rubric:: Footnotes - .. [#] known by a validator registered with :func:`validates` + .. [#] known by a validator registered with + `jsonschema.validators.validates` """ if cls is None: cls = validator_for(schema) + cls.check_schema(schema) - cls(schema, *args, **kwargs).validate(instance) + validator = cls(schema, *args, **kwargs) + error = exceptions.best_match(validator.iter_errors(instance)) + if error is not None: + raise error + + +def validator_for(schema, default=_LATEST_VERSION): + """ + Retrieve the validator class appropriate for validating the given schema. + + Uses the :validator:`$schema` property that should be present in the + given schema to look up the appropriate validator class. + + Arguments: + + schema (collections.Mapping or bool): + + the schema to look at + + default: + + the default to return if the appropriate validator class + cannot be determined. + + If unprovided, the default is to return the latest supported + draft. + """ + if schema is True or schema is False or u"$schema" not in schema: + return default + if schema[u"$schema"] not in meta_schemas: + warn( + ( + "The metaschema specified by $schema was not found. " + "Using the latest draft to validate, but this will raise " + "an error in the future." + ), + DeprecationWarning, + stacklevel=2, + ) + return meta_schemas.get(schema[u"$schema"], _LATEST_VERSION) |