This commit is contained in:
Iliyan Angelov
2025-09-19 11:58:53 +03:00
parent 306b20e24a
commit 6b247e5b9f
11423 changed files with 1500615 additions and 778 deletions

View File

@@ -0,0 +1,120 @@
"""
An implementation of JSON Schema for Python.
The main functionality is provided by the validator classes for each of the
supported JSON Schema versions.
Most commonly, `jsonschema.validators.validate` is the quickest way to simply
validate a given instance under a schema, and will create a validator
for you.
"""
import warnings
from jsonschema._format import FormatChecker
from jsonschema._types import TypeChecker
from jsonschema.exceptions import SchemaError, ValidationError
from jsonschema.validators import (
Draft3Validator,
Draft4Validator,
Draft6Validator,
Draft7Validator,
Draft201909Validator,
Draft202012Validator,
validate,
)
def __getattr__(name):
if name == "__version__":
warnings.warn(
"Accessing jsonschema.__version__ is deprecated and will be "
"removed in a future release. Use importlib.metadata directly "
"to query for jsonschema's version.",
DeprecationWarning,
stacklevel=2,
)
from importlib import metadata
return metadata.version("jsonschema")
elif name == "RefResolver":
from jsonschema.validators import _RefResolver
warnings.warn(
_RefResolver._DEPRECATION_MESSAGE,
DeprecationWarning,
stacklevel=2,
)
return _RefResolver
elif name == "ErrorTree":
warnings.warn(
"Importing ErrorTree directly from the jsonschema package "
"is deprecated and will become an ImportError. Import it from "
"jsonschema.exceptions instead.",
DeprecationWarning,
stacklevel=2,
)
from jsonschema.exceptions import ErrorTree
return ErrorTree
elif name == "FormatError":
warnings.warn(
"Importing FormatError directly from the jsonschema package "
"is deprecated and will become an ImportError. Import it from "
"jsonschema.exceptions instead.",
DeprecationWarning,
stacklevel=2,
)
from jsonschema.exceptions import FormatError
return FormatError
elif name == "Validator":
warnings.warn(
"Importing Validator directly from the jsonschema package "
"is deprecated and will become an ImportError. Import it from "
"jsonschema.protocols instead.",
DeprecationWarning,
stacklevel=2,
)
from jsonschema.protocols import Validator
return Validator
elif name == "RefResolutionError":
from jsonschema.exceptions import _RefResolutionError
warnings.warn(
_RefResolutionError._DEPRECATION_MESSAGE,
DeprecationWarning,
stacklevel=2,
)
return _RefResolutionError
format_checkers = {
"draft3_format_checker": Draft3Validator,
"draft4_format_checker": Draft4Validator,
"draft6_format_checker": Draft6Validator,
"draft7_format_checker": Draft7Validator,
"draft201909_format_checker": Draft201909Validator,
"draft202012_format_checker": Draft202012Validator,
}
ValidatorForFormat = format_checkers.get(name)
if ValidatorForFormat is not None:
warnings.warn(
f"Accessing jsonschema.{name} is deprecated and will be "
"removed in a future release. Instead, use the FORMAT_CHECKER "
"attribute on the corresponding Validator.",
DeprecationWarning,
stacklevel=2,
)
return ValidatorForFormat.FORMAT_CHECKER
raise AttributeError(f"module {__name__} has no attribute {name}")
__all__ = [
"Draft3Validator",
"Draft4Validator",
"Draft6Validator",
"Draft7Validator",
"Draft201909Validator",
"Draft202012Validator",
"FormatChecker",
"SchemaError",
"TypeChecker",
"ValidationError",
"validate",
]

View File

@@ -0,0 +1,6 @@
"""
The jsonschema CLI is now deprecated in favor of check-jsonschema.
"""
from jsonschema.cli import main
main()

View File

@@ -0,0 +1,546 @@
from __future__ import annotations
from contextlib import suppress
from datetime import date, datetime
from uuid import UUID
import ipaddress
import re
import typing
import warnings
from jsonschema.exceptions import FormatError
_FormatCheckCallable = typing.Callable[[object], bool]
#: A format checker callable.
_F = typing.TypeVar("_F", bound=_FormatCheckCallable)
_RaisesType = typing.Union[type[Exception], tuple[type[Exception], ...]]
_RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII)
class FormatChecker:
"""
A ``format`` property checker.
JSON Schema does not mandate that the ``format`` property actually do any
validation. If validation is desired however, instances of this class can
be hooked into validators to enable format validation.
`FormatChecker` objects always return ``True`` when asked about
formats that they do not know how to validate.
To add a check for a custom format use the `FormatChecker.checks`
decorator.
Arguments:
formats:
The known formats to validate. This argument can be used to
limit which formats will be used during validation.
"""
checkers: dict[
str,
tuple[_FormatCheckCallable, _RaisesType],
] = {} # noqa: RUF012
def __init__(self, formats: typing.Iterable[str] | None = None):
if formats is None:
formats = self.checkers.keys()
self.checkers = {k: self.checkers[k] for k in formats}
def __repr__(self):
return f"<FormatChecker checkers={sorted(self.checkers)}>"
def checks(
self, format: str, raises: _RaisesType = (),
) -> typing.Callable[[_F], _F]:
"""
Register a decorated function as validating a new format.
Arguments:
format:
The format that the decorated function will check.
raises:
The exception(s) raised by the decorated function when an
invalid instance is found.
The exception object will be accessible as the
`jsonschema.exceptions.ValidationError.cause` attribute of the
resulting validation error.
"""
def _checks(func: _F) -> _F:
self.checkers[format] = (func, raises)
return func
return _checks
@classmethod
def cls_checks(
cls, format: str, raises: _RaisesType = (),
) -> typing.Callable[[_F], _F]:
warnings.warn(
(
"FormatChecker.cls_checks is deprecated. Call "
"FormatChecker.checks on a specific FormatChecker instance "
"instead."
),
DeprecationWarning,
stacklevel=2,
)
return cls._cls_checks(format=format, raises=raises)
@classmethod
def _cls_checks(
cls, format: str, raises: _RaisesType = (),
) -> typing.Callable[[_F], _F]:
def _checks(func: _F) -> _F:
cls.checkers[format] = (func, raises)
return func
return _checks
def check(self, instance: object, format: str) -> None:
"""
Check whether the instance conforms to the given format.
Arguments:
instance (*any primitive type*, i.e. str, number, bool):
The instance to check
format:
The format that instance should conform to
Raises:
FormatError:
if the instance does not conform to ``format``
"""
if format not in self.checkers:
return
func, raises = self.checkers[format]
result, cause = None, None
try:
result = func(instance)
except raises as e:
cause = e
if not result:
raise FormatError(f"{instance!r} is not a {format!r}", cause=cause)
def conforms(self, instance: object, format: str) -> bool:
"""
Check whether the instance conforms to the given format.
Arguments:
instance (*any primitive type*, i.e. str, number, bool):
The instance to check
format:
The format that instance should conform to
Returns:
bool: whether it conformed
"""
try:
self.check(instance, format)
except FormatError:
return False
else:
return True
draft3_format_checker = FormatChecker()
draft4_format_checker = FormatChecker()
draft6_format_checker = FormatChecker()
draft7_format_checker = FormatChecker()
draft201909_format_checker = FormatChecker()
draft202012_format_checker = FormatChecker()
_draft_checkers: dict[str, FormatChecker] = dict(
draft3=draft3_format_checker,
draft4=draft4_format_checker,
draft6=draft6_format_checker,
draft7=draft7_format_checker,
draft201909=draft201909_format_checker,
draft202012=draft202012_format_checker,
)
def _checks_drafts(
name=None,
draft3=None,
draft4=None,
draft6=None,
draft7=None,
draft201909=None,
draft202012=None,
raises=(),
) -> typing.Callable[[_F], _F]:
draft3 = draft3 or name
draft4 = draft4 or name
draft6 = draft6 or name
draft7 = draft7 or name
draft201909 = draft201909 or name
draft202012 = draft202012 or name
def wrap(func: _F) -> _F:
if draft3:
func = _draft_checkers["draft3"].checks(draft3, raises)(func)
if draft4:
func = _draft_checkers["draft4"].checks(draft4, raises)(func)
if draft6:
func = _draft_checkers["draft6"].checks(draft6, raises)(func)
if draft7:
func = _draft_checkers["draft7"].checks(draft7, raises)(func)
if draft201909:
func = _draft_checkers["draft201909"].checks(draft201909, 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 #519 and test_format_checkers_come_with_defaults
FormatChecker._cls_checks(
draft202012 or draft201909 or draft7 or draft6 or draft4 or draft3,
raises,
)(func)
return func
return wrap
@_checks_drafts(name="idn-email")
@_checks_drafts(name="email")
def is_email(instance: object) -> bool:
if not isinstance(instance, str):
return True
return "@" in instance
@_checks_drafts(
draft3="ip-address",
draft4="ipv4",
draft6="ipv4",
draft7="ipv4",
draft201909="ipv4",
draft202012="ipv4",
raises=ipaddress.AddressValueError,
)
def is_ipv4(instance: object) -> bool:
if not isinstance(instance, str):
return True
return bool(ipaddress.IPv4Address(instance))
@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError)
def is_ipv6(instance: object) -> bool:
if not isinstance(instance, str):
return True
address = ipaddress.IPv6Address(instance)
return not getattr(address, "scope_id", "")
with suppress(ImportError):
from fqdn import FQDN
@_checks_drafts(
draft3="host-name",
draft4="hostname",
draft6="hostname",
draft7="hostname",
draft201909="hostname",
draft202012="hostname",
# fqdn.FQDN("") raises a ValueError due to a bug
# however, it's not clear when or if that will be fixed, so catch it
# here for now
raises=ValueError,
)
def is_host_name(instance: object) -> bool:
if not isinstance(instance, str):
return True
return FQDN(instance, min_labels=1).is_valid
with suppress(ImportError):
# The built-in `idna` codec only implements RFC 3890, so we go elsewhere.
import idna
@_checks_drafts(
draft7="idn-hostname",
draft201909="idn-hostname",
draft202012="idn-hostname",
raises=(idna.IDNAError, UnicodeError),
)
def is_idn_host_name(instance: object) -> bool:
if not isinstance(instance, str):
return True
idna.encode(instance)
return True
try:
import rfc3987
except ImportError:
with suppress(ImportError):
from rfc3986_validator import validate_rfc3986
@_checks_drafts(name="uri")
def is_uri(instance: object) -> bool:
if not isinstance(instance, str):
return True
return validate_rfc3986(instance, rule="URI")
@_checks_drafts(
draft6="uri-reference",
draft7="uri-reference",
draft201909="uri-reference",
draft202012="uri-reference",
raises=ValueError,
)
def is_uri_reference(instance: object) -> bool:
if not isinstance(instance, str):
return True
return validate_rfc3986(instance, rule="URI_reference")
with suppress(ImportError):
from rfc3987_syntax import is_valid_syntax as _rfc3987_is_valid_syntax
@_checks_drafts(
draft7="iri",
draft201909="iri",
draft202012="iri",
raises=ValueError,
)
def is_iri(instance: object) -> bool:
if not isinstance(instance, str):
return True
return _rfc3987_is_valid_syntax("iri", instance)
@_checks_drafts(
draft7="iri-reference",
draft201909="iri-reference",
draft202012="iri-reference",
raises=ValueError,
)
def is_iri_reference(instance: object) -> bool:
if not isinstance(instance, str):
return True
return _rfc3987_is_valid_syntax("iri_reference", instance)
else:
@_checks_drafts(
draft7="iri",
draft201909="iri",
draft202012="iri",
raises=ValueError,
)
def is_iri(instance: object) -> bool:
if not isinstance(instance, str):
return True
return rfc3987.parse(instance, rule="IRI")
@_checks_drafts(
draft7="iri-reference",
draft201909="iri-reference",
draft202012="iri-reference",
raises=ValueError,
)
def is_iri_reference(instance: object) -> bool:
if not isinstance(instance, str):
return True
return rfc3987.parse(instance, rule="IRI_reference")
@_checks_drafts(name="uri", raises=ValueError)
def is_uri(instance: object) -> bool:
if not isinstance(instance, str):
return True
return rfc3987.parse(instance, rule="URI")
@_checks_drafts(
draft6="uri-reference",
draft7="uri-reference",
draft201909="uri-reference",
draft202012="uri-reference",
raises=ValueError,
)
def is_uri_reference(instance: object) -> bool:
if not isinstance(instance, str):
return True
return rfc3987.parse(instance, rule="URI_reference")
with suppress(ImportError):
from rfc3339_validator import validate_rfc3339
@_checks_drafts(name="date-time")
def is_datetime(instance: object) -> bool:
if not isinstance(instance, str):
return True
return validate_rfc3339(instance.upper())
@_checks_drafts(
draft7="time",
draft201909="time",
draft202012="time",
)
def is_time(instance: object) -> bool:
if not isinstance(instance, str):
return True
return is_datetime("1970-01-01T" + instance)
@_checks_drafts(name="regex", raises=re.error)
def is_regex(instance: object) -> bool:
if not isinstance(instance, str):
return True
return bool(re.compile(instance))
@_checks_drafts(
draft3="date",
draft7="date",
draft201909="date",
draft202012="date",
raises=ValueError,
)
def is_date(instance: object) -> bool:
if not isinstance(instance, str):
return True
return bool(_RE_DATE.fullmatch(instance) and date.fromisoformat(instance))
@_checks_drafts(draft3="time", raises=ValueError)
def is_draft3_time(instance: object) -> bool:
if not isinstance(instance, str):
return True
return bool(datetime.strptime(instance, "%H:%M:%S")) # noqa: DTZ007
with suppress(ImportError):
import webcolors
@_checks_drafts(draft3="color", raises=(ValueError, TypeError))
def is_css21_color(instance: object) -> bool:
if isinstance(instance, str):
try:
webcolors.name_to_hex(instance)
except ValueError:
webcolors.normalize_hex(instance.lower())
return True
with suppress(ImportError):
import jsonpointer
@_checks_drafts(
draft6="json-pointer",
draft7="json-pointer",
draft201909="json-pointer",
draft202012="json-pointer",
raises=jsonpointer.JsonPointerException,
)
def is_json_pointer(instance: object) -> bool:
if not isinstance(instance, str):
return True
return bool(jsonpointer.JsonPointer(instance))
# TODO: I don't want to maintain this, so it
# needs to go either into jsonpointer (pending
# https://github.com/stefankoegl/python-json-pointer/issues/34) or
# into a new external library.
@_checks_drafts(
draft7="relative-json-pointer",
draft201909="relative-json-pointer",
draft202012="relative-json-pointer",
raises=jsonpointer.JsonPointerException,
)
def is_relative_json_pointer(instance: object) -> bool:
# Definition taken from:
# https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
if not isinstance(instance, str):
return True
if not instance:
return False
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
if not non_negative_integer:
return False
rest = instance[i:]
break
return (rest == "#") or bool(jsonpointer.JsonPointer(rest))
with suppress(ImportError):
import uri_template
@_checks_drafts(
draft6="uri-template",
draft7="uri-template",
draft201909="uri-template",
draft202012="uri-template",
)
def is_uri_template(instance: object) -> bool:
if not isinstance(instance, str):
return True
return uri_template.validate(instance)
with suppress(ImportError):
import isoduration
@_checks_drafts(
draft201909="duration",
draft202012="duration",
raises=isoduration.DurationParsingException,
)
def is_duration(instance: object) -> bool:
if not isinstance(instance, str):
return True
isoduration.parse_duration(instance)
# FIXME: See bolsote/isoduration#25 and bolsote/isoduration#21
return instance.endswith(tuple("DMYWHMS"))
@_checks_drafts(
draft201909="uuid",
draft202012="uuid",
raises=ValueError,
)
def is_uuid(instance: object) -> bool:
if not isinstance(instance, str):
return True
UUID(instance)
return all(instance[position] == "-" for position in (8, 13, 18, 23))

View File

