~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
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
18
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
19
def text_lines_to_set(text):
20
    return set(line.strip() for line in text.splitlines() if line.strip())
21
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
22
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
23
# zope.testing.doctest: called as part of creating a DocTestSuite.
24
permitted_database_imports = text_lines_to_set("""
25
    zope.testing.doctest
26
    canonical.librarian.db
27
    canonical.doap.fileimporter
28
    canonical.foaf.nickname
3691.93.7 by Christian Reis
Step 2 in refactoring: move FTPArchive stuff out of Publisher.
29
    canonical.archivepublisher.ftparchive
3691.93.6 by Christian Reis
Checkpoint first part of publisher refactoring: moving code from publish-distro to Publishing, seriously.
30
    canonical.archivepublisher.publishing
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
31
    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..
32
    canonical.archivepublisher.deathrow
4330.4.30 by Jonathan Lange
Merge RF, resolving conflicts
33
    canonical.authserver.database
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
34
    canonical.launchpad.vocabularies.dbobjects
5485.1.12 by Edwin Grubbs
Fixed some problems with PersonValidatorBase.
35
    canonical.launchpad.validators.person
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
36
    canonical.librarian.client
3691.25.1 by James Henstridge
move bzrsync code to canonical.launchpad.scripts
37
    importd.Job
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
38
    """)
39
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
40
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
41
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.
42
    canonical.launchpad.scripts.ftpmaster
3691.164.16 by Guilherme Salgado
Lots of fixes and tests suggested by Bjorn
43
    canonical.launchpad.scripts.gina.handlers
5121.2.7 by Stuart Bishop
More required code changes
44
    canonical.launchpad.browser.distroseries
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
45
    canonical.launchpad.scripts.builddmaster
2570.1.7 by Carlos Perello Marin
Lots of fixes + new code + tests updates
46
    canonical.launchpad.scripts.po_import
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
47
    canonical.launchpad.systemhomes
48
    canonical.rosetta
49
    """)
50
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
51
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
52
def database_import_allowed_into(module_path):
53
    """Return True if database code is allowed to be imported into the given
54
    module path.  Otherwise, returns False.
55
56
    It is allowed if:
57
        - The import was made with the __import__ hook.
58
        - The importer is from within canonical.launchpad.database.
59
        - The importer is a 'test' module.
60
        - The importer is in the set of permitted_database_imports.
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
68
        is_test_module(module_path)):
69
        return True
70
    return module_path in permitted_database_imports
71
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
72
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
73
def is_test_module(module_path):
74
    """Returns True if the module is for unit or functional tests.
75
76
    Otherwise returns False.
77
    """
78
    name_splitted = module_path.split('.')
5554.4.10 by James Henstridge
* Remove some unnecessary imports from test_pages in page tests.
79
    return ('tests' in name_splitted or
80
            'ftests' in name_splitted or
81
            'testing' in name_splitted)
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
82
83
84
class attrsgetter:
85
    """Like operator.attrgetter, but works on multiple attribute names."""
86
87
    def __init__(self, *names):
88
        self.names = names
89
90
    def __call__(self, obj):
91
        return tuple(getattr(obj, name) for name in self.names)
92
93
94
class JackbootError(ImportError):
95
    """Import Fascist says you can't make this import."""
96
97
    def __init__(self, import_into, name, *args):
98
        ImportError.__init__(self, import_into, name, *args)
99
        self.import_into = import_into
100
        self.name = name
101
102
    def format_message(self):
103
        return 'Generic JackbootError: %s imported into %s' % (
104
            self.name, self.import_into)
105
106
    def __str__(self):
107
        return self.format_message()
108
109
110
class DatabaseImportPolicyViolation(JackbootError):
111
    """Database code is imported directly into other code."""
112
113
    def format_message(self):
114
        return 'You should not import %s into %s' % (
115
            self.name, self.import_into)
116
117
118
class FromStarPolicyViolation(JackbootError):
119
    """import * from a module that has no __all__."""
120
121
    def format_message(self):
122
        return ('You should not import * from %s because it has no __all__'
123
                ' (in %s)' % (self.name, self.import_into))
124
125
126
class NotInModuleAllPolicyViolation(JackbootError):
127
    """import of a name that does not appear in a module's __all__."""
128
129
    def __init__(self, import_into, name, attrname):
130
        JackbootError.__init__(self, import_into, name, attrname)
131
        self.attrname = attrname
132
133
    def format_message(self):
134
        return ('You should not import %s into %s from %s,'
135
                ' because it is not in its __all__.' %
136
                (self.attrname, self.import_into, self.name))
137
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
138
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.
139
class NotFoundPolicyViolation(JackbootError):
140
    """import of zope.exceptions.NotFoundError into
141
    canonical.launchpad.database.
142
    """
143
144
    def __init__(self, import_into):
145
        JackbootError.__init__(self, import_into, '')
146
147
    def format_message(self):
148
        return ('%s\nDo not import zope.exceptions.NotFoundError.\n'
149
                'Use canonical.launchpad.interfaces.NotFoundError instead.'
150
                % self.import_into)
151
152
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
153
def import_fascist(name, globals={}, locals={}, fromlist=[]):
6667.1.2 by Barry Warsaw
Lint cleanups
154
    global naughty_imports
155
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
156
    try:
157
        module = original_import(name, globals, locals, fromlist)
