Skip to content

Commit

Permalink
Type checking of class decorators (#4544)
Browse files Browse the repository at this point in the history
It checks that the calls are well-type and that applying the decorators works.
It does not check/apply the end result.

Helps with #3135
  • Loading branch information
euresti authored and ilevkivskyi committed Feb 9, 2018
1 parent dcb85ea commit 46ecea5
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 2 deletions.
25 changes: 24 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from mypy.sametypes import is_same_type, is_same_types
from mypy.messages import MessageBuilder, make_inferred_type_note
import mypy.checkexpr
from mypy.checkmember import map_type_from_supertype, bind_self, erase_to_bound
from mypy.checkmember import map_type_from_supertype, bind_self, erase_to_bound, type_object_type
from mypy import messages
from mypy.subtypes import (
is_subtype, is_equivalent, is_proper_subtype, is_more_precise,
Expand Down Expand Up @@ -1255,6 +1255,29 @@ def visit_class_def(self, defn: ClassDef) -> None:
# Otherwise we've already found errors; more errors are not useful
self.check_multiple_inheritance(typ)

if defn.decorators:
sig = type_object_type(defn.info, self.named_type)
# Decorators are applied in reverse order.
for decorator in reversed(defn.decorators):
if (isinstance(decorator, CallExpr)
and isinstance(decorator.analyzed, PromoteExpr)):
# _promote is a special type checking related construct.
continue

dec = self.expr_checker.accept(decorator)
temp = self.temp_node(sig)
fullname = None
if isinstance(decorator, RefExpr):
fullname = decorator.fullname

# TODO: Figure out how to have clearer error messages.
# (e.g. "class decorator must be a function that accepts a type."
sig, _ = self.expr_checker.check_call(dec, [temp],
[nodes.ARG_POS], defn,
callable_name=fullname)
# 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]])

def check_protocol_variance(self, defn: ClassDef) -> None:
"""Check that protocol definition is compatible with declared
variances of type variables.
Expand Down
38 changes: 37 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4115,7 +4115,8 @@ def f() -> type: return M
class C1(six.with_metaclass(M), object): pass # E: Invalid base class
class C2(C1, six.with_metaclass(M)): pass # E: Invalid base class
class C3(six.with_metaclass(A)): pass # E: Metaclasses not inheriting from 'type' are not supported
@six.add_metaclass(A) # E: Metaclasses not inheriting from 'type' are not supported
@six.add_metaclass(A) # E: Argument 1 to "add_metaclass" has incompatible type "Type[A]"; expected "Type[type]" \
# E: Metaclasses not inheriting from 'type' are not supported
class D3(A): pass
class C4(six.with_metaclass(M), metaclass=M): pass # E: Multiple metaclass definitions
@six.add_metaclass(M) # E: Multiple metaclass definitions
Expand Down Expand Up @@ -4223,3 +4224,38 @@ class C(Any):
reveal_type(self.bar().__name__) # E: Revealed type is 'builtins.str'
[builtins fixtures/type.pyi]
[out]

[case testClassDecoratorIsTypeChecked]
from typing import Callable, Type
def decorate(x: int) -> Callable[[type], type]: # N: "decorate" defined here
...
def decorate_forward_ref() -> Callable[[Type[A]], Type[A]]:
...
@decorate(y=17) # E: Unexpected keyword argument "y" for "decorate"
@decorate() # E: Too few arguments for "decorate"
@decorate(22, 25) # E: Too many arguments for "decorate"
@decorate_forward_ref()
@decorate(11)
class A: pass

@decorate # E: Argument 1 to "decorate" has incompatible type "Type[A2]"; expected "int"
class A2: pass

[case testClassDecoratorIncorrect]
def not_a_class_decorator(x: int) -> int: ...
@not_a_class_decorator(7) # E: "int" not callable
class A3: pass

not_a_function = 17
@not_a_function() # E: "int" not callable
class B: pass

@not_a_function # E: "int" not callable
class B2: pass

b = object()
@b.nothing # E: "object" has no attribute "nothing"
class C: pass

@undefined # E: Name 'undefined' is not defined
class D: pass

0 comments on commit 46ecea5

Please sign in to comment.