@@ -0,0 +1,449 @@
from fractions import Fraction
import re
from jsonschema._utils import (
ensure_list,
equal,
extras_msg,
find_additional_properties,
find_evaluated_item_indexes_by_schema,
find_evaluated_property_keys_by_schema,
uniq,
)
from jsonschema.exceptions import FormatError, ValidationError
def patternProperties(validator, patternProperties, instance, schema):
if not validator.is_type(instance, "object"):
return
for pattern, subschema in patternProperties.items():
for k, v in instance.items():
if re.search(pattern, k):
yield from validator.descend(
v, subschema, path=k, schema_path=pattern,
)
def propertyNames(validator, propertyNames, instance, schema):
if not validator.is_type(instance, "object"):
return
for property in instance:
yield from validator.descend(instance=property, schema=propertyNames)
def additionalProperties(validator, aP, instance, schema):
if not validator.is_type(instance, "object"):
return
extras = set(find_additional_properties(instance, schema))
if validator.is_type(aP, "object"):
for extra in extras:
yield from validator.descend(instance[extra], aP, path=extra)
elif not aP and extras:
if "patternProperties" in schema:
verb = "does" if len(extras) == 1 else "do"
joined = ", ".join(repr(each) for each in sorted(extras))
patterns = ", ".join(
repr(each) for each in sorted(schema["patternProperties"])
)
error = f"{joined} {verb} not match any of the regexes: {patterns}"
yield ValidationError(error)
else:
error = "Additional properties are not allowed (%s %s unexpected)"
yield ValidationError(error % extras_msg(sorted(extras, key=str)))
def items(validator, items, instance, schema):
if not validator.is_type(instance, "array"):
return
prefix = len(schema.get("prefixItems", []))
total = len(instance)
extra = total - prefix
if extra <= 0:
return
if items is False:
rest = instance[prefix:] if extra != 1 else instance[prefix]
item = "items" if prefix != 1 else "item"
yield ValidationError(
f"Expected at most {prefix} {item} but found {extra} "
f"extra: {rest!r}",
)
else:
for index in range(prefix, total):
yield from validator.descend(
instance=instance[index],
schema=items,
path=index,
)
def const(validator, const, instance, schema):
if not equal(instance, const):
yield ValidationError(f"{const!r} was expected")
def contains(validator, contains, instance, schema):
if not validator.is_type(instance, "array"):
return
matches = 0
min_contains = schema.get("minContains", 1)
max_contains = schema.get("maxContains", len(instance))
contains_validator = validator.evolve(schema=contains)
for each in instance:
if contains_validator.is_valid(each):
matches += 1
if matches > max_contains:
yield ValidationError(
"Too many items match the given schema "
f"(expected at most {max_contains})",
validator="maxContains",
validator_value=max_contains,
)
return
if matches < min_contains:
if not matches:
yield ValidationError(
f"{instance!r} does not contain items "
"matching the given schema",
)
else:
yield ValidationError(
"Too few items match the given schema (expected at least "
f"{min_contains} but only {matches} matched)",
validator="minContains",
validator_value=min_contains,
)
def exclusiveMinimum(validator, minimum, instance, schema):
if not validator.is_type(instance, "number"):
return
if instance <= minimum:
yield ValidationError(
f"{instance!r} is less than or equal to "
f"the minimum of {minimum!r}",
)
def exclusiveMaximum(validator, maximum, instance, schema):
if not validator.is_type(instance, "number"):
return
if instance >= maximum:
yield ValidationError(
f"{instance!r} is greater than or equal "
f"to the maximum of {maximum!r}",
)
def minimum(validator, minimum, instance, schema):
if not validator.is_type(instance, "number"):
return
if instance < minimum:
message = f"{instance!r} is less than the minimum of {minimum!r}"
yield ValidationError(message)
def maximum(validator, maximum, instance, schema):
if not validator.is_type(instance, "number"):
return
if instance > maximum:
message = f"{instance!r} is greater than the maximum of {maximum!r}"
yield ValidationError(message)
def multipleOf(validator, dB, instance, schema):
if not validator.is_type(instance, "number"):
return
if isinstance(dB, float):
quotient = instance / dB
try:
failed = int(quotient) != quotient
except OverflowError:
# When `instance` is large and `dB` is less than one,
# quotient can overflow to infinity; and then casting to int
# raises an error.
#
# In this case we fall back to Fraction logic, which is
# exact and cannot overflow. The performance is also
# acceptable: we try the fast all-float option first, and
# we know that fraction(dB) can have at most a few hundred
# digits in each part. The worst-case slowdown is therefore
# for already-slow enormous integers or Decimals.
failed = (Fraction(instance) / Fraction(dB)).denominator != 1
else:
failed = instance % dB
if failed:
yield ValidationError(f"{instance!r} is not a multiple of {dB}")
def minItems(validator, mI, instance, schema):
if validator.is_type(instance, "array") and len(instance) < mI:
message = "should be non-empty" if mI == 1 else "is too short"
yield ValidationError(f"{instance!r} {message}")
def maxItems(validator, mI, instance, schema):
if validator.is_type(instance, "array") and len(instance) > mI:
message = "is expected to be empty" if mI == 0 else "is too long"
yield ValidationError(f"{instance!r} {message}")
def uniqueItems(validator, uI, instance, schema):
if (
uI
and validator.is_type(instance, "array")
and not uniq(instance)
):
yield ValidationError(f"{instance!r} has non-unique elements")
def pattern(validator, patrn, instance, schema):
if (
validator.is_type(instance, "string")
and not re.search(patrn, instance)
):
yield ValidationError(f"{instance!r} does not match {patrn!r}")
def format(validator, format, instance, schema):
if validator.format_checker is not None:
try:
validator.format_checker.check(instance, format)
except FormatError as error:
yield ValidationError(error.message, cause=error.cause)
def minLength(validator, mL, instance, schema):
if validator.is_type(instance, "string") and len(instance) < mL:
message = "should be non-empty" if mL == 1 else "is too short"
yield ValidationError(f"{instance!r} {message}")
def maxLength(validator, mL, instance, schema):
if validator.is_type(instance, "string") and len(instance) > mL:
message = "is expected to be empty" if mL == 0 else "is too long"
yield ValidationError(f"{instance!r} {message}")
def dependentRequired(validator, dependentRequired, instance, schema):
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 = f"{each!r} is a dependency of {property!r}"
yield ValidationError(message)
def dependentSchemas(validator, dependentSchemas, instance, schema):
if not validator.is_type(instance, "object"):
return
for property, dependency in dependentSchemas.items():
if property not in instance:
continue
yield from validator.descend(
instance, dependency, schema_path=property,
)
def enum(validator, enums, instance, schema):
if all(not equal(each, instance) for each in enums):
yield ValidationError(f"{instance!r} is not one of {enums!r}")
def ref(validator, ref, instance, schema):
yield from validator._validate_reference(ref=ref, instance=instance)
def dynamicRef(validator, dynamicRef, instance, schema):
yield from validator._validate_reference(ref=dynamicRef, instance=instance)
def type(validator, types, instance, schema):
types = ensure_list(types)
if not any(validator.is_type(instance, type) for type in types):
reprs = ", ".join(repr(type) for type in types)
yield ValidationError(f"{instance!r} is not of type {reprs}")
def properties(validator, properties, instance, schema):
if not validator.is_type(instance, "object"):
return
for property, subschema in properties.items():
if property in instance:
yield from validator.descend(
instance[property],
subschema,
path=property,
schema_path=property,
)
def required(validator, required, instance, schema):
if not validator.is_type(instance, "object"):
return
for property in required:
if property not in instance:
yield ValidationError(f"{property!r} is a required property")
def minProperties(validator, mP, instance, schema):
if validator.is_type(instance, "object") and len(instance) < mP:
message = (
"should be non-empty" if mP == 1
else "does not have enough properties"
)
yield ValidationError(f"{instance!r} {message}")
def maxProperties(validator, mP, instance, schema):
if not validator.is_type(instance, "object"):
return
if validator.is_type(instance, "object") and len(instance) > mP:
message = (
"is expected to be empty" if mP == 0
else "has too many properties"
)
yield ValidationError(f"{instance!r} {message}")
def allOf(validator, allOf, instance, schema):
for index, subschema in enumerate(allOf):
yield from validator.descend(instance, subschema, schema_path=index)
def anyOf(validator, anyOf, instance, schema):
all_errors = []
for index, subschema in enumerate(anyOf):
errs = list(validator.descend(instance, subschema, schema_path=index))
if not errs:
break
all_errors.extend(errs)
else:
yield ValidationError(
f"{instance!r} is not valid under any of the given schemas",
context=all_errors,
)
def oneOf(validator, oneOf, instance, schema):
subschemas = enumerate(oneOf)
all_errors = []
for index, subschema in subschemas:
errs = list(validator.descend(instance, subschema, schema_path=index))
if not errs:
first_valid = subschema
break
all_errors.extend(errs)
else:
yield ValidationError(
f"{instance!r} is not valid under any of the given schemas",
context=all_errors,
)
more_valid = [
each for _, each in subschemas
if validator.evolve(schema=each).is_valid(instance)
]
if more_valid:
more_valid.append(first_valid)
reprs = ", ".join(repr(schema) for schema in more_valid)
yield ValidationError(f"{instance!r} is valid under each of {reprs}")
def not_(validator, not_schema, instance, schema):
if validator.evolve(schema=not_schema).is_valid(instance):
message = f"{instance!r} should not be valid under {not_schema!r}"
yield ValidationError(message)
def if_(validator, if_schema, instance, schema):
if validator.evolve(schema=if_schema).is_valid(instance):
if "then" in schema:
then = schema["then"]
yield from validator.descend(instance, then, schema_path="then")
elif "else" in schema:
else_ = schema["else"]
yield from validator.descend(instance, else_, schema_path="else")
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,
)
unevaluated_items = [
item for index, item in enumerate(instance)
if index not in evaluated_item_indexes
]
if unevaluated_items:
error = "Unevaluated items are not allowed (%s %s unexpected)"
yield ValidationError(error % extras_msg(unevaluated_items))
def unevaluatedProperties(validator, unevaluatedProperties, instance, schema):
if not validator.is_type(instance, "object"):
return
evaluated_keys = find_evaluated_property_keys_by_schema(
validator, instance, schema,
)
unevaluated_keys = []
for property in instance:
if property not in evaluated_keys:
for _ in validator.descend(
instance[property],
unevaluatedProperties,
path=property,
schema_path=property,
):
# FIXME: Include context for each unevaluated property
# indicating why it's invalid under the subschema.
unevaluated_keys.append(property) # noqa: PERF401
if unevaluated_keys:
if unevaluatedProperties is False:
error = "Unevaluated properties are not allowed (%s %s unexpected)"
extras = sorted(unevaluated_keys, key=str)
yield ValidationError(error % extras_msg(extras))
else:
error = (
"Unevaluated properties are not valid under "
"the given schema (%s %s unevaluated and invalid)"
)
yield ValidationError(error % extras_msg(unevaluated_keys))
def prefixItems(validator, prefixItems, instance, schema):
if not validator.is_type(instance, "array"):
return
for (index, item), subschema in zip(enumerate(instance), prefixItems):
yield from validator.descend(
instance=item,
schema=subschema,
schema_path=index,
path=index,
)

View File

@@ -0,0 +1,449 @@
import re
from referencing.jsonschema import lookup_recursive_ref
from jsonschema import _utils
from jsonschema.exceptions import ValidationError
def ignore_ref_siblings(schema):
"""
Ignore siblings of ``$ref`` if it is present.
Otherwise, return all keywords.
Suitable for use with `create`'s ``applicable_validators`` argument.
"""
ref = schema.get("$ref")
if ref is not None:
return [("$ref", ref)]
else:
return schema.items()
def dependencies_draft3(validator, dependencies, instance, schema):
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, "object"):
yield from validator.descend(
instance, dependency, schema_path=property,
)
elif validator.is_type(dependency, "string"):
if dependency not in instance:
message = f"{dependency!r} is a dependency of {property!r}"
yield ValidationError(message)
else:
for each in dependency:
if each not in instance:
message = f"{each!r} is a dependency of {property!r}"
yield ValidationError(message)
def dependencies_draft4_draft6_draft7(
validator,
dependencies,
instance,
schema,
):
"""
Support for the ``dependencies`` keyword from pre-draft 2019-09.
In later drafts, the keyword 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 = f"{each!r} is a dependency of {property!r}"
yield ValidationError(message)
else:
yield from validator.descend(
instance, dependency, schema_path=property,
)
def disallow_draft3(validator, disallow, instance, schema):
for disallowed in _utils.ensure_list(disallow):
if validator.evolve(schema={"type": [disallowed]}).is_valid(instance):
message = f"{disallowed!r} is disallowed for {instance!r}"
yield ValidationError(message)
def extends_draft3(validator, extends, instance, schema):
if validator.is_type(extends, "object"):
yield from validator.descend(instance, extends)
return
for index, subschema in enumerate(extends):
yield from validator.descend(instance, subschema, schema_path=index)
def items_draft3_draft4(validator, items, instance, schema):
if not validator.is_type(instance, "array"):
return
if validator.is_type(items, "object"):
for index, item in enumerate(instance):
yield from validator.descend(item, items, path=index)
else:
for (index, item), subschema in zip(enumerate(instance), items):
yield from validator.descend(
item, subschema, path=index, schema_path=index,
)
def additionalItems(validator, aI, instance, schema):
if (
not validator.is_type(instance, "array")
or validator.is_type(schema.get("items", {}), "object")
):
return
len_items = len(schema.get("items", []))
if validator.is_type(aI, "object"):
for index, item in enumerate(instance[len_items:], start=len_items):
yield from validator.descend(item, aI, path=index)
elif not aI and len(instance) > len(schema.get("items", [])):
error = "Additional items are not allowed (%s %s unexpected)"
yield ValidationError(
error % _utils.extras_msg(instance[len(schema.get("items", [])):]),
)
def items_draft6_draft7_draft201909(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):
yield from validator.descend(
item, subschema, path=index, schema_path=index,
)
else:
for index, item in enumerate(instance):
yield from validator.descend(item, items, path=index)
def minimum_draft3_draft4(validator, minimum, instance, schema):
if not validator.is_type(instance, "number"):
return
if schema.get("exclusiveMinimum", False):
failed = instance <= minimum
cmp = "less than or equal to"
else:
failed = instance < minimum
cmp = "less than"
if failed:
message = f"{instance!r} is {cmp} the minimum of {minimum!r}"
yield ValidationError(message)
def maximum_draft3_draft4(validator, maximum, instance, schema):
if not validator.is_type(instance, "number"):
return
if schema.get("exclusiveMaximum", False):
failed = instance >= maximum
cmp = "greater than or equal to"
else:
failed = instance > maximum
cmp = "greater than"
if failed:
message = f"{instance!r} is {cmp} the maximum of {maximum!r}"
yield ValidationError(message)
def properties_draft3(validator, properties, instance, schema):
if not validator.is_type(instance, "object"):
return
for property, subschema in properties.items():
if property in instance:
yield from validator.descend(
instance[property],
subschema,
path=property,
schema_path=property,
)
elif subschema.get("required", False):
error = ValidationError(f"{property!r} is a required property")
error._set(
validator="required",
validator_value=subschema["required"],
instance=instance,
schema=schema,
)
error.path.appendleft(property)
error.schema_path.extend([property, "required"])
yield error
def type_draft3(validator, types, instance, schema):
types = _utils.ensure_list(types)
all_errors = []
for index, type in enumerate(types):
if validator.is_type(type, "object"):
errors = list(validator.descend(instance, type, schema_path=index))
if not errors:
return
all_errors.extend(errors)
elif validator.is_type(instance, type):
return
reprs = []
for type in types:
try:
reprs.append(repr(type["name"]))
except Exception: # noqa: BLE001
reprs.append(repr(type))
yield ValidationError(
f"{instance!r} is not of type {', '.join(reprs)}",
context=all_errors,
)
def contains_draft6_draft7(validator, contains, instance, schema):
if not validator.is_type(instance, "array"):
return
if not any(
validator.evolve(schema=contains).is_valid(element)
for element in instance
):
yield ValidationError(
f"None of {instance!r} are valid under the given schema",
)
def recursiveRef(validator, recursiveRef, instance, schema):
resolved = lookup_recursive_ref(validator._resolver)
yield from validator.descend(
instance,
resolved.contents,
resolver=resolved.resolver,
)
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
"""
if validator.is_type(schema, "boolean"):
return []
evaluated_indexes = []
ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
if "$recursiveRef" in schema:
resolved = lookup_recursive_ref(validator._resolver)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
if "items" in schema:
if "additionalItems" in schema:
return list(range(len(instance)))
if validator.is_type(schema["items"], "object"):
return list(range(len(instance)))
evaluated_indexes += list(range(len(schema["items"])))
if "if" in schema:
if validator.evolve(schema=schema["if"]).is_valid(instance):
evaluated_indexes += find_evaluated_item_indexes_by_schema(
validator, instance, schema["if"],
)
if "then" in schema:
evaluated_indexes += find_evaluated_item_indexes_by_schema(
validator, instance, schema["then"],
)
elif "else" in schema:
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.evolve(schema=schema[keyword]).is_valid(v):
evaluated_indexes.append(k)
for keyword in ["allOf", "oneOf", "anyOf"]:
if keyword in schema:
for subschema in schema[keyword]:
errs = next(validator.descend(instance, subschema), None)
if errs is None:
evaluated_indexes += find_evaluated_item_indexes_by_schema(
validator, instance, subschema,
)
return evaluated_indexes
def unevaluatedItems_draft2019(validator, unevaluatedItems, instance, schema):
if not validator.is_type(instance, "array"):
return
evaluated_item_indexes = find_evaluated_item_indexes_by_schema(
validator, instance, schema,
)
unevaluated_items = [
item for index, item in enumerate(instance)
if index not in evaluated_item_indexes
]
if unevaluated_items:
error = "Unevaluated items are not allowed (%s %s unexpected)"
yield ValidationError(error % _utils.extras_msg(unevaluated_items))
def find_evaluated_property_keys_by_schema(validator, instance, schema):
if validator.is_type(schema, "boolean"):
return []
evaluated_keys = []
ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
if "$recursiveRef" in schema:
resolved = lookup_recursive_ref(validator._resolver)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
for keyword in [
"properties", "additionalProperties", "unevaluatedProperties",
]:
if keyword in schema:
schema_value = schema[keyword]
if validator.is_type(schema_value, "boolean") and schema_value:
evaluated_keys += instance.keys()
elif validator.is_type(schema_value, "object"):
for property in schema_value:
if property in instance:
evaluated_keys.append(property)
if "patternProperties" in schema:
for property in instance:
for pattern in schema["patternProperties"]:
if re.search(pattern, property):
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,
)
for keyword in ["allOf", "oneOf", "anyOf"]:
if keyword in schema:
for subschema in schema[keyword]:
errs = next(validator.descend(instance, subschema), None)
if errs is None:
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, subschema,
)
if "if" in schema:
if validator.evolve(schema=schema["if"]).is_valid(instance):
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, schema["if"],
)
if "then" in schema:
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, schema["then"],
)
elif "else" in schema:
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, schema["else"],
)
return evaluated_keys
def unevaluatedProperties_draft2019(validator, uP, instance, schema):
if not validator.is_type(instance, "object"):
return
evaluated_keys = find_evaluated_property_keys_by_schema(
validator, instance, schema,
)
unevaluated_keys = []
for property in instance:
if property not in evaluated_keys:
for _ in validator.descend(
instance[property],
uP,
path=property,
schema_path=property,
):
# FIXME: Include context for each unevaluated property
# indicating why it's invalid under the subschema.
unevaluated_keys.append(property) # noqa: PERF401
if unevaluated_keys:
if uP is False:
error = "Unevaluated properties are not allowed (%s %s unexpected)"
extras = sorted(unevaluated_keys, key=str)
yield ValidationError(error % _utils.extras_msg(extras))
else:
error = (
"Unevaluated properties are not valid under "
"the given schema (%s %s unevaluated and invalid)"
)
yield ValidationError(error % _utils.extras_msg(unevaluated_keys))

View File

@@ -0,0 +1,204 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import numbers
from attrs import evolve, field, frozen
from rpds import HashTrieMap
from jsonschema.exceptions import UndefinedTypeCheck
if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Any, Callable
# unfortunately, the type of HashTrieMap is generic, and if used as an attrs
# converter, the generic type is presented to mypy, which then fails to match
# the concrete type of a type checker mapping
# this "do nothing" wrapper presents the correct information to mypy
def _typed_map_converter(
init_val: Mapping[str, Callable[[TypeChecker, Any], bool]],
) -> HashTrieMap[str, Callable[[TypeChecker, Any], bool]]:
return HashTrieMap.convert(init_val)
def is_array(checker, instance):
return isinstance(instance, list)
def is_bool(checker, instance):
return isinstance(instance, bool)
def is_integer(checker, instance):
# bool inherits from int, so ensure bools aren't reported as ints
if isinstance(instance, bool):
return False
return isinstance(instance, int)
def is_null(checker, instance):
return instance is None
def is_number(checker, instance):
# bool inherits from int, so ensure bools aren't reported as ints
if isinstance(instance, bool):
return False
return isinstance(instance, numbers.Number)
def is_object(checker, instance):
return isinstance(instance, dict)
def is_string(checker, instance):
return isinstance(instance, str)
def is_any(checker, instance):
return True
@frozen(repr=False)
class TypeChecker:
"""
A :kw:`type` property checker.
A `TypeChecker` performs type checking for a `Validator`, converting
between the defined JSON Schema types and some associated Python types or
objects.
Modifying the behavior just mentioned by redefining which Python objects
are considered to be of which JSON Schema types can be done using
`TypeChecker.redefine` or `TypeChecker.redefine_many`, and types can be
removed via `TypeChecker.remove`. Each of these return a new `TypeChecker`.
Arguments:
type_checkers:
The initial mapping of types to their checking functions.
"""
_type_checkers: HashTrieMap[
str, Callable[[TypeChecker, Any], bool],
] = field(default=HashTrieMap(), converter=_typed_map_converter)
def __repr__(self):
types = ", ".join(repr(k) for k in sorted(self._type_checkers))
return f"<{self.__class__.__name__} types={{{types}}}>"
def is_type(self, instance, type: str) -> bool:
"""
Check if the instance is of the appropriate type.
Arguments:
instance:
The instance to check
type:
The name of the type that is expected.
Raises:
`jsonschema.exceptions.UndefinedTypeCheck`:
if ``type`` is unknown to this object.
"""
try:
fn = self._type_checkers[type]
except KeyError:
raise UndefinedTypeCheck(type) from None
return fn(self, instance)
def redefine(self, type: str, fn) -> TypeChecker:
"""
Produce a new checker with the given type redefined.
Arguments:
type:
The name of the type to check.
fn (collections.abc.Callable):
A callable taking exactly two parameters - the type
checker calling the function and the instance to check.
The function should return true if instance is of this
type and false otherwise.
"""
return self.redefine_many({type: fn})
def redefine_many(self, definitions=()) -> TypeChecker:
"""
Produce a new checker with the given types redefined.
Arguments:
definitions (dict):
A dictionary mapping types to their checking functions.
"""
type_checkers = self._type_checkers.update(definitions)
return evolve(self, type_checkers=type_checkers)
def remove(self, *types) -> TypeChecker:
"""
Produce a new checker with the given types forgotten.
Arguments:
types:
the names of the types to remove.
Raises:
`jsonschema.exceptions.UndefinedTypeCheck`:
if any given type is unknown to this object
"""
type_checkers = self._type_checkers
for each in types:
try:
type_checkers = type_checkers.remove(each)
except KeyError:
raise UndefinedTypeCheck(each) from None
return evolve(self, type_checkers=type_checkers)
draft3_type_checker = TypeChecker(
{
"any": is_any,
"array": is_array,
"boolean": is_bool,
"integer": is_integer,
"object": is_object,
"null": is_null,
"number": is_number,
"string": is_string,
},
)
draft4_type_checker = draft3_type_checker.remove("any")
draft6_type_checker = draft4_type_checker.redefine(
"integer",
lambda checker, instance: (
is_integer(checker, instance)
or (isinstance(instance, float) and instance.is_integer())
),
)
draft7_type_checker = draft6_type_checker
draft201909_type_checker = draft7_type_checker
draft202012_type_checker = draft201909_type_checker

