~launchpad-pqm/launchpad/devel

2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
1
# Copyright Canonical Limited 2005.  All rights reserved.
2
3
import __builtin__
4
import atexit
5
import itertools
3203.1.16 by Steve Alexander
Make the fascist ignore names imported not appearing in __all__ when the
6
import types
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
7
from operator import attrgetter
8
9
original_import = __builtin__.__import__
10
database_root = 'canonical.launchpad.database'
11
naughty_imports = set()
12
6002.2.4 by Barry Warsaw
Drive-by to silence the UserWarnings caused by Hardy packages.
13
# Silence bogus warnings from Hardy's python-pkg-resources package.
6019.1.1 by Barry Warsaw
Shut up UserWarnings on Hardy.
14
import warnings
6002.2.4 by Barry Warsaw
Drive-by to silence the UserWarnings caused by Hardy packages.
15
warnings.filterwarnings('ignore', category=UserWarning, append=True,
16
                        message=r'Module .*? is being added to sys.path')
17
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
18
def text_lines_to_set(text):
19
    return set(line.strip() for line in text.splitlines() if line.strip())
20
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
21
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
22
# zope.testing.doctest: called as part of creating a DocTestSuite.
23
permitted_database_imports = text_lines_to_set("""
8091.1.1 by Jonathan Lange
Revert database merge.
24
    zope.testing.doctest
25
    canonical.librarian.db
26
    canonical.doap.fileimporter
3691.93.7 by Christian Reis
Step 2 in refactoring: move FTPArchive stuff out of Publisher.
27
    canonical.archivepublisher.ftparchive
3691.93.6 by Christian Reis
Checkpoint first part of publisher refactoring: moving code from publish-distro to Publishing, seriously.
28
    canonical.archivepublisher.publishing
8091.1.1 by Jonathan Lange
Revert database merge.
29
    canonical.archivepublisher.domination
30
    canonical.archivepublisher.deathrow
7362.9.13 by Jonathan Lange
Add a test for creating a sourcepackage branch via createBranch.
31
    canonical.codehosting.inmemory
7860.1.4 by Jonathan Lange
Move the _listingToSortOrder method to browser, since it's really view
32
    canonical.launchpad.browser.branchlisting
7860.1.1 by Jonathan Lange
Make the branch feed view use collections.
33
    canonical.launchpad.feed.branch
8091.1.1 by Jonathan Lange
Revert database merge.
34
    canonical.launchpad.vocabularies.dbobjects
7675.74.11 by Stuart Bishop
garbo needs access to database classes
35
    canonical.launchpad.validators.person
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
36
    canonical.librarian.client
37
    """)
38
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
39
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
40
warned_database_imports = text_lines_to_set("""
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
41
    canonical.launchpad.scripts.ftpmaster
3691.164.16 by Guilherme Salgado
Lots of fixes and tests suggested by Bjorn
42
    canonical.launchpad.scripts.gina.handlers
5121.2.7 by Stuart Bishop
More required code changes
43
    canonical.launchpad.browser.distroseries
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
44
    canonical.launchpad.scripts.builddmaster
2570.1.7 by Carlos Perello Marin
Lots of fixes + new code + tests updates
45
    canonical.launchpad.scripts.po_import
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
46
    canonical.launchpad.systemhomes
47
    canonical.rosetta
48
    """)
49
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
50
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
51
def database_import_allowed_into(module_path):
52
    """Return True if database code is allowed to be imported into the given
53
    module path.  Otherwise, returns False.
54
55
    It is allowed if:
56
        - The import was made with the __import__ hook.
57
        - The importer is from within canonical.launchpad.database.
58
        - The importer is a 'test' module.
59
        - The importer is in the set of permitted_database_imports.
7944.3.22 by Francis J. Lacoste
Renamed database to model.
60
        - The importer is within a model module or package.
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
61
62
    Note that being in the set of warned_database_imports does not make
63
    the import allowed.
64
65
    """
66
    if (module_path == '__import__ hook' or
67
        module_path.startswith('canonical.launchpad.database') or
7944.3.22 by Francis J. Lacoste
Renamed database to model.
68
        '.model' in module_path or
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
69
        is_test_module(module_path)):
70
        return True
71
    return module_path in permitted_database_imports
72
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
73
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
74
def is_test_module(module_path):
75
    """Returns True if the module is for unit or functional tests.
76
77
    Otherwise returns False.
78
    """
79
    name_splitted = module_path.split('.')
5554.4.10 by James Henstridge
* Remove some unnecessary imports from test_pages in page tests.
80
    return ('tests' in name_splitted or
81
            'ftests' in name_splitted or
82
            'testing' in name_splitted)
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
83
84
85
class attrsgetter:
86
    """Like operator.attrgetter, but works on multiple attribute names."""
87
88
    def __init__(self, *names):
89
        self.names = names
90
91
    def __call__(self, obj):
92
        return tuple(getattr(obj, name) for name in self.names)
93
94
95
class JackbootError(ImportError):
96
    """Import Fascist says you can't make this import."""
