##// END OF EJS Templates
import-checker: increase portability for python 2.6.x...
FUJIWARA Katsunori -
r29374:7712fcde default
parent child Browse files
Show More
@@ -1,717 +1,717 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 import ast
5 import ast
6 import collections
6 import collections
7 import os
7 import os
8 import re
8 import re
9 import sys
9 import sys
10
10
11 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
11 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
12 # to work when run from a virtualenv. The modules were chosen empirically
12 # to work when run from a virtualenv. The modules were chosen empirically
13 # so that the return value matches the return value without virtualenv.
13 # so that the return value matches the return value without virtualenv.
14 if True: # disable lexical sorting checks
14 if True: # disable lexical sorting checks
15 import BaseHTTPServer
15 import BaseHTTPServer
16 import zlib
16 import zlib
17
17
18 # Whitelist of modules that symbols can be directly imported from.
18 # Whitelist of modules that symbols can be directly imported from.
19 allowsymbolimports = (
19 allowsymbolimports = (
20 '__future__',
20 '__future__',
21 'mercurial.hgweb.common',
21 'mercurial.hgweb.common',
22 'mercurial.hgweb.request',
22 'mercurial.hgweb.request',
23 'mercurial.i18n',
23 'mercurial.i18n',
24 'mercurial.node',
24 'mercurial.node',
25 )
25 )
26
26
27 # Modules that must be aliased because they are commonly confused with
27 # Modules that must be aliased because they are commonly confused with
28 # common variables and can create aliasing and readability issues.
28 # common variables and can create aliasing and readability issues.
29 requirealias = {
29 requirealias = {
30 'ui': 'uimod',
30 'ui': 'uimod',
31 }
31 }
32
32
33 def usingabsolute(root):
33 def usingabsolute(root):
34 """Whether absolute imports are being used."""
34 """Whether absolute imports are being used."""
35 if sys.version_info[0] >= 3:
35 if sys.version_info[0] >= 3:
36 return True
36 return True
37
37
38 for node in ast.walk(root):
38 for node in ast.walk(root):
39 if isinstance(node, ast.ImportFrom):
39 if isinstance(node, ast.ImportFrom):
40 if node.module == '__future__':
40 if node.module == '__future__':
41 for n in node.names:
41 for n in node.names:
42 if n.name == 'absolute_import':
42 if n.name == 'absolute_import':
43 return True
43 return True
44
44
45 return False
45 return False
46
46
47 def walklocal(root):
47 def walklocal(root):
48 """Recursively yield all descendant nodes but not in a different scope"""
48 """Recursively yield all descendant nodes but not in a different scope"""
49 todo = collections.deque(ast.iter_child_nodes(root))
49 todo = collections.deque(ast.iter_child_nodes(root))
50 yield root, False
50 yield root, False
51 while todo:
51 while todo:
52 node = todo.popleft()
52 node = todo.popleft()
53 newscope = isinstance(node, ast.FunctionDef)
53 newscope = isinstance(node, ast.FunctionDef)
54 if not newscope:
54 if not newscope:
55 todo.extend(ast.iter_child_nodes(node))
55 todo.extend(ast.iter_child_nodes(node))
56 yield node, newscope
56 yield node, newscope
57
57
58 def dotted_name_of_path(path, trimpure=False):
58 def dotted_name_of_path(path, trimpure=False):
59 """Given a relative path to a source file, return its dotted module name.
59 """Given a relative path to a source file, return its dotted module name.
60
60
61 >>> dotted_name_of_path('mercurial/error.py')
61 >>> dotted_name_of_path('mercurial/error.py')
62 'mercurial.error'
62 'mercurial.error'
63 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
63 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
64 'mercurial.parsers'
64 'mercurial.parsers'
65 >>> dotted_name_of_path('zlibmodule.so')
65 >>> dotted_name_of_path('zlibmodule.so')
66 'zlib'
66 'zlib'
67 """
67 """
68 parts = path.replace(os.sep, '/').split('/')
68 parts = path.replace(os.sep, '/').split('/')
69 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
69 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
70 if parts[-1].endswith('module'):
70 if parts[-1].endswith('module'):
71 parts[-1] = parts[-1][:-6]
71 parts[-1] = parts[-1][:-6]
72 if trimpure:
72 if trimpure:
73 return '.'.join(p for p in parts if p != 'pure')
73 return '.'.join(p for p in parts if p != 'pure')
74 return '.'.join(parts)
74 return '.'.join(parts)
75
75
76 def fromlocalfunc(modulename, localmods):
76 def fromlocalfunc(modulename, localmods):
77 """Get a function to examine which locally defined module the
77 """Get a function to examine which locally defined module the
78 target source imports via a specified name.
78 target source imports via a specified name.
79
79
80 `modulename` is an `dotted_name_of_path()`-ed source file path,
80 `modulename` is an `dotted_name_of_path()`-ed source file path,
81 which may have `.__init__` at the end of it, of the target source.
81 which may have `.__init__` at the end of it, of the target source.
82
82
83 `localmods` is a dict (or set), of which key is an absolute
83 `localmods` is a dict (or set), of which key is an absolute
84 `dotted_name_of_path()`-ed source file path of locally defined (=
84 `dotted_name_of_path()`-ed source file path of locally defined (=
85 Mercurial specific) modules.
85 Mercurial specific) modules.
86
86
87 This function assumes that module names not existing in
87 This function assumes that module names not existing in
88 `localmods` are from the Python standard library.
88 `localmods` are from the Python standard library.
89
89
90 This function returns the function, which takes `name` argument,
90 This function returns the function, which takes `name` argument,
91 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
91 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
92 matches against locally defined module. Otherwise, it returns
92 matches against locally defined module. Otherwise, it returns
93 False.
93 False.
94
94
95 It is assumed that `name` doesn't have `.__init__`.
95 It is assumed that `name` doesn't have `.__init__`.
96
96
97 `absname` is an absolute module name of specified `name`
97 `absname` is an absolute module name of specified `name`
98 (e.g. "hgext.convert"). This can be used to compose prefix for sub
98 (e.g. "hgext.convert"). This can be used to compose prefix for sub
99 modules or so.
99 modules or so.
100
100
101 `dottedpath` is a `dotted_name_of_path()`-ed source file path
101 `dottedpath` is a `dotted_name_of_path()`-ed source file path
102 (e.g. "hgext.convert.__init__") of `name`. This is used to look
102 (e.g. "hgext.convert.__init__") of `name`. This is used to look
103 module up in `localmods` again.
103 module up in `localmods` again.
104
104
105 `hassubmod` is whether it may have sub modules under it (for
105 `hassubmod` is whether it may have sub modules under it (for
106 convenient, even though this is also equivalent to "absname !=
106 convenient, even though this is also equivalent to "absname !=
107 dottednpath")
107 dottednpath")
108
108
109 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
109 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
110 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
110 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
111 ... 'baz.__init__': True, 'baz.baz1': True }
111 ... 'baz.__init__': True, 'baz.baz1': True }
112 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
112 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
113 >>> # relative
113 >>> # relative
114 >>> fromlocal('foo1')
114 >>> fromlocal('foo1')
115 ('foo.foo1', 'foo.foo1', False)
115 ('foo.foo1', 'foo.foo1', False)
116 >>> fromlocal('bar')
116 >>> fromlocal('bar')
117 ('foo.bar', 'foo.bar.__init__', True)
117 ('foo.bar', 'foo.bar.__init__', True)
118 >>> fromlocal('bar.bar1')
118 >>> fromlocal('bar.bar1')
119 ('foo.bar.bar1', 'foo.bar.bar1', False)
119 ('foo.bar.bar1', 'foo.bar.bar1', False)
120 >>> # absolute
120 >>> # absolute
121 >>> fromlocal('baz')
121 >>> fromlocal('baz')
122 ('baz', 'baz.__init__', True)
122 ('baz', 'baz.__init__', True)
123 >>> fromlocal('baz.baz1')
123 >>> fromlocal('baz.baz1')
124 ('baz.baz1', 'baz.baz1', False)
124 ('baz.baz1', 'baz.baz1', False)
125 >>> # unknown = maybe standard library
125 >>> # unknown = maybe standard library
126 >>> fromlocal('os')
126 >>> fromlocal('os')
127 False
127 False
128 >>> fromlocal(None, 1)
128 >>> fromlocal(None, 1)
129 ('foo', 'foo.__init__', True)
129 ('foo', 'foo.__init__', True)
130 >>> fromlocal('foo1', 1)
130 >>> fromlocal('foo1', 1)
131 ('foo.foo1', 'foo.foo1', False)
131 ('foo.foo1', 'foo.foo1', False)
132 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
132 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
133 >>> fromlocal2(None, 2)
133 >>> fromlocal2(None, 2)
134 ('foo', 'foo.__init__', True)
134 ('foo', 'foo.__init__', True)
135 >>> fromlocal2('bar2', 1)
135 >>> fromlocal2('bar2', 1)
136 False
136 False
137 >>> fromlocal2('bar', 2)
137 >>> fromlocal2('bar', 2)
138 ('foo.bar', 'foo.bar.__init__', True)
138 ('foo.bar', 'foo.bar.__init__', True)
139 """
139 """
140 prefix = '.'.join(modulename.split('.')[:-1])
140 prefix = '.'.join(modulename.split('.')[:-1])
141 if prefix:
141 if prefix:
142 prefix += '.'
142 prefix += '.'
143 def fromlocal(name, level=0):
143 def fromlocal(name, level=0):
144 # name is None when relative imports are used.
144 # name is false value when relative imports are used.
145 if name is None:
145 if not name:
146 # If relative imports are used, level must not be absolute.
146 # If relative imports are used, level must not be absolute.
147 assert level > 0
147 assert level > 0
148 candidates = ['.'.join(modulename.split('.')[:-level])]
148 candidates = ['.'.join(modulename.split('.')[:-level])]
149 else:
149 else:
150 if not level:
150 if not level:
151 # Check relative name first.
151 # Check relative name first.
152 candidates = [prefix + name, name]
152 candidates = [prefix + name, name]
153 else:
153 else:
154 candidates = ['.'.join(modulename.split('.')[:-level]) +
154 candidates = ['.'.join(modulename.split('.')[:-level]) +
155 '.' + name]
155 '.' + name]
156
156
157 for n in candidates:
157 for n in candidates:
158 if n in localmods:
158 if n in localmods:
159 return (n, n, False)
159 return (n, n, False)
160 dottedpath = n + '.__init__'
160 dottedpath = n + '.__init__'
161 if dottedpath in localmods:
161 if dottedpath in localmods:
162 return (n, dottedpath, True)
162 return (n, dottedpath, True)
163 return False
163 return False
164 return fromlocal
164 return fromlocal
165
165
166 def list_stdlib_modules():
166 def list_stdlib_modules():
167 """List the modules present in the stdlib.
167 """List the modules present in the stdlib.
168
168
169 >>> mods = set(list_stdlib_modules())
169 >>> mods = set(list_stdlib_modules())
170 >>> 'BaseHTTPServer' in mods
170 >>> 'BaseHTTPServer' in mods
171 True
171 True
172
172
173 os.path isn't really a module, so it's missing:
173 os.path isn't really a module, so it's missing:
174
174
175 >>> 'os.path' in mods
175 >>> 'os.path' in mods
176 False
176 False
177
177
178 sys requires special treatment, because it's baked into the
178 sys requires special treatment, because it's baked into the
179 interpreter, but it should still appear:
179 interpreter, but it should still appear:
180
180
181 >>> 'sys' in mods
181 >>> 'sys' in mods
182 True
182 True
183
183
184 >>> 'collections' in mods
184 >>> 'collections' in mods
185 True
185 True
186
186
187 >>> 'cStringIO' in mods
187 >>> 'cStringIO' in mods
188 True
188 True
189 """
189 """
190 for m in sys.builtin_module_names:
190 for m in sys.builtin_module_names:
191 yield m
191 yield m
192 # These modules only exist on windows, but we should always
192 # These modules only exist on windows, but we should always
193 # consider them stdlib.
193 # consider them stdlib.
194 for m in ['msvcrt', '_winreg']:
194 for m in ['msvcrt', '_winreg']:
195 yield m
195 yield m
196 yield 'builtins' # python3 only
196 yield 'builtins' # python3 only
197 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
197 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
198 yield m
198 yield m
199 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
199 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
200 yield m
200 yield m
201 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
201 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
202 # We need to supplement the list of prefixes for the search to work
202 # We need to supplement the list of prefixes for the search to work
203 # when run from within a virtualenv.
203 # when run from within a virtualenv.
204 for mod in (BaseHTTPServer, zlib):
204 for mod in (BaseHTTPServer, zlib):
205 try:
205 try:
206 # Not all module objects have a __file__ attribute.
206 # Not all module objects have a __file__ attribute.
207 filename = mod.__file__
207 filename = mod.__file__
208 except AttributeError:
208 except AttributeError:
209 continue
209 continue
210 dirname = os.path.dirname(filename)
210 dirname = os.path.dirname(filename)
211 for prefix in stdlib_prefixes:
211 for prefix in stdlib_prefixes:
212 if dirname.startswith(prefix):
212 if dirname.startswith(prefix):
213 # Then this directory is redundant.
213 # Then this directory is redundant.
214 break
214 break
215 else:
215 else:
216 stdlib_prefixes.add(dirname)
216 stdlib_prefixes.add(dirname)
217 for libpath in sys.path:
217 for libpath in sys.path:
218 # We want to walk everything in sys.path that starts with
218 # We want to walk everything in sys.path that starts with
219 # something in stdlib_prefixes.
219 # something in stdlib_prefixes.
220 if not any(libpath.startswith(p) for p in stdlib_prefixes):
220 if not any(libpath.startswith(p) for p in stdlib_prefixes):
221 continue
221 continue
222 for top, dirs, files in os.walk(libpath):
222 for top, dirs, files in os.walk(libpath):
223 for i, d in reversed(list(enumerate(dirs))):
223 for i, d in reversed(list(enumerate(dirs))):
224 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
224 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
225 or top == libpath and d in ('hgext', 'mercurial')):
225 or top == libpath and d in ('hgext', 'mercurial')):
226 del dirs[i]
226 del dirs[i]
227 for name in files:
227 for name in files:
228 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
228 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
229 continue
229 continue
230 if name.startswith('__init__.py'):
230 if name.startswith('__init__.py'):
231 full_path = top
231 full_path = top
232 else:
232 else:
233 full_path = os.path.join(top, name)
233 full_path = os.path.join(top, name)
234 rel_path = full_path[len(libpath) + 1:]
234 rel_path = full_path[len(libpath) + 1:]
235 mod = dotted_name_of_path(rel_path)
235 mod = dotted_name_of_path(rel_path)
236 yield mod
236 yield mod
237
237
238 stdlib_modules = set(list_stdlib_modules())
238 stdlib_modules = set(list_stdlib_modules())
239
239
240 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
240 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
241 """Given the source of a file as a string, yield the names
241 """Given the source of a file as a string, yield the names
242 imported by that file.
242 imported by that file.
243
243
244 Args:
244 Args:
245 source: The python source to examine as a string.
245 source: The python source to examine as a string.
246 modulename: of specified python source (may have `__init__`)
246 modulename: of specified python source (may have `__init__`)
247 localmods: dict of locally defined module names (may have `__init__`)
247 localmods: dict of locally defined module names (may have `__init__`)
248 ignore_nested: If true, import statements that do not start in
248 ignore_nested: If true, import statements that do not start in
249 column zero will be ignored.
249 column zero will be ignored.
250
250
251 Returns:
251 Returns:
252 A list of absolute module names imported by the given source.
252 A list of absolute module names imported by the given source.
253
253
254 >>> f = 'foo/xxx.py'
254 >>> f = 'foo/xxx.py'
255 >>> modulename = 'foo.xxx'
255 >>> modulename = 'foo.xxx'
256 >>> localmods = {'foo.__init__': True,
256 >>> localmods = {'foo.__init__': True,
257 ... 'foo.foo1': True, 'foo.foo2': True,
257 ... 'foo.foo1': True, 'foo.foo2': True,
258 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
258 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
259 ... 'baz.__init__': True, 'baz.baz1': True }
259 ... 'baz.__init__': True, 'baz.baz1': True }
260 >>> # standard library (= not locally defined ones)
260 >>> # standard library (= not locally defined ones)
261 >>> sorted(imported_modules(
261 >>> sorted(imported_modules(
262 ... 'from stdlib1 import foo, bar; import stdlib2',
262 ... 'from stdlib1 import foo, bar; import stdlib2',
263 ... modulename, f, localmods))
263 ... modulename, f, localmods))
264 []
264 []
265 >>> # relative importing
265 >>> # relative importing
266 >>> sorted(imported_modules(
266 >>> sorted(imported_modules(
267 ... 'import foo1; from bar import bar1',
267 ... 'import foo1; from bar import bar1',
268 ... modulename, f, localmods))
268 ... modulename, f, localmods))
269 ['foo.bar.bar1', 'foo.foo1']
269 ['foo.bar.bar1', 'foo.foo1']
270 >>> sorted(imported_modules(
270 >>> sorted(imported_modules(
271 ... 'from bar.bar1 import name1, name2, name3',
271 ... 'from bar.bar1 import name1, name2, name3',
272 ... modulename, f, localmods))
272 ... modulename, f, localmods))
273 ['foo.bar.bar1']
273 ['foo.bar.bar1']
274 >>> # absolute importing
274 >>> # absolute importing
275 >>> sorted(imported_modules(
275 >>> sorted(imported_modules(
276 ... 'from baz import baz1, name1',
276 ... 'from baz import baz1, name1',
277 ... modulename, f, localmods))
277 ... modulename, f, localmods))
278 ['baz.__init__', 'baz.baz1']
278 ['baz.__init__', 'baz.baz1']
279 >>> # mixed importing, even though it shouldn't be recommended
279 >>> # mixed importing, even though it shouldn't be recommended
280 >>> sorted(imported_modules(
280 >>> sorted(imported_modules(
281 ... 'import stdlib, foo1, baz',
281 ... 'import stdlib, foo1, baz',
282 ... modulename, f, localmods))
282 ... modulename, f, localmods))
283 ['baz.__init__', 'foo.foo1']
283 ['baz.__init__', 'foo.foo1']
284 >>> # ignore_nested
284 >>> # ignore_nested
285 >>> sorted(imported_modules(
285 >>> sorted(imported_modules(
286 ... '''import foo
286 ... '''import foo
287 ... def wat():
287 ... def wat():
288 ... import bar
288 ... import bar
289 ... ''', modulename, f, localmods))
289 ... ''', modulename, f, localmods))
290 ['foo.__init__', 'foo.bar.__init__']
290 ['foo.__init__', 'foo.bar.__init__']
291 >>> sorted(imported_modules(
291 >>> sorted(imported_modules(
292 ... '''import foo
292 ... '''import foo
293 ... def wat():
293 ... def wat():
294 ... import bar
294 ... import bar
295 ... ''', modulename, f, localmods, ignore_nested=True))
295 ... ''', modulename, f, localmods, ignore_nested=True))
296 ['foo.__init__']
296 ['foo.__init__']
297 """
297 """
298 fromlocal = fromlocalfunc(modulename, localmods)
298 fromlocal = fromlocalfunc(modulename, localmods)
299 for node in ast.walk(ast.parse(source, f)):
299 for node in ast.walk(ast.parse(source, f)):
300 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
300 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
301 continue
301 continue
302 if isinstance(node, ast.Import):
302 if isinstance(node, ast.Import):
303 for n in node.names:
303 for n in node.names:
304 found = fromlocal(n.name)
304 found = fromlocal(n.name)
305 if not found:
305 if not found:
306 # this should import standard library
306 # this should import standard library
307 continue
307 continue
308 yield found[1]
308 yield found[1]
309 elif isinstance(node, ast.ImportFrom):
309 elif isinstance(node, ast.ImportFrom):
310 found = fromlocal(node.module, node.level)
310 found = fromlocal(node.module, node.level)
311 if not found:
311 if not found:
312 # this should import standard library
312 # this should import standard library
313 continue
313 continue
314
314
315 absname, dottedpath, hassubmod = found
315 absname, dottedpath, hassubmod = found
316 if not hassubmod:
316 if not hassubmod:
317 # "dottedpath" is not a package; must be imported
317 # "dottedpath" is not a package; must be imported
318 yield dottedpath
318 yield dottedpath
319 # examination of "node.names" should be redundant
319 # examination of "node.names" should be redundant
320 # e.g.: from mercurial.node import nullid, nullrev
320 # e.g.: from mercurial.node import nullid, nullrev
321 continue
321 continue
322
322
323 modnotfound = False
323 modnotfound = False
324 prefix = absname + '.'
324 prefix = absname + '.'
325 for n in node.names:
325 for n in node.names:
326 found = fromlocal(prefix + n.name)
326 found = fromlocal(prefix + n.name)
327 if not found:
327 if not found:
328 # this should be a function or a property of "node.module"
328 # this should be a function or a property of "node.module"
329 modnotfound = True
329 modnotfound = True
330 continue
330 continue
331 yield found[1]
331 yield found[1]
332 if modnotfound:
332 if modnotfound:
333 # "dottedpath" is a package, but imported because of non-module
333 # "dottedpath" is a package, but imported because of non-module
334 # lookup
334 # lookup
335 yield dottedpath
335 yield dottedpath
336
336
337 def verify_import_convention(module, source, localmods):
337 def verify_import_convention(module, source, localmods):
338 """Verify imports match our established coding convention.
338 """Verify imports match our established coding convention.
339
339
340 We have 2 conventions: legacy and modern. The modern convention is in
340 We have 2 conventions: legacy and modern. The modern convention is in
341 effect when using absolute imports.
341 effect when using absolute imports.
342
342
343 The legacy convention only looks for mixed imports. The modern convention
343 The legacy convention only looks for mixed imports. The modern convention
344 is much more thorough.
344 is much more thorough.
345 """
345 """
346 root = ast.parse(source)
346 root = ast.parse(source)
347 absolute = usingabsolute(root)
347 absolute = usingabsolute(root)
348
348
349 if absolute:
349 if absolute:
350 return verify_modern_convention(module, root, localmods)
350 return verify_modern_convention(module, root, localmods)
351 else:
351 else:
352 return verify_stdlib_on_own_line(root)
352 return verify_stdlib_on_own_line(root)
353
353
354 def verify_modern_convention(module, root, localmods, root_col_offset=0):
354 def verify_modern_convention(module, root, localmods, root_col_offset=0):
355 """Verify a file conforms to the modern import convention rules.
355 """Verify a file conforms to the modern import convention rules.
356
356
357 The rules of the modern convention are:
357 The rules of the modern convention are:
358
358
359 * Ordering is stdlib followed by local imports. Each group is lexically
359 * Ordering is stdlib followed by local imports. Each group is lexically
360 sorted.
360 sorted.
361 * Importing multiple modules via "import X, Y" is not allowed: use
361 * Importing multiple modules via "import X, Y" is not allowed: use
362 separate import statements.
362 separate import statements.
363 * Importing multiple modules via "from X import ..." is allowed if using
363 * Importing multiple modules via "from X import ..." is allowed if using
364 parenthesis and one entry per line.
364 parenthesis and one entry per line.
365 * Only 1 relative import statement per import level ("from .", "from ..")
365 * Only 1 relative import statement per import level ("from .", "from ..")
366 is allowed.
366 is allowed.
367 * Relative imports from higher levels must occur before lower levels. e.g.
367 * Relative imports from higher levels must occur before lower levels. e.g.
368 "from .." must be before "from .".
368 "from .." must be before "from .".
369 * Imports from peer packages should use relative import (e.g. do not
369 * Imports from peer packages should use relative import (e.g. do not
370 "import mercurial.foo" from a "mercurial.*" module).
370 "import mercurial.foo" from a "mercurial.*" module).
371 * Symbols can only be imported from specific modules (see
371 * Symbols can only be imported from specific modules (see
372 `allowsymbolimports`). For other modules, first import the module then
372 `allowsymbolimports`). For other modules, first import the module then
373 assign the symbol to a module-level variable. In addition, these imports
373 assign the symbol to a module-level variable. In addition, these imports
374 must be performed before other local imports. This rule only
374 must be performed before other local imports. This rule only
375 applies to import statements outside of any blocks.
375 applies to import statements outside of any blocks.
376 * Relative imports from the standard library are not allowed.
376 * Relative imports from the standard library are not allowed.
377 * Certain modules must be aliased to alternate names to avoid aliasing
377 * Certain modules must be aliased to alternate names to avoid aliasing
378 and readability problems. See `requirealias`.
378 and readability problems. See `requirealias`.
379 """
379 """
380 topmodule = module.split('.')[0]
380 topmodule = module.split('.')[0]
381 fromlocal = fromlocalfunc(module, localmods)
381 fromlocal = fromlocalfunc(module, localmods)
382
382
383 # Whether a local/non-stdlib import has been performed.
383 # Whether a local/non-stdlib import has been performed.
384 seenlocal = None
384 seenlocal = None
385 # Whether a local/non-stdlib, non-symbol import has been seen.
385 # Whether a local/non-stdlib, non-symbol import has been seen.
386 seennonsymbollocal = False
386 seennonsymbollocal = False
387 # The last name to be imported (for sorting).
387 # The last name to be imported (for sorting).
388 lastname = None
388 lastname = None
389 # Relative import levels encountered so far.
389 # Relative import levels encountered so far.
390 seenlevels = set()
390 seenlevels = set()
391
391
392 for node, newscope in walklocal(root):
392 for node, newscope in walklocal(root):
393 def msg(fmt, *args):
393 def msg(fmt, *args):
394 return (fmt % args, node.lineno)
394 return (fmt % args, node.lineno)
395 if newscope:
395 if newscope:
396 # Check for local imports in function
396 # Check for local imports in function
397 for r in verify_modern_convention(module, node, localmods,
397 for r in verify_modern_convention(module, node, localmods,
398 node.col_offset + 4):
398 node.col_offset + 4):
399 yield r
399 yield r
400 elif isinstance(node, ast.Import):
400 elif isinstance(node, ast.Import):
401 # Disallow "import foo, bar" and require separate imports
401 # Disallow "import foo, bar" and require separate imports
402 # for each module.
402 # for each module.
403 if len(node.names) > 1:
403 if len(node.names) > 1:
404 yield msg('multiple imported names: %s',
404 yield msg('multiple imported names: %s',
405 ', '.join(n.name for n in node.names))
405 ', '.join(n.name for n in node.names))
406
406
407 name = node.names[0].name
407 name = node.names[0].name
408 asname = node.names[0].asname
408 asname = node.names[0].asname
409
409
410 # Ignore sorting rules on imports inside blocks.
410 # Ignore sorting rules on imports inside blocks.
411 if node.col_offset == root_col_offset:
411 if node.col_offset == root_col_offset:
412 if lastname and name < lastname:
412 if lastname and name < lastname:
413 yield msg('imports not lexically sorted: %s < %s',
413 yield msg('imports not lexically sorted: %s < %s',
414 name, lastname)
414 name, lastname)
415
415
416 lastname = name
416 lastname = name
417
417
418 # stdlib imports should be before local imports.
418 # stdlib imports should be before local imports.
419 stdlib = name in stdlib_modules
419 stdlib = name in stdlib_modules
420 if stdlib and seenlocal and node.col_offset == root_col_offset:
420 if stdlib and seenlocal and node.col_offset == root_col_offset:
421 yield msg('stdlib import "%s" follows local import: %s',
421 yield msg('stdlib import "%s" follows local import: %s',
422 name, seenlocal)
422 name, seenlocal)
423
423
424 if not stdlib:
424 if not stdlib:
425 seenlocal = name
425 seenlocal = name
426
426
427 # Import of sibling modules should use relative imports.
427 # Import of sibling modules should use relative imports.
428 topname = name.split('.')[0]
428 topname = name.split('.')[0]
429 if topname == topmodule:
429 if topname == topmodule:
430 yield msg('import should be relative: %s', name)
430 yield msg('import should be relative: %s', name)
431
431
432 if name in requirealias and asname != requirealias[name]:
432 if name in requirealias and asname != requirealias[name]:
433 yield msg('%s module must be "as" aliased to %s',
433 yield msg('%s module must be "as" aliased to %s',
434 name, requirealias[name])
434 name, requirealias[name])
435
435
436 elif isinstance(node, ast.ImportFrom):
436 elif isinstance(node, ast.ImportFrom):
437 # Resolve the full imported module name.
437 # Resolve the full imported module name.
438 if node.level > 0:
438 if node.level > 0:
439 fullname = '.'.join(module.split('.')[:-node.level])
439 fullname = '.'.join(module.split('.')[:-node.level])
440 if node.module:
440 if node.module:
441 fullname += '.%s' % node.module
441 fullname += '.%s' % node.module
442 else:
442 else:
443 assert node.module
443 assert node.module
444 fullname = node.module
444 fullname = node.module
445
445
446 topname = fullname.split('.')[0]
446 topname = fullname.split('.')[0]
447 if topname == topmodule:
447 if topname == topmodule:
448 yield msg('import should be relative: %s', fullname)
448 yield msg('import should be relative: %s', fullname)
449
449
450 # __future__ is special since it needs to come first and use
450 # __future__ is special since it needs to come first and use
451 # symbol import.
451 # symbol import.
452 if fullname != '__future__':
452 if fullname != '__future__':
453 if not fullname or fullname in stdlib_modules:
453 if not fullname or fullname in stdlib_modules:
454 yield msg('relative import of stdlib module')
454 yield msg('relative import of stdlib module')
455 else:
455 else:
456 seenlocal = fullname
456 seenlocal = fullname
457
457
458 # Direct symbol import is only allowed from certain modules and
458 # Direct symbol import is only allowed from certain modules and
459 # must occur before non-symbol imports.
459 # must occur before non-symbol imports.
460 found = fromlocal(node.module, node.level)
460 found = fromlocal(node.module, node.level)
461 if found and found[2]: # node.module is a package
461 if found and found[2]: # node.module is a package
462 prefix = found[0] + '.'
462 prefix = found[0] + '.'
463 symbols = [n.name for n in node.names
463 symbols = [n.name for n in node.names
464 if not fromlocal(prefix + n.name)]
464 if not fromlocal(prefix + n.name)]
465 else:
465 else:
466 symbols = [n.name for n in node.names]
466 symbols = [n.name for n in node.names]
467 if node.module and node.col_offset == root_col_offset:
467 if node.module and node.col_offset == root_col_offset:
468 if symbols and fullname not in allowsymbolimports:
468 if symbols and fullname not in allowsymbolimports:
469 yield msg('direct symbol import %s from %s',
469 yield msg('direct symbol import %s from %s',
470 ', '.join(symbols), fullname)
470 ', '.join(symbols), fullname)
471
471
472 if symbols and seennonsymbollocal:
472 if symbols and seennonsymbollocal:
473 yield msg('symbol import follows non-symbol import: %s',
473 yield msg('symbol import follows non-symbol import: %s',
474 fullname)
474 fullname)
475 if not symbols and fullname not in stdlib_modules:
475 if not symbols and fullname not in stdlib_modules:
476 seennonsymbollocal = True
476 seennonsymbollocal = True
477
477
478 if not node.module:
478 if not node.module:
479 assert node.level
479 assert node.level
480
480
481 # Only allow 1 group per level.
481 # Only allow 1 group per level.
482 if (node.level in seenlevels
482 if (node.level in seenlevels
483 and node.col_offset == root_col_offset):
483 and node.col_offset == root_col_offset):
484 yield msg('multiple "from %s import" statements',
484 yield msg('multiple "from %s import" statements',
485 '.' * node.level)
485 '.' * node.level)
486
486
487 # Higher-level groups come before lower-level groups.
487 # Higher-level groups come before lower-level groups.
488 if any(node.level > l for l in seenlevels):
488 if any(node.level > l for l in seenlevels):
489 yield msg('higher-level import should come first: %s',
489 yield msg('higher-level import should come first: %s',
490 fullname)
490 fullname)
491
491
492 seenlevels.add(node.level)
492 seenlevels.add(node.level)
493
493
494 # Entries in "from .X import ( ... )" lists must be lexically
494 # Entries in "from .X import ( ... )" lists must be lexically
495 # sorted.
495 # sorted.
496 lastentryname = None
496 lastentryname = None
497
497
498 for n in node.names:
498 for n in node.names:
499 if lastentryname and n.name < lastentryname:
499 if lastentryname and n.name < lastentryname:
500 yield msg('imports from %s not lexically sorted: %s < %s',
500 yield msg('imports from %s not lexically sorted: %s < %s',
501 fullname, n.name, lastentryname)
501 fullname, n.name, lastentryname)
502
502
503 lastentryname = n.name
503 lastentryname = n.name
504
504
505 if n.name in requirealias and n.asname != requirealias[n.name]:
505 if n.name in requirealias and n.asname != requirealias[n.name]:
506 yield msg('%s from %s must be "as" aliased to %s',
506 yield msg('%s from %s must be "as" aliased to %s',
507 n.name, fullname, requirealias[n.name])
507 n.name, fullname, requirealias[n.name])
508
508
509 def verify_stdlib_on_own_line(root):
509 def verify_stdlib_on_own_line(root):
510 """Given some python source, verify that stdlib imports are done
510 """Given some python source, verify that stdlib imports are done
511 in separate statements from relative local module imports.
511 in separate statements from relative local module imports.
512
512
513 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
513 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
514 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
514 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
515 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
515 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
516 []
516 []
517 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
517 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
518 []
518 []
519 """
519 """
520 for node in ast.walk(root):
520 for node in ast.walk(root):
521 if isinstance(node, ast.Import):
521 if isinstance(node, ast.Import):
522 from_stdlib = {False: [], True: []}
522 from_stdlib = {False: [], True: []}
523 for n in node.names:
523 for n in node.names:
524 from_stdlib[n.name in stdlib_modules].append(n.name)
524 from_stdlib[n.name in stdlib_modules].append(n.name)
525 if from_stdlib[True] and from_stdlib[False]:
525 if from_stdlib[True] and from_stdlib[False]:
526 yield ('mixed imports\n stdlib: %s\n relative: %s' %
526 yield ('mixed imports\n stdlib: %s\n relative: %s' %
527 (', '.join(sorted(from_stdlib[True])),
527 (', '.join(sorted(from_stdlib[True])),
528 ', '.join(sorted(from_stdlib[False]))), node.lineno)
528 ', '.join(sorted(from_stdlib[False]))), node.lineno)
529
529
530 class CircularImport(Exception):
530 class CircularImport(Exception):
531 pass
531 pass
532
532
533 def checkmod(mod, imports):
533 def checkmod(mod, imports):
534 shortest = {}
534 shortest = {}
535 visit = [[mod]]
535 visit = [[mod]]
536 while visit:
536 while visit:
537 path = visit.pop(0)
537 path = visit.pop(0)
538 for i in sorted(imports.get(path[-1], [])):
538 for i in sorted(imports.get(path[-1], [])):
539 if len(path) < shortest.get(i, 1000):
539 if len(path) < shortest.get(i, 1000):
540 shortest[i] = len(path)
540 shortest[i] = len(path)
541 if i in path:
541 if i in path:
542 if i == path[0]:
542 if i == path[0]:
543 raise CircularImport(path)
543 raise CircularImport(path)
544 continue
544 continue
545 visit.append(path + [i])
545 visit.append(path + [i])
546
546
547 def rotatecycle(cycle):
547 def rotatecycle(cycle):
548 """arrange a cycle so that the lexicographically first module listed first
548 """arrange a cycle so that the lexicographically first module listed first
549
549
550 >>> rotatecycle(['foo', 'bar'])
550 >>> rotatecycle(['foo', 'bar'])
551 ['bar', 'foo', 'bar']
551 ['bar', 'foo', 'bar']
552 """
552 """
553 lowest = min(cycle)
553 lowest = min(cycle)
554 idx = cycle.index(lowest)
554 idx = cycle.index(lowest)
555 return cycle[idx:] + cycle[:idx] + [lowest]
555 return cycle[idx:] + cycle[:idx] + [lowest]
556
556
557 def find_cycles(imports):
557 def find_cycles(imports):
558 """Find cycles in an already-loaded import graph.
558 """Find cycles in an already-loaded import graph.
559
559
560 All module names recorded in `imports` should be absolute one.
560 All module names recorded in `imports` should be absolute one.
561
561
562 >>> from __future__ import print_function
562 >>> from __future__ import print_function
563 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
563 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
564 ... 'top.bar': ['top.baz', 'sys'],
564 ... 'top.bar': ['top.baz', 'sys'],
565 ... 'top.baz': ['top.foo'],
565 ... 'top.baz': ['top.foo'],
566 ... 'top.qux': ['top.foo']}
566 ... 'top.qux': ['top.foo']}
567 >>> print('\\n'.join(sorted(find_cycles(imports))))
567 >>> print('\\n'.join(sorted(find_cycles(imports))))
568 top.bar -> top.baz -> top.foo -> top.bar
568 top.bar -> top.baz -> top.foo -> top.bar
569 top.foo -> top.qux -> top.foo
569 top.foo -> top.qux -> top.foo
570 """
570 """
571 cycles = set()
571 cycles = set()
572 for mod in sorted(imports.keys()):
572 for mod in sorted(imports.keys()):
573 try:
573 try:
574 checkmod(mod, imports)
574 checkmod(mod, imports)
575 except CircularImport as e:
575 except CircularImport as e:
576 cycle = e.args[0]
576 cycle = e.args[0]
577 cycles.add(" -> ".join(rotatecycle(cycle)))
577 cycles.add(" -> ".join(rotatecycle(cycle)))
578 return cycles
578 return cycles
579
579
580 def _cycle_sortkey(c):
580 def _cycle_sortkey(c):
581 return len(c), c
581 return len(c), c
582
582
583 def embedded(f, modname, src):
583 def embedded(f, modname, src):
584 """Extract embedded python code
584 """Extract embedded python code
585
585
586 >>> def test(fn, lines):
586 >>> def test(fn, lines):
587 ... for s, m, f, l in embedded(fn, "example", lines):
587 ... for s, m, f, l in embedded(fn, "example", lines):
588 ... print("%s %s %s" % (m, f, l))
588 ... print("%s %s %s" % (m, f, l))
589 ... print(repr(s))
589 ... print(repr(s))
590 >>> lines = [
590 >>> lines = [
591 ... 'comment',
591 ... 'comment',
592 ... ' >>> from __future__ import print_function',
592 ... ' >>> from __future__ import print_function',
593 ... " >>> ' multiline",
593 ... " >>> ' multiline",
594 ... " ... string'",
594 ... " ... string'",
595 ... ' ',
595 ... ' ',
596 ... 'comment',
596 ... 'comment',
597 ... ' $ cat > foo.py <<EOF',
597 ... ' $ cat > foo.py <<EOF',
598 ... ' > from __future__ import print_function',
598 ... ' > from __future__ import print_function',
599 ... ' > EOF',
599 ... ' > EOF',
600 ... ]
600 ... ]
601 >>> test("example.t", lines)
601 >>> test("example.t", lines)
602 example[2] doctest.py 2
602 example[2] doctest.py 2
603 "from __future__ import print_function\\n' multiline\\nstring'\\n"
603 "from __future__ import print_function\\n' multiline\\nstring'\\n"
604 example[7] foo.py 7
604 example[7] foo.py 7
605 'from __future__ import print_function\\n'
605 'from __future__ import print_function\\n'
606 """
606 """
607 inlinepython = 0
607 inlinepython = 0
608 shpython = 0
608 shpython = 0
609 script = []
609 script = []
610 prefix = 6
610 prefix = 6
611 t = ''
611 t = ''
612 n = 0
612 n = 0
613 for l in src:
613 for l in src:
614 n += 1
614 n += 1
615 if not l.endswith(b'\n'):
615 if not l.endswith(b'\n'):
616 l += b'\n'
616 l += b'\n'
617 if l.startswith(b' >>> '): # python inlines
617 if l.startswith(b' >>> '): # python inlines
618 if shpython:
618 if shpython:
619 print("%s:%d: Parse Error" % (f, n))
619 print("%s:%d: Parse Error" % (f, n))
620 if not inlinepython:
620 if not inlinepython:
621 # We've just entered a Python block.
621 # We've just entered a Python block.
622 inlinepython = n
622 inlinepython = n
623 t = 'doctest.py'
623 t = 'doctest.py'
624 script.append(l[prefix:])
624 script.append(l[prefix:])
625 continue
625 continue
626 if l.startswith(b' ... '): # python inlines
626 if l.startswith(b' ... '): # python inlines
627 script.append(l[prefix:])
627 script.append(l[prefix:])
628 continue
628 continue
629 cat = re.search(r"\$ \s*cat\s*>\s*(\S+\.py)\s*<<\s*EOF", l)
629 cat = re.search(r"\$ \s*cat\s*>\s*(\S+\.py)\s*<<\s*EOF", l)
630 if cat:
630 if cat:
631 if inlinepython:
631 if inlinepython:
632 yield ''.join(script), ("%s[%d]" %
632 yield ''.join(script), ("%s[%d]" %
633 (modname, inlinepython)), t, inlinepython
633 (modname, inlinepython)), t, inlinepython
634 script = []
634 script = []
635 inlinepython = 0
635 inlinepython = 0
636 shpython = n
636 shpython = n
637 t = cat.group(1)
637 t = cat.group(1)
638 continue
638 continue
639 if shpython and l.startswith(b' > '): # sh continuation
639 if shpython and l.startswith(b' > '): # sh continuation
640 if l == b' > EOF\n':
640 if l == b' > EOF\n':
641 yield ''.join(script), ("%s[%d]" %
641 yield ''.join(script), ("%s[%d]" %
642 (modname, shpython)), t, shpython
642 (modname, shpython)), t, shpython
643 script = []
643 script = []
644 shpython = 0
644 shpython = 0
645 else:
645 else:
646 script.append(l[4:])
646 script.append(l[4:])
647 continue
647 continue
648 if inlinepython and l == b' \n':
648 if inlinepython and l == b' \n':
649 yield ''.join(script), ("%s[%d]" %
649 yield ''.join(script), ("%s[%d]" %
650 (modname, inlinepython)), t, inlinepython
650 (modname, inlinepython)), t, inlinepython
651 script = []
651 script = []
652 inlinepython = 0
652 inlinepython = 0
653 continue
653 continue
654
654
655 def sources(f, modname):
655 def sources(f, modname):
656 """Yields possibly multiple sources from a filepath
656 """Yields possibly multiple sources from a filepath
657
657
658 input: filepath, modulename
658 input: filepath, modulename
659 yields: script(string), modulename, filepath, linenumber
659 yields: script(string), modulename, filepath, linenumber
660
660
661 For embedded scripts, the modulename and filepath will be different
661 For embedded scripts, the modulename and filepath will be different
662 from the function arguments. linenumber is an offset relative to
662 from the function arguments. linenumber is an offset relative to
663 the input file.
663 the input file.
664 """
664 """
665 py = False
665 py = False
666 if not f.endswith('.t'):
666 if not f.endswith('.t'):
667 with open(f) as src:
667 with open(f) as src:
668 yield src.read(), modname, f, 0
668 yield src.read(), modname, f, 0
669 py = True
669 py = True
670 if py or f.endswith('.t'):
670 if py or f.endswith('.t'):
671 with open(f) as src:
671 with open(f) as src:
672 for script, modname, t, line in embedded(f, modname, src):
672 for script, modname, t, line in embedded(f, modname, src):
673 yield script, modname, t, line
673 yield script, modname, t, line
674
674
675 def main(argv):
675 def main(argv):
676 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
676 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
677 print('Usage: %s {-|file [file] [file] ...}')
677 print('Usage: %s {-|file [file] [file] ...}')
678 return 1
678 return 1
679 if argv[1] == '-':
679 if argv[1] == '-':
680 argv = argv[:1]
680 argv = argv[:1]
681 argv.extend(l.rstrip() for l in sys.stdin.readlines())
681 argv.extend(l.rstrip() for l in sys.stdin.readlines())
682 localmods = {}
682 localmods = {}
683 used_imports = {}
683 used_imports = {}
684 any_errors = False
684 any_errors = False
685 for source_path in argv[1:]:
685 for source_path in argv[1:]:
686 modname = dotted_name_of_path(source_path, trimpure=True)
686 modname = dotted_name_of_path(source_path, trimpure=True)
687 localmods[modname] = source_path
687 localmods[modname] = source_path
688 for localmodname, source_path in sorted(localmods.items()):
688 for localmodname, source_path in sorted(localmods.items()):
689 for src, modname, name, line in sources(source_path, localmodname):
689 for src, modname, name, line in sources(source_path, localmodname):
690 try:
690 try:
691 used_imports[modname] = sorted(
691 used_imports[modname] = sorted(
692 imported_modules(src, modname, name, localmods,
692 imported_modules(src, modname, name, localmods,
693 ignore_nested=True))
693 ignore_nested=True))
694 for error, lineno in verify_import_convention(modname, src,
694 for error, lineno in verify_import_convention(modname, src,
695 localmods):
695 localmods):
696 any_errors = True
696 any_errors = True
697 print('%s:%d: %s' % (source_path, lineno + line, error))
697 print('%s:%d: %s' % (source_path, lineno + line, error))
698 except SyntaxError as e:
698 except SyntaxError as e:
699 print('%s:%d: SyntaxError: %s' %
699 print('%s:%d: SyntaxError: %s' %
700 (source_path, e.lineno + line, e))
700 (source_path, e.lineno + line, e))
701 cycles = find_cycles(used_imports)
701 cycles = find_cycles(used_imports)
702 if cycles:
702 if cycles:
703 firstmods = set()
703 firstmods = set()
704 for c in sorted(cycles, key=_cycle_sortkey):
704 for c in sorted(cycles, key=_cycle_sortkey):
705 first = c.split()[0]
705 first = c.split()[0]
706 # As a rough cut, ignore any cycle that starts with the
706 # As a rough cut, ignore any cycle that starts with the
707 # same module as some other cycle. Otherwise we see lots
707 # same module as some other cycle. Otherwise we see lots
708 # of cycles that are effectively duplicates.
708 # of cycles that are effectively duplicates.
709 if first in firstmods:
709 if first in firstmods:
710 continue
710 continue
711 print('Import cycle:', c)
711 print('Import cycle:', c)
712 firstmods.add(first)
712 firstmods.add(first)
713 any_errors = True
713 any_errors = True
714 return any_errors != 0
714 return any_errors != 0
715
715
716 if __name__ == '__main__':
716 if __name__ == '__main__':
717 sys.exit(int(main(sys.argv)))
717 sys.exit(int(main(sys.argv)))
General Comments 0
You need to be logged in to leave comments. Login now