View File

@@ -0,0 +1,29 @@
"""
Some (initially private) typing helpers for jsonschema's types.
"""
from collections.abc import Iterable
from typing import Any, Callable, Protocol, Union
import referencing.jsonschema
from jsonschema.protocols import Validator
class SchemaKeywordValidator(Protocol):
def __call__(
self,
validator: Validator,
value: Any,
instance: Any,
schema: referencing.jsonschema.Schema,
) -> None:
...
id_of = Callable[[referencing.jsonschema.Schema], Union[str, None]]
ApplicableValidators = Callable[
[referencing.jsonschema.Schema],
Iterable[tuple[str, Any]],
]

View File

@@ -0,0 +1,355 @@
from collections.abc import Mapping, MutableMapping, Sequence
from urllib.parse import urlsplit
import itertools
import re
class URIDict(MutableMapping):
"""
Dictionary which uses normalized URIs as keys.
"""
def normalize(self, uri):
return urlsplit(uri).geturl()
def __init__(self, *args, **kwargs):
self.store = dict()
self.store.update(*args, **kwargs)
def __getitem__(self, uri):
return self.store[self.normalize(uri)]
def __setitem__(self, uri, value):
self.store[self.normalize(uri)] = value
def __delitem__(self, uri):
del self.store[self.normalize(uri)]
def __iter__(self):
return iter(self.store)
def __len__(self): # pragma: no cover -- untested, but to be removed
return len(self.store)
def __repr__(self): # pragma: no cover -- untested, but to be removed
return repr(self.store)
class Unset:
"""
An as-of-yet unset attribute or unprovided default parameter.
"""
def __repr__(self): # pragma: no cover
return "<unset>"
def format_as_index(container, indices):
"""
Construct a single string containing indexing operations for the indices.
For example for a container ``bar``, [1, 2, "foo"] -> bar[1][2]["foo"]
Arguments:
container (str):
A word to use for the thing being indexed
indices (sequence):
The indices to format.
"""
if not indices:
return container
return f"{container}[{']['.join(repr(index) for index in indices)}]"
def find_additional_properties(instance, schema):
"""
Return the set of additional properties for the given ``instance``.
Weeds out properties that should have been validated by ``properties`` and
/ or ``patternProperties``.
Assumes ``instance`` is dict-like already.
"""
properties = schema.get("properties", {})
patterns = "|".join(schema.get("patternProperties", {}))
for property in instance:
if property not in properties:
if patterns and re.search(patterns, property):
continue
yield property
def extras_msg(extras):
"""
Create an error message for extra items or properties.
"""
verb = "was" if len(extras) == 1 else "were"
return ", ".join(repr(extra) for extra in extras), verb
def ensure_list(thing):
"""
Wrap ``thing`` in a list if it's a single str.
Otherwise, return it unchanged.
"""
if isinstance(thing, str):
return [thing]
return thing
def _mapping_equal(one, two):
"""
Check if two mappings are equal using the semantics of `equal`.
"""
if len(one) != len(two):
return False
return all(
key in two and equal(value, two[key])
for key, value in one.items()
)
def _sequence_equal(one, two):
"""
Check if two sequences are equal using the semantics of `equal`.
"""
if len(one) != len(two):
return False
return all(equal(i, j) for i, j in zip(one, two))
def equal(one, two):
"""
Check if two things are equal evading some Python type hierarchy semantics.
Specifically in JSON Schema, evade `bool` inheriting from `int`,
recursing into sequences to do the same.
"""
if one is two:
return True
if isinstance(one, str) or isinstance(two, str):
return one == two
if isinstance(one, Sequence) and isinstance(two, Sequence):
return _sequence_equal(one, two)
if isinstance(one, Mapping) and isinstance(two, Mapping):
return _mapping_equal(one, two)
return unbool(one) == unbool(two)
def unbool(element, true=object(), false=object()):
"""
A hack to make True and 1 and False and 0 unique for ``uniq``.
"""
if element is True:
return true
elif element is False:
return false
return element
def uniq(container):
"""
Check if all of a container's elements are unique.
Tries to rely on the container being recursively sortable, or otherwise
falls back on (slow) brute force.
"""
try:
sort = sorted(unbool(i) for i in container)
sliced = itertools.islice(sort, 1, None)
for i, j in zip(sort, sliced):
if equal(i, j):
return False
except (NotImplementedError, TypeError):
seen = []
for e in container:
e = unbool(e)
for i in seen:
if equal(i, e):
return False
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 all keywords related to unevaluatedItems: items, prefixItems, if,
then, else, contains, unevaluatedItems, allOf, oneOf, anyOf
"""
if validator.is_type(schema, "boolean"):
return []
evaluated_indexes = []
if "items" in schema:
return list(range(len(instance)))
ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
dynamicRef = schema.get("$dynamicRef")
if dynamicRef is not None:
resolved = validator._resolver.lookup(dynamicRef)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
if "prefixItems" in schema:
evaluated_indexes += list(range(len(schema["prefixItems"])))
if "if" in schema:
if validator.evolve(schema=schema["if"]).is_valid(instance):
evaluated_indexes += find_evaluated_item_indexes_by_schema(
validator, instance, schema["if"],
)
if "then" in schema:
evaluated_indexes += find_evaluated_item_indexes_by_schema(
validator, instance, schema["then"],
)
elif "else" in schema:
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.evolve(schema=schema[keyword]).is_valid(v):
evaluated_indexes.append(k)
for keyword in ["allOf", "oneOf", "anyOf"]:
if keyword in schema:
for subschema in schema[keyword]:
errs = next(validator.descend(instance, subschema), None)
if errs is None:
evaluated_indexes += find_evaluated_item_indexes_by_schema(
validator, instance, subschema,
)
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
"""
if validator.is_type(schema, "boolean"):
return []
evaluated_keys = []
ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
dynamicRef = schema.get("$dynamicRef")
if dynamicRef is not None:
resolved = validator._resolver.lookup(dynamicRef)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)
properties = schema.get("properties")
if validator.is_type(properties, "object"):
evaluated_keys += properties.keys() & instance.keys()
for keyword in ["additionalProperties", "unevaluatedProperties"]:
if (subschema := schema.get(keyword)) is None:
continue
evaluated_keys += (
key
for key, value in instance.items()
if is_valid(validator.descend(value, subschema))
)
if "patternProperties" in schema:
for property in instance:
for pattern in schema["patternProperties"]:
if re.search(pattern, property):
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,
)
for keyword in ["allOf", "oneOf", "anyOf"]:
for subschema in schema.get(keyword, []):
if not is_valid(validator.descend(instance, subschema)):
continue
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, subschema,
)
if "if" in schema:
if validator.evolve(schema=schema["if"]).is_valid(instance):
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, schema["if"],
)
if "then" in schema:
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, schema["then"],
)
elif "else" in schema:
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, schema["else"],
)
return evaluated_keys
def is_valid(errs_it):
"""Whether there are no errors in the given iterator."""
return next(errs_it, None) is None

View File

@@ -0,0 +1,5 @@
"""
Benchmarks for validation.
This package is *not* public API.
"""

View File

@@ -0,0 +1,30 @@
"""
A benchmark for comparing equivalent validation of `const` and `enum`.
"""
from pyperf import Runner
from jsonschema import Draft202012Validator
value = [37] * 100
const_schema = {"const": list(value)}
enum_schema = {"enum": [list(value)]}
valid = list(value)
invalid = [*valid, 73]
const = Draft202012Validator(const_schema)
enum = Draft202012Validator(enum_schema)
assert const.is_valid(valid)
assert enum.is_valid(valid)
assert not const.is_valid(invalid)
assert not enum.is_valid(invalid)
if __name__ == "__main__":
runner = Runner()
runner.bench_func("const valid", lambda: const.is_valid(valid))
runner.bench_func("const invalid", lambda: const.is_valid(invalid))
runner.bench_func("enum valid", lambda: enum.is_valid(valid))
runner.bench_func("enum invalid", lambda: enum.is_valid(invalid))

View File

