~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: John Arbash Meinel
  • Date: 2005-09-15 21:35:53 UTC
  • mfrom: (907.1.57)
  • mto: (1393.2.1)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: john@arbash-meinel.com-20050915213552-a6c83a5ef1e20897
(broken) Transport work is merged in. Tests do not pass yet.

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