Skip to content

Commit b7f96ca

Browse files
authored
Fix annotation of decorated methods, nested methods, nested classes (#91)
The determination of the name of a function is currently done by looking at the function node and its grandparent node (for the class), and nothing else. This misses the class name for decorated methods as well as producing nonsensible results for nested classes and functions (though none of the frontends handle those right yet either). Fix this by doing a full traversal up the tree and including all functions and classes. The broken decorator behavior was also sort of accidentally causing the class name to be left off for staticmethods and classmethods (which the collector does also), so we need to keep supporting that behavior.
1 parent ce8afa9 commit b7f96ca

File tree

4 files changed

+204
-16
lines changed

4 files changed

+204
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
__pycache__
77
/env*
88
*~
9+
.mypy_cache/

pyannotate_tools/fixes/fix_annotate_json.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,26 @@ def get_init_file(dir):
8282
return f
8383
return None
8484

85-
def get_funcname(name, node):
86-
# type: (Leaf, Node) -> Text
87-
"""Get function name by the following rules:
85+
def get_funcname(node):
86+
# type: (Optional[Node]) -> Text
87+
"""Get function name by (approximately) the following rules:
8888
8989
- function -> function_name
90-
- instance method -> ClassName.function_name
90+
- method -> ClassName.function_name
91+
92+
More specifically, we include every class and function name that
93+
the node is a child of, so nested classes and functions get names like
94+
OuterClass.InnerClass.outer_fn.inner_fn.
9195
"""
92-
funcname = name.value
93-
if node.parent and node.parent.parent:
94-
grand = node.parent.parent
95-
if grand.type == syms.classdef:
96-
grandname = grand.children[1]
97-
assert grandname.type == token.NAME, repr(name)
98-
assert isinstance(grandname, Leaf) # Same as previous, for mypy
99-
funcname = grandname.value + '.' + funcname
100-
return funcname
96+
components = [] # type: List[str]
97+
while node:
98+
if node.type in (syms.classdef, syms.funcdef):
99+
name = node.children[1]
100+
assert name.type == token.NAME, repr(name)
101+
assert isinstance(name, Leaf) # Same as previous, for mypy
102+
components.append(name.value)
103+
node = node.parent
104+
return '.'.join(reversed(components))
101105

102106
def count_args(node, results):
103107
# type: (Node, Dict[str, Base]) -> Tuple[int, bool, bool, bool]
@@ -172,8 +176,19 @@ def make_annotation(self, node, results):
172176
name = results['name']
173177
assert isinstance(name, Leaf), repr(name)
174178
assert name.type == token.NAME, repr(name)
175-
funcname = get_funcname(name, node)
179+
funcname = get_funcname(node)
176180
res = self.get_annotation_from_stub(node, results, funcname)
181+
182+
# If we couldn't find an annotation and this is a classmethod or
183+
# staticmethod, try again with just the funcname, since the
184+
# type collector can't figure out class names for those.
185+
# (We try with the full name above first so that tools that *can* figure
186+
# that out, like dmypy suggest, can use it.)
187+
if not res:
188+
decs = self.get_decorators(node)
189+
if 'staticmethod' in decs or 'classmethod' in decs:
190+
res = self.get_annotation_from_stub(node, results, name.value)
191+
177192
return res
178193

179194
stub_json_file = os.getenv('TYPE_COLLECTION_JSON')

pyannotate_tools/fixes/tests/test_annotate_json_py2.py

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,102 @@ def nop(foo, bar):
6060
"""
6161
self.check(a, b)
6262

63+
def test_decorator_func(self):
64+
self.setTestData(
65+
[{"func_name": "foo",
66+
"path": "<string>",
67+
"line": 2,
68+
"signature": {
69+
"arg_types": [],
70+
"return_type": "int"},
71+
}])
72+
a = """\
73+
@dec
74+
def foo():
75+
return 42
76+
"""
77+
b = """\
78+
@dec
79+
def foo():
80+
# type: () -> int
81+
return 42
82+
"""
83+
self.check(a, b)
84+
85+
def test_decorator_method(self):
86+
self.setTestData(
87+
[{"func_name": "Bar.foo",
88+
"path": "<string>",
89+
"line": 3,
90+
"signature": {
91+
"arg_types": [],
92+
"return_type": "int"},
93+
}])
94+
a = """\
95+
class Bar:
96+
@dec
97+
@dec2
98+
def foo(self):
99+
return 42
100+
"""
101+
b = """\
102+
class Bar:
103+
@dec
104+
@dec2
105+
def foo(self):
106+
# type: () -> int
107+
return 42
108+
"""
109+
self.check(a, b)
110+
111+
def test_nested_class_func(self):
112+
self.setTestData(
113+
[{"func_name": "A.B.foo",
114+
"path": "<string>",
115+
"line": 3,
116+
"signature": {
117+
"arg_types": ['str'],
118+
"return_type": "int"},
119+
}])
120+
a = """\
121+
class A:
122+
class B:
123+
def foo(x):
124+
return 42
125+
"""
126+
b = """\
127+
class A:
128+
class B:
129+
def foo(x):
130+
# type: (str) -> int
131+
return 42
132+
"""
133+
self.check(a, b)
134+
135+
def test_nested_func(self):
136+
self.setTestData(
137+
[{"func_name": "A.foo.bar",
138+
"path": "<string>",
139+
"line": 3,
140+
"signature": {
141+
"arg_types": [],
142+
"return_type": "int"},
143+
}])
144+
a = """\
145+
class A:
146+
def foo():
147+
def bar():
148+
return 42
149+
"""
150+
b = """\
151+
class A:
152+
def foo():
153+
def bar():
154+
# type: () -> int
155+
return 42
156+
"""
157+
self.check(a, b)
158+
63159
def test_keyword_only_argument(self):
64160
self.setTestData(
65161
[{"func_name": "nop",
@@ -432,7 +528,7 @@ def yep(a):
432528
self.check(a, b)
433529

434530
def test_classmethod(self):
435-
# Class method names currently are returned without class name
531+
# Class methods need to work without a class name
436532
self.setTestData(
437533
[{"func_name": "nop",
438534
"path": "<string>",
@@ -456,8 +552,33 @@ def nop(cls, a):
456552
"""
457553
self.check(a, b)
458554

555+
def test_classmethod_named(self):
556+
# Class methods also should work *with* a class name
557+
self.setTestData(
558+
[{"func_name": "C.nop",
559+
"path": "<string>",
560+
"line": 3,
561+
"signature": {
562+
"arg_types": ["int"],
563+
"return_type": "int"}
564+
}])
565+
a = """\
566+
class C:
567+
@classmethod
568+
def nop(cls, a):
569+
return a
570+
"""
571+
b = """\
572+
class C:
573+
@classmethod
574+
def nop(cls, a):
575+
# type: (int) -> int
576+
return a
577+
"""
578+
self.check(a, b)
579+
459580
def test_staticmethod(self):
460-
# Static method names currently are returned without class name
581+
# Static methods need to work without a class name
461582
self.setTestData(
462583
[{"func_name": "nop",
463584
"path": "<string>",
@@ -481,6 +602,31 @@ def nop(a):
481602
"""
482603
self.check(a, b)
483604

605+
def test_staticmethod_named(self):
606+
# Static methods also should work *with* a class name
607+
self.setTestData(
608+
[{"func_name": "C.nop",
609+
"path": "<string>",
610+
"line": 3,
611+
"signature": {
612+
"arg_types": ["int"],
613+
"return_type": "int"}
614+
}])
615+
a = """\
616+
class C:
617+
@staticmethod
618+
def nop(a):
619+
return a
620+
"""
621+
b = """\
622+
class C:
623+
@staticmethod
624+
def nop(a):
625+
# type: (int) -> int
626+
return a
627+
"""
628+
self.check(a, b)
629+
484630
def test_long_form(self):
485631
self.maxDiff = None
486632
self.setTestData(

pyannotate_tools/fixes/tests/test_annotate_json_py3.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import json
55
import os
66
import tempfile
7+
import unittest
8+
import sys
79

810
from lib2to3.tests.test_fixers import FixerTestCase
911

@@ -606,3 +608,27 @@ def nop(a): return 0
606608
def nop(a: Tuple[int, ...]) -> int: return 0
607609
"""
608610
self.check(a, b)
611+
612+
@unittest.skipIf(sys.version_info < (3, 5), 'async not supported on old python')
613+
def test_nested_class_async_func(self):
614+
self.setTestData(
615+
[{"func_name": "A.B.foo",
616+
"path": "<string>",
617+
"line": 3,
618+
"signature": {
619+
"arg_types": ['str'],
620+
"return_type": "int"},
621+
}])
622+
a = """\
623+
class A:
624+
class B:
625+
async def foo(x):
626+
return 42
627+
"""
628+
b = """\
629+
class A:
630+
class B:
631+
async def foo(x: str) -> int:
632+
return 42
633+
"""
634+
self.check(a, b)

0 commit comments

Comments
 (0)