~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("""
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
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
29
    canonical.archivepublisher.domination
3691.93.1 by Christian Reis
Factor the death row processing into a separate script. Implement the DeathRow processor, with size calculation, some optimizations and properly restricting it to a specific distribution, and a process-death-row script, which has a dry run mode. Do minor cleanups in DAR.publish(), implementing a DR.isUnstable() method. Test DeathRow. Change DiskPool file removal code to return the filesize of the file removed. Implement Source/BinaryPackageFilePublishing.displayname, with tests. Clean up publish-distro as much as I can..
30
    canonical.archivepublisher.deathrow
7362.9.13 by Jonathan Lange
Add a test for creating a sourcepackage branch via createBranch.
31
    canonical.codehosting.inmemory
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
32
    canonical.launchpad.vocabularies.dbobjects
5485.1.12 by Edwin Grubbs
Fixed some problems with PersonValidatorBase.
33
    canonical.launchpad.validators.person
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
34
    canonical.librarian.client
35
    """)
36
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
37
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
38
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.
39
    canonical.launchpad.scripts.ftpmaster
3691.164.16 by Guilherme Salgado
Lots of fixes and tests suggested by Bjorn
40
    canonical.launchpad.scripts.gina.handlers
5121.2.7 by Stuart Bishop
More required code changes
41
    canonical.launchpad.browser.distroseries
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
42
    canonical.launchpad.scripts.builddmaster
2570.1.7 by Carlos Perello Marin
Lots of fixes + new code + tests updates
43
    canonical.launchpad.scripts.po_import
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
44
    canonical.launchpad.systemhomes
45
    canonical.rosetta
46
    """)
47
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
48
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
49
def database_import_allowed_into(module_path):
50
    """Return True if database code is allowed to be imported into the given
51
    module path.  Otherwise, returns False.
52
53
    It is allowed if:
54
        - The import was made with the __import__ hook.
55
        - The importer is from within canonical.launchpad.database.
56
        - The importer is a 'test' module.
57
        - The importer is in the set of permitted_database_imports.
58
59
    Note that being in the set of warned_database_imports does not make
60
    the import allowed.
61
62
    """
63
    if (module_path == '__import__ hook' or
64
        module_path.startswith('canonical.launchpad.database') or
65
        is_test_module(module_path)):
66
        return True
67
    return module_path in permitted_database_imports
68
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
69
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
70
def is_test_module(module_path):
71
    """Returns True if the module is for unit or functional tests.
72
73
    Otherwise returns False.
74
    """
75
    name_splitted = module_path.split('.')
5554.4.10 by James Henstridge
* Remove some unnecessary imports from test_pages in page tests.
76
    return ('tests' in name_splitted or
77
            'ftests' in name_splitted or
78
            'testing' in name_splitted)
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
79
80
81
class attrsgetter:
82
    """Like operator.attrgetter, but works on multiple attribute names."""
83
84
    def __init__(self, *names):
85
        self.names = names
86
87
    def __call__(self, obj):
88
        return tuple(getattr(obj, name) for name in self.names)
89
90
91
class JackbootError(ImportError):
92
    """Import Fascist says you can't make this import."""
93
94
    def __init__(self, import_into, name, *args):
95
        ImportError.__init__(self, import_into, name, *args)
96
        self.import_into = import_into
97
        self.name = name
98
99
    def format_message(self):
100
        return 'Generic JackbootError: %s imported into %s' % (
101
            self.name, self.import_into)
102
103
    def __str__(self):
104
        return self.format_message()
105
106
107
class DatabaseImportPolicyViolation(JackbootError):
108
    """Database code is imported directly into other code."""
109
110
    def format_message(self):
111
        return 'You should not import %s into %s' % (
112
            self.name, self.import_into)
113
114
115
class FromStarPolicyViolation(JackbootError):
116
    """import * from a module that has no __all__."""
117
118
    def format_message(self):
