Exemple #1
0
    def _analyze_annotation_model_union(mcs, annotation,
                                        options: List[Any]) -> Field:
        """Union[ModelSubClass, ...] -> DynamicModelField"""
        from stereotype import DynamicModelField

        type_map = {}
        for option in options:
            if not hasattr(option, 'type'):
                raise ConfigurationError(
                    f"Model {option.__name__} used in a dynamic model field {annotation} but does "
                    f"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 {annotation} but it's "
                    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 {annotation} but it's "
                    f"`type` field {option.type} is not a string")
            if option.type in type_map:
                raise ConfigurationError(
                    f"Conflicting dynamic model field types in {annotation}: "
                    f"{type_map[option.type].__name__} vs {option.__name__}")
            type_map[option.type] = option

        field = DynamicModelField()
        field.type_map = type_map
        field.types = tuple(type_map.values())
        return field
Exemple #2
0
    def _analyze_fields(mcs, cls: Type[Model],
                        field_values: dict) -> Iterable[Field]:
        for name, annotation in mcs._iterate_fields(
                mcs._resolve_annotations(cls)):
            analyzed_field = mcs._analyze_annotation(annotation)
            value = field_values.get(name)
            if isinstance(value, Field):
                field = value.copy_field()
                if not isinstance(field, type(analyzed_field)):
                    field.fill_in_name(name)
                    analyzed_field.fill_in_name(name)
                    raise ConfigurationError(
                        f'Annotations for {analyzed_field} require custom field type '
                        f'{type(analyzed_field).__name__}, got {type(field).__name__}'
                    )
                field.type_config_from(analyzed_field)
            else:
                field = analyzed_field
                if name in field_values:
                    field.set_default(value)

            field.fill_in_name(name)
            field.check_default()

            validator_method = getattr(cls, f'validate_{name}', None)
            if validator_method is not None:
                field.validator_method = validator_method

            yield field
Exemple #3
0
 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):
     super().__init__(default=default,
                      hide_none=hide_none,
                      hide_empty=hide_empty,
                      primitive_name=primitive_name,
                      to_primitive_name=to_primitive_name)
     if (min_length > 0 or max_length is not None) and choices is not None:
         raise ConfigurationError(
             'Cannot use min_length or max_length together with choices')
     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
     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
Exemple #4
0
    def __new__(mcs, name: str, bases: Tuple[type, ...], attrs: Dict[str,
                                                                     Any]):
        if not bases:
            return type.__new__(mcs, name, bases, attrs)

        # Only annotated attributes iterated here for the purposes of slots, not serializable fields
        field_names = [
            name for name, annotation in mcs._iterate_fields(
                attrs.get('__annotations__', {}))
        ]
        field_values = {}
        for field_name in field_names:
            if field_name in attrs:
                # Field attributes popped as they will become instance attributes instead of class attributes
                field_values[field_name] = attrs.pop(field_name)

        # Find serializable, keep them in `attrs` as they need to remain as properties
        serializable_names = {
            name
            for name, _ in mcs._iterate_serializable(attrs)
        }

        # Using dicts instead of sets to preserve order
        all_slots = {
            **{
                slot: 0
                for parent in bases for slot in parent.__slots__ or getattr(
                    parent, '__abstract_slots__', ())
            },
            **{slot: 1
               for slot in field_names},
            **{slot: 2
               for slot in attrs.get('__slots__', ())},
        }
        if attrs.pop('__abstract__', False):
            attrs['__abstract_slots__'] = all_slots
        else:
            attrs['__slots__'] = [
                name for name in all_slots if name not in serializable_names
            ]
        attrs['__fields__'] = NotImplemented
        attrs['__input_fields__'] = NotImplemented
        attrs['__validated_fields__'] = NotImplemented
        attrs['__role_fields__'] = NotImplemented
        attrs['__roles__'] = NotImplemented

        own_field_names = set(field_names) | serializable_names

        try:
            cls = cast(Type['Model'], type.__new__(mcs, name, bases, attrs))
        except TypeError as e:
            raise ConfigurationError(
                f'{name}: {e}, if inheriting from multiple models, only one may have __slots__ '
                f'(declare abstract models without __slots__ by adding class attribute '
                f'`__abstract__ = True`)')
        cls.__initialize_model__ = lambda *_: mcs._initialize_model(
            cls, bases, own_field_names, field_values)
        return cls
