Skip to content

Commit

Permalink
Allow generic decorators on abstarct classes (#13398)
Browse files Browse the repository at this point in the history
Fixes #5374

As discussed in the issue, we should allow this common use case, although it may be technically unsafe (e.g. if one would instantiate a class in a class decorator body).
  • Loading branch information
ilevkivskyi committed Aug 12, 2022
1 parent 816aeb3 commit 52f1dd3
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 0 deletions.
8 changes: 8 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,11 @@ def __init__(
# of giving a note on possibly missing "await". It is used to avoid infinite recursion.
self.checking_missing_await = False

# While this is True, allow passing an abstract class where Type[T] is expected.
# although this is technically unsafe, this is desirable in some context, for
# example when type-checking class decorators.
self.allow_abstract_call = False

@property
def type_context(self) -> List[Optional[Type]]:
return self.expr_checker.type_context
Expand Down Expand Up @@ -2027,9 +2032,12 @@ def visit_class_def(self, defn: ClassDef) -> None:

# TODO: Figure out how to have clearer error messages.
# (e.g. "class decorator must be a function that accepts a type."
old_allow_abstract_call = self.allow_abstract_call
self.allow_abstract_call = True
sig, _ = self.expr_checker.check_call(
dec, [temp], [nodes.ARG_POS], defn, callable_name=fullname
)
self.allow_abstract_call = old_allow_abstract_call
# TODO: Apply the sig to the actual TypeInfo so we can handle decorators
# that completely swap out the type. (e.g. Callable[[Type[A]], Type[B]])
if typ.is_protocol and typ.defn.type_vars:
Expand Down
1 change: 1 addition & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1914,6 +1914,7 @@ def check_arg(
and (caller_type.type_object().is_abstract or caller_type.type_object().is_protocol)
and isinstance(callee_type.item, Instance)
and (callee_type.item.type.is_abstract or callee_type.item.type.is_protocol)
and not self.chk.allow_abstract_call
):
self.msg.concrete_only_call(callee_type, context)
elif not is_subtype(caller_type, callee_type, options=self.chk.options):
Expand Down
12 changes: 12 additions & 0 deletions test-data/unit/check-abstract.test
Original file line number Diff line number Diff line change
Expand Up @@ -1018,3 +1018,15 @@ d = my_abstract_types['B']() # E: Cannot instantiate abstract class "MyAbstract
d.do()

[builtins fixtures/dict.pyi]

[case testAbstractClassesWorkWithGenericDecorators]
from abc import abstractmethod, ABCMeta
from typing import Type, TypeVar

T = TypeVar("T")
def deco(cls: Type[T]) -> Type[T]: ...

@deco
class A(metaclass=ABCMeta):
@abstractmethod
def foo(self, x: int) -> None: ...

0 comments on commit 52f1dd3

Please sign in to comment.