119
        return ('You should not import * from %s because it has no __all__'
120
                ' (in %s)' % (self.name, self.import_into))
121
122
123
class NotInModuleAllPolicyViolation(JackbootError):
124
    """import of a name that does not appear in a module's __all__."""
125
126
    def __init__(self, import_into, name, attrname):
127
        JackbootError.__init__(self, import_into, name, attrname)
128
        self.attrname = attrname
129
130
    def format_message(self):
131
        return ('You should not import %s into %s from %s,'
132
                ' because it is not in its __all__.' %
133
                (self.attrname, self.import_into, self.name))
134
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
135
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.
136
class NotFoundPolicyViolation(JackbootError):
137
    """import of zope.exceptions.NotFoundError into
138
    canonical.launchpad.database.
139
    """
140
141
    def __init__(self, import_into):
142
        JackbootError.__init__(self, import_into, '')
143
144
    def format_message(self):
145
        return ('%s\nDo not import zope.exceptions.NotFoundError.\n'
146
                'Use canonical.launchpad.interfaces.NotFoundError instead.'
147
                % self.import_into)
148
149
7014.4.4 by Guilherme Salgado
Couple changes suggested by Celso.
150
# pylint: disable-msg=W0102,W0602
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
151
def import_fascist(name, globals={}, locals={}, fromlist=[]):
6667.1.2 by Barry Warsaw
Lint cleanups
152
    global naughty_imports
153
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
154
    try:
6061.2.28 by Gary Poster
make import fascist generally be gentle to zope.app.layers code; this fixes navigation.txt
155
        module = original_import(name, globals, locals, fromlist)
156
    except ImportError:
6061.14.19 by Francis J. Lacoste
Add bug number for import_fascist XXX.
157
        # 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
158
        # import_fascist screws zope configuration module which introspects
159
        # the stack to determine if an ImportError means a module
160
        # initialization error or a genuine error. The browser:page always
161
        # tries to load a layer from zope.app.layers first, which most of the
162
        # time doesn't exist and dies a horrible death because of the import
163
        # fascist. That's the long explanation for why we special case this
164
        # module.
165
        if name.startswith('zope.app.layers.'):
6061.12.1 by Curtis Hovey
importfascist is was breaking the testrunner.
166
            name = name[16:]
6061.2.28 by Gary Poster
make import fascist generally be gentle to zope.app.layers code; this fixes navigation.txt
167
            module = original_import(name, globals, locals, fromlist)
168
        else:
169
            raise
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
170
    # Python's re module imports some odd stuff every time certain regexes
171
    # 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.
172
    # Also, 'dedent' is not in textwrap.__all__.
173
    if name == 'sre' or name == 'textwrap':
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
174
        return module
175
6667.1.2 by Barry Warsaw
Lint cleanups
176
    # Mailman 2.1 code base is originally circa 1998, so yeah, no __all__'s.
177
    if name.startswith('Mailman'):
178
        return module
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
179
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
180
    # Some uses of __import__ pass None for globals, so handle that.
181
    import_into = None
182
    if globals is not None:
4108.5.2 by James Henstridge
Add zstorm initialisation files, hooking it into the test suite
183
        import_into = globals.get('__name__')
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
184
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
185
    if import_into is None:
186
        # We're being imported from the __import__ builtin.
187
        # We could find out by jumping up the stack a frame.
188
        # Let's not for now.
189
        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.
190
    if (import_into.startswith('canonical.launchpad.database') and
191
        name == 'zope.exceptions'):
192
        if fromlist and 'NotFoundError' in fromlist:
193
            raise NotFoundPolicyViolation(import_into)
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
194
    if (name.startswith(database_root) and
195
        not database_import_allowed_into(import_into)):
196
        error = DatabaseImportPolicyViolation(import_into, name)
197
        naughty_imports.add(error)
198
        # Raise an error except in the case of browser.traversers.
