Complex typing in Python - python

I can't figure out how to properly define return types for the function.
Details:
This is the function that parses the settings object and returns a list of parameter values, it can be tuple[str], tuple[str, str], tuple[Type[BaseCountryRatingComponent], bool] whatever from the set of [str, bool, Type[BaseCountryRatingComponent]].
Question:
How to properly define return types for settings_traversal?
from typing import Union, Type
class BaseCountryRatingComponent:
pass
class ComponentSetting:
pass
class CompositeComponentSetting:
pass
def settings_traversal(
settings: list[Union["ComponentSetting", "CompositeComponentSetting"]],
query_params: list[str],
) -> list[Union[str, bool, Type["BaseCountryRatingComponent"]]]:
pass
def get_model_and_orm_key(
self,
) -> list[tuple[Type["BaseCountryRatingComponent"], str]]:
return settings_traversal(settings, ["model", "orm_key"])

One possible way is to manually write everything we need
#classmethod
def settings_traversal(
cls,
settings: list[Union[ComponentSetting, CompositeComponentSetting]],
query_params: list[str],
) -> list[
Union[
str,
tuple[Type[BaseCountryRatingComponent], str],
tuple[str, str],
tuple[str, str, bool],
tuple[Union[str, bool, Type[BaseCountryRatingComponent]], ...],
]
]:
or simplifies the return types to the
#classmethod
def settings_traversal(
cls,
settings: list[Union[ComponentSetting, CompositeComponentSetting]],
query_params: list[str],
) -> list[Any]

Related

Typing an overloaded decorator wrapped in partial

I am trying to get the typing of an overloaded decorator right that gets wrapped by partial:
from functools import partial
from typing import Any, Callable, Optional, Union, overload
AnyCallable = Callable[..., Any]
class Wrapped:
def __init__(self, func: AnyCallable, foo: str, bar: bool) -> None:
pass
#overload
def create_wrapped(foo: str, func: AnyCallable) -> Wrapped:
...
#overload
def create_wrapped(foo: str, *, bar: bool = ...) -> Callable[[AnyCallable], Wrapped]:
...
def create_wrapped(
foo: str,
func: Optional[AnyCallable] = None,
*,
bar: bool = True,
) -> Union[Wrapped, Callable[[AnyCallable], Wrapped]]:
def wrapper(func_: AnyCallable) -> Wrapped:
return Wrapped(func_, foo, bar)
if func is None:
return wrapper
return wrapper(func)
baz = partial(create_wrapped, "baz")
#baz
def func_1() -> None:
pass
#baz(bar=False)
def func_2() -> None:
pass
The code is correct, but mypy gives
47: error: "Wrapped" not callable
which indicates that the actual argument types are lost when applying partial, since #baz(bar=False) should match the second overload as it's the same as #create_wrapped("baz", bar=False), which does work without an issue.
I'm not sure how else I could annotate this, in fact I couldn't come up with any way to make mypy not complain about this, even if I was fine with not having proper types for the decorator since in that case, I'd get an Untyped decorator makes function untyped error.
mypy does not currently correctly infer the type of a partially applied function: https://github.com/python/mypy/issues/1484.
You can work around it by casting the return of the partial call to a proper Protocol.
from functools import partial
from typing import Any, Callable, Optional, Protocol, Union, overload, cast
AnyCallable = Callable[..., Any]
class Wrapped:
def __init__(self, func: AnyCallable, foo: str, bar: bool) -> None:
pass
#overload
def create_wrapped(foo: str, func: AnyCallable) -> Wrapped:
...
#overload
def create_wrapped(foo: str, *, bar: bool = ...) -> Callable[[AnyCallable], Wrapped]:
...
def create_wrapped(
foo: str,
func: Optional[AnyCallable] = None,
*,
bar: bool = True,
) -> Union[Wrapped, Callable[[AnyCallable], Wrapped]]:
def wrapper(func_: AnyCallable) -> Wrapped:
return Wrapped(func_, foo, bar)
if func is None:
return wrapper
return wrapper(func)
class partial_create_wrapped(Protocol):
#overload
def __call__(self, *, bar: bool = ...) -> Callable[[AnyCallable], Wrapped]:
...
#overload
def __call__(self, func: AnyCallable) -> Wrapped:
...
baz = cast(partial_create_wrapped, partial(create_wrapped, "baz"))
#baz
def func_1() -> None:
pass
#baz(bar=False)
def func_2() -> None:
pass

