Dynamically generate mypy-compliant property setters - python

I am trying to declare a base class with certain attributes for which the (very expensive) calculation differs depending on the subclass, but that accepts injecting the value if previously calculated
class Test:
_value1: int | None = None
_value2: str | None = None
_value3: list | None = None
_value4: dict | None = None
#property
def value1(self) -> int:
if self._value1 is None:
self._value1 = self._get_value1()
return self._value1
#value1.setter
def value1(self, value1: int) -> None:
self._value1 = value1
def _get_value1(self) -> int:
raise NotImplementedError
class SubClass(Test):
def _get_value1(self) -> int:
time.sleep(1000000)
return 1
instance = SubClass()
instance.value1 = 1
print(instance.value1) # doesn't wait
As you can see it becomes very verbose, with every property having three different functions associated to it.
Is there a way to dynamically declare at the very least the setter, so that mypy knows it's always the same function but with proper typing? Or in general, is there a more concise way to declare this kind of writable property for which the underlying implementation must be implemented by the base class, in bulk?
Declaring __setattr__ doesn't seem to be viable, because just having __setattr__ declared tricks mpy into thinking I can just assign any value to anything else that's not overloaded, while I still want errors to show up in case I'm trying to assign the wrong attributes. It also doesn't fix that I still need to declare setters, otherwise it thinks the value is immutable.

Instead of inheriting a bunch of pre-defined properties from a base class, I would move all the logic surrounding each property into a custom descriptor class. (The following assumes Python 3.11 and mypy version 1.0.0.)
from typing import TypeVar, Generic, Callable, Type, Optional, Self, Union, overload
T = TypeVar('T')
C = TypeVar('C')
class Descriptor(Generic[C, T]):
def __init__(self, f: Callable[[C], T]):
self.getter = f
def __set_name__(self, owner: C, name: str):
self.private_name = "_" + name
self.public_name = name
#overload
def __get__(self: Self, obj: C, objtype: Optional[Type[C]]) -> T:
...
#overload
def __get__(self: Self, obj: None, objtype: Type[C]) -> Self:
...
def __get__(self: Self, obj: Optional[C], owner: Optional[Type[C]] = None) -> Union[Self, T]:
if obj is None:
return self
if getattr(obj, self.private_name, None) is None:
init_value = self.getter(obj)
self.__set__(obj, init_value)
return getattr(obj, self.private_name)
def __set__(self, obj: C, value: T):
setattr(obj, self.private_name, value)
Then you can define each descriptor similar to how you would define a property, by decorating the function that will return the value an initial value if none has yet been defined.
class Test:
#Descriptor
def value1(self) -> int:
time.sleep(10000000)
return 1
#Descriptor
def value2(self) -> str:
return "foo"
#Descriptor
def value3(self) -> list:
return [1, 2, 3]
#Descriptor
def value4(self) -> dict:
return dict(foo=9)
The descriptor class is generic in both the class it will be used in and the type of the wrapped value.
x = Test()
reveal_type(x.value1) # int
reveal_type(Test.value1) # Descriptor[Test, int]
x.value1 = 3 # OK
x.value1 = "foo" # error, x.__set__ expects an int, not a str

