diff --git a/jsonschema/_keywords.py b/jsonschema/_keywords.py index f30f95419..6e1698a03 100644 --- a/jsonschema/_keywords.py +++ b/jsonschema/_keywords.py @@ -21,7 +21,10 @@ def patternProperties(validator, patternProperties, instance, schema): for k, v in instance.items(): if re.search(pattern, k): yield from validator.descend( - v, subschema, path=k, schema_path=pattern, + v, + subschema, + path=k, + schema_path=pattern, ) @@ -128,7 +131,7 @@ def exclusiveMinimum(validator, minimum, instance, schema): if not validator.is_type(instance, "number"): return - if instance <= minimum: + if not (instance > minimum): yield ValidationError( f"{instance!r} is less than or equal to " f"the minimum of {minimum!r}", @@ -139,7 +142,7 @@ def exclusiveMaximum(validator, maximum, instance, schema): if not validator.is_type(instance, "number"): return - if instance >= maximum: + if not (instance < maximum): yield ValidationError( f"{instance!r} is greater than or equal " f"to the maximum of {maximum!r}", @@ -150,7 +153,7 @@ def minimum(validator, minimum, instance, schema): if not validator.is_type(instance, "number"): return - if instance < minimum: + if not (instance >= minimum): message = f"{instance!r} is less than the minimum of {minimum!r}" yield ValidationError(message) @@ -159,7 +162,7 @@ def maximum(validator, maximum, instance, schema): if not validator.is_type(instance, "number"): return - if instance > maximum: + if not (instance <= maximum): message = f"{instance!r} is greater than the maximum of {maximum!r}" yield ValidationError(message) @@ -204,18 +207,13 @@ def maxItems(validator, mI, instance, schema): def uniqueItems(validator, uI, instance, schema): - if ( - uI - and validator.is_type(instance, "array") - and not uniq(instance) - ): + if uI and validator.is_type(instance, "array") and not uniq(instance): yield ValidationError(f"{instance!r} has non-unique elements") def pattern(validator, patrn, instance, schema): - if ( - validator.is_type(instance, "string") - and not re.search(patrn, instance) + if validator.is_type(instance, "string") and not re.search( + patrn, instance, ): yield ValidationError(f"{instance!r} does not match {patrn!r}") @@ -262,7 +260,9 @@ def dependentSchemas(validator, dependentSchemas, instance, schema): if property not in instance: continue yield from validator.descend( - instance, dependency, schema_path=property, + instance, + dependency, + schema_path=property, ) @@ -312,7 +312,8 @@ def required(validator, required, instance, schema): def minProperties(validator, mP, instance, schema): if validator.is_type(instance, "object") and len(instance) < mP: message = ( - "should be non-empty" if mP == 1 + "should be non-empty" + if mP == 1 else "does not have enough properties" ) yield ValidationError(f"{instance!r} {message}") @@ -323,8 +324,7 @@ def maxProperties(validator, mP, instance, schema): return if validator.is_type(instance, "object") and len(instance) > mP: message = ( - "is expected to be empty" if mP == 0 - else "has too many properties" + "is expected to be empty" if mP == 0 else "has too many properties" ) yield ValidationError(f"{instance!r} {message}") @@ -364,7 +364,8 @@ def oneOf(validator, oneOf, instance, schema): ) more_valid = [ - each for _, each in subschemas + each + for _, each in subschemas if validator.evolve(schema=each).is_valid(instance) ] if more_valid: @@ -393,10 +394,13 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema): if not validator.is_type(instance, "array"): return evaluated_item_indexes = find_evaluated_item_indexes_by_schema( - validator, instance, schema, + validator, + instance, + schema, ) unevaluated_items = [ - item for index, item in enumerate(instance) + item + for index, item in enumerate(instance) if index not in evaluated_item_indexes ] if unevaluated_items: @@ -408,7 +412,9 @@ def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): if not validator.is_type(instance, "object"): return evaluated_keys = find_evaluated_property_keys_by_schema( - validator, instance, schema, + validator, + instance, + schema, ) unevaluated_keys = [] for property in instance: diff --git a/jsonschema/_legacy_keywords.py b/jsonschema/_legacy_keywords.py index a251571bc..18b5c73cf 100644 --- a/jsonschema/_legacy_keywords.py +++ b/jsonschema/_legacy_keywords.py @@ -31,7 +31,9 @@ def dependencies_draft3(validator, dependencies, instance, schema): if validator.is_type(dependency, "object"): yield from validator.descend( - instance, dependency, schema_path=property, + instance, + dependency, + schema_path=property, ) elif validator.is_type(dependency, "string"): if dependency not in instance: @@ -70,7 +72,9 @@ def dependencies_draft4_draft6_draft7( yield ValidationError(message) else: yield from validator.descend( - instance, dependency, schema_path=property, + instance, + dependency, + schema_path=property, ) @@ -99,14 +103,16 @@ def items_draft3_draft4(validator, items, instance, schema): else: for (index, item), subschema in zip(enumerate(instance), items): yield from validator.descend( - item, subschema, path=index, schema_path=index, + item, + subschema, + path=index, + schema_path=index, ) def additionalItems(validator, aI, instance, schema): - if ( - not validator.is_type(instance, "array") - or validator.is_type(schema.get("items", {}), "object") + if not validator.is_type(instance, "array") or validator.is_type( + schema.get("items", {}), "object", ): return @@ -117,7 +123,8 @@ def additionalItems(validator, aI, instance, schema): elif not aI and len(instance) > len(schema.get("items", [])): error = "Additional items are not allowed (%s %s unexpected)" yield ValidationError( - error % _utils.extras_msg(instance[len(schema.get("items", [])):]), + error + % _utils.extras_msg(instance[len(schema.get("items", [])) :]), ) @@ -128,7 +135,10 @@ def items_draft6_draft7_draft201909(validator, items, instance, schema): if validator.is_type(items, "array"): for (index, item), subschema in zip(enumerate(instance), items): yield from validator.descend( - item, subschema, path=index, schema_path=index, + item, + subschema, + path=index, + schema_path=index, ) else: for index, item in enumerate(instance): @@ -140,10 +150,10 @@ def minimum_draft3_draft4(validator, minimum, instance, schema): return if schema.get("exclusiveMinimum", False): - failed = instance <= minimum + failed = not (instance > minimum) cmp = "less than or equal to" else: - failed = instance < minimum + failed = not (instance >= minimum) cmp = "less than" if failed: @@ -156,10 +166,10 @@ def maximum_draft3_draft4(validator, maximum, instance, schema): return if schema.get("exclusiveMaximum", False): - failed = instance >= maximum + failed = not (instance < maximum) cmp = "greater than or equal to" else: - failed = instance > maximum + failed = not (instance <= maximum) cmp = "greater than" if failed: @@ -203,7 +213,7 @@ def type_draft3(validator, types, instance, schema): return all_errors.extend(errors) elif validator.is_type(instance, type): - return + return reprs = [] for type in types: @@ -288,15 +298,21 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): if "if" in schema: if validator.evolve(schema=schema["if"]).is_valid(instance): evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, schema["if"], + validator, + instance, + schema["if"], ) if "then" in schema: evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, schema["then"], + validator, + instance, + schema["then"], ) elif "else" in schema: evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, schema["else"], + validator, + instance, + schema["else"], ) for keyword in ["contains", "unevaluatedItems"]: @@ -311,7 +327,9 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): errs = next(validator.descend(instance, subschema), None) if errs is None: evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, subschema, + validator, + instance, + subschema, ) return evaluated_indexes @@ -321,10 +339,13 @@ def unevaluatedItems_draft2019(validator, unevaluatedItems, instance, schema): if not validator.is_type(instance, "array"): return evaluated_item_indexes = find_evaluated_item_indexes_by_schema( - validator, instance, schema, + validator, + instance, + schema, ) unevaluated_items = [ - item for index, item in enumerate(instance) + item + for index, item in enumerate(instance) if index not in evaluated_item_indexes ] if unevaluated_items: @@ -388,7 +409,9 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): if property not in instance: continue evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, subschema, + validator, + instance, + subschema, ) for keyword in ["allOf", "oneOf", "anyOf"]: @@ -397,21 +420,29 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): errs = next(validator.descend(instance, subschema), None) if errs is None: evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, subschema, + validator, + instance, + subschema, ) if "if" in schema: if validator.evolve(schema=schema["if"]).is_valid(instance): evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, schema["if"], + validator, + instance, + schema["if"], ) if "then" in schema: evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, schema["then"], + validator, + instance, + schema["then"], ) elif "else" in schema: evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, schema["else"], + validator, + instance, + schema["else"], ) return evaluated_keys @@ -421,7 +452,9 @@ def unevaluatedProperties_draft2019(validator, uP, instance, schema): if not validator.is_type(instance, "object"): return evaluated_keys = find_evaluated_property_keys_by_schema( - validator, instance, schema, + validator, + instance, + schema, ) unevaluated_keys = [] for property in instance: diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 7d8a4c5cd..488fd993b 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -60,7 +60,8 @@ def test_attrs(self): self.Validator.VALIDATORS, self.Validator.META_SCHEMA, self.Validator.TYPE_CHECKER, - ), ( + ), + ( self.validators, self.meta_schema, self.type_checker, @@ -138,7 +139,8 @@ def test_long_repr(self): self.addCleanup(validators._META_SCHEMAS.pop, "something") self.addCleanup(validators._VALIDATORS.pop, "my version") self.assertEqual( - repr(Validator({"a": list(range(1000))})), ( + repr(Validator({"a": list(range(1000))})), + ( "MyVersionValidator(schema={'a': [0, 1, 2, 3, 4, 5, ...]}, " "format_checker=None)" ), @@ -255,7 +257,8 @@ def test_extend(self): Extended.META_SCHEMA, Extended.TYPE_CHECKER, self.Validator.VALIDATORS, - ), ( + ), + ( dict(original, new=new), self.Validator.META_SCHEMA, self.Validator.TYPE_CHECKER, @@ -267,8 +270,10 @@ def test_extend_idof(self): """ Extending a validator preserves its notion of schema IDs. """ + def id_of(schema): return schema.get("__test__", self.Validator.ID_OF(schema)) + correct_id = "the://correct/id/" meta_schema = { "$id": "the://wrong/id/", @@ -458,12 +463,14 @@ def test_invalid_format_default_message(self): self.assertIn("is not a", message) def test_additionalProperties_false_patternProperties(self): - schema = {"type": "object", - "additionalProperties": False, - "patternProperties": { - "^abc$": {"type": "string"}, - "^def$": {"type": "string"}, - }} + schema = { + "type": "object", + "additionalProperties": False, + "patternProperties": { + "^abc$": {"type": "string"}, + "^def$": {"type": "string"}, + }, + } message = self.message_for( instance={"zebra": 123}, schema=schema, @@ -472,7 +479,9 @@ def test_additionalProperties_false_patternProperties(self): self.assertEqual( message, "{} does not match any of the regexes: {}, {}".format( - repr("zebra"), repr("^abc$"), repr("^def$"), + repr("zebra"), + repr("^abc$"), + repr("^def$"), ), ) message = self.message_for( @@ -483,7 +492,10 @@ def test_additionalProperties_false_patternProperties(self): self.assertEqual( message, "{}, {} do not match any of the regexes: {}, {}".format( - repr("fish"), repr("zebra"), repr("^abc$"), repr("^def$"), + repr("fish"), + repr("zebra"), + repr("^abc$"), + repr("^def$"), ), ) @@ -815,6 +827,40 @@ def test_heterogeneous_properties_unevaluatedProperties(self): "Unevaluated properties are not allowed (37, 'a' were unexpected)", ) + def test_minimum_rejects_nan(self): + from math import nan + + message = self.message_for(instance=nan, schema={"minimum": 0}) + self.assertEqual(message, "nan is less than the minimum of 0") + + def test_maximum_rejects_nan(self): + from math import nan + + message = self.message_for(instance=nan, schema={"maximum": 0}) + self.assertEqual(message, "nan is greater than the maximum of 0") + + def test_exclusiveMinimum_rejects_nan(self): + from math import nan + + message = self.message_for( + instance=nan, schema={"exclusiveMinimum": 0}, + ) + self.assertEqual( + message, + "nan is less than or equal to the minimum of 0", + ) + + def test_exclusiveMaximum_rejects_nan(self): + from math import nan + + message = self.message_for( + instance=nan, schema={"exclusiveMaximum": 0}, + ) + self.assertEqual( + message, + "nan is greater than or equal to the maximum of 0", + ) + class TestValidationErrorDetails(TestCase): # TODO: These really need unit tests for each individual keyword, rather @@ -866,7 +912,8 @@ def test_anyOf(self): self.assertEqual(e1.schema_path, deque([0, "minimum"])) self.assertEqual(e1.relative_schema_path, deque([0, "minimum"])) self.assertEqual( - e1.absolute_schema_path, deque(["anyOf", 0, "minimum"]), + e1.absolute_schema_path, + deque(["anyOf", 0, "minimum"]), ) self.assertFalse(e1.context) @@ -953,10 +1000,12 @@ def test_type(self): self.assertEqual(e2.json_path, "$.foo") self.assertEqual( - e2.schema_path, deque([1, "properties", "foo", "enum"]), + e2.schema_path, + deque([1, "properties", "foo", "enum"]), ) self.assertEqual( - e2.relative_schema_path, deque([1, "properties", "foo", "enum"]), + e2.relative_schema_path, + deque([1, "properties", "foo", "enum"]), ) self.assertEqual( e2.absolute_schema_path, @@ -1044,7 +1093,8 @@ def test_multiple_nesting(self): self.assertEqual(e1.schema_path, deque(["type"])) self.assertEqual(e2.schema_path, deque(["items", "type"])) self.assertEqual( - list(e3.schema_path), ["items", "properties", "bar", "type"], + list(e3.schema_path), + ["items", "properties", "bar", "type"], ) self.assertEqual( list(e4.schema_path), @@ -1055,7 +1105,8 @@ def test_multiple_nesting(self): ["items", "properties", "bar", "properties", "baz", "minItems"], ) self.assertEqual( - list(e6.schema_path), ["items", "properties", "foo", "enum"], + list(e6.schema_path), + ["items", "properties", "foo", "enum"], ) self.assertEqual(e1.validator, "type") @@ -1069,23 +1120,25 @@ def test_recursive(self): schema = { "definitions": { "node": { - "anyOf": [{ - "type": "object", - "required": ["name", "children"], - "properties": { - "name": { - "type": "string", - }, - "children": { - "type": "object", - "patternProperties": { - "^.*$": { - "$ref": "#/definitions/node", + "anyOf": [ + { + "type": "object", + "required": ["name", "children"], + "properties": { + "name": { + "type": "string", + }, + "children": { + "type": "object", + "patternProperties": { + "^.*$": { + "$ref": "#/definitions/node", + }, }, }, }, }, - }], + ], }, }, "type": "object", @@ -1111,17 +1164,19 @@ def test_recursive(self): } validator = validators.Draft4Validator(schema) - e, = validator.iter_errors(instance) + (e,) = validator.iter_errors(instance) self.assertEqual(e.absolute_path, deque(["root"])) self.assertEqual( - e.absolute_schema_path, deque(["properties", "root", "anyOf"]), + e.absolute_schema_path, + deque(["properties", "root", "anyOf"]), ) self.assertEqual(e.json_path, "$.root") - e1, = e.context + (e1,) = e.context self.assertEqual(e1.absolute_path, deque(["root", "children", "a"])) self.assertEqual( - e1.absolute_schema_path, deque( + e1.absolute_schema_path, + deque( [ "properties", "root", @@ -1137,14 +1192,16 @@ def test_recursive(self): ) self.assertEqual(e1.json_path, "$.root.children.a") - e2, = e1.context + (e2,) = e1.context self.assertEqual( - e2.absolute_path, deque( + e2.absolute_path, + deque( ["root", "children", "a", "children", "ab"], ), ) self.assertEqual( - e2.absolute_schema_path, deque( + e2.absolute_schema_path, + deque( [ "properties", "root", @@ -1250,7 +1307,7 @@ def test_propertyNames(self): schema = {"propertyNames": {"not": {"const": "foo"}}} validator = validators.Draft7Validator(schema) - error, = validator.iter_errors(instance) + (error,) = validator.iter_errors(instance) self.assertEqual(error.validator, "not") self.assertEqual( @@ -1268,7 +1325,7 @@ def test_if_then(self): } validator = validators.Draft7Validator(schema) - error, = validator.iter_errors(12) + (error,) = validator.iter_errors(12) self.assertEqual(error.validator, "const") self.assertEqual(error.message, "13 was expected") @@ -1283,7 +1340,7 @@ def test_if_else(self): } validator = validators.Draft7Validator(schema) - error, = validator.iter_errors(15) + (error,) = validator.iter_errors(15) self.assertEqual(error.validator, "const") self.assertEqual(error.message, "13 was expected") @@ -1293,7 +1350,7 @@ def test_if_else(self): def test_boolean_schema_False(self): validator = validators.Draft7Validator(False) - error, = validator.iter_errors(12) + (error,) = validator.iter_errors(12) self.assertEqual( ( @@ -1322,7 +1379,7 @@ def test_ref(self): {"$ref": ref}, resolver=validators._RefResolver("", {}, store={ref: schema}), ) - error, = validator.iter_errors({"foo": "notAnInteger"}) + (error,) = validator.iter_errors({"foo": "notAnInteger"}) self.assertEqual( ( @@ -1455,7 +1512,7 @@ def test_contains_too_many(self): """ schema = {"contains": {"type": "string"}, "maxContains": 2} validator = validators.Draft202012Validator(schema) - error, = validator.iter_errors(["foo", 2, "bar", 4, "baz", "quux"]) + (error,) = validator.iter_errors(["foo", 2, "bar", 4, "baz", "quux"]) self.assertEqual( ( error.message, @@ -1482,7 +1539,7 @@ def test_contains_too_many(self): def test_contains_too_few(self): schema = {"contains": {"type": "string"}, "minContains": 2} validator = validators.Draft202012Validator(schema) - error, = validator.iter_errors(["foo", 2, 4]) + (error,) = validator.iter_errors(["foo", 2, 4]) self.assertEqual( ( error.message, @@ -1512,7 +1569,7 @@ def test_contains_too_few(self): def test_contains_none(self): schema = {"contains": {"type": "string"}, "minContains": 2} validator = validators.Draft202012Validator(schema) - error, = validator.iter_errors([2, 4]) + (error,) = validator.iter_errors([2, 4]) self.assertEqual( ( error.message, @@ -1701,6 +1758,7 @@ def test_evolve_with_subclass(self): """ with self.assertWarns(DeprecationWarning): + @define class OhNo(self.Validator): foo = field(factory=lambda: [1, 2, 3]) @@ -1729,9 +1787,13 @@ def test_it_can_validate_with_decimals(self): self.Validator, type_checker=self.Validator.TYPE_CHECKER.redefine( "number", - lambda checker, thing: isinstance( - thing, (int, float, Decimal), - ) and not isinstance(thing, bool), + lambda checker, thing: ( + isinstance( + thing, + (int, float, Decimal), + ) + and not isinstance(thing, bool) + ), ), ) @@ -1746,7 +1808,8 @@ def test_it_can_validate_with_decimals(self): def test_it_returns_true_for_formats_it_does_not_know_about(self): validator = self.Validator( - {"format": "carrot"}, format_checker=FormatChecker(), + {"format": "carrot"}, + format_checker=FormatChecker(), ) validator.validate("bugs") @@ -1768,7 +1831,8 @@ def check(value): self.fail(f"What is {value}? [Baby Don't Hurt Me]") validator = self.Validator( - {"format": "foo"}, format_checker=checker, + {"format": "foo"}, + format_checker=checker, ) validator.validate("good") @@ -1818,10 +1882,12 @@ def test_check_redefined_sequence(self): type_checker=self.Validator.TYPE_CHECKER.redefine_many( { "array": lambda checker, thing: isinstance( - thing, (list, deque), + thing, + (list, deque), ), "object": lambda checker, thing: isinstance( - thing, (dict, MyMapping), + thing, + (dict, MyMapping), ), }, ), @@ -1947,7 +2013,8 @@ def test_any_type_is_redefinable(self): Crazy = validators.extend( self.Validator, type_checker=self.Validator.TYPE_CHECKER.redefine( - "any", lambda checker, thing: isinstance(thing, int), + "any", + lambda checker, thing: isinstance(thing, int), ), ) validator = Crazy({"type": "any"}) @@ -2273,6 +2340,7 @@ def validate(): validate() # just verify it succeeds from threading import Thread + thread = Thread(target=validate) thread.start() thread.join() @@ -2306,7 +2374,6 @@ def test_custom_registries_do_not_autoretrieve_remote_resources(self): class TestRefResolver(TestCase): - base_uri = "" stored_uri = "foo://stored" stored_schema = {"stored": "schema"} @@ -2315,7 +2382,9 @@ def setUp(self): self.referrer = {} self.store = {self.stored_uri: self.stored_schema} self.resolver = validators._RefResolver( - self.base_uri, self.referrer, self.store, + self.base_uri, + self.referrer, + self.store, ) def test_it_does_not_retrieve_schema_urls_from_the_network(self): @@ -2357,7 +2426,9 @@ def test_it_retrieves_unstored_refs_via_requests(self): if "requests" in sys.modules: # pragma: no cover self.addCleanup( - sys.modules.__setitem__, "requests", sys.modules["requests"], + sys.modules.__setitem__, + "requests", + sys.modules["requests"], ) sys.modules["requests"] = ReallyFakeRequests({"http://bar": schema}) @@ -2370,7 +2441,9 @@ def test_it_retrieves_unstored_refs_via_urlopen(self): if "requests" in sys.modules: # pragma: no cover self.addCleanup( - sys.modules.__setitem__, "requests", sys.modules["requests"], + sys.modules.__setitem__, + "requests", + sys.modules["requests"], ) sys.modules["requests"] = None @@ -2442,7 +2515,10 @@ def handler(url): ref = "foo://bar" resolver = validators._RefResolver( - "", {}, cache_remote=True, handlers={"foo": handler}, + "", + {}, + cache_remote=True, + handlers={"foo": handler}, ) with resolver.resolving(ref): pass @@ -2460,7 +2536,10 @@ def handler(url): ref = "foo://bar" resolver = validators._RefResolver( - "", {}, cache_remote=False, handlers={"foo": handler}, + "", + {}, + cache_remote=False, + handlers={"foo": handler}, ) with resolver.resolving(ref): pass @@ -2542,19 +2621,18 @@ def test_refresolver_with_pointer_in_schema_with_no_id(self): ) - def sorted_errors(errors): def key(error): return ( [str(e) for e in error.path], [str(e) for e in error.schema_path], ) + return sorted(errors, key=key) @define class ReallyFakeRequests: - _responses: dict[str, Any] def get(self, url): @@ -2566,7 +2644,6 @@ def get(self, url): @define class _ReallyFakeJSONResponse: - _response: str def json(self):