~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

merge merge tweaks from aaron, which includes latest .dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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
 
Most commonly, the 'lazy_import' function is used to import other modules
22
 
in an on-demand fashion. Typically use looks like::
23
 
 
24
 
    from bzrlib.lazy_import import lazy_import
25
 
    lazy_import(globals(), '''
26
 
    from bzrlib import (
27
 
        errors,
28
 
        osutils,
29
 
        branch,
30
 
        )
31
 
    import bzrlib.branch
32
 
    ''')
33
 
 
34
 
Then 'errors, osutils, branch' and 'bzrlib' will exist as lazy-loaded
35
 
objects which will be replaced with a real object on first use.
36
 
 
37
 
In general, it is best to only load modules in this way. This is because
38
 
it isn't safe to pass these variables to other functions before they
39
 
have been replaced. This is especially true for constants, sometimes
40
 
true for classes or functions (when used as a factory, or you want
41
 
to inherit from them).
42
 
"""
43
 
 
44
 
from __future__ import absolute_import
45
 
 
46
 
 
47
 
class ScopeReplacer(object):
48
 
    """A lazy object that will replace itself in the appropriate scope.
49
 
 
50
 
    This object sits, ready to create the real object the first time it is
51
 
    needed.
52
 
    """
53
 
 
54
 
    __slots__ = ('_scope', '_factory', '_name', '_real_obj')
55
 
 
56
 
    # Setting this to True will allow you to do x = y, and still access members
57
 
    # from both variables. This should not normally be enabled, but is useful
58
 
    # when building documentation.
59
 
    _should_proxy = False
60
 
 
61
 
    def __init__(self, scope, factory, name):
62
 
        """Create a temporary object in the specified scope.
63
 
        Once used, a real object will be placed in the scope.
64
 
 
65
 
        :param scope: The scope the object should appear in
66
 
        :param factory: A callable that will create the real object.
67
 
            It will be passed (self, scope, name)
68
 
        :param name: The variable name in the given scope.
69
 
        """
70
 
        object.__setattr__(self, '_scope', scope)
71
 
        object.__setattr__(self, '_factory', factory)
72
 
        object.__setattr__(self, '_name', name)
73
 
        object.__setattr__(self, '_real_obj', None)
74
 
        scope[name] = self
75
 
 
76
 
    def _replace(self):
77
 
        """Actually replace self with other in the given scope"""
78
 
        name = object.__getattribute__(self, '_name')
79
 
        try:
80
 
            factory = object.__getattribute__(self, '_factory')
81
 
            scope = object.__getattribute__(self, '_scope')
82
 
        except AttributeError, e:
83
 
            # Because ScopeReplacer objects only replace a single
84
 
            # item, passing them to another variable before they are
85
 
            # replaced would cause them to keep getting replaced
86
 
            # (only they are replacing the wrong variable). So we
87
 
            # make it forbidden, and try to give a good error.
88
 
            raise errors.IllegalUseOfScopeReplacer(
89
 
                name, msg="Object already cleaned up, did you assign it"
90
 
                          " to another variable?",
91
 
                extra=e)
92
 
        obj = factory(self, scope, name)
93
 
        if obj is self:
94
 
            raise errors.IllegalUseOfScopeReplacer(name, msg="Object tried"
95
 
                " to replace itself, check it's not using its own scope.")
96
 
        if ScopeReplacer._should_proxy:
97
 
            object.__setattr__(self, '_real_obj', obj)
98
 
        scope[name] = obj
99
 
        return obj
100
 
 
101
 
    def _cleanup(self):
102
 
        """Stop holding on to all the extra stuff"""
103
 
        try:
104
 
            del self._factory
105
 
        except AttributeError:
106
 
            # Oops, we just lost a race with another caller of _cleanup.  Just
107
 
            # ignore it.
108
 
            pass
109
 
 
110
 
        try:
111
 
            del self._scope
112
 
        except AttributeError:
113
 
            # Another race loss.  See above.
114
 
            pass
115
 
 
116
 
        # We keep _name, so that we can report errors
117
 
        # del self._name
118
 
 
119
 
    def __getattribute__(self, attr):
120
 
        obj = object.__getattribute__(self, '_real_obj')
121
 
        if obj is None:
122
 
            _replace = object.__getattribute__(self, '_replace')
123
 
            obj = _replace()
124
 
            _cleanup = object.__getattribute__(self, '_cleanup')
125
 
            _cleanup()
126
 
        return getattr(obj, attr)
127
 
 
128
 
    def __setattr__(self, attr, value):
129
 
        obj = object.__getattribute__(self, '_real_obj')
130
 
        if obj is None:
131
 
            _replace = object.__getattribute__(self, '_replace')
132
 
            obj = _replace()
133
 
            _cleanup = object.__getattribute__(self, '_cleanup')
134
 
            _cleanup()
135
 
        return setattr(obj, attr, value)
136
 
 
137
 
    def __call__(self, *args, **kwargs):
138
 
        _replace = object.__getattribute__(self, '_replace')
139
 
        obj = _replace()
140
 
        _cleanup = object.__getattribute__(self, '_cleanup')
141
 
        _cleanup()
142
 
        return obj(*args, **kwargs)
143
 
 
144
 
 
145
 
class ImportReplacer(ScopeReplacer):
146
 
    """This is designed to replace only a portion of an import list.
