Source code for stereotype.fields.base

from __future__ import annotations

from copy import deepcopy, copy
from typing import Any, Optional, Callable, Iterable, TYPE_CHECKING, List, Tuple

from stereotype.fields.annotations import AnnotationResolver
from stereotype.roles import DEFAULT_ROLE, Role
from stereotype.utils import Missing, ConfigurationError, PathErrorType, ValidationContextType, Validator, \
    ToPrimitiveContextType

if TYPE_CHECKING:  # pragma: no cover
    from stereotype.model import _OutputFieldConfig, _InputFieldConfig, _ValidatedFieldConfig, _ValidatorMethod, \
        _NativeValidator, _SerializableFn


def field_method_overriden(obj, method_name) -> bool:
    return getattr(type(obj), method_name) is not getattr(Field, method_name)


class Field:
    """
    Abstract base class for other field types. Use :class:`AnyField` if type shouldn't be checked.

    :param default: Means the field isn't required, used as default directly or called if callable
    :param hide_none: If the field's value is None, it will be hidden from serialized output
    :param hide_empty: If the field's value is empty_value (varies by field type), it will be hidden
    :param primitive_name: Changes the key used to represent the field in serialized data - input or output
    :param to_primitive_name: Changes the key used to represent the field in serialized data - output only
    :param validators: Optional list of validator callbacks - they raise ``ValueError`` if the value is invalid
    """

    __slots__ = ('name', 'required', 'allow_none', 'default', 'default_factory', 'native_validate', 'validator_method',
                 'validators', 'hide_none', 'hide_empty', 'primitive_name', 'to_primitive_name', 'serializable',
                 'custom_to_primitive')
    type = NotImplemented
    type_repr: str = NotImplemented
    atomic: bool = False
    empty_value = NotImplemented

    def __init__(self, *, default: Any = Missing, hide_none: bool = False, hide_empty: bool = False,
                 primitive_name: Optional[str] = Missing, to_primitive_name: Optional[str] = Missing,
                 validators: Optional[List[Validator]] = None):
        # All NotImplemented *must* be updated later based on annotations
        self.name: str = NotImplemented
        self.allow_none: bool = False
        self.primitive_name: Optional[str] = primitive_name  # Missing falls back to field name
        self.to_primitive_name: Optional[str] = to_primitive_name  # Missing falls back to field name
        if primitive_name is not Missing and to_primitive_name is Missing:
            self.to_primitive_name = primitive_name

        validate_overridden = field_method_overriden(self, 'validate')
        self.native_validate: Optional[_NativeValidator] = self.validate if validate_overridden else None
        self.validator_method: Optional[_ValidatorMethod] = None
        self.validators: Optional[Tuple[Validator, ...]] = tuple(validators) if validators else None
        self.serializable: Optional[_SerializableFn] = None

        # Only user-specifiable options are allowed as arguments to avoid user confusion
        self.required: bool = True
        self.default: Optional[Any] = None
        self.default_factory: Optional[Callable[[], Any]] = None
        if default is not Missing:
            self.init_default(default)
        self.hide_none = hide_none
        assert not (hide_empty and self.empty_value is NotImplemented), f'{type(self)} does not support hide_empty'
        self.hide_empty = hide_empty
        self.custom_to_primitive = field_method_overriden(self, 'to_primitive')

    def init_from_annotation(self, parser: AnnotationResolver):
        """Check this Field type is appropriate for the annotation and load any nested types from it."""
        raise NotImplementedError  # pragma: no cover

    def init_name(self, name: str):
        self.name = name
        if self.primitive_name is Missing:
            self.primitive_name = name
        if self.to_primitive_name is Missing:
            self.to_primitive_name = name

    def init_default(self, default: Any):
        self.required = False
        if callable(default):
            self.default_factory = default
        else:
            self.default = default

    def check_default(self):
        """Check the default is valid input for the field. Called after `init_from_annotation` and `init_name`."""
        if self.required or self.default_factory is not None:
            pass
        elif self.default is None:
            if not self.allow_none:
                raise ConfigurationError("Cannot use None as default on a non-Optional Field")
        elif self.type is not NotImplemented and not isinstance(self.default, self.type):
            raise ConfigurationError(f'Value `{self.default}` used as field default must be of type {self.type_repr}')

    def copy_field(self):
        """
        Copies the field definition - explicit Fields must be copied, otherwise subclasses would share them.
        Any fields where plain `copy.copy` won't be enough should be manually adjusted afterwards.
        """
        copied = copy(self)
        # The native_validate slot contains a method, which would normally remain bound to the old instance
        native_validate = getattr(self, 'native_validate', None)
        if native_validate is not None:
            copied.native_validate = getattr(copied, native_validate.__func__.__name__)
        return copied

    def validation_errors(self, value: Any, context: ValidationContextType) -> Iterable[PathErrorType]:
        if value is Missing or (value is None and not self.allow_none):
            yield (), 'This field is required'
            return
        if self.native_validate is not None and value is not None:
            yield from self.native_validate(value, context)
        if self.validators is not None:
            for validator in self.validators:
                try:
                    validator(value, context)
                except ValueError as e:
                    yield (), str(e)

    def validate(self, value: Any, context: ValidationContextType) -> Iterable[PathErrorType]:
        """Method to override to add native field validation. If not overridden, native_validate will stay None."""
        yield from ()

    def _fill_missing(self):
        if self.required:
            return Missing
        if self.default_factory is None:
            return self.default
        return self.default_factory()

    def convert(self, value: Any) -> Any:
        if value is Missing:
            return self._fill_missing()
        if value is None:
            return None
        return self.type(value)

    def to_primitive(self, value: Any, role: Role = DEFAULT_ROLE, context: ToPrimitiveContextType = None) -> Any:
        return value

    def copy_value(self, value: Any) -> Any:
        return value

    def make_input_config(self) -> _InputFieldConfig:
        return self.name, self.primitive_name, self.convert, (None if self.atomic else self.copy_value)

    def make_validated_config(self) -> _ValidatedFieldConfig:
        return (self.name, self.primitive_name or self.to_primitive_name or self.name, self.allow_none,
                self.validation_errors, self.validator_method)

    def make_output_config(self) -> _OutputFieldConfig:
        # Note this isn't expected to be used if to_primitive_name is None
        return (self, self.name, self.serializable,
                self.to_primitive if self.custom_to_primitive else None, self.to_primitive_name,
                self.hide_none, self.hide_empty, self.empty_value)

    def has_validation(self) -> bool:
        return self.required or not self.allow_none or self.validator_method or self.native_validate or self.validators

    def __repr__(self):
        type_repr = f'Optional[{self.type_repr}]' if self.allow_none else self.type_repr
        return (
            f'<Field{f" {self.name}" if self.name is not NotImplemented else ""} of type {type_repr}, '
            f'{"required" if self.required else f"default=<{self.default}>"}'
            f'{f", primitive name {self.primitive_name}" if self.primitive_name != self.name else ""}'
            f'{f", to output {self.to_primitive_name}" if self.to_primitive_name != self.primitive_name else ""}>'
        )


