~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: Andrew Bennetts
  • Date: 2007-03-26 07:11:40 UTC
  • mto: This revision was merged to the branch mainline in revision 2376.
  • Revision ID: andrew.bennetts@canonical.com-20070326071140-s1ldac8zkl1b417h
Fix test_snapshot_new_revision to use branch.repository.get_transaction instead of branch.get_transaction.

Show diffs side-by-side

added added

removed removed

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