@@ -0,0 +1,28 @@
"""
A benchmark for validation of the `contains` keyword.
"""
from pyperf import Runner
from jsonschema import Draft202012Validator
schema = {
"type": "array",
"contains": {"const": 37},
}
validator = Draft202012Validator(schema)
size = 1000
beginning = [37] + [0] * (size - 1)
middle = [0] * (size // 2) + [37] + [0] * (size // 2)
end = [0] * (size - 1) + [37]
invalid = [0] * size
if __name__ == "__main__":
runner = Runner()
runner.bench_func("baseline", lambda: validator.is_valid([]))
runner.bench_func("beginning", lambda: validator.is_valid(beginning))
runner.bench_func("middle", lambda: validator.is_valid(middle))
runner.bench_func("end", lambda: validator.is_valid(end))
runner.bench_func("invalid", lambda: validator.is_valid(invalid))

View File

@@ -0,0 +1,25 @@
"""
A performance benchmark using the example from issue #232.
See https://github.com/python-jsonschema/jsonschema/pull/232.
"""
from pathlib import Path
from pyperf import Runner
from referencing import Registry
from jsonschema.tests._suite import Version
import jsonschema
issue232 = Version(
path=Path(__file__).parent / "issue232",
remotes=Registry(),
name="issue232",
)
if __name__ == "__main__":
issue232.benchmark(
runner=Runner(),
Validator=jsonschema.Draft4Validator,
)

View File

@@ -0,0 +1,12 @@
"""
A performance benchmark using the official test suite.
This benchmarks jsonschema using every valid example in the
JSON-Schema-Test-Suite. It will take some time to complete.
"""
from pyperf import Runner
from jsonschema.tests._suite import Suite
if __name__ == "__main__":
Suite().benchmark(runner=Runner())

View File

@@ -0,0 +1,56 @@
"""
Validating highly nested schemas shouldn't cause exponential time blowups.
See https://github.com/python-jsonschema/jsonschema/issues/1097.
"""
from itertools import cycle
from jsonschema.validators import validator_for
metaschemaish = {
"$id": "https://example.com/draft/2020-12/schema/strict",
"$schema": "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",
"$ref": "https://json-schema.org/draft/2020-12/schema",
"unevaluatedProperties": False,
}
def nested_schema(levels):
"""
Produce a schema which validates deeply nested objects and arrays.
"""
names = cycle(["foo", "bar", "baz", "quux", "spam", "eggs"])
schema = {"type": "object", "properties": {"ham": {"type": "string"}}}
for _, name in zip(range(levels - 1), names):
schema = {"type": "object", "properties": {name: schema}}
return schema
validator = validator_for(metaschemaish)(metaschemaish)
if __name__ == "__main__":
from pyperf import Runner
runner = Runner()
not_nested = nested_schema(levels=1)
runner.bench_func("not nested", lambda: validator.is_valid(not_nested))
for levels in range(1, 11, 3):
schema = nested_schema(levels=levels)
runner.bench_func(
f"nested * {levels}",
lambda schema=schema: validator.is_valid(schema),
)

View File

@@ -0,0 +1,42 @@
"""
A benchmark which tries to compare the possible slow subparts of validation.
"""
from referencing import Registry
from referencing.jsonschema import DRAFT202012
from rpds import HashTrieMap, HashTrieSet
from jsonschema import Draft202012Validator
schema = {
"type": "array",
"minLength": 1,
"maxLength": 1,
"items": {"type": "integer"},
}
hmap = HashTrieMap()
hset = HashTrieSet()
registry = Registry()
v = Draft202012Validator(schema)
def registry_data_structures():
return hmap.insert("foo", "bar"), hset.insert("foo")
def registry_add():
resource = DRAFT202012.create_resource(schema)
return registry.with_resource(uri="urn:example", resource=resource)
if __name__ == "__main__":
from pyperf import Runner
runner = Runner()
runner.bench_func("HashMap/HashSet insertion", registry_data_structures)
runner.bench_func("Registry insertion", registry_add)
runner.bench_func("Success", lambda: v.is_valid([1]))
runner.bench_func("Failure", lambda: v.is_valid(["foo"]))
runner.bench_func("Metaschema validation", lambda: v.check_schema(schema))

View File

@@ -0,0 +1,35 @@
"""
An unused schema registry should not cause slower validation.
"Unused" here means one where no reference resolution is occurring anyhow.
See https://github.com/python-jsonschema/jsonschema/issues/1088.
"""
from pyperf import Runner
from referencing import Registry
from referencing.jsonschema import DRAFT201909
from jsonschema import Draft201909Validator
registry = Registry().with_resource(
"urn:example:foo",
DRAFT201909.create_resource({}),
)
schema = {"$ref": "https://json-schema.org/draft/2019-09/schema"}
instance = {"maxLength": 4}
no_registry = Draft201909Validator(schema)
with_useless_registry = Draft201909Validator(schema, registry=registry)
if __name__ == "__main__":
runner = Runner()
runner.bench_func(
"no registry",
lambda: no_registry.is_valid(instance),
)
runner.bench_func(
"useless registry",
lambda: with_useless_registry.is_valid(instance),
)

View File

@@ -0,0 +1,106 @@
"""
A benchmark for validation of applicators containing lots of useless schemas.
Signals a small possible optimization to remove all such schemas ahead of time.
"""
from pyperf import Runner
from jsonschema import Draft202012Validator as Validator
NUM_USELESS = 100000
subschema = {"const": 37}
valid = 37
invalid = 12
baseline = Validator(subschema)
# These should be indistinguishable from just `subschema`
by_name = {
"single subschema": {
"anyOf": Validator({"anyOf": [subschema]}),
"allOf": Validator({"allOf": [subschema]}),
"oneOf": Validator({"oneOf": [subschema]}),
},
"redundant subschemas": {
"anyOf": Validator({"anyOf": [subschema] * NUM_USELESS}),
"allOf": Validator({"allOf": [subschema] * NUM_USELESS}),
},
"useless successful subschemas (beginning)": {
"anyOf": Validator({"anyOf": [subschema, *[True] * NUM_USELESS]}),
"allOf": Validator({"allOf": [subschema, *[True] * NUM_USELESS]}),
},
"useless successful subschemas (middle)": {
"anyOf": Validator(
{
"anyOf": [
*[True] * (NUM_USELESS // 2),
subschema,
*[True] * (NUM_USELESS // 2),
],
},
),
"allOf": Validator(
{
"allOf": [
*[True] * (NUM_USELESS // 2),
subschema,
*[True] * (NUM_USELESS // 2),
],
},
),
},
"useless successful subschemas (end)": {
"anyOf": Validator({"anyOf": [*[True] * NUM_USELESS, subschema]}),
"allOf": Validator({"allOf": [*[True] * NUM_USELESS, subschema]}),
},
"useless failing subschemas (beginning)": {
"anyOf": Validator({"anyOf": [subschema, *[False] * NUM_USELESS]}),
"oneOf": Validator({"oneOf": [subschema, *[False] * NUM_USELESS]}),
},
"useless failing subschemas (middle)": {
"anyOf": Validator(
{
"anyOf": [
*[False] * (NUM_USELESS // 2),
subschema,
*[False] * (NUM_USELESS // 2),
],
},
),
"oneOf": Validator(
{
"oneOf": [
*[False] * (NUM_USELESS // 2),
subschema,
*[False] * (NUM_USELESS // 2),
],
},
),
},
"useless failing subschemas (end)": {
"anyOf": Validator({"anyOf": [*[False] * NUM_USELESS, subschema]}),
"oneOf": Validator({"oneOf": [*[False] * NUM_USELESS, subschema]}),
},
}
if __name__ == "__main__":
runner = Runner()
runner.bench_func("baseline valid", lambda: baseline.is_valid(valid))
runner.bench_func("baseline invalid", lambda: baseline.is_valid(invalid))
for group, applicators in by_name.items():
for applicator, validator in applicators.items():
runner.bench_func(
f"{group}: {applicator} valid",
lambda validator=validator: validator.is_valid(valid),
)
runner.bench_func(
f"{group}: {applicator} invalid",
lambda validator=validator: validator.is_valid(invalid),
)

View File

@@ -0,0 +1,32 @@
"""
A benchmark for validation of schemas containing lots of useless keywords.
Checks we filter them out once, ahead of time.
"""
from pyperf import Runner
from jsonschema import Draft202012Validator
NUM_USELESS = 100000
schema = dict(
[
("not", {"const": 42}),
*((str(i), i) for i in range(NUM_USELESS)),
("type", "integer"),
*((str(i), i) for i in range(NUM_USELESS, NUM_USELESS)),
("minimum", 37),
],
)
validator = Draft202012Validator(schema)
valid = 3737
invalid = 12
if __name__ == "__main__":
runner = Runner()
runner.bench_func("beginning of schema", lambda: validator.is_valid(42))
runner.bench_func("middle of schema", lambda: validator.is_valid("foo"))
runner.bench_func("end of schema", lambda: validator.is_valid(12))
runner.bench_func("valid", lambda: validator.is_valid(3737))

View File

@@ -0,0 +1,14 @@
from pyperf import Runner
from jsonschema import Draft202012Validator
schema = {
"type": "array",
"minLength": 1,
"maxLength": 1,
"items": {"type": "integer"},
}
if __name__ == "__main__":
Runner().bench_func("validator creation", Draft202012Validator, schema)

View File

@@ -0,0 +1,292 @@
"""
The ``jsonschema`` command line.
"""
from importlib import metadata
from json import JSONDecodeError
from pkgutil import resolve_name
from textwrap import dedent
import argparse
import json
import sys
import traceback
import warnings
from attrs import define, field
from jsonschema.exceptions import SchemaError
from jsonschema.validators import _RefResolver, validator_for
warnings.warn(
(
"The jsonschema CLI is deprecated and will be removed in a future "
"version. Please use check-jsonschema instead, which can be installed "
"from https://pypi.org/project/check-jsonschema/"
),
DeprecationWarning,
stacklevel=2,
)
class _CannotLoadFile(Exception):
pass
@define
class _Outputter:
_formatter = field()
_stdout = field()
_stderr = field()
@classmethod
def from_arguments(cls, arguments, stdout, stderr):
if arguments["output"] == "plain":
formatter = _PlainFormatter(arguments["error_format"])
elif arguments["output"] == "pretty":
formatter = _PrettyFormatter()
return cls(formatter=formatter, stdout=stdout, stderr=stderr)
def load(self, path):
try:
file = open(path) # noqa: SIM115, PTH123
except FileNotFoundError as error:
self.filenotfound_error(path=path, exc_info=sys.exc_info())
raise _CannotLoadFile() from error
with file:
try:
return json.load(file)
except JSONDecodeError as error:
self.parsing_error(path=path, exc_info=sys.exc_info())
raise _CannotLoadFile() from error
def filenotfound_error(self, **kwargs):
self._stderr.write(self._formatter.filenotfound_error(**kwargs))
def parsing_error(self, **kwargs):
self._stderr.write(self._formatter.parsing_error(**kwargs))
def validation_error(self, **kwargs):
self._stderr.write(self._formatter.validation_error(**kwargs))
def validation_success(self, **kwargs):
self._stdout.write(self._formatter.validation_success(**kwargs))
@define
class _PrettyFormatter:
_ERROR_MSG = dedent(
"""\
===[{type}]===({path})===
{body}
-----------------------------
""",
)
_SUCCESS_MSG = "===[SUCCESS]===({path})===\n"
def filenotfound_error(self, path, exc_info):
return self._ERROR_MSG.format(
path=path,
type="FileNotFoundError",
body=f"{path!r} does not exist.",
)
def parsing_error(self, path, exc_info):
exc_type, exc_value, exc_traceback = exc_info
exc_lines = "".join(
traceback.format_exception(exc_type, exc_value, exc_traceback),
)
return self._ERROR_MSG.format(
path=path,
type=exc_type.__name__,
body=exc_lines,
)
def validation_error(self, instance_path, error):
return self._ERROR_MSG.format(
path=instance_path,
type=error.__class__.__name__,
body=error,
)
def validation_success(self, instance_path):
return self._SUCCESS_MSG.format(path=instance_path)
@define
class _PlainFormatter:
_error_format = field()
def filenotfound_error(self, path, exc_info):
return f"{path!r} does not exist.\n"
def parsing_error(self, path, exc_info):
return "Failed to parse {}: {}\n".format(
"<stdin>" if path == "<stdin>" else repr(path),
exc_info[1],
)
def validation_error(self, instance_path, error):
return self._error_format.format(file_name=instance_path, error=error)
def validation_success(self, instance_path):
return ""
def _resolve_name_with_default(name):
if "." not in name:
name = "jsonschema." + name
return resolve_name(name)
parser = argparse.ArgumentParser(
description="JSON Schema Validation CLI",
)
parser.add_argument(
"-i", "--instance",
action="append",
dest="instances",
help="""
a path to a JSON instance (i.e. filename.json) to validate (may
be specified multiple times). If no instances are provided via this
option, one will be expected on standard input.
""",
)
parser.add_argument(
"-F", "--error-format",
help="""
the format to use for each validation error message, specified
in a form suitable for str.format. This string will be passed
one formatted object named 'error' for each ValidationError.
Only provide this option when using --output=plain, which is the
default. If this argument is unprovided and --output=plain is
used, a simple default representation will be used.
""",
)
parser.add_argument(
"-o", "--output",
choices=["plain", "pretty"],
default="plain",
help="""
an output format to use. 'plain' (default) will produce minimal
text with one line for each error, while 'pretty' will produce
more detailed human-readable output on multiple lines.
""",
)
parser.add_argument(
"-V", "--validator",
type=_resolve_name_with_default,
help="""
the fully qualified object name of a validator to use, or, for
validators that are registered with jsonschema, simply the name
of the class.
""",
)
parser.add_argument(
"--base-uri",
help="""
a base URI to assign to the provided schema, even if it does not
declare one (via e.g. $id). This option can be used if you wish to
resolve relative references to a particular URI (or local path)
""",
)
parser.add_argument(
"--version",
action="version",
version=metadata.version("jsonschema"),
)
parser.add_argument(
"schema",
help="the path to a JSON Schema to validate with (i.e. schema.json)",
)
def parse_args(args): # noqa: D103
arguments = vars(parser.parse_args(args=args or ["--help"]))
if arguments["output"] != "plain" and arguments["error_format"]:
raise parser.error(
"--error-format can only be used with --output plain",
)
if arguments["output"] == "plain" and arguments["error_format"] is None:
arguments["error_format"] = "{error.instance}: {error.message}\n"
return arguments
def _validate_instance(instance_path, instance, validator, outputter):
invalid = False
for error in validator.iter_errors(instance):
invalid = True
outputter.validation_error(instance_path=instance_path, error=error)
if not invalid:
outputter.validation_success(instance_path=instance_path)
return invalid
def main(args=sys.argv[1:]): # noqa: D103
sys.exit(run(arguments=parse_args(args=args)))
def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): # noqa: D103
outputter = _Outputter.from_arguments(
arguments=arguments,
stdout=stdout,
stderr=stderr,
)
try:
schema = outputter.load(arguments["schema"])
except _CannotLoadFile:
return 1
Validator = arguments["validator"]
if Validator is None:
Validator = validator_for(schema)
try:
Validator.check_schema(schema)
except SchemaError as error:
outputter.validation_error(
instance_path=arguments["schema"],
error=error,
)
return 1
if arguments["instances"]:
load, instances = outputter.load, arguments["instances"]
else:
def load(_):
try:
return json.load(stdin)
except JSONDecodeError as error:
outputter.parsing_error(
path="<stdin>", exc_info=sys.exc_info(),
)
raise _CannotLoadFile() from error
instances = ["<stdin>"]
resolver = _RefResolver(
base_uri=arguments["base_uri"],
referrer=schema,
) if arguments["base_uri"] is not None else None
validator = Validator(schema, resolver=resolver)
exit_code = 0
for each in instances:
try:
instance = load(each)
except _CannotLoadFile:
exit_code = 1
else:
exit_code |= _validate_instance(
instance_path=each,
instance=instance,
validator=validator,
outputter=outputter,
)
return exit_code

View File

@@ -0,0 +1,490 @@
"""
Validation errors, and some surrounding helpers.
"""
from __future__ import annotations
from collections import defaultdict, deque
from pprint import pformat
from textwrap import dedent, indent
from typing import TYPE_CHECKING, Any, ClassVar
import heapq
import re
import warnings
from attrs import define
from referencing.exceptions import Unresolvable as _Unresolvable
from jsonschema import _utils
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
from jsonschema import _types
WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
STRONG_MATCHES: frozenset[str] = frozenset()
_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$")
_unset = _utils.Unset()
def _pretty(thing: Any, prefix: str):
"""
Format something for an error message as prettily as we currently can.
"""
return indent(pformat(thing, width=72, sort_dicts=False), prefix).lstrip()
def __getattr__(name):
if name == "RefResolutionError":
warnings.warn(
_RefResolutionError._DEPRECATION_MESSAGE,
DeprecationWarning,
stacklevel=2,
)
return _RefResolutionError
raise AttributeError(f"module {__name__} has no attribute {name}")
class _Error(Exception):
_word_for_schema_in_error_message: ClassVar[str]
_word_for_instance_in_error_message: ClassVar[str]
def __init__(
self,
message: str,
validator: str = _unset, # type: ignore[assignment]
path: Iterable[str | int] = (),
cause: Exception | None = None,
context=(),
validator_value: Any = _unset,
instance: Any = _unset,
schema: Mapping[str, Any] | bool = _unset, # type: ignore[assignment]
schema_path: Iterable[str | int] = (),
parent: _Error | None = None,
type_checker: _types.TypeChecker = _unset, # type: ignore[assignment]
) -> None:
super().__init__(
message,
validator,
path,
cause,
context,
validator_value,
instance,
schema,
schema_path,
parent,
)
self.message = message
self.path = self.relative_path = deque(path)
self.schema_path = self.relative_schema_path = deque(schema_path)
self.context = list(context)
self.cause = self.__cause__ = cause
self.validator = validator
self.validator_value = validator_value
self.instance = instance
self.schema = schema
self.parent = parent
self._type_checker = type_checker
for error in context:
error.parent = self
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {self.message!r}>"
def __str__(self) -> str:
essential_for_verbose = (
self.validator, self.validator_value, self.instance, self.schema,
)
if any(m is _unset for m in essential_for_verbose):
return self.message
schema_path = _utils.format_as_index(
container=self._word_for_schema_in_error_message,
indices=list(self.relative_schema_path)[:-1],
)
instance_path = _utils.format_as_index(
container=self._word_for_instance_in_error_message,
indices=self.relative_path,
)
prefix = 16 * " "
return dedent(
f"""\
{self.message}
Failed validating {self.validator!r} in {schema_path}:
{_pretty(self.schema, prefix=prefix)}
On {instance_path}:
{_pretty(self.instance, prefix=prefix)}
""".rstrip(),
)
@classmethod
def create_from(cls, other: _Error):
return cls(**other._contents())
@property
def absolute_path(self) -> Sequence[str | int]:
parent = self.parent
if parent is None:
return self.relative_path
path = deque(self.relative_path)
path.extendleft(reversed(parent.absolute_path))
return path
@property
def absolute_schema_path(self) -> Sequence[str | int]:
parent = self.parent
if parent is None:
return self.relative_schema_path
path = deque(self.relative_schema_path)
path.extendleft(reversed(parent.absolute_schema_path))
return path
@property
def json_path(self) -> str:
path = "$"
for elem in self.absolute_path:
if isinstance(elem, int):
path += "[" + str(elem) + "]"
elif _JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem):
path += "." + elem
else:
escaped_elem = elem.replace("\\", "\\\\").replace("'", r"\'")
path += "['" + escaped_elem + "']"
return path
def _set(
self,
type_checker: _types.TypeChecker | None = None,
**kwargs: Any,
) -> None:
if type_checker is not None and self._type_checker is _unset:
self._type_checker = type_checker
for k, v in kwargs.items():
if getattr(self, k) is _unset:
setattr(self, k, v)
def _contents(self):
attrs = (
"message", "cause", "context", "validator", "validator_value",
"path", "schema_path", "instance", "schema", "parent",
)
return {attr: getattr(self, attr) for attr in attrs}
def _matches_type(self) -> bool:
try:
# We ignore this as we want to simply crash if this happens
expected = self.schema["type"] # type: ignore[index]
except (KeyError, TypeError):
return False
if isinstance(expected, str):
return self._type_checker.is_type(self.instance, expected)
return any(
self._type_checker.is_type(self.instance, expected_type)
for expected_type in expected
)
class ValidationError(_Error):
"""
An instance was invalid under a provided schema.
"""
_word_for_schema_in_error_message = "schema"
_word_for_instance_in_error_message = "instance"
class SchemaError(_Error):
"""
A schema was invalid under its corresponding metaschema.
"""
_word_for_schema_in_error_message = "metaschema"
_word_for_instance_in_error_message = "schema"
@define(slots=False)
class _RefResolutionError(Exception): # noqa: PLW1641
"""
A ref could not be resolved.
"""
_DEPRECATION_MESSAGE = (
"jsonschema.exceptions.RefResolutionError is deprecated as of version "
"4.18.0. If you wish to catch potential reference resolution errors, "
"directly catch referencing.exceptions.Unresolvable."
)
_cause: Exception
def __eq__(self, other):
if self.__class__ is not other.__class__:
return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501
return self._cause == other._cause
def __str__(self) -> str:
return str(self._cause)
class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501
def __init__(self, cause: _Unresolvable):
object.__setattr__(self, "_wrapped", cause)
def __eq__(self, other):
if other.__class__ is self.__class__:
return self._wrapped == other._wrapped
elif other.__class__ is self._wrapped.__class__:
return self._wrapped == other
return NotImplemented
def __getattr__(self, attr):
return getattr(self._wrapped, attr)
def __hash__(self):
return hash(self._wrapped)
def __repr__(self):
return f"<WrappedReferencingError {self._wrapped!r}>"
def __str__(self):
return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
class UndefinedTypeCheck(Exception):
"""
A type checker was asked to check a type it did not have registered.
"""
def __init__(self, type: str) -> None:
self.type = type
def __str__(self) -> str:
return f"Type {self.type!r} is unknown to this type checker"
class UnknownType(Exception):
"""
A validator was asked to validate an instance against an unknown type.
"""
def __init__(self, type, instance, schema):
self.type = type
self.instance = instance
self.schema = schema
def __str__(self):
prefix = 16 * " "
return dedent(
f"""\
Unknown type {self.type!r} for validator with schema:
{_pretty(self.schema, prefix=prefix)}
While checking instance:
{_pretty(self.instance, prefix=prefix)}
""".rstrip(),
)
class FormatError(Exception):
"""
Validating a format failed.
"""
def __init__(self, message, cause=None):
super().__init__(message, cause)
self.message = message
self.cause = self.__cause__ = cause
def __str__(self):
return self.message
class ErrorTree:
"""
ErrorTrees make it easier to check which validations failed.
"""
_instance = _unset
def __init__(self, errors: Iterable[ValidationError] = ()):
self.errors: MutableMapping[str, ValidationError] = {}
self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
for error in errors:
container = self
for element in error.path:
container = container[element]
container.errors[error.validator] = error
container._instance = error.instance
def __contains__(self, index: str | int):
"""
Check whether ``instance[index]`` has any errors.
"""
return index in self._contents
def __getitem__(self, index):
"""
Retrieve the child tree one level down at the given ``index``.
If the index is not in the instance that this tree corresponds
to and is not known by this tree, whatever error would be raised
by ``instance.__getitem__`` will be propagated (usually this is
some subclass of `LookupError`.
"""
if self._instance is not _unset and index not in self:
self._instance[index]
return self._contents[index]
def __setitem__(self, index: str | int, value: ErrorTree):
"""
Add an error to the tree at the given ``index``.
.. deprecated:: v4.20.0
Setting items on an `ErrorTree` is deprecated without replacement.
To populate a tree, provide all of its sub-errors when you
construct the tree.
"""
warnings.warn(
"ErrorTree.__setitem__ is deprecated without replacement.",
DeprecationWarning,
stacklevel=2,
)
self._contents[index] = value # type: ignore[index]
def __iter__(self):
"""
Iterate (non-recursively) over the indices in the instance with errors.
"""
return iter(self._contents)
def __len__(self):
"""
Return the `total_errors`.
"""
return self.total_errors
def __repr__(self):
total = len(self)
errors = "error" if total == 1 else "errors"
return f"<{self.__class__.__name__} ({total} total {errors})>"
@property
def total_errors(self):
"""
The total number of errors in the entire tree, including children.
"""
child_errors = sum(len(tree) for _, tree in self._contents.items())
return len(self.errors) + child_errors
def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
"""
Create a key function that can be used to sort errors by relevance.
Arguments:
weak (set):
a collection of validation keywords to consider to be
"weak". If there are two errors at the same level of the
instance and one is in the set of weak validation keywords,
the other error will take priority. By default, :kw:`anyOf`
and :kw:`oneOf` are considered weak keywords and will be
superseded by other same-level validation errors.
strong (set):
a collection of validation keywords to consider to be
"strong"
"""
def relevance(error):
validator = error.validator
return ( # prefer errors which are ...
-len(error.path), # 'deeper' and thereby more specific
error.path, # earlier (for sibling errors)
validator not in weak, # for a non-low-priority keyword
validator in strong, # for a high priority keyword
not error._matches_type(), # at least match the instance's type
) # otherwise we'll treat them the same
return relevance
relevance = by_relevance()
"""
A key function (e.g. to use with `sorted`) which sorts errors by relevance.
Example:
.. code:: python
sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
"""
def best_match(errors, key=relevance):
"""
Try to find an error that appears to be the best match among given errors.
In general, errors that are higher up in the instance (i.e. for which
`ValidationError.path` is shorter) are considered better matches,
since they indicate "more" is wrong with the instance.
If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
*opposite* assumption is made -- i.e. the deepest error is picked,
since these keywords only need to match once, and any other errors
may not be relevant.
Arguments:
errors (collections.abc.Iterable):
the errors to select from. Do not provide a mixture of
errors from different validation attempts (i.e. from
different instances or schemas), since it won't produce
sensical output.
key (collections.abc.Callable):
the key to use when sorting errors. See `relevance` and
transitively `by_relevance` for more details (the default is
to sort with the defaults of that function). Changing the
default is only useful if you want to change the function
that rates errors but still want the error context descent
done by this function.
Returns:
the best matching error, or ``None`` if the iterable was empty
.. note::
This function is a heuristic. Its return value may change for a given
set of inputs from version to version if better heuristics are added.
"""
best = max(errors, key=key, default=None)
if best is None:
return
while best.context:
# Calculate the minimum via nsmallest, because we don't recurse if
# all nested errors have the same relevance (i.e. if min == max == all)
smallest = heapq.nsmallest(2, best.context, key=key)
if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): # noqa: PLR2004
return best
best = smallest[0]
return best

View File

@@ -0,0 +1,230 @@
"""
typing.Protocol classes for jsonschema interfaces.
"""
# for reference material on Protocols, see
# https://www.python.org/dev/peps/pep-0544/
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable
# in order for Sphinx to resolve references accurately from type annotations,
# it needs to see names like `jsonschema.TypeChecker`
# therefore, only import at type-checking time (to avoid circular references),
# but use `jsonschema` for any types which will otherwise not be resolvable
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
import referencing.jsonschema
from jsonschema import _typing
from jsonschema.exceptions import ValidationError
import jsonschema
import jsonschema.validators
# For code authors working on the validator protocol, these are the three
# use-cases which should be kept in mind:
#
# 1. As a protocol class, it can be used in type annotations to describe the
# available methods and attributes of a validator
# 2. It is the source of autodoc for the validator documentation
# 3. It is runtime_checkable, meaning that it can be used in isinstance()
# checks.
#
# Since protocols are not base classes, isinstance() checking is limited in
# its capabilities. See docs on runtime_checkable for detail
@runtime_checkable
class Validator(Protocol):
"""
The protocol to which all validator classes adhere.
Arguments:
schema:
The schema that the validator object will validate with.
It is assumed to be valid, and providing
an invalid schema can lead to undefined behavior. See
`Validator.check_schema` to validate a schema first.
registry:
a schema registry that will be used for looking up JSON references
resolver:
a resolver that will be used to resolve :kw:`$ref`
properties (JSON references). If unprovided, one will be created.
.. deprecated:: v4.18.0
`RefResolver <_RefResolver>` has been deprecated in favor of
`referencing`, and with it, this argument.
format_checker:
if provided, a checker which will be used to assert about
:kw:`format` properties present in the schema. If unprovided,
*no* format validation is done, and the presence of format
within schemas is strictly informational. Certain formats
require additional packages to be installed in order to assert
against instances. Ensure you've installed `jsonschema` with
its `extra (optional) dependencies <index:extras>` when
invoking ``pip``.
.. deprecated:: v4.12.0
Subclassing validator classes now explicitly warns this is not part of
their public API.
"""
#: An object representing the validator's meta schema (the schema that
#: describes valid schemas in the given version).
META_SCHEMA: ClassVar[Mapping]
#: A mapping of validation keywords (`str`\s) to functions that
#: validate the keyword with that name. For more information see
#: `creating-validators`.
VALIDATORS: ClassVar[Mapping]
#: A `jsonschema.TypeChecker` that will be used when validating
#: :kw:`type` keywords in JSON schemas.
TYPE_CHECKER: ClassVar[jsonschema.TypeChecker]
#: A `jsonschema.FormatChecker` that will be used when validating
#: :kw:`format` keywords in JSON schemas.
FORMAT_CHECKER: ClassVar[jsonschema.FormatChecker]
#: A function which given a schema returns its ID.
ID_OF: _typing.id_of
#: The schema that will be used to validate instances
schema: Mapping | bool
def __init__(
self,
schema: Mapping | bool,
resolver: Any = None, # deprecated
format_checker: jsonschema.FormatChecker | None = None,
*,
registry: referencing.jsonschema.SchemaRegistry = ...,
) -> None: ...
@classmethod
def check_schema(cls, schema: Mapping | bool) -> None:
"""
Validate the given schema against the validator's `META_SCHEMA`.
Raises:
`jsonschema.exceptions.SchemaError`:
if the schema is invalid
"""
def is_type(self, instance: Any, type: str) -> bool:
"""
Check if the instance is of the given (JSON Schema) type.
Arguments:
instance:
the value to check
type:
the name of a known (JSON Schema) type
Returns:
whether the instance is of the given type
Raises:
`jsonschema.exceptions.UnknownType`:
if ``type`` is not a known type
"""
def is_valid(self, instance: Any) -> bool:
"""
Check if the instance is valid under the current `schema`.
Returns:
whether the instance is valid or not
>>> schema = {"maxItems" : 2}
>>> Draft202012Validator(schema).is_valid([2, 3, 4])
False
"""
def iter_errors(self, instance: Any) -> Iterable[ValidationError]:
r"""
Lazily yield each of the validation errors in the given instance.
>>> schema = {
... "type" : "array",
... "items" : {"enum" : [1, 2, 3]},
... "maxItems" : 2,
... }
>>> v = Draft202012Validator(schema)
>>> for error in sorted(v.iter_errors([2, 3, 4]), key=str):
... print(error.message)
4 is not one of [1, 2, 3]
[2, 3, 4] is too long
.. deprecated:: v4.0.0
Calling this function with a second schema argument is deprecated.
Use `Validator.evolve` instead.
"""
def validate(self, instance: Any) -> None:
"""
Check if the instance is valid under the current `schema`.
Raises:
`jsonschema.exceptions.ValidationError`:
if the instance is invalid
>>> schema = {"maxItems" : 2}
>>> Draft202012Validator(schema).validate([2, 3, 4])
Traceback (most recent call last):
...
ValidationError: [2, 3, 4] is too long
"""
def evolve(self, **kwargs) -> Validator:
"""
Create a new validator like this one, but with given changes.
Preserves all other attributes, so can be used to e.g. create a
validator with a different schema but with the same :kw:`$ref`
resolution behavior.
>>> validator = Draft202012Validator({})
>>> validator.evolve(schema={"type": "number"})
Draft202012Validator(schema={'type': 'number'}, format_checker=None)
The returned object satisfies the validator protocol, but may not
be of the same concrete class! In particular this occurs
when a :kw:`$ref` occurs to a schema with a different
:kw:`$schema` than this one (i.e. for a different draft).
>>> validator.evolve(
... schema={"$schema": Draft7Validator.META_SCHEMA["$id"]}
... )
Draft7Validator(schema=..., format_checker=None)
"""

View File

@@ -0,0 +1,285 @@
"""
Python representations of the JSON Schema Test Suite tests.
"""
from __future__ import annotations
from contextlib import suppress
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any
import json
import os
import re
import sys
import unittest
from attrs import field, frozen
from referencing import Registry
import referencing.jsonschema
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping, Sequence
from referencing.jsonschema import Schema
import pyperf
from jsonschema.validators import _VALIDATORS
import jsonschema
MAGIC_REMOTE_URL = "http://localhost:1234"
_DELIMITERS = re.compile(r"[\W\- ]+")
def _find_suite():
root = os.environ.get("JSON_SCHEMA_TEST_SUITE")
if root is not None:
return Path(root)
root = Path(jsonschema.__file__).parent.parent / "json"
if not root.is_dir(): # pragma: no cover
raise ValueError(
(
"Can't find the JSON-Schema-Test-Suite directory. "
"Set the 'JSON_SCHEMA_TEST_SUITE' environment "
"variable or run the tests from alongside a checkout "
"of the suite."
),
)
return root
@frozen
class Suite:
_root: Path = field(factory=_find_suite)
def benchmark(self, runner: pyperf.Runner): # pragma: no cover
for name, Validator in _VALIDATORS.items():
self.version(name=name).benchmark(
runner=runner,
Validator=Validator,
)
def version(self, name) -> Version:
Validator = _VALIDATORS[name]
uri: str = Validator.ID_OF(Validator.META_SCHEMA) # type: ignore[assignment]
specification = referencing.jsonschema.specification_with(uri)
registry = Registry().with_contents(
remotes_in(root=self._root / "remotes", name=name, uri=uri),
default_specification=specification,
)
return Version(
name=name,
path=self._root / "tests" / name,
remotes=registry,
)
@frozen
class Version:
_path: Path
_remotes: referencing.jsonschema.SchemaRegistry
name: str
def benchmark(self, **kwargs): # pragma: no cover
for case in self.cases():
case.benchmark(**kwargs)
def cases(self) -> Iterable[_Case]:
return self._cases_in(paths=self._path.glob("*.json"))
def format_cases(self) -> Iterable[_Case]:
return self._cases_in(paths=self._path.glob("optional/format/*.json"))
def optional_cases_of(self, name: str) -> Iterable[_Case]:
return self._cases_in(paths=[self._path / "optional" / f"{name}.json"])
def to_unittest_testcase(self, *groups, **kwargs):
name = kwargs.pop("name", "Test" + self.name.title().replace("-", ""))
methods = {
method.__name__: method
for method in (
test.to_unittest_method(**kwargs)
for group in groups
for case in group
for test in case.tests
)
}
cls = type(name, (unittest.TestCase,), methods)
# We're doing crazy things, so if they go wrong, like a function
# behaving differently on some other interpreter, just make them
# not happen.
with suppress(Exception):
cls.__module__ = _someone_save_us_the_module_of_the_caller()
return cls
def _cases_in(self, paths: Iterable[Path]) -> Iterable[_Case]:
for path in paths:
for case in json.loads(path.read_text(encoding="utf-8")):
yield _Case.from_dict(
case,
version=self,
subject=path.stem,
remotes=self._remotes,
)
@frozen
class _Case:
version: Version
subject: str
description: str
schema: Mapping[str, Any] | bool
tests: list[_Test]
comment: str | None = None
specification: Sequence[dict[str, str]] = ()
@classmethod
def from_dict(cls, data, remotes, **kwargs):
data.update(kwargs)
tests = [
_Test(
version=data["version"],
subject=data["subject"],
case_description=data["description"],
schema=data["schema"],
remotes=remotes,
**test,
) for test in data.pop("tests")
]
return cls(tests=tests, **data)
def benchmark(self, runner: pyperf.Runner, **kwargs): # pragma: no cover
for test in self.tests:
runner.bench_func(
test.fully_qualified_name,
partial(test.validate_ignoring_errors, **kwargs),
)
def remotes_in(
root: Path,
name: str,
uri: str,
) -> Iterable[tuple[str, Schema]]:
# This messy logic is because the test suite is terrible at indicating
# what remotes are needed for what drafts, and mixes in schemas which
# have no $schema and which are invalid under earlier versions, in with
# other schemas which are needed for tests.
for each in root.rglob("*.json"):
schema = json.loads(each.read_text())
relative = str(each.relative_to(root)).replace("\\", "/")
if (
( # invalid boolean schema
name in {"draft3", "draft4"}
and each.stem == "tree"
) or
( # draft<NotThisDialect>/*.json
"$schema" not in schema
and relative.startswith("draft")
and not relative.startswith(name)
)
):
continue
yield f"{MAGIC_REMOTE_URL}/{relative}", schema
@frozen(repr=False)
class _Test:
version: Version
subject: str
case_description: str
description: str
data: Any
schema: Mapping[str, Any] | bool
valid: bool
_remotes: referencing.jsonschema.SchemaRegistry
comment: str | None = None
def __repr__(self): # pragma: no cover
return f"<Test {self.fully_qualified_name}>"
@property
def fully_qualified_name(self): # pragma: no cover
return " > ".join( # noqa: FLY002
[
self.version.name,
self.subject,
self.case_description,
self.description,
],
)
def to_unittest_method(self, skip=lambda test: None, **kwargs):
if self.valid:
def fn(this):
self.validate(**kwargs)
else:
def fn(this):
with this.assertRaises(jsonschema.ValidationError):
self.validate(**kwargs)
fn.__name__ = "_".join(
[
"test",
_DELIMITERS.sub("_", self.subject),
_DELIMITERS.sub("_", self.case_description),
_DELIMITERS.sub("_", self.description),
],
)
reason = skip(self)
if reason is None or os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0":
return fn
elif os.environ.get("JSON_SCHEMA_EXPECTED_FAILURES", "0") != "0": # pragma: no cover # noqa: E501
return unittest.expectedFailure(fn)
else:
return unittest.skip(reason)(fn)
def validate(self, Validator, **kwargs):
Validator.check_schema(self.schema)
validator = Validator(
schema=self.schema,
registry=self._remotes,
**kwargs,
)
if os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": # pragma: no cover
breakpoint() # noqa: T100
validator.validate(instance=self.data)
def validate_ignoring_errors(self, Validator): # pragma: no cover
with suppress(jsonschema.ValidationError):
self.validate(Validator=Validator)
def _someone_save_us_the_module_of_the_caller():
"""
The FQON of the module 2nd stack frames up from here.
This is intended to allow us to dynamically return test case classes that
are indistinguishable from being defined in the module that wants them.
Otherwise, trial will mis-print the FQON, and copy pasting it won't re-run
the class that really is running.
Save us all, this is all so so so so so terrible.
"""
return sys._getframe(2).f_globals["__name__"]

View File

@@ -0,0 +1,50 @@
"""
Fuzzing setup for OSS-Fuzz.
See https://github.com/google/oss-fuzz/tree/master/projects/jsonschema for the
other half of the setup here.
"""
import sys
from hypothesis import given, strategies
import jsonschema
PRIM = strategies.one_of(
strategies.booleans(),
strategies.integers(),
strategies.floats(allow_nan=False, allow_infinity=False),
strategies.text(),
)
DICT = strategies.recursive(
base=strategies.one_of(
strategies.booleans(),
strategies.dictionaries(strategies.text(), PRIM),
),
extend=lambda inner: strategies.dictionaries(strategies.text(), inner),
)
@given(obj1=DICT, obj2=DICT)
def test_schemas(obj1, obj2):
try:
jsonschema.validate(instance=obj1, schema=obj2)
except jsonschema.exceptions.ValidationError:
pass
except jsonschema.exceptions.SchemaError:
pass
def main():
atheris.instrument_all()
atheris.Setup(
sys.argv,
test_schemas.hypothesis.fuzz_one_input,
enable_python_coverage=True,
)
atheris.Fuzz()
if __name__ == "__main__":
import atheris
main()

View File

@@ -0,0 +1,904 @@
from contextlib import redirect_stderr, redirect_stdout
from importlib import metadata
from io import StringIO
from json import JSONDecodeError
from pathlib import Path
from textwrap import dedent
from unittest import TestCase
import json
import os
import subprocess
import sys
import tempfile
import warnings
from jsonschema import Draft4Validator, Draft202012Validator
from jsonschema.exceptions import (
SchemaError,
ValidationError,
_RefResolutionError,
)
from jsonschema.validators import _LATEST_VERSION, validate
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from jsonschema import cli
def fake_validator(*errors):
errors = list(reversed(errors))
class FakeValidator:
def __init__(self, *args, **kwargs):
pass
def iter_errors(self, instance):
if errors:
return errors.pop()
return [] # pragma: no cover
@classmethod
def check_schema(self, schema):
pass
return FakeValidator
def fake_open(all_contents):
def open(path):
contents = all_contents.get(path)
if contents is None:
raise FileNotFoundError(path)
return StringIO(contents)
return open
def _message_for(non_json):
try:
json.loads(non_json)
except JSONDecodeError as error:
return str(error)
else: # pragma: no cover
raise RuntimeError("Tried and failed to capture a JSON dump error.")
class TestCLI(TestCase):
def run_cli(
self, argv, files=None, stdin=StringIO(), exit_code=0, **override,
):
arguments = cli.parse_args(argv)
arguments.update(override)
self.assertFalse(hasattr(cli, "open"))
cli.open = fake_open(files or {})
try:
stdout, stderr = StringIO(), StringIO()
actual_exit_code = cli.run(
arguments,
stdin=stdin,
stdout=stdout,
stderr=stderr,
)
finally:
del cli.open
self.assertEqual(
actual_exit_code, exit_code, msg=dedent(
f"""
Expected an exit code of {exit_code} != {actual_exit_code}.
stdout: {stdout.getvalue()}
stderr: {stderr.getvalue()}
""",
),
)
return stdout.getvalue(), stderr.getvalue()
def assertOutputs(self, stdout="", stderr="", **kwargs):
self.assertEqual(
self.run_cli(**kwargs),
(dedent(stdout), dedent(stderr)),
)
def test_invalid_instance(self):
error = ValidationError("I am an error!", instance=12)
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_instance=json.dumps(error.instance),
),
validator=fake_validator([error]),
argv=["-i", "some_instance", "some_schema"],
exit_code=1,
stderr="12: I am an error!\n",
)
def test_invalid_instance_pretty_output(self):
error = ValidationError("I am an error!", instance=12)
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_instance=json.dumps(error.instance),
),
validator=fake_validator([error]),
argv=["-i", "some_instance", "--output", "pretty", "some_schema"],
exit_code=1,
stderr="""\
===[ValidationError]===(some_instance)===
I am an error!
-----------------------------
""",
)
def test_invalid_instance_explicit_plain_output(self):
error = ValidationError("I am an error!", instance=12)
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_instance=json.dumps(error.instance),
),
validator=fake_validator([error]),
argv=["--output", "plain", "-i", "some_instance", "some_schema"],
exit_code=1,
stderr="12: I am an error!\n",
)
def test_invalid_instance_multiple_errors(self):
instance = 12
first = ValidationError("First error", instance=instance)
second = ValidationError("Second error", instance=instance)
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_instance=json.dumps(instance),
),
validator=fake_validator([first, second]),
argv=["-i", "some_instance", "some_schema"],
exit_code=1,
stderr="""\
12: First error
12: Second error
""",
)
def test_invalid_instance_multiple_errors_pretty_output(self):
instance = 12
first = ValidationError("First error", instance=instance)
second = ValidationError("Second error", instance=instance)
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_instance=json.dumps(instance),
),
validator=fake_validator([first, second]),
argv=["-i", "some_instance", "--output", "pretty", "some_schema"],
exit_code=1,
stderr="""\
===[ValidationError]===(some_instance)===
First error
-----------------------------
===[ValidationError]===(some_instance)===
Second error
-----------------------------
""",
)
def test_multiple_invalid_instances(self):
first_instance = 12
first_errors = [
ValidationError("An error", instance=first_instance),
ValidationError("Another error", instance=first_instance),
]
second_instance = "foo"
second_errors = [ValidationError("BOOM", instance=second_instance)]
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_first_instance=json.dumps(first_instance),
some_second_instance=json.dumps(second_instance),
),
validator=fake_validator(first_errors, second_errors),
argv=[
"-i", "some_first_instance",
"-i", "some_second_instance",
"some_schema",
],
exit_code=1,
stderr="""\
12: An error
12: Another error
foo: BOOM
""",
)
def test_multiple_invalid_instances_pretty_output(self):
first_instance = 12
first_errors = [
ValidationError("An error", instance=first_instance),
ValidationError("Another error", instance=first_instance),
]
second_instance = "foo"
second_errors = [ValidationError("BOOM", instance=second_instance)]
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_first_instance=json.dumps(first_instance),
some_second_instance=json.dumps(second_instance),
),
validator=fake_validator(first_errors, second_errors),
argv=[
"--output", "pretty",
"-i", "some_first_instance",
"-i", "some_second_instance",
"some_schema",
],
exit_code=1,
stderr="""\
===[ValidationError]===(some_first_instance)===
An error
-----------------------------
===[ValidationError]===(some_first_instance)===
Another error
-----------------------------
===[ValidationError]===(some_second_instance)===
BOOM
-----------------------------
""",
)
def test_custom_error_format(self):
first_instance = 12
first_errors = [
ValidationError("An error", instance=first_instance),
ValidationError("Another error", instance=first_instance),
]
second_instance = "foo"
second_errors = [ValidationError("BOOM", instance=second_instance)]
self.assertOutputs(
files=dict(
some_schema='{"does not": "matter since it is stubbed"}',
some_first_instance=json.dumps(first_instance),
some_second_instance=json.dumps(second_instance),
),
validator=fake_validator(first_errors, second_errors),
argv=[
"--error-format", ":{error.message}._-_.{error.instance}:",
"-i", "some_first_instance",
"-i", "some_second_instance",
"some_schema",
],
exit_code=1,
stderr=":An error._-_.12::Another error._-_.12::BOOM._-_.foo:",
)
def test_invalid_schema(self):
self.assertOutputs(
files=dict(some_schema='{"type": 12}'),
argv=["some_schema"],
exit_code=1,
stderr="""\
12: 12 is not valid under any of the given schemas
""",
)
def test_invalid_schema_pretty_output(self):
schema = {"type": 12}
with self.assertRaises(SchemaError) as e:
validate(schema=schema, instance="")
error = str(e.exception)
self.assertOutputs(
files=dict(some_schema=json.dumps(schema)),
argv=["--output", "pretty", "some_schema"],
exit_code=1,
stderr=(
"===[SchemaError]===(some_schema)===\n\n"
+ str(error)
+ "\n-----------------------------\n"
),
)
def test_invalid_schema_multiple_errors(self):
self.assertOutputs(
files=dict(some_schema='{"type": 12, "items": 57}'),
argv=["some_schema"],
exit_code=1,
stderr="""\
57: 57 is not of type 'object', 'boolean'
""",
)
def test_invalid_schema_multiple_errors_pretty_output(self):
schema = {"type": 12, "items": 57}
with self.assertRaises(SchemaError) as e:
validate(schema=schema, instance="")
error = str(e.exception)
self.assertOutputs(
files=dict(some_schema=json.dumps(schema)),
argv=["--output", "pretty", "some_schema"],
exit_code=1,
stderr=(
"===[SchemaError]===(some_schema)===\n\n"
+ str(error)
+ "\n-----------------------------\n"
),
)
def test_invalid_schema_with_invalid_instance(self):
"""
"Validating" an instance that's invalid under an invalid schema
just shows the schema error.
"""
self.assertOutputs(
files=dict(
some_schema='{"type": 12, "minimum": 30}',
some_instance="13",
),
argv=["-i", "some_instance", "some_schema"],
exit_code=1,
stderr="""\
12: 12 is not valid under any of the given schemas
""",
)
def test_invalid_schema_with_invalid_instance_pretty_output(self):
instance, schema = 13, {"type": 12, "minimum": 30}
with self.assertRaises(SchemaError) as e:
validate(schema=schema, instance=instance)
error = str(e.exception)
self.assertOutputs(
files=dict(
some_schema=json.dumps(schema),
some_instance=json.dumps(instance),
),
argv=["--output", "pretty", "-i", "some_instance", "some_schema"],
exit_code=1,
stderr=(
"===[SchemaError]===(some_schema)===\n\n"
+ str(error)
+ "\n-----------------------------\n"
),
)
def test_invalid_instance_continues_with_the_rest(self):
self.assertOutputs(
files=dict(
some_schema='{"minimum": 30}',
first_instance="not valid JSON!",
second_instance="12",
),
argv=[
"-i", "first_instance",
"-i", "second_instance",
"some_schema",
],
exit_code=1,
stderr="""\
Failed to parse 'first_instance': {}
12: 12 is less than the minimum of 30
""".format(_message_for("not valid JSON!")),
)
def test_custom_error_format_applies_to_schema_errors(self):
instance, schema = 13, {"type": 12, "minimum": 30}
with self.assertRaises(SchemaError):
validate(schema=schema, instance=instance)
self.assertOutputs(
files=dict(some_schema=json.dumps(schema)),
argv=[
"--error-format", ":{error.message}._-_.{error.instance}:",
"some_schema",
],
exit_code=1,
stderr=":12 is not valid under any of the given schemas._-_.12:",
)
def test_instance_is_invalid_JSON(self):
instance = "not valid JSON!"
self.assertOutputs(
files=dict(some_schema="{}", some_instance=instance),
argv=["-i", "some_instance", "some_schema"],
exit_code=1,
stderr=f"""\
Failed to parse 'some_instance': {_message_for(instance)}
""",
)
def test_instance_is_invalid_JSON_pretty_output(self):
stdout, stderr = self.run_cli(
files=dict(
some_schema="{}",
some_instance="not valid JSON!",
),
argv=["--output", "pretty", "-i", "some_instance", "some_schema"],
exit_code=1,
)
self.assertFalse(stdout)
self.assertIn(
"(some_instance)===\n\nTraceback (most recent call last):\n",
stderr,
)
self.assertNotIn("some_schema", stderr)
def test_instance_is_invalid_JSON_on_stdin(self):
instance = "not valid JSON!"
self.assertOutputs(
files=dict(some_schema="{}"),
stdin=StringIO(instance),
argv=["some_schema"],
exit_code=1,
stderr=f"""\
Failed to parse <stdin>: {_message_for(instance)}
""",
)
def test_instance_is_invalid_JSON_on_stdin_pretty_output(self):
stdout, stderr = self.run_cli(
files=dict(some_schema="{}"),
stdin=StringIO("not valid JSON!"),
argv=["--output", "pretty", "some_schema"],
exit_code=1,
)
self.assertFalse(stdout)
self.assertIn(
"(<stdin>)===\n\nTraceback (most recent call last):\n",
stderr,
)
self.assertNotIn("some_schema", stderr)
def test_schema_is_invalid_JSON(self):
schema = "not valid JSON!"
self.assertOutputs(
files=dict(some_schema=schema),
argv=["some_schema"],
exit_code=1,
stderr=f"""\
Failed to parse 'some_schema': {_message_for(schema)}
""",
)
def test_schema_is_invalid_JSON_pretty_output(self):
stdout, stderr = self.run_cli(
files=dict(some_schema="not valid JSON!"),
argv=["--output", "pretty", "some_schema"],
exit_code=1,
)
self.assertFalse(stdout)
self.assertIn(
"(some_schema)===\n\nTraceback (most recent call last):\n",
stderr,
)
def test_schema_and_instance_are_both_invalid_JSON(self):
"""
Only the schema error is reported, as we abort immediately.
"""
schema, instance = "not valid JSON!", "also not valid JSON!"
self.assertOutputs(
files=dict(some_schema=schema, some_instance=instance),
argv=["some_schema"],
exit_code=1,
stderr=f"""\
Failed to parse 'some_schema': {_message_for(schema)}
""",
)
def test_schema_and_instance_are_both_invalid_JSON_pretty_output(self):
"""
Only the schema error is reported, as we abort immediately.
"""
stdout, stderr = self.run_cli(
files=dict(
some_schema="not valid JSON!",
some_instance="also not valid JSON!",
),
argv=["--output", "pretty", "-i", "some_instance", "some_schema"],
exit_code=1,
)
self.assertFalse(stdout)
self.assertIn(
"(some_schema)===\n\nTraceback (most recent call last):\n",
stderr,
)
self.assertNotIn("some_instance", stderr)
def test_instance_does_not_exist(self):
self.assertOutputs(
files=dict(some_schema="{}"),
argv=["-i", "nonexisting_instance", "some_schema"],
exit_code=1,
stderr="""\
'nonexisting_instance' does not exist.
""",
)
def test_instance_does_not_exist_pretty_output(self):
self.assertOutputs(
files=dict(some_schema="{}"),
argv=[
"--output", "pretty",
"-i", "nonexisting_instance",
"some_schema",
],
exit_code=1,
stderr="""\
===[FileNotFoundError]===(nonexisting_instance)===
'nonexisting_instance' does not exist.
-----------------------------
""",
)
def test_schema_does_not_exist(self):
self.assertOutputs(
argv=["nonexisting_schema"],
exit_code=1,
stderr="'nonexisting_schema' does not exist.\n",
)
def test_schema_does_not_exist_pretty_output(self):
self.assertOutputs(
argv=["--output", "pretty", "nonexisting_schema"],
exit_code=1,
stderr="""\
===[FileNotFoundError]===(nonexisting_schema)===
'nonexisting_schema' does not exist.
-----------------------------
""",
)
def test_neither_instance_nor_schema_exist(self):
self.assertOutputs(
argv=["-i", "nonexisting_instance", "nonexisting_schema"],
exit_code=1,
stderr="'nonexisting_schema' does not exist.\n",
)
def test_neither_instance_nor_schema_exist_pretty_output(self):
self.assertOutputs(
argv=[
"--output", "pretty",
"-i", "nonexisting_instance",
"nonexisting_schema",
],
exit_code=1,
stderr="""\
===[FileNotFoundError]===(nonexisting_schema)===
'nonexisting_schema' does not exist.
-----------------------------
""",
)
def test_successful_validation(self):
self.assertOutputs(
files=dict(some_schema="{}", some_instance="{}"),
argv=["-i", "some_instance", "some_schema"],
stdout="",
stderr="",
)
def test_successful_validation_pretty_output(self):
self.assertOutputs(
files=dict(some_schema="{}", some_instance="{}"),
argv=["--output", "pretty", "-i", "some_instance", "some_schema"],
stdout="===[SUCCESS]===(some_instance)===\n",
stderr="",
)
def test_successful_validation_of_stdin(self):
self.assertOutputs(
files=dict(some_schema="{}"),
stdin=StringIO("{}"),
argv=["some_schema"],
stdout="",
stderr="",
)
def test_successful_validation_of_stdin_pretty_output(self):
self.assertOutputs(
files=dict(some_schema="{}"),
stdin=StringIO("{}"),
argv=["--output", "pretty", "some_schema"],
stdout="===[SUCCESS]===(<stdin>)===\n",
stderr="",
)
def test_successful_validation_of_just_the_schema(self):
self.assertOutputs(
files=dict(some_schema="{}", some_instance="{}"),
argv=["-i", "some_instance", "some_schema"],
stdout="",
stderr="",
)
def test_successful_validation_of_just_the_schema_pretty_output(self):
self.assertOutputs(
files=dict(some_schema="{}", some_instance="{}"),
argv=["--output", "pretty", "-i", "some_instance", "some_schema"],
stdout="===[SUCCESS]===(some_instance)===\n",
stderr="",
)
def test_successful_validation_via_explicit_base_uri(self):
ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
ref_schema_file.close()
self.addCleanup(os.remove, ref_schema_file.name)
ref_path = Path(ref_schema_file.name)
ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}')
schema = f'{{"$ref": "{ref_path.name}#/definitions/num"}}'
self.assertOutputs(
files=dict(some_schema=schema, some_instance="1"),
argv=[
"-i", "some_instance",
"--base-uri", ref_path.parent.as_uri() + "/",
"some_schema",
],
stdout="",
stderr="",
)
def test_unsuccessful_validation_via_explicit_base_uri(self):
ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
ref_schema_file.close()
self.addCleanup(os.remove, ref_schema_file.name)
ref_path = Path(ref_schema_file.name)
ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}')
schema = f'{{"$ref": "{ref_path.name}#/definitions/num"}}'
self.assertOutputs(
files=dict(some_schema=schema, some_instance='"1"'),
argv=[
"-i", "some_instance",
"--base-uri", ref_path.parent.as_uri() + "/",
"some_schema",
],
exit_code=1,
stdout="",
stderr="1: '1' is not of type 'integer'\n",
)
def test_nonexistent_file_with_explicit_base_uri(self):
schema = '{"$ref": "someNonexistentFile.json#definitions/num"}'
instance = "1"
with self.assertRaises(_RefResolutionError) as e:
self.assertOutputs(
files=dict(
some_schema=schema,
some_instance=instance,
),
argv=[
"-i", "some_instance",
"--base-uri", Path.cwd().as_uri(),
"some_schema",
],
)
error = str(e.exception)
self.assertIn(f"{os.sep}someNonexistentFile.json'", error)
def test_invalid_explicit_base_uri(self):
schema = '{"$ref": "foo.json#definitions/num"}'
instance = "1"
with self.assertRaises(_RefResolutionError) as e:
self.assertOutputs(
files=dict(
some_schema=schema,
some_instance=instance,
),
argv=[
"-i", "some_instance",
"--base-uri", "not@UR1",
"some_schema",
],
)
error = str(e.exception)
self.assertEqual(
error, "unknown url type: 'foo.json'",
)
def test_it_validates_using_the_latest_validator_when_unspecified(self):
# There isn't a better way now I can think of to ensure that the
# latest version was used, given that the call to validator_for
# 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(Draft202012Validator, _LATEST_VERSION)
self.assertOutputs(
files=dict(some_schema='{"const": "check"}', some_instance='"a"'),
argv=["-i", "some_instance", "some_schema"],
exit_code=1,
stdout="",
stderr="a: 'check' was expected\n",
)
def test_it_validates_using_draft7_when_specified(self):
"""
Specifically, `const` validation applies for Draft 7.
"""
schema = """
{
"$schema": "http://json-schema.org/draft-07/schema#",
"const": "check"
}
"""
instance = '"foo"'
self.assertOutputs(
files=dict(some_schema=schema, some_instance=instance),
argv=["-i", "some_instance", "some_schema"],
exit_code=1,
stdout="",
stderr="foo: 'check' was expected\n",
)
def test_it_validates_using_draft4_when_specified(self):
"""
Specifically, `const` validation *does not* apply for Draft 4.
"""
schema = """
{
"$schema": "http://json-schema.org/draft-04/schema#",
"const": "check"
}
"""
instance = '"foo"'
self.assertOutputs(
files=dict(some_schema=schema, some_instance=instance),
argv=["-i", "some_instance", "some_schema"],
stdout="",
stderr="",
)
class TestParser(TestCase):
FakeValidator = fake_validator()
def test_find_validator_by_fully_qualified_object_name(self):
arguments = cli.parse_args(
[
"--validator",
"jsonschema.tests.test_cli.TestParser.FakeValidator",
"--instance", "mem://some/instance",
"mem://some/schema",
],
)
self.assertIs(arguments["validator"], self.FakeValidator)
def test_find_validator_in_jsonschema(self):
arguments = cli.parse_args(
[
"--validator", "Draft4Validator",
"--instance", "mem://some/instance",
"mem://some/schema",
],
)
self.assertIs(arguments["validator"], Draft4Validator)
def cli_output_for(self, *argv):
stdout, stderr = StringIO(), StringIO()
with redirect_stdout(stdout), redirect_stderr(stderr): # noqa: SIM117
with self.assertRaises(SystemExit):
cli.parse_args(argv)
return stdout.getvalue(), stderr.getvalue()
def test_unknown_output(self):
stdout, stderr = self.cli_output_for(
"--output", "foo",
"mem://some/schema",
)
self.assertIn("invalid choice: 'foo'", stderr)
self.assertFalse(stdout)
def test_useless_error_format(self):
stdout, stderr = self.cli_output_for(
"--output", "pretty",
"--error-format", "foo",
"mem://some/schema",
)
self.assertIn(
"--error-format can only be used with --output plain",
stderr,
)
self.assertFalse(stdout)
class TestCLIIntegration(TestCase):
def test_license(self):
our_metadata = metadata.metadata("jsonschema")
self.assertEqual(our_metadata.get("License-Expression"), "MIT")
def test_version(self):
version = subprocess.check_output(
[sys.executable, "-W", "ignore", "-m", "jsonschema", "--version"],
stderr=subprocess.STDOUT,
)
version = version.decode("utf-8").strip()
self.assertEqual(version, metadata.version("jsonschema"))
def test_no_arguments_shows_usage_notes(self):
output = subprocess.check_output(
[sys.executable, "-m", "jsonschema"],
stderr=subprocess.STDOUT,
)
output_for_help = subprocess.check_output(
[sys.executable, "-m", "jsonschema", "--help"],
stderr=subprocess.STDOUT,
)
self.assertEqual(output, output_for_help)