97
98
    def __init__(self, import_into, name, *args):
99
        ImportError.__init__(self, import_into, name, *args)
100
        self.import_into = import_into
101
        self.name = name
102
103
    def format_message(self):
104
        return 'Generic JackbootError: %s imported into %s' % (
105
            self.name, self.import_into)
106
107
    def __str__(self):
108
        return self.format_message()
109
110
111
class DatabaseImportPolicyViolation(JackbootError):
112
    """Database code is imported directly into other code."""
113
114
    def format_message(self):
115
        return 'You should not import %s into %s' % (
116
            self.name, self.import_into)
117
118
119
class FromStarPolicyViolation(JackbootError):
120
    """import * from a module that has no __all__."""
121
122
    def format_message(self):
123
        return ('You should not import * from %s because it has no __all__'
124
                ' (in %s)' % (self.name, self.import_into))
125
126
127
class NotInModuleAllPolicyViolation(JackbootError):
128
    """import of a name that does not appear in a module's __all__."""
129
130
    def __init__(self, import_into, name, attrname):
131
        JackbootError.__init__(self, import_into, name, attrname)
132
        self.attrname = attrname
133
134
    def format_message(self):
135
        return ('You should not import %s into %s from %s,'
136
                ' because it is not in its __all__.' %
137
                (self.attrname, self.import_into, self.name))
138
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
139
2630 by Canonical.com Patch Queue Manager
[trivial] lots of tidying up. converting all database classes to use NotFoundError consistently, and to import it from launchpad.interfaces in preparation for the move to a new zope3. Also, introduced a NameNotAvailable error. removed browser:traverse rdirective. commented out shipit test that fails sometimes.
140
class NotFoundPolicyViolation(JackbootError):
141
    """import of zope.exceptions.NotFoundError into
142
    canonical.launchpad.database.
143
    """
144
145
    def __init__(self, import_into):
146
        JackbootError.__init__(self, import_into, '')
147
148
    def format_message(self):
149
        return ('%s\nDo not import zope.exceptions.NotFoundError.\n'
150
                'Use canonical.launchpad.interfaces.NotFoundError instead.'
151
                % self.import_into)
152
153
7014.4.4 by Guilherme Salgado
Couple changes suggested by Celso.
154
# pylint: disable-msg=W0102,W0602
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
155
def import_fascist(name, globals={}, locals={}, fromlist=[]):
6667.1.2 by Barry Warsaw
Lint cleanups
156
    global naughty_imports
157
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
158
    try:
6061.2.28 by Gary Poster
make import fascist generally be gentle to zope.app.layers code; this fixes navigation.txt
159
        module = original_import(name, globals, locals, fromlist)
160
    except ImportError:
6061.14.19 by Francis J. Lacoste
Add bug number for import_fascist XXX.
161
        # XXX sinzui 2008-04-17 bug=277274:
6061.2.28 by Gary Poster
make import fascist generally be gentle to zope.app.layers code; this fixes navigation.txt
162
        # import_fascist screws zope configuration module which introspects
163
        # the stack to determine if an ImportError means a module
164
        # initialization error or a genuine error. The browser:page always
165
        # tries to load a layer from zope.app.layers first, which most of the
166
        # time doesn't exist and dies a horrible death because of the import
167
        # fascist. That's the long explanation for why we special case this
168
        # module.
169
        if name.startswith('zope.app.layers.'):
6061.12.1 by Curtis Hovey
importfascist is was breaking the testrunner.
170
            name = name[16:]
6061.2.28 by Gary Poster
make import fascist generally be gentle to zope.app.layers code; this fixes navigation.txt
171
            module = original_import(name, globals, locals, fromlist)
172
        else:
173
            raise
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
174
    # Python's re module imports some odd stuff every time certain regexes
175
    # are used.  Let's optimize this.
2381 by Canonical.com Patch Queue Manager
[r=BjornT] added a facet attribute to zcml directives for page. refactored and added tests for the previously overridden defaultView directive.
176
    # Also, 'dedent' is not in textwrap.__all__.
177
    if name == 'sre' or name == 'textwrap':
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
178
        return module
179
6667.1.2 by Barry Warsaw
Lint cleanups
180
    # Mailman 2.1 code base is originally circa 1998, so yeah, no __all__'s.
181
    if name.startswith('Mailman'):
182
        return module
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
183
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
184
    # Some uses of __import__ pass None for globals, so handle that.
185
    import_into = None
186
    if globals is not None:
4108.5.2 by James Henstridge
Add zstorm initialisation files, hooking it into the test suite
187
        import_into = globals.get('__name__')
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
188
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
189
    if import_into is None:
190
        # We're being imported from the __import__ builtin.
191
        # We could find out by jumping up the stack a frame.
192
        # Let's not for now.
193
        import_into = '__import__ hook'
2630 by Canonical.com Patch Queue Manager
[trivial] lots of tidying up. converting all database classes to use NotFoundError consistently, and to import it from launchpad.interfaces in preparation for the move to a new zope3. Also, introduced a NameNotAvailable error. removed browser:traverse rdirective. commented out shipit test that fails sometimes.
194
    if (import_into.startswith('canonical.launchpad.database') and
195
        name == 'zope.exceptions'):
