~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: Robert Collins
  • Date: 2005-10-18 13:11:57 UTC
  • mfrom: (1185.16.72) (0.2.1)
  • Revision ID: robertc@robertcollins.net-20051018131157-76a9970aa78e927e
Merged Martin.

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
 
""")