From 173582ef022dd5de5eb4282d39263db40624015c Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Mon, 14 Jun 2021 09:35:15 +0200 Subject: [PATCH 01/27] Julian/jsonschema#782: Enable draft2020-12 test suite --- jsonschema/__init__.py | 2 + jsonschema/_format.py | 3 +- jsonschema/_types.py | 1 + jsonschema/schemas/draft2020-12.json | 58 +++++++++++++++++++ .../tests/test_jsonschema_test_suite.py | 14 +++++ jsonschema/validators.py | 42 +++++++++++++- 6 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 jsonschema/schemas/draft2020-12.json diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py index 619a7eabb..c1cb2e91d 100644 --- a/jsonschema/__init__.py +++ b/jsonschema/__init__.py @@ -14,6 +14,7 @@ draft4_format_checker, draft6_format_checker, draft7_format_checker, + draft202012_format_checker, ) from jsonschema._types import TypeChecker from jsonschema.exceptions import ( @@ -28,6 +29,7 @@ Draft4Validator, Draft6Validator, Draft7Validator, + Draft202012Validator, RefResolver, validate, ) diff --git a/jsonschema/_format.py b/jsonschema/_format.py index bdd893da8..345545bad 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -131,13 +131,14 @@ def conforms(self, instance, format): draft4_format_checker = FormatChecker() draft6_format_checker = FormatChecker() draft7_format_checker = FormatChecker() - +draft202012_format_checker = FormatChecker() _draft_checkers = dict( draft3=draft3_format_checker, draft4=draft4_format_checker, draft6=draft6_format_checker, draft7=draft7_format_checker, + draft202012=draft202012_format_checker, ) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index 50bcf99c9..3eab7b006 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -185,3 +185,4 @@ def remove(self, *types): ), ) draft7_type_checker = draft6_type_checker +draft202012_type_checker = draft7_type_checker diff --git a/jsonschema/schemas/draft2020-12.json b/jsonschema/schemas/draft2020-12.json new file mode 100644 index 000000000..d5e2d31c3 --- /dev/null +++ b/jsonschema/schemas/draft2020-12.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/unevaluated"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format-annotation"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", + "properties": { + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "deprecated": true, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$dynamicRef": "#meta" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + }, + "deprecated": true, + "default": {} + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "meta/core#/$defs/anchorString", + "deprecated": true + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "meta/core#/$defs/uriReferenceString", + "deprecated": true + } + } +} diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index 40e3d8592..3f821002d 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -13,10 +13,12 @@ Draft4Validator, Draft6Validator, Draft7Validator, + Draft202012Validator, draft3_format_checker, draft4_format_checker, draft6_format_checker, draft7_format_checker, + draft202012_format_checker, ) from jsonschema.tests._helpers import bug from jsonschema.tests._suite import Suite @@ -26,6 +28,7 @@ DRAFT4 = SUITE.version(name="draft4") DRAFT6 = SUITE.version(name="draft6") DRAFT7 = SUITE.version(name="draft7") +DRAFT202012 = SUITE.version(name="draft2020-12") def skip(message, **kwargs): @@ -398,3 +401,14 @@ def leap_second(test): )(test) ), ) + + +DRAFT202012 = DRAFT202012.to_unittest_testcase( + DRAFT202012.tests(), + DRAFT202012.format_tests(), + DRAFT202012.optional_tests_of(name="bignum"), + DRAFT202012.optional_tests_of(name="content"), + DRAFT202012.optional_tests_of(name="non-bmp-regex"), + Validator=Draft202012Validator, + format_checker=draft202012_format_checker, +) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 70d46c2fd..c1b07537e 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -428,7 +428,47 @@ def extend(validator, validators=(), version=None, type_checker=None): version="draft7", ) -_LATEST_VERSION = Draft7Validator +Draft202012Validator = create( + meta_schema=_utils.load_schema("draft2020-12"), + validators={ + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"allOf": _validators.allOf, + u"anyOf": _validators.anyOf, + u"const": _validators.const, + u"contains": _validators.contains, + u"dependencies": _validators.dependencies, + u"enum": _validators.enum, + u"exclusiveMaximum": _validators.exclusiveMaximum, + u"exclusiveMinimum": _validators.exclusiveMinimum, + u"format": _validators.format, + u"if": _validators.if_, + u"items": _validators.items, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maxProperties": _validators.maxProperties, + u"maximum": _validators.maximum, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minProperties": _validators.minProperties, + u"minimum": _validators.minimum, + u"multipleOf": _validators.multipleOf, + u"oneOf": _validators.oneOf, + u"not": _validators.not_, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _validators.properties, + u"propertyNames": _validators.propertyNames, + u"required": _validators.required, + u"type": _validators.type, + u"uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft202012_type_checker, + version="draft2020-12", +) + +_LATEST_VERSION = Draft202012Validator class RefResolver(object): From e87769871ac4d3cc05be9da72fd203b56e7bfdba Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Mon, 28 Jun 2021 17:37:38 +0200 Subject: [PATCH 02/27] Julian/jsonschema#782: Split format and regular test cases on draft2020-12 --- .../tests/test_jsonschema_test_suite.py | 97 ++++++++++++++++++- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index 3f821002d..a0a0cecc1 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -128,6 +128,62 @@ def leap_second(test): )(test) +def ecmascript_regex_validation(test): + """ + Considering switching from re to js-regex after the following issues are + resolved: + * https://github.com/Julian/jsonschema/issues/612 + * https://github.com/Zac-HD/js-regex/issues/4 + + Notice: Zac-HD/js-regex Repository has been archived + """ + return skip( + message=bug(612), + subject="ecmascript-regex", + description='NKO DIGIT ZERO does not match (unlike e.g. Python)', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='NKO DIGIT ZERO (as \\u escape) does not match', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='NKO DIGIT ZERO matches (unlike e.g. Python)', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='NKO DIGIT ZERO (as \\u escape) matches', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='zero-width whitespace matches', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='zero-width whitespace does not match', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='latin-1 e-acute matches (unlike e.g. Python)', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='latin-1 e-acute does not match (unlike e.g. Python)', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='matches in Python, but should not in jsonschema', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='does not match', + )(test) or skip( + message=bug(612), + subject="ecmascript-regex", + description='matches', + )(test) + + TestDraft3 = DRAFT3.to_unittest_testcase( DRAFT3.tests(), DRAFT3.format_tests(), @@ -403,12 +459,47 @@ def leap_second(test): ) -DRAFT202012 = DRAFT202012.to_unittest_testcase( +TestDraft202012 = DRAFT202012.to_unittest_testcase( DRAFT202012.tests(), - DRAFT202012.format_tests(), DRAFT202012.optional_tests_of(name="bignum"), - DRAFT202012.optional_tests_of(name="content"), + DRAFT202012.optional_tests_of(name="ecmascript-regex"), + DRAFT202012.optional_tests_of(name="float-overflow"), DRAFT202012.optional_tests_of(name="non-bmp-regex"), + DRAFT202012.optional_tests_of(name="refOfUnknownKeyword"), + Validator=Draft202012Validator, + skip=lambda test: ( + narrow_unicode_build(test) + or ecmascript_regex_validation(test) + or skip( + message="ToDo: Extend validation", + subject="dynamicRef", + case_description="A $dynamicRef that initially resolves to a " + "schema with a matching $dynamicAnchor should " + "resolve to the first $dynamicAnchor in the " + "dynamic scope", + description='The recursive part is not valid against the root', + )(test) + or skip( + message="ToDo: Extend validation", + subject="dynamicRef", + case_description="multiple dynamic paths to the $dynamicRef " + "keyword", + description='recurse to integerNode - floats are not allowed', + )(test) + ), +) + + +TestDraft202012Format = DRAFT202012.to_unittest_testcase( + DRAFT202012.format_tests(), Validator=Draft202012Validator, format_checker=draft202012_format_checker, + skip=lambda test: ( + complex_email_validation(test) + or missing_date_fromisoformat(test) + or allowed_leading_zeros(test) + or leap_second(test) + or missing_format(draft202012_format_checker)(test) + or complex_email_validation(test) + ) ) From 491168de3b21ea30531406f0f8ddd1415b6f0cba Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Tue, 15 Jun 2021 09:19:56 +0200 Subject: [PATCH 03/27] Julian/jsonschema#782: Add dependentRequired and dependentSchemas validation --- jsonschema/_validators.py | 38 ++++++++++++++++++++++++++++++++++++++ jsonschema/validators.py | 3 ++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 0f7b6fb1c..0e973f30d 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -234,6 +234,10 @@ def maxLength(validator, mL, instance, schema): def dependencies(validator, dependencies, instance, schema): + """ + The dependencies keyword has been deprecated since draft 2019-09 and has been split into dependentRequired + and dependentSchemas. + """ if not validator.is_type(instance, "object"): return @@ -253,6 +257,40 @@ def dependencies(validator, dependencies, instance, schema): yield error +def dependentRequired(validator, dependentRequired, instance, schema): + """ + Split from dependencies + """ + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependentRequired.items(): + if property not in instance: + continue + + for each in dependency: + if each not in instance: + message = "%r is a dependency of %r" + yield ValidationError(message % (each, property)) + + +def dependentSchemas(validator, dependentSchemas, instance, schema): + """ + Split from dependencies + """ + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependentSchemas.items(): + if property not in instance: + continue + + for error in validator.descend( + instance, dependency, schema_path=property, + ): + yield error + + def enum(validator, enums, instance, schema): if instance == 0 or instance == 1: unbooled = unbool(instance) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index c1b07537e..8c0edd462 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -438,7 +438,8 @@ def extend(validator, validators=(), version=None, type_checker=None): u"anyOf": _validators.anyOf, u"const": _validators.const, u"contains": _validators.contains, - u"dependencies": _validators.dependencies, + u"dependentRequired": _validators.dependentRequired, + u"dependentSchemas": _validators.dependentSchemas, u"enum": _validators.enum, u"exclusiveMaximum": _validators.exclusiveMaximum, u"exclusiveMinimum": _validators.exclusiveMinimum, From edc5eb086b557336dfb6963ed38f47464ee17727 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Mon, 14 Jun 2021 15:02:12 +0200 Subject: [PATCH 04/27] Julian/jsonschema#782: Extend format check for draft2020-12, add duration format check --- jsonschema/_format.py | 58 ++++++++++++++++++++++++++++++++++++++----- setup.cfg | 2 ++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/jsonschema/_format.py b/jsonschema/_format.py index 345545bad..49b6b7121 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -148,12 +148,14 @@ def _checks_drafts( draft4=None, draft6=None, draft7=None, + draft202012=None, raises=(), ): draft3 = draft3 or name draft4 = draft4 or name draft6 = draft6 or name draft7 = draft7 or name + draft202012 = draft202012 or name def wrap(func): if draft3: @@ -164,11 +166,13 @@ def wrap(func): func = _draft_checkers["draft6"].checks(draft6, raises)(func) if draft7: func = _draft_checkers["draft7"].checks(draft7, raises)(func) + if draft202012: + func = _draft_checkers["draft202012"].checks(draft202012, raises)(func) # Oy. This is bad global state, but relied upon for now, until # deprecation. See https://github.com/Julian/jsonschema/issues/519 # and test_format_checkers_come_with_defaults - FormatChecker.cls_checks(draft7 or draft6 or draft4 or draft3, raises)( + FormatChecker.cls_checks(draft202012 or draft7 or draft6 or draft4 or draft3, raises)( func, ) return func @@ -188,6 +192,7 @@ def is_email(instance): draft4="ipv4", draft6="ipv4", draft7="ipv4", + draft202012="ipv4", raises=ipaddress.AddressValueError, ) def is_ipv4(instance): @@ -214,6 +219,7 @@ def is_ipv6(instance): draft4="hostname", draft6="hostname", draft7="hostname", + draft202012="hostname", ) def is_host_name(instance): if not isinstance(instance, str): @@ -229,6 +235,7 @@ def is_host_name(instance): else: @_checks_drafts( draft7="idn-hostname", + draft202012="idn-hostname", raises=(idna.IDNAError, UnicodeError), ) def is_idn_host_name(instance): @@ -255,6 +262,7 @@ def is_uri(instance): @_checks_drafts( draft6="uri-reference", draft7="uri-reference", + draft202012="uri-reference", raises=ValueError, ) def is_uri_reference(instance): @@ -263,19 +271,30 @@ def is_uri_reference(instance): return validate_rfc3986(instance, rule="URI_reference") else: - @_checks_drafts(draft7="iri", raises=ValueError) + @_checks_drafts( + draft7="iri", + draft202012="iri", + raises=ValueError, + ) def is_iri(instance): if not isinstance(instance, str): return True return rfc3987.parse(instance, rule="IRI") - @_checks_drafts(draft7="iri-reference", raises=ValueError) + @_checks_drafts( + draft7="iri-reference", + draft202012="iri-reference", + raises=ValueError, + ) def is_iri_reference(instance): if not isinstance(instance, str): return True return rfc3987.parse(instance, rule="IRI_reference") - @_checks_drafts(name="uri", raises=ValueError) + @_checks_drafts( + name="uri", + raises=ValueError, + ) def is_uri(instance): if not isinstance(instance, str): return True @@ -284,6 +303,7 @@ def is_uri(instance): @_checks_drafts( draft6="uri-reference", draft7="uri-reference", + draft202012="uri-reference", raises=ValueError, ) def is_uri_reference(instance): @@ -307,7 +327,10 @@ def is_datetime(instance): return True return validate_rfc3339(instance.upper()) - @_checks_drafts(draft7="time") + @_checks_drafts( + draft7="time", + draft202012="time", + ) def is_time(instance): if not isinstance(instance, str): return True @@ -328,7 +351,12 @@ def _is_date(instance): return datetime.datetime.strptime(instance, "%Y-%m-%d") -@_checks_drafts(draft3="date", draft7="date", raises=ValueError) +@_checks_drafts( + draft3="date", + draft7="date", + draft202012="date", + raises=ValueError, +) def is_date(instance): if not isinstance(instance, str): return True @@ -378,6 +406,7 @@ def is_css3_color(instance): @_checks_drafts( draft6="json-pointer", draft7="json-pointer", + draft202012="json-pointer", raises=jsonpointer.JsonPointerException, ) def is_json_pointer(instance): @@ -391,6 +420,7 @@ def is_json_pointer(instance): # into a new external library. @_checks_drafts( draft7="relative-json-pointer", + draft202012="relative-json-pointer", raises=jsonpointer.JsonPointerException, ) def is_relative_json_pointer(instance): @@ -420,8 +450,24 @@ def is_relative_json_pointer(instance): @_checks_drafts( draft6="uri-template", draft7="uri-template", + draft202012="uri-template", ) def is_uri_template(instance): if not isinstance(instance, str): return True return uri_template.validate(instance) + + +try: + import isoduration +except ImportError: + pass +else: + @_checks_drafts( + draft202012="duration", + raises=isoduration.DurationParsingException, + ) + def is_duration(instance): + if not isinstance(instance, str): + return True + return isoduration.parse_duration(instance) diff --git a/setup.cfg b/setup.cfg index 880672691..a53aea0cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,6 +39,7 @@ format = strict-rfc3339 webcolors uri_template + isoduration;python_version>'3.6' format_nongpl = fqdn idna @@ -47,6 +48,7 @@ format_nongpl = rfc3986-validator>0.1.0 rfc3339-validator uri_template + isoduration;python_version>'3.6' [options.entry_points] console_scripts = From 0e4eaeb86569b9dfb664efee436573cedc4fa333 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 16 Jun 2021 14:09:25 +0200 Subject: [PATCH 05/27] Julian/jsonschema#782: Add checks for prefixItems, basic check for unevaluatedItems --- jsonschema/_validators.py | 24 ++++++++++++++++++++++++ jsonschema/validators.py | 2 ++ 2 files changed, 26 insertions(+) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 0e973f30d..7b273f4df 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -422,3 +422,27 @@ def if_(validator, if_schema, instance, schema): else_ = schema[u"else"] for error in validator.descend(instance, else_, schema_path="else"): yield error + + +def unevaluatedItems(validator, unevaluatedItems, instance, schema): + if not validator.is_type(instance, "array"): + return + + if unevaluatedItems: + return + + # ToDo: Implement additional checks for "prefixItems", "items", "contains", "if", "then", "else", "allOf", "anyOf", + # "oneOf" and "not" keywords + + +def prefixItems(validator, prefixItems, instance, schema): + if "unevaluatedItems" in schema: + return + + if not validator.is_type(instance, "array"): + return + + for k, v in enumerate(instance): + if k < len(prefixItems): + for error in validator.descend(v, prefixItems[k], schema_path="prefixItems"): + yield error diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 8c0edd462..3d382b21c 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -464,6 +464,8 @@ def extend(validator, validators=(), version=None, type_checker=None): u"required": _validators.required, u"type": _validators.type, u"uniqueItems": _validators.uniqueItems, + u"unevaluatedItems": _validators.unevaluatedItems, + u"prefixItems": _validators.prefixItems, }, type_checker=_types.draft202012_type_checker, version="draft2020-12", From 753c415866986d413a270c8a199225bdacf62ddb Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 16 Jun 2021 20:04:36 +0200 Subject: [PATCH 06/27] Julian/jsonschema#782: Adapt items to work with prefixItems --- jsonschema/_validators.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 7b273f4df..5a1f56a49 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -71,12 +71,28 @@ def items(validator, items, instance, schema): return if validator.is_type(items, "array"): + """ + Used in in Draft 7 an bellow, probably also useful for legacy schema format + """ for (index, item), subschema in zip(enumerate(instance), items): for error in validator.descend( item, subschema, path=index, schema_path=index, ): yield error + elif validator.is_type(items, "boolean") and 'prefixItems' in schema: + if len(instance) > len(schema['prefixItems']): + yield ValidationError("%r has more items than defined in prefixItems" % instance) + else: + for error in validator.descend(instance, {'prefixItems': schema['prefixItems']}, path='items__prefixItems'): + yield error else: + if 'prefixItems' in schema: + for error in validator.descend(instance, {'prefixItems': schema['prefixItems']}, path='items__prefixItems'): + yield error + + # Remove evaluated prefixItems indexes + del instance[0:len(schema['prefixItems'])] + for index, item in enumerate(instance): for error in validator.descend(item, items, path=index): yield error From 96b7fe8a2e0827d19637d6a854c5161738e284a4 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Thu, 17 Jun 2021 08:50:14 +0200 Subject: [PATCH 07/27] Julian/jsonschema#782: Extend contains with minContains and maxContaints, add contains legacy validator --- jsonschema/_legacy_validators.py | 10 ++++++ jsonschema/_validators.py | 55 +++++++++++++++++++++++++++++++- jsonschema/validators.py | 4 +-- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index 80f798b7a..26f1ab103 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -138,3 +138,13 @@ def type_draft3(validator, types, instance, schema): yield ValidationError( _utils.types_msg(instance, types), context=all_errors, ) + + +def contains_draft6_draft7(validator, contains, instance, schema): + if not validator.is_type(instance, "array"): + return + + if not any(validator.is_valid(element, contains) for element in instance): + yield ValidationError( + "None of %r are valid under the given schema" % (instance,) + ) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 5a1f56a49..0ca215aeb 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -127,10 +127,63 @@ def contains(validator, contains, instance, schema): if not validator.is_type(instance, "array"): return - if not any(validator.is_valid(element, contains) for element in instance): + min_contains = max_contains = None + + if 'minContains' in schema: + min_contains = schema['minContains'] + if not validator.is_type(min_contains, "integer"): + yield ValidationError( + "minContains of %r in not valid under the given schema" % (min_contains,) + ) + return + + if 'maxContains' in schema: + max_contains = schema['maxContains'] + if not validator.is_type(max_contains, "integer"): + yield ValidationError( + "maxContains of %r is not valid under the given schema" % (instance,) + ) + return + + # minContains set to 0 will ignore contains + if min_contains == 0: + return + + matches = len(list(filter(lambda x: x, [validator.is_valid(element, contains) for element in instance]))) + + # default contains behavior + if not matches: yield ValidationError( "None of %r are valid under the given schema" % (instance,) ) + return + + if min_contains and max_contains is None: + if matches < min_contains: + yield ValidationError( + "Invalid number or matches of %r under the given schema, expected min %d, got %d" % ( + instance, min_contains, matches + ) + ) + return + + if min_contains is None and max_contains: + if matches > max_contains: + yield ValidationError( + "Invalid number or matches of %r under the given schema, expected max %d, got %d" % ( + instance, max_contains, matches + ) + ) + return + + if min_contains and max_contains: + if matches < min_contains or matches > max_contains: + yield ValidationError( + "Invalid number or matches of %r under the given schema, expected min %d and max %d, got %d" % ( + instance, min_contains, max_contains, matches + ) + ) + return def exclusiveMinimum(validator, minimum, instance, schema): diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 3d382b21c..dae3ddb7d 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -358,7 +358,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"allOf": _validators.allOf, u"anyOf": _validators.anyOf, u"const": _validators.const, - u"contains": _validators.contains, + u"contains": _legacy_validators.contains_draft6_draft7, u"dependencies": _validators.dependencies, u"enum": _validators.enum, u"exclusiveMaximum": _validators.exclusiveMaximum, @@ -397,7 +397,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"allOf": _validators.allOf, u"anyOf": _validators.anyOf, u"const": _validators.const, - u"contains": _validators.contains, + u"contains": _legacy_validators.contains_draft6_draft7, u"dependencies": _validators.dependencies, u"enum": _validators.enum, u"exclusiveMaximum": _validators.exclusiveMaximum, From bb8b7eee22e986b5f8c682e30c38a05e474e53d8 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Fri, 18 Jun 2021 16:10:21 +0200 Subject: [PATCH 08/27] Julian/jsonschema#782: Implements unevaluatedItems validations --- jsonschema/_legacy_validators.py | 16 ++++++++++++ jsonschema/_utils.py | 42 ++++++++++++++++++++++++++++++++ jsonschema/_validators.py | 38 ++++++++++++----------------- jsonschema/validators.py | 4 +-- 4 files changed, 76 insertions(+), 24 deletions(-) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index 26f1ab103..b33715169 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -61,6 +61,22 @@ def items_draft3_draft4(validator, items, instance, schema): yield error +def items_draft6_draft7(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "array"): + for (index, item), subschema in zip(enumerate(instance), items): + for error in validator.descend( + item, subschema, path=index, schema_path=index, + ): + yield error + else: + for index, item in enumerate(instance): + for error in validator.descend(item, items, path=index): + yield error + + def minimum_draft3_draft4(validator, minimum, instance, schema): if not validator.is_type(instance, "number"): return diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index 97b7f8f5a..100b65368 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -232,3 +232,45 @@ def uniq(container): seen.append(e) return True + + +def find_evaluated_item_indexes_by_schema(validator, instance, schema): + """ + Get all indexes of items that get evaluated under the current schema + + Covers are keywords related t unevaluatedItems: items, prefixItems, if, then, else, 'contains', 'unevaluatedItems', + 'allOf', 'oneOf', 'anyOf' + """ + if not validator.is_type(schema, "object"): + return [] + evaluated_item_indexes = [] + + if 'items' in schema: + return list(range(0, len(instance))) + + if 'prefixItems' in schema: + evaluated_item_indexes = list(range(0, len(schema['prefixItems']))) + + if 'if' in schema: + if validator.is_valid(instance, schema['if']): + evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, schema['if']) + if 'then' in schema: + evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, schema['then']) + else: + if 'else' in schema: + evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, schema['else']) + + for keyword in ['contains', 'unevaluatedItems']: + if keyword in schema: + for k, v in enumerate(instance): + if validator.is_valid(v, schema[keyword]): + evaluated_item_indexes.append(k) + + for keyword in ['allOf', 'oneOf', 'anyOf']: + if keyword in schema: + for subschema in schema[keyword]: + errs = list(validator.descend(instance, subschema)) + if not errs: + evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, subschema) + + return evaluated_item_indexes diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 0ca215aeb..b0a4f8843 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -6,6 +6,7 @@ equal, extras_msg, find_additional_properties, + find_evaluated_item_indexes_by_schema, types_msg, unbool, uniq, @@ -70,21 +71,13 @@ def items(validator, items, instance, schema): if not validator.is_type(instance, "array"): return - if validator.is_type(items, "array"): - """ - Used in in Draft 7 an bellow, probably also useful for legacy schema format - """ - for (index, item), subschema in zip(enumerate(instance), items): - for error in validator.descend( - item, subschema, path=index, schema_path=index, - ): - yield error - elif validator.is_type(items, "boolean") and 'prefixItems' in schema: - if len(instance) > len(schema['prefixItems']): - yield ValidationError("%r has more items than defined in prefixItems" % instance) - else: - for error in validator.descend(instance, {'prefixItems': schema['prefixItems']}, path='items__prefixItems'): - yield error + if validator.is_type(items, "boolean") and 'prefixItems' in schema: + if not items: + if len(instance) > len(schema['prefixItems']): + yield ValidationError("%r has more items than defined in prefixItems" % instance) + else: + for error in validator.descend(instance, {'prefixItems': schema['prefixItems']}, path='items__prefixItems'): + yield error else: if 'prefixItems' in schema: for error in validator.descend(instance, {'prefixItems': schema['prefixItems']}, path='items__prefixItems'): @@ -497,17 +490,18 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema): if not validator.is_type(instance, "array"): return - if unevaluatedItems: - return + if validator.is_type(unevaluatedItems, "boolean"): + if unevaluatedItems: + return - # ToDo: Implement additional checks for "prefixItems", "items", "contains", "if", "then", "else", "allOf", "anyOf", - # "oneOf" and "not" keywords + evaluated_item_indexes = find_evaluated_item_indexes_by_schema(validator, instance, schema) + for k, v in enumerate(instance): + if k not in evaluated_item_indexes: + for error in validator.descend(v, unevaluatedItems, schema_path="unevaluatedItems"): + yield error def prefixItems(validator, prefixItems, instance, schema): - if "unevaluatedItems" in schema: - return - if not validator.is_type(instance, "array"): return diff --git a/jsonschema/validators.py b/jsonschema/validators.py index dae3ddb7d..8cc65be8e 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -364,7 +364,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"exclusiveMaximum": _validators.exclusiveMaximum, u"exclusiveMinimum": _validators.exclusiveMinimum, u"format": _validators.format, - u"items": _validators.items, + u"items": _legacy_validators.items_draft6_draft7, u"maxItems": _validators.maxItems, u"maxLength": _validators.maxLength, u"maxProperties": _validators.maxProperties, @@ -404,7 +404,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"exclusiveMinimum": _validators.exclusiveMinimum, u"format": _validators.format, u"if": _validators.if_, - u"items": _validators.items, + u"items": _legacy_validators.items_draft6_draft7, u"maxItems": _validators.maxItems, u"maxLength": _validators.maxLength, u"maxProperties": _validators.maxProperties, From 45ee736b9eaf5d5e0eafb1fbf7658582af3dd1ce Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Mon, 21 Jun 2021 11:15:28 +0200 Subject: [PATCH 09/27] Julian/jsonschema#782: Implements unevaluatedProperties validations --- jsonschema/_utils.py | 60 ++++++++++++++++++++++++++++++++++++++- jsonschema/_validators.py | 23 ++++++++++++++- jsonschema/validators.py | 1 + 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index 100b65368..be52f6670 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -238,7 +238,7 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): """ Get all indexes of items that get evaluated under the current schema - Covers are keywords related t unevaluatedItems: items, prefixItems, if, then, else, 'contains', 'unevaluatedItems', + Covers all keywords related to unevaluatedItems: items, prefixItems, if, then, else, 'contains', 'unevaluatedItems', 'allOf', 'oneOf', 'anyOf' """ if not validator.is_type(schema, "object"): @@ -274,3 +274,61 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, subschema) return evaluated_item_indexes + + +def find_evaluated_property_keys_by_schema(validator, instance, schema): + """ + Get all keys of items that get evaluated under the current schema + + Covers all keywords related to unevaluatedProperties: properties, 'additionalProperties', 'unevaluatedProperties', + patternProperties, dependentSchemas, 'allOf', 'oneOf', 'anyOf', if, then, else + """ + if not validator.is_type(schema, "object"): + return [] + evaluated_property_keys = [] + + for keyword in ['properties', 'additionalProperties', 'unevaluatedProperties']: + if keyword in schema: + if validator.is_type(schema[keyword], "boolean"): + for property, value in instance.items(): + if validator.is_valid({property: value}, schema[keyword]): + evaluated_property_keys.append(property) + + if validator.is_type(schema[keyword], "object"): + for property, subschema in schema[keyword].items(): + if property in instance and validator.is_valid(instance[property], subschema): + evaluated_property_keys.append(property) + + if 'patternProperties' in schema: + for property, value in instance.items(): + for pattern, subschema in schema['patternProperties'].items(): + if re.search(pattern, property): + if validator.is_valid({property: value}, schema['patternProperties']): + evaluated_property_keys.append(property) + + if 'dependentSchemas' in schema: + for property, subschema in schema['dependentSchemas'].items(): + if property not in instance: + continue + + errs = list(validator.descend(instance, subschema)) + if not errs: + evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, subschema) + + for keyword in ['allOf', 'oneOf', 'anyOf']: + if keyword in schema: + for subschema in schema[keyword]: + errs = list(validator.descend(instance, subschema)) + if not errs: + evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, subschema) + + if 'if' in schema: + if validator.is_valid(instance, schema['if']): + evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, schema['if']) + if 'then' in schema: + evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, schema['then']) + else: + if 'else' in schema: + evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, schema['else']) + + return evaluated_property_keys diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index b0a4f8843..24c56ecd1 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -7,6 +7,7 @@ extras_msg, find_additional_properties, find_evaluated_item_indexes_by_schema, + find_evaluated_property_keys_by_schema, types_msg, unbool, uniq, @@ -76,7 +77,11 @@ def items(validator, items, instance, schema): if len(instance) > len(schema['prefixItems']): yield ValidationError("%r has more items than defined in prefixItems" % instance) else: - for error in validator.descend(instance, {'prefixItems': schema['prefixItems']}, path='items__prefixItems'): + for error in validator.descend( + instance, + {'prefixItems': schema['prefixItems']}, + path='items__prefixItems', + ): yield error else: if 'prefixItems' in schema: @@ -501,6 +506,22 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema): yield error +def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): + if not validator.is_type(instance, "object"): + return + + evaluated_property_keys = find_evaluated_property_keys_by_schema(validator, instance, schema) + for property, subschema in instance.items(): + if property not in evaluated_property_keys: + for error in validator.descend( + instance[property], + unevaluatedProperties, + path=property, + schema_path=property, + ): + yield error + + def prefixItems(validator, prefixItems, instance, schema): if not validator.is_type(instance, "array"): return diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 8cc65be8e..19fe15e92 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -465,6 +465,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"type": _validators.type, u"uniqueItems": _validators.uniqueItems, u"unevaluatedItems": _validators.unevaluatedItems, + u"unevaluatedProperties": _validators.unevaluatedProperties, u"prefixItems": _validators.prefixItems, }, type_checker=_types.draft202012_type_checker, From 52d31345d621ced63e9d8b8c25660f5cbd6f821f Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Mon, 21 Jun 2021 16:07:41 +0200 Subject: [PATCH 10/27] Julian/jsonschema#782: Extend implementation of ref --- jsonschema/_utils.py | 25 ++++++++++++++++++++++++- jsonschema/_validators.py | 4 ---- jsonschema/validators.py | 34 ++++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index be52f6670..61d489541 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -248,8 +248,20 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): if 'items' in schema: return list(range(0, len(instance))) + if '$ref' in schema: + resolve = getattr(validator.resolver, "resolve", None) + if resolve: + scope, resolved = validator.resolver.resolve(schema['$ref']) + validator.resolver.push_scope(scope) + + try: + evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, resolved) + finally: + validator.resolver.pop_scope() + if 'prefixItems' in schema: - evaluated_item_indexes = list(range(0, len(schema['prefixItems']))) + if validator.is_valid(instance, {'prefixItems': schema['prefixItems']}): + evaluated_item_indexes = list(range(0, len(schema['prefixItems']))) if 'if' in schema: if validator.is_valid(instance, schema['if']): @@ -287,6 +299,17 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): return [] evaluated_property_keys = [] + if '$ref' in schema: + resolve = getattr(validator.resolver, "resolve", None) + if resolve: + scope, resolved = validator.resolver.resolve(schema['$ref']) + validator.resolver.push_scope(scope) + + try: + evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, resolved) + finally: + validator.resolver.pop_scope() + for keyword in ['properties', 'additionalProperties', 'unevaluatedProperties']: if keyword in schema: if validator.is_type(schema[keyword], "boolean"): diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 24c56ecd1..994e33570 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -495,10 +495,6 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema): if not validator.is_type(instance, "array"): return - if validator.is_type(unevaluatedItems, "boolean"): - if unevaluatedItems: - return - evaluated_item_indexes = find_evaluated_item_indexes_by_schema(validator, instance, schema) for k, v in enumerate(instance): if k not in evaluated_item_indexes: diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 19fe15e92..e76e02ec0 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -161,12 +161,7 @@ def iter_errors(self, instance, _schema=None): if scope: self.resolver.push_scope(scope) try: - ref = _schema.get(u"$ref") - if ref is not None: - validators = [(u"$ref", ref)] - else: - validators = _schema.items() - + validators = _schema.items() for k, v in validators: validator = self.VALIDATORS.get(k) if validator is None: @@ -644,11 +639,38 @@ def resolving(self, ref): finally: self.pop_scope() + def _finditem(self, schema, key): + results = [] + + if isinstance(schema, dict): + if key in schema: + results.append(schema) + + for k, v in schema.items(): + if isinstance(v, dict): + results += self._finditem(v, key) + + return results + + def resolve_local(self, ref, url, schema): + """ + Resolve the given reference within the schema + """ + for subschema in self._finditem(schema, "$id"): + if subschema['$id'] == ref or subschema['$id'] == url: + if self.cache_remote: + self.store[url] = schema + return subschema + def resolve(self, ref): """ Resolve the given reference. """ url = self._urljoin_cache(self.resolution_scope, ref) + local_resolve = self.resolve_local(ref, url, self.referrer) + + if local_resolve: + return url, local_resolve return url, self._remote_cache(url) def resolve_from_url(self, url): From 3d81c571a7a0f6e833801b1834914ebb491a872b Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Tue, 22 Jun 2021 10:28:16 +0200 Subject: [PATCH 11/27] Julian/jsonschema#782: Fixes ref validation priority --- jsonschema/_utils.py | 2 +- jsonschema/validators.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index 61d489541..2220f95f7 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -261,7 +261,7 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): if 'prefixItems' in schema: if validator.is_valid(instance, {'prefixItems': schema['prefixItems']}): - evaluated_item_indexes = list(range(0, len(schema['prefixItems']))) + evaluated_item_indexes += list(range(0, len(schema['prefixItems']))) if 'if' in schema: if validator.is_valid(instance, schema['if']): diff --git a/jsonschema/validators.py b/jsonschema/validators.py index e76e02ec0..c0f618388 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -161,7 +161,16 @@ def iter_errors(self, instance, _schema=None): if scope: self.resolver.push_scope(scope) try: - validators = _schema.items() + validators = [] + + # We make sure $ref is evaluated first if available in schema + ref = _schema.get(u"$ref") + if ref is not None: + validators = [(u"$ref", ref)] + + # Add all remaining validators + validators += [(x, _schema[x]) for x in _schema if x not in ["$ref"]] + for k, v in validators: validator = self.VALIDATORS.get(k) if validator is None: @@ -659,7 +668,7 @@ def resolve_local(self, ref, url, schema): for subschema in self._finditem(schema, "$id"): if subschema['$id'] == ref or subschema['$id'] == url: if self.cache_remote: - self.store[url] = schema + self.store[url] = subschema return subschema def resolve(self, ref): From eccb7ece1e0b8bd312d560587cfe2b9c2af660fa Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Tue, 22 Jun 2021 12:52:32 +0200 Subject: [PATCH 12/27] Julian/jsonschema#782: Fixes ref resolver for folders --- jsonschema/validators.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index c0f618388..239baa990 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -665,17 +665,24 @@ def resolve_local(self, ref, url, schema): """ Resolve the given reference within the schema """ + targets = [urldefrag(url), urldefrag(ref)] + for subschema in self._finditem(schema, "$id"): - if subschema['$id'] == ref or subschema['$id'] == url: - if self.cache_remote: - self.store[url] = subschema - return subschema + for uri, fragment in targets: + if subschema['$id'] == uri: + + if fragment: + subschema = self.resolve_fragment(subschema, fragment) + + if self.cache_remote: + self.store[url] = subschema + return subschema def resolve(self, ref): """ Resolve the given reference. """ - url = self._urljoin_cache(self.resolution_scope, ref) + url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/") local_resolve = self.resolve_local(ref, url, self.referrer) if local_resolve: From 11b77ba65ac8f2ff0850deba82812e8a8955b0a6 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Tue, 22 Jun 2021 15:56:35 +0200 Subject: [PATCH 13/27] Julian/jsonschema#782: Extend resolver for anchor --- jsonschema/validators.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 239baa990..4bc8d9613 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -661,29 +661,27 @@ def _finditem(self, schema, key): return results - def resolve_local(self, ref, url, schema): + def resolve_local(self, url, schema): """ Resolve the given reference within the schema """ - targets = [urldefrag(url), urldefrag(ref)] + uri, fragment = urldefrag(url) for subschema in self._finditem(schema, "$id"): - for uri, fragment in targets: - if subschema['$id'] == uri: + if self._urljoin_cache(self.resolution_scope, subschema['$id']).rstrip("/") == uri.rstrip("/"): + if fragment: + subschema = self.resolve_fragment(subschema, fragment) - if fragment: - subschema = self.resolve_fragment(subschema, fragment) - - if self.cache_remote: - self.store[url] = subschema - return subschema + if self.cache_remote: + self.store[url] = subschema + return subschema def resolve(self, ref): """ Resolve the given reference. """ url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/") - local_resolve = self.resolve_local(ref, url, self.referrer) + local_resolve = self.resolve_local(url, self.referrer) if local_resolve: return url, local_resolve @@ -720,8 +718,14 @@ def resolve_fragment(self, document, fragment): """ fragment = fragment.lstrip(u"/") - parts = unquote(fragment).split(u"/") if fragment else [] + # Resolve via anchor + for subschema in self._finditem(document, "$anchor"): + if fragment == subschema['$anchor']: + return subschema + + # Resolve via path + parts = unquote(fragment).split(u"/") if fragment else [] for part in parts: part = part.replace(u"~1", u"/").replace(u"~0", u"~") From 00614ccc4a8672bcdcdf87ea11687b1bb1e95278 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 23 Jun 2021 08:21:42 +0200 Subject: [PATCH 14/27] Julian/jsonschema#782: Implements defs validations --- jsonschema/_validators.py | 15 +++++++++++++++ jsonschema/validators.py | 1 + 2 files changed, 16 insertions(+) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 994e33570..448b3064d 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -384,6 +384,21 @@ def ref(validator, ref, instance, schema): validator.resolver.pop_scope() +def defs(validator, defs, instance, schema): + if not validator.is_type(instance, "object"): + return + + if '$defs' in instance: + for definition, subschema in instance['$defs'].items(): + for error in validator.descend( + subschema, + schema, + path=definition, + schema_path=definition, + ): + yield error + + def type(validator, types, instance, schema): types = ensure_list(types) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 4bc8d9613..3fc31495e 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -436,6 +436,7 @@ def extend(validator, validators=(), version=None, type_checker=None): meta_schema=_utils.load_schema("draft2020-12"), validators={ u"$ref": _validators.ref, + u"$defs": _validators.defs, u"additionalItems": _validators.additionalItems, u"additionalProperties": _validators.additionalProperties, u"allOf": _validators.allOf, From 73ec5a45f21112d9d8233f6e374acc2163a18dbd Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Thu, 24 Jun 2021 19:03:25 +0200 Subject: [PATCH 15/27] Julian/jsonschema#782: Add validation for uuid format --- jsonschema/_format.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/jsonschema/_format.py b/jsonschema/_format.py index 49b6b7121..f9825b0f5 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -1,3 +1,4 @@ +from uuid import UUID import datetime import ipaddress import re @@ -471,3 +472,15 @@ def is_duration(instance): if not isinstance(instance, str): return True return isoduration.parse_duration(instance) + + +@_checks_drafts( + draft202012="uuid", + raises=ValueError, +) +def is_uuid(instance): + if not isinstance(instance, str): + return True + if "-" not in instance: + raise ValueError("Invalid UUID format") + return UUID(instance) From f8555cd4a7458b0dd8c39f869d18c1b3cc67e816 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 23 Jun 2021 21:15:41 +0200 Subject: [PATCH 16/27] Julian/jsonschema#782: Implements dynamicRef validations --- jsonschema/_validators.py | 19 +++++++++++++++++++ .../tests/test_jsonschema_test_suite.py | 18 +++++++++++++++++- jsonschema/validators.py | 19 +++++++++++++++---- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 448b3064d..588d957b2 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -1,4 +1,5 @@ from fractions import Fraction +from urllib.parse import urldefrag, urljoin import re from jsonschema._utils import ( @@ -384,6 +385,24 @@ def ref(validator, ref, instance, schema): validator.resolver.pop_scope() +def dynamicRef(validator, dynamicRef, instance, schema): + _, fragment = urldefrag(dynamicRef) + scope_stack = validator.resolver.scopes_stack_copy + + for url in scope_stack: + with validator.resolver.resolving(urljoin(url, dynamicRef)) as lookup_schema: + if "$dynamicAnchor" in lookup_schema and fragment == lookup_schema["$dynamicAnchor"]: + subschema = lookup_schema + for error in validator.descend(instance, subschema): + yield error + break + else: + with validator.resolver.resolving(dynamicRef) as lookup_schema: + subschema = lookup_schema + for error in validator.descend(instance, subschema): + yield error + + def defs(validator, defs, instance, schema): if not validator.is_type(instance, "object"): return diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index a0a0cecc1..61a887b71 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -484,7 +484,23 @@ def ecmascript_regex_validation(test): subject="dynamicRef", case_description="multiple dynamic paths to the $dynamicRef " "keyword", - description='recurse to integerNode - floats are not allowed', + description="recurse to integerNode - floats are not allowed", + )(test) + or skip( + message="ToDo: Extend validation", + subject="dynamicRef", + case_description="after leaving a dynamic scope, it should not be " + "used by a $dynamicRef", + description="/then/$defs/thingy is the final stop for the " + "$dynamicRef", + )(test) + or skip( + message="ToDo: Extend validation", + subject="dynamicRef", + case_description="after leaving a dynamic scope, it should not be " + 'used by a $dynamicRef', + description="string matches /$defs/thingy, but the $dynamicRef " + "does not stop here", )(test) ), ) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 3fc31495e..9acf65ab5 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -437,6 +437,7 @@ def extend(validator, validators=(), version=None, type_checker=None): validators={ u"$ref": _validators.ref, u"$defs": _validators.defs, + u"$dynamicRef": _validators.dynamicRef, u"additionalItems": _validators.additionalItems, u"additionalProperties": _validators.additionalProperties, u"allOf": _validators.allOf, @@ -609,6 +610,13 @@ def resolution_scope(self): """ return self._scopes_stack[-1] + @property + def scopes_stack_copy(self): + """ + Retrieve a copy of the stack of resolution scopes. + """ + return self._scopes_stack.copy() + @property def base_uri(self): """ @@ -677,6 +685,7 @@ def resolve_local(self, url, schema): self.store[url] = subschema return subschema + def resolve(self, ref): """ Resolve the given reference. @@ -720,10 +729,12 @@ def resolve_fragment(self, document, fragment): fragment = fragment.lstrip(u"/") - # Resolve via anchor - for subschema in self._finditem(document, "$anchor"): - if fragment == subschema['$anchor']: - return subschema + # Resolve fragment via $anchor or $dynamicAnchor + if fragment: + for keyword in ["$anchor", "$dynamicAnchor"]: + for subschema in self._finditem(document, keyword): + if fragment == subschema[keyword]: + return subschema # Resolve via path parts = unquote(fragment).split(u"/") if fragment else [] From df1953ff50c16d7bad396b4e38677bbc0502c182 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Fri, 25 Jun 2021 08:41:19 +0200 Subject: [PATCH 17/27] Julian/jsonschema#782: Load dependencies from legacy validators --- jsonschema/_legacy_validators.py | 26 ++++++++++++++++++++++++++ jsonschema/_validators.py | 24 ------------------------ jsonschema/validators.py | 6 +++--- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index b33715169..8e4619091 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -27,6 +27,32 @@ def dependencies_draft3(validator, dependencies, instance, schema): yield ValidationError(message % (each, property)) +def dependencies_draft4_draft6_draft7(validator, dependencies, instance, schema): + """ + Support for the ``dependencies`` validator from pre-draft 2019-09. + + In later drafts, the validator was split into separate ``dependentRequired`` + and ``dependentSchemas`` validators. + """ + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependencies.items(): + if property not in instance: + continue + + if validator.is_type(dependency, "array"): + for each in dependency: + if each not in instance: + message = "%r is a dependency of %r" + yield ValidationError(message % (each, property)) + else: + for error in validator.descend( + instance, dependency, schema_path=property, + ): + yield error + + def disallow_draft3(validator, disallow, instance, schema): for disallowed in _utils.ensure_list(disallow): if validator.is_valid(instance, {"type": [disallowed]}): diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 588d957b2..093aaba1a 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -301,30 +301,6 @@ def maxLength(validator, mL, instance, schema): yield ValidationError("%r is too long" % (instance,)) -def dependencies(validator, dependencies, instance, schema): - """ - The dependencies keyword has been deprecated since draft 2019-09 and has been split into dependentRequired - and dependentSchemas. - """ - if not validator.is_type(instance, "object"): - return - - for property, dependency in dependencies.items(): - if property not in instance: - continue - - if validator.is_type(dependency, "array"): - for each in dependency: - if each not in instance: - message = "%r is a dependency of %r" - yield ValidationError(message % (each, property)) - else: - for error in validator.descend( - instance, dependency, schema_path=property, - ): - yield error - - def dependentRequired(validator, dependentRequired, instance, schema): """ Split from dependencies diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 9acf65ab5..30f011607 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -326,7 +326,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"additionalProperties": _validators.additionalProperties, u"allOf": _validators.allOf, u"anyOf": _validators.anyOf, - u"dependencies": _validators.dependencies, + u"dependencies": _legacy_validators.dependencies_draft4_draft6_draft7, u"enum": _validators.enum, u"format": _validators.format, u"items": _legacy_validators.items_draft3_draft4, @@ -363,7 +363,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"anyOf": _validators.anyOf, u"const": _validators.const, u"contains": _legacy_validators.contains_draft6_draft7, - u"dependencies": _validators.dependencies, + u"dependencies": _legacy_validators.dependencies_draft4_draft6_draft7, u"enum": _validators.enum, u"exclusiveMaximum": _validators.exclusiveMaximum, u"exclusiveMinimum": _validators.exclusiveMinimum, @@ -402,7 +402,7 @@ def extend(validator, validators=(), version=None, type_checker=None): u"anyOf": _validators.anyOf, u"const": _validators.const, u"contains": _legacy_validators.contains_draft6_draft7, - u"dependencies": _validators.dependencies, + u"dependencies": _legacy_validators.dependencies_draft4_draft6_draft7, u"enum": _validators.enum, u"exclusiveMaximum": _validators.exclusiveMaximum, u"exclusiveMinimum": _validators.exclusiveMinimum, From 16d00de8bf5c47ff6dfa5b9ca04938ec2129f0d2 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Fri, 25 Jun 2021 12:45:00 +0200 Subject: [PATCH 18/27] Julian/jsonschema#782: Fixes relative json pointer format validation for leading zero on digit --- jsonschema/_format.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jsonschema/_format.py b/jsonschema/_format.py index f9825b0f5..05b0e76af 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -432,6 +432,10 @@ def is_relative_json_pointer(instance): non_negative_integer, rest = [], "" for i, character in enumerate(instance): if character.isdigit(): + # digits with a leading "0" are not allowed + if i > 0 and int(instance[i-1]) == 0: + return False + non_negative_integer.append(character) continue From 98b49be3f533c4acaa6524d0adeb384e89002474 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Fri, 25 Jun 2021 14:40:34 +0200 Subject: [PATCH 19/27] Julian/jsonschema#782: Adapt validator test for draft2020-12, fixes code styles --- jsonschema/_format.py | 12 ++-- jsonschema/_legacy_validators.py | 11 +++- jsonschema/_utils.py | 86 +++++++++++++++++++---------- jsonschema/_validators.py | 56 +++++++++++-------- jsonschema/tests/test_validators.py | 19 ++++++- jsonschema/validators.py | 6 +- 6 files changed, 128 insertions(+), 62 deletions(-) diff --git a/jsonschema/_format.py b/jsonschema/_format.py index 05b0e76af..19f428302 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -168,14 +168,16 @@ def wrap(func): if draft7: func = _draft_checkers["draft7"].checks(draft7, raises)(func) if draft202012: - func = _draft_checkers["draft202012"].checks(draft202012, raises)(func) + func = _draft_checkers["draft202012"].checks( + draft202012, raises + )(func) # Oy. This is bad global state, but relied upon for now, until # deprecation. See https://github.com/Julian/jsonschema/issues/519 # and test_format_checkers_come_with_defaults - FormatChecker.cls_checks(draft202012 or draft7 or draft6 or draft4 or draft3, raises)( - func, - ) + FormatChecker.cls_checks( + draft202012 or draft7 or draft6 or draft4 or draft3, raises + )(func) return func return wrap @@ -465,7 +467,7 @@ def is_uri_template(instance): try: import isoduration -except ImportError: +except ImportError: # pragma: no cover pass else: @_checks_drafts( diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index 8e4619091..54b5fad46 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -27,12 +27,17 @@ def dependencies_draft3(validator, dependencies, instance, schema): yield ValidationError(message % (each, property)) -def dependencies_draft4_draft6_draft7(validator, dependencies, instance, schema): +def dependencies_draft4_draft6_draft7( + validator, + dependencies, + instance, + schema, +): """ Support for the ``dependencies`` validator from pre-draft 2019-09. - In later drafts, the validator was split into separate ``dependentRequired`` - and ``dependentSchemas`` validators. + In later drafts, the validator was split into separate + ``dependentRequired`` and ``dependentSchemas`` validators. """ if not validator.is_type(instance, "object"): return diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index 2220f95f7..fa928e6e7 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -238,12 +238,12 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): """ Get all indexes of items that get evaluated under the current schema - Covers all keywords related to unevaluatedItems: items, prefixItems, if, then, else, 'contains', 'unevaluatedItems', - 'allOf', 'oneOf', 'anyOf' + Covers all keywords related to unevaluatedItems: items, prefixItems, if, + then, else, 'contains', 'unevaluatedItems', 'allOf', 'oneOf', 'anyOf' """ if not validator.is_type(schema, "object"): return [] - evaluated_item_indexes = [] + evaluated_indexes = [] if 'items' in schema: return list(range(0, len(instance))) @@ -255,49 +255,61 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): validator.resolver.push_scope(scope) try: - evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, resolved) + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, resolved) finally: validator.resolver.pop_scope() if 'prefixItems' in schema: - if validator.is_valid(instance, {'prefixItems': schema['prefixItems']}): - evaluated_item_indexes += list(range(0, len(schema['prefixItems']))) + if validator.is_valid( + instance, {'prefixItems': schema['prefixItems']} + ): + evaluated_indexes += list(range(0, len(schema['prefixItems']))) if 'if' in schema: if validator.is_valid(instance, schema['if']): - evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, schema['if']) + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema['if'] + ) if 'then' in schema: - evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, schema['then']) + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema['then'] + ) else: if 'else' in schema: - evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, schema['else']) + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema['else'] + ) for keyword in ['contains', 'unevaluatedItems']: if keyword in schema: for k, v in enumerate(instance): if validator.is_valid(v, schema[keyword]): - evaluated_item_indexes.append(k) + evaluated_indexes.append(k) for keyword in ['allOf', 'oneOf', 'anyOf']: if keyword in schema: for subschema in schema[keyword]: errs = list(validator.descend(instance, subschema)) if not errs: - evaluated_item_indexes += find_evaluated_item_indexes_by_schema(validator, instance, subschema) + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, subschema + ) - return evaluated_item_indexes + return evaluated_indexes def find_evaluated_property_keys_by_schema(validator, instance, schema): """ Get all keys of items that get evaluated under the current schema - Covers all keywords related to unevaluatedProperties: properties, 'additionalProperties', 'unevaluatedProperties', - patternProperties, dependentSchemas, 'allOf', 'oneOf', 'anyOf', if, then, else + Covers all keywords related to unevaluatedProperties: properties, + 'additionalProperties', 'unevaluatedProperties', patternProperties, + dependentSchemas, 'allOf', 'oneOf', 'anyOf', if, then, else """ if not validator.is_type(schema, "object"): return [] - evaluated_property_keys = [] + evaluated_keys = [] if '$ref' in schema: resolve = getattr(validator.resolver, "resolve", None) @@ -306,28 +318,36 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): validator.resolver.push_scope(scope) try: - evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, resolved) + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, resolved + ) finally: validator.resolver.pop_scope() - for keyword in ['properties', 'additionalProperties', 'unevaluatedProperties']: + for keyword in [ + 'properties', 'additionalProperties', 'unevaluatedProperties' + ]: if keyword in schema: if validator.is_type(schema[keyword], "boolean"): for property, value in instance.items(): if validator.is_valid({property: value}, schema[keyword]): - evaluated_property_keys.append(property) + evaluated_keys.append(property) if validator.is_type(schema[keyword], "object"): for property, subschema in schema[keyword].items(): - if property in instance and validator.is_valid(instance[property], subschema): - evaluated_property_keys.append(property) + if property in instance and validator.is_valid( + instance[property], subschema + ): + evaluated_keys.append(property) if 'patternProperties' in schema: for property, value in instance.items(): for pattern, subschema in schema['patternProperties'].items(): if re.search(pattern, property): - if validator.is_valid({property: value}, schema['patternProperties']): - evaluated_property_keys.append(property) + if validator.is_valid( + {property: value}, schema['patternProperties'] + ): + evaluated_keys.append(property) if 'dependentSchemas' in schema: for property, subschema in schema['dependentSchemas'].items(): @@ -336,22 +356,32 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): errs = list(validator.descend(instance, subschema)) if not errs: - evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, subschema) + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema + ) for keyword in ['allOf', 'oneOf', 'anyOf']: if keyword in schema: for subschema in schema[keyword]: errs = list(validator.descend(instance, subschema)) if not errs: - evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, subschema) + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema + ) if 'if' in schema: if validator.is_valid(instance, schema['if']): - evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, schema['if']) + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema['if'] + ) if 'then' in schema: - evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, schema['then']) + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema['then'] + ) else: if 'else' in schema: - evaluated_property_keys += find_evaluated_property_keys_by_schema(validator, instance, schema['else']) + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema['else'] + ) - return evaluated_property_keys + return evaluated_keys diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 093aaba1a..3d4f4b8d0 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -76,7 +76,9 @@ def items(validator, items, instance, schema): if validator.is_type(items, "boolean") and 'prefixItems' in schema: if not items: if len(instance) > len(schema['prefixItems']): - yield ValidationError("%r has more items than defined in prefixItems" % instance) + yield ValidationError( + "%r has more items than defined in prefixItems" % instance + ) else: for error in validator.descend( instance, @@ -86,7 +88,11 @@ def items(validator, items, instance, schema): yield error else: if 'prefixItems' in schema: - for error in validator.descend(instance, {'prefixItems': schema['prefixItems']}, path='items__prefixItems'): + for error in validator.descend( + instance, + {'prefixItems': schema['prefixItems']}, + path='items__prefixItems', + ): yield error # Remove evaluated prefixItems indexes @@ -130,25 +136,18 @@ def contains(validator, contains, instance, schema): if 'minContains' in schema: min_contains = schema['minContains'] - if not validator.is_type(min_contains, "integer"): - yield ValidationError( - "minContains of %r in not valid under the given schema" % (min_contains,) - ) - return if 'maxContains' in schema: max_contains = schema['maxContains'] - if not validator.is_type(max_contains, "integer"): - yield ValidationError( - "maxContains of %r is not valid under the given schema" % (instance,) - ) - return # minContains set to 0 will ignore contains if min_contains == 0: return - matches = len(list(filter(lambda x: x, [validator.is_valid(element, contains) for element in instance]))) + matches = len(list( + filter(lambda x: x, [validator.is_valid(element, contains) for + element in instance])) + ) # default contains behavior if not matches: @@ -160,7 +159,8 @@ def contains(validator, contains, instance, schema): if min_contains and max_contains is None: if matches < min_contains: yield ValidationError( - "Invalid number or matches of %r under the given schema, expected min %d, got %d" % ( + "Invalid number or matches of %r under the given schema, " + "expected min %d, got %d" % ( instance, min_contains, matches ) ) @@ -169,7 +169,8 @@ def contains(validator, contains, instance, schema): if min_contains is None and max_contains: if matches > max_contains: yield ValidationError( - "Invalid number or matches of %r under the given schema, expected max %d, got %d" % ( + "Invalid number or matches of %r under the given schema, " + "expected max %d, got %d" % ( instance, max_contains, matches ) ) @@ -178,7 +179,8 @@ def contains(validator, contains, instance, schema): if min_contains and max_contains: if matches < min_contains or matches > max_contains: yield ValidationError( - "Invalid number or matches of %r under the given schema, expected min %d and max %d, got %d" % ( + "Invalid number or matches of %r under the given schema, " + "expected min %d and max %d, got %d" % ( instance, min_contains, max_contains, matches ) ) @@ -366,8 +368,10 @@ def dynamicRef(validator, dynamicRef, instance, schema): scope_stack = validator.resolver.scopes_stack_copy for url in scope_stack: - with validator.resolver.resolving(urljoin(url, dynamicRef)) as lookup_schema: - if "$dynamicAnchor" in lookup_schema and fragment == lookup_schema["$dynamicAnchor"]: + lookup_url = urljoin(url, dynamicRef) + with validator.resolver.resolving(lookup_url) as lookup_schema: + if "$dynamicAnchor" in lookup_schema \ + and fragment == lookup_schema["$dynamicAnchor"]: subschema = lookup_schema for error in validator.descend(instance, subschema): yield error @@ -505,10 +509,14 @@ 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) + evaluated_item_indexes = find_evaluated_item_indexes_by_schema( + validator, instance, schema + ) for k, v in enumerate(instance): if k not in evaluated_item_indexes: - for error in validator.descend(v, unevaluatedItems, schema_path="unevaluatedItems"): + for error in validator.descend( + v, unevaluatedItems, schema_path="unevaluatedItems" + ): yield error @@ -516,7 +524,9 @@ def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): if not validator.is_type(instance, "object"): return - evaluated_property_keys = find_evaluated_property_keys_by_schema(validator, instance, schema) + evaluated_property_keys = find_evaluated_property_keys_by_schema( + validator, instance, schema + ) for property, subschema in instance.items(): if property not in evaluated_property_keys: for error in validator.descend( @@ -534,5 +544,7 @@ def prefixItems(validator, prefixItems, instance, schema): for k, v in enumerate(instance): if k < len(prefixItems): - for error in validator.descend(v, prefixItems[k], schema_path="prefixItems"): + for error in validator.descend( + v, prefixItems[k], schema_path="prefixItems" + ): yield error diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index af5dfebce..81fb28e32 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1410,8 +1410,23 @@ def test_draft7_validator_is_chosen(self): Validator=validators.Draft7Validator, ) - def test_draft7_validator_is_the_default(self): - self.assertUses(schema={}, Validator=validators.Draft7Validator) + def test_draft202012_validator_is_chosen(self): + self.assertUses( + schema={ + "$schema": "https://json-schema.org/draft/2020-12/schema#" + }, + Validator=validators.Draft202012Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={ + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + Validator=validators.Draft202012Validator, + ) + + def test_draft202012_validator_is_the_default(self): + self.assertUses(schema={}, Validator=validators.Draft202012Validator) def test_validation_error_message(self): with self.assertRaises(exceptions.ValidationError) as e: diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 30f011607..328f6bc20 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -677,7 +677,10 @@ def resolve_local(self, url, schema): uri, fragment = urldefrag(url) for subschema in self._finditem(schema, "$id"): - if self._urljoin_cache(self.resolution_scope, subschema['$id']).rstrip("/") == uri.rstrip("/"): + target_uri = self._urljoin_cache( + self.resolution_scope, subschema['$id'] + ) + if target_uri.rstrip("/") == uri.rstrip("/"): if fragment: subschema = self.resolve_fragment(subschema, fragment) @@ -685,7 +688,6 @@ def resolve_local(self, url, schema): self.store[url] = subschema return subschema - def resolve(self, ref): """ Resolve the given reference. From 95742b4573b81d0b1e3a4f4be8c1f4238689eb6c Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Sat, 26 Jun 2021 07:52:11 +0200 Subject: [PATCH 20/27] Julian/jsonschema#782: Fixes failing styles --- jsonschema/tests/test_cli.py | 6 +++--- jsonschema/tests/test_validators.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py index 884a037ab..ae4f13d7e 100644 --- a/jsonschema/tests/test_cli.py +++ b/jsonschema/tests/test_cli.py @@ -10,7 +10,7 @@ import sys import tempfile -from jsonschema import Draft4Validator, Draft7Validator, __version__, cli +from jsonschema import Draft4Validator, Draft202012Validator, __version__, cli from jsonschema.exceptions import ( RefResolutionError, SchemaError, @@ -336,7 +336,7 @@ def test_invalid_schema_multiple_errors(self): exit_code=1, stderr="""\ - 57: 57 is not valid under any of the given schemas + 57: 57 is not of type 'object', 'boolean' """, ) @@ -776,7 +776,7 @@ def test_it_validates_using_the_latest_validator_when_unspecified(self): # is hidden inside the CLI, so guard that that's the case, and # this test will have to be updated when versions change until # we can think of a better way to ensure this behavior. - self.assertIs(Draft7Validator, _LATEST_VERSION) + self.assertIs(Draft202012Validator, _LATEST_VERSION) self.assertOutputs( files=dict(some_schema='{"const": "check"}', some_instance='"a"'), diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 81fb28e32..fc6a23971 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1244,6 +1244,12 @@ class TestDraft7Validator(ValidatorTestMixin, TestCase): invalid = {"type": "integer"}, "foo" +class TestDraft202012Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft202012Validator + valid = {}, {} + invalid = {"type": "integer"}, "foo" + + class TestValidatorFor(SynchronousTestCase): def test_draft_3(self): schema = {"$schema": "http://json-schema.org/draft-03/schema"} @@ -1297,6 +1303,19 @@ def test_draft_7(self): validators.Draft7Validator, ) + def test_draft_202012(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft202012Validator, + ) + + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft202012Validator, + ) + def test_True(self): self.assertIs( validators.validator_for(True), From 769478dc39395e16bce1943c691abf5719cf5bb7 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 30 Jun 2021 14:05:46 +0200 Subject: [PATCH 21/27] Julian/jsonschema#782: Update validation message for unevaluatedProperties and unevaluatedItems --- jsonschema/_validators.py | 14 ++++++++++-- jsonschema/tests/test_validators.py | 34 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 3d4f4b8d0..c2c136fa7 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -512,12 +512,17 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema): evaluated_item_indexes = find_evaluated_item_indexes_by_schema( validator, instance, schema ) + unevaluated_items = [] for k, v in enumerate(instance): if k not in evaluated_item_indexes: for error in validator.descend( v, unevaluatedItems, schema_path="unevaluatedItems" ): - yield error + unevaluated_items.append(v) + + if len(unevaluated_items): + error = "Unevaluated items are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(unevaluated_items)) def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): @@ -527,6 +532,7 @@ def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): evaluated_property_keys = find_evaluated_property_keys_by_schema( validator, instance, schema ) + unevaluated_property_keys = [] for property, subschema in instance.items(): if property not in evaluated_property_keys: for error in validator.descend( @@ -535,7 +541,11 @@ def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): path=property, schema_path=property, ): - yield error + unevaluated_property_keys.append(property) + + if len(unevaluated_property_keys): + error = "Unevaluated properties are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(unevaluated_property_keys)) def prefixItems(validator, prefixItems, instance, schema): diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index fc6a23971..bb3fefb30 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -383,6 +383,40 @@ def test_False_schema(self): ) self.assertIn("False schema does not allow 'something'", message) + def test_unevaluated_properties(self): + schema = { + "type": "object", + "unevaluatedProperties": False + } + message = self.message_for( + instance={ + "foo": "foo", + "bar": "bar", + }, + schema=schema, + cls=validators.Draft202012Validator, + ) + self.assertIn( + "Unevaluated properties are not allowed " + "('foo', 'bar' were unexpected)", + message, + ) + + def test_unevaluated_items(self): + schema = { + "type": "array", + "unevaluatedItems": False + } + message = self.message_for( + instance=["foo", "bar"], + schema=schema, + cls=validators.Draft202012Validator, + ) + self.assertIn( + "Unevaluated items are not allowed ('foo', 'bar' were unexpected)", + message, + ) + class TestValidationErrorDetails(TestCase): # TODO: These really need unit tests for each individual validator, rather From c380b09ea9aeb9eb3a5c8ac92f0245843b113aa7 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Thu, 1 Jul 2021 09:04:11 +0200 Subject: [PATCH 22/27] Julian/jsonschema#782: Refactor items behavior with prefixItems --- jsonschema/_validators.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index c2c136fa7..7acd178fb 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -79,26 +79,11 @@ def items(validator, items, instance, schema): yield ValidationError( "%r has more items than defined in prefixItems" % instance ) - else: - for error in validator.descend( - instance, - {'prefixItems': schema['prefixItems']}, - path='items__prefixItems', - ): - yield error else: - if 'prefixItems' in schema: - for error in validator.descend( - instance, - {'prefixItems': schema['prefixItems']}, - path='items__prefixItems', - ): - yield error - - # Remove evaluated prefixItems indexes - del instance[0:len(schema['prefixItems'])] + non_prefixed_items = instance[len(schema['prefixItems']):] \ + if 'prefixItems' in schema else instance - for index, item in enumerate(instance): + for index, item in enumerate(non_prefixed_items): for error in validator.descend(item, items, path=index): yield error From bede403b64d5044396af891a40e9670d0d3bb549 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Mon, 28 Jun 2021 14:43:38 +0200 Subject: [PATCH 23/27] Julian/jsonschema#782: Resolve meta schema vocabularies from local cache --- jsonschema/_utils.py | 16 +++ .../schemas/draft2020-12/applicator.json | 48 +++++++++ jsonschema/schemas/draft2020-12/content.json | 17 ++++ jsonschema/schemas/draft2020-12/core.json | 51 ++++++++++ .../draft2020-12/format-annotation.json | 14 +++ .../schemas/draft2020-12/meta-data.json | 37 +++++++ .../schemas/draft2020-12/unevaluated.json | 15 +++ .../schemas/draft2020-12/validation.json | 98 +++++++++++++++++++ jsonschema/validators.py | 24 ++++- setup.cfg | 2 +- 10 files changed, 317 insertions(+), 5 deletions(-) create mode 100644 jsonschema/schemas/draft2020-12/applicator.json create mode 100644 jsonschema/schemas/draft2020-12/content.json create mode 100644 jsonschema/schemas/draft2020-12/core.json create mode 100644 jsonschema/schemas/draft2020-12/format-annotation.json create mode 100644 jsonschema/schemas/draft2020-12/meta-data.json create mode 100644 jsonschema/schemas/draft2020-12/unevaluated.json create mode 100644 jsonschema/schemas/draft2020-12/validation.json diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index fa928e6e7..e963e390b 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -1,9 +1,12 @@ from collections.abc import Mapping, MutableMapping, Sequence +from pathlib import Path from urllib.parse import urlsplit import itertools import json +import os import pkgutil import re +import sys class URIDict(MutableMapping): @@ -55,6 +58,19 @@ def load_schema(name): return json.loads(data.decode("utf-8")) +def load_vocabulary(name): + """ + Load all schema files from ./schemas/``name`` and return them as a list. + """ + vocabulary = [] + base_path = os.path.dirname(sys.modules["jsonschema"].__file__) + pathlist = Path(os.path.join(base_path, 'schemas', name)).glob('*.json') + for path in pathlist: + with open(path) as data: + vocabulary.append(json.load(data)) + return vocabulary + + def format_as_index(indices): """ Construct a single string containing indexing operations for the indices. diff --git a/jsonschema/schemas/draft2020-12/applicator.json b/jsonschema/schemas/draft2020-12/applicator.json new file mode 100644 index 000000000..ca6992309 --- /dev/null +++ b/jsonschema/schemas/draft2020-12/applicator.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/applicator", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/applicator": true + }, + "$dynamicAnchor": "meta", + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "prefixItems": { "$ref": "#/$defs/schemaArray" }, + "items": { "$dynamicRef": "#meta" }, + "contains": { "$dynamicRef": "#meta" }, + "additionalProperties": { "$dynamicRef": "#meta" }, + "properties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "propertyNames": { "$dynamicRef": "#meta" }, + "if": { "$dynamicRef": "#meta" }, + "then": { "$dynamicRef": "#meta" }, + "else": { "$dynamicRef": "#meta" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$dynamicRef": "#meta" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$dynamicRef": "#meta" } + } + } +} diff --git a/jsonschema/schemas/draft2020-12/content.json b/jsonschema/schemas/draft2020-12/content.json new file mode 100644 index 000000000..2f6e056a9 --- /dev/null +++ b/jsonschema/schemas/draft2020-12/content.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/content", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentEncoding": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentSchema": { "$dynamicRef": "#meta" } + } +} diff --git a/jsonschema/schemas/draft2020-12/core.json b/jsonschema/schemas/draft2020-12/core.json new file mode 100644 index 000000000..dfc092d96 --- /dev/null +++ b/jsonschema/schemas/draft2020-12/core.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/core", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true + }, + "$dynamicAnchor": "meta", + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "$ref": "#/$defs/uriReferenceString", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { "$ref": "#/$defs/uriString" }, + "$ref": { "$ref": "#/$defs/uriReferenceString" }, + "$anchor": { "$ref": "#/$defs/anchorString" }, + "$dynamicRef": { "$ref": "#/$defs/uriReferenceString" }, + "$dynamicAnchor": { "$ref": "#/$defs/anchorString" }, + "$vocabulary": { + "type": "object", + "propertyNames": { "$ref": "#/$defs/uriString" }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" } + } + }, + "$defs": { + "anchorString": { + "type": "string", + "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$" + }, + "uriString": { + "type": "string", + "format": "uri" + }, + "uriReferenceString": { + "type": "string", + "format": "uri-reference" + } + } +} diff --git a/jsonschema/schemas/draft2020-12/format-annotation.json b/jsonschema/schemas/draft2020-12/format-annotation.json new file mode 100644 index 000000000..51ef7ea11 --- /dev/null +++ b/jsonschema/schemas/draft2020-12/format-annotation.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true + }, + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for annotation results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/jsonschema/schemas/draft2020-12/meta-data.json b/jsonschema/schemas/draft2020-12/meta-data.json new file mode 100644 index 000000000..05cbc22af --- /dev/null +++ b/jsonschema/schemas/draft2020-12/meta-data.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/meta-data", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/meta-data": true + }, + "$dynamicAnchor": "meta", + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/jsonschema/schemas/draft2020-12/unevaluated.json b/jsonschema/schemas/draft2020-12/unevaluated.json new file mode 100644 index 000000000..5f62a3ffa --- /dev/null +++ b/jsonschema/schemas/draft2020-12/unevaluated.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true + }, + "$dynamicAnchor": "meta", + + "title": "Unevaluated applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "unevaluatedItems": { "$dynamicRef": "#meta" }, + "unevaluatedProperties": { "$dynamicRef": "#meta" } + } +} diff --git a/jsonschema/schemas/draft2020-12/validation.json b/jsonschema/schemas/draft2020-12/validation.json new file mode 100644 index 000000000..606b87ba2 --- /dev/null +++ b/jsonschema/schemas/draft2020-12/validation.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "$dynamicAnchor": "meta", + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 328f6bc20..3da509141 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -26,6 +26,7 @@ validators = {} meta_schemas = _utils.URIDict() +vocabulary_schemas = _utils.URIDict() def validates(version): @@ -53,6 +54,12 @@ def _validates(cls): meta_schema_id = cls.ID_OF(cls.META_SCHEMA) if meta_schema_id: meta_schemas[meta_schema_id] = cls + + for vocabulary in cls.VOCABULARY_SCHEMAS: + vocabulary_id = cls.ID_OF(vocabulary) + if vocabulary_id: + vocabulary_schemas[vocabulary_id] = vocabulary + return cls return _validates @@ -63,8 +70,18 @@ def _id_of(schema): return schema.get(u"$id", u"") +def _store_schema_list(): + return [ + (id, validator.META_SCHEMA) + for id, validator in meta_schemas.items() + ] + [ + (id, schema) for id, schema in vocabulary_schemas.items() + ] + + def create( meta_schema, + vocabulary_schemas=(), validators=(), version=None, type_checker=_types.draft7_type_checker, @@ -120,6 +137,7 @@ class Validator: VALIDATORS = dict(validators) META_SCHEMA = dict(meta_schema) + VOCABULARY_SCHEMAS = list(vocabulary_schemas) TYPE_CHECKER = type_checker ID_OF = staticmethod(id_of) @@ -434,6 +452,7 @@ def extend(validator, validators=(), version=None, type_checker=None): Draft202012Validator = create( meta_schema=_utils.load_schema("draft2020-12"), + vocabulary_schemas=_utils.load_vocabulary("draft2020-12"), validators={ u"$ref": _validators.ref, u"$defs": _validators.defs, @@ -545,10 +564,7 @@ def __init__( self.handlers = dict(handlers) self._scopes_stack = [base_uri] - self.store = _utils.URIDict( - (id, validator.META_SCHEMA) - for id, validator in meta_schemas.items() - ) + self.store = _utils.URIDict(_store_schema_list()) self.store.update(store) self.store[base_uri] = referrer diff --git a/setup.cfg b/setup.cfg index a53aea0cd..a4c1fb452 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,7 @@ console_scripts = jsonschema = jsonschema.cli:main [options.package_data] -jsonschema = schemas/*.json +jsonschema = schemas/*.json, schemas/draft2020-12/*.json [bdist_wheel] universal = 1 From 1caee547d3fc7a7d59d1e35ee098dc3a2c3667a0 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 14 Jul 2021 16:32:28 +0200 Subject: [PATCH 24/27] Julian/jsonschema#782: Extend format tests --- jsonschema/tests/test_format.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py index 6dba484e6..f4cec9d3c 100644 --- a/jsonschema/tests/test_format.py +++ b/jsonschema/tests/test_format.py @@ -86,3 +86,22 @@ def test_repr(self): repr(checker), "", ) + + def test_duration_format(self): + try: + from jsonschema._format import is_duration # noqa: F401 + except ImportError: # pragma: no cover + pass + else: + checker = FormatChecker() + self.assertTrue(checker.conforms(1, "duration")) + self.assertTrue(checker.conforms("P4Y", "duration")) + self.assertFalse(checker.conforms("test", "duration")) + + def test_uuid_format(self): + checker = FormatChecker() + self.assertTrue(checker.conforms(1, "uuid")) + self.assertTrue( + checker.conforms("6e6659ec-4503-4428-9f03-2e2ea4d6c278", "uuid") + ) + self.assertFalse(checker.conforms("test", "uuid")) From 841a0a2ad5fac6dafed85ea395e7aa9176084549 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 14 Jul 2021 14:56:31 +0200 Subject: [PATCH 25/27] Julian/jsonschema#782: Code clenaup, fixes validation messages --- jsonschema/_legacy_validators.py | 8 +- jsonschema/_utils.py | 125 ++++++++++++++----------------- jsonschema/_validators.py | 71 +++++++----------- jsonschema/tests/test_format.py | 6 +- jsonschema/validators.py | 10 +-- setup.cfg | 2 +- 6 files changed, 93 insertions(+), 129 deletions(-) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index 54b5fad46..b707889be 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -28,10 +28,10 @@ def dependencies_draft3(validator, dependencies, instance, schema): def dependencies_draft4_draft6_draft7( - validator, - dependencies, - instance, - schema, + validator, + dependencies, + instance, + schema, ): """ Support for the ``dependencies`` validator from pre-draft 2019-09. diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index e963e390b..1b81a09a9 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -255,55 +255,50 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema): Get all indexes of items that get evaluated under the current schema Covers all keywords related to unevaluatedItems: items, prefixItems, if, - then, else, 'contains', 'unevaluatedItems', 'allOf', 'oneOf', 'anyOf' + then, else, contains, unevaluatedItems, allOf, oneOf, anyOf """ - if not validator.is_type(schema, "object"): + if validator.is_type(schema, "boolean"): return [] evaluated_indexes = [] - if 'items' in schema: + if "items" in schema: return list(range(0, len(instance))) - if '$ref' in schema: - resolve = getattr(validator.resolver, "resolve", None) - if resolve: - scope, resolved = validator.resolver.resolve(schema['$ref']) - validator.resolver.push_scope(scope) + if "$ref" in schema: + scope, resolved = validator.resolver.resolve(schema["$ref"]) + validator.resolver.push_scope(scope) - try: - evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, resolved) - finally: - validator.resolver.pop_scope() - - if 'prefixItems' in schema: - if validator.is_valid( - instance, {'prefixItems': schema['prefixItems']} - ): - evaluated_indexes += list(range(0, len(schema['prefixItems']))) - - if 'if' in schema: - if validator.is_valid(instance, schema['if']): + try: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, resolved) + finally: + validator.resolver.pop_scope() + + if "prefixItems" in schema: + evaluated_indexes += list(range(0, len(schema["prefixItems"]))) + + if "if" in schema: + if validator.is_valid(instance, schema["if"]): evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, schema['if'] + validator, instance, schema["if"] ) - if 'then' in schema: + if "then" in schema: evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, schema['then'] + validator, instance, schema["then"] ) else: - if 'else' in schema: + if "else" in schema: evaluated_indexes += find_evaluated_item_indexes_by_schema( - validator, instance, schema['else'] + validator, instance, schema["else"] ) - for keyword in ['contains', 'unevaluatedItems']: + for keyword in ["contains", "unevaluatedItems"]: if keyword in schema: for k, v in enumerate(instance): if validator.is_valid(v, schema[keyword]): evaluated_indexes.append(k) - for keyword in ['allOf', 'oneOf', 'anyOf']: + for keyword in ["allOf", "oneOf", "anyOf"]: if keyword in schema: for subschema in schema[keyword]: errs = list(validator.descend(instance, subschema)) @@ -320,28 +315,26 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): Get all keys of items that get evaluated under the current schema Covers all keywords related to unevaluatedProperties: properties, - 'additionalProperties', 'unevaluatedProperties', patternProperties, - dependentSchemas, 'allOf', 'oneOf', 'anyOf', if, then, else + additionalProperties, unevaluatedProperties, patternProperties, + dependentSchemas, allOf, oneOf, anyOf, if, then, else """ - if not validator.is_type(schema, "object"): + if validator.is_type(schema, "boolean"): return [] evaluated_keys = [] - if '$ref' in schema: - resolve = getattr(validator.resolver, "resolve", None) - if resolve: - scope, resolved = validator.resolver.resolve(schema['$ref']) - validator.resolver.push_scope(scope) + if "$ref" in schema: + scope, resolved = validator.resolver.resolve(schema["$ref"]) + validator.resolver.push_scope(scope) - try: - evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, resolved - ) - finally: - validator.resolver.pop_scope() + try: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, resolved + ) + finally: + validator.resolver.pop_scope() for keyword in [ - 'properties', 'additionalProperties', 'unevaluatedProperties' + "properties", "additionalProperties", "unevaluatedProperties" ]: if keyword in schema: if validator.is_type(schema[keyword], "boolean"): @@ -356,27 +349,23 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): ): evaluated_keys.append(property) - if 'patternProperties' in schema: + if "patternProperties" in schema: for property, value in instance.items(): - for pattern, subschema in schema['patternProperties'].items(): - if re.search(pattern, property): - if validator.is_valid( - {property: value}, schema['patternProperties'] - ): - evaluated_keys.append(property) - - if 'dependentSchemas' in schema: - for property, subschema in schema['dependentSchemas'].items(): + for pattern, subschema in schema["patternProperties"].items(): + if re.search(pattern, property) and validator.is_valid( + {property: value}, schema["patternProperties"] + ): + evaluated_keys.append(property) + + if "dependentSchemas" in schema: + for property, subschema in schema["dependentSchemas"].items(): if property not in instance: continue + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema + ) - errs = list(validator.descend(instance, subschema)) - if not errs: - evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, subschema - ) - - for keyword in ['allOf', 'oneOf', 'anyOf']: + for keyword in ["allOf", "oneOf", "anyOf"]: if keyword in schema: for subschema in schema[keyword]: errs = list(validator.descend(instance, subschema)) @@ -385,19 +374,19 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): validator, instance, subschema ) - if 'if' in schema: - if validator.is_valid(instance, schema['if']): + if "if" in schema: + if validator.is_valid(instance, schema["if"]): evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, schema['if'] + validator, instance, schema["if"] ) - if 'then' in schema: + if "then" in schema: evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, schema['then'] + validator, instance, schema["then"] ) else: - if 'else' in schema: + if "else" in schema: evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, schema['else'] + validator, instance, schema["else"] ) return evaluated_keys diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index 7acd178fb..b6a2f9c8d 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -129,10 +129,7 @@ def contains(validator, contains, instance, schema): if min_contains == 0: return - matches = len(list( - filter(lambda x: x, [validator.is_valid(element, contains) for - element in instance])) - ) + matches = sum(1 for each in instance if validator.is_valid(each, contains)) # default contains behavior if not matches: @@ -144,9 +141,9 @@ def contains(validator, contains, instance, schema): if min_contains and max_contains is None: if matches < min_contains: yield ValidationError( - "Invalid number or matches of %r under the given schema, " - "expected min %d, got %d" % ( - instance, min_contains, matches + "Too few matches under the given schema. " + "Expected %d but there were only %d." % ( + min_contains, matches ) ) return @@ -154,9 +151,9 @@ def contains(validator, contains, instance, schema): if min_contains is None and max_contains: if matches > max_contains: yield ValidationError( - "Invalid number or matches of %r under the given schema, " - "expected max %d, got %d" % ( - instance, max_contains, matches + "Too many matches under the given schema. " + "Expected %d but there were only %d." % ( + max_contains, matches ) ) return @@ -164,9 +161,9 @@ def contains(validator, contains, instance, schema): if min_contains and max_contains: if matches < min_contains or matches > max_contains: yield ValidationError( - "Invalid number or matches of %r under the given schema, " - "expected min %d and max %d, got %d" % ( - instance, min_contains, max_contains, matches + "Invalid number or matches under the given schema, " + "expected between %d and %d, got %d" % ( + min_contains, max_contains, matches ) ) return @@ -289,9 +286,6 @@ def maxLength(validator, mL, instance, schema): def dependentRequired(validator, dependentRequired, instance, schema): - """ - Split from dependencies - """ if not validator.is_type(instance, "object"): return @@ -306,12 +300,6 @@ def dependentRequired(validator, dependentRequired, instance, schema): def dependentSchemas(validator, dependentSchemas, instance, schema): - """ - Split from dependencies - """ - if not validator.is_type(instance, "object"): - return - for property, dependency in dependentSchemas.items(): if property not in instance: continue @@ -355,8 +343,8 @@ def dynamicRef(validator, dynamicRef, instance, schema): for url in scope_stack: lookup_url = urljoin(url, dynamicRef) with validator.resolver.resolving(lookup_url) as lookup_schema: - if "$dynamicAnchor" in lookup_schema \ - and fragment == lookup_schema["$dynamicAnchor"]: + if ("$dynamicAnchor" in lookup_schema + and fragment == lookup_schema["$dynamicAnchor"]): subschema = lookup_schema for error in validator.descend(instance, subschema): yield error @@ -375,10 +363,10 @@ def defs(validator, defs, instance, schema): if '$defs' in instance: for definition, subschema in instance['$defs'].items(): for error in validator.descend( - subschema, - schema, - path=definition, - schema_path=definition, + subschema, + schema, + path=definition, + schema_path=definition, ): yield error @@ -491,9 +479,6 @@ def if_(validator, if_schema, instance, schema): 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 ) @@ -501,7 +486,7 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema): for k, v in enumerate(instance): if k not in evaluated_item_indexes: for error in validator.descend( - v, unevaluatedItems, schema_path="unevaluatedItems" + v, unevaluatedItems, schema_path="unevaluatedItems" ): unevaluated_items.append(v) @@ -511,9 +496,6 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema): def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): - if not validator.is_type(instance, "object"): - return - evaluated_property_keys = find_evaluated_property_keys_by_schema( validator, instance, schema ) @@ -521,10 +503,10 @@ def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): for property, subschema in instance.items(): if property not in evaluated_property_keys: for error in validator.descend( - instance[property], - unevaluatedProperties, - path=property, - schema_path=property, + instance[property], + unevaluatedProperties, + path=property, + schema_path=property, ): unevaluated_property_keys.append(property) @@ -537,9 +519,8 @@ def prefixItems(validator, prefixItems, instance, schema): if not validator.is_type(instance, "array"): return - for k, v in enumerate(instance): - if k < len(prefixItems): - for error in validator.descend( - v, prefixItems[k], schema_path="prefixItems" - ): - yield error + for k, v in enumerate(instance[:min(len(prefixItems), len(instance))]): + for error in validator.descend( + v, prefixItems[k], schema_path="prefixItems" + ): + yield error diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py index f4cec9d3c..06f841c3b 100644 --- a/jsonschema/tests/test_format.py +++ b/jsonschema/tests/test_format.py @@ -79,9 +79,9 @@ def test_format_checkers_come_with_defaults(self): def test_repr(self): checker = FormatChecker(formats=()) - checker.checks("foo")(lambda thing: True) - checker.checks("bar")(lambda thing: True) - checker.checks("baz")(lambda thing: True) + checker.checks("foo")(lambda thing: True) # pragma: no cover + checker.checks("bar")(lambda thing: True) # pragma: no cover + checker.checks("baz")(lambda thing: True) # pragma: no cover self.assertEqual( repr(checker), "", diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 3da509141..8df65ccf3 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -52,13 +52,11 @@ def validates(version): def _validates(cls): validators[version] = cls meta_schema_id = cls.ID_OF(cls.META_SCHEMA) - if meta_schema_id: - meta_schemas[meta_schema_id] = cls + meta_schemas[meta_schema_id] = cls for vocabulary in cls.VOCABULARY_SCHEMAS: vocabulary_id = cls.ID_OF(vocabulary) - if vocabulary_id: - vocabulary_schemas[vocabulary_id] = vocabulary + vocabulary_schemas[vocabulary_id] = vocabulary return cls return _validates @@ -675,7 +673,6 @@ def resolving(self, ref): def _finditem(self, schema, key): results = [] - if isinstance(schema, dict): if key in schema: results.append(schema) @@ -699,9 +696,6 @@ def resolve_local(self, url, schema): if target_uri.rstrip("/") == uri.rstrip("/"): if fragment: subschema = self.resolve_fragment(subschema, fragment) - - if self.cache_remote: - self.store[url] = subschema return subschema def resolve(self, ref): diff --git a/setup.cfg b/setup.cfg index a4c1fb452..d78daafd0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,7 @@ console_scripts = jsonschema = jsonschema.cli:main [options.package_data] -jsonschema = schemas/*.json, schemas/draft2020-12/*.json +jsonschema = schemas/*.json, schemas/*/*.json [bdist_wheel] universal = 1 From c6b0fc454d9c3d5c3f2f27c8dc851cd9691ec9ff Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Thu, 24 Jun 2021 19:22:04 +0200 Subject: [PATCH 26/27] Julian/jsonschema#782: Add compatibility to draft7 and older --- jsonschema/_legacy_validators.py | 12 +++++++++++ jsonschema/validators.py | 34 ++++++++++++++++---------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py index b707889be..194c68fa9 100644 --- a/jsonschema/_legacy_validators.py +++ b/jsonschema/_legacy_validators.py @@ -2,6 +2,18 @@ from jsonschema.exceptions import ValidationError +def ignore_ref_siblings(schema): + """ + Returns a list of validators that should apply for the given schema + Used for draft7 and earlier + """ + ref = schema.get(u"$ref") + if ref is not None: + return [(u"$ref", ref)] + else: + return schema.items() + + def dependencies_draft3(validator, dependencies, instance, schema): if not validator.is_type(instance, "object"): return diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 8df65ccf3..a1d4214dc 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -26,7 +26,7 @@ validators = {} meta_schemas = _utils.URIDict() -vocabulary_schemas = _utils.URIDict() +_VOCABULARIES = _utils.URIDict() def validates(version): @@ -56,7 +56,7 @@ def _validates(cls): for vocabulary in cls.VOCABULARY_SCHEMAS: vocabulary_id = cls.ID_OF(vocabulary) - vocabulary_schemas[vocabulary_id] = vocabulary + _VOCABULARIES[vocabulary_id] = vocabulary return cls return _validates @@ -70,11 +70,10 @@ def _id_of(schema): def _store_schema_list(): return [ - (id, validator.META_SCHEMA) - for id, validator in meta_schemas.items() - ] + [ - (id, schema) for id, schema in vocabulary_schemas.items() - ] + (id, validator.META_SCHEMA) for id, validator in meta_schemas.items() + ] + [ + (id, schema) for id, schema in _VOCABULARIES.items() + ] def create( @@ -84,6 +83,7 @@ def create( version=None, type_checker=_types.draft7_type_checker, id_of=_id_of, + applicable_validators=lambda schema: schema.items(), ): """ Create a new validator class. @@ -126,6 +126,11 @@ def create( A function that given a schema, returns its ID. + applicable_validators (collections.abc.Callable): + + A function that returns a list of validators that should apply + to a given schema + Returns: a new `jsonschema.IValidator` class @@ -177,16 +182,7 @@ def iter_errors(self, instance, _schema=None): if scope: self.resolver.push_scope(scope) try: - validators = [] - - # We make sure $ref is evaluated first if available in schema - ref = _schema.get(u"$ref") - if ref is not None: - validators = [(u"$ref", ref)] - - # Add all remaining validators - validators += [(x, _schema[x]) for x in _schema if x not in ["$ref"]] - + validators = applicable_validators(_schema) for k, v in validators: validator = self.VALIDATORS.get(k) if validator is None: @@ -332,6 +328,7 @@ def extend(validator, validators=(), version=None, type_checker=None): type_checker=_types.draft3_type_checker, version="draft3", id_of=lambda schema: schema.get(u"id", ""), + applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft4Validator = create( @@ -367,6 +364,7 @@ def extend(validator, validators=(), version=None, type_checker=None): type_checker=_types.draft4_type_checker, version="draft4", id_of=lambda schema: schema.get(u"id", ""), + applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft6Validator = create( @@ -406,6 +404,7 @@ def extend(validator, validators=(), version=None, type_checker=None): }, type_checker=_types.draft6_type_checker, version="draft6", + applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft7Validator = create( @@ -446,6 +445,7 @@ def extend(validator, validators=(), version=None, type_checker=None): }, type_checker=_types.draft7_type_checker, version="draft7", + applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft202012Validator = create( From 4547b2ab8a74dd8a83c9104606b2166a9e712fa2 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Wed, 21 Jul 2021 14:02:36 +0200 Subject: [PATCH 27/27] Julian/jsonschema#782: Remove ecmascript validation, extend dynamicRef skip description --- .../tests/test_jsonschema_test_suite.py | 66 ++----------------- 1 file changed, 4 insertions(+), 62 deletions(-) diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index 61a887b71..b1e072302 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -128,62 +128,6 @@ def leap_second(test): )(test) -def ecmascript_regex_validation(test): - """ - Considering switching from re to js-regex after the following issues are - resolved: - * https://github.com/Julian/jsonschema/issues/612 - * https://github.com/Zac-HD/js-regex/issues/4 - - Notice: Zac-HD/js-regex Repository has been archived - """ - return skip( - message=bug(612), - subject="ecmascript-regex", - description='NKO DIGIT ZERO does not match (unlike e.g. Python)', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='NKO DIGIT ZERO (as \\u escape) does not match', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='NKO DIGIT ZERO matches (unlike e.g. Python)', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='NKO DIGIT ZERO (as \\u escape) matches', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='zero-width whitespace matches', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='zero-width whitespace does not match', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='latin-1 e-acute matches (unlike e.g. Python)', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='latin-1 e-acute does not match (unlike e.g. Python)', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='matches in Python, but should not in jsonschema', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='does not match', - )(test) or skip( - message=bug(612), - subject="ecmascript-regex", - description='matches', - )(test) - - TestDraft3 = DRAFT3.to_unittest_testcase( DRAFT3.tests(), DRAFT3.format_tests(), @@ -462,16 +406,14 @@ def ecmascript_regex_validation(test): TestDraft202012 = DRAFT202012.to_unittest_testcase( DRAFT202012.tests(), DRAFT202012.optional_tests_of(name="bignum"), - DRAFT202012.optional_tests_of(name="ecmascript-regex"), DRAFT202012.optional_tests_of(name="float-overflow"), DRAFT202012.optional_tests_of(name="non-bmp-regex"), DRAFT202012.optional_tests_of(name="refOfUnknownKeyword"), Validator=Draft202012Validator, skip=lambda test: ( narrow_unicode_build(test) - or ecmascript_regex_validation(test) or skip( - message="ToDo: Extend validation", + message="Issue: Resolving of dynamicRef based on dynamic scope", subject="dynamicRef", case_description="A $dynamicRef that initially resolves to a " "schema with a matching $dynamicAnchor should " @@ -480,14 +422,14 @@ def ecmascript_regex_validation(test): description='The recursive part is not valid against the root', )(test) or skip( - message="ToDo: Extend validation", + message="Issue: Resolving of dynamicRef based on dynamic scope", subject="dynamicRef", case_description="multiple dynamic paths to the $dynamicRef " "keyword", description="recurse to integerNode - floats are not allowed", )(test) or skip( - message="ToDo: Extend validation", + message="Issue: Resolving of dynamicRef based on dynamic scope", subject="dynamicRef", case_description="after leaving a dynamic scope, it should not be " "used by a $dynamicRef", @@ -495,7 +437,7 @@ def ecmascript_regex_validation(test): "$dynamicRef", )(test) or skip( - message="ToDo: Extend validation", + message="Issue: Resolving of dynamicRef based on dynamic scope", subject="dynamicRef", case_description="after leaving a dynamic scope, it should not be " 'used by a $dynamicRef',