[docs]class AnyField(Field): """ Value of any type (usually annotation ``typing.Any``, but can be anything). :param deep_copy: If true, conversion, serialization and copying will use copy.deepcopy for this value :param default: Means the field isn't required, used as default directly or called if callable :param hide_none: If the field's value is None, it will be hidden from serialized output :param primitive_name: Changes the key used to represent the field in serialized data - input or output :param to_primitive_name: Changes the key used to represent the field in serialized data - output only :param validators: Optional list of validator callbacks - they raise ``ValueError`` if the value is invalid """ __slots__ = Field.__slots__ + ('deep_copy',) type = NotImplemented # There is no appropriate type, NotImplemented is handled specifically type_repr: str = 'Any' atomic = False def __init__(self, *, deep_copy: bool = False, default: Any = Missing, hide_none: bool = False, primitive_name: Optional[str] = Missing, to_primitive_name: Optional[str] = Missing, validators: Optional[List[Validator]] = None): super().__init__(default=default, hide_none=hide_none, primitive_name=primitive_name, to_primitive_name=to_primitive_name, validators=validators) self.deep_copy = deep_copy def init_from_annotation(self, parser: AnnotationResolver): pass # Don't require typing.Any, explicit AnyField should allow any typing def convert(self, value: Any) -> Any: if value is Missing: return self._fill_missing() return deepcopy(value) if self.deep_copy else value def copy_value(self, value: Any) -> Any: return deepcopy(value) def to_primitive(self, value: Any, role: Role = DEFAULT_ROLE, context: ToPrimitiveContextType = None) -> Any: return deepcopy(value) if self.deep_copy else value