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