~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: John Arbash Meinel
  • Date: 2006-09-10 20:59:01 UTC
  • mto: This revision was merged to the branch mainline in revision 2004.
  • Revision ID: john@arbash-meinel.com-20060910205901-ceb5929c1497f81f
start working on some lazy importing code

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
 
 
22
import sys
 
23
 
 
24
 
 
25
class ScopeReplacer(object):
 
26
    """A lazy object that will replace itself in the appropriate scope.
 
27
 
 
28
    This object sits, ready to create the real object the first time it is
 
29
    needed.
 
30
    """
 
31
 
 
32
    __slots__ = ('_scope', '_factory', '_name')
 
33
 
 
34
    def __init__(self, scope, factory, name):
 
35
        """Create a temporary object in the specified scope.
 
36
        Once used, a real object will be placed in the scope.
 
37
 
 
38
        :param scope: The scope the object should appear in
 
39
        :param factory: A callable that will create the real object.
 
40
            It will be passed (self, scope, name)
 
41
        :param name: The variable name in the given scope.
 
42
        """
 
43
        self._scope = scope
 
44
        self._factory = factory
 
45
        self._name = name
 
46
        scope[name] = self
 
47
 
 
48
    def _replace(self):
 
49
        """Actually replace self with other in the given scope"""
 
50
        factory = object.__getattribute__(self, '_factory')
 
51
        scope = object.__getattribute__(self, '_scope')
 
52
        name = object.__getattribute__(self, '_name')
 
53
        obj = factory(self, scope, name)
 
54
        scope[name] = obj
 
55
        return obj
 
56
 
 
57
    def _cleanup(self):
 
58
        """Stop holding on to all the extra stuff"""
 
59
        del self._factory
 
60
        del self._scope
 
61
        del self._name
 
62
 
 
63
    def __getattribute__(self, attr):
 
64
        obj = object.__getattribute__(self, '_replace')()
 
65
        object.__getattribute__(self, '_cleanup')()
 
66
        return getattr(obj, attr)
 
67
 
 
68
    def __call__(self, *args, **kwargs):
 
69
        obj = object.__getattribute__(self, '_replace')()
 
70
        object.__getattribute__(self, '_cleanup')()
 
71
        return obj(*args, **kwargs)
 
72
 
 
73
 
 
74
class ImportReplacer(ScopeReplacer):
 
75
    """This is designed to replace only a portion of an import list.
 
76
 
 
77
    It will replace itself with a module, and then make children
 
78
    entries also ImportReplacer objects.
 
79
 
 
80
    At present, this only supports 'import foo.bar.baz' syntax.
 
81
    """
 
82
 
 
83
    # Intentially a long semi-unique name that won't likely exist
 
84
    # elsewhere. (We can't use isinstance because that accesses __class__
 
85
    # which causes the __getattribute__ to trigger)
 
86
    __slots__ = ('_import_replacer_children', '_member', '_module_path')
 
87
 
 
88
    def __init__(self, scope, name, module_path, member=None, children=[]):
 
89
        """Upon request import 'module_path' as the name 'module_name'.
 
90
        When imported, prepare children to also be imported.
 
91
 
 
92
        :param scope: The scope that objects should be imported into.
 
93
            Typically this is globals()
 
94
        :param name: The variable name. Often this is the same as the 
 
95
            module_path. 'bzrlib'
 
96
        :param module_path: The fully specified dotted path to the module.
 
97
            'bzrlib.foo.bar'
 
98
        :param member: The member inside the module to import, often this is
 
99
            None, indicating the module is being imported.
 
100
        :param children: Children entries to be imported later.
 
101
            This should be a list of children specifications.
 
102
            [('foo', 'bzrlib.foo', [('bar', 'bzrlib.foo.bar'),])]
 
103
        Examples:
 
104
            import foo => name='foo' module_path='foo',
 
105
                          member=None, children=[]
 
106
            import foo.bar => name='foo' module_path='foo', member=None,
 
107
                              children=[('bar', 'foo.bar', [])]
 
108
            from foo import bar => name='bar' module_path='foo', member='bar'
 
109
                                   children=[]
 
110
            from foo import bar, baz would get translated into 2 import
 
111
            requests. On for 'name=bar' and one for 'name=baz'
 
112
        """
 
