5
5
def foo(self, bar, baz=12):
6
6
return bar + baz
7
7
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
9
18
10
- def foo(self, bar, baz=12):
11
- # type: (Any, int) -> Any # noqa: F821
12
- return bar + baz
13
19
14
20
It does not do type inference but it recognizes some basic default
15
21
argument values such as numbers and strings (and assumes their type
@@ -46,7 +52,7 @@ class FixAnnotate(BaseFix):
46
52
47
53
# The pattern to match.
48
54
PATTERN = """
49
- funcdef< 'def' name=any parameters=parameters< '(' [args=any] ')' > ':' suite=any+ >
55
+ funcdef< 'def' name=any parameters=parameters< '(' [args=any] rpar= ')' > ':' suite=any+ >
50
56
"""
51
57
52
58
_maxfixes = os .getenv ('MAXFIXES' )
@@ -69,8 +75,7 @@ def transform(self, node, results):
69
75
if ch .prefix .lstrip ().startswith ('# type:' ):
70
76
return
71
77
72
- suite = results ['suite' ]
73
- children = suite [0 ].children
78
+ children = results ['suite' ][0 ].children
74
79
75
80
# NOTE: I've reverse-engineered the structure of the parse tree.
76
81
# It's always a list of nodes, the first of which contains the
@@ -91,21 +96,180 @@ def transform(self, node, results):
91
96
if ch .prefix .lstrip ().startswith ('# type:' ):
92
97
return # There's already a # type: comment here; don't change anything.
93
98
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
+
94
172
# Compute the annotation
95
173
annot = self .make_annotation (node , results )
96
174
if annot is None :
97
175
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
98
261
99
262
# Insert '# type: {annot}' comment.
100
263
# For reference, see lib2to3/fixes/fix_tuple_params.py in stdlib.
101
264
if len (children ) >= 1 and children [0 ].type != token .NEWLINE :
265
+ # one liner function
102
266
if children [0 ].prefix .strip () == '' :
103
267
children [0 ].prefix = ''
104
268
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 ) + ' ' ))
106
271
children .append (Leaf (token .DEDENT , '' ))
107
272
if len (children ) >= 2 and children [1 ].type == token .INDENT :
108
- argtypes , restype = annot
109
273
degen_str = '(...) -> %s' % restype
110
274
short_str = '(%s) -> %s' % (', ' .join (argtypes ), restype )
111
275
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):
116
280
children [1 ].prefix = '%s# type: %s\n %s' % (children [1 ].value , annot_str ,
117
281
children [1 ].prefix )
118
282
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 )
124
283
else :
125
284
self .log_message ("%s:%d: cannot insert annotation for one-line function" %
126
285
(self .filename , node .get_lineno ()))
@@ -221,7 +380,7 @@ def make_annotation(self, node, results):
221
380
else :
222
381
# Always skip the first argument if it's named 'self'.
223
382
# 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 :
225
384
pass
226
385
else :
227
386
inferred_type = 'Any'
0 commit comments