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