113
        if member is not None:
 
114
            assert not children, \
 
115
                'Cannot supply both a member and children'
 
116
 
 
117
        self._import_replacer_children = children
 
118
        self._member = member
 
119
        self._module_path = module_path
 
120
        ScopeReplacer.__init__(self, scope=scope, name=name,
 
121
                               factory=ImportReplacer._import)
 
122
 
 
123
    def _import(self, scope, name):
 
124
        children = object.__getattribute__(self, '_import_replacer_children')
 
125
        member = object.__getattribute__(self, '_member')
 
126
        module_path = object.__getattribute__(self, '_module_path')
 
127
        if member is not None:
 
128
            module = __import__(module_path, scope, scope, (member,))
 
129
            return getattr(module, member)
 
130
        else:
 
131
            module = __import__(module_path, scope, scope, None)
 
132
 
 
133
        # Prepare the children to be imported
 
134
        for child_name, child_path, grandchildren in children:
 
135
            ImportReplacer(module.__dict__, name=child_name,
 
136
                           module_path=child_path, member=None,
 
137
                           children=grandchildren)
 
138
 
 
139
    def _replace(self):
 
140
        """Actually replace self with other in the given scope"""
 
141
        factory = object.__getattribute__(self, '_factory')
 
142
        scope = object.__getattribute__(self, '_scope')
 
143
        name = object.__getattribute__(self, '_name')
 
144
        obj = factory()
 
145
        scope[name] = obj
 
146
        return obj
 
147
 
 
148
 
 
149
class _Importer(object):
 
150
    """Helper for importing modules, but waiting until they are used.
 
151
 
 
152
    This also helps to ensure that existing ScopeReplacer objects are
 
153
    re-used in the current scope.
 
154
    """
 
155
 
 
156
    __slots__ = ['scope', 'modname', 'fromlist', 'mod']
 
157
 
 
158
    def __init__(self, scope, modname, fromlist):
 
159
        """
 
160
        :param scope: calling context globals() where the import should be made
 
161
        :param modname: The name of the module
 
162
        :param fromlist: the fromlist portion of 'from foo import bar'
 
163
        """
 
164
        self.scope = scope
 
165
        self.modname = modname
 
166
        self.fromlist = fromlist
 
167
        self.mod = None
 
168
 
 
169
    def module(self):
 
170
        """Import a module if not imported yet, and return"""
 
171
        if self.mod is None:
 
172
            self.mod = __import__(self.modname, self.scope, self.scope,
 
173
                                  self.fromlist)
 
174
            if isinstance(self.mod, _replacer):
 
175
                del sys.modules[self.modname]
 
176
                self.mod = __import__(self.modname, self.scope, self.scope,
 
177
                                      self.fromlist)
 
178
            del self.modname, self.fromlist
 
179
        return self.mod
 
180
 
 
181
class _replacer(object):
 
182
    '''placeholder for a demand loaded module. demandload puts this in
 
183
    a target scope.  when an attribute of this object is looked up,
 
184
    this object is replaced in the target scope with the actual
 
185
    module.
 
186
 
 
187
    we use __getattribute__ to avoid namespace clashes between
 
188
    placeholder object and real module.'''
 
189
 
 
190
    def __init__(self, importer, target):
 
191
        self.importer = importer
 
192
        self.target = target
 
193
        # consider case where we do this:
 
194
        #   demandload(globals(), 'foo.bar foo.quux')
 
195
        # foo will already exist in target scope when we get to
 
196
        # foo.quux.  so we remember that we will need to demandload
 
197
        # quux into foo's scope when we really load it.
 
198
        self.later = []
 
199
 
 
200
    def module(self):
 
