1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
# Copyright Canonical Limited 2005. All rights reserved.
import __builtin__
import atexit
import itertools
from operator import attrgetter
original_import = __builtin__.__import__
database_root = 'canonical.launchpad.database'
naughty_imports = set()
def text_lines_to_set(text):
return set(line.strip() for line in text.splitlines() if line.strip())
# zope.testing.doctest: called as part of creating a DocTestSuite.
permitted_database_imports = text_lines_to_set("""
zope.testing.doctest
canonical.librarian.db
canonical.doap.fileimporter
canonical.foaf.nickname
canonical.archivepublisher.domination
canonical.launchpad.hctapi
canonical.launchpad.vocabularies.dbobjects
canonical.librarian.client
""")
warned_database_imports = text_lines_to_set("""
canonical.launchpad.browser.distrorelease
canonical.launchpad.scripts.builddmaster
canonical.launchpad.scripts.po_import
canonical.launchpad.systemhomes
canonical.rosetta
""")
def database_import_allowed_into(module_path):
"""Return True if database code is allowed to be imported into the given
module path. Otherwise, returns False.
It is allowed if:
- The import was made with the __import__ hook.
- The importer is from within canonical.launchpad.database.
- The importer is a 'test' module.
- The importer is in the set of permitted_database_imports.
Note that being in the set of warned_database_imports does not make
the import allowed.
"""
if (module_path == '__import__ hook' or
module_path.startswith('canonical.launchpad.database') or
is_test_module(module_path)):
return True
return module_path in permitted_database_imports
def is_test_module(module_path):
"""Returns True if the module is for unit or functional tests.
Otherwise returns False.
"""
name_splitted = module_path.split('.')
return 'tests' in name_splitted or 'ftests' in name_splitted
class attrsgetter:
"""Like operator.attrgetter, but works on multiple attribute names."""
def __init__(self, *names):
self.names = names
def __call__(self, obj):
return tuple(getattr(obj, name) for name in self.names)
class JackbootError(ImportError):
"""Import Fascist says you can't make this import."""
def __init__(self, import_into, name, *args):
ImportError.__init__(self, import_into, name, *args)
self.import_into = import_into
self.name = name
def format_message(self):
return 'Generic JackbootError: %s imported into %s' % (
self.name, self.import_into)
def __str__(self):
return self.format_message()
class DatabaseImportPolicyViolation(JackbootError):
"""Database code is imported directly into other code."""
def format_message(self):
return 'You should not import %s into %s' % (
self.name, self.import_into)
class FromStarPolicyViolation(JackbootError):
"""import * from a module that has no __all__."""
def format_message(self):
return ('You should not import * from %s because it has no __all__'
' (in %s)' % (self.name, self.import_into))
class NotInModuleAllPolicyViolation(JackbootError):
"""import of a name that does not appear in a module's __all__."""
def __init__(self, import_into, name, attrname):
JackbootError.__init__(self, import_into, name, attrname)
self.attrname = attrname
def format_message(self):
return ('You should not import %s into %s from %s,'
' because it is not in its __all__.' %
(self.attrname, self.import_into, self.name))
class NotFoundPolicyViolation(JackbootError):
"""import of zope.exceptions.NotFoundError into
canonical.launchpad.database.
"""
def __init__(self, import_into):
JackbootError.__init__(self, import_into, '')
def format_message(self):
return ('%s\nDo not import zope.exceptions.NotFoundError.\n'
'Use canonical.launchpad.interfaces.NotFoundError instead.'
% self.import_into)
def import_fascist(name, globals={}, locals={}, fromlist=[]):
module = original_import(name, globals, locals, fromlist)
# Python's re module imports some odd stuff every time certain regexes
# are used. Let's optimize this.
# Also, 'dedent' is not in textwrap.__all__.
if name == 'sre' or name == 'textwrap':
return module
global naughty_imports
import_into = globals.get('__name__')
if import_into is None:
# We're being imported from the __import__ builtin.
# We could find out by jumping up the stack a frame.
# Let's not for now.
import_into = '__import__ hook'
if (import_into.startswith('canonical.launchpad.database') and
name == 'zope.exceptions'):
if fromlist and 'NotFoundError' in fromlist:
raise NotFoundPolicyViolation(import_into)
if (name.startswith(database_root) and
not database_import_allowed_into(import_into)):
error = DatabaseImportPolicyViolation(import_into, name)
naughty_imports.add(error)
# Raise an error except in the case of browser.traversers.
# This exception to raising an error is only temporary, until
# browser.traversers is cleaned up.
if import_into not in warned_database_imports:
raise error
if fromlist is not None and import_into.startswith('canonical'):
# We only want to warn about "from foo import bar" violations in our
# own code.
if list(fromlist) == ['*'] and not hasattr(module, '__all__'):
# "from foo import *" is naughty if foo has no __all__
error = FromStarPolicyViolation(import_into, name)
naughty_imports.add(error)
raise error
elif (list(fromlist) != ['*'] and hasattr(module, '__all__') and
not is_test_module(import_into)):
# "from foo import bar" is naughty if bar isn't in foo.__all__ (and
# foo actually has an __all__). Unless foo is within a tests
# or ftests module.
for attrname in fromlist:
if attrname not in module.__all__:
error = NotInModuleAllPolicyViolation(
import_into, name, attrname)
naughty_imports.add(error)
# Not raising on NotInModuleAllPolicyViolation yet.
#raise error
return module
def report_naughty_imports():
if naughty_imports:
print
print '** %d import policy violations **' % len(naughty_imports)
current_type = None
database_violations = []
fromstar_violations = []
notinall_violations = []
sorting_map = {
DatabaseImportPolicyViolation: database_violations,
FromStarPolicyViolation: fromstar_violations,
NotInModuleAllPolicyViolation: notinall_violations
}
for error in naughty_imports:
sorting_map[error.__class__].append(error)
if database_violations:
print
print "There were %s database import violations." % (
len(database_violations))
sorted_violations = sorted(
database_violations,
key=attrsgetter('name', 'import_into'))
for name, sequence in itertools.groupby(
sorted_violations, attrgetter('name')):
print "You should not import %s into:" % name
for import_into, unused_duplicates_seq in itertools.groupby(
sequence, attrgetter('import_into')):
# Show first occurrence only, to avoid duplicates.
print " ", import_into
if fromstar_violations:
print
print "There were %s imports 'from *' without an __all__." % (
len(fromstar_violations))
sorted_violations = sorted(
fromstar_violations,
key=attrsgetter('import_into', 'name'))
for import_into, sequence in itertools.groupby(
sorted_violations, attrgetter('import_into')):
print "You should not import * into %s from" % import_into
for error in sequence:
print " ", error.name
if notinall_violations:
print
print (
"There were %s imports of names not appearing in the __all__."
% len(notinall_violations))
sorted_violations = sorted(
notinall_violations,
key=attrsgetter('name', 'attrname', 'import_into'))
for (name, attrname), sequence in itertools.groupby(
sorted_violations, attrsgetter('name', 'attrname')):
print "You should not import %s from %s:" % (attrname, name)
import_intos = sorted(
set([error.import_into for error in sequence]))
for import_into in import_intos:
print " ", import_into
def install_import_fascist():
# XXX: Import fascist currently disabled as it appears to stop
# the ZCML engine from importing modules. Open a bug on this.
return
__builtin__.__import__ = import_fascist
atexit.register(report_naughty_imports)
|