forked from pallets/flask
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathflask-07-upgrade.py
297 lines (246 loc) · 10.4 KB
/
flask-07-upgrade.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
flask-07-upgrade
~~~~~~~~~~~~~~~~
This command line script scans a whole application tree and attempts to
output an unified diff with all the changes that are necessary to easily
upgrade the application to 0.7 and to not yield deprecation warnings.
This will also attempt to find `after_request` functions that don't modify
the response and appear to be better suited for `teardown_request`.
This application is indeed an incredible hack, but because what it
attempts to accomplish is impossible to do statically it tries to support
the most common patterns at least. The diff it generates should be
hand reviewed and not applied blindly without making backups.
:copyright: (c) Copyright 2015 by Armin Ronacher.
:license: see LICENSE for more details.
"""
import re
import os
import inspect
import difflib
import posixpath
from optparse import OptionParser
try:
import ast
except ImportError:
ast = None
TEMPLATE_LOOKAHEAD = 4096
_app_re_part = r'((?:[a-zA-Z_][a-zA-Z0-9_]*app)|app|application)'
_string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \
r'|"([^"\\]*(?:\\.[^"\\]*)*)")'
_from_import_re = re.compile(r'^\s*from flask import\s+')
_url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part)
_render_template_re = re.compile(r'\b(render_template\()(%s)' % _string_re_part)
_after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)')
_module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module'
r'\(__name__\s*(?:,\s*(?:name\s*=\s*)?(%s))?' %
_string_re_part)
_error_handler_re = re.compile(r'%s\.error_handlers\[\s*(\d+)\s*\]' % _app_re_part)
_mod_route_re = re.compile(r'@([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route')
_blueprint_related = [
(re.compile(r'request\.module'), 'request.blueprint'),
(re.compile(r'register_module'), 'register_blueprint'),
(re.compile(r'%s\.modules' % _app_re_part), '\\1.blueprints')
]
def make_diff(filename, old, new):
for line in difflib.unified_diff(old.splitlines(), new.splitlines(),
posixpath.normpath(posixpath.join('a', filename)),
posixpath.normpath(posixpath.join('b', filename)),
lineterm=''):
print line
def looks_like_teardown_function(node):
returns = [x for x in ast.walk(node) if isinstance(x, ast.Return)]
if len(returns) != 1:
return
return_def = returns[0]
resp_name = node.args.args[0]
if not isinstance(return_def.value, ast.Name) or \
return_def.value.id != resp_name.id:
return
for body_node in node.body:
for child in ast.walk(body_node):
if isinstance(child, ast.Name) and \
child.id == resp_name.id:
if child is not return_def.value:
return
return resp_name.id
def fix_url_for(contents, module_declarations=None):
if module_declarations is None:
skip_module_test = True
else:
skip_module_test = False
mapping = dict(module_declarations)
annotated_lines = []
def make_line_annotations():
if not annotated_lines:
last_index = 0
for line in contents.splitlines(True):
last_index += len(line)
annotated_lines.append((last_index, line))
def backtrack_module_name(call_start):
make_line_annotations()
for idx, (line_end, line) in enumerate(annotated_lines):
if line_end > call_start:
for _, line in reversed(annotated_lines[:idx]):
match = _mod_route_re.search(line)
if match is not None:
shortname = match.group(1)
return mapping.get(shortname)
def handle_match(match):
if not skip_module_test:
modname = backtrack_module_name(match.start())
if modname is None:
return match.group(0)
prefix = match.group(1)
endpoint = ast.literal_eval(match.group(2))
if endpoint.startswith('.'):
endpoint = endpoint[1:]
elif '.' not in endpoint:
endpoint = '.' + endpoint
else:
return match.group(0)
return prefix + repr(endpoint)
return _url_for_re.sub(handle_match, contents)
def fix_teardown_funcs(contents):
def is_return_line(line):
args = line.strip().split()
return args and args[0] == 'return'
def fix_single(match, lines, lineno):
if not lines[lineno + 1].startswith('def'):
return
block_lines = inspect.getblock(lines[lineno + 1:])
func_code = ''.join(block_lines)
if func_code[0].isspace():
node = ast.parse('if 1:\n' + func_code).body[0].body
else:
node = ast.parse(func_code).body[0]
response_param_name = looks_like_teardown_function(node)
if response_param_name is None:
return
before = lines[:lineno]
decorator = [match.group(1) +
match.group(2).replace('after_', 'teardown_') +
match.group(3)]
body = [line.replace(response_param_name, 'exception')
for line in block_lines if
not is_return_line(line)]
after = lines[lineno + len(block_lines) + 1:]
return before + decorator + body + after
content_lines = contents.splitlines(True)
while 1:
found_one = False
for idx, line in enumerate(content_lines):
match = _after_request_re.match(line)
if match is None:
continue
new_content_lines = fix_single(match, content_lines, idx)
if new_content_lines is not None:
content_lines = new_content_lines
break
else:
break
return ''.join(content_lines)
def get_module_autoname(filename):
directory, filename = os.path.split(filename)
if filename != '__init__.py':
return os.path.splitext(filename)[0]
return os.path.basename(directory)
def rewrite_from_imports(prefix, fromlist, lineiter):
import_block = [prefix, fromlist]
if fromlist[0] == '(' and fromlist[-1] != ')':
for line in lineiter:
import_block.append(line)
if line.rstrip().endswith(')'):
break
elif fromlist[-1] == '\\':
for line in lineiter:
import_block.append(line)
if line.rstrip().endswith('\\'):
break
return ''.join(import_block).replace('Module', 'Blueprint')
def rewrite_blueprint_imports(contents):
new_file = []
lineiter = iter(contents.splitlines(True))
for line in lineiter:
match = _from_import_re.search(line)
if match is not None:
new_file.extend(rewrite_from_imports(match.group(),
line[match.end():],
lineiter))
else:
new_file.append(line)
return ''.join(new_file)
def rewrite_for_blueprints(contents, filename):
modules_declared = []
def handle_match(match):
target = match.group(1)
name_param = match.group(2)
if name_param is None:
modname = get_module_autoname(filename)
else:
modname = ast.literal_eval(name_param)
modules_declared.append((target, modname))
return '%s = %s' % (target, 'Blueprint(%r, __name__' % modname)
new_contents = _module_constructor_re.sub(handle_match, contents)
if modules_declared:
new_contents = rewrite_blueprint_imports(new_contents)
for pattern, replacement in _blueprint_related:
new_contents = pattern.sub(replacement, new_contents)
return new_contents, dict(modules_declared)
def upgrade_python_file(filename, contents, teardown):
new_contents = contents
if teardown:
new_contents = fix_teardown_funcs(new_contents)
new_contents, modules = rewrite_for_blueprints(new_contents, filename)
new_contents = fix_url_for(new_contents, modules)
new_contents = _error_handler_re.sub('\\1.error_handler_spec[None][\\2]',
new_contents)
make_diff(filename, contents, new_contents)
def upgrade_template_file(filename, contents):
new_contents = fix_url_for(contents, None)
make_diff(filename, contents, new_contents)
def walk_path(path):
this_file = os.path.realpath(__file__).rstrip('c')
for dirpath, dirnames, filenames in os.walk(path):
dirnames[:] = [x for x in dirnames if not x.startswith('.')]
for filename in filenames:
filename = os.path.join(dirpath, filename)
if os.path.realpath(filename) == this_file:
continue
if filename.endswith('.py'):
yield filename, 'python'
# skip files that are diffs. These might be false positives
# when run multiple times.
elif not filename.endswith(('.diff', '.patch', '.udiff')):
with open(filename) as f:
contents = f.read(TEMPLATE_LOOKAHEAD)
if '{% for' or '{% if' or '{{ url_for' in contents:
yield filename, 'template'
def scan_path(path=None, teardown=True):
for filename, type in walk_path(path):
with open(filename) as f:
contents = f.read()
if type == 'python':
upgrade_python_file(filename, contents, teardown)
elif type == 'template':
upgrade_template_file(filename, contents)
def main():
"""Entrypoint"""
parser = OptionParser(usage='%prog [options] [paths]')
parser.add_option('-T', '--no-teardown-detection', dest='no_teardown',
action='store_true', help='Do not attempt to '
'detect teardown function rewrites.')
parser.add_option('-b', '--bundled-templates', dest='bundled_tmpl',
action='store_true', help='Indicate to the system '
'that templates are bundled with modules. Default '
'is auto detect.')
options, args = parser.parse_args()
if not args:
args = ['.']
if ast is None:
parser.error('Python 2.6 or later is required to run the upgrade script.')
for path in args:
scan_path(path, teardown=not options.no_teardown)
if __name__ == '__main__':
main()