~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: Martin Pool
  • Date: 2005-09-15 08:37:41 UTC
  • Revision ID: mbp@sourcefrog.net-20050915083741-70d7550b97c7b580
- some updates for fetch/update function

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