comparison hgdemandimport/demandimportpy2.py @ 32420:0906b85bf222

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