Skip to content
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

Adds decorated @property support, refs #1362 #11719

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3756,20 +3756,37 @@ def visit_decorator(self, e: Decorator) -> None:
continue
dec = self.expr_checker.accept(d)
temp = self.temp_node(sig, context=e)
fullname = None
if isinstance(d, RefExpr):
fullname = d.fullname
# if this is a expression like @b.a where b is an object, get the type of b
# so we can pass it the method hook in the plugins
object_type: Optional[Type] = None
if fullname is None and isinstance(d, MemberExpr) and d.expr in self.type_map:
object_type = self.type_map[d.expr]
fullname = self.expr_checker.method_fullname(object_type, d.name)
fullname, object_type = self.get_decorator_fullname_and_object_type(d)
self.check_for_untyped_decorator(e.func, dec, d)
sig, t2 = self.expr_checker.check_call(dec, [temp],
[nodes.ARG_POS], e,
callable_name=fullname,
object_type=object_type)

if e.property_decorator is not None:
# What we do here: we ensure that `@property` / `@cached_property`
# decorators get the correct type, but we don't override the resulting
# signature. Why? Because decorated properties were not supported
# for a long time. And a lot of implementation details assume
# that we won't atually get `builtins.property` type in the result.
# And we also know how to handle properties pretty well.
# So, this is a not-really-dirty-hack
# that allows us to keep this feature simple.
# See: https://github.com/python/mypy/issues/1362
fullname, object_type = self.get_decorator_fullname_and_object_type(
e.property_decorator,
)
prop_type = self.expr_checker.accept(e.property_decorator)
temp = self.temp_node(sig, context=e)
self.expr_checker.check_call(
prop_type,
[temp],
[nodes.ARG_POS],
e,
callable_name=fullname,
object_type=object_type,
)

self.check_untyped_after_decorator(sig, e.func)
sig = set_callable_name(sig, e.func)
e.var.type = sig
Expand All @@ -3783,6 +3800,20 @@ def visit_decorator(self, e: Decorator) -> None:
if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)):
self.fail(message_registry.BAD_CONSTRUCTOR_TYPE, e)

def get_decorator_fullname_and_object_type(
self, dec: Expression,
) -> Tuple[Optional[str], Optional[Type]]:
fullname = None
if isinstance(dec, RefExpr):
fullname = dec.fullname
# if this is a expression like @b.a where b is an object, get the type of b
# so we can pass it the method hook in the plugins
object_type: Optional[Type] = None
if fullname is None and isinstance(dec, MemberExpr) and dec.expr in self.type_map:
object_type = self.type_map[dec.expr]
fullname = self.expr_checker.method_fullname(object_type, dec.name)
return fullname, object_type

