Source code for statham.schema.elements.base

# False positive. The cycle exists but is avoided by importing last.
# pylint: disable=cyclic-import
from typing import Any, cast, Dict, List, Generic, TypeVar, Union

from statham.schema.constants import NotPassed, Maybe
from statham.schema.exceptions import ValidationError
from statham.schema.helpers import custom_repr
from statham.schema.property import _Property, _PropertyDict
from statham.schema.validation import (
    get_validators,
    InstanceOf,
    NoMatch,
    Validator,
)


T = TypeVar("T")

Numeric = Union[int, float]


# This emulates the options available to a general JSON Schema object.
# pylint: disable=too-many-instance-attributes
[docs]class Element(Generic[T]): # pylint: disable=line-too-long """An un-typed schema element. Accepts JSON Schema keywords as arguments. :param default: The default value this element should return when not provided. :param const: Restrict this element to a constant value. :param enum: Restrict this element to an enumeration of provided values. :param items: The :class:`Element` with which to validate list items. If :paramref:`~Element.items` is a list of :class:`Element`, then each :class:`Element` validates the corresponding index of the value. Subsequent values are validated by :paramref:`~Element.additionalItems`. :param additionalItems: The :class:`Element` with which to validate additional items in a list, when :paramref:`items` is a list and shorter than the passed list. :param minItems: The minimum number of items to allow in list values. :param maxItems: The maximum number of items to allow in list values. :param uniqueItems: If :const:`True`, validate that list values contain unique elements. :param contains: Validate that list values contain at least one element matching this :class:`Element`. :param minimum: Validate that numeric values are greater than or equal to this value. :param maximum: Validate that numeric values are less than or equal to this value. :param exclusiveMinimum: Validate that numeric values are strictly greater than this value. :param exclusiveMaximum: Validate that numeric values are strictly less than this value. :param multipleOf: Validate that numeric values are are multiple of this value. :param format: Validate that string values conform to the specified format. See :func:`~statham.schema.validation.format.format_checker` to learn about included formats and how to add your own. :param pattern: Validate that string values match this value as a regular expression. :param minLength: Validate that string values are at least as long as this value. :param maxLength: Validate that string values are at most as long as this value. :param required: Validate that dictionary values contain each of these keys. :param properties: Validate that dictionary values match these properties. See :class:`statham.schema.property.Property` for more information. :param patternProperties: Validate that keys of dictionaries matching these patterns conform to the provided :class:`Element`. Keys may match on multiple :paramref:`~Element.patternProperties` and :paramref:`~Element.properties` at the same time. :param additionalProperties: Validate properties not matched by either :paramref:`~Element.properties` or :paramref:`~Element.patternProperties` against this element. If :const:`True`, any value is allowed. If :const:`False`, no additional properties are allowed. :param minProperties: Validate that dictionary values contain at least this many members. :param maxProperties: Validate that dictionary values contain at most this many members. :param propertyNames: Validate that keys of dictionary values match are accepted by this :class:`Element`. Any :class:`Element` is allowed here, but it is not useful to use any other than :class:`statham.schema.elements.String`. :param dependencies: Define JSON Schema dependencies. There are two types of dependencies. If the dict value is a list, then the each property name in that list must be present when the property name of the key is present. If the value is an :class:`Element`, then it's validation applies whenever the property name of the key is present. See `Object Dependencies <https://json-schema.org/understanding-json-schema/reference/object.html#dependencies>`_ for more detail. :param description: Description of the element. """ # pylint: enable=line-too-long _properties: Maybe[_PropertyDict] # This is how many options there are! # pylint: disable=too-many-locals def __init__( self, *, # Bad name to match JSON Schema keywords. # pylint: disable=redefined-builtin default: Maybe[Any] = NotPassed(), const: Maybe[Any] = NotPassed(), enum: Maybe[List[Any]] = NotPassed(), items: Maybe[Union["Element", List["Element"]]] = NotPassed(), additionalItems: Union["Element", bool] = True, minItems: Maybe[int] = NotPassed(), maxItems: Maybe[int] = NotPassed(), uniqueItems: bool = False, contains: Maybe["Element"] = NotPassed(), minimum: Maybe[Numeric] = NotPassed(), maximum: Maybe[Numeric] = NotPassed(), exclusiveMinimum: Maybe[Numeric] = NotPassed(), exclusiveMaximum: Maybe[Numeric] = NotPassed(), multipleOf: Maybe[Numeric] = NotPassed(), format: Maybe[str] = NotPassed(), pattern: Maybe[str] = NotPassed(), minLength: Maybe[int] = NotPassed(), maxLength: Maybe[int] = NotPassed(), required: Maybe[List[str]] = NotPassed(), properties: Maybe[Dict[str, "_Property"]] = NotPassed(), patternProperties: Maybe[Dict[str, "Element"]] = NotPassed(), additionalProperties: Union["Element", bool] = True, minProperties: Maybe[int] = NotPassed(), maxProperties: Maybe[int] = NotPassed(), propertyNames: Maybe["Element"] = NotPassed(), dependencies: Maybe[ Dict[str, Union[List[str], "Element"]] ] = NotPassed(), description: Maybe[str] = NotPassed(), ): self.default = default self.const = const self.enum = enum self.items = items self.additionalItems = additionalItems self.minItems = minItems self.maxItems = maxItems self.uniqueItems = uniqueItems self.contains = contains self.minimum = minimum self.maximum = maximum self.exclusiveMinimum = exclusiveMinimum self.exclusiveMaximum = exclusiveMaximum self.multipleOf = multipleOf self.format = format self.pattern = pattern self.minLength = minLength self.maxLength = maxLength self.required = required # https://github.com/python/mypy/issues/3004 self.properties = properties # type: ignore self.patternProperties = patternProperties self.additionalProperties = additionalProperties self.minProperties = minProperties self.maxProperties = maxProperties self.propertyNames = propertyNames self.dependencies = dependencies self.description = description @property def properties(self) -> Maybe[_PropertyDict]: return self._properties @properties.setter def properties(self, value: Maybe[Dict[str, _Property]]): if isinstance(value, NotPassed): self._properties = value return self._properties = _PropertyDict(cast(Dict[str, _Property], value)) self._properties.parent = self def __repr__(self): """Dynamically construct the repr to match value instantiation.""" return custom_repr(self) def __eq__(self, other): """Check if two elements are equivalent. This just checks the type and public attribute values. Useful for testing parser logic, and could be used to automatically DRY up messy schemas in future. """ if not isinstance(other, self.__class__): return False pub_vars = lambda x: { k: v for k, v in vars(x).items() if not k.startswith("_") or k == "_properties" } return pub_vars(self) == pub_vars(other) @property def annotation(self) -> str: # pylint: disable=no-member generic = type(self).__orig_bases__[0].__args__[0] # type: ignore if isinstance(generic, TypeVar): # type: ignore return "Any" return generic.__name__ @property def type_validator(self) -> Validator: return InstanceOf() @property def validators(self) -> List[Validator]: validators: List[Validator] = [self.type_validator] + list( get_validators(self) ) return validators def construct(self, value, property_): if isinstance(value, list): return self.__items__(value, property_) if isinstance(value, dict): return _AnonymousObject(**self.__properties__(value)) return value @property def __properties__(self) -> "Properties": return Properties( self, getattr(self, "properties", NotPassed()), getattr(self, "patternProperties", NotPassed()), getattr(self, "additionalProperties", True), ) @property def __items__(self) -> "Items": return Items( getattr(self, "items", NotPassed()), getattr(self, "additionalItems", True), )
[docs] def __call__(self, value: Any, property_=None) -> Maybe[T]: """Validate and convert input data against this :class:`Element`. Dictionary (object) values which pass validation are returned as a subclass of :const:`dict` allowing attribute access. :param value: The input data. :param property_: Optionally specify the outer property scope enclosing this :class:`Element`. Used automatically by object validation to produce more useful error messages. :return: The parsed value. """ property_ = property_ or UNBOUND_PROPERTY def create(value): for validator in self.validators: validator(value, property_) return self.construct(value, property_) if not isinstance(self.default, NotPassed) and isinstance( value, NotPassed ): try: return create(self.default) except (TypeError, ValidationError): return self.default if isinstance(value, NotPassed): return value return create(value)
UNBOUND_PROPERTY: _Property = _Property(Element(), required=False) UNBOUND_PROPERTY.bind(parent=Element(), name="<unbound>")
[docs]class Nothing(Element): """Element which matches nothing. Equivalent to :const:`False`.""" def __init__(self): super().__init__() # Don't allow args to Nothing. def __bool__(self): return False @property def annotation(self) -> str: return "None" @property def validators(self) -> List[Validator]: return [NoMatch()]
# Needs to be imported last to prevent cyclic import. # pylint: disable=wrong-import-position from statham.schema.elements.properties import Properties from statham.schema.elements.items import Items class _AnonymousObject(dict): """Anonymous object type. Allows dictionaries passed to untyped objects to use attribute access, to match `Object` instances. """ def __getattr__(self, key: str) -> Any: return self.__getitem__(key) def __setattr__(self, key: str, value: Any): return self.__setitem__(key, value)