/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-05-24 00:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 7504.
  • Revision ID: jelmer@jelmer.uk-20200524003950-bbc545r76vc5yajg
Add github action.

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