/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: John Arbash Meinel
  • Date: 2006-09-12 20:46:42 UTC
  • mto: This revision was merged to the branch mainline in revision 2071.
  • Revision ID: john@arbash-meinel.com-20060912204642-91599869cc863f07
Cleanup, deprecated, and get the tests passing again.

bzrlib.builtins.merge is heavily used by the test suite, though it is
really the wrong place for a function like that.
lazy imports work badly with doctests, but so far none of the doc tested
modules had anything worth testing in that fashion.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 by Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Functionality to create lazy evaluation objects.
 
18
 
 
19
This includes waiting to import a module until it is actually used.
 
20
"""
 
21
 
 
22
import re
 
23
import sys
 
24
 
 
25
from bzrlib import (
 
26
    errors,
 
27
    )
 
28
 
 
29
 
 
30
class ScopeReplacer(object):
 
31
    """A lazy object that will replace itself in the appropriate scope.
 
32
 
 
33
    This object sits, ready to create the real object the first time it is
 
34
    needed.
 
35
    """
 
36
 
 
37
    __slots__ = ('_scope', '_factory', '_name')
 
38
 
 
39
    def __init__(self, scope, factory, name):
 
40
        """Create a temporary object in the specified scope.
 
41
        Once used, a real object will be placed in the scope.
 
42
 
 
43
        :param scope: The scope the object should appear in
 
44
        :param factory: A callable that will create the real object.
 
45
            It will be passed (self, scope, name)
 
46
        :param name: The variable name in the given scope.
 
47
        """
 
48
        self._scope = scope
 
49
        self._factory = factory
 
50
        self._name = name
 
51
        scope[name] = self
 
52
 
 
53
    def _replace(self):
 
54
        """Actually replace self with other in the given scope"""
 
55
        name = object.__getattribute__(self, '_name')
 
56
        try:
 
57
            factory = object.__getattribute__(self, '_factory')
 
58
            scope = object.__getattribute__(self, '_scope')
 
59
        except AttributeError, e:
 
60
            # Because ScopeReplacer objects only replace a single
 
61
            # item, passing them to another variable before they are
 
62
            # replaced would cause them to keep getting replaced
 
63
            # (only they are replacing the wrong variable). So we
 
64
            # make it forbidden, and try to give a good error.
 
65
            raise errors.IllegalUseOfScopeReplacer(
 
66
                name, msg="Object already cleaned up, did you assign it"
 
67
                          "to another variable?",
 
68
                extra=e)
 
69
        obj = factory(self, scope, name)
 
70
        scope[name] = obj
 
71
        return obj
 
72
 
 
73
    def _cleanup(self):
 
74
        """Stop holding on to all the extra stuff"""
 
75
        del self._factory
 
76
        del self._scope
 
77
        # We keep _name, so that we can report errors
 
78
        # del self._name
 
79
 
 
80
    def __getattribute__(self, attr):
 
81
        _replace = object.__getattribute__(self, '_replace')
 
82
        obj = _replace()
 
83
        _cleanup = object.__getattribute__(self, '_cleanup')
 
84
        _cleanup()
 
85
        return getattr(obj, attr)
 
86
 
 
87
    def __call__(self, *args, **kwargs):
 
88
        _replace = object.__getattribute__(self, '_replace')
 
89
        obj = _replace()
 
90
        _cleanup = object.__getattribute__(self, '_cleanup')
 
91
        _cleanup()
 
92
        return obj(*args, **kwargs)
 
93
 
 
94
 
 
95
class ImportReplacer(ScopeReplacer):
 
96
    """This is designed to replace only a portion of an import list.
 
97
 
 
98
    It will replace itself with a module, and then make children
 
99
    entries also ImportReplacer objects.
 
100
 
 
101
    At present, this only supports 'import foo.bar.baz' syntax.
 
102
    """
 
103
 
 
104
    # Intentially a long semi-unique name that won't likely exist
 
105
    # elsewhere. (We can't use isinstance because that accesses __class__
 
106
    # which causes the __getattribute__ to trigger)
 
107
    __slots__ = ('_import_replacer_children', '_member', '_module_path')
 
108
 
 
109
    def __init__(self, scope, name, module_path, member=None, children={}):
 
110
        """Upon request import 'module_path' as the name 'module_name'.
 
111
        When imported, prepare children to also be imported.
 
112
 
 
113
        :param scope: The scope that objects should be imported into.
 
114
            Typically this is globals()
 
115
        :param name: The variable name. Often this is the same as the 
 
116
            module_path. 'bzrlib'
 
117
        :param module_path: A list for the fully specified module path
 
118
            ['bzrlib', 'foo', 'bar']
 
119
        :param member: The member inside the module to import, often this is
 
120
            None, indicating the module is being imported.
 
121
        :param children: Children entries to be imported later.
 
