/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: John Arbash Meinel
  • Date: 2006-09-13 02:51:49 UTC
  • mto: This revision was merged to the branch mainline in revision 2071.
  • Revision ID: john@arbash-meinel.com-20060913025149-ecee20a9ca6fc4a6
lazy_import AtomicFile

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