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