199
        # This exception to raising an error is only temporary, until
200
        # browser.traversers is cleaned up.
201
        if import_into not in warned_database_imports:
202
            raise error
203
204
    if fromlist is not None and import_into.startswith('canonical'):
6667.1.2 by Barry Warsaw
Lint cleanups
205
        # 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
206
        # own code.
207
        if list(fromlist) == ['*'] and not hasattr(module, '__all__'):
208
            # "from foo import *" is naughty if foo has no __all__
209
            error = FromStarPolicyViolation(import_into, name)
210
            naughty_imports.add(error)
211
            raise error
212
        elif (list(fromlist) != ['*'] and hasattr(module, '__all__') and
213
              not is_test_module(import_into)):
7014.4.4 by Guilherme Salgado
Couple changes suggested by Celso.
214
            # "from foo import bar" is naughty if bar isn't in foo.__all__
215
            # (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
216
            # or ftests module or bar is itself a module.
217
            for attrname in fromlist:
3249.4.2 by Steve Alexander
Make importfascist not complain about imports of __doc__ when that name
218
                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
219
                    if not isinstance(
220
                        getattr(module, attrname, None), types.ModuleType):
221
                        error = NotInModuleAllPolicyViolation(
222
                            import_into, name, attrname)
223
                        naughty_imports.add(error)
224
                        # Not raising on NotInModuleAllPolicyViolation yet.
225
                        #raise error
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
226
    return module
227
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
228
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
229
def report_naughty_imports():
230
    if naughty_imports:
231
        print
232
        print '** %d import policy violations **' % len(naughty_imports)
233
        current_type = None
234
235
        database_violations = []
236
        fromstar_violations = []
237
        notinall_violations = []
238
        sorting_map = {
239
            DatabaseImportPolicyViolation: database_violations,
240
            FromStarPolicyViolation: fromstar_violations,
241
            NotInModuleAllPolicyViolation: notinall_violations
242
            }
243
        for error in naughty_imports:
244
            sorting_map[error.__class__].append(error)
245
246
        if database_violations:
247
            print
248
            print "There were %s database import violations." % (
249
                len(database_violations))
250
            sorted_violations = sorted(
251
                database_violations,
252
                key=attrsgetter('name', 'import_into'))
253
254
            for name, sequence in itertools.groupby(
255
                sorted_violations, attrgetter('name')):
256
                print "You should not import %s into:" % name
257
                for import_into, unused_duplicates_seq in itertools.groupby(
258
                    sequence, attrgetter('import_into')):
259
                    # Show first occurrence only, to avoid duplicates.
260
                    print "   ", import_into
261
262
        if fromstar_violations:
263
            print
264
            print "There were %s imports 'from *' without an __all__." % (
265
                len(fromstar_violations))
266
            sorted_violations = sorted(
267
                fromstar_violations,
268
                key=attrsgetter('import_into', 'name'))
269
270
            for import_into, sequence in itertools.groupby(
271
                sorted_violations, attrgetter('import_into')):
272
                print "You should not import * into %s from" % import_into
273
                for error in sequence:
274
                    print "   ", error.name
275
276
        if notinall_violations:
277
            print
278
            print (
279
                "There were %s imports of names not appearing in the __all__."
280
                % len(notinall_violations))
281
            sorted_violations = sorted(
282
                notinall_violations,
283
                key=attrsgetter('name', 'attrname', 'import_into'))
284
285
            for (name, attrname), sequence in itertools.groupby(
286
                sorted_violations, attrsgetter('name', 'attrname')):
287
                print "You should not import %s from %s:" % (attrname, name)
288
                import_intos = sorted(
289
                    set([error.import_into for error in sequence]))
290
                for import_into in import_intos:
291
                    print "   ", import_into
292
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
293
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
294
def install_import_fascist():
295
    __builtin__.__import__ = import_fascist
296
    atexit.register(report_naughty_imports)