1
# Copyright (C) 2006-2010 Canonical Ltd
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.
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.
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
17
"""Functionality to create lazy evaluation objects.
19
This includes waiting to import a module until it is actually used.
21
Most commonly, the 'lazy_import' function is used to import other modules
22
in an on-demand fashion. Typically use looks like::
24
from .lazy_import import lazy_import
25
lazy_import(globals(), '''
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.
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).
44
from __future__ import absolute_import
46
from .errors import BzrError, InternalBzrError
49
class ImportNameCollision(InternalBzrError):
51
_fmt = ("Tried to import an object to the same name as"
52
" an existing object. %(name)s")
54
def __init__(self, name):
55
BzrError.__init__(self)
59
class IllegalUseOfScopeReplacer(InternalBzrError):
61
_fmt = ("ScopeReplacer object %(name)r was used incorrectly:"
64
def __init__(self, name, msg, extra=None):
65
BzrError.__init__(self)
69
self.extra = ': ' + str(extra)
74
class InvalidImportLine(InternalBzrError):
76
_fmt = "Not a valid import statement: %(msg)\n%(text)s"
78
def __init__(self, text, msg):
79
BzrError.__init__(self)
84
class ScopeReplacer(object):
85
"""A lazy object that will replace itself in the appropriate scope.
87
This object sits, ready to create the real object the first time it is
91
__slots__ = ('_scope', '_factory', '_name', '_real_obj')
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.
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.
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.
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)
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')
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)
124
raise IllegalUseOfScopeReplacer(
125
name, msg="Object tried"
126
" to replace itself, check it's not using its own scope.")
128
# Check if another thread has jumped in while obj was generated.
129
real_obj = object.__getattribute__(self, '_real_obj')
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)
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?")
145
def __getattribute__(self, attr):
146
obj = object.__getattribute__(self, '_resolve')()
147
return getattr(obj, attr)
149
def __setattr__(self, attr, value):
150
obj = object.__getattribute__(self, '_resolve')()
151
return setattr(obj, attr, value)
153
def __call__(self, *args, **kwargs):
154
obj = object.__getattribute__(self, '_resolve')()
155
return obj(*args, **kwargs)
158
def disallow_proxying():
159
"""Disallow lazily imported modules to be used as proxies.
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.
165
Only lazy imports that happen after this call are affected.
167
ScopeReplacer._should_proxy = False
170
_builtin_import = __import__
173
class ImportReplacer(ScopeReplacer):
174
"""This is designed to replace only a portion of an import list.
176
It will replace itself with a module, and then make children
177
entries also ImportReplacer objects.
179
At present, this only supports 'import foo.bar.baz' syntax.
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
189
__slots__ = ('_import_replacer_children', '_member', '_module_path')
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.
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.
207
{'foo':(['breezy', 'foo'], None,
208
{'bar':(['breezy', 'foo', 'bar'], None {})})
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'
219
from foo import bar, baz would get translated into 2 import
220
requests. On for 'name=bar' and one for 'name=baz'
222
if (member is not None) and children:
223
raise ValueError('Cannot supply both a member and children')
225
object.__setattr__(self, '_import_replacer_children', children)
226
object.__setattr__(self, '_member', member)
227
object.__setattr__(self, '_module_path', module_path)
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,
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)
244
module = _builtin_import(name, scope, scope, [], level=0)
245
for path in module_path[1:]:
246
module = getattr(module, path)
248
# Prepare the children to be imported
249
for child_name, (child_path, child_member, grandchildren) in \
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)
260
class ImportProcessor(object):
261
"""Convert text that users input into lazy import requests"""
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.
269
__slots__ = ['imports', '_lazy_import_class']
271
def __init__(self, lazy_import_class=None):
273
if lazy_import_class is None:
274
self._lazy_import_class = ImportReplacer
276
self._lazy_import_class = lazy_import_class
278
def lazy_import(self, scope, text):
279
"""Convert the given text into a bunch of lazy import objects.
281
This takes a text string, which should be similar to normal python
284
self._build_map(text)
285
self._convert_imports(scope)
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])
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)
301
raise InvalidImportLine(
302
line, "doesn't start with 'import ' or 'from '")
304
def _convert_import_str(self, import_str):
305
"""This converts a import string into an import map.
307
This only understands 'import foo, foo.bar, foo.bar.baz as bing'
309
:param import_str: The import string to process
311
if not import_str.startswith('import '):
312
raise ValueError('bad import string %r' % (import_str,))
313
import_str = import_str[len('import '):]
315
for path in import_str.split(','):
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, {})
333
# Now we need to handle
334
module_path = path.split('.')
335
name = module_path[0]
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
343
module_def = self.imports[name]
347
for child in module_path[1:]:
348
cur_path.append(child)
352
next = (cur_path[:], None, {})
356
def _convert_from_str(self, from_str):
357
"""This converts a 'from foo import bar' string into an import map.
359
:param from_str: The import string to process
361
if not from_str.startswith('from '):
362
raise ValueError('bad from/import %r' % from_str)
363
from_str = from_str[len('from '):]
365
from_module, import_list = from_str.split(' import ')
367
from_module_path = from_module.split('.')
369
if not from_module_path[0]:
370
raise ImportError(from_module)
372
for path in import_list.split(','):
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()
385
if name in self.imports:
386
raise ImportNameCollision(name)
387
self.imports[name] = (from_module_path, module, {})
389
def _canonicalize_import_text(self, text):
390
"""Take a list of imports, and split it into regularized form.
392
This is meant to take regular import text, and convert it to
393
the forms that the rest of the converters prefer.
398
for line in text.split('\n'):
402
line = line[:loc].strip()
407
if line.endswith(')'):
408
out.append(cur + ' ' + line[:-1])
413
if '(' in line and ')' not in line:
414
cur = line.replace('(', '')
416
out.append(line.replace('(', '').replace(')', ''))
418
raise InvalidImportLine(cur, 'Unmatched parenthesis')
422
def lazy_import(scope, text, lazy_import_class=None):
423
"""Create lazy imports for all of the imports in text.
425
This is typically used as something like::
427
from breezy.lazy_import import lazy_import
428
lazy_import(globals(), '''
435
import breezy.transport
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.
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
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)