def check_for_untyped_decorator(self,
func: FuncDef,
dec_type: Type,
Expand Down
11 changes: 10 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,12 +776,20 @@ class Decorator(SymbolNode, Statement):
A single Decorator object can include any number of function decorators.
"""

__slots__ = ('func', 'decorators', 'original_decorators', 'var', 'is_overload')
__slots__ = (
'func', 'decorators', 'original_decorators',
'property_decorator',
'var', 'is_overload',
)

func: FuncDef # Decorated function
decorators: List[Expression] # Decorators (may be empty)
# Some decorators are removed by semanal, keep the original here.
original_decorators: List[Expression]
# We use special field, where we store the `@property` / `@cached_property`
# decorator. It is implied that `@property` is the top-most decorator in the chain.
# Other use-cases are so rare, that we don't support them.
property_decorator: Optional[Expression]
# TODO: This is mostly used for the type; consider replacing with a 'type' attribute
var: "Var" # Represents the decorated function obj
is_overload: bool
Expand All @@ -792,6 +800,7 @@ def __init__(self, func: FuncDef, decorators: List[Expression],
self.func = func
self.decorators = decorators
self.original_decorators = decorators.copy()
self.property_decorator = None
self.var = var
self.is_overload = False

Expand Down
34 changes: 17 additions & 17 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -937,24 +937,24 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -
"""
defn.is_property = True
items = defn.items
first_item = cast(Decorator, defn.items[0])
property_def = cast(Decorator, defn.items[0])
deleted_items = []
for i, item in enumerate(items[1:]):
if isinstance(item, Decorator):
if len(item.decorators) == 1:
node = item.decorators[0]
if isinstance(node, MemberExpr):
if node.name == 'setter':
# The first item represents the entire property.
first_item.var.is_settable_property = True
# Get abstractness from the original definition.
item.func.is_abstract = first_item.func.is_abstract
else:
self.fail("Decorated property not supported", item)
for node in item.decorators:
if (isinstance(node, MemberExpr)
and isinstance(node.expr, NameExpr)
and node.expr.name == property_def.var.name
and node.name == 'setter'):
# The `property_def` represents the entire property.
property_def.var.is_settable_property = True
# Get abstractness from the original definition.
item.func.is_abstract = property_def.func.is_abstract
item.func.accept(self)
else:
self.fail('Unexpected definition for property "{}"'.format(first_item.func.name),
item)
self.fail('Unexpected definition for property "{}"'.format(
property_def.func.name,
), item)
deleted_items.append(i + 1)
for i in reversed(deleted_items):
del items[i]
Expand Down Expand Up @@ -1062,8 +1062,10 @@ def visit_decorator(self, dec: Decorator) -> None:
elif refers_to_fullname(d, 'functools.cached_property'):
dec.var.is_settable_property = True
self.check_decorated_function_is_method('property', dec)
if len(dec.func.arguments) > 1:
self.fail('Too many arguments', dec.func)
if i == 0:
dec.property_decorator = d
else:
self.fail('Property must be used as the top-most decorator', d)
elif refers_to_fullname(d, 'typing.no_type_check'):
dec.var.type = AnyType(TypeOfAny.special_form)
no_type_check = True
Expand All @@ -1086,8 +1088,6 @@ def visit_decorator(self, dec: Decorator) -> None:
dec.var.is_initialized_in_class = True
if not no_type_check and self.recurse_into_functions:
dec.func.accept(self)
if dec.decorators and dec.var.is_property:
self.fail('Decorated property not supported', dec)

def check_decorated_function_is_method(self, decorator: str,
context: Context) -> None:
Expand Down
65 changes: 63 additions & 2 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -7011,6 +7011,68 @@ class A:
reveal_type(A().y) # N: Revealed type is "builtins.int"
[builtins fixtures/property.pyi]

[case testDecoratedProperty]
from typing import Callable, TypeVar

def correct(x: Callable[['X', int], int]) -> Callable[['X'], int]:
def inner(self: 'X') -> int:
return x(self, 1)
return inner

def changing_type(x: Callable[['X', int], int]) -> Callable[['X'], str]:
def inner(self: 'X') -> str:
return 'a'
return inner

def wrong_input(x: Callable[['X', int], int]) -> Callable[['X'], int]:
def inner(self: 'X') -> int:
return 1
return inner

def wrong_return(x: Callable[['X', int], int]) -> Callable[[], int]:
def inner() -> int:
return 1
return inner

C = TypeVar('C', bound=Callable)

def return_itself(c: C) -> C:
return c

class X:
@property
@correct
def a(self, arg: int) -> int:
return 1

@property
@changing_type
def b(self, arg: int) -> int:
return 2

@property # E: Argument 1 to "wrong_input" has incompatible type "Callable[[X, str], str]"; expected "Callable[[X, int], int]"
@wrong_input
def c(self, arg: str) -> str:
return 'a'

@property # E: Argument 1 to "property" has incompatible type "Callable[[], int]"; expected "Optional[Callable[[Any], Any]]"
@wrong_return
def d(self, arg: int) -> int:
return 3

@property
@return_itself
def e(self) -> int:
return 4

reveal_type(X().a) # N: Revealed type is "builtins.int"
reveal_type(X().b) # N: Revealed type is "builtins.str"
reveal_type(X().c) # N: Revealed type is "builtins.int"
reveal_type(X().d) # E: Attribute function "d" with type "Callable[[], int]" does not accept self argument \
# N: Revealed type is "builtins.int"
reveal_type(X().e) # N: Revealed type is "builtins.int"
[builtins fixtures/property.pyi]

[case testEnclosingScopeLambdaNoCrash]
class C:
x = lambda x: x.y.g()
Expand Down Expand Up @@ -7057,11 +7119,10 @@ class B:
return A

class C:
@property
@property # E: Argument 1 to "property" has incompatible type "Callable[[], Type[A]]"; expected "Callable[[Any], Any]"
@staticmethod
def A() -> Type[A]:
return A

[builtins fixtures/staticmethod.pyi]

[case testRefMethodWithOverloadDecorator]
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-functools.test
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ class Child(Parent):
def f(self) -> str: pass
@cached_property
def g(self) -> int: pass
@cached_property
def h(self, arg) -> int: pass # E: Too many arguments
@cached_property # E: Argument 1 to "cached_property" has incompatible type "Callable[[Child, Any], int]"; expected "Callable[[Any], int]"
def h(self, arg) -> int: pass
reveal_type(Parent().f) # N: Revealed type is "builtins.str"
reveal_type(Child().f) # N: Revealed type is "builtins.str"
reveal_type(Child().g) # N: Revealed type is "builtins.int"
Expand Down
3 changes: 1 addition & 2 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -1475,11 +1475,10 @@ class A:
self.x = [] # E: Need type annotation for "x" (hint: "x: List[<type>] = ...")

class B(A):
# TODO?: This error is kind of a false positive, unfortunately
@property
def x(self) -> List[int]: # E: Signature of "x" incompatible with supertype "A"
return [123]
[builtins fixtures/list.pyi]
[builtins fixtures/property.pyi]

[case testInferSetInitializedToEmpty]
a = set()
Expand Down
15 changes: 8 additions & 7 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ main:37: error: Argument 1 to "fun" has incompatible type "B"; expected "P"
main:37: note: Following member(s) of "B" have conflicts:
main:37: note: x: expected "int", got "str"
main:40: note: Revealed type is "builtins.int"
[builtins fixtures/bool.pyi]
[builtins fixtures/property.pyi]

[case testSimpleProtocolOneAbstractMethod]
from typing import Protocol
Expand Down Expand Up @@ -894,7 +894,7 @@ t: Traversable
t = D[int]() # OK
if int():
t = C() # E: Incompatible types in assignment (expression has type "C", variable has type "Traversable")
[builtins fixtures/list.pyi]
[builtins fixtures/property.pyi]
[typing fixtures/typing-medium.pyi]

[case testRecursiveProtocols2]
Expand Down Expand Up @@ -951,7 +951,7 @@ t = A() # OK
if int():
t = B() # E: Incompatible types in assignment (expression has type "B", variable has type "P1")
t = C() # E: Incompatible types in assignment (expression has type "C", variable has type "P1")
[builtins fixtures/list.pyi]
[builtins fixtures/property.pyi]
[typing fixtures/typing-medium.pyi]

[case testMutuallyRecursiveProtocolsTypesWithSubteMismatch]
Expand All @@ -974,7 +974,8 @@ t: P1
t = A() # E: Incompatible types in assignment (expression has type "A", variable has type "P1") \
# N: Following member(s) of "A" have conflicts: \
# N: attr1: expected "Sequence[P2]", got "List[B]"
[builtins fixtures/list.pyi]
[builtins fixtures/property.pyi]
[typing fixtures/typing-medium.pyi]

[case testMutuallyRecursiveProtocolsTypesWithSubteMismatchWriteable]
from typing import Protocol
Expand Down Expand Up @@ -1831,7 +1832,7 @@ fun(N2(1)) # E: Argument 1 to "fun" has incompatible type "N2"; expected "P[int,
reveal_type(fun3(z)) # N: Revealed type is "builtins.object*"

reveal_type(fun3(z3)) # N: Revealed type is "builtins.int*"
[builtins fixtures/list.pyi]
[builtins fixtures/property.pyi]

[case testBasicCallableStructuralSubtyping]
from typing import Callable, Generic, TypeVar
Expand Down Expand Up @@ -2017,7 +2018,7 @@ f(C()) # E: Argument 1 to "f" has incompatible type "C"; expected "P" \
# N: attr1: expected "int", got "str" \
# N: attr2: expected "str", got "int" \
# N: Protocol member P.attr2 expected settable variable, got read-only attribute
[builtins fixtures/list.pyi]
[builtins fixtures/property.pyi]

[case testIterableProtocolOnClass]
from typing import TypeVar, Iterator
Expand Down Expand Up @@ -2083,7 +2084,7 @@ fun_p(D()) # E: Argument 1 to "fun_p" has incompatible type "D"; expected "PP"
# N: Following member(s) of "D" have conflicts: \
# N: attr: expected "int", got "str"
fun_p(C()) # OK
[builtins fixtures/list.pyi]
[builtins fixtures/property.pyi]

[case testImplicitTypesInProtocols]
from typing import Protocol
Expand Down
6 changes: 5 additions & 1 deletion test-data/unit/fixtures/dataclasses.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import (
Any, Callable,
Generic, Iterator, Iterable, Mapping, Optional, Sequence, Tuple,
TypeVar, Union, overload,
)
Expand Down Expand Up @@ -40,4 +41,7 @@ class dict(Mapping[KT, VT]):
class list(Generic[_T], Sequence[_T]): pass
class function: pass
class classmethod: pass
property = object()

class property(object):
def __init__(self, fget: Callable[[Any], Any]) -> None:
pass
6 changes: 4 additions & 2 deletions test-data/unit/fixtures/list.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Builtins stub used in list-related test cases.

from typing import TypeVar, Generic, Iterable, Iterator, Sequence, overload
from typing import Any, Callable, TypeVar, Generic, Iterable, Iterator, Sequence, overload

T = TypeVar('T')

Expand Down Expand Up @@ -35,4 +35,6 @@ class str:
def __len__(self) -> bool: pass
class bool(int): pass

property = object() # Dummy definition.
class property(object):
def __init__(self, fget: Callable[[object], object]) -> None:
pass
Loading