##// END OF EJS Templates
import-checker: allow all absolute imports of stdlib modules...
Martin von Zweigbergk -
r44406:30357611 default
parent child Browse files
Show More
@@ -1,820 +1,820
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 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 'bzrlib',
26 'bzrlib',
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 for i, d in reversed(list(enumerate(dirs))):
281 for i, d in reversed(list(enumerate(dirs))):
282 if (
282 if (
283 not os.path.exists(os.path.join(top, d, '__init__.py'))
283 not os.path.exists(os.path.join(top, d, '__init__.py'))
284 or top == libpath
284 or top == libpath
285 and d in ('hgdemandimport', 'hgext', 'mercurial')
285 and d in ('hgdemandimport', 'hgext', 'mercurial')
286 ):
286 ):
287 del dirs[i]
287 del dirs[i]
288 for name in files:
288 for name in files:
289 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
289 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
290 continue
290 continue
291 if name.startswith('__init__.py'):
291 if name.startswith('__init__.py'):
292 full_path = top
292 full_path = top
293 else:
293 else:
294 full_path = os.path.join(top, name)
294 full_path = os.path.join(top, name)
295 rel_path = full_path[len(libpath) + 1 :]
295 rel_path = full_path[len(libpath) + 1 :]
296 mod = dotted_name_of_path(rel_path)
296 mod = dotted_name_of_path(rel_path)
297 yield mod
297 yield mod
298
298
299
299
300 stdlib_modules = set(list_stdlib_modules())
300 stdlib_modules = set(list_stdlib_modules())
301
301
302
302
303 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
303 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
304 """Given the source of a file as a string, yield the names
304 """Given the source of a file as a string, yield the names
305 imported by that file.
305 imported by that file.
306
306
307 Args:
307 Args:
308 source: The python source to examine as a string.
308 source: The python source to examine as a string.
309 modulename: of specified python source (may have `__init__`)
309 modulename: of specified python source (may have `__init__`)
310 localmods: set of locally defined module names (may have `__init__`)
310 localmods: set of locally defined module names (may have `__init__`)
311 ignore_nested: If true, import statements that do not start in
311 ignore_nested: If true, import statements that do not start in
312 column zero will be ignored.
312 column zero will be ignored.
313
313
314 Returns:
314 Returns:
315 A list of absolute module names imported by the given source.
315 A list of absolute module names imported by the given source.
316
316
317 >>> f = 'foo/xxx.py'
317 >>> f = 'foo/xxx.py'
318 >>> modulename = 'foo.xxx'
318 >>> modulename = 'foo.xxx'
319 >>> localmods = {'foo.__init__': True,
319 >>> localmods = {'foo.__init__': True,
320 ... 'foo.foo1': True, 'foo.foo2': True,
320 ... 'foo.foo1': True, 'foo.foo2': True,
321 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
321 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
322 ... 'baz.__init__': True, 'baz.baz1': True }
322 ... 'baz.__init__': True, 'baz.baz1': True }
323 >>> # standard library (= not locally defined ones)
323 >>> # standard library (= not locally defined ones)
324 >>> sorted(imported_modules(
324 >>> sorted(imported_modules(
325 ... 'from stdlib1 import foo, bar; import stdlib2',
325 ... 'from stdlib1 import foo, bar; import stdlib2',
326 ... modulename, f, localmods))
326 ... modulename, f, localmods))
327 []
327 []
328 >>> # relative importing
328 >>> # relative importing
329 >>> sorted(imported_modules(
329 >>> sorted(imported_modules(
330 ... 'import foo1; from bar import bar1',
330 ... 'import foo1; from bar import bar1',
331 ... modulename, f, localmods))
331 ... modulename, f, localmods))
332 ['foo.bar.bar1', 'foo.foo1']
332 ['foo.bar.bar1', 'foo.foo1']
333 >>> sorted(imported_modules(
333 >>> sorted(imported_modules(
334 ... 'from bar.bar1 import name1, name2, name3',
334 ... 'from bar.bar1 import name1, name2, name3',
335 ... modulename, f, localmods))
335 ... modulename, f, localmods))
336 ['foo.bar.bar1']
336 ['foo.bar.bar1']
337 >>> # absolute importing
337 >>> # absolute importing
338 >>> sorted(imported_modules(
338 >>> sorted(imported_modules(
339 ... 'from baz import baz1, name1',
339 ... 'from baz import baz1, name1',
340 ... modulename, f, localmods))
340 ... modulename, f, localmods))
341 ['baz.__init__', 'baz.baz1']
341 ['baz.__init__', 'baz.baz1']
342 >>> # mixed importing, even though it shouldn't be recommended
342 >>> # mixed importing, even though it shouldn't be recommended
343 >>> sorted(imported_modules(
343 >>> sorted(imported_modules(
344 ... 'import stdlib, foo1, baz',
344 ... 'import stdlib, foo1, baz',
345 ... modulename, f, localmods))
345 ... modulename, f, localmods))
346 ['baz.__init__', 'foo.foo1']
346 ['baz.__init__', 'foo.foo1']
347 >>> # ignore_nested
347 >>> # ignore_nested
348 >>> sorted(imported_modules(
348 >>> sorted(imported_modules(
349 ... '''import foo
349 ... '''import foo
350 ... def wat():
350 ... def wat():
351 ... import bar
351 ... import bar
352 ... ''', modulename, f, localmods))
352 ... ''', modulename, f, localmods))
353 ['foo.__init__', 'foo.bar.__init__']
353 ['foo.__init__', 'foo.bar.__init__']
354 >>> sorted(imported_modules(
354 >>> sorted(imported_modules(
355 ... '''import foo
355 ... '''import foo
356 ... def wat():
356 ... def wat():
357 ... import bar
357 ... import bar
358 ... ''', modulename, f, localmods, ignore_nested=True))
358 ... ''', modulename, f, localmods, ignore_nested=True))
359 ['foo.__init__']
359 ['foo.__init__']
360 """
360 """
361 fromlocal = fromlocalfunc(modulename, localmods)
361 fromlocal = fromlocalfunc(modulename, localmods)
362 for node in ast.walk(ast.parse(source, f)):
362 for node in ast.walk(ast.parse(source, f)):
363 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
363 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
364 continue
364 continue
365 if isinstance(node, ast.Import):
365 if isinstance(node, ast.Import):
366 for n in node.names:
366 for n in node.names:
367 found = fromlocal(n.name)
367 found = fromlocal(n.name)
368 if not found:
368 if not found:
369 # this should import standard library
369 # this should import standard library
370 continue
370 continue
371 yield found[1]
371 yield found[1]
372 elif isinstance(node, ast.ImportFrom):
372 elif isinstance(node, ast.ImportFrom):
373 found = fromlocal(node.module, node.level)
373 found = fromlocal(node.module, node.level)
374 if not found:
374 if not found:
375 # this should import standard library
375 # this should import standard library
376 continue
376 continue
377
377
378 absname, dottedpath, hassubmod = found
378 absname, dottedpath, hassubmod = found
379 if not hassubmod:
379 if not hassubmod:
380 # "dottedpath" is not a package; must be imported
380 # "dottedpath" is not a package; must be imported
381 yield dottedpath
381 yield dottedpath
382 # examination of "node.names" should be redundant
382 # examination of "node.names" should be redundant
383 # e.g.: from mercurial.node import nullid, nullrev
383 # e.g.: from mercurial.node import nullid, nullrev
384 continue
384 continue
385
385
386 modnotfound = False
386 modnotfound = False
387 prefix = absname + '.'
387 prefix = absname + '.'
388 for n in node.names:
388 for n in node.names:
389 found = fromlocal(prefix + n.name)
389 found = fromlocal(prefix + n.name)
390 if not found:
390 if not found:
391 # this should be a function or a property of "node.module"
391 # this should be a function or a property of "node.module"
392 modnotfound = True
392 modnotfound = True
393 continue
393 continue
394 yield found[1]
394 yield found[1]
395 if modnotfound:
395 if modnotfound:
396 # "dottedpath" is a package, but imported because of non-module
396 # "dottedpath" is a package, but imported because of non-module
397 # lookup
397 # lookup
398 yield dottedpath
398 yield dottedpath
399
399
400
400
401 def verify_import_convention(module, source, localmods):
401 def verify_import_convention(module, source, localmods):
402 """Verify imports match our established coding convention.
402 """Verify imports match our established coding convention.
403
403
404 We have 2 conventions: legacy and modern. The modern convention is in
404 We have 2 conventions: legacy and modern. The modern convention is in
405 effect when using absolute imports.
405 effect when using absolute imports.
406
406
407 The legacy convention only looks for mixed imports. The modern convention
407 The legacy convention only looks for mixed imports. The modern convention
408 is much more thorough.
408 is much more thorough.
409 """
409 """
410 root = ast.parse(source)
410 root = ast.parse(source)
411 absolute = usingabsolute(root)
411 absolute = usingabsolute(root)
412
412
413 if absolute:
413 if absolute:
414 return verify_modern_convention(module, root, localmods)
414 return verify_modern_convention(module, root, localmods)
415 else:
415 else:
416 return verify_stdlib_on_own_line(root)
416 return verify_stdlib_on_own_line(root)
417
417
418
418
419 def verify_modern_convention(module, root, localmods, root_col_offset=0):
419 def verify_modern_convention(module, root, localmods, root_col_offset=0):
420 """Verify a file conforms to the modern import convention rules.
420 """Verify a file conforms to the modern import convention rules.
421
421
422 The rules of the modern convention are:
422 The rules of the modern convention are:
423
423
424 * Ordering is stdlib followed by local imports. Each group is lexically
424 * Ordering is stdlib followed by local imports. Each group is lexically
425 sorted.
425 sorted.
426 * Importing multiple modules via "import X, Y" is not allowed: use
426 * Importing multiple modules via "import X, Y" is not allowed: use
427 separate import statements.
427 separate import statements.
428 * Importing multiple modules via "from X import ..." is allowed if using
428 * Importing multiple modules via "from X import ..." is allowed if using
429 parenthesis and one entry per line.
429 parenthesis and one entry per line.
430 * Only 1 relative import statement per import level ("from .", "from ..")
430 * Only 1 relative import statement per import level ("from .", "from ..")
431 is allowed.
431 is allowed.
432 * Relative imports from higher levels must occur before lower levels. e.g.
432 * Relative imports from higher levels must occur before lower levels. e.g.
433 "from .." must be before "from .".
433 "from .." must be before "from .".
434 * Imports from peer packages should use relative import (e.g. do not
434 * Imports from peer packages should use relative import (e.g. do not
435 "import mercurial.foo" from a "mercurial.*" module).
435 "import mercurial.foo" from a "mercurial.*" module).
436 * Symbols can only be imported from specific modules (see
436 * Symbols can only be imported from specific modules (see
437 `allowsymbolimports`). For other modules, first import the module then
437 `allowsymbolimports`). For other modules, first import the module then
438 assign the symbol to a module-level variable. In addition, these imports
438 assign the symbol to a module-level variable. In addition, these imports
439 must be performed before other local imports. This rule only
439 must be performed before other local imports. This rule only
440 applies to import statements outside of any blocks.
440 applies to import statements outside of any blocks.
441 * Relative imports from the standard library are not allowed, unless that
441 * Relative imports from the standard library are not allowed, unless that
442 library is also a local module.
442 library is also a local module.
443 * Certain modules must be aliased to alternate names to avoid aliasing
443 * Certain modules must be aliased to alternate names to avoid aliasing
444 and readability problems. See `requirealias`.
444 and readability problems. See `requirealias`.
445 """
445 """
446 if not isinstance(module, str):
446 if not isinstance(module, str):
447 module = module.decode('ascii')
447 module = module.decode('ascii')
448 topmodule = module.split('.')[0]
448 topmodule = module.split('.')[0]
449 fromlocal = fromlocalfunc(module, localmods)
449 fromlocal = fromlocalfunc(module, localmods)
450
450
451 # Whether a local/non-stdlib import has been performed.
451 # Whether a local/non-stdlib import has been performed.
452 seenlocal = None
452 seenlocal = None
453 # Whether a local/non-stdlib, non-symbol import has been seen.
453 # Whether a local/non-stdlib, non-symbol import has been seen.
454 seennonsymbollocal = False
454 seennonsymbollocal = False
455 # The last name to be imported (for sorting).
455 # The last name to be imported (for sorting).
456 lastname = None
456 lastname = None
457 laststdlib = None
457 laststdlib = None
458 # Relative import levels encountered so far.
458 # Relative import levels encountered so far.
459 seenlevels = set()
459 seenlevels = set()
460
460
461 for node, newscope in walklocal(root):
461 for node, newscope in walklocal(root):
462
462
463 def msg(fmt, *args):
463 def msg(fmt, *args):
464 return (fmt % args, node.lineno)
464 return (fmt % args, node.lineno)
465
465
466 if newscope:
466 if newscope:
467 # Check for local imports in function
467 # Check for local imports in function
468 for r in verify_modern_convention(
468 for r in verify_modern_convention(
469 module, node, localmods, node.col_offset + 4
469 module, node, localmods, node.col_offset + 4
470 ):
470 ):
471 yield r
471 yield r
472 elif isinstance(node, ast.Import):
472 elif isinstance(node, ast.Import):
473 # Disallow "import foo, bar" and require separate imports
473 # Disallow "import foo, bar" and require separate imports
474 # for each module.
474 # for each module.
475 if len(node.names) > 1:
475 if len(node.names) > 1:
476 yield msg(
476 yield msg(
477 'multiple imported names: %s',
477 'multiple imported names: %s',
478 ', '.join(n.name for n in node.names),
478 ', '.join(n.name for n in node.names),
479 )
479 )
480
480
481 name = node.names[0].name
481 name = node.names[0].name
482 asname = node.names[0].asname
482 asname = node.names[0].asname
483
483
484 stdlib = name in stdlib_modules
484 stdlib = name in stdlib_modules
485
485
486 # Ignore sorting rules on imports inside blocks.
486 # Ignore sorting rules on imports inside blocks.
487 if node.col_offset == root_col_offset:
487 if node.col_offset == root_col_offset:
488 if lastname and name < lastname and laststdlib == stdlib:
488 if lastname and name < lastname and laststdlib == stdlib:
489 yield msg(
489 yield msg(
490 'imports not lexically sorted: %s < %s', name, lastname
490 'imports not lexically sorted: %s < %s', name, lastname
491 )
491 )
492
492
493 lastname = name
493 lastname = name
494 laststdlib = stdlib
494 laststdlib = stdlib
495
495
496 # stdlib imports should be before local imports.
496 # stdlib imports should be before local imports.
497 if stdlib and seenlocal and node.col_offset == root_col_offset:
497 if stdlib and seenlocal and node.col_offset == root_col_offset:
498 yield msg(
498 yield msg(
499 'stdlib import "%s" follows local import: %s',
499 'stdlib import "%s" follows local import: %s',
500 name,
500 name,
501 seenlocal,
501 seenlocal,
502 )
502 )
503
503
504 if not stdlib:
504 if not stdlib:
505 seenlocal = name
505 seenlocal = name
506
506
507 # Import of sibling modules should use relative imports.
507 # Import of sibling modules should use relative imports.
508 topname = name.split('.')[0]
508 topname = name.split('.')[0]
509 if topname == topmodule:
509 if topname == topmodule:
510 yield msg('import should be relative: %s', name)
510 yield msg('import should be relative: %s', name)
511
511
512 if name in requirealias and asname != requirealias[name]:
512 if name in requirealias and asname != requirealias[name]:
513 yield msg(
513 yield msg(
514 '%s module must be "as" aliased to %s',
514 '%s module must be "as" aliased to %s',
515 name,
515 name,
516 requirealias[name],
516 requirealias[name],
517 )
517 )
518
518
519 elif isinstance(node, ast.ImportFrom):
519 elif isinstance(node, ast.ImportFrom):
520 # Resolve the full imported module name.
520 # Resolve the full imported module name.
521 if node.level > 0:
521 if node.level > 0:
522 fullname = '.'.join(module.split('.')[: -node.level])
522 fullname = '.'.join(module.split('.')[: -node.level])
523 if node.module:
523 if node.module:
524 fullname += '.%s' % node.module
524 fullname += '.%s' % node.module
525 else:
525 else:
526 assert node.module
526 assert node.module
527 fullname = node.module
527 fullname = node.module
528
528
529 topname = fullname.split('.')[0]
529 topname = fullname.split('.')[0]
530 if topname == topmodule:
530 if topname == topmodule:
531 yield msg('import should be relative: %s', fullname)
531 yield msg('import should be relative: %s', fullname)
532
532
533 # __future__ is special since it needs to come first and use
533 # __future__ is special since it needs to come first and use
534 # symbol import.
534 # symbol import.
535 if fullname != '__future__':
535 if fullname != '__future__':
536 if not fullname or (
536 if not fullname or (
537 fullname in stdlib_modules
537 fullname in stdlib_modules
538 # allow standard 'from typing import ...' style
538 # allow standard 'from typing import ...' style
539 and fullname != 'typing'
539 and fullname.startswith('.')
540 and fullname not in localmods
540 and fullname not in localmods
541 and fullname + '.__init__' not in localmods
541 and fullname + '.__init__' not in localmods
542 ):
542 ):
543 yield msg('relative import of stdlib module')
543 yield msg('relative import of stdlib module')
544 else:
544 else:
545 seenlocal = fullname
545 seenlocal = fullname
546
546
547 # Direct symbol import is only allowed from certain modules and
547 # Direct symbol import is only allowed from certain modules and
548 # must occur before non-symbol imports.
548 # must occur before non-symbol imports.
549 found = fromlocal(node.module, node.level)
549 found = fromlocal(node.module, node.level)
550 if found and found[2]: # node.module is a package
550 if found and found[2]: # node.module is a package
551 prefix = found[0] + '.'
551 prefix = found[0] + '.'
552 symbols = (
552 symbols = (
553 n.name for n in node.names if not fromlocal(prefix + n.name)
553 n.name for n in node.names if not fromlocal(prefix + n.name)
554 )
554 )
555 else:
555 else:
556 symbols = (n.name for n in node.names)
556 symbols = (n.name for n in node.names)
557 symbols = [sym for sym in symbols if sym not in directsymbols]
557 symbols = [sym for sym in symbols if sym not in directsymbols]
558 if node.module and node.col_offset == root_col_offset:
558 if node.module and node.col_offset == root_col_offset:
559 if symbols and fullname not in allowsymbolimports:
559 if symbols and fullname not in allowsymbolimports:
560 yield msg(
560 yield msg(
561 'direct symbol import %s from %s',
561 'direct symbol import %s from %s',
562 ', '.join(symbols),
562 ', '.join(symbols),
563 fullname,
563 fullname,
564 )
564 )
565
565
566 if symbols and seennonsymbollocal:
566 if symbols and seennonsymbollocal:
567 yield msg(
567 yield msg(
568 'symbol import follows non-symbol import: %s', fullname
568 'symbol import follows non-symbol import: %s', fullname
569 )
569 )
570 if not symbols and fullname not in stdlib_modules:
570 if not symbols and fullname not in stdlib_modules:
571 seennonsymbollocal = True
571 seennonsymbollocal = True
572
572
573 if not node.module:
573 if not node.module:
574 assert node.level
574 assert node.level
575
575
576 # Only allow 1 group per level.
576 # Only allow 1 group per level.
577 if (
577 if (
578 node.level in seenlevels
578 node.level in seenlevels
579 and node.col_offset == root_col_offset
579 and node.col_offset == root_col_offset
580 ):
580 ):
581 yield msg(
581 yield msg(
582 'multiple "from %s import" statements', '.' * node.level
582 'multiple "from %s import" statements', '.' * node.level
583 )
583 )
584
584
585 # Higher-level groups come before lower-level groups.
585 # Higher-level groups come before lower-level groups.
586 if any(node.level > l for l in seenlevels):
586 if any(node.level > l for l in seenlevels):
587 yield msg(
587 yield msg(
588 'higher-level import should come first: %s', fullname
588 'higher-level import should come first: %s', fullname
589 )
589 )
590
590
591 seenlevels.add(node.level)
591 seenlevels.add(node.level)
592
592
593 # Entries in "from .X import ( ... )" lists must be lexically
593 # Entries in "from .X import ( ... )" lists must be lexically
594 # sorted.
594 # sorted.
595 lastentryname = None
595 lastentryname = None
596
596
597 for n in node.names:
597 for n in node.names:
598 if lastentryname and n.name < lastentryname:
598 if lastentryname and n.name < lastentryname:
599 yield msg(
599 yield msg(
600 'imports from %s not lexically sorted: %s < %s',
600 'imports from %s not lexically sorted: %s < %s',
601 fullname,
601 fullname,
602 n.name,
602 n.name,
603 lastentryname,
603 lastentryname,
604 )
604 )
605
605
606 lastentryname = n.name
606 lastentryname = n.name
607
607
608 if n.name in requirealias and n.asname != requirealias[n.name]:
608 if n.name in requirealias and n.asname != requirealias[n.name]:
609 yield msg(
609 yield msg(
610 '%s from %s must be "as" aliased to %s',
610 '%s from %s must be "as" aliased to %s',
611 n.name,
611 n.name,
612 fullname,
612 fullname,
613 requirealias[n.name],
613 requirealias[n.name],
614 )
614 )
615
615
616
616
617 def verify_stdlib_on_own_line(root):
617 def verify_stdlib_on_own_line(root):
618 """Given some python source, verify that stdlib imports are done
618 """Given some python source, verify that stdlib imports are done
619 in separate statements from relative local module imports.
619 in separate statements from relative local module imports.
620
620
621 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
621 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
622 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
622 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
623 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
623 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
624 []
624 []
625 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
625 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
626 []
626 []
627 """
627 """
628 for node in ast.walk(root):
628 for node in ast.walk(root):
629 if isinstance(node, ast.Import):
629 if isinstance(node, ast.Import):
630 from_stdlib = {False: [], True: []}
630 from_stdlib = {False: [], True: []}
631 for n in node.names:
631 for n in node.names:
632 from_stdlib[n.name in stdlib_modules].append(n.name)
632 from_stdlib[n.name in stdlib_modules].append(n.name)
633 if from_stdlib[True] and from_stdlib[False]:
633 if from_stdlib[True] and from_stdlib[False]:
634 yield (
634 yield (
635 'mixed imports\n stdlib: %s\n relative: %s'
635 'mixed imports\n stdlib: %s\n relative: %s'
636 % (
636 % (
637 ', '.join(sorted(from_stdlib[True])),
637 ', '.join(sorted(from_stdlib[True])),
638 ', '.join(sorted(from_stdlib[False])),
638 ', '.join(sorted(from_stdlib[False])),
639 ),
639 ),
640 node.lineno,
640 node.lineno,
641 )
641 )
642
642
643
643
644 class CircularImport(Exception):
644 class CircularImport(Exception):
645 pass
645 pass
646
646
647
647
648 def checkmod(mod, imports):
648 def checkmod(mod, imports):
649 shortest = {}
649 shortest = {}
650 visit = [[mod]]
650 visit = [[mod]]
651 while visit:
651 while visit:
652 path = visit.pop(0)
652 path = visit.pop(0)
653 for i in sorted(imports.get(path[-1], [])):
653 for i in sorted(imports.get(path[-1], [])):
654 if len(path) < shortest.get(i, 1000):
654 if len(path) < shortest.get(i, 1000):
655 shortest[i] = len(path)
655 shortest[i] = len(path)
656 if i in path:
656 if i in path:
657 if i == path[0]:
657 if i == path[0]:
658 raise CircularImport(path)
658 raise CircularImport(path)
659 continue
659 continue
660 visit.append(path + [i])
660 visit.append(path + [i])
661
661
662
662
663 def rotatecycle(cycle):
663 def rotatecycle(cycle):
664 """arrange a cycle so that the lexicographically first module listed first
664 """arrange a cycle so that the lexicographically first module listed first
665
665
666 >>> rotatecycle(['foo', 'bar'])
666 >>> rotatecycle(['foo', 'bar'])
667 ['bar', 'foo', 'bar']
667 ['bar', 'foo', 'bar']
668 """
668 """
669 lowest = min(cycle)
669 lowest = min(cycle)
670 idx = cycle.index(lowest)
670 idx = cycle.index(lowest)
671 return cycle[idx:] + cycle[:idx] + [lowest]
671 return cycle[idx:] + cycle[:idx] + [lowest]
672
672
673
673
674 def find_cycles(imports):
674 def find_cycles(imports):
675 """Find cycles in an already-loaded import graph.
675 """Find cycles in an already-loaded import graph.
676
676
677 All module names recorded in `imports` should be absolute one.
677 All module names recorded in `imports` should be absolute one.
678
678
679 >>> from __future__ import print_function
679 >>> from __future__ import print_function
680 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
680 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
681 ... 'top.bar': ['top.baz', 'sys'],
681 ... 'top.bar': ['top.baz', 'sys'],
682 ... 'top.baz': ['top.foo'],
682 ... 'top.baz': ['top.foo'],
683 ... 'top.qux': ['top.foo']}
683 ... 'top.qux': ['top.foo']}
684 >>> print('\\n'.join(sorted(find_cycles(imports))))
684 >>> print('\\n'.join(sorted(find_cycles(imports))))
685 top.bar -> top.baz -> top.foo -> top.bar
685 top.bar -> top.baz -> top.foo -> top.bar
686 top.foo -> top.qux -> top.foo
686 top.foo -> top.qux -> top.foo
687 """
687 """
688 cycles = set()
688 cycles = set()
689 for mod in sorted(imports.keys()):
689 for mod in sorted(imports.keys()):
690 try:
690 try:
691 checkmod(mod, imports)
691 checkmod(mod, imports)
692 except CircularImport as e:
692 except CircularImport as e:
693 cycle = e.args[0]
693 cycle = e.args[0]
694 cycles.add(" -> ".join(rotatecycle(cycle)))
694 cycles.add(" -> ".join(rotatecycle(cycle)))
695 return cycles
695 return cycles
696
696
697
697
698 def _cycle_sortkey(c):
698 def _cycle_sortkey(c):
699 return len(c), c
699 return len(c), c
700
700
701
701
702 def embedded(f, modname, src):
702 def embedded(f, modname, src):
703 """Extract embedded python code
703 """Extract embedded python code
704
704
705 >>> def _forcestr(thing):
705 >>> def _forcestr(thing):
706 ... if not isinstance(thing, str):
706 ... if not isinstance(thing, str):
707 ... return thing.decode('ascii')
707 ... return thing.decode('ascii')
708 ... return thing
708 ... return thing
709 >>> def test(fn, lines):
709 >>> def test(fn, lines):
710 ... for s, m, f, l in embedded(fn, b"example", lines):
710 ... for s, m, f, l in embedded(fn, b"example", lines):
711 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
711 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
712 ... print(repr(_forcestr(s)))
712 ... print(repr(_forcestr(s)))
713 >>> lines = [
713 >>> lines = [
714 ... 'comment',
714 ... 'comment',
715 ... ' >>> from __future__ import print_function',
715 ... ' >>> from __future__ import print_function',
716 ... " >>> ' multiline",
716 ... " >>> ' multiline",
717 ... " ... string'",
717 ... " ... string'",
718 ... ' ',
718 ... ' ',
719 ... 'comment',
719 ... 'comment',
720 ... ' $ cat > foo.py <<EOF',
720 ... ' $ cat > foo.py <<EOF',
721 ... ' > from __future__ import print_function',
721 ... ' > from __future__ import print_function',
722 ... ' > EOF',
722 ... ' > EOF',
723 ... ]
723 ... ]
724 >>> test(b"example.t", lines)
724 >>> test(b"example.t", lines)
725 example[2] doctest.py 1
725 example[2] doctest.py 1
726 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
726 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
727 example[8] foo.py 7
727 example[8] foo.py 7
728 'from __future__ import print_function\\n'
728 'from __future__ import print_function\\n'
729 """
729 """
730 errors = []
730 errors = []
731 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
731 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
732 if not name:
732 if not name:
733 # use 'doctest.py', in order to make already existing
733 # use 'doctest.py', in order to make already existing
734 # doctest above pass instantly
734 # doctest above pass instantly
735 name = 'doctest.py'
735 name = 'doctest.py'
736 # "starts" is "line number" (1-origin), but embedded() is
736 # "starts" is "line number" (1-origin), but embedded() is
737 # expected to return "line offset" (0-origin). Therefore, this
737 # expected to return "line offset" (0-origin). Therefore, this
738 # yields "starts - 1".
738 # yields "starts - 1".
739 if not isinstance(modname, str):
739 if not isinstance(modname, str):
740 modname = modname.decode('utf8')
740 modname = modname.decode('utf8')
741 yield code, "%s[%d]" % (modname, starts), name, starts - 1
741 yield code, "%s[%d]" % (modname, starts), name, starts - 1
742
742
743
743
744 def sources(f, modname):
744 def sources(f, modname):
745 """Yields possibly multiple sources from a filepath
745 """Yields possibly multiple sources from a filepath
746
746
747 input: filepath, modulename
747 input: filepath, modulename
748 yields: script(string), modulename, filepath, linenumber
748 yields: script(string), modulename, filepath, linenumber
749
749
750 For embedded scripts, the modulename and filepath will be different
750 For embedded scripts, the modulename and filepath will be different
751 from the function arguments. linenumber is an offset relative to
751 from the function arguments. linenumber is an offset relative to
752 the input file.
752 the input file.
753 """
753 """
754 py = False
754 py = False
755 if not f.endswith('.t'):
755 if not f.endswith('.t'):
756 with open(f, 'rb') as src:
756 with open(f, 'rb') as src:
757 yield src.read(), modname, f, 0
757 yield src.read(), modname, f, 0
758 py = True
758 py = True
759 if py or f.endswith('.t'):
759 if py or f.endswith('.t'):
760 # Strictly speaking we should sniff for the magic header that denotes
760 # Strictly speaking we should sniff for the magic header that denotes
761 # Python source file encoding. But in reality we don't use anything
761 # Python source file encoding. But in reality we don't use anything
762 # other than ASCII (mainly) and UTF-8 (in a few exceptions), so
762 # other than ASCII (mainly) and UTF-8 (in a few exceptions), so
763 # simplicity is fine.
763 # simplicity is fine.
764 with io.open(f, 'r', encoding='utf-8') as src:
764 with io.open(f, 'r', encoding='utf-8') as src:
765 for script, modname, t, line in embedded(f, modname, src):
765 for script, modname, t, line in embedded(f, modname, src):
766 yield script, modname.encode('utf8'), t, line
766 yield script, modname.encode('utf8'), t, line
767
767
768
768
769 def main(argv):
769 def main(argv):
770 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
770 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
771 print('Usage: %s {-|file [file] [file] ...}')
771 print('Usage: %s {-|file [file] [file] ...}')
772 return 1
772 return 1
773 if argv[1] == '-':
773 if argv[1] == '-':
774 argv = argv[:1]
774 argv = argv[:1]
775 argv.extend(l.rstrip() for l in sys.stdin.readlines())
775 argv.extend(l.rstrip() for l in sys.stdin.readlines())
776 localmodpaths = {}
776 localmodpaths = {}
777 used_imports = {}
777 used_imports = {}
778 any_errors = False
778 any_errors = False
779 for source_path in argv[1:]:
779 for source_path in argv[1:]:
780 modname = dotted_name_of_path(source_path)
780 modname = dotted_name_of_path(source_path)
781 localmodpaths[modname] = source_path
781 localmodpaths[modname] = source_path
782 localmods = populateextmods(localmodpaths)
782 localmods = populateextmods(localmodpaths)
783 for localmodname, source_path in sorted(localmodpaths.items()):
783 for localmodname, source_path in sorted(localmodpaths.items()):
784 if not isinstance(localmodname, bytes):
784 if not isinstance(localmodname, bytes):
785 # This is only safe because all hg's files are ascii
785 # This is only safe because all hg's files are ascii
786 localmodname = localmodname.encode('ascii')
786 localmodname = localmodname.encode('ascii')
787 for src, modname, name, line in sources(source_path, localmodname):
787 for src, modname, name, line in sources(source_path, localmodname):
788 try:
788 try:
789 used_imports[modname] = sorted(
789 used_imports[modname] = sorted(
790 imported_modules(
790 imported_modules(
791 src, modname, name, localmods, ignore_nested=True
791 src, modname, name, localmods, ignore_nested=True
792 )
792 )
793 )
793 )
794 for error, lineno in verify_import_convention(
794 for error, lineno in verify_import_convention(
795 modname, src, localmods
795 modname, src, localmods
796 ):
796 ):
797 any_errors = True
797 any_errors = True
798 print('%s:%d: %s' % (source_path, lineno + line, error))
798 print('%s:%d: %s' % (source_path, lineno + line, error))
799 except SyntaxError as e:
799 except SyntaxError as e:
800 print(
800 print(
801 '%s:%d: SyntaxError: %s' % (source_path, e.lineno + line, e)
801 '%s:%d: SyntaxError: %s' % (source_path, e.lineno + line, e)
802 )
802 )
803 cycles = find_cycles(used_imports)
803 cycles = find_cycles(used_imports)
804 if cycles:
804 if cycles:
805 firstmods = set()
805 firstmods = set()
806 for c in sorted(cycles, key=_cycle_sortkey):
806 for c in sorted(cycles, key=_cycle_sortkey):
807 first = c.split()[0]
807 first = c.split()[0]
808 # As a rough cut, ignore any cycle that starts with the
808 # As a rough cut, ignore any cycle that starts with the
809 # same module as some other cycle. Otherwise we see lots
809 # same module as some other cycle. Otherwise we see lots
810 # of cycles that are effectively duplicates.
810 # of cycles that are effectively duplicates.
811 if first in firstmods:
811 if first in firstmods:
812 continue
812 continue
813 print('Import cycle:', c)
813 print('Import cycle:', c)
814 firstmods.add(first)
814 firstmods.add(first)
815 any_errors = True
815 any_errors = True
816 return any_errors != 0
816 return any_errors != 0
817
817
818
818
819 if __name__ == '__main__':
819 if __name__ == '__main__':
820 sys.exit(int(main(sys.argv)))
820 sys.exit(int(main(sys.argv)))
@@ -1,159 +1,165
1 #require test-repo
1 #require test-repo
2
2
3 $ . "$TESTDIR/helpers-testrepo.sh"
3 $ . "$TESTDIR/helpers-testrepo.sh"
4 $ testrepohgenv
4 $ testrepohgenv
5 $ import_checker="$TESTDIR"/../contrib/import-checker.py
5 $ import_checker="$TESTDIR"/../contrib/import-checker.py
6
6
7 Run the doctests from the import checker, and make sure
7 Run the doctests from the import checker, and make sure
8 it's working correctly.
8 it's working correctly.
9 $ TERM=dumb
9 $ TERM=dumb
10 $ export TERM
10 $ export TERM
11 $ "$PYTHON" -m doctest $import_checker
11 $ "$PYTHON" -m doctest $import_checker
12
12
13 Run additional tests for the import checker
13 Run additional tests for the import checker
14
14
15 $ mkdir testpackage
15 $ mkdir testpackage
16 $ touch testpackage/__init__.py
16 $ touch testpackage/__init__.py
17
17
18 $ cat > testpackage/multiple.py << EOF
18 $ cat > testpackage/multiple.py << EOF
19 > from __future__ import absolute_import
19 > from __future__ import absolute_import
20 > import os, sys
20 > import os, sys
21 > EOF
21 > EOF
22
22
23 $ cat > testpackage/unsorted.py << EOF
23 $ cat > testpackage/unsorted.py << EOF
24 > from __future__ import absolute_import
24 > from __future__ import absolute_import
25 > import sys
25 > import sys
26 > import os
26 > import os
27 > EOF
27 > EOF
28
28
29 $ cat > testpackage/stdafterlocal.py << EOF
29 $ cat > testpackage/stdafterlocal.py << EOF
30 > from __future__ import absolute_import
30 > from __future__ import absolute_import
31 > from . import unsorted
31 > from . import unsorted
32 > import os
32 > import os
33 > EOF
33 > EOF
34
34
35 $ cat > testpackage/requirerelative.py << EOF
35 $ cat > testpackage/requirerelative.py << EOF
36 > from __future__ import absolute_import
36 > from __future__ import absolute_import
37 > import testpackage.unsorted
37 > import testpackage.unsorted
38 > EOF
38 > EOF
39
39
40 $ cat > testpackage/importalias.py << EOF
40 $ cat > testpackage/importalias.py << EOF
41 > from __future__ import absolute_import
41 > from __future__ import absolute_import
42 > import ui
42 > import ui
43 > EOF
43 > EOF
44
44
45 $ cat > testpackage/relativestdlib.py << EOF
45 $ cat > testpackage/relativestdlib.py << EOF
46 > from __future__ import absolute_import
46 > from __future__ import absolute_import
47 > from .. import os
47 > from .. import os
48 > EOF
48 > EOF
49
49
50 $ cat > testpackage/stdlibfrom.py << EOF
51 > from __future__ import absolute_import
52 > from collections import abc
53 > EOF
54
50 $ cat > testpackage/symbolimport.py << EOF
55 $ cat > testpackage/symbolimport.py << EOF
51 > from __future__ import absolute_import
56 > from __future__ import absolute_import
52 > from .unsorted import foo
57 > from .unsorted import foo
53 > EOF
58 > EOF
54
59
55 $ cat > testpackage/latesymbolimport.py << EOF
60 $ cat > testpackage/latesymbolimport.py << EOF
56 > from __future__ import absolute_import
61 > from __future__ import absolute_import
57 > from . import unsorted
62 > from . import unsorted
58 > from mercurial.node import hex
63 > from mercurial.node import hex
59 > EOF
64 > EOF
60
65
61 $ cat > testpackage/multiplegroups.py << EOF
66 $ cat > testpackage/multiplegroups.py << EOF
62 > from __future__ import absolute_import
67 > from __future__ import absolute_import
63 > from . import unsorted
68 > from . import unsorted
64 > from . import more
69 > from . import more
65 > EOF
70 > EOF
66
71
67 $ mkdir testpackage/subpackage
72 $ mkdir testpackage/subpackage
68 $ cat > testpackage/subpackage/levelpriority.py << EOF
73 $ cat > testpackage/subpackage/levelpriority.py << EOF
69 > from __future__ import absolute_import
74 > from __future__ import absolute_import
70 > from . import foo
75 > from . import foo
71 > from .. import parent
76 > from .. import parent
72 > EOF
77 > EOF
73
78
74 $ touch testpackage/subpackage/foo.py
79 $ touch testpackage/subpackage/foo.py
75 $ cat > testpackage/subpackage/__init__.py << EOF
80 $ cat > testpackage/subpackage/__init__.py << EOF
76 > from __future__ import absolute_import
81 > from __future__ import absolute_import
77 > from . import levelpriority # should not cause cycle
82 > from . import levelpriority # should not cause cycle
78 > EOF
83 > EOF
79
84
80 $ cat > testpackage/subpackage/localimport.py << EOF
85 $ cat > testpackage/subpackage/localimport.py << EOF
81 > from __future__ import absolute_import
86 > from __future__ import absolute_import
82 > from . import foo
87 > from . import foo
83 > def bar():
88 > def bar():
84 > # should not cause "higher-level import should come first"
89 > # should not cause "higher-level import should come first"
85 > from .. import unsorted
90 > from .. import unsorted
86 > # but other errors should be detected
91 > # but other errors should be detected
87 > from .. import more
92 > from .. import more
88 > import testpackage.subpackage.levelpriority
93 > import testpackage.subpackage.levelpriority
89 > EOF
94 > EOF
90
95
91 $ cat > testpackage/importmodulefromsub.py << EOF
96 $ cat > testpackage/importmodulefromsub.py << EOF
92 > from __future__ import absolute_import
97 > from __future__ import absolute_import
93 > from .subpackage import foo # not a "direct symbol import"
98 > from .subpackage import foo # not a "direct symbol import"
94 > EOF
99 > EOF
95
100
96 $ cat > testpackage/importsymbolfromsub.py << EOF
101 $ cat > testpackage/importsymbolfromsub.py << EOF
97 > from __future__ import absolute_import
102 > from __future__ import absolute_import
98 > from .subpackage import foo, nonmodule
103 > from .subpackage import foo, nonmodule
99 > EOF
104 > EOF
100
105
101 $ cat > testpackage/sortedentries.py << EOF
106 $ cat > testpackage/sortedentries.py << EOF
102 > from __future__ import absolute_import
107 > from __future__ import absolute_import
103 > from . import (
108 > from . import (
104 > foo,
109 > foo,
105 > bar,
110 > bar,
106 > )
111 > )
107 > EOF
112 > EOF
108
113
109 $ cat > testpackage/importfromalias.py << EOF
114 $ cat > testpackage/importfromalias.py << EOF
110 > from __future__ import absolute_import
115 > from __future__ import absolute_import
111 > from . import ui
116 > from . import ui
112 > EOF
117 > EOF
113
118
114 $ cat > testpackage/importfromrelative.py << EOF
119 $ cat > testpackage/importfromrelative.py << EOF
115 > from __future__ import absolute_import
120 > from __future__ import absolute_import
116 > from testpackage.unsorted import foo
121 > from testpackage.unsorted import foo
117 > EOF
122 > EOF
118
123
119 $ mkdir testpackage2
124 $ mkdir testpackage2
120 $ touch testpackage2/__init__.py
125 $ touch testpackage2/__init__.py
121
126
122 $ cat > testpackage2/latesymbolimport.py << EOF
127 $ cat > testpackage2/latesymbolimport.py << EOF
123 > from __future__ import absolute_import
128 > from __future__ import absolute_import
124 > from testpackage import unsorted
129 > from testpackage import unsorted
125 > from mercurial.node import hex
130 > from mercurial.node import hex
126 > EOF
131 > EOF
127
132
128 # Shadowing a stdlib module to test "relative import of stdlib module" is
133 # Shadowing a stdlib module to test "relative import of stdlib module" is
129 # allowed if the module is also being checked
134 # allowed if the module is also being checked
130
135
131 $ mkdir email
136 $ mkdir email
132 $ touch email/__init__.py
137 $ touch email/__init__.py
133 $ touch email/errors.py
138 $ touch email/errors.py
134 $ cat > email/utils.py << EOF
139 $ cat > email/utils.py << EOF
135 > from __future__ import absolute_import
140 > from __future__ import absolute_import
136 > from . import errors
141 > from . import errors
137 > EOF
142 > EOF
138
143
139 $ "$PYTHON" "$import_checker" testpackage*/*.py testpackage/subpackage/*.py \
144 $ "$PYTHON" "$import_checker" testpackage*/*.py testpackage/subpackage/*.py \
140 > email/*.py
145 > email/*.py
141 testpackage/importalias.py:2: ui module must be "as" aliased to uimod
146 testpackage/importalias.py:2: ui module must be "as" aliased to uimod
142 testpackage/importfromalias.py:2: ui from testpackage must be "as" aliased to uimod
147 testpackage/importfromalias.py:2: ui from testpackage must be "as" aliased to uimod
143 testpackage/importfromrelative.py:2: import should be relative: testpackage.unsorted
148 testpackage/importfromrelative.py:2: import should be relative: testpackage.unsorted
144 testpackage/importfromrelative.py:2: direct symbol import foo from testpackage.unsorted
149 testpackage/importfromrelative.py:2: direct symbol import foo from testpackage.unsorted
145 testpackage/importsymbolfromsub.py:2: direct symbol import nonmodule from testpackage.subpackage
150 testpackage/importsymbolfromsub.py:2: direct symbol import nonmodule from testpackage.subpackage
146 testpackage/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
151 testpackage/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
147 testpackage/multiple.py:2: multiple imported names: os, sys
152 testpackage/multiple.py:2: multiple imported names: os, sys
148 testpackage/multiplegroups.py:3: multiple "from . import" statements
153 testpackage/multiplegroups.py:3: multiple "from . import" statements
149 testpackage/relativestdlib.py:2: relative import of stdlib module
154 testpackage/relativestdlib.py:2: relative import of stdlib module
150 testpackage/requirerelative.py:2: import should be relative: testpackage.unsorted
155 testpackage/requirerelative.py:2: import should be relative: testpackage.unsorted
151 testpackage/sortedentries.py:2: imports from testpackage not lexically sorted: bar < foo
156 testpackage/sortedentries.py:2: imports from testpackage not lexically sorted: bar < foo
152 testpackage/stdafterlocal.py:3: stdlib import "os" follows local import: testpackage
157 testpackage/stdafterlocal.py:3: stdlib import "os" follows local import: testpackage
158 testpackage/stdlibfrom.py:2: direct symbol import abc from collections
153 testpackage/subpackage/levelpriority.py:3: higher-level import should come first: testpackage
159 testpackage/subpackage/levelpriority.py:3: higher-level import should come first: testpackage
154 testpackage/subpackage/localimport.py:7: multiple "from .. import" statements
160 testpackage/subpackage/localimport.py:7: multiple "from .. import" statements
155 testpackage/subpackage/localimport.py:8: import should be relative: testpackage.subpackage.levelpriority
161 testpackage/subpackage/localimport.py:8: import should be relative: testpackage.subpackage.levelpriority
156 testpackage/symbolimport.py:2: direct symbol import foo from testpackage.unsorted
162 testpackage/symbolimport.py:2: direct symbol import foo from testpackage.unsorted
157 testpackage/unsorted.py:3: imports not lexically sorted: os < sys
163 testpackage/unsorted.py:3: imports not lexically sorted: os < sys
158 testpackage2/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
164 testpackage2/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
159 [1]
165 [1]
General Comments 0
You need to be logged in to leave comments. Login now