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