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