/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: 2020-02-19 23:18:42 UTC
  • mto: (7490.3.4 work)
  • mto: This revision was merged to the branch mainline in revision 7495.
  • Revision ID: jelmer@jelmer.uk-20200219231842-agwjh2db66cpajqg
Consistent return values.

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
from .errors import BzrError, InternalBzrError
 
47
 
 
48
 
 
49
class ImportNameCollision(InternalBzrError):
 
50
 
 
51
    _fmt = ("Tried to import an object to the same name as"
 
52
            " an existing object. %(name)s")
 
53
 
 
54
    def __init__(self, name):
 
55
        BzrError.__init__(self)
 
56
        self.name = name
 
57
 
 
58
 
 
59
class IllegalUseOfScopeReplacer(InternalBzrError):
 
60
 
 
61
    _fmt = ("ScopeReplacer object %(name)r was used incorrectly:"
 
62
            " %(msg)s%(extra)s")
 
63
 
 
64
    def __init__(self, name, msg, extra=None):
 
65
        BzrError.__init__(self)
 
66
        self.name = name
 
67
        self.msg = msg
 
68
        if extra:
 
69
            self.extra = ': ' + str(extra)
 
70
        else:
 
71
            self.extra = ''
 
72
 
 
73
 
 
74
class InvalidImportLine(InternalBzrError):
 
75
 
 
76
    _fmt = "Not a valid import statement: %(msg)\n%(text)s"
 
77
 
 
78
    def __init__(self, text, msg):
 
79
        BzrError.__init__(self)
 
80
        self.text = text
 
81
        self.msg = msg
 
82
 
 
83
 
 
84
class ScopeReplacer(object):
 
85
    """A lazy object that will replace itself in the appropriate scope.
 
86
 
 
87
    This object sits, ready to create the real object the first time it is
 
88
    needed.
 
89
    """
 
90
 
 
91
    __slots__ = ('_scope', '_factory', '_name', '_real_obj')
 
92
 
 
93
    # If you to do x = y, setting this to False will disallow access to
 
94
    # members from the second variable (i.e. x). This should normally
 
95
    # be enabled for reasons of thread safety and documentation, but
 
96
    # will be disabled during the selftest command to check for abuse.
 
97
    _should_proxy = True
 
98
 
 
99
    def __init__(self, scope, factory, name):
 
100
        """Create a temporary object in the specified scope.
 
101
        Once used, a real object will be placed in the scope.
 
102
 
 
103
        :param scope: The scope the object should appear in
 
104
        :param factory: A callable that will create the real object.
 
105
            It will be passed (self, scope, name)
 
106
        :param name: The variable name in the given scope.
 
107
        """
 
108
        object.__setattr__(self, '_scope', scope)
 
109
        object.__setattr__(self, '_factory', factory)
 
110
        object.__setattr__(self, '_name', name)
 
111
        object.__setattr__(self, '_real_obj', None)
 
112
        scope[name] = self
 
113
 
 
114
    def _resolve(self):
 
115
        """Return the real object for which this is a placeholder"""
 
116
        name = object.__getattribute__(self, '_name')
 
117
        real_obj = object.__getattribute__(self, '_real_obj')
 
118
        if real_obj is None:
 
119
            # No obj generated previously, so generate from factory and scope.
 
120
            factory = object.__getattribute__(self, '_factory')
 
121
            scope = object.__getattribute__(self, '_scope')
 
122
            obj = factory(self, scope, name)
 
123
            if obj is self:
 
124
                raise IllegalUseOfScopeReplacer(
 
125
                    name, msg="Object tried"
 
126
                    " to replace itself, check it's not using its own scope.")
 
127
 
 
128
            # Check if another thread has jumped in while obj was generated.
 
129
            real_obj = object.__getattribute__(self, '_real_obj')
 
130
            if real_obj is None:
 
131
                # Still no prexisting obj, so go ahead and assign to scope and
 
132
                # return. There is still a small window here where races will
 
133
                # not be detected, but safest to avoid additional locking.
 
134
                object.__setattr__(self, '_real_obj', obj)
 
135
                scope[name] = obj
 
136
                return obj
 
137
 
 
138
        # Raise if proxying is disabled as obj has already been generated.
 
139
        if not ScopeReplacer._should_proxy:
 
