diff options
author | Todd Gamblin <tgamblin@llnl.gov> | 2021-12-19 11:19:37 -0800 |
---|---|---|
committer | Todd Gamblin <tgamblin@llnl.gov> | 2021-12-19 12:55:42 -0800 |
commit | 77030431955b997d8325e477d9b18803fd74e31a (patch) | |
tree | 7e80d4a5022edcd43ee9596102695a8d13b43f85 | |
parent | f83e0fb81a42639de936669951730d77b08b6eb1 (diff) | |
download | spack-77030431955b997d8325e477d9b18803fd74e31a.tar.gz spack-77030431955b997d8325e477d9b18803fd74e31a.tar.bz2 spack-77030431955b997d8325e477d9b18803fd74e31a.tar.xz spack-77030431955b997d8325e477d9b18803fd74e31a.zip |
externals: Upgrade `jsonschema` to `v3.2.0`
Our `jsonschema` external won't support Python 3.10, so we need to upgrade it.
It currently generates this warning:
lib/spack/external/jsonschema/compat.py:6: DeprecationWarning: Using or importing the ABCs
from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and
in 3.10 it will stop working
This upgrades `jsonschema` to 3.2.0, the latest version with support for Python 2.7. The next
version after this (4.0.0) drops support for 2.7 and 3.6, so we'll have to wait to upgrade to it.
Dependencies have been added in prior commits.
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) |