Skip to content

Commit 25c993b

Browse files
TH3CHARLieilevkivskyi
authored andcommitted
Fix incorrect name lookup for decorated methods (python#8175)
Resolves python#8161 According to comments of `lookup` in `mypy/semanal.py`, when we look up a class attribute, we require that it is defined textually before the reference statement, thus line number is used for comparison. When function has decorators, its line number is determined by the top decorator instead of the `def`. That's why python#8161's code fails because on line 8, the `A` in `Type[A]` has the line number of 8 while the `@staticmethod` function `A` has the line number of 7 due to the decorator. Thus we need to properly handle this by introducing the number of decorators when deciding textural precedence. Also overloads needs special handling to be considered "as a unit".
1 parent 7ef8772 commit 25c993b

File tree

3 files changed

+74
-1
lines changed

3 files changed

+74
-1
lines changed

mypy/semanal.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -3936,11 +3936,41 @@ class C:
39363936
# caught.
39373937
assert self.statement # we are at class scope
39383938
return (node is None
3939-
or node.line < self.statement.line
3939+
or self.is_textually_before_statement(node)
39403940
or not self.is_defined_in_current_module(node.fullname)
39413941
or isinstance(node, TypeInfo)
39423942
or (isinstance(node, PlaceholderNode) and node.becomes_typeinfo))
39433943

3944+
def is_textually_before_statement(self, node: SymbolNode) -> bool:
3945+
"""Check if a node is defined textually before the current statement
3946+
3947+
Note that decorated functions' line number are the same as
3948+
the top decorator.
3949+
"""
3950+
assert self.statement
3951+
line_diff = self.statement.line - node.line
3952+
3953+
# The first branch handles reference an overloaded function variant inside itself,
3954+
# this is a corner case where mypy technically deviates from runtime name resolution,
3955+
# but it is fine because we want an overloaded function to be treated as a single unit.
3956+
if self.is_overloaded_item(node, self.statement):
3957+
return False
3958+
elif isinstance(node, Decorator) and not node.is_overload:
3959+
return line_diff > len(node.original_decorators)
3960+
else:
3961+
return line_diff > 0
3962+
3963+
def is_overloaded_item(self, node: SymbolNode, statement: Statement) -> bool:
3964+
"""Check whehter the function belongs to the overloaded variants"""
3965+
if isinstance(node, OverloadedFuncDef) and isinstance(statement, FuncDef):
3966+
in_items = statement in {item.func if isinstance(item, Decorator)
3967+
else item for item in node.items}
3968+
in_impl = (node.impl is not None and
3969+
((isinstance(node.impl, Decorator) and statement is node.impl.func)
3970+
or statement is node.impl))
3971+
return in_items or in_impl
3972+
return False
3973+
39443974
def is_defined_in_current_module(self, fullname: Optional[str]) -> bool:
39453975
if fullname is None:
39463976
return False

test-data/unit/check-classes.test

+42
Original file line numberDiff line numberDiff line change
@@ -6597,3 +6597,45 @@ reveal_type(D() + "str") # N: Revealed type is 'Any'
65976597
reveal_type(0.5 + D1()) # N: Revealed type is 'Any'
65986598
reveal_type(D1() + 0.5) # N: Revealed type is '__main__.D1'
65996599
[builtins fixtures/primitives.pyi]
6600+
6601+
[case testRefMethodWithDecorator]
6602+
from typing import Type
6603+
6604+
class A:
6605+
pass
6606+
6607+
class B:
6608+
@staticmethod
6609+
def A() -> Type[A]: ...
6610+
@staticmethod
6611+
def B() -> Type[A]: # E: Function "__main__.B.A" is not valid as a type \
6612+
# N: Perhaps you need "Callable[...]" or a callback protocol?
6613+
return A
6614+
6615+
class C:
6616+
@property
6617+
@staticmethod
6618+
def A() -> Type[A]:
6619+
return A
6620+
6621+
[builtins fixtures/staticmethod.pyi]
6622+
6623+
[case testRefMethodWithOverloadDecorator]
6624+
from typing import Type, overload
6625+
6626+
class A:
6627+
pass
6628+
6629+
class B:
6630+
@classmethod
6631+
@overload
6632+
def A(cls, x: int) -> Type[A]: ...
6633+
@classmethod
6634+
@overload
6635+
def A(cls, x: str) -> Type[A]: ...
6636+
@classmethod
6637+
def A(cls, x: object) -> Type[A]: ...
6638+
def B(cls, x: int) -> Type[A]: ... # E: Function "__main__.B.A" is not valid as a type \
6639+
# N: Perhaps you need "Callable[...]" or a callback protocol?
6640+
6641+
[builtins fixtures/classmethod.pyi]

test-data/unit/fixtures/staticmethod.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class type:
99
class function: pass
1010

1111
staticmethod = object() # Dummy definition.
12+
property = object() # Dummy definition
1213

1314
class int:
1415
@staticmethod

0 commit comments

Comments
 (0)