If you wanted to simply omit writing #property.setter (this part)
#value1.setter
def value1(self, value1: int) -> None:
self._value1 = value1
one possible implementation would be to subclass property to automatically implement a __set__ method which matches the behaviour specified in your example:
from __future__ import annotations
import typing as t
if t.TYPE_CHECKING:
import collections.abc as cx
_ValueT = t.TypeVar("_ValueT")
class settable(property, t.Generic[_ValueT]):
fget: cx.Callable[[t.Any], _ValueT]
def __init__(self, fget: cx.Callable[[t.Any], _ValueT], /) -> None:
super().__init__(fget)
if t.TYPE_CHECKING:
# Type-safe descriptor protocol for property retrieval methods (`__get__`)
# see https://docs.python.org/3/howto/descriptor.html
# These are under `typing.TYPE_CHECKING` because we don't need
# to modify their implementation from `builtins.property`, but
# just need to add type-safety.
#t.overload # type: ignore[override, no-overload-impl]
def __get__(self, instance: None, Class: type, /) -> settable[_ValueT]:
"""
Retrieving a property from on a class (`instance: None`) retrieves the
property object (`settable[_ValueT]`)
"""
#t.overload
def __get__(self, instance: object, Class: type, /) -> _ValueT:
"""
Retrieving a property from the instance (all other `typing.overload` cases)
retrieves the value
"""
def __set__(self, instance: t.Any, value: _ValueT) -> None:
"""
Type-safe setter method. Grabs the name of the function first decorated with
`#settable`, then calls `setattr` on the given value with an attribute name of
'_<function name>'.
"""
setattr(instance, f"_{self.fget.__name__}", value)
Here's a demonstration of type-safety:
import time
class Test:
_value1: int | None = None
_value2: str | None = None
_value3: list | None = None
_value4: dict | None = None
#settable
def value1(self) -> int:
if self._value1 is None:
self._value1 = self._get_value1()
return self._value1
def _get_value1(self) -> int:
raise NotImplementedError
class SubClass(Test):
def _get_value1(self) -> int:
time.sleep(1000000)
return 1
>>> instance: SubClass = SubClass()
>>> instance.value1 = 1 # OK
>>>
>>> if t.TYPE_CHECKING:
... reveal_type(instance.value1) # mypy: Revealed type is "builtins.int"
...
>>> print(instance.value1)
1
>>> instance.value1 = "1" # mypy: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]
>>> SubClass.value1 = 1 # mypy: Cannot assign to a method [assignment]
... # mypy: Incompatible types in assignment (expression has type "int", variable has type "settable[int]") [assignment]

Related

Type annotation in a filter() function over a custom generator

