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
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
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
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
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.' )
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
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
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
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}')
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}')