/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 breezy/lazy_import.py

  • Committer: Jelmer Vernooij
  • Date: 2019-02-04 01:01:24 UTC
  • mto: This revision was merged to the branch mainline in revision 7268.
  • Revision ID: jelmer@jelmer.uk-20190204010124-ni0i4qc6f5tnbvux
Fix source tests.

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