~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

Turn completion assertions into separate methods.

Many common assertions used to be expressed as arguments to the complete
method.  This makes the checks more explicit, and the code easier to read.

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