Have a method in a subclass accept a subclass of an argument in its parents signature

I think it's easiest if I provide a PoC snippet instead of explaining it with words:
from abc import ABCMeta, abstractmethod
from typing import Any, Dict
class A:
a: int
class B(A):
b: str
class Getter:
#abstractmethod
def get(self, o: A) -> Dict[str, Any]:
pass
class BGetter(Getter):
def get(self, o: B) -> Dict[str, Any]:
return {
'a': o.a,
'b': o.b,
}
When running mypy on this code I get the following error:
Argument 1 of "get" is incompatible with supertype "Getter"; supertype defines the argument type as "A"
I understand that it wants the get method in BGetter to have the same signature as the parent one, but why if the argument is a subclass of A? Is there a way to tell mypy that this is OK and that it should accept subclasses for arguments for methods in subclasses?
I have stumbled across a solution, though it may be somewhat hacky for some people.
from abc import abstractmethod
from typing import Any, Generic, Protocol, TypeVar
class A:
a: int = 2
class B(A):
b: str = 'string'
T = TypeVar('T', contravariant=True)
class Getter(Generic[T], Protocol):
# abstractmethod
def get(self, o: T) -> dict[str, Any]:
pass
class AGetter(Getter[A]):
def get(self, o: A) -> dict[str, Any]:
return {
'a': o.a,
}
class BGetter(Getter[B]):
def get(self, o: B) -> dict[str, Any]:
return {
'a': o.a,
'b': o.b,
}
def fa(getter: Getter[A], o: A) -> dict[str, Any]:
return getter.get(o)
def fb(getter: Getter[B], o: B) -> dict[str, Any]:
return getter.get(o)
print(fa(AGetter(), A()))
print(fa(AGetter(), B()))
print(fb(BGetter(), B()))
print(fb(BGetter(), A())) # Expected mypy error
Everything but the last line will pass mypy. The last line is expected to error out because BGetter expects an instance of at least B. The only place where this falls short is the following:
def f(getter: Getter, o: A) -> dict[str, Any]:
return getter.get(o)
print(f(BGetter(), A()))
Mypy will not complain in this scenario since both arguments are the correct type, but this will lead to a runtime failure, since BGetter expects to find the b attribute in the provided class.

django-stubs: Missing type parameters for generic type "ModelSerializer"