147
 
 
148
 
    It will replace itself with a module, and then make children
149
 
    entries also ImportReplacer objects.
150
 
 
151
 
    At present, this only supports 'import foo.bar.baz' syntax.
152
 
    """
153
 
 
154
 
    # '_import_replacer_children' is intentionally a long semi-unique name
155
 
    # that won't likely exist elsewhere. This allows us to detect an
156
 
    # ImportReplacer object by using
157
 
    #       object.__getattribute__(obj, '_import_replacer_children')
158
 
    # We can't just use 'isinstance(obj, ImportReplacer)', because that
159
 
    # accesses .__class__, which goes through __getattribute__, and triggers
160
 
    # the replacement.
161
 
    __slots__ = ('_import_replacer_children', '_member', '_module_path')
162
 
 
163
 
    def __init__(self, scope, name, module_path, member=None, children={}):
164
 
        """Upon request import 'module_path' as the name 'module_name'.
165
 
        When imported, prepare children to also be imported.
166
 
 
167
 
        :param scope: The scope that objects should be imported into.
168
 
            Typically this is globals()
169
 
        :param name: The variable name. Often this is the same as the
170
 
            module_path. 'bzrlib'
171
 
        :param module_path: A list for the fully specified module path
172
 
            ['bzrlib', 'foo', 'bar']
173
 
        :param member: The member inside the module to import, often this is
174
 
            None, indicating the module is being imported.
175
 
        :param children: Children entries to be imported later.
176
 
            This should be a map of children specifications.
177
 
            ::
178
 
            
179
 
                {'foo':(['bzrlib', 'foo'], None,
180
 
                    {'bar':(['bzrlib', 'foo', 'bar'], None {})})
181
 
                }
182
 
 
183
 
        Examples::
184
 
 
185
 
            import foo => name='foo' module_path='foo',
186
 
                          member=None, children={}
187
 
            import foo.bar => name='foo' module_path='foo', member=None,
