##// END OF EJS Templates
demandimport: error out early on missing attribute of non package (issue5373)...
Yuya Nishihara -
r30022:26a4e46a default
parent child Browse files
Show More
@@ -1,320 +1,325 b''
1 1 # demandimport.py - global demand-loading of modules for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''
9 9 demandimport - automatic demandloading of modules
10 10
11 11 To enable this module, do:
12 12
13 13 import demandimport; demandimport.enable()
14 14
15 15 Imports of the following forms will be demand-loaded:
16 16
17 17 import a, b.c
18 18 import a.b as c
19 19 from a import b,c # a will be loaded immediately
20 20
21 21 These imports will not be delayed:
22 22
23 23 from a import *
24 24 b = __import__(a)
25 25 '''
26 26
27 27 from __future__ import absolute_import
28 28
29 29 import contextlib
30 30 import os
31 31 import sys
32 32
33 33 # __builtin__ in Python 2, builtins in Python 3.
34 34 try:
35 35 import __builtin__ as builtins
36 36 except ImportError:
37 37 import builtins
38 38
39 39 contextmanager = contextlib.contextmanager
40 40
41 41 _origimport = __import__
42 42
43 43 nothing = object()
44 44
45 45 # Python 3 doesn't have relative imports nor level -1.
46 46 level = -1
47 47 if sys.version_info[0] >= 3:
48 48 level = 0
49 49 _import = _origimport
50 50
51 51 def _hgextimport(importfunc, name, globals, *args, **kwargs):
52 52 try:
53 53 return importfunc(name, globals, *args, **kwargs)
54 54 except ImportError:
55 55 if not globals:
56 56 raise
57 57 # extensions are loaded with "hgext_" prefix
58 58 hgextname = 'hgext_%s' % name
59 59 nameroot = hgextname.split('.', 1)[0]
60 60 contextroot = globals.get('__name__', '').split('.', 1)[0]
61 61 if nameroot != contextroot:
62 62 raise
63 63 # retry to import with "hgext_" prefix
64 64 return importfunc(hgextname, globals, *args, **kwargs)
65 65
66 66 class _demandmod(object):
67 67 """module demand-loader and proxy
68 68
69 69 Specify 1 as 'level' argument at construction, to import module
70 70 relatively.
71 71 """
72 72 def __init__(self, name, globals, locals, level):
73 73 if '.' in name:
74 74 head, rest = name.split('.', 1)
75 75 after = [rest]
76 76 else:
77 77 head = name
78 78 after = []
79 79 object.__setattr__(self, "_data",
80 80 (head, globals, locals, after, level, set()))
81 81 object.__setattr__(self, "_module", None)
82 82 def _extend(self, name):
83 83 """add to the list of submodules to load"""
84 84 self._data[3].append(name)
85 85
86 86 def _addref(self, name):
87 87 """Record that the named module ``name`` imports this module.
88 88
89 89 References to this proxy class having the name of this module will be
90 90 replaced at module load time. We assume the symbol inside the importing
91 91 module is identical to the "head" name of this module. We don't
92 92 actually know if "as X" syntax is being used to change the symbol name
93 93 because this information isn't exposed to __import__.
94 94 """
95 95 self._data[5].add(name)
96 96
97 97 def _load(self):
98 98 if not self._module:
99 99 head, globals, locals, after, level, modrefs = self._data
100 100 mod = _hgextimport(_import, head, globals, locals, None, level)
101 101 if mod is self:
102 102 # In this case, _hgextimport() above should imply
103 103 # _demandimport(). Otherwise, _hgextimport() never
104 104 # returns _demandmod. This isn't intentional behavior,
105 105 # in fact. (see also issue5304 for detail)
106 106 #
107 107 # If self._module is already bound at this point, self
108 108 # should be already _load()-ed while _hgextimport().
109 109 # Otherwise, there is no way to import actual module
110 110 # as expected, because (re-)invoking _hgextimport()
111 111 # should cause same result.
112 112 # This is reason why _load() returns without any more
113 113 # setup but assumes self to be already bound.
114 114 mod = self._module
115 115 assert mod and mod is not self, "%s, %s" % (self, mod)
116 116 return
117 117
118 118 # load submodules
119 119 def subload(mod, p):
120 120 h, t = p, None
121 121 if '.' in p:
122 122 h, t = p.split('.', 1)
123 123 if getattr(mod, h, nothing) is nothing:
124 124 setattr(mod, h, _demandmod(p, mod.__dict__, mod.__dict__,
125 125 level=1))
126 126 elif t:
127 127 subload(getattr(mod, h), t)
128 128
129 129 for x in after:
130 130 subload(mod, x)
131 131
132 132 # Replace references to this proxy instance with the actual module.
133 133 if locals and locals.get(head) == self:
134 134 locals[head] = mod
135 135
136 136 for modname in modrefs:
137 137 modref = sys.modules.get(modname, None)
138 138 if modref and getattr(modref, head, None) == self:
139 139 setattr(modref, head, mod)
140 140
141 141 object.__setattr__(self, "_module", mod)
142 142
143 143 def __repr__(self):
144 144 if self._module:
145 145 return "<proxied module '%s'>" % self._data[0]
146 146 return "<unloaded module '%s'>" % self._data[0]
147 147 def __call__(self, *args, **kwargs):
148 148 raise TypeError("%s object is not callable" % repr(self))
149 149 def __getattribute__(self, attr):
150 150 if attr in ('_data', '_extend', '_load', '_module', '_addref'):
151 151 return object.__getattribute__(self, attr)
152 152 self._load()
153 153 return getattr(self._module, attr)
154 154 def __setattr__(self, attr, val):
155 155 self._load()
156 156 setattr(self._module, attr, val)
157 157
158 158 _pypy = '__pypy__' in sys.builtin_module_names
159 159
160 160 def _demandimport(name, globals=None, locals=None, fromlist=None, level=level):
161 161 if not locals or name in ignore or fromlist == ('*',):
162 162 # these cases we can't really delay
163 163 return _hgextimport(_import, name, globals, locals, fromlist, level)
164 164 elif not fromlist:
165 165 # import a [as b]
166 166 if '.' in name: # a.b
167 167 base, rest = name.split('.', 1)
168 168 # email.__init__ loading email.mime
169 169 if globals and globals.get('__name__', None) == base:
170 170 return _import(name, globals, locals, fromlist, level)
171 171 # if a is already demand-loaded, add b to its submodule list
172 172 if base in locals:
173 173 if isinstance(locals[base], _demandmod):
174 174 locals[base]._extend(rest)
175 175 return locals[base]
176 176 return _demandmod(name, globals, locals, level)
177 177 else:
178 178 # There is a fromlist.
179 179 # from a import b,c,d
180 180 # from . import b,c,d
181 181 # from .a import b,c,d
182 182
183 183 # level == -1: relative and absolute attempted (Python 2 only).
184 184 # level >= 0: absolute only (Python 2 w/ absolute_import and Python 3).
185 185 # The modern Mercurial convention is to use absolute_import everywhere,
186 186 # so modern Mercurial code will have level >= 0.
187 187
188 188 # The name of the module the import statement is located in.
189 189 globalname = globals.get('__name__')
190 190
191 191 def processfromitem(mod, attr):
192 192 """Process an imported symbol in the import statement.
193 193
194 If the symbol doesn't exist in the parent module, it must be a
195 module. We set missing modules up as _demandmod instances.
194 If the symbol doesn't exist in the parent module, and if the
195 parent module is a package, it must be a module. We set missing
196 modules up as _demandmod instances.
196 197 """
197 198 symbol = getattr(mod, attr, nothing)
199 nonpkg = getattr(mod, '__path__', nothing) is nothing
198 200 if symbol is nothing:
201 if nonpkg:
202 # do not try relative import, which would raise ValueError
203 raise ImportError('cannot import name %s' % attr)
199 204 mn = '%s.%s' % (mod.__name__, attr)
200 205 if mn in ignore:
201 206 importfunc = _origimport
202 207 else:
203 208 importfunc = _demandmod
204 209 symbol = importfunc(attr, mod.__dict__, locals, level=1)
205 210 setattr(mod, attr, symbol)
206 211
207 212 # Record the importing module references this symbol so we can
208 213 # replace the symbol with the actual module instance at load
209 214 # time.
210 215 if globalname and isinstance(symbol, _demandmod):
211 216 symbol._addref(globalname)
212 217
213 218 def chainmodules(rootmod, modname):
214 219 # recurse down the module chain, and return the leaf module
215 220 mod = rootmod
216 221 for comp in modname.split('.')[1:]:
217 222 if getattr(mod, comp, nothing) is nothing:
218 223 setattr(mod, comp, _demandmod(comp, mod.__dict__,
219 224 mod.__dict__, level=1))
220 225 mod = getattr(mod, comp)
221 226 return mod
222 227
223 228 if level >= 0:
224 229 if name:
225 230 # "from a import b" or "from .a import b" style
226 231 rootmod = _hgextimport(_origimport, name, globals, locals,
227 232 level=level)
228 233 mod = chainmodules(rootmod, name)
229 234 elif _pypy:
230 235 # PyPy's __import__ throws an exception if invoked
231 236 # with an empty name and no fromlist. Recreate the
232 237 # desired behaviour by hand.
233 238 mn = globalname
234 239 mod = sys.modules[mn]
235 240 if getattr(mod, '__path__', nothing) is nothing:
236 241 mn = mn.rsplit('.', 1)[0]
237 242 mod = sys.modules[mn]
238 243 if level > 1:
239 244 mn = mn.rsplit('.', level - 1)[0]
240 245 mod = sys.modules[mn]
241 246 else:
242 247 mod = _hgextimport(_origimport, name, globals, locals,
243 248 level=level)
244 249
245 250 for x in fromlist:
246 251 processfromitem(mod, x)
247 252
248 253 return mod
249 254
250 255 # But, we still need to support lazy loading of standard library and 3rd
251 256 # party modules. So handle level == -1.
252 257 mod = _hgextimport(_origimport, name, globals, locals)
253 258 mod = chainmodules(mod, name)
254 259
255 260 for x in fromlist:
256 261 processfromitem(mod, x)
257 262
258 263 return mod
259 264
260 265 ignore = [
261 266 '__future__',
262 267 '_hashlib',
263 268 # ImportError during pkg_resources/__init__.py:fixup_namespace_package
264 269 '_imp',
265 270 '_xmlplus',
266 271 'fcntl',
267 272 'nt', # pathlib2 tests the existence of built-in 'nt' module
268 273 'win32com.gen_py',
269 274 '_winreg', # 2.7 mimetypes needs immediate ImportError
270 275 'pythoncom',
271 276 # imported by tarfile, not available under Windows
272 277 'pwd',
273 278 'grp',
274 279 # imported by profile, itself imported by hotshot.stats,
275 280 # not available under Windows
276 281 'resource',
277 282 # this trips up many extension authors
278 283 'gtk',
279 284 # setuptools' pkg_resources.py expects "from __main__ import x" to
280 285 # raise ImportError if x not defined
281 286 '__main__',
282 287 '_ssl', # conditional imports in the stdlib, issue1964
283 288 '_sre', # issue4920
284 289 'rfc822',
285 290 'mimetools',
286 291 'sqlalchemy.events', # has import-time side effects (issue5085)
287 292 # setuptools 8 expects this module to explode early when not on windows
288 293 'distutils.msvc9compiler',
289 294 ]
290 295
291 296 if _pypy:
292 297 ignore.extend([
293 298 # _ctypes.pointer is shadowed by "from ... import pointer" (PyPy 5)
294 299 '_ctypes.pointer',
295 300 ])
296 301
297 302 def isenabled():
298 303 return builtins.__import__ == _demandimport
299 304
300 305 def enable():
301 306 "enable global demand-loading of modules"
302 307 if os.environ.get('HGDEMANDIMPORT') != 'disable':
303 308 builtins.__import__ = _demandimport
304 309
305 310 def disable():
306 311 "disable global demand-loading of modules"
307 312 builtins.__import__ = _origimport
308 313
309 314 @contextmanager
310 315 def deactivated():
311 316 "context manager for disabling demandimport in 'with' blocks"
312 317 demandenabled = isenabled()
313 318 if demandenabled:
314 319 disable()
315 320
316 321 try:
317 322 yield
318 323 finally:
319 324 if demandenabled:
320 325 enable()
@@ -1,71 +1,80 b''
1 1 from __future__ import print_function
2 2
3 3 from mercurial import demandimport
4 4 demandimport.enable()
5 5
6 6 import os
7 7 import subprocess
8 8 import sys
9 9
10 10 # Only run if demandimport is allowed
11 11 if subprocess.call(['python', '%s/hghave' % os.environ['TESTDIR'],
12 12 'demandimport']):
13 13 sys.exit(80)
14 14
15 15 if os.name != 'nt':
16 16 try:
17 17 import distutils.msvc9compiler
18 18 print('distutils.msvc9compiler needs to be an immediate '
19 19 'importerror on non-windows platforms')
20 20 distutils.msvc9compiler
21 21 except ImportError:
22 22 pass
23 23
24 24 import re
25 25
26 26 rsub = re.sub
27 27 def f(obj):
28 28 l = repr(obj)
29 29 l = rsub("0x[0-9a-fA-F]+", "0x?", l)
30 30 l = rsub("from '.*'", "from '?'", l)
31 31 l = rsub("'<[a-z]*>'", "'<whatever>'", l)
32 32 return l
33 33
34 34 import os
35 35
36 36 print("os =", f(os))
37 37 print("os.system =", f(os.system))
38 38 print("os =", f(os))
39 39
40 40 from mercurial import util
41 41
42 42 print("util =", f(util))
43 43 print("util.system =", f(util.system))
44 44 print("util =", f(util))
45 45 print("util.system =", f(util.system))
46 46
47 47 from mercurial import hgweb
48 48 print("hgweb =", f(hgweb))
49 49 print("hgweb_mod =", f(hgweb.hgweb_mod))
50 50 print("hgweb =", f(hgweb))
51 51
52 52 import re as fred
53 53 print("fred =", f(fred))
54 54
55 55 import sys as re
56 56 print("re =", f(re))
57 57
58 58 print("fred =", f(fred))
59 59 print("fred.sub =", f(fred.sub))
60 60 print("fred =", f(fred))
61 61
62 62 print("re =", f(re))
63 63 print("re.stderr =", f(re.stderr))
64 64 print("re =", f(re))
65 65
66 import contextlib
67 print("contextlib =", f(contextlib))
68 try:
69 from contextlib import unknownattr
70 print('no demandmod should be created for attribute of non-package '
71 'module:\ncontextlib.unknownattr =', f(unknownattr))
72 except ImportError as inst:
73 print('contextlib.unknownattr = ImportError: %s' % inst)
74
66 75 demandimport.disable()
67 76 os.environ['HGDEMANDIMPORT'] = 'disable'
68 77 # this enable call should not actually enable demandimport!
69 78 demandimport.enable()
70 79 from mercurial import node
71 80 print("node =", f(node))
@@ -1,19 +1,21 b''
1 1 os = <unloaded module 'os'>
2 2 os.system = <built-in function system>
3 3 os = <module 'os' from '?'>
4 4 util = <unloaded module 'util'>
5 5 util.system = <function system at 0x?>
6 6 util = <module 'mercurial.util' from '?'>
7 7 util.system = <function system at 0x?>
8 8 hgweb = <unloaded module 'hgweb'>
9 9 hgweb_mod = <unloaded module 'hgweb_mod'>
10 10 hgweb = <module 'mercurial.hgweb' from '?'>
11 11 fred = <unloaded module 're'>
12 12 re = <unloaded module 'sys'>
13 13 fred = <unloaded module 're'>
14 14 fred.sub = <function sub at 0x?>
15 15 fred = <proxied module 're'>
16 16 re = <unloaded module 'sys'>
17 17 re.stderr = <open file '<whatever>', mode 'w' at 0x?>
18 18 re = <proxied module 'sys'>
19 contextlib = <unloaded module 'contextlib'>
20 contextlib.unknownattr = ImportError: cannot import name unknownattr
19 21 node = <module 'mercurial.node' from '?'>
General Comments 0
You need to be logged in to leave comments. Login now