##// END OF EJS Templates
py3: fully port doctest to py3
Manuel Jacob -
r52265:617af109 default
parent child Browse files
Show More
@@ -1,771 +1,770 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 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 >>> py3 = sys.version_info[0] >= 3
199 >>> mods = set(list_stdlib_modules())
198 >>> mods = set(list_stdlib_modules())
200 >>> 'BaseHTTPServer' in mods or py3
199 >>> 'http' in mods
201 True
200 True
202
201
203 os.path isn't really a module, so it's missing:
202 os.path isn't really a module, so it's missing:
204
203
205 >>> 'os.path' in mods
204 >>> 'os.path' in mods
206 False
205 False
207
206
208 sys requires special treatment, because it's baked into the
207 sys requires special treatment, because it's baked into the
209 interpreter, but it should still appear:
208 interpreter, but it should still appear:
210
209
211 >>> 'sys' in mods
210 >>> 'sys' in mods
212 True
211 True
213
212
214 >>> 'collections' in mods
213 >>> 'collections' in mods
215 True
214 True
216
215
217 >>> 'cStringIO' in mods or py3
216 >>> 'array' in mods
218 True
217 True
219
218
220 >>> 'cffi' in mods
219 >>> 'cffi' in mods
221 True
220 True
222 """
221 """
223 for m in sys.builtin_module_names:
222 for m in sys.builtin_module_names:
224 yield m
223 yield m
225 # These modules only exist on windows, but we should always
224 # These modules only exist on windows, but we should always
226 # consider them stdlib.
225 # consider them stdlib.
227 for m in ['msvcrt', '_winreg']:
226 for m in ['msvcrt', '_winreg']:
228 yield m
227 yield m
229 yield '__builtin__'
228 yield '__builtin__'
230 yield 'builtins' # python3 only
229 yield 'builtins' # python3 only
231 yield 'importlib.abc' # python3 only
230 yield 'importlib.abc' # python3 only
232 yield 'importlib.machinery' # python3 only
231 yield 'importlib.machinery' # python3 only
233 yield 'importlib.util' # python3 only
232 yield 'importlib.util' # python3 only
234 yield 'packaging.version'
233 yield 'packaging.version'
235 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
234 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
236 yield m
235 yield m
237 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
236 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
238 yield m
237 yield m
239 for m in ['cffi']:
238 for m in ['cffi']:
240 yield m
239 yield m
241 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
240 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
242 # We need to supplement the list of prefixes for the search to work
241 # We need to supplement the list of prefixes for the search to work
243 # when run from within a virtualenv.
242 # when run from within a virtualenv.
244 for mod in (argparse, zlib):
243 for mod in (argparse, zlib):
245 if mod is None:
244 if mod is None:
246 continue
245 continue
247 try:
246 try:
248 # Not all module objects have a __file__ attribute.
247 # Not all module objects have a __file__ attribute.
249 filename = mod.__file__
248 filename = mod.__file__
250 except AttributeError:
249 except AttributeError:
251 continue
250 continue
252 dirname = os.path.dirname(filename)
251 dirname = os.path.dirname(filename)
253 for prefix in stdlib_prefixes:
252 for prefix in stdlib_prefixes:
254 if dirname.startswith(prefix):
253 if dirname.startswith(prefix):
255 # Then this directory is redundant.
254 # Then this directory is redundant.
256 break
255 break
257 else:
256 else:
258 stdlib_prefixes.add(dirname)
257 stdlib_prefixes.add(dirname)
259 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
258 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
260 for libpath in sys.path:
259 for libpath in sys.path:
261 # We want to walk everything in sys.path that starts with something in
260 # We want to walk everything in sys.path that starts with something in
262 # stdlib_prefixes, but not directories from the hg sources.
261 # stdlib_prefixes, but not directories from the hg sources.
263 if os.path.abspath(libpath).startswith(sourceroot) or not any(
262 if os.path.abspath(libpath).startswith(sourceroot) or not any(
264 libpath.startswith(p) for p in stdlib_prefixes
263 libpath.startswith(p) for p in stdlib_prefixes
265 ):
264 ):
266 continue
265 continue
267 for top, dirs, files in os.walk(libpath):
266 for top, dirs, files in os.walk(libpath):
268 if 'dist-packages' in top.split(os.path.sep):
267 if 'dist-packages' in top.split(os.path.sep):
269 continue
268 continue
270 for i, d in reversed(list(enumerate(dirs))):
269 for i, d in reversed(list(enumerate(dirs))):
271 if (
270 if (
272 not os.path.exists(os.path.join(top, d, '__init__.py'))
271 not os.path.exists(os.path.join(top, d, '__init__.py'))
273 or top == libpath
272 or top == libpath
274 and d in ('hgdemandimport', 'hgext', 'mercurial')
273 and d in ('hgdemandimport', 'hgext', 'mercurial')
275 ):
274 ):
276 del dirs[i]
275 del dirs[i]
277 for name in files:
276 for name in files:
278 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
277 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
279 continue
278 continue
280 if name.startswith('__init__.py'):
279 if name.startswith('__init__.py'):
281 full_path = top
280 full_path = top
282 else:
281 else:
283 full_path = os.path.join(top, name)
282 full_path = os.path.join(top, name)
284 rel_path = full_path[len(libpath) + 1 :]
283 rel_path = full_path[len(libpath) + 1 :]
285 mod = dotted_name_of_path(rel_path)
284 mod = dotted_name_of_path(rel_path)
286 yield mod
285 yield mod
287
286
288
287
289 stdlib_modules = set(list_stdlib_modules())
288 stdlib_modules = set(list_stdlib_modules())
290
289
291
290
292 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
291 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
293 """Given the source of a file as a string, yield the names
292 """Given the source of a file as a string, yield the names
294 imported by that file.
293 imported by that file.
295
294
296 Args:
295 Args:
297 source: The python source to examine as a string.
296 source: The python source to examine as a string.
298 modulename: of specified python source (may have `__init__`)
297 modulename: of specified python source (may have `__init__`)
299 localmods: set of locally defined module names (may have `__init__`)
298 localmods: set of locally defined module names (may have `__init__`)
300 ignore_nested: If true, import statements that do not start in
299 ignore_nested: If true, import statements that do not start in
301 column zero will be ignored.
300 column zero will be ignored.
302
301
303 Returns:
302 Returns:
304 A list of absolute module names imported by the given source.
303 A list of absolute module names imported by the given source.
305
304
306 >>> f = 'foo/xxx.py'
305 >>> f = 'foo/xxx.py'
307 >>> modulename = 'foo.xxx'
306 >>> modulename = 'foo.xxx'
308 >>> localmods = {'foo.__init__': True,
307 >>> localmods = {'foo.__init__': True,
309 ... 'foo.foo1': True, 'foo.foo2': True,
308 ... 'foo.foo1': True, 'foo.foo2': True,
310 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
309 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
311 ... 'baz.__init__': True, 'baz.baz1': True }
310 ... 'baz.__init__': True, 'baz.baz1': True }
312 >>> # standard library (= not locally defined ones)
311 >>> # standard library (= not locally defined ones)
313 >>> sorted(imported_modules(
312 >>> sorted(imported_modules(
314 ... 'from stdlib1 import foo, bar; import stdlib2',
313 ... 'from stdlib1 import foo, bar; import stdlib2',
315 ... modulename, f, localmods))
314 ... modulename, f, localmods))
316 []
315 []
317 >>> # relative importing
316 >>> # relative importing
318 >>> sorted(imported_modules(
317 >>> sorted(imported_modules(
319 ... 'import foo1; from bar import bar1',
318 ... 'import foo1; from bar import bar1',
320 ... modulename, f, localmods))
319 ... modulename, f, localmods))
321 ['foo.bar.bar1', 'foo.foo1']
320 ['foo.bar.bar1', 'foo.foo1']
322 >>> sorted(imported_modules(
321 >>> sorted(imported_modules(
323 ... 'from bar.bar1 import name1, name2, name3',
322 ... 'from bar.bar1 import name1, name2, name3',
324 ... modulename, f, localmods))
323 ... modulename, f, localmods))
325 ['foo.bar.bar1']
324 ['foo.bar.bar1']
326 >>> # absolute importing
325 >>> # absolute importing
327 >>> sorted(imported_modules(
326 >>> sorted(imported_modules(
328 ... 'from baz import baz1, name1',
327 ... 'from baz import baz1, name1',
329 ... modulename, f, localmods))
328 ... modulename, f, localmods))
330 ['baz.__init__', 'baz.baz1']
329 ['baz.__init__', 'baz.baz1']
331 >>> # mixed importing, even though it shouldn't be recommended
330 >>> # mixed importing, even though it shouldn't be recommended
332 >>> sorted(imported_modules(
331 >>> sorted(imported_modules(
333 ... 'import stdlib, foo1, baz',
332 ... 'import stdlib, foo1, baz',
334 ... modulename, f, localmods))
333 ... modulename, f, localmods))
335 ['baz.__init__', 'foo.foo1']
334 ['baz.__init__', 'foo.foo1']
336 >>> # ignore_nested
335 >>> # ignore_nested
337 >>> sorted(imported_modules(
336 >>> sorted(imported_modules(
338 ... '''import foo
337 ... '''import foo
339 ... def wat():
338 ... def wat():
340 ... import bar
339 ... import bar
341 ... ''', modulename, f, localmods))
340 ... ''', modulename, f, localmods))
342 ['foo.__init__', 'foo.bar.__init__']
341 ['foo.__init__', 'foo.bar.__init__']
343 >>> sorted(imported_modules(
342 >>> sorted(imported_modules(
344 ... '''import foo
343 ... '''import foo
345 ... def wat():
344 ... def wat():
346 ... import bar
345 ... import bar
347 ... ''', modulename, f, localmods, ignore_nested=True))
346 ... ''', modulename, f, localmods, ignore_nested=True))
348 ['foo.__init__']
347 ['foo.__init__']
349 """
348 """
350 fromlocal = fromlocalfunc(modulename, localmods)
349 fromlocal = fromlocalfunc(modulename, localmods)
351 for node in ast.walk(ast.parse(source, f)):
350 for node in ast.walk(ast.parse(source, f)):
352 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
351 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
353 continue
352 continue
354 if isinstance(node, ast.Import):
353 if isinstance(node, ast.Import):
355 for n in node.names:
354 for n in node.names:
356 found = fromlocal(n.name)
355 found = fromlocal(n.name)
357 if not found:
356 if not found:
358 # this should import standard library
357 # this should import standard library
359 continue
358 continue
360 yield found[1]
359 yield found[1]
361 elif isinstance(node, ast.ImportFrom):
360 elif isinstance(node, ast.ImportFrom):
362 found = fromlocal(node.module, node.level)
361 found = fromlocal(node.module, node.level)
363 if not found:
362 if not found:
364 # this should import standard library
363 # this should import standard library
365 continue
364 continue
366
365
367 absname, dottedpath, hassubmod = found
366 absname, dottedpath, hassubmod = found
368 if not hassubmod:
367 if not hassubmod:
369 # "dottedpath" is not a package; must be imported
368 # "dottedpath" is not a package; must be imported
370 yield dottedpath
369 yield dottedpath
371 # examination of "node.names" should be redundant
370 # examination of "node.names" should be redundant
372 # e.g.: from mercurial.node import nullid, nullrev
371 # e.g.: from mercurial.node import nullid, nullrev
373 continue
372 continue
374
373
375 modnotfound = False
374 modnotfound = False
376 prefix = absname + '.'
375 prefix = absname + '.'
377 for n in node.names:
376 for n in node.names:
378 found = fromlocal(prefix + n.name)
377 found = fromlocal(prefix + n.name)
379 if not found:
378 if not found:
380 # this should be a function or a property of "node.module"
379 # this should be a function or a property of "node.module"
381 modnotfound = True
380 modnotfound = True
382 continue
381 continue
383 yield found[1]
382 yield found[1]
384 if modnotfound and dottedpath != modulename:
383 if modnotfound and dottedpath != modulename:
385 # "dottedpath" is a package, but imported because of non-module
384 # "dottedpath" is a package, but imported because of non-module
386 # lookup
385 # lookup
387 # specifically allow "from . import foo" from __init__.py
386 # specifically allow "from . import foo" from __init__.py
388 yield dottedpath
387 yield dottedpath
389
388
390
389
391 def verify_import_convention(module, source, localmods):
390 def verify_import_convention(module, source, localmods):
392 """Verify imports match our established coding convention."""
391 """Verify imports match our established coding convention."""
393 root = ast.parse(source)
392 root = ast.parse(source)
394
393
395 return verify_modern_convention(module, root, localmods)
394 return verify_modern_convention(module, root, localmods)
396
395
397
396
398 def verify_modern_convention(module, root, localmods, root_col_offset=0):
397 def verify_modern_convention(module, root, localmods, root_col_offset=0):
399 """Verify a file conforms to the modern import convention rules.
398 """Verify a file conforms to the modern import convention rules.
400
399
401 The rules of the modern convention are:
400 The rules of the modern convention are:
402
401
403 * Ordering is stdlib followed by local imports. Each group is lexically
402 * Ordering is stdlib followed by local imports. Each group is lexically
404 sorted.
403 sorted.
405 * Importing multiple modules via "import X, Y" is not allowed: use
404 * Importing multiple modules via "import X, Y" is not allowed: use
406 separate import statements.
405 separate import statements.
407 * Importing multiple modules via "from X import ..." is allowed if using
406 * Importing multiple modules via "from X import ..." is allowed if using
408 parenthesis and one entry per line.
407 parenthesis and one entry per line.
409 * Only 1 relative import statement per import level ("from .", "from ..")
408 * Only 1 relative import statement per import level ("from .", "from ..")
410 is allowed.
409 is allowed.
411 * Relative imports from higher levels must occur before lower levels. e.g.
410 * Relative imports from higher levels must occur before lower levels. e.g.
412 "from .." must be before "from .".
411 "from .." must be before "from .".
413 * Imports from peer packages should use relative import (e.g. do not
412 * Imports from peer packages should use relative import (e.g. do not
414 "import mercurial.foo" from a "mercurial.*" module).
413 "import mercurial.foo" from a "mercurial.*" module).
415 * Symbols can only be imported from specific modules (see
414 * Symbols can only be imported from specific modules (see
416 `allowsymbolimports`). For other modules, first import the module then
415 `allowsymbolimports`). For other modules, first import the module then
417 assign the symbol to a module-level variable. In addition, these imports
416 assign the symbol to a module-level variable. In addition, these imports
418 must be performed before other local imports. This rule only
417 must be performed before other local imports. This rule only
419 applies to import statements outside of any blocks.
418 applies to import statements outside of any blocks.
420 * Relative imports from the standard library are not allowed, unless that
419 * Relative imports from the standard library are not allowed, unless that
421 library is also a local module.
420 library is also a local module.
422 * Certain modules must be aliased to alternate names to avoid aliasing
421 * Certain modules must be aliased to alternate names to avoid aliasing
423 and readability problems. See `requirealias`.
422 and readability problems. See `requirealias`.
424 """
423 """
425 if not isinstance(module, str):
424 if not isinstance(module, str):
426 module = module.decode('ascii')
425 module = module.decode('ascii')
427 topmodule = module.split('.')[0]
426 topmodule = module.split('.')[0]
428 fromlocal = fromlocalfunc(module, localmods)
427 fromlocal = fromlocalfunc(module, localmods)
429
428
430 # Whether a local/non-stdlib import has been performed.
429 # Whether a local/non-stdlib import has been performed.
431 seenlocal = None
430 seenlocal = None
432 # Whether a local/non-stdlib, non-symbol import has been seen.
431 # Whether a local/non-stdlib, non-symbol import has been seen.
433 seennonsymbollocal = False
432 seennonsymbollocal = False
434 # The last name to be imported (for sorting).
433 # The last name to be imported (for sorting).
435 lastname = None
434 lastname = None
436 laststdlib = None
435 laststdlib = None
437 # Relative import levels encountered so far.
436 # Relative import levels encountered so far.
438 seenlevels = set()
437 seenlevels = set()
439
438
440 for node, newscope in walklocal(root):
439 for node, newscope in walklocal(root):
441
440
442 def msg(fmt, *args):
441 def msg(fmt, *args):
443 return (fmt % args, node.lineno)
442 return (fmt % args, node.lineno)
444
443
445 if newscope:
444 if newscope:
446 # Check for local imports in function
445 # Check for local imports in function
447 for r in verify_modern_convention(
446 for r in verify_modern_convention(
448 module, node, localmods, node.col_offset + 4
447 module, node, localmods, node.col_offset + 4
449 ):
448 ):
450 yield r
449 yield r
451 elif isinstance(node, ast.Import):
450 elif isinstance(node, ast.Import):
452 # Disallow "import foo, bar" and require separate imports
451 # Disallow "import foo, bar" and require separate imports
453 # for each module.
452 # for each module.
454 if len(node.names) > 1:
453 if len(node.names) > 1:
455 yield msg(
454 yield msg(
456 'multiple imported names: %s',
455 'multiple imported names: %s',
457 ', '.join(n.name for n in node.names),
456 ', '.join(n.name for n in node.names),
458 )
457 )
459
458
460 name = node.names[0].name
459 name = node.names[0].name
461 asname = node.names[0].asname
460 asname = node.names[0].asname
462
461
463 stdlib = name in stdlib_modules
462 stdlib = name in stdlib_modules
464
463
465 # Ignore sorting rules on imports inside blocks.
464 # Ignore sorting rules on imports inside blocks.
466 if node.col_offset == root_col_offset:
465 if node.col_offset == root_col_offset:
467 if lastname and name < lastname and laststdlib == stdlib:
466 if lastname and name < lastname and laststdlib == stdlib:
468 yield msg(
467 yield msg(
469 'imports not lexically sorted: %s < %s', name, lastname
468 'imports not lexically sorted: %s < %s', name, lastname
470 )
469 )
471
470
472 lastname = name
471 lastname = name
473 laststdlib = stdlib
472 laststdlib = stdlib
474
473
475 # stdlib imports should be before local imports.
474 # stdlib imports should be before local imports.
476 if stdlib and seenlocal and node.col_offset == root_col_offset:
475 if stdlib and seenlocal and node.col_offset == root_col_offset:
477 yield msg(
476 yield msg(
478 'stdlib import "%s" follows local import: %s',
477 'stdlib import "%s" follows local import: %s',
479 name,
478 name,
480 seenlocal,
479 seenlocal,
481 )
480 )
482
481
483 if not stdlib:
482 if not stdlib:
484 seenlocal = name
483 seenlocal = name
485
484
486 # Import of sibling modules should use relative imports.
485 # Import of sibling modules should use relative imports.
487 topname = name.split('.')[0]
486 topname = name.split('.')[0]
488 if topname == topmodule:
487 if topname == topmodule:
489 yield msg('import should be relative: %s', name)
488 yield msg('import should be relative: %s', name)
490
489
491 if name in requirealias and asname != requirealias[name]:
490 if name in requirealias and asname != requirealias[name]:
492 yield msg(
491 yield msg(
493 '%s module must be "as" aliased to %s',
492 '%s module must be "as" aliased to %s',
494 name,
493 name,
495 requirealias[name],
494 requirealias[name],
496 )
495 )
497
496
498 elif isinstance(node, ast.ImportFrom):
497 elif isinstance(node, ast.ImportFrom):
499 # Resolve the full imported module name.
498 # Resolve the full imported module name.
500 if node.level > 0:
499 if node.level > 0:
501 fullname = '.'.join(module.split('.')[: -node.level])
500 fullname = '.'.join(module.split('.')[: -node.level])
502 if node.module:
501 if node.module:
503 fullname += '.%s' % node.module
502 fullname += '.%s' % node.module
504 else:
503 else:
505 assert node.module
504 assert node.module
506 fullname = node.module
505 fullname = node.module
507
506
508 topname = fullname.split('.')[0]
507 topname = fullname.split('.')[0]
509 if topname == topmodule:
508 if topname == topmodule:
510 yield msg('import should be relative: %s', fullname)
509 yield msg('import should be relative: %s', fullname)
511
510
512 # __future__ is special since it needs to come first and use
511 # __future__ is special since it needs to come first and use
513 # symbol import.
512 # symbol import.
514 if fullname != '__future__':
513 if fullname != '__future__':
515 if not fullname or (
514 if not fullname or (
516 fullname in stdlib_modules
515 fullname in stdlib_modules
517 # allow standard 'from typing import ...' style
516 # allow standard 'from typing import ...' style
518 and fullname.startswith('.')
517 and fullname.startswith('.')
519 and fullname not in localmods
518 and fullname not in localmods
520 and fullname + '.__init__' not in localmods
519 and fullname + '.__init__' not in localmods
521 ):
520 ):
522 yield msg('relative import of stdlib module')
521 yield msg('relative import of stdlib module')
523 else:
522 else:
524 seenlocal = fullname
523 seenlocal = fullname
525
524
526 # Direct symbol import is only allowed from certain modules and
525 # Direct symbol import is only allowed from certain modules and
527 # must occur before non-symbol imports.
526 # must occur before non-symbol imports.
528 found = fromlocal(node.module, node.level)
527 found = fromlocal(node.module, node.level)
529 if found and found[2]: # node.module is a package
528 if found and found[2]: # node.module is a package
530 prefix = found[0] + '.'
529 prefix = found[0] + '.'
531 symbols = (
530 symbols = (
532 n.name for n in node.names if not fromlocal(prefix + n.name)
531 n.name for n in node.names if not fromlocal(prefix + n.name)
533 )
532 )
534 else:
533 else:
535 symbols = (n.name for n in node.names)
534 symbols = (n.name for n in node.names)
536 symbols = [sym for sym in symbols if sym not in directsymbols]
535 symbols = [sym for sym in symbols if sym not in directsymbols]
537 if node.module and node.col_offset == root_col_offset:
536 if node.module and node.col_offset == root_col_offset:
538 if symbols and fullname not in allowsymbolimports:
537 if symbols and fullname not in allowsymbolimports:
539 yield msg(
538 yield msg(
540 'direct symbol import %s from %s',
539 'direct symbol import %s from %s',
541 ', '.join(symbols),
540 ', '.join(symbols),
542 fullname,
541 fullname,
543 )
542 )
544
543
545 if symbols and seennonsymbollocal:
544 if symbols and seennonsymbollocal:
546 yield msg(
545 yield msg(
547 'symbol import follows non-symbol import: %s', fullname
546 'symbol import follows non-symbol import: %s', fullname
548 )
547 )
549 if not symbols and fullname not in stdlib_modules:
548 if not symbols and fullname not in stdlib_modules:
550 seennonsymbollocal = True
549 seennonsymbollocal = True
551
550
552 if not node.module:
551 if not node.module:
553 assert node.level
552 assert node.level
554
553
555 # Only allow 1 group per level.
554 # Only allow 1 group per level.
556 if (
555 if (
557 node.level in seenlevels
556 node.level in seenlevels
558 and node.col_offset == root_col_offset
557 and node.col_offset == root_col_offset
559 ):
558 ):
560 yield msg(
559 yield msg(
561 'multiple "from %s import" statements', '.' * node.level
560 'multiple "from %s import" statements', '.' * node.level
562 )
561 )
563
562
564 # Higher-level groups come before lower-level groups.
563 # Higher-level groups come before lower-level groups.
565 if any(node.level > l for l in seenlevels):
564 if any(node.level > l for l in seenlevels):
566 yield msg(
565 yield msg(
567 'higher-level import should come first: %s', fullname
566 'higher-level import should come first: %s', fullname
568 )
567 )
569
568
570 seenlevels.add(node.level)
569 seenlevels.add(node.level)
571
570
572 # Entries in "from .X import ( ... )" lists must be lexically
571 # Entries in "from .X import ( ... )" lists must be lexically
573 # sorted.
572 # sorted.
574 lastentryname = None
573 lastentryname = None
575
574
576 for n in node.names:
575 for n in node.names:
577 if lastentryname and n.name < lastentryname:
576 if lastentryname and n.name < lastentryname:
578 yield msg(
577 yield msg(
579 'imports from %s not lexically sorted: %s < %s',
578 'imports from %s not lexically sorted: %s < %s',
580 fullname,
579 fullname,
581 n.name,
580 n.name,
582 lastentryname,
581 lastentryname,
583 )
582 )
584
583
585 lastentryname = n.name
584 lastentryname = n.name
586
585
587 if n.name in requirealias and n.asname != requirealias[n.name]:
586 if n.name in requirealias and n.asname != requirealias[n.name]:
588 yield msg(
587 yield msg(
589 '%s from %s must be "as" aliased to %s',
588 '%s from %s must be "as" aliased to %s',
590 n.name,
589 n.name,
591 fullname,
590 fullname,
592 requirealias[n.name],
591 requirealias[n.name],
593 )
592 )
594
593
595
594
596 class CircularImport(Exception):
595 class CircularImport(Exception):
597 pass
596 pass
598
597
599
598
600 def checkmod(mod, imports):
599 def checkmod(mod, imports):
601 shortest = {}
600 shortest = {}
602 visit = [[mod]]
601 visit = [[mod]]
603 while visit:
602 while visit:
604 path = visit.pop(0)
603 path = visit.pop(0)
605 for i in sorted(imports.get(path[-1], [])):
604 for i in sorted(imports.get(path[-1], [])):
606 if len(path) < shortest.get(i, 1000):
605 if len(path) < shortest.get(i, 1000):
607 shortest[i] = len(path)
606 shortest[i] = len(path)
608 if i in path:
607 if i in path:
609 if i == path[0]:
608 if i == path[0]:
610 raise CircularImport(path)
609 raise CircularImport(path)
611 continue
610 continue
612 visit.append(path + [i])
611 visit.append(path + [i])
613
612
614
613
615 def rotatecycle(cycle):
614 def rotatecycle(cycle):
616 """arrange a cycle so that the lexicographically first module listed first
615 """arrange a cycle so that the lexicographically first module listed first
617
616
618 >>> rotatecycle(['foo', 'bar'])
617 >>> rotatecycle(['foo', 'bar'])
619 ['bar', 'foo', 'bar']
618 ['bar', 'foo', 'bar']
620 """
619 """
621 lowest = min(cycle)
620 lowest = min(cycle)
622 idx = cycle.index(lowest)
621 idx = cycle.index(lowest)
623 return cycle[idx:] + cycle[:idx] + [lowest]
622 return cycle[idx:] + cycle[:idx] + [lowest]
624
623
625
624
626 def find_cycles(imports):
625 def find_cycles(imports):
627 """Find cycles in an already-loaded import graph.
626 """Find cycles in an already-loaded import graph.
628
627
629 All module names recorded in `imports` should be absolute one.
628 All module names recorded in `imports` should be absolute one.
630
629
631 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
630 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
632 ... 'top.bar': ['top.baz', 'sys'],
631 ... 'top.bar': ['top.baz', 'sys'],
633 ... 'top.baz': ['top.foo'],
632 ... 'top.baz': ['top.foo'],
634 ... 'top.qux': ['top.foo']}
633 ... 'top.qux': ['top.foo']}
635 >>> print('\\n'.join(sorted(find_cycles(imports))))
634 >>> print('\\n'.join(sorted(find_cycles(imports))))
636 top.bar -> top.baz -> top.foo -> top.bar
635 top.bar -> top.baz -> top.foo -> top.bar
637 top.foo -> top.qux -> top.foo
636 top.foo -> top.qux -> top.foo
638 """
637 """
639 cycles = set()
638 cycles = set()
640 for mod in sorted(imports.keys()):
639 for mod in sorted(imports.keys()):
641 try:
640 try:
642 checkmod(mod, imports)
641 checkmod(mod, imports)
643 except CircularImport as e:
642 except CircularImport as e:
644 cycle = e.args[0]
643 cycle = e.args[0]
645 cycles.add(" -> ".join(rotatecycle(cycle)))
644 cycles.add(" -> ".join(rotatecycle(cycle)))
646 return cycles
645 return cycles
647
646
648
647
649 def _cycle_sortkey(c):
648 def _cycle_sortkey(c):
650 return len(c), c
649 return len(c), c
651
650
652
651
653 def embedded(f, modname, src):
652 def embedded(f, modname, src):
654 """Extract embedded python code
653 """Extract embedded python code
655
654
656 >>> def _forcestr(thing):
655 >>> def _forcestr(thing):
657 ... if not isinstance(thing, str):
656 ... if not isinstance(thing, str):
658 ... return thing.decode('ascii')
657 ... return thing.decode('ascii')
659 ... return thing
658 ... return thing
660 >>> def test(fn, lines):
659 >>> def test(fn, lines):
661 ... for s, m, f, l in embedded(fn, b"example", lines):
660 ... for s, m, f, l in embedded(fn, b"example", lines):
662 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
661 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
663 ... print(repr(_forcestr(s)))
662 ... print(repr(_forcestr(s)))
664 >>> lines = [
663 >>> lines = [
665 ... 'comment',
664 ... 'comment',
666 ... ' >>> from __future__ import print_function',
665 ... ' >>> from __future__ import print_function',
667 ... " >>> ' multiline",
666 ... " >>> ' multiline",
668 ... " ... string'",
667 ... " ... string'",
669 ... ' ',
668 ... ' ',
670 ... 'comment',
669 ... 'comment',
671 ... ' $ cat > foo.py <<EOF',
670 ... ' $ cat > foo.py <<EOF',
672 ... ' > from __future__ import print_function',
671 ... ' > from __future__ import print_function',
673 ... ' > EOF',
672 ... ' > EOF',
674 ... ]
673 ... ]
675 >>> test(b"example.t", lines)
674 >>> test(b"example.t", lines)
676 example[2] doctest.py 1
675 example[2] doctest.py 1
677 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
676 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
678 example[8] foo.py 7
677 example[8] foo.py 7
679 'from __future__ import print_function\\n'
678 'from __future__ import print_function\\n'
680 """
679 """
681 errors = []
680 errors = []
682 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
681 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
683 if not name:
682 if not name:
684 # use 'doctest.py', in order to make already existing
683 # use 'doctest.py', in order to make already existing
685 # doctest above pass instantly
684 # doctest above pass instantly
686 name = 'doctest.py'
685 name = 'doctest.py'
687 # "starts" is "line number" (1-origin), but embedded() is
686 # "starts" is "line number" (1-origin), but embedded() is
688 # expected to return "line offset" (0-origin). Therefore, this
687 # expected to return "line offset" (0-origin). Therefore, this
689 # yields "starts - 1".
688 # yields "starts - 1".
690 if not isinstance(modname, str):
689 if not isinstance(modname, str):
691 modname = modname.decode('utf8')
690 modname = modname.decode('utf8')
692 yield code, "%s[%d]" % (modname, starts), name, starts - 1
691 yield code, "%s[%d]" % (modname, starts), name, starts - 1
693
692
694
693
695 def sources(f, modname):
694 def sources(f, modname):
696 """Yields possibly multiple sources from a filepath
695 """Yields possibly multiple sources from a filepath
697
696
698 input: filepath, modulename
697 input: filepath, modulename
699 yields: script(string), modulename, filepath, linenumber
698 yields: script(string), modulename, filepath, linenumber
700
699
701 For embedded scripts, the modulename and filepath will be different
700 For embedded scripts, the modulename and filepath will be different
702 from the function arguments. linenumber is an offset relative to
701 from the function arguments. linenumber is an offset relative to
703 the input file.
702 the input file.
704 """
703 """
705 py = False
704 py = False
706 if not f.endswith('.t'):
705 if not f.endswith('.t'):
707 with open(f, 'rb') as src:
706 with open(f, 'rb') as src:
708 yield src.read(), modname, f, 0
707 yield src.read(), modname, f, 0
709 py = True
708 py = True
710 if py or f.endswith('.t'):
709 if py or f.endswith('.t'):
711 # Strictly speaking we should sniff for the magic header that denotes
710 # Strictly speaking we should sniff for the magic header that denotes
712 # Python source file encoding. But in reality we don't use anything
711 # Python source file encoding. But in reality we don't use anything
713 # other than ASCII (mainly) and UTF-8 (in a few exceptions), so
712 # other than ASCII (mainly) and UTF-8 (in a few exceptions), so
714 # simplicity is fine.
713 # simplicity is fine.
715 with io.open(f, 'r', encoding='utf-8') as src:
714 with io.open(f, 'r', encoding='utf-8') as src:
716 for script, modname, t, line in embedded(f, modname, src):
715 for script, modname, t, line in embedded(f, modname, src):
717 yield script, modname.encode('utf8'), t, line
716 yield script, modname.encode('utf8'), t, line
718
717
719
718
720 def main(argv):
719 def main(argv):
721 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
720 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
722 print('Usage: %s {-|file [file] [file] ...}')
721 print('Usage: %s {-|file [file] [file] ...}')
723 return 1
722 return 1
724 if argv[1] == '-':
723 if argv[1] == '-':
725 argv = argv[:1]
724 argv = argv[:1]
726 argv.extend(l.rstrip() for l in sys.stdin.readlines())
725 argv.extend(l.rstrip() for l in sys.stdin.readlines())
727 localmodpaths = {}
726 localmodpaths = {}
728 used_imports = {}
727 used_imports = {}
729 any_errors = False
728 any_errors = False
730 for source_path in argv[1:]:
729 for source_path in argv[1:]:
731 modname = dotted_name_of_path(source_path)
730 modname = dotted_name_of_path(source_path)
732 localmodpaths[modname] = source_path
731 localmodpaths[modname] = source_path
733 localmods = populateextmods(localmodpaths)
732 localmods = populateextmods(localmodpaths)
734 for localmodname, source_path in sorted(localmodpaths.items()):
733 for localmodname, source_path in sorted(localmodpaths.items()):
735 if not isinstance(localmodname, bytes):
734 if not isinstance(localmodname, bytes):
736 # This is only safe because all hg's files are ascii
735 # This is only safe because all hg's files are ascii
737 localmodname = localmodname.encode('ascii')
736 localmodname = localmodname.encode('ascii')
738 for src, modname, name, line in sources(source_path, localmodname):
737 for src, modname, name, line in sources(source_path, localmodname):
739 try:
738 try:
740 used_imports[modname] = sorted(
739 used_imports[modname] = sorted(
741 imported_modules(
740 imported_modules(
742 src, modname, name, localmods, ignore_nested=True
741 src, modname, name, localmods, ignore_nested=True
743 )
742 )
744 )
743 )
745 for error, lineno in verify_import_convention(
744 for error, lineno in verify_import_convention(
746 modname, src, localmods
745 modname, src, localmods
747 ):
746 ):
748 any_errors = True
747 any_errors = True
749 print('%s:%d: %s' % (source_path, lineno + line, error))
748 print('%s:%d: %s' % (source_path, lineno + line, error))
750 except SyntaxError as e:
749 except SyntaxError as e:
751 print(
750 print(
752 '%s:%d: SyntaxError: %s' % (source_path, e.lineno + line, e)
751 '%s:%d: SyntaxError: %s' % (source_path, e.lineno + line, e)
753 )
752 )
754 cycles = find_cycles(used_imports)
753 cycles = find_cycles(used_imports)
755 if cycles:
754 if cycles:
756 firstmods = set()
755 firstmods = set()
757 for c in sorted(cycles, key=_cycle_sortkey):
756 for c in sorted(cycles, key=_cycle_sortkey):
758 first = c.split()[0]
757 first = c.split()[0]
759 # As a rough cut, ignore any cycle that starts with the
758 # As a rough cut, ignore any cycle that starts with the
760 # same module as some other cycle. Otherwise we see lots
759 # same module as some other cycle. Otherwise we see lots
761 # of cycles that are effectively duplicates.
760 # of cycles that are effectively duplicates.
762 if first in firstmods:
761 if first in firstmods:
763 continue
762 continue
764 print('Import cycle:', c)
763 print('Import cycle:', c)
765 firstmods.add(first)
764 firstmods.add(first)
766 any_errors = True
765 any_errors = True
767 return any_errors != 0
766 return any_errors != 0
768
767
769
768
770 if __name__ == '__main__':
769 if __name__ == '__main__':
771 sys.exit(int(main(sys.argv)))
770 sys.exit(int(main(sys.argv)))
General Comments 0
You need to be logged in to leave comments. Login now