188
 
                              children={'bar':(['foo', 'bar'], None, {}}
189
 
            from foo import bar => name='bar' module_path='foo', member='bar'
190
 
                                   children={}
191
 
            from foo import bar, baz would get translated into 2 import
192
 
            requests. On for 'name=bar' and one for 'name=baz'
193
 
        """
194
 
        if (member is not None) and children:
195
 
            raise ValueError('Cannot supply both a member and children')
196
 
 
197
 
        object.__setattr__(self, '_import_replacer_children', children)
198
 
        object.__setattr__(self, '_member', member)
199
 
        object.__setattr__(self, '_module_path', module_path)
200
 
 
201
 
        # Indirecting through __class__ so that children can
202
 
        # override _import (especially our instrumented version)
203
 
        cls = object.__getattribute__(self, '__class__')
204
 
        ScopeReplacer.__init__(self, scope=scope, name=name,
205
 
                               factory=cls._import)
206
 
 
207
 
    def _import(self, scope, name):
208
 
        children = object.__getattribute__(self, '_import_replacer_children')
209
 
        member = object.__getattribute__(self, '_member')
210
 
        module_path = object.__getattribute__(self, '_module_path')
211
 
        module_python_path = '.'.join(module_path)
212
 
        if member is not None:
213
 
            module = __import__(module_python_path, scope, scope, [member], level=0)
214
 
            return getattr(module, member)
215
 
        else:
216
 
            module = __import__(module_python_path, scope, scope, [], level=0)
217
 
            for path in module_path[1:]:
218
 
                module = getattr(module, path)
219
 
 
220
 
        # Prepare the children to be imported
221
 
        for child_name, (child_path, child_member, grandchildren) in \
222
 
                children.iteritems():
223
 
            # Using self.__class__, so that children get children classes
224
 
            # instantiated. (This helps with instrumented tests)
225
 
            cls = object.__getattribute__(self, '__class__')
226
 
            cls(module.__dict__, name=child_name,
227
 
                module_path=child_path, member=child_member,
228
 
                children=grandchildren)
229
 
        return module
230
 
 
231
 
 
232
 
class ImportProcessor(object):
233
 
    """Convert text that users input into lazy import requests"""
234
 
 
235
 
    # TODO: jam 20060912 This class is probably not strict enough about
236
 
    #       what type of text it allows. For example, you can do:
237
 
    #       import (foo, bar), which is not allowed by python.
238
 
    #       For now, it should be supporting a superset of python import
239
 
    #       syntax which is all we really care about.
240
 
 
241
 
    __slots__ = ['imports', '_lazy_import_class']
242
 
 
243
 
    def __init__(self, lazy_import_class=None):
244
 
        self.imports = {}
245
 
        if lazy_import_class is None:
246
 
            self._lazy_import_class = ImportReplacer
247
 
        else:
248
 
            self._lazy_import_class = lazy_import_class
249
 
 
250
 
    def lazy_import(self, scope, text):
251
 
        """Convert the given text into a bunch of lazy import objects.
252
 
 
253
 
        This takes a text string, which should be similar to normal python
254
 
        import markup.
255
 
        """
256
 
        self._build_map(text)
257
 
        self._convert_imports(scope)
258
 
 
259
 
    def _convert_imports(self, scope):
260
 
        # Now convert the map into a set of imports
261
 
        for name, info in self.imports.iteritems():
262
 
            self._lazy_import_class(scope, name=name, module_path=info[0],
263
 
                                    member=info[1], children=info[2])
264
 
 
265
 
    def _build_map(self, text):
266
 
        """Take a string describing imports, and build up the internal map"""
267
 
        for line in self._canonicalize_import_text(text):
268
 
            if line.startswith('import '):
269
 
                self._convert_import_str(line)
270
 
            elif line.startswith('from '):
271
 
                self._convert_from_str(line)
272
 
            else:
273
 
                raise errors.InvalidImportLine(line,
274
 
                    "doesn't start with 'import ' or 'from '")
275
 
 
276
 
    def _convert_import_str(self, import_str):
277
 
        """This converts a import string into an import map.
278
 
 
279
 
        This only understands 'import foo, foo.bar, foo.bar.baz as bing'
280
 
 
281
 
        :param import_str: The import string to process
282
 
        """
283
 
        if not import_str.startswith('import '):
284
 
            raise ValueError('bad import string %r' % (import_str,))
285
 
        import_str = import_str[len('import '):]
286
 
 
287
 
        for path in import_str.split(','):
288
 
            path = path.strip()
289
 
            if not path:
290
 
                continue
291
 
            as_hunks = path.split(' as ')
292
 
            if len(as_hunks) == 2:
293
 
                # We have 'as' so this is a different style of import
294
 
                # 'import foo.bar.baz as bing' creates a local variable
295
 
                # named 'bing' which points to 'foo.bar.baz'
296
 
                name = as_hunks[1].strip()
297
 
                module_path = as_hunks[0].strip().split('.')
298
 
                if name in self.imports:
299
 
                    raise errors.ImportNameCollision(name)
300
 
                # No children available in 'import foo as bar'
301
 
                self.imports[name] = (module_path, None, {})
302
 
            else:
303
 
                # Now we need to handle
304
 
                module_path = path.split('.')
305
 
                name = module_path[0]
306
 
                if name not in self.imports:
307
 
                    # This is a new import that we haven't seen before
308
 
                    module_def = ([name], None, {})
309
 
                    self.imports[name] = module_def
310
 
                else:
311
 
                    module_def = self.imports[name]
312
 
 
313
 
                cur_path = [name]
314
 
                cur = module_def[2]
315
 
                for child in module_path[1:]:
316
 
                    cur_path.append(child)
317
 
                    if child in cur:
318
 
                        cur = cur[child][2]
319
 
                    else:
320
 
                        next = (cur_path[:], None, {})
321
 
                        cur[child] = next
322
 
                        cur = next[2]
323
 
 
324
 
    def _convert_from_str(self, from_str):
325
 
        """This converts a 'from foo import bar' string into an import map.
326
 
 
327
 
        :param from_str: The import string to process
328
 
        """
329
 
        if not from_str.startswith('from '):
330
 
            raise ValueError('bad from/import %r' % from_str)
331
 
        from_str = from_str[len('from '):]
332
 
 
333
 
        from_module, import_list = from_str.split(' import ')
334
 
 
335
 
        from_module_path = from_module.split('.')
336
 
 
337
 
        for path in import_list.split(','):
338
 
            path = path.strip()
339
 
            if not path:
340
 
                continue
341
 
            as_hunks = path.split(' as ')
342
 
            if len(as_hunks) == 2:
343
 
                # We have 'as' so this is a different style of import
344
 
                # 'import foo.bar.baz as bing' creates a local variable
345
 
                # named 'bing' which points to 'foo.bar.baz'
346
 
                name = as_hunks[1].strip()
347
 
                module = as_hunks[0].strip()
348
 
            else:
349
 
                name = module = path
350
 
            if name in self.imports:
351
 
                raise errors.ImportNameCollision(name)
352
 
            self.imports[name] = (from_module_path, module, {})
353
 
 
354
 
    def _canonicalize_import_text(self, text):
355
 
        """Take a list of imports, and split it into regularized form.
356
 
 
357
 
        This is meant to take regular import text, and convert it to
358
 
        the forms that the rest of the converters prefer.
359
 
        """
360
 
        out = []
361
 
        cur = None
362
 
        continuing = False
363
 
 
364
 
        for line in text.split('\n'):
365
 
            line = line.strip()
366
 
            loc = line.find('#')
367
 
            if loc != -1:
368
 
                line = line[:loc].strip()
369
 
 
370
 
            if not line:
371
 
                continue
372
 
            if cur is not None:
373
 
                if line.endswith(')'):
374
 
                    out.append(cur + ' ' + line[:-1])
375
 
                    cur = None
376
 
                else:
377
 
                    cur += ' ' + line
378
 
            else:
379
 
                if '(' in line and ')' not in line:
380
 
                    cur = line.replace('(', '')
381
 
                else:
382
 
                    out.append(line.replace('(', '').replace(')', ''))
383
 
        if cur is not None:
384
 
            raise errors.InvalidImportLine(cur, 'Unmatched parenthesis')
385
 
        return out
386
 
 
387
 
 
388
 
def lazy_import(scope, text, lazy_import_class=None):
389
 
    """Create lazy imports for all of the imports in text.
390
 
 
391
 
    This is typically used as something like::
392
 
 
393
 
        from bzrlib.lazy_import import lazy_import
394
 
        lazy_import(globals(), '''
395
 
        from bzrlib import (
396
 
            foo,
397
 
            bar,
398
 
            baz,
399
 
            )
400
 
        import bzrlib.branch
401
 
        import bzrlib.transport
402
 
        ''')
403
 
 
404
 
    Then 'foo, bar, baz' and 'bzrlib' will exist as lazy-loaded
405
 
    objects which will be replaced with a real object on first use.
406
 
 
407
 
    In general, it is best to only load modules in this way. This is
408
 
    because other objects (functions/classes/variables) are frequently
409
 
    used without accessing a member, which means we cannot tell they
410
 
    have been used.
411
 
    """
412
 
    # This is just a helper around ImportProcessor.lazy_import
413
 
    proc = ImportProcessor(lazy_import_class=lazy_import_class)
414
 
    return proc.lazy_import(scope, text)
415
 
 
416
 
 
417
 
# The only module that this module depends on is 'bzrlib.errors'. But it
418
 
# can actually be imported lazily, since we only need it if there is a
419
 
# problem.
420
 
 
421
 
lazy_import(globals(), """
422
 
from bzrlib import errors
423
 
""")