I have
class AnimalSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
fields = "__all__"
Now i run mypy
[mypy]
# The mypy configurations: https://mypy.readthedocs.io/en/latest/config_file.html
python_version = 3.9
check_untyped_defs = True
# disallow_any_explicit = True
disallow_any_generics = True
disallow_untyped_calls = True
disallow_untyped_decorators = True
ignore_errors = False
ignore_missing_imports = True
implicit_reexport = False
strict_optional = True
strict_equality = True
no_implicit_optional = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
warn_unreachable = True
warn_no_return = True
mypy_path = /home/simha/app/backend_django/src
plugins =
mypy_django_plugin.main,
mypy_drf_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = petsproject.settings
(venv) $ mypy .
I get
error: Missing type parameters for generic type "ModelSerializer"
The stub file for serializers.ModelSerializer shows that it inherits from a generic base class BaseSerializer. This means that ModelSerializer is also generic:
# (Imports excluded for the sake of brevity)
_MT = TypeVar("_MT", bound=Model) # Model Type
# <-- snip -->
class ModelSerializer(Serializer, BaseSerializer[_MT]):
serializer_field_mapping: Dict[Type[models.Field], Type[Field]] = ...
serializer_related_field: Type[RelatedField] = ...
serializer_related_to_field: Type[RelatedField] = ...
serializer_url_field: Type[RelatedField] = ...
serializer_choice_field: Type[Field] = ...
url_field_name: Optional[str] = ...
instance: Optional[Union[_MT, Sequence[_MT]]] # type: ignore[override]
class Meta:
model: Type[_MT] # type: ignore
fields: Union[Sequence[str], Literal["__all__"]]
read_only_fields: Optional[Sequence[str]]
exclude: Optional[Sequence[str]]
depth: Optional[int]
extra_kwargs: Dict[str, Dict[str, Any]] # type: ignore[override]
def __init__(
self,
instance: Union[None, _MT, Sequence[_MT], QuerySet[_MT], Manager[_MT]] = ...,
data: Any = ...,
partial: bool = ...,
many: bool = ...,
context: Dict[str, Any] = ...,
read_only: bool = ...,
write_only: bool = ...,
required: bool = ...,
default: Union[Union[_MT, Sequence[_MT]], Callable[[], Union[_MT, Sequence[_MT]]]] = ...,
initial: Union[Union[_MT, Sequence[_MT]], Callable[[], Union[_MT, Sequence[_MT]]]] = ...,
source: str = ...,
label: str = ...,
help_text: str = ...,
style: Dict[str, Any] = ...,
error_messages: Dict[str, str] = ...,
validators: Optional[Sequence[Validator[_MT]]] = ...,
allow_null: bool = ...,
allow_empty: bool = ...,
): ...
def update(self, instance: _MT, validated_data: Any) -> _MT: ... # type: ignore[override]
def create(self, validated_data: Any) -> _MT: ... # type: ignore[override]
def save(self, **kwargs: Any) -> _MT: ... # type: ignore[override]
def to_representation(self, instance: _MT) -> Any: ... # type: ignore[override]
def get_field_names(self, declared_fields: Mapping[str, Field], info: FieldInfo) -> List[str]: ...
def get_default_field_names(self, declared_fields: Mapping[str, Field], model_info: FieldInfo) -> List[str]: ...
def build_field(
self, field_name: str, info: FieldInfo, model_class: _MT, nested_depth: int
) -> Tuple[Field, Dict[str, Any]]: ...
def build_standard_field(
self, field_name: str, model_field: Type[models.Field]
) -> Tuple[Field, Dict[str, Any]]: ...
def build_relational_field(
self, field_name: str, relation_info: RelationInfo
) -> Tuple[Type[Field], Dict[str, Any]]: ...
def build_nested_field(
self, field_name: str, relation_info: RelationInfo, nested_depth: int
) -> Tuple[Field, Dict[str, Any]]: ...
def build_property_field(self, field_name: str, model_class: _MT) -> Tuple[Field, Dict[str, Any]]: ...
def build_url_field(self, field_name: str, model_class: _MT) -> Tuple[Field, Dict[str, Any]]: ...
def build_unknown_field(self, field_name: str, model_class: _MT) -> NoReturn: ...
def include_extra_kwargs(
self, kwargs: MutableMapping[str, Any], extra_kwargs: MutableMapping[str, Any]
) -> MutableMapping[str, Any]: ...
def get_extra_kwargs(self) -> Dict[str, Any]: ...
def get_uniqueness_extra_kwargs(
self, field_names: Iterable[str], declared_fields: Mapping[str, Field], extra_kwargs: Dict[str, Any]
) -> Tuple[Dict[str, Any], Dict[str, HiddenField]]: ...
def _get_model_fields(
self, field_names: Iterable[str], declared_fields: Mapping[str, Field], extra_kwargs: MutableMapping[str, Any]
) -> Dict[str, models.Field]: ...
def get_unique_together_validators(self) -> List[UniqueTogetherValidator]: ...
def get_unique_for_date_validators(self) -> List[BaseUniqueForValidator]: ...
When run with the --disallow-any-generics option, MyPy will complain if you inherit from an unparameterised generic, as it is unclear whether you want your inherited class to also be considered generic, or whether you want it to be considered a more concrete version of the base class.
As you have the line model = Animal in the Meta class in your derived class, and the stub file annotates ModelSerializer.Meta.model as being of type Type[_MT], my guess is that you do not want want your AnimalSerializer class to be generic, and instead want it to be specific to an Animal class that is a subclass of Model. As such, you need to rewrite your AnimalSerializer class like this:
class AnimalSerializer(serializers.ModelSerializer[Animal]):
class Meta:
model = Animal
fields = "__all__"