140
            raise IllegalUseOfScopeReplacer(
 
141
                name, msg="Object already replaced, did you assign it"
 
142
                          " to another variable?")
 
143
        return real_obj
 
144
 
 
145
    def __getattribute__(self, attr):
 
146
        obj = object.__getattribute__(self, '_resolve')()
 
147
        return getattr(obj, attr)
 
148
 
 
149
    def __setattr__(self, attr, value):
 
150
        obj = object.__getattribute__(self, '_resolve')()
 
151
        return setattr(obj, attr, value)
 
152
 
 
153
    def __call__(self, *args, **kwargs):
 
154
        obj = object.__getattribute__(self, '_resolve')()
 
155
        return obj(*args, **kwargs)
 
156
 
 
157
 
 
158
def disallow_proxying():
 
159
    """Disallow lazily imported modules to be used as proxies.
 
160
 
 
161
    Calling this function might cause problems with concurrent imports
 
162
    in multithreaded environments, but will help detecting wasteful
 
163
    indirection, so it should be called when executing unit tests.
 
164
 
 
165
    Only lazy imports that happen after this call are affected.
 
166
    """
 
167
    ScopeReplacer._should_proxy = False
 
168
 
 
169
 
 
170
_builtin_import = __import__
 
171
 
 
172
 
 
173
class ImportReplacer(ScopeReplacer):
 
174
    """This is designed to replace only a portion of an import list.
 
175
 
 
176
    It will replace itself with a module, and then make children
 
177
    entries also ImportReplacer objects.
 
178
 
 
179
    At present, this only supports 'import foo.bar.baz' syntax.
 
180
    """
 
181
 
 
182
    # '_import_replacer_children' is intentionally a long semi-unique name
 
183
    # that won't likely exist elsewhere. This allows us to detect an
 
184
    # ImportReplacer object by using
 
185
    #       object.__getattribute__(obj, '_import_replacer_children')
 
186
    # We can't just use 'isinstance(obj, ImportReplacer)', because that
 
187
    # accesses .__class__, which goes through __getattribute__, and triggers
 
188
    # the replacement.
 
189
    __slots__ = ('_import_replacer_children', '_member', '_module_path')
 
190
 
 
191
    def __init__(self, scope, name, module_path, member=None, children={}):
 
192
        """Upon request import 'module_path' as the name 'module_name'.
 
193
        When imported, prepare children to also be imported.
 
194
 
 
195
        :param scope: The scope that objects should be imported into.
 
196
            Typically this is globals()
 
197
        :param name: The variable name. Often this is the same as the
 
198
            module_path. 'breezy'
 
199
        :param module_path: A list for the fully specified module path
 
200
            ['breezy', 'foo', 'bar']
 
201
        :param member: The member inside the module to import, often this is
 
202
            None, indicating the module is being imported.
 
203
        :param children: Children entries to be imported later.
 
204
            This should be a map of children specifications.
 
205
            ::
 
206
 
 
207
                {'foo':(['breezy', 'foo'], None,
 
208
                    {'bar':(['breezy', 'foo', 'bar'], None {})})
 
209
                }
 
210
 
 
211
        Examples::
 
212
 
 
213
            import foo => name='foo' module_path='foo',
 
214
                          member=None, children={}
 
215
            import foo.bar => name='foo' module_path='foo', member=None,
 
216
                              children={'bar':(['foo', 'bar'], None, {}}
 
217
            from foo import bar => name='bar' module_path='foo', member='bar'
 
218
                                   children={}
 
219
            from foo import bar, baz would get translated into 2 import
 
220
            requests. On for 'name=bar' and one for 'name=baz'
 
221
        """
 
222
        if (member is not None) and children:
 
223
            raise ValueError('Cannot supply both a member and children')
 
224
 
 
225
        object.__setattr__(self, '_import_replacer_children', children)
 
226
        object.__setattr__(self, '_member', member)
 
227
        object.__setattr__(self, '_module_path', module_path)
 
228
 
 
229
        # Indirecting through __class__ so that children can
 
230
        # override _import (especially our instrumented version)
 
231
        cls = object.__getattribute__(self, '__class__')
 
232
        ScopeReplacer.__init__(self, scope=scope, name=name,
 
233
                               factory=cls._import)
 
234
 
 
235
    def _import(self, scope, name):
 
