Skip to content

Commit ad82c01

Browse files
bluebird75gvanrossum
authored andcommitted
Python3 annotations (#74)
Fixes #4.
1 parent 0933376 commit ad82c01

File tree

6 files changed

+1397
-16
lines changed

6 files changed

+1397
-16
lines changed

pyannotate_tools/annotations/__main__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
help="Files and directories to update with annotations")
3535
parser.add_argument('-s', '--only-simple', action='store_true',
3636
help="Only annotate functions with trivial types")
37+
parser.add_argument('--python-version', action='store', default='2',
38+
help="Choose annotation style, 2 for Python 2 with comments (the "
39+
"default), 3 for Python 3 with annotation syntax" )
40+
parser.add_argument('--py2', '-2', action='store_const', dest='python_version', const='2',
41+
help="Annotate for Python 2 with comments (default)")
42+
parser.add_argument('--py3', '-3', action='store_const', dest='python_version', const='3',
43+
help="Annotate for Python 3 with argument and return value annotations")
3744

3845

3946
class ModifiedRefactoringTool(StdoutRefactoringTool):
@@ -86,6 +93,11 @@ def main(args_override=None):
8693
except OSError as err:
8794
sys.exit("Can't open type info file: %s" % err)
8895

96+
if args.python_version not in ('2', '3'):
97+
sys.exit('--python-version must be 2 or 3')
98+
99+
annotation_style = 'py' + args.python_version
100+
89101
# Set up logging handler.
90102
level = logging.DEBUG if args.verbose else logging.INFO
91103
logging.basicConfig(format='%(message)s', level=level)
@@ -104,7 +116,8 @@ def main(args_override=None):
104116
else:
105117
FixAnnotateJson.init_stub_json_from_data(data, args.files[0])
106118
fixers = ['pyannotate_tools.fixes.fix_annotate_json']
107-
flags = {'print_function': args.print_function}
119+
flags = {'print_function': args.print_function,
120+
'annotation_style': annotation_style}
108121
rt = ModifiedRefactoringTool(
109122
fixers=fixers,
110123
options=flags,

pyannotate_tools/fixes/fix_annotate.py

Lines changed: 174 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55
def foo(self, bar, baz=12):
66
return bar + baz
77
8-
into
8+
into a type annoted version:
9+
10+
def foo(self, bar, baz=12):
11+
# type: (Any, int) -> Any # noqa: F821
12+
return bar + baz
13+
14+
or (when setting options['annotation_style'] to 'py3'):
15+
16+
def foo(self, bar : Any, baz : int = 12) -> Any:
17+
return bar + baz
918
10-
def foo(self, bar, baz=12):
11-
# type: (Any, int) -> Any # noqa: F821
12-
return bar + baz
1319
1420
It does not do type inference but it recognizes some basic default
1521
argument values such as numbers and strings (and assumes their type
@@ -46,7 +52,7 @@ class FixAnnotate(BaseFix):
4652

4753
# The pattern to match.
4854
PATTERN = """
49-
funcdef< 'def' name=any parameters=parameters< '(' [args=any] ')' > ':' suite=any+ >
55+
funcdef< 'def' name=any parameters=parameters< '(' [args=any] rpar=')' > ':' suite=any+ >
5056
"""
5157

5258
_maxfixes = os.getenv('MAXFIXES')
@@ -69,8 +75,7 @@ def transform(self, node, results):
6975
if ch.prefix.lstrip().startswith('# type:'):
7076
return
7177

72-
suite = results['suite']
73-
children = suite[0].children
78+
children = results['suite'][0].children
7479

7580
# NOTE: I've reverse-engineered the structure of the parse tree.
7681
# It's always a list of nodes, the first of which contains the
@@ -91,21 +96,180 @@ def transform(self, node, results):
9196
if ch.prefix.lstrip().startswith('# type:'):
9297
return # There's already a # type: comment here; don't change anything.
9398

99+
# Python 3 style return annotation are already skipped by the pattern
100+
101+
### Python 3 style argument annotation structure
102+
#
103+
# Structure of the arguments tokens for one positional argument without default value :
104+
# + LPAR '('
105+
# + NAME_NODE_OR_LEAF arg1
106+
# + RPAR ')'
107+
#
108+
# NAME_NODE_OR_LEAF is either:
109+
# 1. Just a leaf with value NAME
110+
# 2. A node with children: NAME, ':", node expr or value leaf
111+
#
112+
# Structure of the arguments tokens for one args with default value or multiple
113+
# args, with or without default value, and/or with extra arguments :
114+
# + LPAR '('
115+
# + node
116+
# [
117+
# + NAME_NODE_OR_LEAF
118+
# [
119+
# + EQUAL '='
120+
# + node expr or value leaf
121+
# ]
122+
# (
123+
# + COMMA ','
124+
# + NAME_NODE_OR_LEAF positional argn
125+
# [
126+
# + EQUAL '='
127+
# + node expr or value leaf
128+
# ]
129+
# )*
130+
# ]
131+
# [
132+
# + STAR '*'
133+
# [
134+
# + NAME_NODE_OR_LEAF positional star argument name
135+
# ]
136+
# ]
137+
# [
138+
# + COMMA ','
139+
# + DOUBLESTAR '**'
140+
# + NAME_NODE_OR_LEAF positional keyword argument name
141+
# ]
142+
# + RPAR ')'
143+
144+
# Let's skip Python 3 argument annotations
145+
it = iter(args.children) if args else iter([])
146+
for ch in it:
147+
if ch.type == token.STAR:
148+
# *arg part
149+
ch = next(it)
150+
if ch.type == token.COMMA:
151+
continue
152+
elif ch.type == token.DOUBLESTAR:
153+
# *arg part
154+
ch = next(it)
155+
if ch.type > 256:
156+
# this is a node, therefore an annotation
157+
assert ch.children[0].type == token.NAME
158+
return
159+
try:
160+
ch = next(it)
161+
if ch.type == token.COLON:
162+
# this is an annotation
163+
return
164+
elif ch.type == token.EQUAL:
165+
ch = next(it)
166+
ch = next(it)
167+
assert ch.type == token.COMMA
168+
continue
169+
except StopIteration:
170+
break
171+
94172
# Compute the annotation
95173
annot = self.make_annotation(node, results)
96174
if annot is None:
97175
return
176+
argtypes, restype = annot
177+
178+
if self.options['annotation_style'] == 'py3':
179+
self.add_py3_annot(argtypes, restype, node, results)
180+
else:
181+
self.add_py2_annot(argtypes, restype, node, results)
182+
183+
# Common to py2 and py3 style annotations:
184+
if FixAnnotate.counter is not None:
185+
FixAnnotate.counter -= 1
186+
187+
# Also add 'from typing import Any' at the top if needed.
188+
self.patch_imports(argtypes + [restype], node)
189+
190+
def add_py3_annot(self, argtypes, restype, node, results):
191+
args = results.get('args')
192+
193+
argleaves = []
194+
if args is None:
195+
# function with 0 arguments
196+
it = iter([])
197+
elif len(args.children) == 0:
198+
# function with 1 argument
199+
it = iter([args])
200+
else:
201+
# function with multiple arguments or 1 arg with default value
202+
it = iter(args.children)
203+
204+
for ch in it:
205+
argstyle = 'name'
206+
if ch.type == token.STAR:
207+
# *arg part
208+
argstyle = 'star'
209+
ch = next(it)
210+
if ch.type == token.COMMA:
211+
continue
212+
elif ch.type == token.DOUBLESTAR:
213+
# *arg part
214+
argstyle = 'keyword'
215+
ch = next(it)
216+
assert ch.type == token.NAME
217+
argleaves.append((argstyle, ch))
218+
try:
219+
ch = next(it)
220+
if ch.type == token.EQUAL:
221+
ch = next(it)
222+
ch = next(it)
223+
assert ch.type == token.COMMA
224+
continue
225+
except StopIteration:
226+
break
227+
228+
# when self or cls is not annotated, argleaves == argtypes+1
229+
argleaves = argleaves[len(argleaves) - len(argtypes):]
230+
231+
for ch_withstyle, chtype in zip(argleaves, argtypes):
232+
style, ch = ch_withstyle
233+
if style == 'star':
234+
assert chtype[0] == '*'
235+
assert chtype[1] != '*'
236+
chtype = chtype[1:]
237+
elif style == 'keyword':
238+
assert chtype[0:2] == '**'
239+
assert chtype[2] != '*'
240+
chtype = chtype[2:]
241+
ch.value = '%s: %s' % (ch.value, chtype)
242+
243+
# put spaces around the equal sign
244+
if ch.next_sibling and ch.next_sibling.type == token.EQUAL:
245+
nextch = ch.next_sibling
246+
if not nextch.prefix[:1].isspace():
247+
nextch.prefix = ' ' + nextch.prefix
248+
nextch = nextch.next_sibling
249+
assert nextch != None
250+
if not nextch.prefix[:1].isspace():
251+
nextch.prefix = ' ' + nextch.prefix
252+
253+
# Add return annotation
254+
rpar = results['rpar']
255+
rpar.value = '%s -> %s' % (rpar.value, restype)
256+
257+
rpar.changed()
258+
259+
def add_py2_annot(self, argtypes, restype, node, results):
260+
children = results['suite'][0].children
98261

99262
# Insert '# type: {annot}' comment.
100263
# For reference, see lib2to3/fixes/fix_tuple_params.py in stdlib.
101264
if len(children) >= 1 and children[0].type != token.NEWLINE:
265+
# one liner function
102266
if children[0].prefix.strip() == '':
103267
children[0].prefix = ''
104268
children.insert(0, Leaf(token.NEWLINE, '\n'))
105-
children.insert(1, Leaf(token.INDENT, find_indentation(node) + ' '))
269+
children.insert(
270+
1, Leaf(token.INDENT, find_indentation(node) + ' '))
106271
children.append(Leaf(token.DEDENT, ''))
107272
if len(children) >= 2 and children[1].type == token.INDENT:
108-
argtypes, restype = annot
109273
degen_str = '(...) -> %s' % restype
110274
short_str = '(%s) -> %s' % (', '.join(argtypes), restype)
111275
if (len(short_str) > 64 or len(argtypes) > 5) and len(short_str) > len(degen_str):
@@ -116,11 +280,6 @@ def transform(self, node, results):
116280
children[1].prefix = '%s# type: %s\n%s' % (children[1].value, annot_str,
117281
children[1].prefix)
118282
children[1].changed()
119-
if FixAnnotate.counter is not None:
120-
FixAnnotate.counter -= 1
121-
122-
# Also add 'from typing import Any' at the top if needed.
123-
self.patch_imports(argtypes + [restype], node)
124283
else:
125284
self.log_message("%s:%d: cannot insert annotation for one-line function" %
126285
(self.filename, node.get_lineno()))
@@ -221,7 +380,7 @@ def make_annotation(self, node, results):
221380
else:
222381
# Always skip the first argument if it's named 'self'.
223382
# Always skip the first argument of a class method.
224-
if child.value == 'self' or 'classmethod' in decorators:
383+
if child.value == 'self' or 'classmethod' in decorators:
225384
pass
226385
else:
227386
inferred_type = 'Any'

pyannotate_tools/fixes/tests/test_annotate_json.py renamed to pyannotate_tools/fixes/tests/test_annotate_json_py2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def setUp(self):
1616
super(TestFixAnnotateJson, self).setUp(
1717
fix_list=["annotate_json"],
1818
fixer_pkg="pyannotate_tools",
19+
options={'annotation_style' : 'py2'},
1920
)
2021
# See https://bugs.python.org/issue14243 for details
2122
self.tf = tempfile.NamedTemporaryFile(mode='w', delete=False)

0 commit comments

Comments
 (0)