122
            This should be a map of children specifications.
 
123
            {'foo':(['bzrlib', 'foo'], None, 
 
124
                {'bar':(['bzrlib', 'foo', 'bar'], None {})})
 
125
            }
 
126
        Examples:
 
127
            import foo => name='foo' module_path='foo',
 
128
                          member=None, children={}
 
129
            import foo.bar => name='foo' module_path='foo', member=None,
 
130
                              children={'bar':(['foo', 'bar'], None, {}}
 
131
            from foo import bar => name='bar' module_path='foo', member='bar'
 
132
                                   children={}
 
133
            from foo import bar, baz would get translated into 2 import
 
134
            requests. On for 'name=bar' and one for 'name=baz'
 
135
        """
 
136
        if member is not None:
 
137
            assert not children, \
 
138
                'Cannot supply both a member and children'
 
139
 
 
140
        self._import_replacer_children = children
 
141
        self._member = member
 
142
        self._module_path = module_path
 
143
 
 
144
        # Indirecting through __class__ so that children can
 
145
        # override _import (especially our instrumented version)
 
146
        cls = object.__getattribute__(self, '__class__')
 
147
        ScopeReplacer.__init__(self, scope=scope, name=name,
 
148
                               factory=cls._import)
 
149
 
 
150
    def _import(self, scope, name):
 
151
        children = object.__getattribute__(self, '_import_replacer_children')
 
152
        member = object.__getattribute__(self, '_member')
 
153
        module_path = object.__getattribute__(self, '_module_path')
 
154
        module_python_path = '.'.join(module_path)
 
155
        if member is not None:
 
156
            module = __import__(module_python_path, scope, scope, [member])
 
157
            return getattr(module, member)
 
158
        else:
 
159
            module = __import__(module_python_path, scope, scope, [])
 
160
            for path in module_path[1:]:
 
161
                module = getattr(module, path)
 
162
 
 
163
        # Prepare the children to be imported
 
164
        for child_name, (child_path, child_member, grandchildren) in \
 
165
                children.iteritems():
 
166
            # Using self.__class__, so that children get children classes
 
167
            # instantiated. (This helps with instrumented tests)
 
168
            cls = object.__getattribute__(self, '__class__')
 
169
            cls(module.__dict__, name=child_name,
 
170
                module_path=child_path, member=child_member,
 
171
                children=grandchildren)
 
172
        return module
 
173
 
 
174
 
 
175
class ImportProcessor(object):
 
176
    """Convert text that users input into lazy import requests"""
 
177
 
 
178
    # TODO: jam 20060912 This class is probably not strict enough about
 
179
    #       what type of text it allows. For example, you can do:
 
180
    #       import (foo, bar), which is not allowed by python.
 
181
    #       For now, it should be supporting a superset of python import
 
182
    #       syntax which is all we really care about.
 
183
 
 
184
    __slots__ = ['imports', '_lazy_import_class']
 
185
 
 
186
    def __init__(self, lazy_import_class=None):
 
187
        self.imports = {}
 
188
        if lazy_import_class is None:
 
189
            self._lazy_import_class = ImportReplacer
 
190
        else:
 
191
            self._lazy_import_class = lazy_import_class
 
192
 
 
193
    def lazy_import(self, scope, text):
 
194
        """Convert the given text into a bunch of lazy import objects.
 
195
 
 
196
        This takes a text string, which should be similar to normal python
 
197
        import markup.
 
198
        """
 
199
        self._build_map(text)
 
200
        self._convert_imports(scope)
 
201
 
 
202
    def _convert_imports(self, scope):
 
203
        # Now convert the map into a set of imports
 
204
        for name, info in self.imports.iteritems():
 
205
            self._lazy_import_class(scope, name=name, module_path=info[0],
 
206
                                    member=info[1], children=info[2])
 
207
 
 
208
    def _build_map(self, text):
 
209
        """Take a string describing imports, and build up the internal map"""
 
210
        for line in self._canonicalize_import_text(text):
 
211
            if line.startswith('import '):
 
212
                self._convert_import_str(line)
 
213
            elif line.startswith('from '):
 
214
                self._convert_from_str(line)
 
215
            else:
 
216
                raise errors.InvalidImportLine(line,
 
217
                    "doesn't start with 'import ' or 'from '")
 
218
 
 
219
    def _convert_import_str(self, import_str):
 
220
        """This converts a import string into an import map.
 
221
 
 
222
        This only understands 'import foo, foo.bar, foo.bar.baz as bing'
 
223
 
 
224
        :param import_str: The import string to process
 
225
        """
 
226
        assert import_str.startswith('import ')
 
227
        import_str = import_str[len('import '):]
 
228
 
 
229
        for path in import_str.split(','):
 
230
            path = path.strip()
 
231
            if not path:
 
232
                continue
 
233
            as_hunks = path.split(' as ')
 
234
            if len(as_hunks) == 2:
 
235
                # We have 'as' so this is a different style of import
 
236
                # 'import foo.bar.baz as bing' creates a local variable
 
237
                # named 'bing' which points to 'foo.bar.baz'
 
238
                name = as_hunks[1].strip()
 
239
                module_path = as_hunks[0].strip().split('.')
 
240
                if name in self.imports:
 
241
                    raise errors.ImportNameCollision(name)
 
242
                # No children available in 'import foo as bar'
 
243
                self.imports[name] = (module_path, None, {})
 
244
            else:
 
245
                # Now we need to handle
 
246
                module_path = path.split('.')
 
247
                name = module_path[0]
 
248
                if name not in self.imports:
 
249
                    # This is a new import that we haven't seen before
 
250
                    module_def = ([name], None, {})
 
251
                    self.imports[name] = module_def
 
252
                else:
 
253
                    module_def = self.imports[name]
 
254
 
 
255
                cur_path = [name]
 
256
                cur = module_def[2]
 
257
                for child in module_path[1:]:
 
258
                    cur_path.append(child)
 
259
                    if child in cur:
 
260
                        cur = cur[child][2]
 
261
                    else:
 
262
                        next = (cur_path[:], None, {})
 
263
                        cur[child] = next
 
264
                        cur = next[2]
 
265
 
 
266
    def _convert_from_str(self, from_str):
 
267
        """This converts a 'from foo import bar' string into an import map.
 
268
 
 
269
        :param from_str: The import string to process
 
270
        """
 
271
        assert from_str.startswith('from ')
 
272
        from_str = from_str[len('from '):]
 
273
 
 
274
        from_module, import_list = from_str.split(' import ')
 
275
 
 
276
        from_module_path = from_module.split('.')
 
277
 
 
278
        for path in import_list.split(','):
 
279
            path = path.strip()
 
280
            if not path:
 
281
                continue
 
282
            as_hunks = path.split(' as ')
 
283
            if len(as_hunks) == 2:
 
284
                # We have 'as' so this is a different style of import
 
285
                # 'import foo.bar.baz as bing' creates a local variable
 
286
                # named 'bing' which points to 'foo.bar.baz'
 
287
                name = as_hunks[1].strip()
 
288
                module = as_hunks[0].strip()
 
289
            else:
 
290
                name = module = path
 
291
            if name in self.imports:
 
292
                raise errors.ImportNameCollision(name)
 
293
            self.imports[name] = (from_module_path, module, {})
 
294
 
 
295
    def _canonicalize_import_text(self, text):
 
296
        """Take a list of imports, and split it into regularized form.
 
297
 
 
298
        This is meant to take regular import text, and convert it to
 
299
        the forms that the rest of the converters prefer.
 
300
        """
 
301
        out = []
 
302
        cur = None
 
303
        continuing = False
 
304
 
 
305
        for line in text.split('\n'):
 
306
            line = line.strip()
 
307
            loc = line.find('#')
 
308
            if loc != -1:
 
309
                line = line[:loc].strip()
 
310
 
 
311
            if not line:
 
312
                continue
 
313
            if cur is not None:
 
314
                if line.endswith(')'):
 
315
                    out.append(cur + ' ' + line[:-1])
 
316
                    cur = None
 
317
                else:
 
318
                    cur += ' ' + line
 
319
            else:
 
320
                if '(' in line and ')' not in line:
 
321
                    cur = line.replace('(', '')
 
322
                else:
 
323
                    out.append(line.replace('(', '').replace(')', ''))
 
324
        if cur is not None:
 
325
            raise errors.InvalidImportLine(cur, 'Unmatched parenthesis')
 
326
        return out
 
327
 
 
328
 
 
329
def lazy_import(scope, text, lazy_import_class=None):
 
330
    """Create lazy imports for all of the imports in text.
 
331
 
 
332
    This is typically used as something like:
 
333
    from bzrlib.lazy_import import lazy_import
 
334
    lazy_import(globals(), '''
 
335
    from bzrlib import (
 
336
        foo,
 
337
        bar,
 
338
        baz,
 
339
        )
 
340
    import bzrlib.branch
 
341
    import bzrlib.transport
 
342
    ''')
 
343
 
 
344
    Then 'foo, bar, baz' and 'bzrlib' will exist as lazy-loaded
 
345
    objects which will be replaced with a real object on first use.
 
346
 
 
347
    In general, it is best to only load modules in this way. This is
 
348
    because other objects (functions/classes/variables) are frequently
 
349
    used without accessing a member, which means we cannot tell they
 
350
    have been used.
 
351
    """
 
352
    # This is just a helper around ImportProcessor.lazy_import
 
353
    proc = ImportProcessor(lazy_import_class=lazy_import_class)
 
354
    return proc.lazy_import(scope, text)