236
        children = object.__getattribute__(self, '_import_replacer_children')
 
237
        member = object.__getattribute__(self, '_member')
 
238
        module_path = object.__getattribute__(self, '_module_path')
 
239
        name = '.'.join(module_path)
 
240
        if member is not None:
 
241
            module = _builtin_import(name, scope, scope, [member], level=0)
 
242
            return getattr(module, member)
 
243
        else:
 
244
            module = _builtin_import(name, scope, scope, [], level=0)
 
245
            for path in module_path[1:]:
 
246
                module = getattr(module, path)
 
247
 
 
248
        # Prepare the children to be imported
 
249
        for child_name, (child_path, child_member, grandchildren) in \
 
250
                children.items():
 
251
            # Using self.__class__, so that children get children classes
 
252
            # instantiated. (This helps with instrumented tests)
 
253
            cls = object.__getattribute__(self, '__class__')
 
254
            cls(module.__dict__, name=child_name,
 
255
                module_path=child_path, member=child_member,
 
256
                children=grandchildren)
 
257
        return module
 
258
 
 
259
 
 
260
class ImportProcessor(object):
 
261
    """Convert text that users input into lazy import requests"""
 
262
 
 
263
    # TODO: jam 20060912 This class is probably not strict enough about
 
264
    #       what type of text it allows. For example, you can do:
 
265
    #       import (foo, bar), which is not allowed by python.
 
266
    #       For now, it should be supporting a superset of python import
 
267
    #       syntax which is all we really care about.
 
268
 
 
269
    __slots__ = ['imports', '_lazy_import_class']
 
270
 
 
271
    def __init__(self, lazy_import_class=None):
 
272
        self.imports = {}
 
273
        if lazy_import_class is None:
 
274
            self._lazy_import_class = ImportReplacer
 
275
        else:
 
276
            self._lazy_import_class = lazy_import_class
 
277
 
 
278
    def lazy_import(self, scope, text):
 
279
        """Convert the given text into a bunch of lazy import objects.
 
280
 
 
281
        This takes a text string, which should be similar to normal python
 
282
        import markup.
 
283
        """
 
284
        self._build_map(text)
 
285
        self._convert_imports(scope)
 
286
 
 
287
    def _convert_imports(self, scope):
 
288
        # Now convert the map into a set of imports
 
289
        for name, info in self.imports.items():
 
290
            self._lazy_import_class(scope, name=name, module_path=info[0],
 
291
                                    member=info[1], children=info[2])
 
292
 
 
293
    def _build_map(self, text):
 
294
        """Take a string describing imports, and build up the internal map"""
 
295
        for line in self._canonicalize_import_text(text):
 
296
            if line.startswith('import '):
 
297
                self._convert_import_str(line)
 
298
            elif line.startswith('from '):
 
299
                self._convert_from_str(line)
 
300
            else:
 
301
                raise InvalidImportLine(
 
302
                    line, "doesn't start with 'import ' or 'from '")
 
303
 
 
304
    def _convert_import_str(self, import_str):
 
305
        """This converts a import string into an import map.
 
306
 
 
307
        This only understands 'import foo, foo.bar, foo.bar.baz as bing'
 
308
 
 
309
        :param import_str: The import string to process
 
310
        """
 
311
        if not import_str.startswith('import '):
 
312
            raise ValueError('bad import string %r' % (import_str,))
 
313
        import_str = import_str[len('import '):]
 
314
 
 
315
        for path in import_str.split(','):
 
316
            path = path.strip()
 
317
            if not path:
 
318
                continue
 
319
            as_hunks = path.split(' as ')
 
320
            if len(as_hunks) == 2:
 
321
                # We have 'as' so this is a different style of import
 
322
                # 'import foo.bar.baz as bing' creates a local variable
 
323
                # named 'bing' which points to 'foo.bar.baz'
 
324
                name = as_hunks[1].strip()
 
325
                module_path = as_hunks[0].strip().split('.')
 
326
                if name in self.imports:
 
327
                    raise ImportNameCollision(name)
 
328
                if not module_path[0]:
 
329
                    raise ImportError(path)
 
330
                # No children available in 'import foo as bar'
 
331
                self.imports[name] = (module_path, None, {})
 
332
            else:
 
333
                # Now we need to handle
 
334
                module_path = path.split('.')
 
335
                name = module_path[0]
 
