~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: Andrew Bennetts
  • Date: 2007-06-14 11:23:38 UTC
  • mto: (2506.2.7 container-format)
  • mto: This revision was merged to the branch mainline in revision 2572.
  • Revision ID: andrew.bennetts@canonical.com-20070614112338-6u3900u6nkag66u8
Return a callable instead of a str from read, and add more validation.

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