View File

@@ -0,0 +1,432 @@
from contextlib import contextmanager
from io import BytesIO
from unittest import TestCase, mock
import importlib.metadata
import json
import subprocess
import sys
import urllib.request
import referencing.exceptions
from jsonschema import FormatChecker, exceptions, protocols, validators
class TestDeprecations(TestCase):
def test_version(self):
"""
As of v4.0.0, __version__ is deprecated in favor of importlib.metadata.
"""
message = "Accessing jsonschema.__version__ is deprecated"
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import __version__
self.assertEqual(__version__, importlib.metadata.version("jsonschema"))
self.assertEqual(w.filename, __file__)
def test_validators_ErrorTree(self):
"""
As of v4.0.0, importing ErrorTree from jsonschema.validators is
deprecated in favor of doing so from jsonschema.exceptions.
"""
message = "Importing ErrorTree from jsonschema.validators is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema.validators import ErrorTree
self.assertEqual(ErrorTree, exceptions.ErrorTree)
self.assertEqual(w.filename, __file__)
def test_import_ErrorTree(self):
"""
As of v4.18.0, importing ErrorTree from the package root is
deprecated in favor of doing so from jsonschema.exceptions.
"""
message = "Importing ErrorTree directly from the jsonschema package "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import ErrorTree
self.assertEqual(ErrorTree, exceptions.ErrorTree)
self.assertEqual(w.filename, __file__)
def test_ErrorTree_setitem(self):
"""
As of v4.20.0, setting items on an ErrorTree is deprecated.
"""
e = exceptions.ValidationError("some error", path=["foo"])
tree = exceptions.ErrorTree()
subtree = exceptions.ErrorTree(errors=[e])
message = "ErrorTree.__setitem__ is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
tree["foo"] = subtree
self.assertEqual(tree["foo"], subtree)
self.assertEqual(w.filename, __file__)
def test_import_FormatError(self):
"""
As of v4.18.0, importing FormatError from the package root is
deprecated in favor of doing so from jsonschema.exceptions.
"""
message = "Importing FormatError directly from the jsonschema package "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import FormatError
self.assertEqual(FormatError, exceptions.FormatError)
self.assertEqual(w.filename, __file__)
def test_import_Validator(self):
"""
As of v4.19.0, importing Validator from the package root is
deprecated in favor of doing so from jsonschema.protocols.
"""
message = "Importing Validator directly from the jsonschema package "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import Validator
self.assertEqual(Validator, protocols.Validator)
self.assertEqual(w.filename, __file__)
def test_validators_validators(self):
"""
As of v4.0.0, accessing jsonschema.validators.validators is
deprecated.
"""
message = "Accessing jsonschema.validators.validators is deprecated"
with self.assertWarnsRegex(DeprecationWarning, message) as w:
value = validators.validators
self.assertEqual(value, validators._VALIDATORS)
self.assertEqual(w.filename, __file__)
def test_validators_meta_schemas(self):
"""
As of v4.0.0, accessing jsonschema.validators.meta_schemas is
deprecated.
"""
message = "Accessing jsonschema.validators.meta_schemas is deprecated"
with self.assertWarnsRegex(DeprecationWarning, message) as w:
value = validators.meta_schemas
self.assertEqual(value, validators._META_SCHEMAS)
self.assertEqual(w.filename, __file__)
def test_RefResolver_in_scope(self):
"""
As of v4.0.0, RefResolver.in_scope is deprecated.
"""
resolver = validators._RefResolver.from_schema({})
message = "jsonschema.RefResolver.in_scope is deprecated "
with self.assertWarnsRegex(DeprecationWarning, message) as w: # noqa: SIM117
with resolver.in_scope("foo"):
pass
self.assertEqual(w.filename, __file__)
def test_Validator_is_valid_two_arguments(self):
"""
As of v4.0.0, calling is_valid with two arguments (to provide a
different schema) is deprecated.
"""
validator = validators.Draft7Validator({})
message = "Passing a schema to Validator.is_valid is deprecated "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
result = validator.is_valid("foo", {"type": "number"})
self.assertFalse(result)
self.assertEqual(w.filename, __file__)
def test_Validator_iter_errors_two_arguments(self):
"""
As of v4.0.0, calling iter_errors with two arguments (to provide a
different schema) is deprecated.
"""
validator = validators.Draft7Validator({})
message = "Passing a schema to Validator.iter_errors is deprecated "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
error, = validator.iter_errors("foo", {"type": "number"})
self.assertEqual(error.validator, "type")
self.assertEqual(w.filename, __file__)
def test_Validator_resolver(self):
"""
As of v4.18.0, accessing Validator.resolver is deprecated.
"""
validator = validators.Draft7Validator({})
message = "Accessing Draft7Validator.resolver is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
self.assertIsInstance(validator.resolver, validators._RefResolver)
self.assertEqual(w.filename, __file__)
def test_RefResolver(self):
"""
As of v4.18.0, RefResolver is fully deprecated.
"""
message = "jsonschema.RefResolver is deprecated"
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import RefResolver
self.assertEqual(w.filename, __file__)
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema.validators import RefResolver # noqa: F401
self.assertEqual(w.filename, __file__)
def test_RefResolutionError(self):
"""
As of v4.18.0, RefResolutionError is deprecated in favor of directly
catching errors from the referencing library.
"""
message = "jsonschema.exceptions.RefResolutionError is deprecated"
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import RefResolutionError
self.assertEqual(RefResolutionError, exceptions._RefResolutionError)
self.assertEqual(w.filename, __file__)
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema.exceptions import RefResolutionError
self.assertEqual(RefResolutionError, exceptions._RefResolutionError)
self.assertEqual(w.filename, __file__)
def test_catching_Unresolvable_directly(self):
"""
This behavior is the intended behavior (i.e. it's not deprecated), but
given we do "tricksy" things in the iterim to wrap exceptions in a
multiple inheritance subclass, we need to be extra sure it works and
stays working.
"""
validator = validators.Draft202012Validator({"$ref": "urn:nothing"})
with self.assertRaises(referencing.exceptions.Unresolvable) as e:
validator.validate(12)
expected = referencing.exceptions.Unresolvable(ref="urn:nothing")
self.assertEqual(
(e.exception, str(e.exception)),
(expected, "Unresolvable: urn:nothing"),
)
def test_catching_Unresolvable_via_RefResolutionError(self):
"""
Until RefResolutionError is removed, it is still possible to catch
exceptions from reference resolution using it, even though they may
have been raised by referencing.
"""
with self.assertWarns(DeprecationWarning):
from jsonschema import RefResolutionError
validator = validators.Draft202012Validator({"$ref": "urn:nothing"})
with self.assertRaises(referencing.exceptions.Unresolvable) as u:
validator.validate(12)
with self.assertRaises(RefResolutionError) as e:
validator.validate(12)
self.assertEqual(
(e.exception, str(e.exception)),
(u.exception, "Unresolvable: urn:nothing"),
)
def test_WrappedReferencingError_hashability(self):
"""
Ensure the wrapped referencing errors are hashable when possible.
"""
with self.assertWarns(DeprecationWarning):
from jsonschema import RefResolutionError
validator = validators.Draft202012Validator({"$ref": "urn:nothing"})
with self.assertRaises(referencing.exceptions.Unresolvable) as u:
validator.validate(12)
with self.assertRaises(RefResolutionError) as e:
validator.validate(12)
self.assertIn(e.exception, {u.exception})
self.assertIn(u.exception, {e.exception})
def test_Validator_subclassing(self):
"""
As of v4.12.0, subclassing a validator class produces an explicit
deprecation warning.
This was never intended to be public API (and some comments over the
years in issues said so, but obviously that's not a great way to make
sure it's followed).
A future version will explicitly raise an error.
"""
message = "Subclassing validator classes is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
class Subclass(validators.Draft202012Validator):
pass
self.assertEqual(w.filename, __file__)
with self.assertWarnsRegex(DeprecationWarning, message) as w:
class AnotherSubclass(validators.create(meta_schema={})):
pass
def test_FormatChecker_cls_checks(self):
"""
As of v4.14.0, FormatChecker.cls_checks is deprecated without
replacement.
"""
self.addCleanup(FormatChecker.checkers.pop, "boom", None)
message = "FormatChecker.cls_checks "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
FormatChecker.cls_checks("boom")
self.assertEqual(w.filename, __file__)
def test_draftN_format_checker(self):
"""
As of v4.16.0, accessing jsonschema.draftn_format_checker is deprecated
in favor of Validator.FORMAT_CHECKER.
"""
message = "Accessing jsonschema.draft202012_format_checker is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import draft202012_format_checker
self.assertIs(
draft202012_format_checker,
validators.Draft202012Validator.FORMAT_CHECKER,
)
self.assertEqual(w.filename, __file__)
message = "Accessing jsonschema.draft201909_format_checker is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import draft201909_format_checker
self.assertIs(
draft201909_format_checker,
validators.Draft201909Validator.FORMAT_CHECKER,
)
self.assertEqual(w.filename, __file__)
message = "Accessing jsonschema.draft7_format_checker is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import draft7_format_checker
self.assertIs(
draft7_format_checker,
validators.Draft7Validator.FORMAT_CHECKER,
)
self.assertEqual(w.filename, __file__)
message = "Accessing jsonschema.draft6_format_checker is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import draft6_format_checker
self.assertIs(
draft6_format_checker,
validators.Draft6Validator.FORMAT_CHECKER,
)
self.assertEqual(w.filename, __file__)
message = "Accessing jsonschema.draft4_format_checker is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import draft4_format_checker
self.assertIs(
draft4_format_checker,
validators.Draft4Validator.FORMAT_CHECKER,
)
self.assertEqual(w.filename, __file__)
message = "Accessing jsonschema.draft3_format_checker is "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
from jsonschema import draft3_format_checker
self.assertIs(
draft3_format_checker,
validators.Draft3Validator.FORMAT_CHECKER,
)
self.assertEqual(w.filename, __file__)
with self.assertRaises(ImportError):
from jsonschema import draft1234_format_checker # noqa: F401
def test_import_cli(self):
"""
As of v4.17.0, importing jsonschema.cli is deprecated.
"""
message = "The jsonschema CLI is deprecated and will be removed "
with self.assertWarnsRegex(DeprecationWarning, message) as w:
import jsonschema.cli
importlib.reload(jsonschema.cli)
self.assertEqual(w.filename, importlib.__file__)
def test_cli(self):
"""
As of v4.17.0, the jsonschema CLI is deprecated.
"""
process = subprocess.run(
[sys.executable, "-m", "jsonschema"],
capture_output=True,
check=True,
)
self.assertIn(b"The jsonschema CLI is deprecated ", process.stderr)
def test_automatic_remote_retrieval(self):
"""
Automatic retrieval of remote references is deprecated as of v4.18.0.
"""
ref = "http://bar#/$defs/baz"
schema = {"$defs": {"baz": {"type": "integer"}}}
if "requests" in sys.modules: # pragma: no cover
self.addCleanup(
sys.modules.__setitem__, "requests", sys.modules["requests"],
)
sys.modules["requests"] = None
@contextmanager
def fake_urlopen(request):
self.assertIsInstance(request, urllib.request.Request)
self.assertEqual(request.full_url, "http://bar")
# Ha ha urllib.request.Request "normalizes" header names and
# Request.get_header does not also normalize them...
(header, value), = request.header_items()
self.assertEqual(header.lower(), "user-agent")
self.assertEqual(
value, "python-jsonschema (deprecated $ref resolution)",
)
yield BytesIO(json.dumps(schema).encode("utf8"))
validator = validators.Draft202012Validator({"$ref": ref})
message = "Automatically retrieving remote references "
patch = mock.patch.object(urllib.request, "urlopen", new=fake_urlopen)
with patch, self.assertWarnsRegex(DeprecationWarning, message):
self.assertEqual(
(validator.is_valid({}), validator.is_valid(37)),
(False, True),
)

