1
# Copyright (C) 2006 by 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Functionality to create lazy evaluation objects.
19
This includes waiting to import a module until it is actually used.
27
class ScopeReplacer(object):
28
"""A lazy object that will replace itself in the appropriate scope.
30
This object sits, ready to create the real object the first time it is
34
__slots__ = ('_scope', '_factory', '_name')
36
def __init__(self, scope, factory, name):
37
"""Create a temporary object in the specified scope.
38
Once used, a real object will be placed in the scope.
40
:param scope: The scope the object should appear in
41
:param factory: A callable that will create the real object.
42
It will be passed (self, scope, name)
43
:param name: The variable name in the given scope.
46
self._factory = factory
51
"""Actually replace self with other in the given scope"""
52
name = object.__getattribute__(self, '_name')
54
factory = object.__getattribute__(self, '_factory')
55
scope = object.__getattribute__(self, '_scope')
56
except AttributeError, e:
57
# Because ScopeReplacer objects only replace a single
58
# item, passing them to another variable before they are
59
# replaced would cause them to keep getting replaced
60
# (only they are replacing the wrong variable). So we
61
# make it forbidden, and try to give a good error.
62
raise errors.IllegalUseOfScopeReplacer(
63
name, msg="Object already cleaned up, did you assign it"
64
"to another variable?",
66
obj = factory(self, scope, name)
71
"""Stop holding on to all the extra stuff"""
74
# We keep _name, so that we can report errors
77
def __getattribute__(self, attr):
78
_replace = object.__getattribute__(self, '_replace')
80
_cleanup = object.__getattribute__(self, '_cleanup')
82
return getattr(obj, attr)
84
def __call__(self, *args, **kwargs):
85
_replace = object.__getattribute__(self, '_replace')
87
_cleanup = object.__getattribute__(self, '_cleanup')
89
return obj(*args, **kwargs)
92
class ImportReplacer(ScopeReplacer):
93
"""This is designed to replace only a portion of an import list.
95
It will replace itself with a module, and then make children
96
entries also ImportReplacer objects.
98
At present, this only supports 'import foo.bar.baz' syntax.
101
# Intentially a long semi-unique name that won't likely exist
102
# elsewhere. (We can't use isinstance because that accesses __class__
103
# which causes the __getattribute__ to trigger)
104
__slots__ = ('_import_replacer_children', '_member', '_module_path')
106
def __init__(self, scope, name, module_path, member=None, children={}):
107
"""Upon request import 'module_path' as the name 'module_name'.
108
When imported, prepare children to also be imported.
110
:param scope: The scope that objects should be imported into.
111
Typically this is globals()
112
:param name: The variable name. Often this is the same as the
113
module_path. 'bzrlib'
114
:param module_path: A list for the fully specified module path
115
['bzrlib', 'foo', 'bar']
116
:param member: The member inside the module to import, often this is
117
None, indicating the module is being imported.
118
:param children: Children entries to be imported later.
119
This should be a map of children specifications.
120
{'foo':(['bzrlib', 'foo'], None,
121
{'bar':(['bzrlib', 'foo', 'bar'], None {})})
124
import foo => name='foo' module_path='foo',
125
member=None, children={}
126
import foo.bar => name='foo' module_path='foo', member=None,
127
children={'bar':(['foo', 'bar'], None, {}}
128
from foo import bar => name='bar' module_path='foo', member='bar'
130
from foo import bar, baz would get translated into 2 import
131
requests. On for 'name=bar' and one for 'name=baz'
133
if member is not None:
134
assert not children, \
135
'Cannot supply both a member and children'
137
self._import_replacer_children = children
138
self._member = member
139
self._module_path = module_path
141
# Indirecting through __class__ so that children can
142
# override _import (especially our instrumented version)
143
cls = object.__getattribute__(self, '__class__')
144
ScopeReplacer.__init__(self, scope=scope, name=name,
147
def _import(self, scope, name):
148
children = object.__getattribute__(self, '_import_replacer_children')
149
member = object.__getattribute__(self, '_member')
150
module_path = object.__getattribute__(self, '_module_path')
151
module_python_path = '.'.join(module_path)
152
if member is not None:
153
module = __import__(module_python_path, scope, scope, [member])
154
return getattr(module, member)
156
module = __import__(module_python_path, scope, scope, [])
157
for path in module_path[1:]:
158
module = getattr(module, path)
160
# Prepare the children to be imported
161
for child_name, (child_path, child_member, grandchildren) in \
162
children.iteritems():
163
# Using self.__class__, so that children get children classes
164
# instantiated. (This helps with instrumented tests)
165
cls = object.__getattribute__(self, '__class__')
166
cls(module.__dict__, name=child_name,
167
module_path=child_path, member=child_member,
168
children=grandchildren)
172
class ImportProcessor(object):
173
"""Convert text that users input into lazy import requests"""
175
# TODO: jam 20060912 This class is probably not strict enough about
176
# what type of text it allows. For example, you can do:
177
# import (foo, bar), which is not allowed by python.
178
# For now, it should be supporting a superset of python import
179
# syntax which is all we really care about.
181
__slots__ = ['imports', '_lazy_import_class']
183
def __init__(self, lazy_import_class=None):
185
if lazy_import_class is None:
186
self._lazy_import_class = ImportReplacer
188
self._lazy_import_class = lazy_import_class
190
def lazy_import(self, scope, text):
191
"""Convert the given text into a bunch of lazy import objects.
193
This takes a text string, which should be similar to normal python
196
self._build_map(text)
197
self._convert_imports(scope)
199
def _convert_imports(self, scope):
200
# Now convert the map into a set of imports
201
for name, info in self.imports.iteritems():
202
self._lazy_import_class(scope, name=name, module_path=info[0],
203
member=info[1], children=info[2])
205
def _build_map(self, text):
206
"""Take a string describing imports, and build up the internal map"""
207
for line in self._canonicalize_import_text(text):
208
if line.startswith('import '):
209
self._convert_import_str(line)
210
elif line.startswith('from '):
211
self._convert_from_str(line)
213
raise errors.InvalidImportLine(line,
214
"doesn't start with 'import ' or 'from '")
216
def _convert_import_str(self, import_str):
217
"""This converts a import string into an import map.
219
This only understands 'import foo, foo.bar, foo.bar.baz as bing'
221
:param import_str: The import string to process
223
assert import_str.startswith('import ')
224
import_str = import_str[len('import '):]
226
for path in import_str.split(','):
230
as_hunks = path.split(' as ')
231
if len(as_hunks) == 2:
232
# We have 'as' so this is a different style of import
233
# 'import foo.bar.baz as bing' creates a local variable
234
# named 'bing' which points to 'foo.bar.baz'
235
name = as_hunks[1].strip()
236
module_path = as_hunks[0].strip().split('.')
237
if name in self.imports:
238
raise errors.ImportNameCollision(name)
239
# No children available in 'import foo as bar'
240
self.imports[name] = (module_path, None, {})
242
# Now we need to handle
243
module_path = path.split('.')
244
name = module_path[0]
245
if name not in self.imports:
246
# This is a new import that we haven't seen before
247
module_def = ([name], None, {})
248
self.imports[name] = module_def
250
module_def = self.imports[name]
254
for child in module_path[1:]:
255
cur_path.append(child)
259
next = (cur_path[:], None, {})
263
def _convert_from_str(self, from_str):
264
"""This converts a 'from foo import bar' string into an import map.
266
:param from_str: The import string to process
268
assert from_str.startswith('from ')
269
from_str = from_str[len('from '):]
271
from_module, import_list = from_str.split(' import ')
273
from_module_path = from_module.split('.')
275
for path in import_list.split(','):
279
as_hunks = path.split(' as ')
280
if len(as_hunks) == 2:
281
# We have 'as' so this is a different style of import
282
# 'import foo.bar.baz as bing' creates a local variable
283
# named 'bing' which points to 'foo.bar.baz'
284
name = as_hunks[1].strip()
285
module = as_hunks[0].strip()
288
if name in self.imports:
289
raise errors.ImportNameCollision(name)
290
self.imports[name] = (from_module_path, module, {})
292
def _canonicalize_import_text(self, text):
293
"""Take a list of imports, and split it into regularized form.
295
This is meant to take regular import text, and convert it to
296
the forms that the rest of the converters prefer.
302
for line in text.split('\n'):
306
line = line[:loc].strip()
311
if line.endswith(')'):
312
out.append(cur + ' ' + line[:-1])
317
if '(' in line and ')' not in line:
318
cur = line.replace('(', '')
320
out.append(line.replace('(', '').replace(')', ''))
322
raise errors.InvalidImportLine(cur, 'Unmatched parenthesis')
326
def lazy_import(scope, text, lazy_import_class=None):
327
"""Create lazy imports for all of the imports in text.
329
This is typically used as something like:
330
from bzrlib.lazy_import import lazy_import
331
lazy_import(globals(), '''
338
import bzrlib.transport
341
Then 'foo, bar, baz' and 'bzrlib' will exist as lazy-loaded
342
objects which will be replaced with a real object on first use.
344
In general, it is best to only load modules in this way. This is
345
because other objects (functions/classes/variables) are frequently
346
used without accessing a member, which means we cannot tell they
349
# This is just a helper around ImportProcessor.lazy_import
350
proc = ImportProcessor(lazy_import_class=lazy_import_class)
351
return proc.lazy_import(scope, text)