diff --git a/docs/creating.rst b/docs/creating.rst index 889d1c3e7..bf49f771c 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:: 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 + :class:`jsonschema.TypeChecker`. + + :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 @@ -59,6 +65,9 @@ Creating or Extending Validator Classes :argument str version: a version for the new validator class + :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 .. note:: Meta Schemas diff --git a/docs/validate.rst b/docs/validate.rst index 160e637e2..cded8fb4b 100644 --- a/docs/validate.rst +++ b/docs/validate.rst @@ -32,10 +32,14 @@ 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:: 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 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 +52,13 @@ 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:: 2.7.0 + Use of this attribute is deprecated in favour of the the new type + checkers. + + 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 @@ -62,6 +71,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. @@ -127,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: @@ -134,10 +168,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 +183,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(checker, 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/__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..12d4528e4 --- /dev/null +++ b/jsonschema/_types.py @@ -0,0 +1,205 @@ +import numbers + +import attr +import pyrsistent + +from jsonschema.compat import str_types, int_types, iteritems +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 :class:`TypeChecker` performs type checking for an instance of + :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 + :class:`TypeChecker` object. + + Arguments: + + type_checkers (dict): + + The initial mapping of types to their checking functions. + """ + _type_checkers = attr.ib(default={}, convert=pyrsistent.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: + + :exc:`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): + """ + 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 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 :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) + type_checkers = self._type_checkers.update(definitions) + return attr.evolve(self, type_checkers=type_checkers) + + 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:`jsonschema.exceptions.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:`jsonschema.exceptions.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(type_) + + return attr.evolve(self, type_checkers=evolver.persistent()) + + +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") diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 4ab57979f..d10f246cd 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -140,6 +140,20 @@ class RefResolutionError(Exception): pass +class UndefinedTypeCheck(Exception): + 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): 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..c1a5a486b --- /dev/null +++ b/jsonschema/tests/test_types.py @@ -0,0 +1,219 @@ +""" +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(checker, instance): + if Draft4Validator.TYPE_CHECKER.is_type(instance, "integer"): + return True + + if 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(checker, 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_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) + 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) + + 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): + + 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..fdd1b713a 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,12 @@ 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) + + # 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) @@ -73,6 +81,150 @@ 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 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): diff --git a/jsonschema/validators.py b/jsonschema/validators.py index d074e1b1e..14a85855d 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -3,18 +3,23 @@ import contextlib import json import numbers +from warnings import warn + +from six import add_metaclass 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,7 +61,59 @@ def _validates(cls): return _validates -def create(meta_schema, validators=(), version=None, default_types=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(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 + +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=None, + type_checker=None): + + 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, @@ -64,16 +121,31 @@ def create(meta_schema, validators=(), version=None, default_types=None): u"string": str_types, } + if type_checker is None: + type_checker = _types.TypeChecker() + + 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, - ): - 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,18 +207,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) and - issubclass(int, pytypes) and - bool not in _utils.flatten(pytypes) - ): - return False - return isinstance(instance, pytypes) def is_valid(self, instance, _schema=None): error = next(self.iter_errors(instance, _schema), None) @@ -159,16 +223,25 @@ 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) - return create( + + if not type_checker: + type_checker = validator.TYPE_CHECKER + + # 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"), @@ -196,6 +269,7 @@ def extend(validator, validators, version=None): u"type": _validators.type_draft3, u"uniqueItems": _validators.uniqueItems, }, + type_checker=_types.draft3_type_checker, version="draft3", ) @@ -229,6 +303,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..b796ba486 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", "six>=1.11.0"], setup_requires=["vcversioner>=2.16.0.0"], extras_require=extras_require, author="Julian Berman",