~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=[]):
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:
155
        module = original_import(name, globals, locals, fromlist)
156
    except:
157
        #if 'layers' in name:
158
        #    import pdb; pdb.set_trace()
159
        raise
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
160
    # Python's re module imports some odd stuff every time certain regexes
161
    # 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.
162
    # Also, 'dedent' is not in textwrap.__all__.
163
    if name == 'sre' or name == 'textwrap':
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
164
        return module
165
166
    global naughty_imports
167
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
168
    # Some uses of __import__ pass None for globals, so handle that.
169
    import_into = None
170
    if globals is not None:
4108.5.2 by James Henstridge
Add zstorm initialisation files, hooking it into the test suite
171
        import_into = globals.get('__name__')
4331.8.1 by James Henstridge
start of port to python-openid-2.0 library
172
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
173
    if import_into is None:
174
        # We're being imported from the __import__ builtin.
175
        # We could find out by jumping up the stack a frame.
176
        # Let's not for now.
177
        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.
178
    if (import_into.startswith('canonical.launchpad.database') and
179
        name == 'zope.exceptions'):
180
        if fromlist and 'NotFoundError' in fromlist:
181
            raise NotFoundPolicyViolation(import_into)
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
182
    if (name.startswith(database_root) and
183
        not database_import_allowed_into(import_into)):
184
        error = DatabaseImportPolicyViolation(import_into, name)
185
        naughty_imports.add(error)
186
        # Raise an error except in the case of browser.traversers.
187
        # This exception to raising an error is only temporary, until
188
        # browser.traversers is cleaned up.
189
        if import_into not in warned_database_imports:
190
            raise error
191
192
    if fromlist is not None and import_into.startswith('canonical'):
193
        # We only want to warn about "from foo import bar" violations in our 
194
        # own code.
195
        if list(fromlist) == ['*'] and not hasattr(module, '__all__'):
196
            # "from foo import *" is naughty if foo has no __all__
197
            error = FromStarPolicyViolation(import_into, name)
198
            naughty_imports.add(error)
199
            raise error
200
        elif (list(fromlist) != ['*'] and hasattr(module, '__all__') and
201
              not is_test_module(import_into)):
202
            # "from foo import bar" is naughty if bar isn't in foo.__all__ (and
203
            # 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
204
            # or ftests module or bar is itself a module.
205
            for attrname in fromlist:
3249.4.2 by Steve Alexander
Make importfascist not complain about imports of __doc__ when that name
206
                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
207
                    if not isinstance(
208
                        getattr(module, attrname, None), types.ModuleType):
209
                        error = NotInModuleAllPolicyViolation(
210
                            import_into, name, attrname)
211
                        naughty_imports.add(error)
212
                        # Not raising on NotInModuleAllPolicyViolation yet.
213
                        #raise error
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
214
    return module
215
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
216
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
217
def report_naughty_imports():
218
    if naughty_imports:
219
        print
220
        print '** %d import policy violations **' % len(naughty_imports)
221
        current_type = None
222
223
        database_violations = []
224
        fromstar_violations = []
225
        notinall_violations = []
226
        sorting_map = {
227
            DatabaseImportPolicyViolation: database_violations,
228
            FromStarPolicyViolation: fromstar_violations,
229
            NotInModuleAllPolicyViolation: notinall_violations
230
            }
231
        for error in naughty_imports:
232
            sorting_map[error.__class__].append(error)
233
234
        if database_violations:
235
            print
236
            print "There were %s database import violations." % (
237
                len(database_violations))
238
            sorted_violations = sorted(
239
                database_violations,
240
                key=attrsgetter('name', 'import_into'))
241
242
            for name, sequence in itertools.groupby(
243
                sorted_violations, attrgetter('name')):
244
                print "You should not import %s into:" % name
245
                for import_into, unused_duplicates_seq in itertools.groupby(
246
                    sequence, attrgetter('import_into')):
247
                    # Show first occurrence only, to avoid duplicates.
248
                    print "   ", import_into
249
250
        if fromstar_violations:
251
            print
252
            print "There were %s imports 'from *' without an __all__." % (
253
                len(fromstar_violations))
254
            sorted_violations = sorted(
255
                fromstar_violations,
256
                key=attrsgetter('import_into', 'name'))
257
258
            for import_into, sequence in itertools.groupby(
259
                sorted_violations, attrgetter('import_into')):
260
                print "You should not import * into %s from" % import_into
261
                for error in sequence:
262
                    print "   ", error.name
263
264
        if notinall_violations:
265
            print
266
            print (
267
                "There were %s imports of names not appearing in the __all__."
268
                % len(notinall_violations))
269
            sorted_violations = sorted(
270
                notinall_violations,
271
                key=attrsgetter('name', 'attrname', 'import_into'))
272
273
            for (name, attrname), sequence in itertools.groupby(
274
                sorted_violations, attrsgetter('name', 'attrname')):
275
                print "You should not import %s from %s:" % (attrname, name)
276
                import_intos = sorted(
277
                    set([error.import_into for error in sequence]))
278
                for import_into in import_intos:
279
                    print "   ", import_into
280
3560.1.2 by Steve Alexander
fix bug 39393, reenabling the importfascist, and cleaning a couple of import-related thing up on the way.
281
2097 by Canonical.com Patch Queue Manager
[trivial] more import fascist improvements
282
def install_import_fascist():
283
    __builtin__.__import__ = import_fascist
284
    atexit.register(report_naughty_imports)