could you help me understand why I am getting the TypeError: 'type' object is not subscriptable error with the code below?
Maybe I'm getting this wrong, but as I understood the Color type annotation in the filter() function is saying that the function will result in an Iterable of Color , which is exactly what I want. But when I try to annotate the function I get the error. ( but the waty, how come a type annotation is preventing the program to run? I thought that type hints in in Python would just matter inside your IDE, not in runtime).
Any light on this would be much appreciated.
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TypeVar, Any, Generic, Iterator, Iterable
from abc import ABC, abstractmethod
from dataclasses import dataclass
T = TypeVar('T', bound=Any)
I = TypeVar('I', bound=Any)
class AbstractGenerator(ABC, Iterator[T], Generic[T, I]):
def __init__(self):
super().__init__()
self._items = None
self._next_item = None
#property
def items(self) -> Any:
return self._items
#items.setter
def items(self, items: Any) -> AbstractGenerator:
self._items = items
return self
#property
def next_item(self) -> Any:
return self._next_item
#next_item.setter
def next_item(self, next_item: Any) -> AbstractGenerator:
self._next_item = next_item
return self
#abstractmethod
def __len__(self) -> int:
pass
#abstractmethod
def __iter__(self) -> Iterable[T]:
pass
#abstractmethod
def __next__(self) -> Iterable[T]:
pass
#abstractmethod
def __getitem__(self, id: I) -> Iterable[T]:
pass
ColorId = int
#dataclass(frozen=True)
class Color:
id: ColorId
name: str
class MyColorsGenerator(AbstractGenerator[Color, int]):
def __init__(self):
super().__init__()
self._colors: list[Color] = []
self._next_color_index: int = 0 #None
#property
def colors(self) -> list[Color]:
return self._colors
#colors.setter
def colors(self, colors: list[Color]) -> MyColorsGenerator:
self._colors = colors
return self
#property
def next_color_index(self) -> int:
return self._next_color_index
#next_color_index.setter
def next_color_index(self, next_color_index: int) -> MyColorsGenerator:
self._next_color_index = next_color_index
return self
def add_color(self, color: Color) -> MyColorsGenerator:
self.colors.append(color)
return self
def __len__(self) -> int:
return len(self.colors)
def __iter__(self) -> Iterable[Color]:
return self
def __next__(self) -> Iterable[Color]:
if self.next_color_index < len(self.colors):
self.next_color_index += 1
return self.colors[self.next_color_index - 1]
else:
raise StopIteration
def __getitem__(self, id: ColorId) -> Iterable[Color]:
return list(filter[Color](lambda color: color.id == id, self.colors))
colors_generator: MyColorsGenerator = MyColorsGenerator()
colors_generator \
.add_color(Color(id=0, name="Blue")) \
.add_color(Color(id=1, name="Red")) \
.add_color(Color(id=2, name="Yellow")) \
.add_color(Color(id=3, name="Green")) \
.add_color(Color(id=4, name="White")) \
.add_color(Color(id=5, name="Black"))
# This results in: TypeError: 'type' object is not subscriptable
#colors: Optional[list[Color]] = list(filter[Color](lambda color: color.id == 4, colors_generator))
# This works, notice the only thing I did was to remove the type annotation for the expected generic type ([Color])
colors: Optional[list[Color]] = list(filter(lambda color: color.id == 4, colors_generator))
print(colors)
The issue is that generics aren't a language-level addition, but a library one. Specifying the generic type parameters actually employs the same [] operator you use for item access in collections, except it is defined on the metaclass. For this reason the generics syntax originally only worked with specific classes in the typing module (typing.List[int], typing.Dict[str, str], etc.). Since python3.9, however, some common classes from the standard library have been extended to support the same operation, for brevity, like list[int], dict[str, str]. This is still NOT a language feature, and most classes in the standard library do not implement it. Moreover, as you've rightfully noticed, these annotations carry (almost) no meaning for the interpreter, and are (mostly) just there for the ide. Among other things, that implies that you don't instantiate generic classes as specialized generics (list() is correct, list[int]() is legal, but pointless and considered a bad practice). filter is a class in the standard library, which does not provide the generic-aliasing [] operation, so you get the error that applying it is not implemented ("'type' object is not subscriptable", filter is an instance of type, and [] is the subscription operator). Python as the language does not understand the concept of a generic, and so it cannot give you a better error message like "'filter' is not a generic class". Even if it was, however, you shouldn't have invoked it this way.
A special note should be made about generic functions. They CANNOT be explicitly supplied with generic parameters. So, if instead of filter we were talking about some function like:
T = typing.TypeVar("T")
def my_filter(f: typing.Callable[[T], bool], seq: list[T]) -> list[T]:
...
, there would have been no way to explicitly tell you're interested in my_filter[Color].
TL;DR: filter is not a generic class in terms of type annotations, so it does not support the [] operation

Injecting a callable object into a class as a method

