~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: Martin Pool
  • Date: 2005-07-22 22:37:53 UTC
  • Revision ID: mbp@sourcefrog.net-20050722223753-7dced4e32d3ce21d
- add the start of a test for inventory file-id matching

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