Proper way to add type hints to a JSON object hook

Taking the example from the json module docs:
>>> def as_complex(dct):
... if '__complex__' in dct:
... return complex(dct['real'], dct['imag'])
... return dct
What would be the proper way to add type hints here? My naive approach with using a TypedDict and overloads fails:
from typing import Any, Dict, TypedDict, TypeVar, Union, overload
_T = TypeVar('_T', bound=Dict[str, Any])
class JSONDict(TypedDict):
__complex__: Any
real: float
imag: float
#overload
def as_complex(dct: JSONDict) -> complex: ...
#overload
def as_complex(dct: _T) -> _T: ...
def as_complex(dct: _T) -> Union[complex, _T]:
if '__complex__' in dct:
return complex(dct['real'], dct['imag'])
return dct
as mypy objects with:
main.py:14: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
main.py:19: error: Overloaded function implementation cannot satisfy signature 1 due to inconsistencies in how they use type variables
Playground gist, should you want to try the sample out in the browser.
Obviously the mypy treats JSONDict and _T as overlapped types, most likely due to Any in the TypeVar definition.
This reminds me of the following situations (it's just example):
class A: ...
class B: ...
class C: ...
class D: ...
#overload
def f(x: Union[A, B]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
#overload
def f(x: Union[B, C]) -> str: ...
def f(x): ...
We can either leave them overlapping or try to separate them. Currently, I can suggest two, rather ugly solutions.
from typing import Any, Dict, TypedDict, TypeVar, Union, overload, cast
_T = TypeVar('_T', bound=Dict[str, Any])
class JSONDict(TypedDict):
__complex__: Any
real: float
imag: float
#overload
def as_complex(dct: JSONDict) -> complex: ...
#overload
def as_complex(dct: _T) -> Union[_T, complex]: ...
def as_complex(dct: Union[_T, JSONDict]) -> Union[complex, _T]:
if '__complex__' in dct:
return complex(dct['real'], dct['imag'])
return cast(_T, dct)
Or try to separate
from typing import Any, Dict, TypedDict, TypeVar, Union, overload, cast
# narrowing the list of types excluding float
_T = TypeVar('_T', bound=Dict[str, Union[int, str, list, tuple, dict, set]])
class JSONDict(TypedDict):
__complex__: Any
real: float
imag: float
#overload
def as_complex(dct: JSONDict) -> complex: ...
#overload
def as_complex(dct: _T) -> _T: ...
def as_complex(dct: Union[_T, JSONDict]) -> Union[complex, _T]:
if '__complex__' in dct:
dct = cast(JSONDict, dct)
return complex(dct['real'], dct['imag'])
return cast(_T, dct)
Hopefully, someone will suggest a more elegant solution

How to specify alternative return type alongside with generic?

I have following function signature:
def get(key, default=None):
pass
I want to specify return type to be as default type, but if default is None then it should be str:
T = TypeVar('T')
def get(key: str, default: Optional[T]=None) -> T:
pass
^ this solves first problem, but I can't figure out how to tell linter that alternative return type should be str and not Any.
I also tried -> Union[str, T] and no luck.
As #jonrsharpe suggested I tried to use #overload decorator:
from typing import TypeVar, overload
T = TypeVar('T')
#overload
def get(key: str, default=None) -> str: ...
#overload
def get(key: str, default: T) -> T: ...
def get(key: str, default: T=None) -> T:
if default is None:
return ''
print(key)
return 2
def main():
a = get('key')
a = get('key', None)
a = get('key', 1)
a = get('key', 1.23)
a = get('key', b'fd')
a = get('key', 'fds')
if __name__ == '__main__':
main()
But because of default is None (by default) detected return type is now Union[str, T].
Final edit
Should be def get(key: str, default: Optional[T]=None) -> T + overloads. Now everything is fine.

Categories