It is possible to inject a function into a class like this:
class MainClass:
...
def simple_injected_func(self: MainClass, arg: str) -> None:
print(f"simple_injected_func({arg})")
MainClass.simple_injected_func = simple_injected_func
main_object = MainClass()
main_object.simple_injected_func("arg")
# outputs: simple_injected_func(arg)
Furthermore it is possible to make an object callable like this
class SimpleCallableClass:
def __call__(self, arg: str) -> None:
print(f"SimpleCallableClass()({arg})")
simple_callable_object = SimpleCallableClass()
simple_callable_object("arg")
# outputs: SimpleCallableClass()(arg)
I now want to combine these two things and inject a callable class/object as a function into another class while keeping access to object variables and methods of both the CallableClass as well as the MainClass. (Internally I want to use this to effectively implement method inheritance and inject those methods into a class from another file)
from inspect import signature
class CallableClass:
def __call__(self_, self: MainClass, arg: str) -> None:
print(f"CallableClass()({arg})")
callable_object = CallableClass()
MainClass.callable_object = callable_object
main_object = MainClass()
print(signature(simple_injected_func))
# outputs: (self: __main__.MainClass, arg: str) -> None
print(signature(callable_object))
# outputs: (self: __main__.MainClass, arg: str) -> None
print(signature(main_object.simple_injected_func))
# outputs: (arg: str) -> None
print(signature(main_object.callable_object))
# outputs: (self: __main__.MainClass, arg: str) -> None
main_object.simple_injected_func("my arg")
# outputs: simple_injected_func(my arg)
main_object.callable_object("my arg")
# Traceback (most recent call last):
# main_object.callable_object("my arg")
# TypeError: CallableClass.__call__() missing 1 required positional argument: 'arg'
Why does the second self not get correctly stripped in case of the callable object? Is there some way of achieving this?
When methods of an instance are accessed, Python performs "binding", i.e. it creates a bound method. See here:
>>> class Class:
... def method(self, x):
... return x
...
>>>
>>> instance = Class()
>>> Class.method
<function Class.method at 0x7fa688037158>
>>> instance.method
<bound method Class.method of <__main__.Class object at 0x7fa688036278>>
The binding is done because methods are implemented as descriptors.
You can also implement your callable as a descriptor if you want to have that behaviour.
In short, you would have to implement a class with at least a __get__ method. That __get__ method will be called when either Class.method or instance.method is evaluated. It should return the callable (which should be a different one depending on whether there is an instance or not).
BTW, to actually bind a method to an instance, it is simplest to use functors.partial:
bound_method = functors.partial(method, instance)
All summed up:
class Callable:
def __call__(self, instance, arg):
print(f"Callable()(arg)")
class Descriptor:
def __init__(self, callable):
self._callable = callable
def __get__(self, instance, owner):
if instance is None:
return self._callable
else:
return functools.partial(self._callable, instance)
class Class:
pass
Class.method = Descriptor(Callable())
And then:
>>> signature(Class.method)
<Signature (instance, arg)>
>>> signature(Class().method)
<Signature (arg)>

Can type hinting infer type of class attribute from another?

There are subclasses that have the class attribute matcher_function set to a function. During instantiation that function is called and sets another attribute matcher. In all cases the return object of the matcher_function is what matcher gets set to.
Is it possible to create a type hint in the base class BaseResolution that would allow both mypy and pycharm to properly infer matcher is the return value of matcher_function?
# contains_the_text.py
from hamcrest import contains_string
from hamcrest.library.text.stringcontains import StringContains
from .base_resolution import BaseResolution
class ContainsTheText(BaseResolution):
matcher: StringContains # <-- this is what I'm curious can be inferred
matcher_function = contains_string # <-- this function returns an instance
# of `StringContains`
# it would be wonderful if
# a. mypy could detect that the matcher type hint is correct based on 'matcher_function'
# b. infer what the type is when the hint is not present in the subclasses.
# base_resolution.py
from typing import Any, Callable, TypeVar
from hamcrest.core.base_matcher import BaseMatcher, Matcher
from hamcrest.core.description import Description
T = TypeVar("T")
class BaseResolution(BaseMatcher[T]):
matcher: Matcher
matcher_function: Callable
expected: Any
def __init__(self, *args: object, **kwargs: object) -> None:
cls = self.__class__
if args and kwargs:
self.expected = (args, kwargs)
self.matcher = cls.matcher_function(*args, **kwargs)
elif args:
self.expected = args if len(args) > 1 else args[0]
self.matcher = cls.matcher_function(*args)
elif kwargs:
self.expected = kwargs
self.matcher = cls.matcher_function(**kwargs)
else:
self.expected = True
self.matcher = cls.matcher_function()
def _matches(self, item: T) -> bool:
"""passthrough to the matcher's method."""
return self.matcher.matches(item)
# truncated a whole bunch of other methods...
While these are likely better typehints, they didn't seem to do the trick.
class BaseResolution(BaseMatcher[T]):
matcher: Matcher[T]
matcher_function: Callable[..., Matcher[T]]
I know you can do something sorta similar using TypeVar(bound=) which will infer function return types based on arguments passed in. But I can't seem to figure out how (if even possible) to apply that at a class attribute level.
from typing import Type, TypeVar, Generic
T = TypeVar("T")
class Foo(Generic[T]):
...
class FooBar(Foo[T]):
...
F = TypeVar("F", bound=Foo) # any instance subclass of Foo
class MyClass(FooBar):
...
def bar(f: Type[F]) -> F:
...
def baz(f: Type[Foo]) -> Foo:
...
objx = bar(MyClass)
objy = baz(MyClass)
reveal_type(objx) # -> MyClass*
reveal_type(objy) # -> Foo[Any]
Given the above example I tried the following but that clearly isn't right.
F = TypeVar("F", bound=Matcher)
class BaseResolution(BaseMatcher[T]):
matcher: F
matcher_function: Callable[..., F]
# mypy returns
# Type variable "base_resolution.F" is unbound

