diff options
author | Julian Berman <Julian@GrayVines.com> | 2021-12-15 04:38:29 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-15 04:38:29 -0800 |
commit | 48280329f11021e8772bacbaf0b16efcd0f78af4 (patch) | |
tree | 813bdcc753037c7bff8b4c8b71779e7ba3a35d65 | |
parent | ac7b8389f14e845a966345171eadf3515cc3052f (diff) | |
parent | 3a6292591334f85f3a79a3c3612dabc79f3e6e31 (diff) | |
download | jsonschema-48280329f11021e8772bacbaf0b16efcd0f78af4.tar.gz |
Merge pull request #890 from sirosen/add-validator-protocol
Add `jsonschema.protocols.IValidator`
-rw-r--r-- | README.rst | 2 | ||||
-rw-r--r-- | docs/creating.rst | 9 | ||||
-rw-r--r-- | docs/errors.rst | 2 | ||||
-rw-r--r-- | docs/faq.rst | 4 | ||||
-rw-r--r-- | docs/spelling-wordlist.txt | 2 | ||||
-rw-r--r-- | docs/validate.rst | 154 | ||||
-rw-r--r-- | jsonschema/__init__.py | 1 | ||||
-rw-r--r-- | jsonschema/_types.py | 2 | ||||
-rw-r--r-- | jsonschema/protocols.py | 153 | ||||
-rw-r--r-- | jsonschema/tests/test_validators.py | 23 | ||||
-rw-r--r-- | jsonschema/validators.py | 13 | ||||
-rw-r--r-- | setup.cfg | 1 |
12 files changed, 209 insertions, 157 deletions
@@ -74,7 +74,7 @@ Features and `Draft 3 <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.Draft3Validator>`_ -* `Lazy validation <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.IValidator.iter_errors>`_ +* `Lazy validation <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.protocols.Validator.iter_errors>`_ that can iteratively report *all* validation errors. * `Programmatic querying <https://python-jsonschema.readthedocs.io/en/latest/errors/>`_ diff --git a/docs/creating.rst b/docs/creating.rst index a9f18d1..810293f 100644 --- a/docs/creating.rst +++ b/docs/creating.rst @@ -23,3 +23,12 @@ Any validating function that validates against a subschema should call instance, or schema, it should pass one or both of the ``path`` or ``schema_path`` arguments to ``descend`` in order to properly maintain where in the instance or schema respectively the error occurred. + +The Validator Protocol +---------------------- + +``jsonschema`` defines a `protocol <typing.Protocol>`, +`jsonschema.protocols.Validator` which can be used in type annotations to +describe the type of a validator object. + +For full details, see `validator-protocol`. diff --git a/docs/errors.rst b/docs/errors.rst index 2b2f485..7cef27e 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -275,7 +275,7 @@ error objects. As you can see, `jsonschema.exceptions.ErrorTree` takes an iterable of `ValidationError`\s when constructing a tree so you can directly pass it the return value of a validator object's -`jsonschema.IValidator.iter_errors` method. +`jsonschema.protocols.Validator.iter_errors` method. `ErrorTree`\s support a number of useful operations. The first one we might want to perform is to check whether a given element in our instance diff --git a/docs/faq.rst b/docs/faq.rst index d8eea5b..4fa2ac3 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -90,8 +90,8 @@ but fail the second! Still, filling in defaults is a thing that is useful. `jsonschema` allows you to `define your own validator classes and callables -<creating>`, so you can easily create an `jsonschema.IValidator` that -does do default setting. Here's some code to get you started. (In +<creating>`, so you can easily create an `jsonschema.protocols.Validator` +that does do default setting. Here's some code to get you started. (In this code, we add the default properties to each object *before* the properties are validated, so the default values themselves will need to be valid under the schema.) diff --git a/docs/spelling-wordlist.txt b/docs/spelling-wordlist.txt index 7a6627b..36805a1 100644 --- a/docs/spelling-wordlist.txt +++ b/docs/spelling-wordlist.txt @@ -1,5 +1,5 @@ # this appears to be misinterpreting Napoleon types as prose, sigh... -IValidator +Validator TypeChecker UnknownType ValidationError diff --git a/docs/validate.rst b/docs/validate.rst index 3d8134d..6be8f9c 100644 --- a/docs/validate.rst +++ b/docs/validate.rst @@ -19,160 +19,26 @@ The simplest way to validate an instance under a given schema is to use the fundamentals underway at `Understanding JSON Schema <https://json-schema.org/understanding-json-schema/>`_ +.. _validator-protocol: -The Validator Interface +The Validator Protocol ----------------------- -`jsonschema` defines an (informal) interface that all validator +`jsonschema` defines a protocol that all validator classes should adhere to. -.. class:: IValidator(schema, types=(), resolver=None, format_checker=None) - - :argument dict schema: the schema that the validator object - will validate with. It is assumed to be valid, and providing - an invalid schema can lead to undefined behavior. See - `IValidator.check_schema` to validate a schema first. - :argument resolver: an instance of `RefResolver` that will be - used to resolve :validator:`$ref` properties (JSON references). If - unprovided, one will be created. - :argument format_checker: an instance of `FormatChecker` - whose `FormatChecker.conforms` method will be called to - check and see if instances conform to each :validator:`format` - property present in the schema. If unprovided, no validation - will be done for :validator:`format`. Certain formats require - additional packages to be installed (ipv5, uri, color, date-time). - The required packages can be found at the bottom of this page. - :argument types: - .. deprecated:: 3.0.0 - - Use `TypeChecker.redefine` and - `jsonschema.validators.extend` instead of this argument. - - See `validating-types` for details. - - If used, this overrides or extends the list of known types when - validating the :validator:`type` property. - - What is provided should map strings (type names) to class objects - that will be checked via `isinstance`. - - - .. attribute:: META_SCHEMA - - An object representing the validator's meta schema (the schema that - describes valid schemas in the given version). - - .. attribute:: VALIDATORS - - A mapping of validator names (`str`\s) to functions - that validate the validator property with that name. For more - information see `creating-validators`. - - .. attribute:: TYPE_CHECKER - - A `TypeChecker` that will be used when validating :validator:`type` - properties in JSON schemas. - - .. attribute:: schema - - The schema that was passed in when initializing the object. - - .. attribute:: DEFAULT_TYPES - - .. deprecated:: 3.0.0 - - Use of this attribute is deprecated in favor of the new `type - checkers <TypeChecker>`. - - See `validating-types` for details. - - For backwards compatibility on existing validator classes, a mapping of - JSON types to Python class objects which define the Python types for - each JSON type. - - Any existing code using this attribute should likely transition to - using `TypeChecker.is_type`. - - - .. classmethod:: check_schema(schema) - - Validate the given schema against the validator's `META_SCHEMA`. - - :raises: `jsonschema.exceptions.SchemaError` if the schema - is invalid - - .. method:: is_type(instance, type) - - Check if the instance is of the given (JSON Schema) type. - - :type type: str - :rtype: bool - :raises: `jsonschema.exceptions.UnknownType` if ``type`` - is not a known type. - - .. method:: is_valid(instance) - - Check if the instance is valid under the current `schema`. - - :rtype: bool - - >>> schema = {"maxItems" : 2} - >>> Draft3Validator(schema).is_valid([2, 3, 4]) - False - - .. method:: iter_errors(instance) - - Lazily yield each of the validation errors in the given instance. - - :rtype: an `collections.abc.Iterable` of - `jsonschema.exceptions.ValidationError`\s - - >>> schema = { - ... "type" : "array", - ... "items" : {"enum" : [1, 2, 3]}, - ... "maxItems" : 2, - ... } - >>> v = Draft3Validator(schema) - >>> for error in sorted(v.iter_errors([2, 3, 4]), key=str): - ... print(error.message) - 4 is not one of [1, 2, 3] - [2, 3, 4] is too long - - .. method:: validate(instance) - - Check if the instance is valid under the current `schema`. - - :raises: `jsonschema.exceptions.ValidationError` if the - instance is invalid - - >>> schema = {"maxItems" : 2} - >>> Draft3Validator(schema).validate([2, 3, 4]) - Traceback (most recent call last): - ... - ValidationError: [2, 3, 4] is too long - - .. method:: evolve(**kwargs) - - Create a new validator like this one, but with given changes. - - Preserves all other attributes, so can be used to e.g. create a - validator with a different schema but with the same :validator:`$ref` - resolution behavior. - - >>> validator = Draft202012Validator({}) - >>> validator.evolve(schema={"type": "number"}) - Draft202012Validator(schema={'type': 'number'}, format_checker=None) - +.. autoclass:: jsonschema.protocols.Validator + :members: All of the `versioned validators <versioned-validators>` that are included with -`jsonschema` adhere to the interface, and implementers of validator classes +`jsonschema` adhere to the protocol, and implementers of validator classes that extend or complement the ones included should adhere to it as well. For more information see `creating-validators`. Type Checking ------------- -To handle JSON Schema's :validator:`type` property, a `IValidator` uses +To handle JSON Schema's :validator:`type` property, a `Validator` uses an associated `TypeChecker`. The type checker provides an immutable mapping between names of types and functions that can test if an instance is of that type. The defaults are suitable for most users - each of the @@ -216,7 +82,7 @@ given how common validating these types are. If you *do* want the generality, or just want to add a few specific additional types as being acceptable for a validator object, then you should update an existing `TypeChecker` or create a new one. You may then create a new -`IValidator` via `jsonschema.validators.extend`. +`Validator` via `jsonschema.validators.extend`. .. code-block:: python @@ -244,7 +110,7 @@ Versioned Validators `jsonschema` ships with validator classes for various versions of the JSON Schema specification. For details on the methods and attributes -that each validator class provides see the `IValidator` interface, +that each validator class provides see the `Validator` interface, which each included validator class implements. .. autoclass:: Draft202012Validator @@ -289,7 +155,7 @@ JSON Schema defines the :validator:`format` property which can be used to check if primitive types (``string``\s, ``number``\s, ``boolean``\s) conform to well-defined formats. By default, no validation is enforced, but optionally, validation can be enabled by hooking in a format-checking object into an -`IValidator`. +`Validator`. .. doctest:: diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py index c7fa76b..cd2102a 100644 --- a/jsonschema/__init__.py +++ b/jsonschema/__init__.py @@ -26,6 +26,7 @@ from jsonschema.exceptions import ( SchemaError, ValidationError, ) +from jsonschema.protocols import Validator from jsonschema.validators import ( Draft3Validator, Draft4Validator, diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 6ba2bad..f24e433 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -49,7 +49,7 @@ class TypeChecker(object): """ A ``type`` property checker. - A `TypeChecker` performs type checking for an `IValidator`. Type + A `TypeChecker` performs type checking for a `Validator`. 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. diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py new file mode 100644 index 0000000..1e2689b --- /dev/null +++ b/jsonschema/protocols.py @@ -0,0 +1,153 @@ +""" +typing.Protocol classes for jsonschema interfaces. +""" + +# for reference material on Protocols, see +# https://www.python.org/dev/peps/pep-0544/ + +from typing import Any, ClassVar, Iterator, Optional + +try: + from typing import Protocol, runtime_checkable +except ImportError: + from typing_extensions import Protocol, runtime_checkable + +from jsonschema._format import FormatChecker + +from ._types import TypeChecker +from .exceptions import ValidationError +from .validators import RefResolver + +# For code authors working on the validator protocol, these are the three +# use-cases which should be kept in mind: +# +# 1. As a protocol class, it can be used in type annotations to describe the +# available methods and attributes of a validator +# 2. It is the source of autodoc for the validator documentation +# 3. It is runtime_checkable, meaning that it can be used in isinstance() +# checks. +# +# Since protocols are not base classes, isinstance() checking is limited in +# its capabilities. See docs on runtime_checkable for detail + + +@runtime_checkable +class Validator(Protocol): + """ + The protocol to which all validator classes should adhere. + + :argument dict schema: the schema that the validator object + will validate with. It is assumed to be valid, and providing + an invalid schema can lead to undefined behavior. See + `Validator.check_schema` to validate a schema first. + :argument resolver: an instance of `jsonschema.RefResolver` that will be + used to resolve :validator:`$ref` properties (JSON references). If + unprovided, one will be created. + :argument format_checker: an instance of `jsonschema.FormatChecker` + whose `jsonschema.FormatChecker.conforms` method will be called to + check and see if instances conform to each :validator:`format` + property present in the schema. If unprovided, no validation + will be done for :validator:`format`. Certain formats require + additional packages to be installed (ipv5, uri, color, date-time). + The required packages can be found at the bottom of this page. + """ + + #: An object representing the validator's meta schema (the schema that + #: describes valid schemas in the given version). + META_SCHEMA: ClassVar[dict] + + #: A mapping of validator names (`str`\s) to functions + #: that validate the validator property with that name. For more + #: information see `creating-validators`. + VALIDATORS: ClassVar[dict] + + #: A `jsonschema.TypeChecker` that will be used when validating + #: :validator:`type` properties in JSON schemas. + TYPE_CHECKER: ClassVar[TypeChecker] + + #: The schema that was passed in when initializing the object. + schema: dict + + def __init__( + self, + schema: dict, + resolver: Optional[RefResolver] = None, + format_checker: Optional[FormatChecker] = None, + ) -> None: + ... + + @classmethod + def check_schema(cls, schema: dict) -> None: + """ + Validate the given schema against the validator's `META_SCHEMA`. + + :raises: `jsonschema.exceptions.SchemaError` if the schema + is invalid + """ + + def is_type(self, instance: Any, type: str) -> bool: + """ + Check if the instance is of the given (JSON Schema) type. + + :type type: str + :rtype: bool + :raises: `jsonschema.exceptions.UnknownType` if ``type`` + is not a known type. + """ + + def is_valid(self, instance: dict) -> bool: + """ + Check if the instance is valid under the current `schema`. + + :rtype: bool + + >>> schema = {"maxItems" : 2} + >>> Draft3Validator(schema).is_valid([2, 3, 4]) + False + """ + + def iter_errors(self, instance: dict) -> Iterator[ValidationError]: + r""" + Lazily yield each of the validation errors in the given instance. + + :rtype: an `collections.abc.Iterable` of + `jsonschema.exceptions.ValidationError`\s + + >>> schema = { + ... "type" : "array", + ... "items" : {"enum" : [1, 2, 3]}, + ... "maxItems" : 2, + ... } + >>> v = Draft3Validator(schema) + >>> for error in sorted(v.iter_errors([2, 3, 4]), key=str): + ... print(error.message) + 4 is not one of [1, 2, 3] + [2, 3, 4] is too long + """ + + def validate(self, instance: dict) -> None: + """ + Check if the instance is valid under the current `schema`. + + :raises: `jsonschema.exceptions.ValidationError` if the + instance is invalid + + >>> schema = {"maxItems" : 2} + >>> Draft3Validator(schema).validate([2, 3, 4]) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + """ + + def evolve(self, **kwargs) -> "Validator": + """ + Create a new validator like this one, but with given changes. + + Preserves all other attributes, so can be used to e.g. create a + validator with a different schema but with the same :validator:`$ref` + resolution behavior. + + >>> validator = Draft202012Validator({}) + >>> validator.evolve(schema={"type": "number"}) + Draft202012Validator(schema={'type': 'number'}, format_checker=None) + """ diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 429f868..9e52ce9 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -13,7 +13,13 @@ import warnings import attr -from jsonschema import FormatChecker, TypeChecker, exceptions, validators +from jsonschema import ( + FormatChecker, + TypeChecker, + exceptions, + protocols, + validators, +) from jsonschema.tests._helpers import bug @@ -2128,6 +2134,21 @@ class TestRefResolver(TestCase): self.assertIn("Failed to pop the scope", str(exc.exception)) +class TestValidatorProtocol(TestCase): + def test_each_validator_is_instance_of_protocol(self): + schema = {} + for validator_cls in [ + validators.Draft3Validator, + validators.Draft4Validator, + validators.Draft6Validator, + validators.Draft7Validator, + validators.Draft201909Validator, + validators.Draft202012Validator, + ]: + validator = validator_cls(schema) + assert isinstance(validator, protocols.Validator) + + def sorted_errors(errors): def key(error): return ( diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 4ab101c..e037c4b 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -155,7 +155,7 @@ def create( Returns: - a new `jsonschema.IValidator` class + a new `jsonschema.protocols.Validator` class """ @attr.s @@ -284,7 +284,7 @@ def extend(validator, validators=(), version=None, type_checker=None): Arguments: - validator (jsonschema.IValidator): + validator (jsonschema.protocols.Validator): an existing validator class @@ -314,11 +314,12 @@ def extend(validator, validators=(), version=None, type_checker=None): a type checker, used when applying the :validator:`type` validator. If unprovided, the type checker of the extended - `jsonschema.IValidator` will be carried along. + `jsonschema.protocols.Validator` will be carried along. Returns: - a new `jsonschema.IValidator` class extending the one provided + a new `jsonschema.protocols.Validator` class extending the one + provided .. note:: Meta Schemas @@ -916,7 +917,7 @@ def validate(instance, schema, cls=None, *args, **kwargs): 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 + likely would prefer using the `Validator.validate` method directly on a specific validator (e.g. ``Draft7Validator.validate``). @@ -930,7 +931,7 @@ def validate(instance, schema, cls=None, *args, **kwargs): The schema to validate with - cls (IValidator): + cls (Validator): The class that will be used to validate the instance. @@ -32,6 +32,7 @@ install_requires = importlib_metadata;python_version<'3.8' importlib_resources>=1.4.0;python_version<'3.9' pyrsistent>=0.14.0,!=0.17.0,!=0.17.1,!=0.17.2 + typing_extensions;python_version<'3.8' [options.extras_require] format = |