Source code for stereotype.fields.atomic

from __future__ import annotations

import re
from typing import Union, Any, Iterable, Optional, List

from stereotype.fields.annotations import AnnotationResolver
from stereotype.fields.base import Field
from stereotype.utils import Missing, ConfigurationError, PathErrorType, ValidationContextType, Validator


class _AtomicField(Field):
    atomic = True

    def init_from_annotation(self, parser: AnnotationResolver):
        if parser.annotation is not self.type:
            raise parser.incorrect_type(self)


[docs]class BoolField(_AtomicField): """ Boolean value (annotation ``bool``), accepting boolean values or true/yes/false/no strings. :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_false: If the field's value is False, 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 = bool type_repr = 'bool' empty_value = False def __init__(self, *, default: Any = Missing, hide_none: bool = False, hide_false: 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_false, primitive_name=primitive_name, to_primitive_name=to_primitive_name, validators=validators) def convert(self, value: Any) -> Any: if value is Missing: return self._fill_missing() if value is None: return None if value is False or value is True: return value if value in {'true', 'True', 'yes', 'Yes'}: return True if value in {'false', 'False', 'no', 'No'}: return False raise TypeError('Value must be a boolean or a true/false/yes/no string value')
class _BaseNumberField(_AtomicField): __slots__ = Field.__slots__ + ('min_value', 'max_value') min_value: Union[int, float, None] max_value: Union[int, float, None] def _set_min_max_value_validation(self, min_value: Union[int, float, None], max_value: Union[int, float, None]): if min_value is not None and max_value is not None: self.native_validate = self._validate_min_max_value elif min_value is not None: self.native_validate = self._validate_min_value elif max_value is not None: self.native_validate = self._validate_max_value def _validate_min_max_value(self, value: Any, _: ValidationContextType) -> Iterable[PathErrorType]: if not (self.min_value <= value <= self.max_value): yield (), f'Must be between {self.min_value} and {self.max_value}' def _validate_min_value(self, value: Any, _: ValidationContextType) -> Iterable[PathErrorType]: if value < self.min_value: yield (), f'Must be at least {self.min_value}' def _validate_max_value(self, value: Any, _: ValidationContextType) -> Iterable[PathErrorType]: if value > self.max_value: yield (), f'Must be at most {self.max_value}'
[docs]class IntField(_BaseNumberField): """ Integer value (annotation ``int``), accepting integer values, whole float values or strings with integer values. :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_zero: If the field's value is 0, 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 min_value: Validation enforces the number is greater than or equal to this value :param max_value: Validation enforces the number is lower than or equal to this value :param validators: Optional list of validator callbacks - they raise ``ValueError`` if the value is invalid """ __slots__ = _BaseNumberField.__slots__ type = int type_repr = 'int' empty_value = 0 def __init__(self, *, default: Any = Missing, hide_none: bool = False, hide_zero: bool = False, primitive_name: Optional[str] = Missing, to_primitive_name: Optional[str] = Missing, min_value: Optional[int] = None, max_value: Optional[int] = None, validators: Optional[List[Validator]] = None): super().__init__(default=default, hide_none=hide_none, hide_empty=hide_zero, primitive_name=primitive_name, to_primitive_name=to_primitive_name, validators=validators) self.min_value = min_value self.max_value = max_value self._set_min_max_value_validation(min_value, max_value) def convert(self, value: Any) -> Any: if value is Missing: return self._fill_missing() if value is None: return None if isinstance(value, float) and value != int(value): raise TypeError(f'Numeric value {value} is not an integer') try: return int(value) except (ValueError, TypeError): raise TypeError(f'Value {value!r} is not an integer number')
[docs]class FloatField(_BaseNumberField): """ Floating point value (annotation ``float``), accepting float values, integers or strings with float values. :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_zero: If the field's value is 0.0, 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 min_value: Validation enforces the number is greater than or equal to this value :param max_value: Validation enforces the number is lower than or equal to this value :param validators: Optional list of validator callbacks - they raise ``ValueError`` if the value is invalid """ __slots__ = _BaseNumberField.__slots__ type = float type_repr = 'float' empty_value = 0. def __init__(self, *, default: Any = Missing, hide_none: bool = False, hide_zero: bool = False, primitive_name: Optional[str] = Missing, to_primitive_name: Optional[str] = Missing, min_value: Optional[float] = None, max_value: Optional[float] = None, validators: Optional[List[Validator]] = None): super().__init__(default=default, hide_none=hide_none, hide_empty=hide_zero, primitive_name=primitive_name, to_primitive_name=to_primitive_name, validators=validators) self.min_value = min_value self.max_value = max_value self._set_min_max_value_validation(min_value, max_value)
[docs]class StrField(_AtomicField): """ String value (annotation ``str``), accepting string values, or anything that can be cast to a string. :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 an empty string, 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 min_length: Validation enforces the string has a minimum number of characters (1 => non-empty) :param max_length: Validation enforces the string has a maximum number of characters :param choices: Validation enforces the string matches (case-sensitive) one of these choices :param regex: Validation enforces the string matches the regular expression :param validators: Optional list of validator callbacks - they raise ``ValueError`` if the value is invalid """ __slots__ = _AtomicField.__slots__ + ('min_length', 'max_length', 'choices', 'regex') type = str type_repr = 'str' 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, min_length: int = 0, max_length: Optional[int] = None, choices: Optional[Iterable[str]] = None, regex: str | re.Pattern | None = None, 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) if sum([min_length > 0 or max_length is not None, choices is not None, regex is not None]) > 1: raise ConfigurationError('Can only validate length, choices or regex; not combinations of these') self.min_length = min_length self.max_length = max_length self.choices = {choice: None for choice in choices} if choices is not None else None # Sets are not ordered self.regex = re.compile(regex) if isinstance(regex, str) else regex if self.choices is not None: self.native_validate = self._validate_choices elif min_length > 0 and max_length is not None: self.native_validate = self._validate_min_max_length elif min_length == 1: self.native_validate = self._validate_not_empty elif min_length > 0: self.native_validate = self._validate_min_length elif max_length is not None: self.native_validate = self._validate_max_length elif regex is not None: self.native_validate = self._validate_regex def _validate_choices(self, value: str, _: ValidationContextType) -> Iterable[PathErrorType]: if value not in self.choices: yield (), f'Must be one of: {", ".join(self.choices)}' def _validate_min_max_length(self, value: str, _: ValidationContextType) -> Iterable[PathErrorType]: if not (self.min_length <= len(value) <= self.max_length): if self.min_length == self.max_length: yield (), f'Must be exactly {self.min_length} character{"s" if self.min_length > 1 else ""} long' else: yield (), f'Must be {self.min_length} to {self.max_length} characters long' # Note: the validation methods that are put in place of native_validate may not be static # noinspection PyMethodMayBeStatic def _validate_not_empty(self, value: str, _: ValidationContextType) -> Iterable[PathErrorType]: if not value: yield (), 'This value cannot be empty' def _validate_min_length(self, value: str, _: ValidationContextType) -> Iterable[PathErrorType]: if len(value) < self.min_length: yield (), f'Must be at least {self.min_length} characters long' def _validate_max_length(self, value: str, _: ValidationContextType) -> Iterable[PathErrorType]: if len(value) > self.max_length: yield (), f'Must be at most {self.max_length} character{"s" if self.max_length > 1 else ""} long' def _validate_regex(self, value: str, _: ValidationContextType) -> Iterable[PathErrorType]: if not self.regex.match(value): case = " (case insensitive)" if self.regex.flags & re.I else "" yield (), f'Must match regex `{self.regex.pattern}`{case}'
ATOMIC_TYPE_MAPPING = { bool: BoolField, int: IntField, float: FloatField, str: StrField, }