How can I use MyPy to overload the __init__ method to adjust a getter's return value?

Let's say I have a class like this (pseudo-code, please ignore the odd db structure):
class Blog():
title = StringProperty()
comments = StringProperty(repeated=True)
I want to type check StringProperty such that Blog().title returns a str type, and Blog().comments returns a List[str] type. MyPy mentions that something like this is possible by dynamically typing the __init__ method.
Here's what I've tried:
U = TypeVar('U', bound=StringProperty)
V = TypeVar('V', bound=StringProperty)
class StringProperty(Property[T]):
#overload
def __init__(self: StringProperty[U], repeated: Literal[False]=False, **kwargs) -> None: ...
#overload
def __init__(self: StringProperty[V], repeated: Literal[True]=True, **kwargs) -> None: ...
#overload
def __get__(self: StringProperty[U], instance, cls) -> str: ...
#overload
def __get__(self: StringProperty[V], instance, cls) -> List[str]: ...
def __set__(self, instance, value: Optional[Union[str, List[str]]]) -> None: ...
However, this throws an error that the second __get__ type signature will never be matched. How can I set MyPy to know the return value of the StringProperty.__get__ method dynamically by whether the repeated property is True or False?
__init__ can be overloaded. self will become the given type.
TypeVar needs to become some kind of real type during type analysis. It can't stay as T or U or V. It must be filled in with a type like str or Literal["foo"].
from __future__ import annotations
from typing import TypeVar, overload, Literal, Generic
_GetReturnT = TypeVar('_GetReturnT', str, list[str], str | list[str])
class StringProperty(Generic[_GetReturnT]):
# Handles the default value case too.
#overload
def __init__(self: StringProperty[str], repeated: Literal[False]=False, **kwargs) -> None: ...
#overload
def __init__(self: StringProperty[list[str]], repeated: Literal[True], **kwargs) -> None: ...
# Callers won't always pass a literal bool right at the call site. The bool
# could come from somewhere far. Then we can't know what exactly get()
# will return.
#overload
def __init__(self: StringProperty[str | list[str]], repeated: bool, **kwargs) -> None: ...
def __init__(self, repeated: bool = False, **kwargs) -> None:
self._repeated = repeated
def get(self) -> _GetReturnT:
if self._repeated:
return ["Hello", "world!"]
else:
return "just one string"
default = StringProperty() # StringProperty[str]
default_get = default.get() # str
false_literal = StringProperty(repeated=False) # StringProperty[str]
false_literal_get = false_literal.get() # str
true_literal = StringProperty(repeated=True) # StringProperty[list[str]]
true_literal_get = true_literal.get() # list[str]
import random
some_bool = random.choice([True, False]) # bool
unknown_bool = StringProperty(repeated=some_bool) # StringProperty[str | list[str]]
unknown_bool_get = unknown_bool.get() # str | list[str]
reveal_locals()
# error: Value of type variable "_GetReturnT" of "StringProperty" cannot be "int"
#
# This error happens because we limited _GetReturnT's possible types in
# TypeVar(). If we didn't limit the types, users could accidentally refer to a
# type in an annotation that's impossible to instantiate.
def some_user_function(prop: StringProperty[int]) -> None:
prop.get()
Note that setting and reading self._repeated does not aid in typing here in any way. StringProperty gets its type only from what types were passed to the constructor. If someone runs false_literal._repeated = True, then false_literal.get() would return ["Hello", "world!"], and the typing information is wrong.
Using str or list[str] for StringProperty's type here was convenient. But the type can be less specific for weirder classes. Here we could've used Literal[True], Literal[False], and Literal[True] | Literal[False] to represent the quality of being repeated. Then get() would need overrides based on self to determine the return type.
_T = TypeVar('_T',
Literal["NOT_REPEATED"],
Literal["REPEATED"],
Literal[MyEnum.AMAZING],
Literal[MyEnum.KINDA_OK_I_GUESS])
# For brevity I don't show Unions in this example, but you'd
# need them for a class that works properly.
class StringProperty(Generic[_T]):
#overload
def __init__(self: StringProperty[Literal["NOT_REPEATED"]],
repeated: Literal[False]) -> None: ...
#overload
def __init__(self: StringProperty[Literal["REPEATED"]],
repeated: Literal[True]) -> None: ...
def __init__(self, repeated: bool) -> None:
self._repeated = repeated
#overload
def get(self: StringProperty[Literal["NOT_REPEATED"]]) -> str: ...
#overload
def get(self: StringProperty[Literal["REPEATED"]]) -> list[str]: ...
def get(self) -> str | list[str]:
if self._repeated:
return ["Hello", "world!"]
else:
return "just one string"
I also had to overload get in #Slix first example:
from __future__ import annotations
from typing import TypeVar, overload, Literal, Generic
_GetReturnT = TypeVar('_GetReturnT', str, list[str], str | list[str])
class StringProperty(Generic[_GetReturnT]):
# Handles the default value case too.
#overload
def __init__(self: StringProperty[str], repeated: Literal[False]=False, **kwargs) -> None: ...
#overload
def __init__(self: StringProperty[list[str]], repeated: Literal[True], **kwargs) -> None: ...
# Callers won't always pass a literal bool right at the call site. The bool
# could come from somewhere far. Then we can't know what exactly get()
# will return.
#overload
def __init__(self: StringProperty[str | list[str]], repeated: bool, **kwargs) -> None: ...
def __init__(self, repeated: bool = False, **kwargs) -> None:
self._repeated = repeated
#overload
def get(self: StringProperty[str]) -> str:
...
#overload
def get(self: StringProperty[list[str]]) -> list[str]:
...
#overload
def get(self: StringProperty[str | list[str]]) -> str | list[str]:
...
def get(self) -> str | list[str]:
if self._repeated:
return ["Hello", "world!"]
else:
return "just one string"
default = StringProperty() # StringProperty[str]
default_get = default.get() # str
false_literal = StringProperty(repeated=False) # StringProperty[str]
false_literal_get = false_literal.get() # str
true_literal = StringProperty(repeated=True) # StringProperty[list[str]]
true_literal_get = true_literal.get() # list[str]
import random
some_bool = random.choice([True, False]) # bool
unknown_bool = StringProperty(repeated=some_bool) # StringProperty[str | list[str]]
unknown_bool_get = unknown_bool.get() # str | list[str]
reveal_locals()
# error: Value of type variable "_GetReturnT" of "StringProperty" cannot be "int"
#
# This error happens because we limited _GetReturnT's possible types in
# TypeVar(). If we didn't limit the types, users could accidentally refer to a
# type in an annotation that's impossible to instantiate.
def some_user_function(prop: StringProperty[int]) -> None:
prop.get()
> venv/bin/mypy --version
mypy 0.991 (compiled: yes)
> venv/bin/mypy field.py
field.py:57: note: Revealed local types are:
field.py:57: note: default: field.StringProperty[builtins.str]
field.py:57: note: default_get: builtins.str
field.py:57: note: false_literal: field.StringProperty[builtins.str]
field.py:57: note: false_literal_get: builtins.str
field.py:57: note: some_bool: builtins.bool
field.py:57: note: true_literal: field.StringProperty[builtins.list[builtins.str]]
field.py:57: note: true_literal_get: builtins.list[builtins.str]
field.py:57: note: unknown_bool: field.StringProperty[Union[builtins.str, builtins.list[builtins.str]]]
field.py:57: note: unknown_bool_get: Union[builtins.str, builtins.list[builtins.str]]
field.py:64: error: Value of type variable "_GetReturnT" of "StringProperty" cannot be "int" [type-var]
field.py:65: error: Invalid self argument "StringProperty[int]" to attribute function "get" with type "Callable[[StringProperty[str]], str]" [misc]
Found 2 errors in 1 file (checked 1 source file)