196
        if fromlist and 'NotFoundError' in fromlist:
197
            raise NotFoundPolicyViolation(import_into)
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
198
    if (name.startswith(database_root) and
199
        not database_import_allowed_into(import_into)):
200
        error = DatabaseImportPolicyViolation(import_into, name)
201
        naughty_imports.add(error)
202
        # Raise an error except in the case of browser.traversers.
203
        # This exception to raising an error is only temporary, until
204
        # browser.traversers is cleaned up.
205
        if import_into not in warned_database_imports:
206
            raise error
207
208
    if fromlist is not None and import_into.startswith('canonical'):
6667.1.2 by Barry Warsaw
Lint cleanups
209
        # We only want to warn about "from foo import bar" violations in our
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
210
        # own code.
211
        if list(fromlist) == ['*'] and not hasattr(module, '__all__'):
212
            # "from foo import *" is naughty if foo has no __all__
213
            error = FromStarPolicyViolation(import_into, name)
214
            naughty_imports.add(error)
215
            raise error
216
        elif (list(fromlist) != ['*'] and hasattr(module, '__all__') and
217
              not is_test_module(import_into)):
7014.4.4 by Guilherme Salgado
Couple changes suggested by Celso.
218
            # "from foo import bar" is naughty if bar isn't in foo.__all__
219
            # (and foo actually has an __all__).  Unless foo is within a tests
3203.1.16 by Steve Alexander
Make the fascist ignore names imported not appearing in __all__ when the
220
            # or ftests module or bar is itself a module.
221
            for attrname in fromlist:
7709.4.6 by Guilherme Salgado
A couple changes suggested by Gary
222
                if (attrname == 'adapter' 
223
                    and module.__name__ == 'zope.component'):
224
                    # 'adapter' is not in zope.component.__all__, but that's
225
                    # where it should be imported from.
226
                    continue
3249.4.2 by Steve Alexander
Make importfascist not complain about imports of __doc__ when that name
227
                if attrname != '__doc__' and attrname not in module.__all__:
3203.1.16 by Steve Alexander
Make the fascist ignore names imported not appearing in __all__ when the
228
                    if not isinstance(
229
                        getattr(module, attrname, None), types.ModuleType):
230
                        error = NotInModuleAllPolicyViolation(
231
                            import_into, name, attrname)
232
                        naughty_imports.add(error)
233
                        # Not raising on NotInModuleAllPolicyViolation yet.
234
                        #raise error
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
235
    return module
236
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
237
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
238
def report_naughty_imports():
239
    if naughty_imports:
240
        print
241
        print '** %d import policy violations **' % len(naughty_imports)
242
        current_type = None
243
244
        database_violations = []
245
        fromstar_violations = []
246
        notinall_violations = []
247
        sorting_map = {
248
            DatabaseImportPolicyViolation: database_violations,
249
            FromStarPolicyViolation: fromstar_violations,
250
            NotInModuleAllPolicyViolation: notinall_violations
251
            }
252
        for error in naughty_imports:
253
            sorting_map[error.__class__].append(error)
254
255
        if database_violations:
256
            print
257
            print "There were %s database import violations." % (
258
                len(database_violations))
259
            sorted_violations = sorted(
260
                database_violations,
261
                key=attrsgetter('name', 'import_into'))
262
263
            for name, sequence in itertools.groupby(
264
                sorted_violations, attrgetter('name')):
265
                print "You should not import %s into:" % name
266
                for import_into, unused_duplicates_seq in itertools.groupby(
267
                    sequence, attrgetter('import_into')):
268
                    # Show first occurrence only, to avoid duplicates.
269
                    print "   ", import_into
270
271
        if fromstar_violations:
272
            print
273
            print "There were %s imports 'from *' without an __all__." % (
274
                len(fromstar_violations))
275
            sorted_violations = sorted(
276
                fromstar_violations,
277
                key=attrsgetter('import_into', 'name'))
278
279
            for import_into, sequence in itertools.groupby(
280
                sorted_violations, attrgetter('import_into')):
281
                print "You should not import * into %s from" % import_into
282
                for error in sequence:
283
                    print "   ", error.name
284
285
        if notinall_violations:
286
            print
287
            print (
288
                "There were %s imports of names not appearing in the __all__."
289
                % len(notinall_violations))
290
            sorted_violations = sorted(
291
                notinall_violations,
292
                key=attrsgetter('name', 'attrname', 'import_into'))
293
294
            for (name, attrname), sequence in itertools.groupby(
295
                sorted_violations, attrsgetter('name', 'attrname')):
296
                print "You should not import %s from %s:" % (attrname, name)
297
                import_intos = sorted(
298
                    set([error.import_into for error in sequence]))
299
                for import_into in import_intos:
300
                    print "   ", import_into
301
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
302
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
303
def install_import_fascist():
304
    __builtin__.__import__ = import_fascist
305
    atexit.register(report_naughty_imports)