View File

@@ -0,0 +1,759 @@
from unittest import TestCase
import textwrap
import jsonpath_ng
from jsonschema import exceptions
from jsonschema.validators import _LATEST_VERSION
class TestBestMatch(TestCase):
def best_match_of(self, instance, schema):
errors = list(_LATEST_VERSION(schema).iter_errors(instance))
msg = f"No errors found for {instance} under {schema!r}!"
self.assertTrue(errors, msg=msg)
best = exceptions.best_match(iter(errors))
reversed_best = exceptions.best_match(reversed(errors))
self.assertEqual(
best._contents(),
reversed_best._contents(),
f"No consistent best match!\nGot: {best}\n\nThen: {reversed_best}",
)
return best
def test_shallower_errors_are_better_matches(self):
schema = {
"properties": {
"foo": {
"minProperties": 2,
"properties": {"bar": {"type": "object"}},
},
},
}
best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema)
self.assertEqual(best.validator, "minProperties")
def test_oneOf_and_anyOf_are_weak_matches(self):
"""
A property you *must* match is probably better than one you have to
match a part of.
"""
schema = {
"minProperties": 2,
"anyOf": [{"type": "string"}, {"type": "number"}],
"oneOf": [{"type": "string"}, {"type": "number"}],
}
best = self.best_match_of(instance={}, schema=schema)
self.assertEqual(best.validator, "minProperties")
def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self):
"""
If the most relevant error is an anyOf, then we traverse its context
and select the otherwise *least* relevant error, since in this case
that means the most specific, deep, error inside the instance.
I.e. since only one of the schemas must match, we look for the most
relevant one.
"""
schema = {
"properties": {
"foo": {
"anyOf": [
{"type": "string"},
{"properties": {"bar": {"type": "array"}}},
],
},
},
}
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
self.assertEqual(best.validator_value, "array")
def test_no_anyOf_traversal_for_equally_relevant_errors(self):
"""
We don't traverse into an anyOf (as above) if all of its context errors
seem to be equally "wrong" against the instance.
"""
schema = {
"anyOf": [
{"type": "string"},
{"type": "integer"},
{"type": "object"},
],
}
best = self.best_match_of(instance=[], schema=schema)
self.assertEqual(best.validator, "anyOf")
def test_anyOf_traversal_for_single_equally_relevant_error(self):
"""
We *do* traverse anyOf with a single nested error, even though it is
vacuously equally relevant to itself.
"""
schema = {
"anyOf": [
{"type": "string"},
],
}
best = self.best_match_of(instance=[], schema=schema)
self.assertEqual(best.validator, "type")
def test_anyOf_traversal_for_single_sibling_errors(self):
"""
We *do* traverse anyOf with a single subschema that fails multiple
times (e.g. on multiple items).
"""
schema = {
"anyOf": [
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")
def test_anyOf_traversal_for_non_type_matching_sibling_errors(self):
"""
We *do* traverse anyOf with multiple subschemas when one does not type
match.
"""
schema = {
"anyOf": [
{"type": "object"},
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")
def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):
"""
If the most relevant error is an oneOf, then we traverse its context
and select the otherwise *least* relevant error, since in this case
that means the most specific, deep, error inside the instance.
I.e. since only one of the schemas must match, we look for the most
relevant one.
"""
schema = {
"properties": {
"foo": {
"oneOf": [
{"type": "string"},
{"properties": {"bar": {"type": "array"}}},
],
},
},
}
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
self.assertEqual(best.validator_value, "array")
def test_no_oneOf_traversal_for_equally_relevant_errors(self):
"""
We don't traverse into an oneOf (as above) if all of its context errors
seem to be equally "wrong" against the instance.
"""
schema = {
"oneOf": [
{"type": "string"},
{"type": "integer"},
{"type": "object"},
],
}
best = self.best_match_of(instance=[], schema=schema)
self.assertEqual(best.validator, "oneOf")
def test_oneOf_traversal_for_single_equally_relevant_error(self):
"""
We *do* traverse oneOf with a single nested error, even though it is
vacuously equally relevant to itself.
"""
schema = {
"oneOf": [
{"type": "string"},
],
}
best = self.best_match_of(instance=[], schema=schema)
self.assertEqual(best.validator, "type")
def test_oneOf_traversal_for_single_sibling_errors(self):
"""
We *do* traverse oneOf with a single subschema that fails multiple
times (e.g. on multiple items).
"""
schema = {
"oneOf": [
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")
def test_oneOf_traversal_for_non_type_matching_sibling_errors(self):
"""
We *do* traverse oneOf with multiple subschemas when one does not type
match.
"""
schema = {
"oneOf": [
{"type": "object"},
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")
def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self):
"""
Now, if the error is allOf, we traverse but select the *most* relevant
error from the context, because all schemas here must match anyways.
"""
schema = {
"properties": {
"foo": {
"allOf": [
{"type": "string"},
{"properties": {"bar": {"type": "array"}}},
],
},
},
}
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
self.assertEqual(best.validator_value, "string")
def test_nested_context_for_oneOf(self):
"""
We traverse into nested contexts (a oneOf containing an error in a
nested oneOf here).
"""
schema = {
"properties": {
"foo": {
"oneOf": [
{"type": "string"},
{
"oneOf": [
{"type": "string"},
{
"properties": {
"bar": {"type": "array"},
},
},
],
},
],
},
},
}
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
self.assertEqual(best.validator_value, "array")
def test_it_prioritizes_matching_types(self):
schema = {
"properties": {
"foo": {
"anyOf": [
{"type": "array", "minItems": 2},
{"type": "string", "minLength": 10},
],
},
},
}
best = self.best_match_of(instance={"foo": "bar"}, schema=schema)
self.assertEqual(best.validator, "minLength")
reordered = {
"properties": {
"foo": {
"anyOf": [
{"type": "string", "minLength": 10},
{"type": "array", "minItems": 2},
],
},
},
}
best = self.best_match_of(instance={"foo": "bar"}, schema=reordered)
self.assertEqual(best.validator, "minLength")
def test_it_prioritizes_matching_union_types(self):
schema = {
"properties": {
"foo": {
"anyOf": [
{"type": ["array", "object"], "minItems": 2},
{"type": ["integer", "string"], "minLength": 10},
],
},
},
}
best = self.best_match_of(instance={"foo": "bar"}, schema=schema)
self.assertEqual(best.validator, "minLength")
reordered = {
"properties": {
"foo": {
"anyOf": [
{"type": "string", "minLength": 10},
{"type": "array", "minItems": 2},
],
},
},
}
best = self.best_match_of(instance={"foo": "bar"}, schema=reordered)
self.assertEqual(best.validator, "minLength")
def test_boolean_schemas(self):
schema = {"properties": {"foo": False}}
best = self.best_match_of(instance={"foo": "bar"}, schema=schema)
self.assertIsNone(best.validator)
def test_one_error(self):
validator = _LATEST_VERSION({"minProperties": 2})
error, = validator.iter_errors({})
self.assertEqual(
exceptions.best_match(validator.iter_errors({})).validator,
"minProperties",
)
def test_no_errors(self):
validator = _LATEST_VERSION({})
self.assertIsNone(exceptions.best_match(validator.iter_errors({})))
class TestByRelevance(TestCase):
def test_short_paths_are_better_matches(self):
shallow = exceptions.ValidationError("Oh no!", path=["baz"])
deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"])
match = max([shallow, deep], key=exceptions.relevance)
self.assertIs(match, shallow)
match = max([deep, shallow], key=exceptions.relevance)
self.assertIs(match, shallow)
def test_global_errors_are_even_better_matches(self):
shallow = exceptions.ValidationError("Oh no!", path=[])
deep = exceptions.ValidationError("Oh yes!", path=["foo"])
errors = sorted([shallow, deep], key=exceptions.relevance)
self.assertEqual(
[list(error.path) for error in errors],
[["foo"], []],
)
errors = sorted([deep, shallow], key=exceptions.relevance)
self.assertEqual(
[list(error.path) for error in errors],
[["foo"], []],
)
def test_weak_keywords_are_lower_priority(self):
weak = exceptions.ValidationError("Oh no!", path=[], validator="a")
normal = exceptions.ValidationError("Oh yes!", path=[], validator="b")
best_match = exceptions.by_relevance(weak="a")
match = max([weak, normal], key=best_match)
self.assertIs(match, normal)
match = max([normal, weak], key=best_match)
self.assertIs(match, normal)
def test_strong_keywords_are_higher_priority(self):
weak = exceptions.ValidationError("Oh no!", path=[], validator="a")
normal = exceptions.ValidationError("Oh yes!", path=[], validator="b")
strong = exceptions.ValidationError("Oh fine!", path=[], validator="c")
best_match = exceptions.by_relevance(weak="a", strong="c")
match = max([weak, normal, strong], key=best_match)
self.assertIs(match, strong)
match = max([strong, normal, weak], key=best_match)
self.assertIs(match, strong)
class TestErrorTree(TestCase):
def test_it_knows_how_many_total_errors_it_contains(self):
# FIXME: #442
errors = [
exceptions.ValidationError("Something", validator=i)
for i in range(8)
]
tree = exceptions.ErrorTree(errors)
self.assertEqual(tree.total_errors, 8)
def test_it_contains_an_item_if_the_item_had_an_error(self):
errors = [exceptions.ValidationError("a message", path=["bar"])]
tree = exceptions.ErrorTree(errors)
self.assertIn("bar", tree)
def test_it_does_not_contain_an_item_if_the_item_had_no_error(self):
errors = [exceptions.ValidationError("a message", path=["bar"])]
tree = exceptions.ErrorTree(errors)
self.assertNotIn("foo", tree)
def test_keywords_that_failed_appear_in_errors_dict(self):
error = exceptions.ValidationError("a message", validator="foo")
tree = exceptions.ErrorTree([error])
self.assertEqual(tree.errors, {"foo": error})
def test_it_creates_a_child_tree_for_each_nested_path(self):
errors = [
exceptions.ValidationError("a bar message", path=["bar"]),
exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]),
]
tree = exceptions.ErrorTree(errors)
self.assertIn(0, tree["bar"])
self.assertNotIn(1, tree["bar"])
def test_children_have_their_errors_dicts_built(self):
e1, e2 = (
exceptions.ValidationError("1", validator="foo", path=["bar", 0]),
exceptions.ValidationError("2", validator="quux", path=["bar", 0]),
)
tree = exceptions.ErrorTree([e1, e2])
self.assertEqual(tree["bar"][0].errors, {"foo": e1, "quux": e2})
def test_multiple_errors_with_instance(self):
e1, e2 = (
exceptions.ValidationError(
"1",
validator="foo",
path=["bar", "bar2"],
instance="i1"),
exceptions.ValidationError(
"2",
validator="quux",
path=["foobar", 2],
instance="i2"),
)
exceptions.ErrorTree([e1, e2])
def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self):
error = exceptions.ValidationError("123", validator="foo", instance=[])
tree = exceptions.ErrorTree([error])
with self.assertRaises(IndexError):
tree[0]
def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self):
"""
If a keyword refers to a path that isn't in the instance, the
tree still properly returns a subtree for that path.
"""
error = exceptions.ValidationError(
"a message", validator="foo", instance={}, path=["foo"],
)
tree = exceptions.ErrorTree([error])
self.assertIsInstance(tree["foo"], exceptions.ErrorTree)
def test_iter(self):
e1, e2 = (
exceptions.ValidationError(
"1",
validator="foo",
path=["bar", "bar2"],
instance="i1"),
exceptions.ValidationError(
"2",
validator="quux",
path=["foobar", 2],
instance="i2"),
)
tree = exceptions.ErrorTree([e1, e2])
self.assertEqual(set(tree), {"bar", "foobar"})
def test_repr_single(self):
error = exceptions.ValidationError(
"1",
validator="foo",
path=["bar", "bar2"],
instance="i1",
)
tree = exceptions.ErrorTree([error])
self.assertEqual(repr(tree), "<ErrorTree (1 total error)>")
def test_repr_multiple(self):
e1, e2 = (
exceptions.ValidationError(
"1",
validator="foo",
path=["bar", "bar2"],
instance="i1"),
exceptions.ValidationError(
"2",
validator="quux",
path=["foobar", 2],
instance="i2"),
)
tree = exceptions.ErrorTree([e1, e2])
self.assertEqual(repr(tree), "<ErrorTree (2 total errors)>")
def test_repr_empty(self):
tree = exceptions.ErrorTree([])
self.assertEqual(repr(tree), "<ErrorTree (0 total errors)>")
class TestErrorInitReprStr(TestCase):
def make_error(self, **kwargs):
defaults = dict(
message="hello",
validator="type",
validator_value="string",
instance=5,
schema={"type": "string"},
)
defaults.update(kwargs)
return exceptions.ValidationError(**defaults)
def assertShows(self, expected, **kwargs):
expected = textwrap.dedent(expected).rstrip("\n")
error = self.make_error(**kwargs)
message_line, _, rest = str(error).partition("\n")
self.assertEqual(message_line, error.message)
self.assertEqual(rest, expected)
def test_it_calls_super_and_sets_args(self):
error = self.make_error()
self.assertGreater(len(error.args), 1)
def test_repr(self):
self.assertEqual(
repr(exceptions.ValidationError(message="Hello!")),
"<ValidationError: 'Hello!'>",
)
def test_unset_error(self):
error = exceptions.ValidationError("message")
self.assertEqual(str(error), "message")
kwargs = {
"validator": "type",
"validator_value": "string",
"instance": 5,
"schema": {"type": "string"},
}
# Just the message should show if any of the attributes are unset
for attr in kwargs:
k = dict(kwargs)
del k[attr]
error = exceptions.ValidationError("message", **k)
self.assertEqual(str(error), "message")
def test_empty_paths(self):
self.assertShows(
"""
Failed validating 'type' in schema:
{'type': 'string'}
On instance:
5
""",
path=[],
schema_path=[],
)
def test_one_item_paths(self):
self.assertShows(
"""
Failed validating 'type' in schema:
{'type': 'string'}
On instance[0]:
5
""",
path=[0],
schema_path=["items"],
)
def test_multiple_item_paths(self):
self.assertShows(
"""
Failed validating 'type' in schema['items'][0]:
{'type': 'string'}
On instance[0]['a']:
5
""",
path=[0, "a"],
schema_path=["items", 0, 1],
)
def test_uses_pprint(self):
self.assertShows(
"""
Failed validating 'maxLength' in schema:
{0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
10: 10,
11: 11,
12: 12,
13: 13,
14: 14,
15: 15,
16: 16,
17: 17,
18: 18,
19: 19}
On instance:
[0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24]
""",
instance=list(range(25)),
schema=dict(zip(range(20), range(20))),
validator="maxLength",
)
def test_does_not_reorder_dicts(self):
self.assertShows(
"""
Failed validating 'type' in schema:
{'do': 3, 'not': 7, 'sort': 37, 'me': 73}
On instance:
{'here': 73, 'too': 37, 'no': 7, 'sorting': 3}
""",
schema={
"do": 3,
"not": 7,
"sort": 37,
"me": 73,
},
instance={
"here": 73,
"too": 37,
"no": 7,
"sorting": 3,
},
)
def test_str_works_with_instances_having_overriden_eq_operator(self):
"""
Check for #164 which rendered exceptions unusable when a
`ValidationError` involved instances with an `__eq__` method
that returned truthy values.
"""
class DontEQMeBro:
def __eq__(this, other): # pragma: no cover
self.fail("Don't!")
def __ne__(this, other): # pragma: no cover
self.fail("Don't!")
instance = DontEQMeBro()
error = exceptions.ValidationError(
"a message",
validator="foo",
instance=instance,
validator_value="some",
schema="schema",
)
self.assertIn(repr(instance), str(error))
class TestHashable(TestCase):
def test_hashable(self):
{exceptions.ValidationError("")}
{exceptions.SchemaError("")}
class TestJsonPathRendering(TestCase):
def validate_json_path_rendering(self, property_name, expected_path):
error = exceptions.ValidationError(
path=[property_name],
message="1",
validator="foo",
instance="i1",
)
rendered_json_path = error.json_path
self.assertEqual(rendered_json_path, expected_path)
re_parsed_name = jsonpath_ng.parse(rendered_json_path).right.fields[0]
self.assertEqual(re_parsed_name, property_name)
def test_basic(self):
self.validate_json_path_rendering("x", "$.x")
def test_empty(self):
self.validate_json_path_rendering("", "$['']")
def test_number(self):
self.validate_json_path_rendering("1", "$['1']")
def test_period(self):
self.validate_json_path_rendering(".", "$['.']")
def test_single_quote(self):
self.validate_json_path_rendering("'", r"$['\'']")
def test_space(self):
self.validate_json_path_rendering(" ", "$[' ']")
def test_backslash(self):
self.validate_json_path_rendering("\\", r"$['\\']")
def test_backslash_single_quote(self):
self.validate_json_path_rendering(r"\'", r"$['\\\'']")
def test_underscore(self):
self.validate_json_path_rendering("_", r"$['_']")
def test_double_quote(self):
self.validate_json_path_rendering('"', """$['"']""")
def test_hyphen(self):
self.validate_json_path_rendering("-", "$['-']")
def test_json_path_injection(self):
self.validate_json_path_rendering("a[0]", "$['a[0]']")
def test_open_bracket(self):
self.validate_json_path_rendering("[", "$['[']")

View File

@@ -0,0 +1,91 @@
"""
Tests for the parts of jsonschema related to the :kw:`format` keyword.
"""
from unittest import TestCase
from jsonschema import FormatChecker, ValidationError
from jsonschema.exceptions import FormatError
from jsonschema.validators import Draft4Validator
BOOM = ValueError("Boom!")
BANG = ZeroDivisionError("Bang!")
def boom(thing):
if thing == "bang":
raise BANG
raise BOOM
class TestFormatChecker(TestCase):
def test_it_can_validate_no_formats(self):
checker = FormatChecker(formats=())
self.assertFalse(checker.checkers)
def test_it_raises_a_key_error_for_unknown_formats(self):
with self.assertRaises(KeyError):
FormatChecker(formats=["o noes"])
def test_it_can_register_cls_checkers(self):
original = dict(FormatChecker.checkers)
self.addCleanup(FormatChecker.checkers.pop, "boom")
with self.assertWarns(DeprecationWarning):
FormatChecker.cls_checks("boom")(boom)
self.assertEqual(
FormatChecker.checkers,
dict(original, boom=(boom, ())),
)
def test_it_can_register_checkers(self):
checker = FormatChecker()
checker.checks("boom")(boom)
self.assertEqual(
checker.checkers,
dict(FormatChecker.checkers, boom=(boom, ())),
)
def test_it_catches_registered_errors(self):
checker = FormatChecker()
checker.checks("boom", raises=type(BOOM))(boom)
with self.assertRaises(FormatError) as cm:
checker.check(instance=12, format="boom")
self.assertIs(cm.exception.cause, BOOM)
self.assertIs(cm.exception.__cause__, BOOM)
self.assertEqual(str(cm.exception), "12 is not a 'boom'")
# Unregistered errors should not be caught
with self.assertRaises(type(BANG)):
checker.check(instance="bang", format="boom")
def test_format_error_causes_become_validation_error_causes(self):
checker = FormatChecker()
checker.checks("boom", raises=ValueError)(boom)
validator = Draft4Validator({"format": "boom"}, format_checker=checker)
with self.assertRaises(ValidationError) as cm:
validator.validate("BOOM")
self.assertIs(cm.exception.cause, BOOM)
self.assertIs(cm.exception.__cause__, BOOM)
def test_format_checkers_come_with_defaults(self):
# This is bad :/ but relied upon.
# The docs for quite awhile recommended people do things like
# validate(..., format_checker=FormatChecker())
# We should change that, but we can't without deprecation...
checker = FormatChecker()
with self.assertRaises(FormatError):
checker.check(instance="not-an-ipv4", format="ipv4")
def test_repr(self):
checker = FormatChecker(formats=())
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),
"<FormatChecker checkers=['bar', 'baz', 'foo']>",
)