Exemple #5
0
 def _resolve_annotations(cls: Type[Model]) -> Dict[str, Any]:
     extra_types: Set[Type[Model]] = cls.resolve_extra_types()
     extra_locals = {typ.__name__: typ for typ in extra_types}
     try:
         return get_type_hints(cls, localns=extra_locals)
     except NameError as e:
         raise ConfigurationError(
             f'Model {cls.__name__} annotation {str(e)}. If not a global symbol or cannot be '
             f'imported globally, use the class method `resolve_extra_types` to provide it.'
         )
Exemple #6
0
 def __init__(self, role: Role, fields, is_whitelist: bool,
              override_parents: bool):
     self.fields, non_descriptors = self._collect_input_fields(fields)
     if non_descriptors:
         raise ConfigurationError(
             f'Role blacklist/whitelist needs member descriptors (e.g. cls.my_field), '
             f'got {non_descriptors[0]!r}')
     self.role = role
     self.is_whitelist = is_whitelist
     self.override_parents = override_parents
Exemple #7
0
 def _analyze_annotation_dict(mcs, annotation) -> Field:
     """Dict[..., ....] -> DictField"""
     field = DictField()
     key_annotation, value_annotation = annotation.__args__
     field.key_field = mcs._analyze_annotation(key_annotation)
     if not field.key_field.atomic:
         raise ConfigurationError(
             f'DictField keys may only be booleans, numbers or strings: {annotation}'
         )
     field.value_field = mcs._analyze_annotation(value_annotation)
     return field
Exemple #8
0
 def _collect_own_requested_roles(
         mcs, cls: Type[Model]
 ) -> Tuple[Set[str], Dict[Role, RequestedRoleFields]]:
     all_field_names = {field.name for field in cls.__fields__}
     own_requested_roles: Dict[Role, RequestedRoleFields] = {}
     for requested in cls.declare_roles():
         if requested.role in own_requested_roles:
             raise ConfigurationError(
                 f'Role {requested.role.name} configured for {cls.__name__} multiple times'
             )
         own_requested_roles[requested.role] = requested
     return all_field_names, own_requested_roles
Exemple #9
0
    def _analyze_annotation_union(mcs, annotation) -> Field:
        """Optional or Union -> any Field with allow_none or DynamicModelField"""
        from stereotype.model import Model

        options = annotation.__args__
        non_none = [
            option for option in options if option not in (type(None), )
        ]

        if len(non_none) < len(options):
            # Optional is always represented by Union[None, ...]
            return mcs._analyze_annotation_optional(non_none)
        elif all(issubclass(option, Model) for option in options):
            return mcs._analyze_annotation_model_union(annotation, options)
        else:
            raise ConfigurationError(
                f'Union Model fields can only be Optional or Union of Model subclass types, '
                f'got {annotation!r}')
Exemple #10
0
    def _analyze_annotation(mcs, annotation) -> Field:
        """Any supported annotation -> any Field"""
        from stereotype.model import Model

        typing_repr = repr(annotation)
        if typing_repr.startswith('typing.'):
            if typing_repr.startswith('typing.List['):
                return mcs._analyze_annotation_list(annotation)
            if typing_repr.startswith('typing.Dict['):
                return mcs._analyze_annotation_dict(annotation)
            if typing_repr.startswith('typing.Union['):
                return mcs._analyze_annotation_union(annotation)
            if typing_repr == 'typing.Any':
                return AnyField()
        else:
            if annotation in (bool, int, float, str):
                return mcs._analyze_annotation_atomic(annotation)
            if issubclass(annotation, Model):
                return mcs._analyze_annotation_model(annotation)

        raise ConfigurationError(f'Unsupported Model field {annotation!r}')