From 644259b1ebf862baeedd88871330cbee648ac0e3 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 27 Nov 2017 09:28:15 +0000 Subject: [PATCH 1/9] The new type interface: a TypeChecker class The TypeChecker provides an immutable interface to a mapping of type names to functions that check a value. Includes test coverage for the new TypeChecker, including demonstrations that it is sufficient to extend types. The old type interface is still supported, but will raise a DeprecationWarning. The old isinstance checks are converted to functions & passed to the TypeChecker. The old types are also checked with the JSON schema test suite 'type' tests. --- jsonschema/__init__.py | 5 + jsonschema/_types.py | 203 ++++++++++++++++++ jsonschema/compat.py | 2 +- jsonschema/exceptions.py | 3 + .../tests/test_jsonschema_test_suite.py | 17 ++ jsonschema/tests/test_types.py | 189 ++++++++++++++++ jsonschema/tests/test_validators.py | 73 ++++++- jsonschema/validators.py | 100 +++++++-- setup.py | 1 + 9 files changed, 566 insertions(+), 27 deletions(-) create mode 100644 jsonschema/_types.py create mode 100644 jsonschema/tests/test_types.py diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py index baf1d89b3..6955d278b 100644 --- a/jsonschema/__init__.py +++ b/jsonschema/__init__.py @@ -15,6 +15,11 @@ from jsonschema._format import ( FormatChecker, draft3_format_checker, draft4_format_checker, ) +from jsonschema._types import ( + TypeChecker, + draft3_type_checker, + draft4_type_checker, +) from jsonschema.validators import ( Draft3Validator, Draft4Validator, RefResolver, validate ) diff --git a/jsonschema/_types.py b/jsonschema/_types.py new file mode 100644 index 000000000..26c8aa97f --- /dev/null +++ b/jsonschema/_types.py @@ -0,0 +1,203 @@ +import numbers + +import attr +import pyrsistent + +from jsonschema.compat import str_types, int_types, iteritems +from jsonschema.exceptions import UndefinedTypeCheck + + +def is_array(instance): + return isinstance(instance, list) + + +def is_bool(instance): + return isinstance(instance, bool) + + +def is_integer(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(instance): + return instance is None + + +def is_number(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(instance): + return isinstance(instance, dict) + + +def is_string(instance): + return isinstance(instance, str_types) + + +def is_any(instance): + return True + + +@attr.s(frozen=True) +class TypeChecker(object): + """ + A ``type`` property checker. + + A :class:`TypeChecker` performs type checking for an instance of + :class:`Validator`. Type checks to perform are set using + :meth:`TypeChecker.redefine or :meth:`TypeChecker.redefine_many` and + removed via :meth:`TypeChecker.remove` or + :meth:`TypeChecker.remove_many`. Each of these return a new + :class:`TypeChecker` object. + + Arguments: + + None + """ + type_checkers = attr.ib(default=pyrsistent.pmap({})) + + def is_type(self, instance, type_): + """ + Check if the instance is of the appropriate type. + + Arguments: + + instance (any primitive type, i.e. str, number, bool): + + The instance to check + + type_ (str): + + The name of the type that is expected. + + Returns: + + bool: Whether it conformed. + + + Raises: + + :exc:`UndefinedTypeCheck` if type_ is unknown to this object. + + """ + try: + return self.type_checkers[type_](instance) + except KeyError: + raise UndefinedTypeCheck + + def redefine(self, type_, fn): + """ + Redefine the checker for type_ to the function fn. + + Arguments: + + type_ (str): + + The name of the type to check. + + fn (callable): + + A function taking exactly one parameter, instance, + that checks if instance is of this type. + + Returns: + + A new :class:`TypeChecker` instance. + + """ + return self.redefine_many({type_:fn}) + + def redefine_many(self, definitions=()): + """ + Redefine multiple type checkers. + + Arguments: + + definitions (dict): + + A dictionary mapping types to their checking functions. + + Returns: + + A new :class:`TypeChecker` instance. + + """ + definitions = dict(definitions) + evolver = self.type_checkers.evolver() + + for type_, checker in iteritems(definitions): + evolver[type_] = checker + + return attr.evolve(self, type_checkers=evolver.persistent()) + + def remove(self, type_): + """ + Remove the type from the checkers that this object understands. + + Arguments: + + type_ (str): + + The name of the type to remove. + + Returns: + + A new :class:`TypeChecker` instance + + Raises: + + :exc:`UndefinedTypeCheck` if type_ is unknown to this object + + """ + return self.remove_many((type_,)) + + def remove_many(self, types): + """ + Remove multiple types from the checkers that this object understands. + + Arguments: + + types (iterable): + + An iterable of types to remove. + + Returns: + + A new :class:`TypeChecker` instance + + Raises: + + :exc:`UndefinedTypeCheck` if any of the types are unknown to + this object + + """ + evolver = self.type_checkers.evolver() + + for type_ in types: + try: + del evolver[type_] + except KeyError: + raise UndefinedTypeCheck + + return attr.evolve(self, type_checkers=evolver.persistent()) + + +draft3_type_checker = TypeChecker().redefine_many({ + 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") diff --git a/jsonschema/compat.py b/jsonschema/compat.py index ff91fe620..2dd607d95 100644 --- a/jsonschema/compat.py +++ b/jsonschema/compat.py @@ -28,7 +28,7 @@ ) from urllib import unquote # noqa from urllib2 import urlopen # noqa - str_types = basestring + str_types = basestring, int_types = int, long iteritems = operator.methodcaller("iteritems") diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 4ab57979f..c6ef8c13d 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -140,6 +140,9 @@ class RefResolutionError(Exception): pass +class UndefinedTypeCheck(Exception): + pass + class UnknownType(Exception): def __init__(self, type, instance, schema): self.type = type diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index c86ea60ca..3dfe57f94 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -19,6 +19,7 @@ from jsonschema.compat import PY3 from jsonschema.tests.compat import mock from jsonschema.tests._suite import Suite +from jsonschema.validators import create SUITE = Suite() @@ -232,3 +233,19 @@ class Draft3RemoteResolution(unittest.TestCase): ) class Draft4RemoteResolution(unittest.TestCase): validator_class = Draft4Validator + + +@load_json_cases(tests=DRAFT3.tests_of(name="type")) +class TestDraft3LegacyTypeCheck(unittest.TestCase): + Validator = create(meta_schema=Draft3Validator.META_SCHEMA, + validators=Draft3Validator.VALIDATORS, + type_checker=None) + validator_class = Validator + + +@load_json_cases(tests=DRAFT4.tests_of(name="type")) +class TestDraft4LegacyTypeCheck(unittest.TestCase): + Validator = create(meta_schema=Draft4Validator.META_SCHEMA, + validators=Draft4Validator.VALIDATORS, + type_checker=None) + validator_class = Validator diff --git a/jsonschema/tests/test_types.py b/jsonschema/tests/test_types.py new file mode 100644 index 000000000..91c71deb2 --- /dev/null +++ b/jsonschema/tests/test_types.py @@ -0,0 +1,189 @@ +""" +Tests on the new type interface. The actual correctness of the type checking +is handled in test_jsonschema_test_suite; these tests check that TypeChecker +functions correctly and can facilitate extensions to type checking +""" +from collections import namedtuple +from unittest import TestCase + +from jsonschema import _types, ValidationError, _validators +from jsonschema.exceptions import UndefinedTypeCheck +from jsonschema.validators import Draft4Validator, extend + + +def is_int_or_string_int(instance): + if Draft4Validator.TYPE_CHECKER.is_type(instance, "integer"): + return True + + if Draft4Validator.TYPE_CHECKER.is_type(instance, "string"): + try: + int(instance) + return True + except ValueError: + pass + return False + + +def is_namedtuple(instance): + if isinstance(instance, tuple) and getattr(instance, '_fields', + None): + return True + + return False + + +def is_object_or_named_tuple(instance): + if Draft4Validator.TYPE_CHECKER.is_type(instance, "object"): + return True + + if is_namedtuple(instance): + return True + + return False + + +def coerce_named_tuple(fn): + def coerced(validator, value, instance, schema): + if is_namedtuple(instance): + instance = instance._asdict() + return fn(validator, value, instance, schema) + return coerced + +required = coerce_named_tuple(_validators.required_draft4) +properties = coerce_named_tuple(_validators.properties_draft4) + + +class TestTypeChecker(TestCase): + + def test_initialised_empty(self): + tc = _types.TypeChecker() + self.assertEqual(len(tc.type_checkers), 0) + + def test_checks_can_be_added(self): + tc = _types.TypeChecker() + tc = tc.redefine("integer", _types.is_integer) + self.assertEqual(len(tc.type_checkers), 1) + + def test_added_checks_are_accessible(self): + tc = _types.TypeChecker() + tc = tc.redefine("integer", _types.is_integer) + + self.assertTrue(tc.is_type(4, "integer")) + self.assertFalse(tc.is_type(4.4, "integer")) + + def test_checks_can_be_redefined(self): + tc = _types.TypeChecker() + tc = tc.redefine("integer", _types.is_integer) + self.assertEqual(tc.type_checkers["integer"], _types.is_integer) + tc = tc.redefine("integer", _types.is_string) + self.assertEqual(tc.type_checkers["integer"], _types.is_string) + + def test_checks_can_be_removed(self): + tc = _types.TypeChecker() + tc = tc.redefine("integer", _types.is_integer) + tc = tc.remove("integer") + + with self.assertRaises(UndefinedTypeCheck): + tc.is_type(4, "integer") + + def test_changes_do_not_affect_original(self): + tc = _types.TypeChecker() + tc2 = tc.redefine("integer", _types.is_integer) + self.assertEqual(len(tc.type_checkers), 0) + + tc3 = tc2.remove("integer") + self.assertEqual(len(tc2.type_checkers), 1) + + def test_many_checks_can_be_added(self): + tc = _types.TypeChecker() + tc = tc.redefine_many({ + "integer": _types.is_integer, + "string": _types.is_string + }) + + self.assertEqual(len(tc.type_checkers), 2) + + def test_many_checks_can_be_removed(self): + tc = _types.TypeChecker() + tc = tc.redefine_many({ + "integer": _types.is_integer, + "string": _types.is_string + }) + + tc = tc.remove_many(("integer", "string")) + + self.assertEqual(len(tc.type_checkers), 0) + + +class TestCustomTypes(TestCase): + + def test_simple_type_can_be_extended(self): + schema = {'type': 'integer'} + + type_checker = Draft4Validator.TYPE_CHECKER.redefine( + "integer", is_int_or_string_int + ) + + CustomValidator = extend(Draft4Validator, type_checker=type_checker) + v = CustomValidator(schema) + + v.validate(4) + v.validate('4') + + with self.assertRaises(ValidationError): + v.validate(4.4) + + def test_object_can_be_extended(self): + schema = {'type': 'object'} + + Point = namedtuple('Point', ['x', 'y']) + + type_checker = Draft4Validator.TYPE_CHECKER.redefine( + u"object", is_object_or_named_tuple + ) + + CustomValidator = extend(Draft4Validator, type_checker=type_checker) + v = CustomValidator(schema) + + v.validate(Point(x=4, y=5)) + + def test_object_extensions_require_custom_validators(self): + schema = {'type': 'object', 'required': ['x']} + + type_checker = Draft4Validator.TYPE_CHECKER.redefine( + u"object", is_object_or_named_tuple + ) + + CustomValidator = extend(Draft4Validator, type_checker=type_checker) + v = CustomValidator(schema) + + Point = namedtuple('Point', ['x', 'y']) + # Cannot handle required + with self.assertRaises(ValidationError): + v.validate(Point(x=4, y=5)) + + def test_object_extensions_can_handle_custom_validators(self): + schema = {'type': 'object', + 'required': ['x'], + 'properties': {'x': + {'type': 'integer'} + } + } + + type_checker = Draft4Validator.TYPE_CHECKER.redefine( + u"object", is_object_or_named_tuple + ) + + CustomValidator = extend(Draft4Validator, + type_checker=type_checker, + validators={"required": required, + 'properties': properties}) + + v = CustomValidator(schema) + + Point = namedtuple('Point', ['x', 'y']) + # Can now process required and properties + v.validate(Point(x=4, y=5)) + + with self.assertRaises(ValidationError): + v.validate(Point(x="not an integer", y=5)) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 2df497148..fafcf2905 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -3,11 +3,14 @@ from unittest import TestCase import json -from jsonschema import FormatChecker, SchemaError, ValidationError +from jsonschema import ( + FormatChecker, SchemaError, ValidationError, TypeChecker, _types +) from jsonschema.tests.compat import mock from jsonschema.validators import ( RefResolutionError, UnknownType, Draft3Validator, Draft4Validator, RefResolver, create, extend, validator_for, validate, + _generate_legacy_type_checks ) @@ -16,11 +19,11 @@ 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.type_checker = TypeChecker() self.Validator = create( meta_schema=self.meta_schema, validators=self.validators, - default_types=self.types, + type_checker=self.type_checker ) self.validator_value = 12 @@ -30,7 +33,8 @@ def setUp(self): 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) + self.assertEqual(self.Validator.TYPE_CHECKER, self.type_checker) + self.assertEqual(self.Validator.DEFAULT_TYPES, {}) def test_init(self): self.assertEqual(self.validator.schema, self.schema) @@ -73,6 +77,67 @@ def test_extend(self): self.assertEqual(Extended.META_SCHEMA, self.Validator.META_SCHEMA) self.assertEqual(Extended.DEFAULT_TYPES, self.Validator.DEFAULT_TYPES) + self.assertEqual(Extended.TYPE_CHECKER, self.Validator.TYPE_CHECKER) + + +class TestLegacyTypeCheckCreation(TestCase): + def setUp(self): + self.meta_schema = {u"properties": {u"smelly": {}}} + self.smelly = mock.MagicMock() + self.validators = {u"smelly": self.smelly} + + def test_empty_dict_is_default(self): + definitions = _generate_legacy_type_checks() + self.assertEqual(definitions, {}) + + def test_functions_are_created(self): + definitions = _generate_legacy_type_checks({"object": dict}) + self.assertTrue(callable(definitions["object"])) + + def test_default_types_used_if_no_type_checker_given(self): + Validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + ) + + expected_types = {u"array", u"boolean", u"integer", u"null", u"number", + u"object", u"string"} + + self.assertEqual(set(Validator.DEFAULT_TYPES), expected_types) + + self.assertEqual(set(Validator.TYPE_CHECKER.type_checkers), + expected_types) + + def test_default_types_update_type_checker(self): + tc = TypeChecker() + tc = tc.redefine(u"integer", _types.is_integer) + Validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=tc, + default_types={u"array": list} + ) + + self.assertEqual(set(Validator.DEFAULT_TYPES), {u"array"}) + + self.assertEqual(set(Validator.TYPE_CHECKER.type_checkers), + {u"array", u"integer"}) + + def test_types_update_type_checker(self): + tc = TypeChecker() + tc = tc.redefine(u"integer", _types.is_integer) + Validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=tc, + ) + + v = Validator({}) + self.assertEqual(set(v.type_checker.type_checkers), {u"integer"}) + + v = Validator({}, types={u"array": list}) + self.assertEqual(set(v.type_checker.type_checkers), {u"integer", + u"array"}) class TestIterErrors(TestCase): diff --git a/jsonschema/validators.py b/jsonschema/validators.py index c8148fe3f..fb375227b 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -3,18 +3,21 @@ import contextlib import json import numbers +from warnings import warn try: import requests except ImportError: requests = None -from jsonschema import _utils, _validators +from jsonschema import _utils, _validators, _types from jsonschema.compat import ( Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen, str_types, int_types, iteritems, lru_cache, ) -from jsonschema.exceptions import RefResolutionError, SchemaError, UnknownType +from jsonschema.exceptions import ( + RefResolutionError, SchemaError, UnknownType, UndefinedTypeCheck +) # Sigh. https://gitlab.com/pycqa/flake8/issues/280 # https://github.com/pyga/ebb-lint/issues/7 @@ -56,24 +59,80 @@ def _validates(cls): return _validates -def create(meta_schema, validators=(), version=None, default_types=None): - if default_types is None: +def _generate_legacy_type_checks(types=()): + """ + Generate type check definitions suitable for TypeChecker.redefine_many, + using the supplied types. Type Checks are simple isinstance checks, + except checking that numbers aren't really bools. + + 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(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 + + +def create(meta_schema, validators=(), version=None, default_types=(), + type_checker=None): + + if not default_types and not type_checker: + warn("default_types is deprecated, use type_checker", + DeprecationWarning) 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, } + if type_checker is None: + type_checker = _types.TypeChecker() + + type_checker = type_checker.redefine_many( + _generate_legacy_type_checks(default_types)) + + class Validator(object): VALIDATORS = dict(validators) META_SCHEMA = dict(meta_schema) DEFAULT_TYPES = dict(default_types) + TYPE_CHECKER = type_checker + def __init__( - self, schema, types=(), resolver=None, format_checker=None, - ): - self._types = dict(self.DEFAULT_TYPES) - self._types.update(types) + self, schema, types=(), resolver=None, format_checker=None): + + if types: + warn("The use of types is deprecated, use type_checker in " + "create", + DeprecationWarning) + + self.type_checker = self.TYPE_CHECKER.redefine_many( + _generate_legacy_type_checks(types)) + if resolver is None: resolver = RefResolver.from_schema(schema) @@ -135,19 +194,10 @@ def validate(self, *args, **kwargs): raise error def is_type(self, instance, type): - if type not in self._types: + try: + return self.type_checker.is_type(instance, type) + except UndefinedTypeCheck: 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) def is_valid(self, instance, _schema=None): error = next(self.iter_errors(instance, _schema), None) @@ -160,17 +210,21 @@ def is_valid(self, instance, _schema=None): return Validator -def extend(validator, validators, version=None): +def extend(validator, validators=(), version=None, type_checker=None): all_validators = dict(validator.VALIDATORS) all_validators.update(validators) + + if not type_checker: + type_checker = validator.TYPE_CHECKER + return create( meta_schema=validator.META_SCHEMA, validators=all_validators, version=version, default_types=validator.DEFAULT_TYPES, + type_checker=type_checker ) - Draft3Validator = create( meta_schema=_utils.load_schema("draft3"), validators={ @@ -197,6 +251,7 @@ def extend(validator, validators, version=None): u"type": _validators.type_draft3, u"uniqueItems": _validators.uniqueItems, }, + type_checker=_types.draft3_type_checker, version="draft3", ) @@ -230,6 +285,7 @@ def extend(validator, validators, version=None): u"type": _validators.type_draft4, u"uniqueItems": _validators.uniqueItems, }, + type_checker=_types.draft4_type_checker, version="draft4", ) diff --git a/setup.py b/setup.py index 2a912c606..8b2ce9092 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ name="jsonschema", packages=["jsonschema", "jsonschema.tests"], package_data={"jsonschema": ["schemas/*.json"]}, + install_requires=["attrs>=17.3.0", "pyrsistent>=0.14.0"], setup_requires=["vcversioner>=2.16.0.0"], extras_require=extras_require, author="Julian Berman", From 7c41d0ba8352dbe6b9a8ffabf9a0e8fadf9717d1 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 27 Nov 2017 20:30:05 +0000 Subject: [PATCH 2/9] Updated docs with new TypeChecker interface - Included description of the new TypeChecker class and autodocs, added new exception, marked old interface as deprecated, updated example on how to provide a custom type check. Couple of code changes from writing docs: - Renamed type_ -> type in TypeChecker method params as Sphinx isn't happy with these names - Marked the type_checkers attribute as private --- docs/creating.rst | 18 +++++++++--- docs/index.rst | 1 + docs/types.rst | 23 +++++++++++++++ docs/validate.rst | 45 ++++++++++++++++------------- jsonschema/_types.py | 45 +++++++++++++++-------------- jsonschema/tests/test_types.py | 16 +++++----- jsonschema/tests/test_validators.py | 8 ++--- 7 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 docs/types.rst diff --git a/docs/creating.rst b/docs/creating.rst index 889d1c3e7..0a65fd976 100644 --- a/docs/creating.rst +++ b/docs/creating.rst @@ -29,10 +29,16 @@ Creating or Extending Validator Classes will have :func:`validates` automatically called for the given version. - :argument dict default_types: a default mapping to use for instances - of the validator class when mapping between JSON types to Python - types. The default for this argument is probably fine. Instances - can still have their types customized on a per-instance basis. + :argument dict default_types: Deprecated. 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 TypeChecker + + :argument jsonschema.TypeChecker type_checker: an instance + of :class:`TypeChecker`, whose :meth:`is_type` will be called to + validate the :validator:`type` property If unprovided, a default + :class:`TypeChecker` will be created, with no support types. :returns: a new :class:`jsonschema.IValidator` class @@ -59,6 +65,10 @@ Creating or Extending Validator Classes :argument str version: a version for the new validator class + :argument jsonschema.TypeChecker type_checker: an instance + of :class:`TypeChecker`. If unprovided, the existing + :class:`TypeChecker` will be used. + :returns: a new :class:`jsonschema.IValidator` class .. note:: Meta Schemas diff --git a/docs/index.rst b/docs/index.rst index 93dc05801..e97278f62 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -47,6 +47,7 @@ Contents: references creating faq + types Indices and tables diff --git a/docs/types.rst b/docs/types.rst new file mode 100644 index 000000000..006060ea0 --- /dev/null +++ b/docs/types.rst @@ -0,0 +1,23 @@ +.. currentmodule:: jsonschema + +============= +Type Checking +============= + +Each :class:`IValidator` has an associated :class:`TypeChecker`. The +TypeChecker 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 predefined Validators (Draft3, Draft4) +has a :class:`TypeChecker` that can correctly handle that draft. + +See :ref:`validating-types` for an example of providing a custom type check. + +.. autoclass:: TypeChecker + :members: + +.. autoexception:: jsonschema.exceptions.UndefinedTypeCheck + + Raised when trying to remove a type check that is not known to this + TypeChecker. Internally this is also raised when calling + :meth:`TypeChecker.is_type`, but is caught and re-raised as a + :class:`jsonschema.exceptions.UnknownType` exception. diff --git a/docs/validate.rst b/docs/validate.rst index 160e637e2..83ee8dc1f 100644 --- a/docs/validate.rst +++ b/docs/validate.rst @@ -32,10 +32,12 @@ classes should adhere to. will validate with. It is assumed to be valid, and providing an invalid schema can lead to undefined behavior. See :meth:`IValidator.check_schema` to validate a schema first. - :argument types: Override or extend the list of known types when + :argument types: Deprecated. Instead, create a custom TypeChecker + and extend the validator. See :ref:`validating-types` for details. + + If used, this overrides or extends the list of known type when validating the :validator:`type` property. Should map strings (type names) to class objects that will be checked via :func:`isinstance`. - See :ref:`validating-types` for details. :type types: dict or iterable of 2-tuples :argument resolver: an instance of :class:`RefResolver` that will be used to resolve :validator:`$ref` properties (JSON references). If @@ -48,8 +50,10 @@ classes should adhere to. .. attribute:: DEFAULT_TYPES - The default mapping of JSON types to Python types used when validating - :validator:`type` properties in JSON schemas. + Deprecated. Under normal usage, this will be an empty dictionary. + + If set, it provides mappings of JSON types to Python types that will + be converted to functions and redefined in this object's TypeChecker .. attribute:: META_SCHEMA @@ -62,6 +66,10 @@ classes should adhere to. that validate the validator property with that name. For more information see :ref:`creating-validators`. + .. attribute:: TYPE_CHECKER + A :class:`TypeChecker` that can be used validating :validator:`type` + properties in JSON schemas. + .. attribute:: schema The schema that was passed in when initializing the object. @@ -134,10 +142,7 @@ Validating With Additional Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Occasionally it can be useful to provide additional or alternate types when -validating the JSON Schema's :validator:`type` property. Validators allow this -by taking a ``types`` argument on construction that specifies additional types, -or which can be used to specify a different set of Python types to map to a -given JSON type. +validating the JSON Schema's :validator:`type` property. :mod:`jsonschema` tries to strike a balance between performance in the common case and generality. For instance, JSON Schema defines a ``number`` type, which @@ -152,24 +157,24 @@ more general instance checks can introduce significant slowdown, especially 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, :class:`IValidator`\s have a -``types`` argument that can be used to provide additional or new types. +types as being acceptable for a validator object, then you should update an +existing :class:`TypeChecker` or create a new one. You may then create a new +:class:`IValidator` via :meth:`extend`. .. code-block:: python class MyInteger(object): - ... + pass + + def is_my_int(instance): + return Draft3Validator.TYPE_CHECKER.is_type(instance, "number") or \ + isinstance(instance, MyInteger) + + type_checker = Draft3Validator.TYPE_CHECKER.redefine("number", is_my_int) - Draft3Validator( - schema={"type" : "number"}, - types={"number" : (numbers.Number, MyInteger)}, - ) + CustomValidator = extend(Draft3Validator, type_checker=type_checker) + validator = CustomValidator(schema={"type" : "number"}) -The list of default Python types for each JSON type is available on each -validator object in the :attr:`IValidator.DEFAULT_TYPES` attribute. Note -that you need to specify all types to match if you override one of the -existing JSON types, so you may want to access the set of default types -when specifying your additional type. .. _versioned-validators: diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 26c8aa97f..989c1dfa2 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -52,18 +52,21 @@ class TypeChecker(object): A :class:`TypeChecker` performs type checking for an instance of :class:`Validator`. Type checks to perform are set using - :meth:`TypeChecker.redefine or :meth:`TypeChecker.redefine_many` and + :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` and removed via :meth:`TypeChecker.remove` or :meth:`TypeChecker.remove_many`. Each of these return a new :class:`TypeChecker` object. Arguments: - None + type_checkers (pyrsistent.pmap): + + It is recommend to set type checkers through + :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` """ - type_checkers = attr.ib(default=pyrsistent.pmap({})) + _type_checkers = attr.ib(default=pyrsistent.pmap({})) - def is_type(self, instance, type_): + def is_type(self, instance, type): """ Check if the instance is of the appropriate type. @@ -73,7 +76,7 @@ def is_type(self, instance, type_): The instance to check - type_ (str): + type (str): The name of the type that is expected. @@ -84,21 +87,21 @@ def is_type(self, instance, type_): Raises: - :exc:`UndefinedTypeCheck` if type_ is unknown to this object. - + :exc:`jsonschema.exceptions.UndefinedTypeCheck`: + if type is unknown to this object. """ try: - return self.type_checkers[type_](instance) + return self._type_checkers[type](instance) except KeyError: raise UndefinedTypeCheck - def redefine(self, type_, fn): + def redefine(self, type, fn): """ - Redefine the checker for type_ to the function fn. + Redefine the checker for type to the function fn. Arguments: - type_ (str): + type (str): The name of the type to check. @@ -112,7 +115,7 @@ def redefine(self, type_, fn): A new :class:`TypeChecker` instance. """ - return self.redefine_many({type_:fn}) + return self.redefine_many({type:fn}) def redefine_many(self, definitions=()): """ @@ -130,20 +133,20 @@ def redefine_many(self, definitions=()): """ definitions = dict(definitions) - evolver = self.type_checkers.evolver() + evolver = self._type_checkers.evolver() for type_, checker in iteritems(definitions): evolver[type_] = checker return attr.evolve(self, type_checkers=evolver.persistent()) - def remove(self, type_): + def remove(self, type): """ Remove the type from the checkers that this object understands. Arguments: - type_ (str): + type (str): The name of the type to remove. @@ -153,10 +156,11 @@ def remove(self, type_): Raises: - :exc:`UndefinedTypeCheck` if type_ is unknown to this object + :exc:`jsonschema.exceptions.UndefinedTypeCheck`: + if type is unknown to this object """ - return self.remove_many((type_,)) + return self.remove_many((type,)) def remove_many(self, types): """ @@ -174,11 +178,10 @@ def remove_many(self, types): Raises: - :exc:`UndefinedTypeCheck` if any of the types are unknown to - this object - + :exc:`jsonschema.exceptions.UndefinedTypeCheck`: + if any of the types are unknown to this object """ - evolver = self.type_checkers.evolver() + evolver = self._type_checkers.evolver() for type_ in types: try: diff --git a/jsonschema/tests/test_types.py b/jsonschema/tests/test_types.py index 91c71deb2..a0e4b766b 100644 --- a/jsonschema/tests/test_types.py +++ b/jsonschema/tests/test_types.py @@ -57,12 +57,12 @@ class TestTypeChecker(TestCase): def test_initialised_empty(self): tc = _types.TypeChecker() - self.assertEqual(len(tc.type_checkers), 0) + self.assertEqual(len(tc._type_checkers), 0) def test_checks_can_be_added(self): tc = _types.TypeChecker() tc = tc.redefine("integer", _types.is_integer) - self.assertEqual(len(tc.type_checkers), 1) + self.assertEqual(len(tc._type_checkers), 1) def test_added_checks_are_accessible(self): tc = _types.TypeChecker() @@ -74,9 +74,9 @@ def test_added_checks_are_accessible(self): def test_checks_can_be_redefined(self): tc = _types.TypeChecker() tc = tc.redefine("integer", _types.is_integer) - self.assertEqual(tc.type_checkers["integer"], _types.is_integer) + self.assertEqual(tc._type_checkers["integer"], _types.is_integer) tc = tc.redefine("integer", _types.is_string) - self.assertEqual(tc.type_checkers["integer"], _types.is_string) + self.assertEqual(tc._type_checkers["integer"], _types.is_string) def test_checks_can_be_removed(self): tc = _types.TypeChecker() @@ -89,10 +89,10 @@ def test_checks_can_be_removed(self): def test_changes_do_not_affect_original(self): tc = _types.TypeChecker() tc2 = tc.redefine("integer", _types.is_integer) - self.assertEqual(len(tc.type_checkers), 0) + self.assertEqual(len(tc._type_checkers), 0) tc3 = tc2.remove("integer") - self.assertEqual(len(tc2.type_checkers), 1) + self.assertEqual(len(tc2._type_checkers), 1) def test_many_checks_can_be_added(self): tc = _types.TypeChecker() @@ -101,7 +101,7 @@ def test_many_checks_can_be_added(self): "string": _types.is_string }) - self.assertEqual(len(tc.type_checkers), 2) + self.assertEqual(len(tc._type_checkers), 2) def test_many_checks_can_be_removed(self): tc = _types.TypeChecker() @@ -112,7 +112,7 @@ def test_many_checks_can_be_removed(self): tc = tc.remove_many(("integer", "string")) - self.assertEqual(len(tc.type_checkers), 0) + self.assertEqual(len(tc._type_checkers), 0) class TestCustomTypes(TestCase): diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index fafcf2905..d2dd07503 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -105,7 +105,7 @@ def test_default_types_used_if_no_type_checker_given(self): self.assertEqual(set(Validator.DEFAULT_TYPES), expected_types) - self.assertEqual(set(Validator.TYPE_CHECKER.type_checkers), + self.assertEqual(set(Validator.TYPE_CHECKER._type_checkers), expected_types) def test_default_types_update_type_checker(self): @@ -120,7 +120,7 @@ def test_default_types_update_type_checker(self): self.assertEqual(set(Validator.DEFAULT_TYPES), {u"array"}) - self.assertEqual(set(Validator.TYPE_CHECKER.type_checkers), + self.assertEqual(set(Validator.TYPE_CHECKER._type_checkers), {u"array", u"integer"}) def test_types_update_type_checker(self): @@ -133,10 +133,10 @@ def test_types_update_type_checker(self): ) v = Validator({}) - self.assertEqual(set(v.type_checker.type_checkers), {u"integer"}) + self.assertEqual(set(v.type_checker._type_checkers), {u"integer"}) v = Validator({}, types={u"array": list}) - self.assertEqual(set(v.type_checker.type_checkers), {u"integer", + self.assertEqual(set(v.type_checker._type_checkers), {u"integer", u"array"}) From 9c1c265e80888f2d91b010ab25ad6796ad38fff3 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Wed, 29 Nov 2017 19:45:35 +0000 Subject: [PATCH 3/9] Allow _type_checkers to be def'd as a dict at init Converted to a pmap in attr's post init. --- jsonschema/_types.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 989c1dfa2..491a6bf9c 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -51,7 +51,7 @@ class TypeChecker(object): A ``type`` property checker. A :class:`TypeChecker` performs type checking for an instance of - :class:`Validator`. Type checks to perform are set using + :class:`Validator`. Type checks to perform are updated using :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` and removed via :meth:`TypeChecker.remove` or :meth:`TypeChecker.remove_many`. Each of these return a new @@ -59,12 +59,15 @@ class TypeChecker(object): Arguments: - type_checkers (pyrsistent.pmap): + type_checkers (dict): - It is recommend to set type checkers through - :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` + The initial mapping of types to their checking functions. """ - _type_checkers = attr.ib(default=pyrsistent.pmap({})) + _type_checkers = attr.ib(default=()) + + def __attrs_post_init__(self): + object.__setattr__(self, "_type_checkers", + pyrsistent.pmap(dict(self._type_checkers))) def is_type(self, instance, type): """ @@ -192,7 +195,7 @@ def remove_many(self, types): return attr.evolve(self, type_checkers=evolver.persistent()) -draft3_type_checker = TypeChecker().redefine_many({ +draft3_type_checker = TypeChecker({ u"any": is_any, u"array": is_array, u"boolean": is_bool, From 89a67390fab6626dac4facdeaac8e08998a68330 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 4 Dec 2017 21:49:50 +0000 Subject: [PATCH 4/9] Fix the default_types deprecation - Default_types is always available, in case of direct access. - Use a metaclass (provided by six for py2 & py3 compat) to set a classproperty on a Validator that raises a deprecation warning - Add test coverage to check deprecation warnings are raised when appropriate --- jsonschema/tests/test_validators.py | 89 ++++++++++++++++++++++++++++- jsonschema/validators.py | 36 +++++++++--- setup.py | 2 +- 3 files changed, 116 insertions(+), 11 deletions(-) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index d2dd07503..fdd1b713a 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -34,7 +34,11 @@ def test_attrs(self): self.assertEqual(self.Validator.VALIDATORS, self.validators) self.assertEqual(self.Validator.META_SCHEMA, self.meta_schema) self.assertEqual(self.Validator.TYPE_CHECKER, self.type_checker) - self.assertEqual(self.Validator.DEFAULT_TYPES, {}) + + # Default types should still be set to the old default if not provided + expected_types = {u"array", u"boolean", u"integer", u"null", u"number", + u"object", u"string"} + self.assertEqual(set(self.Validator.DEFAULT_TYPES), expected_types) def test_init(self): self.assertEqual(self.validator.schema, self.schema) @@ -140,6 +144,89 @@ def test_types_update_type_checker(self): u"array"}) +class TestLegacyTypeCheckingDeprecation(TestCase): + + def setUp(self): + self.meta_schema = {u"properties": {u"smelly": {}}} + self.smelly = mock.MagicMock() + self.validators = {u"smelly": self.smelly} + self.type_checker = TypeChecker() + + def test_default_usage_does_not_generate_warning(self): + with mock.patch("jsonschema.validators.warn") as mocked: + validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=self.type_checker + ) + + v = validator({}) + self.assertFalse(mocked.called) + + def test_create_with_custom_default_types_generates_warning(self): + with mock.patch("jsonschema.validators.warn") as mocked: + validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + default_types={"foo": object}, + type_checker=self.type_checker + ) + + v = validator({}) + self.assertEqual(mocked.call_count, 1) + self.assertEqual(mocked.call_args[0][1], DeprecationWarning) + + def test_extend_does_not_generate_warning(self): + with mock.patch("jsonschema.validators.warn") as mocked: + validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + default_types={"foo": object}, + type_checker=self.type_checker + ) + # The create should generate a warning, but not the extend + self.assertEqual(mocked.call_count, 1) + extended = extend(validator) + self.assertEqual(mocked.call_count, 1) + + def test_create_without_type_checker_generates_warning(self): + with mock.patch("jsonschema.validators.warn") as mocked: + # type_checker=None enforces use of default_types + validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=None + ) + + self.assertEqual(mocked.call_count, 1) + self.assertEqual(mocked.call_args[0][1], DeprecationWarning) + + def test_default_type_access_generates_warning(self): + with mock.patch("jsonschema.validators.warn") as mocked: + validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=self.type_checker + ) + self.assertEqual(mocked.call_count, 0) + + _ = validator.DEFAULT_TYPES + self.assertEqual(mocked.call_count, 1) + self.assertEqual(mocked.call_args[0][1], DeprecationWarning) + + def test_custom_types_generates_warning(self): + with mock.patch("jsonschema.validators.warn") as mocked: + validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=self.type_checker + ) + self.assertEqual(mocked.call_count, 0) + v = validator({}, types={"bar": object}) + self.assertEqual(mocked.call_count, 1) + self.assertEqual(mocked.call_args[0][1], DeprecationWarning) + + class TestIterErrors(TestCase): def setUp(self): self.validator = Draft3Validator({}) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index fb375227b..1f5035a94 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -5,6 +5,8 @@ import numbers from warnings import warn +from six import add_metaclass + try: import requests except ImportError: @@ -95,13 +97,24 @@ def type_check(instance): return definitions +class ValidatorMetaClass(type): + @property + def DEFAULT_TYPES(self): + warn("DEFAULT_TYPES is deprecated, use type_checker", + DeprecationWarning) + return self._DEFAULT_TYPES + -def create(meta_schema, validators=(), version=None, default_types=(), +def create(meta_schema, validators=(), version=None, default_types=None, type_checker=None): - if not default_types and not type_checker: + use_default_types=False + if default_types is not None or type_checker is None: + use_default_types = True warn("default_types is deprecated, use type_checker", DeprecationWarning) + + 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, @@ -111,17 +124,17 @@ def create(meta_schema, validators=(), version=None, default_types=(), if type_checker is None: type_checker = _types.TypeChecker() - type_checker = type_checker.redefine_many( - _generate_legacy_type_checks(default_types)) - + if use_default_types: + type_checker = type_checker.redefine_many( + _generate_legacy_type_checks(default_types)) + @add_metaclass(ValidatorMetaClass) class Validator(object): VALIDATORS = dict(validators) META_SCHEMA = dict(meta_schema) - DEFAULT_TYPES = dict(default_types) + _DEFAULT_TYPES = dict(default_types) TYPE_CHECKER = type_checker - def __init__( self, schema, types=(), resolver=None, format_checker=None): @@ -217,13 +230,18 @@ def extend(validator, validators=(), version=None, type_checker=None): if not type_checker: type_checker = validator.TYPE_CHECKER - return create( + # Set the default_types to None during class creation to avoid + # overwriting the type checker (and triggering the deprecation warning). + # Then set them directly + new_validator_cls = create( meta_schema=validator.META_SCHEMA, validators=all_validators, version=version, - default_types=validator.DEFAULT_TYPES, + default_types=None, type_checker=type_checker ) + new_validator_cls._DEFAULT_TYPES = validator._DEFAULT_TYPES + return new_validator_cls Draft3Validator = create( meta_schema=_utils.load_schema("draft3"), diff --git a/setup.py b/setup.py index 8b2ce9092..b796ba486 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ name="jsonschema", packages=["jsonschema", "jsonschema.tests"], package_data={"jsonschema": ["schemas/*.json"]}, - install_requires=["attrs>=17.3.0", "pyrsistent>=0.14.0"], + install_requires=["attrs>=17.3.0", "pyrsistent>=0.14.0", "six>=1.11.0"], setup_requires=["vcversioner>=2.16.0.0"], extras_require=extras_require, author="Julian Berman", From 9467cbf72073e44e7bb99c83c3087a3fe9534236 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 4 Dec 2017 21:51:16 +0000 Subject: [PATCH 5/9] Revert "Allow _type_checkers to be def'd as a dict at init" This reverts commit 9c1c265e80888f2d91b010ab25ad6796ad38fff3. --- jsonschema/_types.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 491a6bf9c..989c1dfa2 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -51,7 +51,7 @@ class TypeChecker(object): A ``type`` property checker. A :class:`TypeChecker` performs type checking for an instance of - :class:`Validator`. Type checks to perform are updated using + :class:`Validator`. Type checks to perform are set using :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` and removed via :meth:`TypeChecker.remove` or :meth:`TypeChecker.remove_many`. Each of these return a new @@ -59,15 +59,12 @@ class TypeChecker(object): Arguments: - type_checkers (dict): + type_checkers (pyrsistent.pmap): - The initial mapping of types to their checking functions. + It is recommend to set type checkers through + :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` """ - _type_checkers = attr.ib(default=()) - - def __attrs_post_init__(self): - object.__setattr__(self, "_type_checkers", - pyrsistent.pmap(dict(self._type_checkers))) + _type_checkers = attr.ib(default=pyrsistent.pmap({})) def is_type(self, instance, type): """ @@ -195,7 +192,7 @@ def remove_many(self, types): return attr.evolve(self, type_checkers=evolver.persistent()) -draft3_type_checker = TypeChecker({ +draft3_type_checker = TypeChecker().redefine_many({ u"any": is_any, u"array": is_array, u"boolean": is_bool, From 34227e88cc82adc060570ca7f80b0925f46640e4 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 4 Dec 2017 21:56:18 +0000 Subject: [PATCH 6/9] Use attr.ib's convert arg to generate the pmap - Cleaned than the post_init approach reverted in the previous commit --- jsonschema/_types.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 989c1dfa2..1a0a524a2 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -51,7 +51,7 @@ class TypeChecker(object): A ``type`` property checker. A :class:`TypeChecker` performs type checking for an instance of - :class:`Validator`. Type checks to perform are set using + :class:`Validator`. Type checks to perform are updated using :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` and removed via :meth:`TypeChecker.remove` or :meth:`TypeChecker.remove_many`. Each of these return a new @@ -59,12 +59,11 @@ class TypeChecker(object): Arguments: - type_checkers (pyrsistent.pmap): + type_checkers (dict): - It is recommend to set type checkers through - :meth:`TypeChecker.redefine` or :meth:`TypeChecker.redefine_many` + The initial mapping of types to their checking functions. """ - _type_checkers = attr.ib(default=pyrsistent.pmap({})) + _type_checkers = attr.ib(default={}, convert=pyrsistent.pmap) def is_type(self, instance, type): """ @@ -192,7 +191,7 @@ def remove_many(self, types): return attr.evolve(self, type_checkers=evolver.persistent()) -draft3_type_checker = TypeChecker().redefine_many({ +draft3_type_checker = TypeChecker({ u"any": is_any, u"array": is_array, u"boolean": is_bool, From 30bec1a0100d8d8788eef2eb723fab9f60a7d769 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 11 Dec 2017 13:48:31 +0000 Subject: [PATCH 7/9] Add checker instance to type checking functions Improves composability, esp for custom types. --- jsonschema/_types.py | 18 +++++++++--------- jsonschema/tests/test_types.py | 6 +++--- jsonschema/validators.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 1a0a524a2..2ddcf3d16 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -7,41 +7,41 @@ from jsonschema.exceptions import UndefinedTypeCheck -def is_array(instance): +def is_array(checker, instance): return isinstance(instance, list) -def is_bool(instance): +def is_bool(checker, instance): return isinstance(instance, bool) -def is_integer(instance): +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(instance): +def is_null(checker, instance): return instance is None -def is_number(instance): +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(instance): +def is_object(checker, instance): return isinstance(instance, dict) -def is_string(instance): +def is_string(checker, instance): return isinstance(instance, str_types) -def is_any(instance): +def is_any(checker, instance): return True @@ -90,7 +90,7 @@ def is_type(self, instance, type): if type is unknown to this object. """ try: - return self._type_checkers[type](instance) + return self._type_checkers[type](self, instance) except KeyError: raise UndefinedTypeCheck diff --git a/jsonschema/tests/test_types.py b/jsonschema/tests/test_types.py index a0e4b766b..b2747b3e2 100644 --- a/jsonschema/tests/test_types.py +++ b/jsonschema/tests/test_types.py @@ -11,11 +11,11 @@ from jsonschema.validators import Draft4Validator, extend -def is_int_or_string_int(instance): +def is_int_or_string_int(checker, instance): if Draft4Validator.TYPE_CHECKER.is_type(instance, "integer"): return True - if Draft4Validator.TYPE_CHECKER.is_type(instance, "string"): + if checker.is_type(instance, "string"): try: int(instance) return True @@ -32,7 +32,7 @@ def is_namedtuple(instance): return False -def is_object_or_named_tuple(instance): +def is_object_or_named_tuple(checker, instance): if Draft4Validator.TYPE_CHECKER.is_type(instance, "object"): return True diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 1f5035a94..14a85855d 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -83,7 +83,7 @@ def _generate_legacy_type_checks(types=()): def gen_type_check(pytypes): pytypes = _utils.flatten(pytypes) - def type_check(instance): + def type_check(checker, instance): if isinstance(instance, bool): if bool not in pytypes: return False From c202c48295b63b526f822efcbaf94a6dbf64b8a3 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 11 Dec 2017 14:38:42 +0000 Subject: [PATCH 8/9] Minor changes addressing PR review issues --- docs/creating.rst | 19 ++++++++------- docs/index.rst | 1 - docs/types.rst | 23 ------------------- docs/validate.rst | 42 +++++++++++++++++++++++++++------- jsonschema/_types.py | 16 ++++++++----- jsonschema/compat.py | 2 +- jsonschema/exceptions.py | 13 ++++++++++- jsonschema/tests/test_types.py | 30 ++++++++++++++++++++++++ 8 files changed, 96 insertions(+), 50 deletions(-) delete mode 100644 docs/types.rst diff --git a/docs/creating.rst b/docs/creating.rst index 0a65fd976..bf49f771c 100644 --- a/docs/creating.rst +++ b/docs/creating.rst @@ -29,16 +29,16 @@ Creating or Extending Validator Classes will have :func:`validates` automatically called for the given version. - :argument dict default_types: Deprecated. Please use the type_checker - argument instead. + :argument dict default_types: + .. deprecated:: 2.7.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 TypeChecker + be converted to functions and redefined in this object's + :class:`jsonschema.TypeChecker`. - :argument jsonschema.TypeChecker type_checker: an instance - of :class:`TypeChecker`, whose :meth:`is_type` will be called to - validate the :validator:`type` property If unprovided, a default - :class:`TypeChecker` will be created, with no support types. + :argument jsonschema.TypeChecker type_checker: a type checker. If + unprovided, a :class:`jsonschema.TypeChecker` will created with no + supported types. :returns: a new :class:`jsonschema.IValidator` class @@ -65,9 +65,8 @@ Creating or Extending Validator Classes :argument str version: a version for the new validator class - :argument jsonschema.TypeChecker type_checker: an instance - of :class:`TypeChecker`. If unprovided, the existing - :class:`TypeChecker` will be used. + :argument jsonschema.TypeChecker type_checker: a type checker. If + unprovided, the existing :class:`jsonschema.TypeChecker` will be used. :returns: a new :class:`jsonschema.IValidator` class diff --git a/docs/index.rst b/docs/index.rst index e97278f62..93dc05801 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -47,7 +47,6 @@ Contents: references creating faq - types Indices and tables diff --git a/docs/types.rst b/docs/types.rst deleted file mode 100644 index 006060ea0..000000000 --- a/docs/types.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. currentmodule:: jsonschema - -============= -Type Checking -============= - -Each :class:`IValidator` has an associated :class:`TypeChecker`. The -TypeChecker 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 predefined Validators (Draft3, Draft4) -has a :class:`TypeChecker` that can correctly handle that draft. - -See :ref:`validating-types` for an example of providing a custom type check. - -.. autoclass:: TypeChecker - :members: - -.. autoexception:: jsonschema.exceptions.UndefinedTypeCheck - - Raised when trying to remove a type check that is not known to this - TypeChecker. Internally this is also raised when calling - :meth:`TypeChecker.is_type`, but is caught and re-raised as a - :class:`jsonschema.exceptions.UnknownType` exception. diff --git a/docs/validate.rst b/docs/validate.rst index 83ee8dc1f..cded8fb4b 100644 --- a/docs/validate.rst +++ b/docs/validate.rst @@ -32,8 +32,10 @@ classes should adhere to. will validate with. It is assumed to be valid, and providing an invalid schema can lead to undefined behavior. See :meth:`IValidator.check_schema` to validate a schema first. - :argument types: Deprecated. Instead, create a custom TypeChecker - and extend the validator. See :ref:`validating-types` for details. + :argument types: + .. deprecated:: 2.7.0 + Instead, create a custom type checker and extend the validator. See + :ref:`validating-types` for details. If used, this overrides or extends the list of known type when validating the :validator:`type` property. Should map strings (type @@ -50,10 +52,13 @@ classes should adhere to. .. attribute:: DEFAULT_TYPES - Deprecated. Under normal usage, this will be an empty dictionary. + .. deprecated:: 2.7.0 + Use of this attribute is deprecated in favour of the the new type + checkers. - If set, it provides mappings of JSON types to Python types that will - be converted to functions and redefined in this object's TypeChecker + It provides mappings of JSON types to Python types that will + be converted to functions and redefined in this object's type checker + if one is not provided. .. attribute:: META_SCHEMA @@ -135,6 +140,27 @@ implementors of validator classes that extend or complement the ones included should adhere to it as well. For more information see :ref:`creating-validators`. +Type Checking +------------- + +To handle JSON Schema's :validator:`type` property, a :class:`IValidator` uses +an associated :class:`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 +predefined Validators (Draft3, Draft4) has a :class:`TypeChecker` that can +correctly handle that draft. + +See :ref:`validating-types` for an example of providing a custom type check. + +.. autoclass:: TypeChecker + :members: + +.. autoexception:: jsonschema.exceptions.UndefinedTypeCheck + + Raised when trying to remove a type check that is not known to this + TypeChecker. Internally this is also raised when calling + :meth:`TypeChecker.is_type`, but is caught and re-raised as a + :class:`jsonschema.exceptions.UnknownType` exception. .. _validating-types: @@ -166,9 +192,9 @@ existing :class:`TypeChecker` or create a new one. You may then create a new class MyInteger(object): pass - def is_my_int(instance): - return Draft3Validator.TYPE_CHECKER.is_type(instance, "number") or \ - isinstance(instance, MyInteger) + def is_my_int(checker, instance): + return (Draft3Validator.TYPE_CHECKER.is_type(instance, "number") or + isinstance(instance, MyInteger)) type_checker = Draft3Validator.TYPE_CHECKER.redefine("number", is_my_int) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 2ddcf3d16..ad75ff344 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -71,7 +71,7 @@ def is_type(self, instance, type): Arguments: - instance (any primitive type, i.e. str, number, bool): + instance (object): The instance to check @@ -90,9 +90,11 @@ def is_type(self, instance, type): if type is unknown to this object. """ try: - return self._type_checkers[type](self, instance) + fn = self._type_checkers[type] except KeyError: - raise UndefinedTypeCheck + raise UndefinedTypeCheck(type) + + return fn(self, instance) def redefine(self, type, fn): """ @@ -106,8 +108,10 @@ def redefine(self, type, fn): fn (callable): - A function taking exactly one parameter, instance, - that checks if instance is of this type. + 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: @@ -186,7 +190,7 @@ def remove_many(self, types): try: del evolver[type_] except KeyError: - raise UndefinedTypeCheck + raise UndefinedTypeCheck(type_) return attr.evolve(self, type_checkers=evolver.persistent()) diff --git a/jsonschema/compat.py b/jsonschema/compat.py index 2dd607d95..ff91fe620 100644 --- a/jsonschema/compat.py +++ b/jsonschema/compat.py @@ -28,7 +28,7 @@ ) from urllib import unquote # noqa from urllib2 import urlopen # noqa - str_types = basestring, + str_types = basestring int_types = int, long iteritems = operator.methodcaller("iteritems") diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index c6ef8c13d..d10f246cd 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -141,7 +141,18 @@ class RefResolutionError(Exception): class UndefinedTypeCheck(Exception): - pass + 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): def __init__(self, type, instance, schema): diff --git a/jsonschema/tests/test_types.py b/jsonschema/tests/test_types.py index b2747b3e2..c1a5a486b 100644 --- a/jsonschema/tests/test_types.py +++ b/jsonschema/tests/test_types.py @@ -59,6 +59,10 @@ def test_initialised_empty(self): tc = _types.TypeChecker() self.assertEqual(len(tc._type_checkers), 0) + def test_checks_can_be_added_at_init(self): + tc = _types.TypeChecker({"integer": _types.is_integer}) + self.assertEqual(len(tc._type_checkers), 1) + def test_checks_can_be_added(self): tc = _types.TypeChecker() tc = tc.redefine("integer", _types.is_integer) @@ -114,6 +118,32 @@ def test_many_checks_can_be_removed(self): self.assertEqual(len(tc._type_checkers), 0) + def test_unknown_type_raises_exception_on_is_type(self): + tc = _types.TypeChecker() + with self.assertRaises(UndefinedTypeCheck) as context: + tc.is_type(4, 'foobar') + + self.assertIn('foobar', str(context.exception)) + + def test_unknown_type_raises_exception_on_remove(self): + tc = _types.TypeChecker() + with self.assertRaises(UndefinedTypeCheck) as context: + tc.remove('foobar') + + self.assertIn('foobar', str(context.exception)) + + def test_type_check_can_raise_key_error(self): + def raises_keyerror(checker, instance): + raise KeyError("internal error") + + tc = _types.TypeChecker({"object": raises_keyerror}) + + with self.assertRaises(KeyError) as context: + tc.is_type(4, "object") + + self.assertNotIn("object", str(context.exception)) + self.assertIn("internal error", str(context.exception)) + class TestCustomTypes(TestCase): From e2264e56c7375c9ec5aeb1175773da4f0079f3c7 Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Mon, 11 Dec 2017 15:13:04 +0000 Subject: [PATCH 9/9] Simplify redefine_many. --- jsonschema/_types.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index ad75ff344..12d4528e4 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -136,12 +136,8 @@ def redefine_many(self, definitions=()): """ definitions = dict(definitions) - evolver = self._type_checkers.evolver() - - for type_, checker in iteritems(definitions): - evolver[type_] = checker - - return attr.evolve(self, type_checkers=evolver.persistent()) + type_checkers = self._type_checkers.update(definitions) + return attr.evolve(self, type_checkers=type_checkers) def remove(self, type): """