201
        return object.__getattribute__(self, 'importer').module()
 
202
 
 
203
    def __getattribute__(self, key):
 
204
        '''look up an attribute in a module and return it. replace the
 
205
        name of the module in the caller\'s dict with the actual
 
206
        module.'''
 
207
 
 
208
        module = object.__getattribute__(self, 'module')()
 
209
        target = object.__getattribute__(self, 'target')
 
210
        importer = object.__getattribute__(self, 'importer')
 
211
        later = object.__getattribute__(self, 'later')
 
212
 
 
213
        if later:
 
214
            demandload(module.__dict__, ' '.join(later))
 
215
 
 
216
        importer.scope[target] = module
 
217
 
 
218
        return getattr(module, key)
 
219
 
 
220
class _replacer_from(_replacer):
 
221
    '''placeholder for a demand loaded module.  used for "from foo
 
222
    import ..." emulation. semantics of this are different than
 
223
    regular import, so different implementation needed.'''
 
224
 
 
225
    def module(self):
 
226
        importer = object.__getattribute__(self, 'importer')
 
227
        target = object.__getattribute__(self, 'target')
 
228
 
 
229
        return getattr(importer.module(), target)
 
230
 
 
231
    def __call__(self, *args, **kwargs):
 
232
        target = object.__getattribute__(self, 'module')()
 
233
        return target(*args, **kwargs)
 
234
 
 
235
def demandload(scope, modules):
 
236
    '''import modules into scope when each is first used.
 
237
 
 
238
    scope should be the value of globals() in the module calling this
 
239
    function, or locals() in the calling function.
 
240
 
 
241
    modules is a string listing module names, separated by white
 
242
    space.  names are handled like this:
 
243
 
 
244
    foo            import foo
 
245
    foo bar        import foo, bar
 
246
    foo.bar        import foo.bar
 
247
    foo:bar        from foo import bar
 
248
    foo:bar,quux   from foo import bar, quux
 
249
    foo.bar:quux   from foo.bar import quux'''
 
250
 
 
251
    for mod in modules.split():
 
252
        col = mod.find(':')
 
253
        if col >= 0:
 
254
            fromlist = mod[col+1:].split(',')
 
255
            mod = mod[:col]
 
256
        else:
 
257
            fromlist = []
 
258
        importer = _importer(scope, mod, fromlist)
 
259
        if fromlist:
 
260
            for name in fromlist:
 
261
                scope[name] = _replacer_from(importer, name)
 
262
        else:
 
263
            dot = mod.find('.')
 
264
            if dot >= 0:
 
265
                basemod = mod[:dot]
 
266
                val = scope.get(basemod)
 
267
                # if base module has already been demandload()ed,
 
268
                # remember to load this submodule into its namespace
 
269
                # when needed.
 
270
                if isinstance(val, _replacer):
 
271
                    later = object.__getattribute__(val, 'later')
 
272
                    later.append(mod[dot+1:])
 
273
                    continue
 
274
            else:
 
275
                basemod = mod
 
276
            scope[basemod] = _replacer(importer, basemod)
 
277
 
 
278
def lazy_import(scope, module_name, member=None, import_as=None):
 
279
    """Lazily import a module into the correct scope.
 
280
 
 
281
    This is meant as a possible replacement for __import__.
 
282
    It will return a ScopeReplacer object, which will call the real
 
283
    '__import__' at the appropriate time.
 
284
 
 
285
    :param module_name: The dotted module name
 
286
    :param member: Optional, if supplied return the sub member instead of
 
287
        the base module.
 
288
    :param import_as: Use this as the local object name instead of the
 
289
        default name.
 
290
    """
 
291
    if import_as is None:
 
292
        if member is None:
 
293
            module_pieces = module_name.split('.')
 
294
            final_name = module_pieces[0]
 
295
            def factory():
 
296
                return __import__(module_name, scope, locals(), [])
 
297
        else:
 
298
            final_name = member
 
299
    else:
 
300
        raise NotImplemented('import_as is not yet implemented')