View File

@@ -0,0 +1,262 @@
"""
Test runner for the JSON Schema official test suite
Tests comprehensive correctness of each draft's validator.
See https://github.com/json-schema-org/JSON-Schema-Test-Suite for details.
"""
from jsonschema.tests._suite import Suite
import jsonschema
SUITE = Suite()
DRAFT3 = SUITE.version(name="draft3")
DRAFT4 = SUITE.version(name="draft4")
DRAFT6 = SUITE.version(name="draft6")
DRAFT7 = SUITE.version(name="draft7")
DRAFT201909 = SUITE.version(name="draft2019-09")
DRAFT202012 = SUITE.version(name="draft2020-12")
def skip(message, **kwargs):
def skipper(test):
if all(value == getattr(test, attr) for attr, value in kwargs.items()):
return message
return skipper
def ecmascript_regex(test):
if test.subject == "ecmascript-regex":
return "ECMA regex support will be added in #1142."
def missing_format(Validator):
def missing_format(test): # pragma: no cover
schema = test.schema
if (
schema is True
or schema is False
or "format" not in schema
or schema["format"] in Validator.FORMAT_CHECKER.checkers
or test.valid
):
return
return f"Format checker {schema['format']!r} not found."
return missing_format
def complex_email_validation(test):
if test.subject != "email":
return
message = "Complex email validation is (intentionally) unsupported."
return skip(
message=message,
description="an invalid domain",
)(test) or skip(
message=message,
description="an invalid IPv4-address-literal",
)(test) or skip(
message=message,
description="dot after local part is not valid",
)(test) or skip(
message=message,
description="dot before local part is not valid",
)(test) or skip(
message=message,
description="two subsequent dots inside local part are not valid",
)(test)
def leap_second(test):
message = "Leap seconds are unsupported."
return skip(
message=message,
subject="time",
description="a valid time string with leap second",
)(test) or skip(
message=message,
subject="time",
description="a valid time string with leap second, Zulu",
)(test) or skip(
message=message,
subject="time",
description="a valid time string with leap second with offset",
)(test) or skip(
message=message,
subject="time",
description="valid leap second, positive time-offset",
)(test) or skip(
message=message,
subject="time",
description="valid leap second, negative time-offset",
)(test) or skip(
message=message,
subject="time",
description="valid leap second, large positive time-offset",
)(test) or skip(
message=message,
subject="time",
description="valid leap second, large negative time-offset",
)(test) or skip(
message=message,
subject="time",
description="valid leap second, zero time-offset",
)(test) or skip(
message=message,
subject="date-time",
description="a valid date-time with a leap second, UTC",
)(test) or skip(
message=message,
subject="date-time",
description="a valid date-time with a leap second, with minus offset",
)(test)
TestDraft3 = DRAFT3.to_unittest_testcase(
DRAFT3.cases(),
DRAFT3.format_cases(),
DRAFT3.optional_cases_of(name="bignum"),
DRAFT3.optional_cases_of(name="non-bmp-regex"),
DRAFT3.optional_cases_of(name="zeroTerminatedFloats"),
Validator=jsonschema.Draft3Validator,
format_checker=jsonschema.Draft3Validator.FORMAT_CHECKER,
skip=lambda test: (
ecmascript_regex(test)
or missing_format(jsonschema.Draft3Validator)(test)
or complex_email_validation(test)
),
)
TestDraft4 = DRAFT4.to_unittest_testcase(
DRAFT4.cases(),
DRAFT4.format_cases(),
DRAFT4.optional_cases_of(name="bignum"),
DRAFT4.optional_cases_of(name="float-overflow"),
DRAFT4.optional_cases_of(name="id"),
DRAFT4.optional_cases_of(name="non-bmp-regex"),
DRAFT4.optional_cases_of(name="zeroTerminatedFloats"),
Validator=jsonschema.Draft4Validator,
format_checker=jsonschema.Draft4Validator.FORMAT_CHECKER,
skip=lambda test: (
ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft4Validator)(test)
or complex_email_validation(test)
),
)
TestDraft6 = DRAFT6.to_unittest_testcase(
DRAFT6.cases(),
DRAFT6.format_cases(),
DRAFT6.optional_cases_of(name="bignum"),
DRAFT6.optional_cases_of(name="float-overflow"),
DRAFT6.optional_cases_of(name="id"),
DRAFT6.optional_cases_of(name="non-bmp-regex"),
Validator=jsonschema.Draft6Validator,
format_checker=jsonschema.Draft6Validator.FORMAT_CHECKER,
skip=lambda test: (
ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft6Validator)(test)
or complex_email_validation(test)
),
)
TestDraft7 = DRAFT7.to_unittest_testcase(
DRAFT7.cases(),
DRAFT7.format_cases(),
DRAFT7.optional_cases_of(name="bignum"),
DRAFT7.optional_cases_of(name="cross-draft"),
DRAFT7.optional_cases_of(name="float-overflow"),
DRAFT6.optional_cases_of(name="id"),
DRAFT7.optional_cases_of(name="non-bmp-regex"),
DRAFT7.optional_cases_of(name="unknownKeyword"),
Validator=jsonschema.Draft7Validator,
format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER,
skip=lambda test: (
ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft7Validator)(test)
or complex_email_validation(test)
),
)
TestDraft201909 = DRAFT201909.to_unittest_testcase(
DRAFT201909.cases(),
DRAFT201909.optional_cases_of(name="anchor"),
DRAFT201909.optional_cases_of(name="bignum"),
DRAFT201909.optional_cases_of(name="cross-draft"),
DRAFT201909.optional_cases_of(name="float-overflow"),
DRAFT201909.optional_cases_of(name="id"),
DRAFT201909.optional_cases_of(name="no-schema"),
DRAFT201909.optional_cases_of(name="non-bmp-regex"),
DRAFT201909.optional_cases_of(name="refOfUnknownKeyword"),
DRAFT201909.optional_cases_of(name="unknownKeyword"),
Validator=jsonschema.Draft201909Validator,
skip=skip(
message="Vocabulary support is still in-progress.",
subject="vocabulary",
description=(
"no validation: invalid number, but it still validates"
),
),
)
TestDraft201909Format = DRAFT201909.to_unittest_testcase(
DRAFT201909.format_cases(),
name="TestDraft201909Format",
Validator=jsonschema.Draft201909Validator,
format_checker=jsonschema.Draft201909Validator.FORMAT_CHECKER,
skip=lambda test: (
complex_email_validation(test)
or ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft201909Validator)(test)
or complex_email_validation(test)
),
)
TestDraft202012 = DRAFT202012.to_unittest_testcase(
DRAFT202012.cases(),
DRAFT201909.optional_cases_of(name="anchor"),
DRAFT202012.optional_cases_of(name="bignum"),
DRAFT202012.optional_cases_of(name="cross-draft"),
DRAFT202012.optional_cases_of(name="float-overflow"),
DRAFT202012.optional_cases_of(name="id"),
DRAFT202012.optional_cases_of(name="no-schema"),
DRAFT202012.optional_cases_of(name="non-bmp-regex"),
DRAFT202012.optional_cases_of(name="refOfUnknownKeyword"),
DRAFT202012.optional_cases_of(name="unknownKeyword"),
Validator=jsonschema.Draft202012Validator,
skip=skip(
message="Vocabulary support is still in-progress.",
subject="vocabulary",
description=(
"no validation: invalid number, but it still validates"
),
),
)
TestDraft202012Format = DRAFT202012.to_unittest_testcase(
DRAFT202012.format_cases(),
name="TestDraft202012Format",
Validator=jsonschema.Draft202012Validator,
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
skip=lambda test: (
complex_email_validation(test)
or ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft202012Validator)(test)
or complex_email_validation(test)
),
)