336
                if not name:
 
337
                    raise ImportError(path)
 
338
                if name not in self.imports:
 
339
                    # This is a new import that we haven't seen before
 
340
                    module_def = ([name], None, {})
 
341
                    self.imports[name] = module_def
 
342
                else:
 
343
                    module_def = self.imports[name]
 
344
 
 
345
                cur_path = [name]
 
346
                cur = module_def[2]
 
347
                for child in module_path[1:]:
 
348
                    cur_path.append(child)
 
349
                    if child in cur:
 
350
                        cur = cur[child][2]
 
351
                    else:
 
352
                        next = (cur_path[:], None, {})
 
353
                        cur[child] = next
 
354
                        cur = next[2]
 
355
 
 
356
    def _convert_from_str(self, from_str):
 
357
        """This converts a 'from foo import bar' string into an import map.
 
358
 
 
359
        :param from_str: The import string to process
 
360
        """
 
361
        if not from_str.startswith('from '):
 
362
            raise ValueError('bad from/import %r' % from_str)
 
363
        from_str = from_str[len('from '):]
 
364
 
 
365
        from_module, import_list = from_str.split(' import ')
 
366
 
 
367
        from_module_path = from_module.split('.')
 
368
 
 
369
        if not from_module_path[0]:
 
370
            raise ImportError(from_module)
 
371
 
 
372
        for path in import_list.split(','):
 
373
            path = path.strip()
 
374
            if not path:
 
375
                continue
 
376
            as_hunks = path.split(' as ')
 
377
            if len(as_hunks) == 2:
 
378
                # We have 'as' so this is a different style of import
 
379
                # 'import foo.bar.baz as bing' creates a local variable
 
380
                # named 'bing' which points to 'foo.bar.baz'
 
381
                name = as_hunks[1].strip()
 
382
                module = as_hunks[0].strip()
 
383
            else:
 
384
                name = module = path
 
385
            if name in self.imports:
 
386
                raise ImportNameCollision(name)
 
387
            self.imports[name] = (from_module_path, module, {})
 
388
 
 
389
    def _canonicalize_import_text(self, text):
 
390
        """Take a list of imports, and split it into regularized form.
 
391
 
 
392
        This is meant to take regular import text, and convert it to
 
393
        the forms that the rest of the converters prefer.
 
394
        """
 
395
        out = []
 
396
        cur = None
 
397
 
 
398
        for line in text.split('\n'):
 
399
            line = line.strip()
 
400
            loc = line.find('#')
 
401
            if loc != -1:
 
402
                line = line[:loc].strip()
 
403
 
 
404
            if not line:
 
405
                continue
 
406
            if cur is not None:
 
407
                if line.endswith(')'):
 
408
                    out.append(cur + ' ' + line[:-1])
 
409
                    cur = None
 
410
                else:
 
411
                    cur += ' ' + line
 
412
            else:
 
413
                if '(' in line and ')' not in line:
 
414
                    cur = line.replace('(', '')
 
415
                else:
 
416
                    out.append(line.replace('(', '').replace(')', ''))
 
417
        if cur is not None:
 
418
            raise InvalidImportLine(cur, 'Unmatched parenthesis')
 
419
        return out
 
420
 
 
421
 
 
422
def lazy_import(scope, text, lazy_import_class=None):
 
423
    """Create lazy imports for all of the imports in text.
 
424
 
 
425
    This is typically used as something like::
 
426
 
 
427
        from breezy.lazy_import import lazy_import
 
428
        lazy_import(globals(), '''
 
429
        from breezy import (
 
430
            foo,
 
431
            bar,
 
432
            baz,
 
433
            )
 
434
        import breezy.branch
 
435
        import breezy.transport
 
436
        ''')
 
437
 
 
438
    Then 'foo, bar, baz' and 'breezy' will exist as lazy-loaded
 
439
    objects which will be replaced with a real object on first use.
 
440
 
 
441
    In general, it is best to only load modules in this way. This is
 
442
    because other objects (functions/classes/variables) are frequently
 
443
    used without accessing a member, which means we cannot tell they
 
444
    have been used.
 
445
    """
 
446
    # This is just a helper around ImportProcessor.lazy_import
 
447
    proc = ImportProcessor(lazy_import_class=lazy_import_class)
 
448
    return proc.lazy_import(scope, text)