Inherit generic type in python 3 with typing

I'm doing some experiments with typing in Python 3.6 and mypy. I want to design an entity class that can be instantiated in two ways:
By the use of an ordinary initializer (p = Person(name='Hannes', age=27))
Statically from a state object (p = Person.from_state(person_state)).
The Entity class, from which Person derives, has the state class as a generic parameter. However, when validating the code with mypy, I receive an error that Person.from_state doesn't pick up the state type from the class it inherits from:
untitled2.py:47: error: Argument 1 to "from_state" of "Entity" has incompatible type "UserState"; expected "StateType"
I thought that by inheriting from Entity[UserState], StateType would be bound to UserState and the method signatures in the child classes would update accordingly.
This is the full code. I have marked the line where I suspect I'm doing things wrong with ?????. Line 47 is almost at the bottom and marked in the code.
from typing import TypeVar, Generic, NamedTuple, List, NewType
EntityId = NewType('EntityId', str)
StateType = TypeVar('StateType')
class Entity(Generic[StateType]):
id: EntityId = None
state: StateType = None
#classmethod
def from_state(cls, state: StateType): # ?????
ret = object.__new__(cls)
ret.id = None
ret.state = state
return ret
def assign_id(self, id: EntityId) -> None:
self.id = id
class UserState(NamedTuple):
name: str
age: int
class User(Entity[UserState]):
def __init__(self, name, age) -> None:
super().__init__()
self.state = UserState(name=name, age=age)
#property
def name(self) -> str:
return self.state.name
#property
def age(self) -> int:
return self.state.age
def have_birthday(self) -> None:
new_age = self.state.age+1
self.state = self.state._replace(age=new_age)
# Create first object with constructor
u1 = User(name='Anders', age=47)
# Create second object from state
user_state = UserState(name='Hannes', age=27)
u2 = User.from_state(user_state) # Line 47
print(u1.state)
print(u2.state)
This was a bug in mypy that was fixed in mypy 0.700. As several people in the comments noted, that line of code validates fine in newer versions.
Note that in newer versions of mypy, the code in the question has a different problem:
main.py:8: error: Incompatible types in assignment (expression has type "None", variable has type "EntityId")
main.py:9: error: Incompatible types in assignment (expression has type "None", variable has type "StateType")
But that's outside the scope of the question and up to you to resolve however you'd like.
I know this question is a little old, but just for future reference:
from __future__ import annotations
from typing import NamedTuple, Optional, Type
class UserState(NamedTuple):
name: str
age: int
class Entity:
id: Optional[str]
state: UserState
#classmethod
def from_state(cls: Type[Entity], state: UserState) -> Entity:
entity_from_state: Entity = object.__new__(cls)
entity_from_state.id = None
entity_from_state.state = state
return entity_from_state
def assign_id(self, id: str) -> None:
self.id = id
class User(Entity):
def __init__(self, name: str, age: int) -> None:
self.state = UserState(name=name, age=age)
#property
def name(self) -> str:
return self.state.name
#property
def age(self) -> int:
return self.state.age
def have_birthday(self) -> None:
new_age = self.state.age + 1
self.state = self.state._replace(age=new_age)
# Create first object with constructor
u1 = User(name="Anders", age=47)
# Create second object from state
user_state = UserState(name="Hannes", age=27)
u2 = User.from_state(user_state) # Line 47
print(u1.state)
print(u2.state)

Categories