View File

@@ -0,0 +1,221 @@
"""
Tests for the `TypeChecker`-based type interface.
The actual correctness of the type checking is handled in
`test_jsonschema_test_suite`; these tests check that TypeChecker
functions correctly at a more granular level.
"""
from collections import namedtuple
from unittest import TestCase
from jsonschema import ValidationError, _keywords
from jsonschema._types import TypeChecker
from jsonschema.exceptions import UndefinedTypeCheck, UnknownType
from jsonschema.validators import Draft202012Validator, extend
def equals_2(checker, instance):
return instance == 2
def is_namedtuple(instance):
return isinstance(instance, tuple) and getattr(instance, "_fields", None)
def is_object_or_named_tuple(checker, instance):
if Draft202012Validator.TYPE_CHECKER.is_type(instance, "object"):
return True
return is_namedtuple(instance)
class TestTypeChecker(TestCase):
def test_is_type(self):
checker = TypeChecker({"two": equals_2})
self.assertEqual(
(
checker.is_type(instance=2, type="two"),
checker.is_type(instance="bar", type="two"),
),
(True, False),
)
def test_is_unknown_type(self):
with self.assertRaises(UndefinedTypeCheck) as e:
TypeChecker().is_type(4, "foobar")
self.assertIn(
"'foobar' is unknown to this type checker",
str(e.exception),
)
self.assertTrue(
e.exception.__suppress_context__,
msg="Expected the internal KeyError to be hidden.",
)
def test_checks_can_be_added_at_init(self):
checker = TypeChecker({"two": equals_2})
self.assertEqual(checker, TypeChecker().redefine("two", equals_2))
def test_redefine_existing_type(self):
self.assertEqual(
TypeChecker().redefine("two", object()).redefine("two", equals_2),
TypeChecker().redefine("two", equals_2),
)
def test_remove(self):
self.assertEqual(
TypeChecker({"two": equals_2}).remove("two"),
TypeChecker(),
)
def test_remove_unknown_type(self):
with self.assertRaises(UndefinedTypeCheck) as context:
TypeChecker().remove("foobar")
self.assertIn("foobar", str(context.exception))
def test_redefine_many(self):
self.assertEqual(
TypeChecker().redefine_many({"foo": int, "bar": str}),
TypeChecker().redefine("foo", int).redefine("bar", str),
)
def test_remove_multiple(self):
self.assertEqual(
TypeChecker({"foo": int, "bar": str}).remove("foo", "bar"),
TypeChecker(),
)
def test_type_check_can_raise_key_error(self):
"""
Make sure no one writes:
try:
self._type_checkers[type](...)
except KeyError:
ignoring the fact that the function itself can raise that.
"""
error = KeyError("Stuff")
def raises_keyerror(checker, instance):
raise error
with self.assertRaises(KeyError) as context:
TypeChecker({"foo": raises_keyerror}).is_type(4, "foo")
self.assertIs(context.exception, error)
def test_repr(self):
checker = TypeChecker({"foo": is_namedtuple, "bar": is_namedtuple})
self.assertEqual(repr(checker), "<TypeChecker types={'bar', 'foo'}>")
class TestCustomTypes(TestCase):
def test_simple_type_can_be_extended(self):
def int_or_str_int(checker, instance):
if not isinstance(instance, (int, str)):
return False
try:
int(instance)
except ValueError:
return False
return True
CustomValidator = extend(
Draft202012Validator,
type_checker=Draft202012Validator.TYPE_CHECKER.redefine(
"integer", int_or_str_int,
),
)
validator = CustomValidator({"type": "integer"})
validator.validate(4)
validator.validate("4")
with self.assertRaises(ValidationError):
validator.validate(4.4)
with self.assertRaises(ValidationError):
validator.validate("foo")
def test_object_can_be_extended(self):
schema = {"type": "object"}
Point = namedtuple("Point", ["x", "y"])
type_checker = Draft202012Validator.TYPE_CHECKER.redefine(
"object", is_object_or_named_tuple,
)
CustomValidator = extend(
Draft202012Validator,
type_checker=type_checker,
)
validator = CustomValidator(schema)
validator.validate(Point(x=4, y=5))
def test_object_extensions_require_custom_validators(self):
schema = {"type": "object", "required": ["x"]}
type_checker = Draft202012Validator.TYPE_CHECKER.redefine(
"object", is_object_or_named_tuple,
)
CustomValidator = extend(
Draft202012Validator,
type_checker=type_checker,
)
validator = CustomValidator(schema)
Point = namedtuple("Point", ["x", "y"])
# Cannot handle required
with self.assertRaises(ValidationError):
validator.validate(Point(x=4, y=5))
def test_object_extensions_can_handle_custom_validators(self):
schema = {
"type": "object",
"required": ["x"],
"properties": {"x": {"type": "integer"}},
}
type_checker = Draft202012Validator.TYPE_CHECKER.redefine(
"object", is_object_or_named_tuple,
)
def coerce_named_tuple(fn):
def coerced(validator, value, instance, schema):
if is_namedtuple(instance):
instance = instance._asdict()
return fn(validator, value, instance, schema)
return coerced
required = coerce_named_tuple(_keywords.required)
properties = coerce_named_tuple(_keywords.properties)
CustomValidator = extend(
Draft202012Validator,
type_checker=type_checker,
validators={"required": required, "properties": properties},
)
validator = CustomValidator(schema)
Point = namedtuple("Point", ["x", "y"])
# Can now process required and properties
validator.validate(Point(x=4, y=5))
with self.assertRaises(ValidationError):
validator.validate(Point(x="not an integer", y=5))
# As well as still handle objects.
validator.validate({"x": 4, "y": 5})
with self.assertRaises(ValidationError):
validator.validate({"x": "not an integer", "y": 5})
def test_unknown_type(self):
with self.assertRaises(UnknownType) as e:
Draft202012Validator({}).is_type(12, "some unknown type")
self.assertIn("'some unknown type'", str(e.exception))

View File

@@ -0,0 +1,138 @@
from math import nan
from unittest import TestCase
from jsonschema._utils import equal
class TestEqual(TestCase):
def test_none(self):
self.assertTrue(equal(None, None))
def test_nan(self):
self.assertTrue(equal(nan, nan))
class TestDictEqual(TestCase):
def test_equal_dictionaries(self):
dict_1 = {"a": "b", "c": "d"}
dict_2 = {"c": "d", "a": "b"}
self.assertTrue(equal(dict_1, dict_2))
def test_equal_dictionaries_with_nan(self):
dict_1 = {"a": nan, "c": "d"}
dict_2 = {"c": "d", "a": nan}
self.assertTrue(equal(dict_1, dict_2))
def test_missing_key(self):
dict_1 = {"a": "b", "c": "d"}
dict_2 = {"c": "d", "x": "b"}
self.assertFalse(equal(dict_1, dict_2))
def test_additional_key(self):
dict_1 = {"a": "b", "c": "d"}
dict_2 = {"c": "d", "a": "b", "x": "x"}
self.assertFalse(equal(dict_1, dict_2))
def test_missing_value(self):
dict_1 = {"a": "b", "c": "d"}
dict_2 = {"c": "d", "a": "x"}
self.assertFalse(equal(dict_1, dict_2))
def test_empty_dictionaries(self):
dict_1 = {}
dict_2 = {}
self.assertTrue(equal(dict_1, dict_2))
def test_one_none(self):
dict_1 = None
dict_2 = {"a": "b", "c": "d"}
self.assertFalse(equal(dict_1, dict_2))
def test_same_item(self):
dict_1 = {"a": "b", "c": "d"}
self.assertTrue(equal(dict_1, dict_1))
def test_nested_equal(self):
dict_1 = {"a": {"a": "b", "c": "d"}, "c": "d"}
dict_2 = {"c": "d", "a": {"a": "b", "c": "d"}}
self.assertTrue(equal(dict_1, dict_2))
def test_nested_dict_unequal(self):
dict_1 = {"a": {"a": "b", "c": "d"}, "c": "d"}
dict_2 = {"c": "d", "a": {"a": "b", "c": "x"}}
self.assertFalse(equal(dict_1, dict_2))
def test_mixed_nested_equal(self):
dict_1 = {"a": ["a", "b", "c", "d"], "c": "d"}
dict_2 = {"c": "d", "a": ["a", "b", "c", "d"]}
self.assertTrue(equal(dict_1, dict_2))
def test_nested_list_unequal(self):
dict_1 = {"a": ["a", "b", "c", "d"], "c": "d"}
dict_2 = {"c": "d", "a": ["b", "c", "d", "a"]}
self.assertFalse(equal(dict_1, dict_2))
class TestListEqual(TestCase):
def test_equal_lists(self):
list_1 = ["a", "b", "c"]
list_2 = ["a", "b", "c"]
self.assertTrue(equal(list_1, list_2))
def test_equal_lists_with_nan(self):
list_1 = ["a", nan, "c"]
list_2 = ["a", nan, "c"]
self.assertTrue(equal(list_1, list_2))
def test_unsorted_lists(self):
list_1 = ["a", "b", "c"]
list_2 = ["b", "b", "a"]
self.assertFalse(equal(list_1, list_2))
def test_first_list_larger(self):
list_1 = ["a", "b", "c"]
list_2 = ["a", "b"]
self.assertFalse(equal(list_1, list_2))
def test_second_list_larger(self):
list_1 = ["a", "b"]
list_2 = ["a", "b", "c"]
self.assertFalse(equal(list_1, list_2))
def test_list_with_none_unequal(self):
list_1 = ["a", "b", None]
list_2 = ["a", "b", "c"]
self.assertFalse(equal(list_1, list_2))
list_1 = ["a", "b", None]
list_2 = [None, "b", "c"]
self.assertFalse(equal(list_1, list_2))
def test_list_with_none_equal(self):
list_1 = ["a", None, "c"]
list_2 = ["a", None, "c"]
self.assertTrue(equal(list_1, list_2))
def test_empty_list(self):
list_1 = []
list_2 = []
self.assertTrue(equal(list_1, list_2))
def test_one_none(self):
list_1 = None
list_2 = []
self.assertFalse(equal(list_1, list_2))
def test_same_list(self):
list_1 = ["a", "b", "c"]
self.assertTrue(equal(list_1, list_1))
def test_equal_nested_lists(self):
list_1 = ["a", ["b", "c"], "d"]
list_2 = ["a", ["b", "c"], "d"]
self.assertTrue(equal(list_1, list_2))
def test_unequal_nested_lists(self):
list_1 = ["a", ["b", "c"], "d"]
list_2 = ["a", [], "c"]
self.assertFalse(equal(list_1, list_2))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
"""
This module acts as a test that type checkers will allow each validator
class to be assigned to a variable of type `type[Validator]`
The assignation is only valid if type checkers recognize each Validator
implementation as a valid implementer of the protocol.
"""
from jsonschema.protocols import Validator
from jsonschema.validators import (
Draft3Validator,
Draft4Validator,
Draft6Validator,
Draft7Validator,
Draft201909Validator,
Draft202012Validator,
)
my_validator: type[Validator]
my_validator = Draft3Validator
my_validator = Draft4Validator
my_validator = Draft6Validator
my_validator = Draft7Validator
my_validator = Draft201909Validator
my_validator = Draft202012Validator
# in order to confirm that none of the above were incorrectly typed as 'Any'
# ensure that each of these assignments to a non-validator variable requires an
# ignore
none_var: None
none_var = Draft3Validator # type: ignore[assignment]
none_var = Draft4Validator # type: ignore[assignment]
none_var = Draft6Validator # type: ignore[assignment]
none_var = Draft7Validator # type: ignore[assignment]
none_var = Draft201909Validator # type: ignore[assignment]
none_var = Draft202012Validator # type: ignore[assignment]

File diff suppressed because it is too large Load Diff