Skip to content

Commit

Permalink
Remove TypeType exception for abstract instantiation
Browse files Browse the repository at this point in the history
If A is abstract, it's weird to me that we have a difference in the
following two calls:
```
from abc import abstractmethod, ABCMeta
class A(metaclass=ABCMeta):
    @AbstractMethod
    def __init__(self, a: int) -> None:
        pass

def test_a(A_t: type[A]) -> None:
    A_t(1)
    A(1)
```

Mypy tries to then enforce soundness by preventing you from passing `A`
to a parameter of `type[A]`. But this is very unpopular, since there
are legitimate uses of `A` that have nothing to do with instantiation.
See #4717

As mentioned in
https://discuss.python.org/t/compatibility-of-protocol-class-object-with-type-t-and-type-any/48442/2
I think we should switch to disallowing instantiation of `type[Proto]` and
`type[Abstract]`.

This also makes tackling `__init__` unsoundness more tractable. If
people want unsound `__init__`, they can use `Callable[..., P]`.
  • Loading branch information
hauntsaninja committed Nov 3, 2024
1 parent 1f200dd commit d72df74
Show file tree
Hide file tree
Showing 6 changed files with 20 additions and 30 deletions.
9 changes: 1 addition & 8 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1668,21 +1668,14 @@ def check_callable_call(
# An Enum() call that failed SemanticAnalyzerPass2.check_enum_call().
return callee.ret_type, callee

if (
callee.is_type_obj()
and callee.type_object().is_protocol
# Exception for Type[...]
and not callee.from_type_type
):
if callee.is_type_obj() and callee.type_object().is_protocol:
self.chk.fail(
message_registry.CANNOT_INSTANTIATE_PROTOCOL.format(callee.type_object().name),
context,
)
elif (
callee.is_type_obj()
and callee.type_object().is_abstract
# Exception for Type[...]
and not callee.from_type_type
and not callee.type_object().fallback_to_any
):
type = callee.type_object()
Expand Down
20 changes: 8 additions & 12 deletions test-data/unit/check-abstract.test
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ class C(B):
pass

def f(cls: Type[A]) -> A:
return cls() # OK
return cls() # E: Cannot instantiate abstract class "A" with abstract attribute "m"
def g() -> A:
return A() # E: Cannot instantiate abstract class "A" with abstract attribute "m"

Expand All @@ -196,7 +196,6 @@ f(B) # E: Only concrete class can be given where "Type[A]" is expected
f(C) # OK
x: Type[B]
f(x) # OK
[out]

[case testAbstractTypeInADict]
from typing import Dict, Type
Expand Down Expand Up @@ -229,15 +228,14 @@ class C(B):
pass

def f(cls: Type[A]) -> A:
return cls() # OK
return cls() # E: Cannot instantiate abstract class "A" with abstract attribute "m"

Alias = A
GoodAlias = C
Alias() # E: Cannot instantiate abstract class "A" with abstract attribute "m"
GoodAlias()
f(Alias) # E: Only concrete class can be given where "Type[A]" is expected
f(GoodAlias)
[out]

[case testInstantiationAbstractsInTypeForVariables]
# flags: --no-strict-optional
Expand All @@ -253,7 +251,7 @@ class C(B):
pass

var: Type[A]
var()
var() # E: Cannot instantiate abstract class "A" with abstract attribute "m"
if int():
var = A # E: Can only assign concrete classes to a variable of type "Type[A]"
if int():
Expand All @@ -262,7 +260,7 @@ if int():
var = C # OK

var_old = None # type: Type[A] # Old syntax for variable annotations
var_old()
var_old() # E: Cannot instantiate abstract class "A" with abstract attribute "m"
if int():
var_old = A # E: Can only assign concrete classes to a variable of type "Type[A]"
if int():
Expand All @@ -278,7 +276,7 @@ class D(A):
def __new__(cls, a=None) -> "D": ...
if int():
var = D # E: Can only assign concrete classes to a variable of type "Type[A]"
[out]


[case testInstantiationAbstractsInTypeForClassMethods]
from typing import Type
Expand All @@ -291,13 +289,12 @@ class Logger:
class C:
@classmethod
def action(cls) -> None:
cls() #OK for classmethods
cls() # E: Cannot instantiate abstract class "C" with abstract attribute "m"
Logger.log(cls) #OK for classmethods
@abstractmethod
def m(self) -> None:
pass
[builtins fixtures/classmethod.pyi]
[out]

[case testInstantiatingClassWithInheritedAbstractMethodAndSuppression]
from abc import abstractmethod, ABCMeta
Expand All @@ -324,7 +321,6 @@ class A(metaclass=ABCMeta):
@abstractmethod
def j(self): pass
a = A() # E: Cannot instantiate abstract class "A" with abstract attributes "a", "b", ... and "j" (7 methods suppressed)
[out]


-- Implementing abstract methods
Expand Down Expand Up @@ -1085,9 +1081,9 @@ my_abstract_types = {
reveal_type(my_concrete_types) # N: Revealed type is "builtins.dict[builtins.str, def () -> __main__.MyAbstractType]"
reveal_type(my_abstract_types) # N: Revealed type is "builtins.dict[builtins.str, def () -> __main__.MyAbstractType]"

a = my_concrete_types['A']()
a = my_concrete_types['A']() # E: Cannot instantiate abstract class "MyAbstractType" with abstract attribute "do"
a.do()
b = my_concrete_types['B']()
b = my_concrete_types['B']() # E: Cannot instantiate abstract class "MyAbstractType" with abstract attribute "do"
b.do()

c = my_abstract_types['A']() # E: Cannot instantiate abstract class "MyAbstractType" with abstract attribute "do"
Expand Down
3 changes: 2 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1526,7 +1526,8 @@ import typing
class C:
@classmethod
def foo(cls) -> None:
cls().bar()
c = cls() # E: Cannot instantiate abstract class "C" with abstract attribute "bar"
c.bar()
@abstractmethod
def bar(self) -> None:
pass
Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-functools.test
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,9 @@ class A(ABC):
def method(self) -> None: ...

def f1(cls: type[A]) -> None:
cls()
partial_cls = partial(cls)
partial_cls()
cls() # E: Cannot instantiate abstract class "A" with abstract attribute "method"
partial_cls = partial(cls) # E: Cannot instantiate abstract class "A" with abstract attribute "method"
partial_cls() # E: Cannot instantiate abstract class "A" with abstract attribute "method"

def f2() -> None:
A() # E: Cannot instantiate abstract class "A" with abstract attribute "method"
Expand Down
10 changes: 5 additions & 5 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -1603,7 +1603,7 @@ class C:
pass

def f(cls: Type[P]) -> P:
return cls() # OK
return cls() # E: Cannot instantiate protocol class "P"
def g() -> P:
return P() # E: Cannot instantiate protocol class "P"

Expand All @@ -1625,7 +1625,7 @@ class C:
pass

def f(cls: Type[P]) -> P:
return cls() # OK
return cls() # E: Cannot instantiate protocol class "P"

Alias = P
GoodAlias = C
Expand All @@ -1646,14 +1646,14 @@ class C:
pass

var: Type[P]
var()
var() # E: Cannot instantiate protocol class "P"
if int():
var = P # E: Can only assign concrete classes to a variable of type "Type[P]"
var = B # OK
var = C # OK

var_old = None # type: Type[P] # Old syntax for variable annotations
var_old()
var_old() # E: Cannot instantiate protocol class "P"
if int():
var_old = P # E: Can only assign concrete classes to a variable of type "Type[P]"
var_old = B # OK
Expand All @@ -1669,7 +1669,7 @@ class Logger:
class C(Protocol):
@classmethod
def action(cls) -> None:
cls() #OK for classmethods
cls() # E: Cannot instantiate protocol class "C"
Logger.log(cls) #OK for classmethods
[builtins fixtures/classmethod.pyi]

Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,7 @@ T = TypeVar('T', bound=HasX)
class Meta(type):
def do_x(cls: Type[T]) -> T:
cls.x
return cls()
return cls() # E: Cannot instantiate protocol class "HasX"

class Good(metaclass=Meta):
x: int
Expand Down

0 comments on commit d72df74

Please sign in to comment.