~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lazy_import.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil, Patch Queue Manager, Jelmer Vernooij
  • Date: 2017-01-17 16:20:41 UTC
  • mfrom: (6619.1.2 trunk)
  • Revision ID: tarmac-20170117162041-oo62uk1qsmgc9j31
Merge 2.7 into trunk including fixes for bugs #1622039, #1644003, #1579093 and #1645017. [r=vila]

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
from __future__ import absolute_import
 
45
 
 
46
 
 
47
class ScopeReplacer(object):
 
48
    """A lazy object that will replace itself in the appropriate scope.
 
49
 
 
50
    This object sits, ready to create the real object the first time it is
 
51
    needed.
 
52
    """
 
53
 
 
54
    __slots__ = ('_scope', '_factory', '_name', '_real_obj')
 
55
 
 
56
    # If you to do x = y, setting this to False will disallow access to
 
57
    # members from the second variable (i.e. x). This should normally
 
58
    # be enabled for reasons of thread safety and documentation, but
 
59
    # will be disabled during the selftest command to check for abuse.
 
60
    _should_proxy = True
 
61
 
 
62
    def __init__(self, scope, factory, name):
 
63
        """Create a temporary object in the specified scope.
 
64
        Once used, a real object will be placed in the scope.
 
65
 
 
66
        :param scope: The scope the object should appear in
 
67
        :param factory: A callable that will create the real object.
 
68
            It will be passed (self, scope, name)
 
69
        :param name: The variable name in the given scope.
 
70
        """
 
71
        object.__setattr__(self, '_scope', scope)
 
72
        object.__setattr__(self, '_factory', factory)
 
73
        object.__setattr__(self, '_name', name)
 
74
        object.__setattr__(self, '_real_obj', None)
 
75
        scope[name] = self
 
76
 
 
77
    def _resolve(self):
 
78
        """Return the real object for which this is a placeholder"""
 
79
        name = object.__getattribute__(self, '_name')
 
80
        real_obj = object.__getattribute__(self, '_real_obj')
 
81
        if real_obj is None:
 
82
            # No obj generated previously, so generate from factory and scope.
 
83
            factory = object.__getattribute__(self, '_factory')
 
84
            scope = object.__getattribute__(self, '_scope')
 
85
            obj = factory(self, scope, name)
 
86
            if obj is self:
 
87
                raise errors.IllegalUseOfScopeReplacer(name, msg="Object tried"
 
88
                    " to replace itself, check it's not using its own scope.")
 
89
 
 
90
            # Check if another thread has jumped in while obj was generated.
 
91
            real_obj = object.__getattribute__(self, '_real_obj')
 
92
            if real_obj is None:
 
93
                # Still no prexisting obj, so go ahead and assign to scope and
 
94
                # return. There is still a small window here where races will
 
95
                # not be detected, but safest to avoid additional locking.
 
96
                object.__setattr__(self, '_real_obj', obj)
 
97
                scope[name] = obj
 
98
                return obj
 
99
 
 
100
        # Raise if proxying is disabled as obj has already been generated.
 
101
        if not ScopeReplacer._should_proxy:
 
102
            raise errors.IllegalUseOfScopeReplacer(
 
103
                name, msg="Object already replaced, did you assign it"
 
104
                          " to another variable?")
 
105
        return real_obj
 
106
 
 
107
    def __getattribute__(self, attr):
 
108
        obj = object.__getattribute__(self, '_resolve')()
 
109
        return getattr(obj, attr)
 
110
 
 
111
    def __setattr__(self, attr, value):
 
112
        obj = object.__getattribute__(self, '_resolve')()
 
113
        return setattr(obj, attr, value)
 
114
 
 
115
    def __call__(self, *args, **kwargs):
 
116
        obj = object.__getattribute__(self, '_resolve')()
 
117
        return obj(*args, **kwargs)
 
118
 
 
119
 
 
120
def disallow_proxying():
 
121
    """Disallow lazily imported modules to be used as proxies.
 
122
 
 
123
    Calling this function might cause problems with concurrent imports
 
124
    in multithreaded environments, but will help detecting wasteful
 
125
    indirection, so it should be called when executing unit tests.
 
126
 
 
127
    Only lazy imports that happen after this call are affected.
 
128
    """
 
129
    ScopeReplacer._should_proxy = False
 
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], level=0)
 
201
            return getattr(module, member)
 
202
        else:
 
203
            module = __import__(module_python_path, scope, scope, [], level=0)
 
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
""")