-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
class object typed with Type[SomeAbstractClass] should not complain about instantiation #1843
Comments
Wouldn't it be better to have |
No, I don't think the little bit of extra safety is worth the cost of a new
keyword and explaining the difference.
…--Guido (mobile)
|
Let's do this. We have such code in our codebase that would be flagged as an error except for another bug (#2430). |
What exactly should be done? What differentiates |
Consider: from typing import Type
from abc import abstractmethod
class A:
@abstractmethod
def m(self) -> None: pass
def f(cls: Type[A]) -> A:
return cls()
def g() -> A:
return A() Currently both f() and g() have the same error:
I would want f() to be error-free but g() should still have the error. |
I understand the examples, but not the general rule. What should happen in the following? class A:
@abstractmethod
def m(self) -> None: pass
A1 = A
A1() # error? |
I don't really care what happens in that case. If it's considered an alias
for A, it should be an error; if it's seen as a variable whose type is
Type[A], then it should pass. Perhaps the implementation should just look
whether it's represented as a def with is_type_obj() true or as a TypeType
instance?
|
Fixes #1843 (It was also necessary to fix few minor things to make this work correctly) The rules are simple, assuming we have: ```python class A: @AbstractMethod def m(self) -> None: pass class C(A): def m(self) -> None: ... ``` then ```python def fun(cls: Type[A]): cls() # OK fun(A) # Error fun(C) # OK ``` The same applies to variables: ```python var: Type[A] var() # OK var = A # Error var = C # OK ``` Also there is an option for people who want to pass abstract classes around: type aliases, they work as before. For non-abstract ``A``, ``Type[A]`` also works as before. My intuition why you opened #1843 is when someone writes annotation ``Type[A]`` with an abstract ``A``, then most probably one wants a class object that _implements_ a certain protocol, not just inherits from ``A``. NOTE: As discussed in python/peps#224 this behaviour is good for both protocols and usual ABCs.
I'm totally lost with all the comments about this subject. So how typing correctly an abstract class? def get_concrete[T: ABC](interface: Type[T]) -> Type[T]: ... Currently, mypy forbids to pass an abstract class as argument of this method. This is very annoying, and one of the worst specifications ever made. :/ |
@vtgn, the typing spec doesn't specifically state whether an abstract class is assignable to Here's a variant of your code that works with mypy: def get_concrete[T: type[ABC]](interface: T) -> T: ... |
This is the only case I haven't tested in this signature. Thanks for this tips! And the worst in there, is that the linters type automatically a variable storing an abstract class AC inside a method by type[AC], which is inconsistent with this decision. class AbstractClass(ABC):
@staticmethod
@abstractmethod
def func(): ...
def meth():
untypedAC = AbstractClass # OK, but untypedAC is considered as type[AbstractClass] by the linters
typedAC: type[AbstractClass] = AbstractClass # NOK, the linters refuses AbstractClass to be set into this variable, which is inconsistent with the type given to untypedAC |
Finally, there is still a typing issue, but inside the implementation of the method. def get_concrete[T: Type[ABC]](module_name: str, class_name: str, interface: T) -> Optional[T]:
module: Optional[ModuleType] = None
the_class: Optional[object] = None
result: Optional[T] = None
try:
module = import_module(module_name)
except ValueError:
pass
else:
if hasattr(module, class_name):
the_class = getattr(module, class_name)
if inspect.isclass(the_class):
if not inspect.isabstract(the_class):
if issubclass(the_class, interface):
result = the_class # <-- Error of typing for linters
return result
Is casting the only one solution? :/
However, even if I fix this problem by using the cast, there is still a new typing problem when we want to store the result of the method: untyped_result = get_concrete("module_name", "ClassName", "Interface") # OK, considered as Type[Interface] by the linters
typed_result: Type[Interface] = get_concrete("module_name", "ClassName", "Interface") # mypy error: Can only assign concrete classes to a variable of type "type[BaseModelFactoryInterface]" mypy(type-abstract) I don't understand how this error is possible here, because the method's signature indicates it returns a Type[Interface] value, which is considered as a concrete class by mypy. |
Another typing issue about the same subject: class A(ABC):
@staticmethod
@abstractmethod
def static_method() -> int:
pass
class B(A):
@staticmethod
def static_method() -> int:
return 1
class C(B):
@staticmethod
def static_method() -> int:
return 2
def meth[T: Type[ABC]](interface: T, classes: list[T]) -> T:
if inspect.isabstract(interface):
c = random.choice(classes)
return c
raise ValueError()
result = meth(A, [B, C]) # mypy Error : Cannot infer type argument 1 of "meth" mypy (misc) The problem disappears if the class C inherits from A instead of B. |
I wish this code would work:
The obvious intention is to pass f() a concrete subclass of A.
(UPDATE: My real-world use case is a bit more involved, in asyncio there are some classes that implement an elaborate ABC, and I'd like to just say
SelectorEventLoop = ... # type: Type[AbstractEventLoop]
rather than having to write out the class definition with all 38 concrete method definitions.)The text was updated successfully, but these errors were encountered: