Source code for stereotype.fields.model

from __future__ import annotations

from typing import Any, Optional, Type, Iterable, Tuple, Dict, cast, get_args, List

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


[docs]class ModelField(Field): """ Field containing another specific :class:`Model`, as specified in the annotation. :param default: Means the field isn't required, should be None or a callable, for example the model's class :param hide_none: If the field's value is None, it will be hidden from serialized output :param hide_empty: If the model serializes as an empty dict, 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__ + ('type',) atomic = False empty_value = {} 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): super().__init__(default=default, hide_none=hide_none, hide_empty=hide_empty, primitive_name=primitive_name, to_primitive_name=to_primitive_name, validators=validators) self.type: Type[Model] = cast(Type[Model], NotImplemented) def init_from_annotation(self, parser: AnnotationResolver): if not issubclass(parser.annotation, Model): raise parser.incorrect_type(self) self.type = parser.annotation def validate(self, value: Model, context: ValidationContextType) -> Iterable[PathErrorType]: yield from value.validation_errors(context) def convert(self, value: Any) -> Any: if value is Missing: if self.required: return Missing return self.default if self.default_factory is None else self.default_factory() if value is None: return None if isinstance(value, self.type): return value if not isinstance(value, dict): raise TypeError(f'Supplied type {type(value).__name__}, needs a mapping or {self.type.__name__}') return self.type(value) def copy_value(self, value: Any) -> Any: if value is None or value is Missing: return value return value.copy(deep=True) def to_primitive(self, value: Any, role: Role = DEFAULT_ROLE, context: ToPrimitiveContextType = None) -> Any: if value is None or value is Missing: return value return value.to_primitive(role, context) @property def type_repr(self): return self.type.__name__ if self.type is not NotImplemented else '(unknown)'
[docs]class DynamicModelField(Field): """ Field containing one of models recognized by their ``type``, specified as a ``typing.Union`` of :class:`Model` subclasses. :param default: Means the field isn't required, should be None or a callable, for example a model's class :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__ + ('types', 'type_map') atomic = False type = Model def __init__(self, *, 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.types: Tuple[Type[Model], ...] = NotImplemented self.type_map: Dict[str, Type[Model]] = NotImplemented def init_from_annotation(self, parser: AnnotationResolver): if not parser.is_union_origin(): raise parser.incorrect_type(self) options = get_args(parser.annotation) if not all(issubclass(option, Model) for option in options): raise ConfigurationError(f'Union Model fields can only be Optional or Union of Model subclass types, ' f'got {parser!r}') type_map = {} for option in options: if not hasattr(option, 'type'): raise ConfigurationError(f"Model {option.__name__} used in a dynamic model field {parser!r} but " f"does not define a non-type-annotated string `type` field") if type(option.type).__name__ == 'member_descriptor': raise ConfigurationError(f"Model {option.__name__} used in a dynamic model field {parser!r} but its " f"`type` field has a type annotation making it a field, must be an attribute") if not isinstance(option.type, str): raise ConfigurationError(f"Model {option.__name__} used in a dynamic model field {parser!r} but its " f"`type` field {option.type} is not a string") if option.type in type_map: raise ConfigurationError(f"Conflicting dynamic model field types in {parser!r}: " f"{type_map[option.type].__name__} vs {option.__name__}") type_map[option.type] = option self.type_map = type_map self.types = tuple(type_map.values()) def validate(self, value: Any, context: ValidationContextType) -> Iterable[PathErrorType]: yield from value.validation_errors(context) def convert(self, value: Any) -> Any: if value is Missing: if self.required: return Missing return self.default if self.default_factory is None else self.default_factory() is_model = isinstance(value, Model) if is_model and not isinstance(value, self.types): raise TypeError(f'Expected {self.type_repr}, got {type(value).__name__}') if is_model or value is None: return value try: value_type = value['type'] except TypeError: raise TypeError(f'Expected a mapping with a `type` field, got type {type(value).__name__}') except KeyError: raise TypeError('Expected a mapping with a `type` field, got no `type` field') type_cls = self.type_map.get(value_type, None) if type_cls is None: raise TypeError(f'Got a mapping with unsupported `type` {value_type!r}') return type_cls(value) def copy_value(self, value: Any) -> Any: if value is None or value is Missing: return value return value.copy(deep=True) def to_primitive(self, value: Any, role: Role = DEFAULT_ROLE, context: ToPrimitiveContextType = None) -> Any: if value is None or value is Missing: return value result = value.to_primitive(role, context) result['type'] = value.type return result @property def type_repr(self): return f'Union[{", ".join(option.__name__ for option in self.types)}]'