annotate contrib/import-checker.py @ 20197:761f2929a6ad

import-checker: refactor sys.path prefix check (issue4129) This patch refactors the logic in contrib/import-checker.py responsible for checking the beginnings of the paths in sys.path. In particular, it adds a variable that defines the set of allowed prefixes. The primary purpose of this change is to make it easier to add more allowed prefixes. This will be useful in resolving issue4129, which involves making the function list_stdlib_modules() work when run from a virtualenv.
author Chris Jerdonek <chris.jerdonek@gmail.com>
date Sun, 22 Dec 2013 14:10:26 -0800
parents c65a6937b828
children f5393a9dc4e5
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
1 import ast
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
2 import os
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
3 import sys
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
4
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
5 def dotted_name_of_path(path):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
6 """Given a relative path to a source file, return its dotted module name.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
7
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
8
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
9 >>> dotted_name_of_path('mercurial/error.py')
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
10 'mercurial.error'
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
11 """
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
12 parts = path.split('/')
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
13 parts[-1] = parts[-1][:-3] # remove .py
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
14 return '.'.join(parts)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
15
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
16
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
17 def list_stdlib_modules():
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
18 """List the modules present in the stdlib.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
19
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
20 >>> mods = set(list_stdlib_modules())
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
21 >>> 'BaseHTTPServer' in mods
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
22 True
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
23
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
24 os.path isn't really a module, so it's missing:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
25
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
26 >>> 'os.path' in mods
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
27 False
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
28
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
29 sys requires special treatment, because it's baked into the
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
30 interpreter, but it should still appear:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
31
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
32 >>> 'sys' in mods
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
33 True
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
34
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
35 >>> 'collections' in mods
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
36 True
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
37
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
38 >>> 'cStringIO' in mods
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
39 True
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
40 """
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
41 for m in sys.builtin_module_names:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
42 yield m
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
43 # These modules only exist on windows, but we should always
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
44 # consider them stdlib.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
45 for m in ['msvcrt', '_winreg']:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
46 yield m
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
47 # These get missed too
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
48 for m in 'ctypes', 'email':
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
49 yield m
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
50 yield 'builtins' # python3 only
20197
761f2929a6ad import-checker: refactor sys.path prefix check (issue4129)
Chris Jerdonek <chris.jerdonek@gmail.com>
parents: 20038
diff changeset
51 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
52 for libpath in sys.path:
20197
761f2929a6ad import-checker: refactor sys.path prefix check (issue4129)
Chris Jerdonek <chris.jerdonek@gmail.com>
parents: 20038
diff changeset
53 # We want to walk everything in sys.path that starts with something
761f2929a6ad import-checker: refactor sys.path prefix check (issue4129)
Chris Jerdonek <chris.jerdonek@gmail.com>
parents: 20038
diff changeset
54 # in stdlib_prefixes.
761f2929a6ad import-checker: refactor sys.path prefix check (issue4129)
Chris Jerdonek <chris.jerdonek@gmail.com>
parents: 20038
diff changeset
55 for prefix in stdlib_prefixes:
761f2929a6ad import-checker: refactor sys.path prefix check (issue4129)
Chris Jerdonek <chris.jerdonek@gmail.com>
parents: 20038
diff changeset
56 if libpath.startswith(prefix):
761f2929a6ad import-checker: refactor sys.path prefix check (issue4129)
Chris Jerdonek <chris.jerdonek@gmail.com>
parents: 20038
diff changeset
57 break
761f2929a6ad import-checker: refactor sys.path prefix check (issue4129)
Chris Jerdonek <chris.jerdonek@gmail.com>
parents: 20038
diff changeset
58 else:
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
59 continue
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
60 if 'site-packages' in libpath:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
61 continue
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
62 for top, dirs, files in os.walk(libpath):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
63 for name in files:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
64 if name == '__init__.py':
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
65 continue
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
66 if not (name.endswith('.py') or name.endswith('.so')):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
67 continue
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
68 full_path = os.path.join(top, name)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
69 if 'site-packages' in full_path:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
70 continue
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
71 rel_path = full_path[len(libpath) + 1:]
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
72 mod = dotted_name_of_path(rel_path)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
73 yield mod
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
74
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
75 stdlib_modules = set(list_stdlib_modules())
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
76
20037
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
77 def imported_modules(source, ignore_nested=False):
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
78 """Given the source of a file as a string, yield the names
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
79 imported by that file.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
80
20037
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
81 Args:
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
82 source: The python source to examine as a string.
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
83 ignore_nested: If true, import statements that do not start in
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
84 column zero will be ignored.
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
85
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
86 Returns:
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
87 A list of module names imported by the given source.
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
88
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
89 >>> sorted(imported_modules(
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
90 ... 'import foo ; from baz import bar; import foo.qux'))
20037
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
91 ['baz.bar', 'foo', 'foo.qux']
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
92 >>> sorted(imported_modules(
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
93 ... '''import foo
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
94 ... def wat():
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
95 ... import bar
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
96 ... ''', ignore_nested=True))
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
97 ['foo']
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
98 """
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
99 for node in ast.walk(ast.parse(source)):
20037
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
100 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
101 continue
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
102 if isinstance(node, ast.Import):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
103 for n in node.names:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
104 yield n.name
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
105 elif isinstance(node, ast.ImportFrom):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
106 prefix = node.module + '.'
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
107 for n in node.names:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
108 yield prefix + n.name
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
109
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
110 def verify_stdlib_on_own_line(source):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
111 """Given some python source, verify that stdlib imports are done
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
112 in separate statements from relative local module imports.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
113
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
114 Observing this limitation is important as it works around an
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
115 annoying lib2to3 bug in relative import rewrites:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
116 http://bugs.python.org/issue19510.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
117
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
118 >>> list(verify_stdlib_on_own_line('import sys, foo'))
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
119 ['mixed stdlib and relative imports:\\n foo, sys']
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
120 >>> list(verify_stdlib_on_own_line('import sys, os'))
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
121 []
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
122 >>> list(verify_stdlib_on_own_line('import foo, bar'))
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
123 []
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
124 """
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
125 for node in ast.walk(ast.parse(source)):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
126 if isinstance(node, ast.Import):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
127 from_stdlib = {}
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
128 for n in node.names:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
129 from_stdlib[n.name] = n.name in stdlib_modules
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
130 num_std = len([x for x in from_stdlib.values() if x])
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
131 if num_std not in (len(from_stdlib.values()), 0):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
132 yield ('mixed stdlib and relative imports:\n %s' %
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
133 ', '.join(sorted(from_stdlib.iterkeys())))
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
134
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
135 class CircularImport(Exception):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
136 pass
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
137
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
138
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
139 def cyclekey(names):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
140 return tuple(sorted(set(names)))
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
141
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
142 def check_one_mod(mod, imports, path=None, ignore=None):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
143 if path is None:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
144 path = []
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
145 if ignore is None:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
146 ignore = []
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
147 path = path + [mod]
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
148 for i in sorted(imports.get(mod, [])):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
149 if i not in stdlib_modules:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
150 i = mod.rsplit('.', 1)[0] + '.' + i
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
151 if i in path:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
152 firstspot = path.index(i)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
153 cycle = path[firstspot:] + [i]
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
154 if cyclekey(cycle) not in ignore:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
155 raise CircularImport(cycle)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
156 continue
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
157 check_one_mod(i, imports, path=path, ignore=ignore)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
158
20038
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
159 def rotatecycle(cycle):
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
160 """arrange a cycle so that the lexicographically first module listed first
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
161
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
162 >>> rotatecycle(['foo', 'bar', 'foo'])
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
163 ['bar', 'foo', 'bar']
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
164 """
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
165 lowest = min(cycle)
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
166 idx = cycle.index(lowest)
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
167 return cycle[idx:] + cycle[1:idx] + [lowest]
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
168
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
169 def find_cycles(imports):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
170 """Find cycles in an already-loaded import graph.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
171
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
172 >>> imports = {'top.foo': ['bar', 'os.path', 'qux'],
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
173 ... 'top.bar': ['baz', 'sys'],
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
174 ... 'top.baz': ['foo'],
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
175 ... 'top.qux': ['foo']}
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
176 >>> print '\\n'.join(sorted(find_cycles(imports)))
20038
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
177 top.bar -> top.baz -> top.foo -> top.bar -> top.bar
c65a6937b828 import-checker: try a little harder to show fewer cycles
Augie Fackler <raf@durin42.com>
parents: 20037
diff changeset
178 top.foo -> top.qux -> top.foo -> top.foo
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
179 """
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
180 cycles = {}
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
181 for mod in sorted(imports.iterkeys()):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
182 try:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
183 check_one_mod(mod, imports, ignore=cycles)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
184 except CircularImport, e:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
185 cycle = e.args[0]
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
186 cycles[cyclekey(cycle)] = ' -> '.join(rotatecycle(cycle))
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
187 return cycles.values()
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
188
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
189 def _cycle_sortkey(c):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
190 return len(c), c
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
191
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
192 def main(argv):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
193 if len(argv) < 2:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
194 print 'Usage: %s file [file] [file] ...'
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
195 return 1
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
196 used_imports = {}
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
197 any_errors = False
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
198 for source_path in argv[1:]:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
199 f = open(source_path)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
200 modname = dotted_name_of_path(source_path)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
201 src = f.read()
20037
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
202 used_imports[modname] = sorted(
957b43371928 import-checker: ignore nested imports
Augie Fackler <raf@durin42.com>
parents: 20036
diff changeset
203 imported_modules(src, ignore_nested=True))
20036
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
204 for error in verify_stdlib_on_own_line(src):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
205 any_errors = True
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
206 print source_path, error
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
207 f.close()
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
208 cycles = find_cycles(used_imports)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
209 if cycles:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
210 firstmods = set()
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
211 for c in sorted(cycles, key=_cycle_sortkey):
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
212 first = c.split()[0]
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
213 # As a rough cut, ignore any cycle that starts with the
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
214 # same module as some other cycle. Otherwise we see lots
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
215 # of cycles that are effectively duplicates.
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
216 if first in firstmods:
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
217 continue
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
218 print 'Import cycle:', c
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
219 firstmods.add(first)
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
220 any_errors = True
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
221 return not any_errors
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
222
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
223 if __name__ == '__main__':
e5d51fa51aba contrib: add an import checker
Augie Fackler <raf@durin42.com>
parents:
diff changeset
224 sys.exit(int(main(sys.argv)))