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