def test_annotations__invalid_type(): class X: foo: List with pytest.raises(InvalidAnnotation): annotations( X, globalns=globals(), localns=locals(), invalid_types={List}, skip_classvar=True, )
def test_annotations__no_local_ns_raises(): class Bar: ... class X: bar: "Bar" with pytest.raises(NameError): annotations( X, globalns=None, localns=None, )
def _contribute_to_options(cls, options: ModelOptions) -> None: # Find attributes and their types, and create indexes for these. # This only happens once when the class is created, so Faust # models are fast at runtime. fields, defaults = annotations( cls, stop=Record, skip_classvar=True, alias_types=ALIAS_FIELD_TYPES, localns={cls.__name__: cls}, ) options.fields = cast(Mapping, fields) options.fieldset = frozenset(fields) options.fieldpos = {i: k for i, k in enumerate(fields.keys())} is_concretely = _is_concretely # extract all default values, but only for actual fields. options.defaults = { k: v.default if isinstance(v, FieldDescriptor) else v for k, v in defaults.items() if k in fields and not (isinstance(v, FieldDescriptor) and v.required) } options.models = {} modelattrs = options.modelattrs = {} for field, typ in fields.items(): is_model, polymorphic_type = _is_model(typ) if is_model: # Extract all model fields options.models[field] = typ # Create mapping of model fields to polymorphic types if # available modelattrs[field] = polymorphic_type if is_optional(typ): # Optional[X] also needs to be added to defaults mapping. options.defaults.setdefault(field, None) # Create frozenset index of default fields. options.optionalset = frozenset(options.defaults) # extract all fields that we want to coerce to a different type # (decimals=True, isodates=True, coercions={MyClass: converter}) # Then move them to options.field_coerce, which is what the # model.__init__ method uses to coerce any fields that need to # be coerced. options.field_coerce = {} if options.isodates: options.coercions.setdefault(DATE_TYPES, iso8601.parse) if options.decimals: options.coercions.setdefault(DECIMAL_TYPES, str_to_decimal) for coerce_types, coerce_handler in options.coercions.items(): options.field_coerce.update({ field: TypeCoerce(typ, coerce_handler) for field, typ in fields.items() if field not in modelattrs and is_concretely(coerce_types, typ) })
def _contribute_to_options(cls, options: ModelOptions) -> None: # Find attributes and their types, and create indexes for these. # This only happens once when the class is created, so Faust # models are fast at runtime. fields, defaults = annotations( cls, stop=Record, skip_classvar=True, alias_types=ALIAS_FIELD_TYPES, localns={cls.__name__: cls}, ) options.fields = cast(Mapping, fields) options.fieldset = frozenset(fields) options.fieldpos = {i: k for i, k in enumerate(fields.keys())} is_date = _is_date is_decimal = _is_decimal # extract all default values, but only for actual fields. options.defaults = { k: v.default if isinstance(v, FieldDescriptor) else v for k, v in defaults.items() if k in fields and not ( isinstance(v, FieldDescriptor) and v.required) } options.optionalset = frozenset(options.defaults) options.models = {} modelattrs = options.modelattrs = {} for field, typ in fields.items(): is_model, concrete_type = _is_model(typ) if is_model: # Extract all model fields options.models[field] = typ # Create mapping of model fields to concrete types if available modelattrs[field] = concrete_type # extract all fields that are not built-in types, # e.g. List[datetime] options.converse = {} if options.isodates: options.converse.update({ field: Converter(typ, cls._parse_iso8601) for field, typ in fields.items() if field not in modelattrs and is_date(typ) }) if options.decimals: options.converse.update({ field: Converter(typ, cls._parse_decimal) for field, typ in fields.items() if field not in modelattrs and is_decimal(typ) })
def _contribute_to_options(cls, options: ModelOptions) -> None: # Find attributes and their types, and create indexes for these. # This only happens once when the class is created, so Faust # models are fast at runtime. fields, defaults = annotations( cls, stop=Record, skip_classvar=True, alias_types=ALIAS_FIELD_TYPES, localns={cls.__name__: cls}, ) options.fields = cast(Mapping, fields) options.fieldset = frozenset(fields) options.fieldpos = {i: k for i, k in enumerate(fields.keys())} # extract all default values, but only for actual fields. options.defaults = { k: v.default if isinstance(v, FieldDescriptor) else v for k, v in defaults.items() if k in fields and not (isinstance(v, FieldDescriptor) and v.required) } # Raise error if non-defaults are mixed in with defaults # like namedtuple/dataclasses do. local_defaults = [] for attr_name in cls.__annotations__: if attr_name in cls.__dict__: default_value = cls.__dict__[attr_name] if isinstance(default_value, FieldDescriptorT): if not default_value.required: local_defaults.append(attr_name) else: local_defaults.append(attr_name) else: if local_defaults: raise TypeError( E_NON_DEFAULT_FOLLOWS_DEFAULT.format( cls_name=cls.__name__, field_name=attr_name, fields=pluralize(len(local_defaults), 'field'), default_names=', '.join(local_defaults), )) for field, typ in fields.items(): if is_optional(typ): # Optional[X] also needs to be added to defaults mapping. options.defaults.setdefault(field, None) # Create frozenset index of default fields. options.optionalset = frozenset(options.defaults)
def _extract_signals( self, case_cls: Type[_Case], base_case: Type[_Case]) -> Iterable[Tuple[str, Type[BaseSignal]]]: fields, defaults = annotations( case_cls, stop=base_case, skip_classvar=True, localns={case_cls.__name__: case_cls}, ) for attr_name, attr_type in fields.items(): actual_type = getattr(attr_type, '__origin__', attr_type) if actual_type is None: # Python <3.7 actual_type = attr_type try: if issubclass(actual_type, BaseSignal): yield attr_name, attr_type except TypeError: pass
def test_annotations__skip_classvar(): class X: Foo: ClassVar[int] = 3 foo: "int" bar: List["X"] baz: Union[List["X"], str] mas: int = 3 fields, defaults = annotations( X, globalns=globals(), localns=locals(), skip_classvar=True, ) assert fields == { "foo": int, "bar": List[X], "baz": Union[List[X], str], "mas": int, } assert defaults["mas"] == 3
def test_annotations__skip_classvar(): class X: Foo: ClassVar[int] = 3 foo: 'int' bar: List['X'] baz: Union[List['X'], str] mas: int = 3 fields, defaults = annotations( X, globalns=globals(), localns=locals(), skip_classvar=True, ) assert fields == { 'foo': int, 'bar': List[X], 'baz': Union[List[X], str], 'mas': int, } assert defaults['mas'] == 3
def _contribute_to_options(cls, options: ModelOptions) -> None: # Find attributes and their types, and create indexes for these. # This only happens once when the class is created, so Faust # models are fast at runtime. fields, defaults = annotations( cls, stop=Record, skip_classvar=True, alias_types=ALIAS_FIELD_TYPES, localns={cls.__name__: cls}, ) options.fields = cast(Mapping, fields) options.fieldset = frozenset(fields) options.fieldpos = {i: k for i, k in enumerate(fields.keys())} # extract all default values, but only for actual fields. options.defaults = { k: v.default if isinstance(v, FieldDescriptor) else v for k, v in defaults.items() if k in fields and not (isinstance(v, FieldDescriptor) and v.required) } options.models = {} options.polyindex = {} modelattrs = options.modelattrs = {} def _is_concrete_type(field: str, wanted: IsInstanceArgT) -> bool: typeinfo = options.polyindex[field] try: return issubclass(typeinfo.member_type, wanted) except TypeError: return False # Raise error if non-defaults are mixed in with defaults # like namedtuple/dataclasses do. local_defaults = [] for attr_name in cls.__annotations__: if attr_name in cls.__dict__: default_value = cls.__dict__[attr_name] if isinstance(default_value, FieldDescriptorT): if not default_value.required: local_defaults.append(attr_name) else: local_defaults.append(attr_name) else: if local_defaults: raise TypeError( E_NON_DEFAULT_FOLLOWS_DEFAULT.format( cls_name=cls.__name__, field_name=attr_name, fields=pluralize(len(local_defaults), 'field'), default_names=', '.join(local_defaults), )) for field, typ in fields.items(): is_model, member_type, generic_type = _is_model(typ) options.polyindex[field] = TypeInfo(generic_type, member_type) if is_model: # Extract all model fields options.models[field] = typ # Create mapping of model fields to polymorphic types if # available modelattrs[field] = generic_type if is_optional(typ): # Optional[X] also needs to be added to defaults mapping. options.defaults.setdefault(field, None) # Create frozenset index of default fields. options.optionalset = frozenset(options.defaults) # extract all fields that we want to coerce to a different type # (decimals=True, isodates=True, coercions={MyClass: converter}) # Then move them to options.field_coerce, which is what the # model.__init__ method uses to coerce any fields that need to # be coerced. options.field_coerce = {} if options.isodates: options.coercions.setdefault(DATE_TYPES, iso8601.parse) if options.decimals: options.coercions.setdefault(DECIMAL_TYPES, str_to_decimal) for coerce_types, coerce_handler in options.coercions.items(): options.field_coerce.update({ field: TypeCoerce(typ, coerce_handler) for field, typ in fields.items() if (field not in modelattrs and _is_concrete_type(field, coerce_types)) })