158
    except:
159
        #if 'layers' in name:
160
        #    import pdb; pdb.set_trace()
161
        raise
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
162
    # Python's re module imports some odd stuff every time certain regexes
163
    # 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.
164
    # Also, 'dedent' is not in textwrap.__all__.
165
    if name == 'sre' or name == 'textwrap':
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
166
        return module
167
6667.1.2 by Barry Warsaw
Lint cleanups
168
    # Mailman 2.1 code base is originally circa 1998, so yeah, no __all__'s.
169
    if name.startswith('Mailman'):
170
        return module
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
171
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
172
    # Some uses of __import__ pass None for globals, so handle that.
173
    import_into = None
174
    if globals is not None:
4108.5.2 by James Henstridge
Add zstorm initialisation files, hooking it into the test suite
175
        import_into = globals.get('__name__')
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
176
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
177
    if import_into is None:
178
        # We're being imported from the __import__ builtin.
179
        # We could find out by jumping up the stack a frame.
180
        # Let's not for now.
181
        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.
182
    if (import_into.startswith('canonical.launchpad.database') and
183
        name == 'zope.exceptions'):
184
        if fromlist and 'NotFoundError' in fromlist:
185
            raise NotFoundPolicyViolation(import_into)
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
186
    if (name.startswith(database_root) and
187
        not database_import_allowed_into(import_into)):
188
        error = DatabaseImportPolicyViolation(import_into, name)
189
        naughty_imports.add(error)
190
        # Raise an error except in the case of browser.traversers.
191
        # This exception to raising an error is only temporary, until
192
        # browser.traversers is cleaned up.
193
        if import_into not in warned_database_imports:
194
            raise error
195
196
    if fromlist is not None and import_into.startswith('canonical'):
6667.1.2 by Barry Warsaw
Lint cleanups
197
        # 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
198
        # own code.
199
        if list(fromlist) == ['*'] and not hasattr(module, '__all__'):
200
            # "from foo import *" is naughty if foo has no __all__
201
            error = FromStarPolicyViolation(import_into, name)
202
            naughty_imports.add(error)
203
            raise error
204
        elif (list(fromlist) != ['*'] and hasattr(module, '__all__') and
205
              not is_test_module(import_into)):
206
            # "from foo import bar" is naughty if bar isn't in foo.__all__ (and
207
            # 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
208
            # or ftests module or bar is itself a module.
209
            for attrname in fromlist:
3249.4.2 by Steve Alexander
Make importfascist not complain about imports of __doc__ when that name
210
                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
211
                    if not isinstance(
212
                        getattr(module, attrname, None), types.ModuleType):
213
                        error = NotInModuleAllPolicyViolation(
214
                            import_into, name, attrname)
215
                        naughty_imports.add(error)
216
                        # Not raising on NotInModuleAllPolicyViolation yet.
217
                        #raise error
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
218
    return module
219
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
220
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
221
def report_naughty_imports():
222
    if naughty_imports:
223
        print
224
        print '** %d import policy violations **' % len(naughty_imports)
225
        current_type = None
226
227
        database_violations = []
228
        fromstar_violations = []
229
        notinall_violations = []
230
        sorting_map = {
231
            DatabaseImportPolicyViolation: database_violations,
232
            FromStarPolicyViolation: fromstar_violations,
233
            NotInModuleAllPolicyViolation: notinall_violations
234
            }
235
        for error in naughty_imports:
236
            sorting_map[error.__class__].append(error)
237
238
        if database_violations:
239
            print
240
            print "There were %s database import violations." % (
241
                len(database_violations))
242
            sorted_violations = sorted(
243
                database_violations,
244
                key=attrsgetter('name', 'import_into'))
245
246
            for name, sequence in itertools.groupby(
247
                sorted_violations, attrgetter('name')):
248
                print "You should not import %s into:" % name
249
                for import_into, unused_duplicates_seq in itertools.groupby(
250
                    sequence, attrgetter('import_into')):
251
                    # Show first occurrence only, to avoid duplicates.
252
                    print "   ", import_into
253
254
        if fromstar_violations:
255
            print
256
            print "There were %s imports 'from *' without an __all__." % (
257
                len(fromstar_violations))
258
            sorted_violations = sorted(
259
                fromstar_violations,
260
                key=attrsgetter('import_into', 'name'))
261
262
            for import_into, sequence in itertools.groupby(
263
                sorted_violations, attrgetter('import_into')):
264
                print "You should not import * into %s from" % import_into
265
                for error in sequence:
266
                    print "   ", error.name
267
268
        if notinall_violations:
269
            print
270
            print (
271
                "There were %s imports of names not appearing in the __all__."
272
                % len(notinall_violations))
273
            sorted_violations = sorted(
274
                notinall_violations,
275
                key=attrsgetter('name', 'attrname', 'import_into'))
276
277
            for (name, attrname), sequence in itertools.groupby(
278
                sorted_violations, attrsgetter('name', 'attrname')):
279
                print "You should not import %s from %s:" % (attrname, name)
280
                import_intos = sorted(
281
                    set([error.import_into for error in sequence]))
282
                for import_into in import_intos:
283
                    print "   ", import_into
284
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
285
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
286
def install_import_fascist():
287
    __builtin__.__import__ = import_fascist
288
    atexit.register(report_naughty_imports)