##// END OF EJS Templates
demandimport: move HGDEMANDIMPORT test to __init__.py...
Jun Wu -
r33861:8fb52126 default
parent child Browse files
Show More
@@ -1,73 +1,77 b''
1 1 # hgdemandimport - global demand-loading of modules for Mercurial
2 2 #
3 3 # Copyright 2017 Facebook Inc.
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 '''demandimport - automatic demand-loading of modules'''
9 9
10 10 # This is in a separate package from mercurial because in Python 3,
11 11 # demand loading is per-package. Keeping demandimport in the mercurial package
12 12 # would disable demand loading for any modules in mercurial.
13 13
14 14 from __future__ import absolute_import
15 15
16 import os
16 17 import sys
17 18
18 19 if sys.version_info[0] >= 3:
19 20 from . import demandimportpy3 as demandimport
20 21 else:
21 22 from . import demandimportpy2 as demandimport
22 23
23 24 # Extensions can add to this list if necessary.
24 25 ignore = [
25 26 '__future__',
26 27 '_hashlib',
27 28 # ImportError during pkg_resources/__init__.py:fixup_namespace_package
28 29 '_imp',
29 30 '_xmlplus',
30 31 'fcntl',
31 32 'nt', # pathlib2 tests the existence of built-in 'nt' module
32 33 'win32com.gen_py',
33 34 'win32com.shell', # 'appdirs' tries to import win32com.shell
34 35 '_winreg', # 2.7 mimetypes needs immediate ImportError
35 36 'pythoncom',
36 37 # imported by tarfile, not available under Windows
37 38 'pwd',
38 39 'grp',
39 40 # imported by profile, itself imported by hotshot.stats,
40 41 # not available under Windows
41 42 'resource',
42 43 # this trips up many extension authors
43 44 'gtk',
44 45 # setuptools' pkg_resources.py expects "from __main__ import x" to
45 46 # raise ImportError if x not defined
46 47 '__main__',
47 48 '_ssl', # conditional imports in the stdlib, issue1964
48 49 '_sre', # issue4920
49 50 'rfc822',
50 51 'mimetools',
51 52 'sqlalchemy.events', # has import-time side effects (issue5085)
52 53 # setuptools 8 expects this module to explode early when not on windows
53 54 'distutils.msvc9compiler',
54 55 '__builtin__',
55 56 'builtins',
56 57 'urwid.command_map', # for pudb
57 58 ]
58 59
59 60 _pypy = '__pypy__' in sys.builtin_module_names
60 61
61 62 if _pypy:
62 63 ignore.extend([
63 64 # _ctypes.pointer is shadowed by "from ... import pointer" (PyPy 5)
64 65 '_ctypes.pointer',
65 66 ])
66 67
67 68 demandimport.init(ignore)
68 69
69 70 # Re-export.
70 71 isenabled = demandimport.isenabled
71 enable = demandimport.enable
72 72 disable = demandimport.disable
73 73 deactivated = demandimport.deactivated
74
75 def enable():
76 if os.environ.get('HGDEMANDIMPORT') != 'disable':
77 demandimport.enable()
@@ -1,306 +1,304 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 __builtin__ as builtins
30 30 import contextlib
31 import os
32 31 import sys
33 32
34 33 contextmanager = contextlib.contextmanager
35 34
36 35 _origimport = __import__
37 36
38 37 nothing = object()
39 38
40 39 def _hgextimport(importfunc, name, globals, *args, **kwargs):
41 40 try:
42 41 return importfunc(name, globals, *args, **kwargs)
43 42 except ImportError:
44 43 if not globals:
45 44 raise
46 45 # extensions are loaded with "hgext_" prefix
47 46 hgextname = 'hgext_%s' % name
48 47 nameroot = hgextname.split('.', 1)[0]
49 48 contextroot = globals.get('__name__', '').split('.', 1)[0]
50 49 if nameroot != contextroot:
51 50 raise
52 51 # retry to import with "hgext_" prefix
53 52 return importfunc(hgextname, globals, *args, **kwargs)
54 53
55 54 class _demandmod(object):
56 55 """module demand-loader and proxy
57 56
58 57 Specify 1 as 'level' argument at construction, to import module
59 58 relatively.
60 59 """
61 60
62 61 def __init__(self, name, globals, locals, level):
63 62 if '.' in name:
64 63 head, rest = name.split('.', 1)
65 64 after = [rest]
66 65 else:
67 66 head = name
68 67 after = []
69 68 object.__setattr__(self, r"_data",
70 69 (head, globals, locals, after, level, set()))
71 70 object.__setattr__(self, r"_module", None)
72 71
73 72 def _extend(self, name):
74 73 """add to the list of submodules to load"""
75 74 self._data[3].append(name)
76 75
77 76 def _addref(self, name):
78 77 """Record that the named module ``name`` imports this module.
79 78
80 79 References to this proxy class having the name of this module will be
81 80 replaced at module load time. We assume the symbol inside the importing
82 81 module is identical to the "head" name of this module. We don't
83 82 actually know if "as X" syntax is being used to change the symbol name
84 83 because this information isn't exposed to __import__.
85 84 """
86 85 self._data[5].add(name)
87 86
88 87 def _load(self):
89 88 if not self._module:
90 89 head, globals, locals, after, level, modrefs = self._data
91 90 mod = _hgextimport(_origimport, head, globals, locals, None, level)
92 91 if mod is self:
93 92 # In this case, _hgextimport() above should imply
94 93 # _demandimport(). Otherwise, _hgextimport() never
95 94 # returns _demandmod. This isn't intentional behavior,
96 95 # in fact. (see also issue5304 for detail)
97 96 #
98 97 # If self._module is already bound at this point, self
99 98 # should be already _load()-ed while _hgextimport().
100 99 # Otherwise, there is no way to import actual module
101 100 # as expected, because (re-)invoking _hgextimport()
102 101 # should cause same result.
103 102 # This is reason why _load() returns without any more
104 103 # setup but assumes self to be already bound.
105 104 mod = self._module
106 105 assert mod and mod is not self, "%s, %s" % (self, mod)
107 106 return
108 107
109 108 # load submodules
110 109 def subload(mod, p):
111 110 h, t = p, None
112 111 if '.' in p:
113 112 h, t = p.split('.', 1)
114 113 if getattr(mod, h, nothing) is nothing:
115 114 setattr(mod, h, _demandmod(p, mod.__dict__, mod.__dict__,
116 115 level=1))
117 116 elif t:
118 117 subload(getattr(mod, h), t)
119 118
120 119 for x in after:
121 120 subload(mod, x)
122 121
123 122 # Replace references to this proxy instance with the actual module.
124 123 if locals:
125 124 if locals.get(head) is self:
126 125 locals[head] = mod
127 126 elif locals.get(head + r'mod') is self:
128 127 locals[head + r'mod'] = mod
129 128
130 129 for modname in modrefs:
131 130 modref = sys.modules.get(modname, None)
132 131 if modref and getattr(modref, head, None) is self:
133 132 setattr(modref, head, mod)
134 133
135 134 object.__setattr__(self, r"_module", mod)
136 135
137 136 def __repr__(self):
138 137 if self._module:
139 138 return "<proxied module '%s'>" % self._data[0]
140 139 return "<unloaded module '%s'>" % self._data[0]
141 140
142 141 def __call__(self, *args, **kwargs):
143 142 raise TypeError("%s object is not callable" % repr(self))
144 143
145 144 def __getattr__(self, attr):
146 145 self._load()
147 146 return getattr(self._module, attr)
148 147
149 148 def __setattr__(self, attr, val):
150 149 self._load()
151 150 setattr(self._module, attr, val)
152 151
153 152 @property
154 153 def __dict__(self):
155 154 self._load()
156 155 return self._module.__dict__
157 156
158 157 @property
159 158 def __doc__(self):
160 159 self._load()
161 160 return self._module.__doc__
162 161
163 162 _pypy = '__pypy__' in sys.builtin_module_names
164 163
165 164 def _demandimport(name, globals=None, locals=None, fromlist=None, level=-1):
166 165 if locals is None or name in ignore or fromlist == ('*',):
167 166 # these cases we can't really delay
168 167 return _hgextimport(_origimport, name, globals, locals, fromlist, level)
169 168 elif not fromlist:
170 169 # import a [as b]
171 170 if '.' in name: # a.b
172 171 base, rest = name.split('.', 1)
173 172 # email.__init__ loading email.mime
174 173 if globals and globals.get('__name__', None) == base:
175 174 return _origimport(name, globals, locals, fromlist, level)
176 175 # if a is already demand-loaded, add b to its submodule list
177 176 if base in locals:
178 177 if isinstance(locals[base], _demandmod):
179 178 locals[base]._extend(rest)
180 179 return locals[base]
181 180 return _demandmod(name, globals, locals, level)
182 181 else:
183 182 # There is a fromlist.
184 183 # from a import b,c,d
185 184 # from . import b,c,d
186 185 # from .a import b,c,d
187 186
188 187 # level == -1: relative and absolute attempted (Python 2 only).
189 188 # level >= 0: absolute only (Python 2 w/ absolute_import and Python 3).
190 189 # The modern Mercurial convention is to use absolute_import everywhere,
191 190 # so modern Mercurial code will have level >= 0.
192 191
193 192 # The name of the module the import statement is located in.
194 193 globalname = globals.get('__name__')
195 194
196 195 def processfromitem(mod, attr):
197 196 """Process an imported symbol in the import statement.
198 197
199 198 If the symbol doesn't exist in the parent module, and if the
200 199 parent module is a package, it must be a module. We set missing
201 200 modules up as _demandmod instances.
202 201 """
203 202 symbol = getattr(mod, attr, nothing)
204 203 nonpkg = getattr(mod, '__path__', nothing) is nothing
205 204 if symbol is nothing:
206 205 if nonpkg:
207 206 # do not try relative import, which would raise ValueError,
208 207 # and leave unknown attribute as the default __import__()
209 208 # would do. the missing attribute will be detected later
210 209 # while processing the import statement.
211 210 return
212 211 mn = '%s.%s' % (mod.__name__, attr)
213 212 if mn in ignore:
214 213 importfunc = _origimport
215 214 else:
216 215 importfunc = _demandmod
217 216 symbol = importfunc(attr, mod.__dict__, locals, level=1)
218 217 setattr(mod, attr, symbol)
219 218
220 219 # Record the importing module references this symbol so we can
221 220 # replace the symbol with the actual module instance at load
222 221 # time.
223 222 if globalname and isinstance(symbol, _demandmod):
224 223 symbol._addref(globalname)
225 224
226 225 def chainmodules(rootmod, modname):
227 226 # recurse down the module chain, and return the leaf module
228 227 mod = rootmod
229 228 for comp in modname.split('.')[1:]:
230 229 obj = getattr(mod, comp, nothing)
231 230 if obj is nothing:
232 231 obj = _demandmod(comp, mod.__dict__, mod.__dict__, level=1)
233 232 setattr(mod, comp, obj)
234 233 elif mod.__name__ + '.' + comp in sys.modules:
235 234 # prefer loaded module over attribute (issue5617)
236 235 obj = sys.modules[mod.__name__ + '.' + comp]
237 236 mod = obj
238 237 return mod
239 238
240 239 if level >= 0:
241 240 if name:
242 241 # "from a import b" or "from .a import b" style
243 242 rootmod = _hgextimport(_origimport, name, globals, locals,
244 243 level=level)
245 244 mod = chainmodules(rootmod, name)
246 245 elif _pypy:
247 246 # PyPy's __import__ throws an exception if invoked
248 247 # with an empty name and no fromlist. Recreate the
249 248 # desired behaviour by hand.
250 249 mn = globalname
251 250 mod = sys.modules[mn]
252 251 if getattr(mod, '__path__', nothing) is nothing:
253 252 mn = mn.rsplit('.', 1)[0]
254 253 mod = sys.modules[mn]
255 254 if level > 1:
256 255 mn = mn.rsplit('.', level - 1)[0]
257 256 mod = sys.modules[mn]
258 257 else:
259 258 mod = _hgextimport(_origimport, name, globals, locals,
260 259 level=level)
261 260
262 261 for x in fromlist:
263 262 processfromitem(mod, x)
264 263
265 264 return mod
266 265
267 266 # But, we still need to support lazy loading of standard library and 3rd
268 267 # party modules. So handle level == -1.
269 268 mod = _hgextimport(_origimport, name, globals, locals)
270 269 mod = chainmodules(mod, name)
271 270
272 271 for x in fromlist:
273 272 processfromitem(mod, x)
274 273
275 274 return mod
276 275
277 276 ignore = []
278 277
279 278 def init(ignorelist):
280 279 global ignore
281 280 ignore = ignorelist
282 281
283 282 def isenabled():
284 283 return builtins.__import__ == _demandimport
285 284
286 285 def enable():
287 286 "enable global demand-loading of modules"
288 if os.environ.get('HGDEMANDIMPORT') != 'disable':
289 builtins.__import__ = _demandimport
287 builtins.__import__ = _demandimport
290 288
291 289 def disable():
292 290 "disable global demand-loading of modules"
293 291 builtins.__import__ = _origimport
294 292
295 293 @contextmanager
296 294 def deactivated():
297 295 "context manager for disabling demandimport in 'with' blocks"
298 296 demandenabled = isenabled()
299 297 if demandenabled:
300 298 disable()
301 299
302 300 try:
303 301 yield
304 302 finally:
305 303 if demandenabled:
306 304 enable()
@@ -1,112 +1,110 b''
1 1 # demandimportpy3 - global demand-loading of modules for Mercurial
2 2 #
3 3 # Copyright 2017 Facebook Inc.
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 """Lazy loading for Python 3.6 and above.
9 9
10 10 This uses the new importlib finder/loader functionality available in Python 3.5
11 11 and up. The code reuses most of the mechanics implemented inside importlib.util,
12 12 but with a few additions:
13 13
14 14 * Allow excluding certain modules from lazy imports.
15 15 * Expose an interface that's substantially the same as demandimport for
16 16 Python 2.
17 17
18 18 This also has some limitations compared to the Python 2 implementation:
19 19
20 20 * Much of the logic is per-package, not per-module, so any packages loaded
21 21 before demandimport is enabled will not be lazily imported in the future. In
22 22 practice, we only expect builtins to be loaded before demandimport is
23 23 enabled.
24 24 """
25 25
26 26 # This line is unnecessary, but it satisfies test-check-py3-compat.t.
27 27 from __future__ import absolute_import
28 28
29 29 import contextlib
30 import os
31 30 import sys
32 31
33 32 import importlib.abc
34 33 import importlib.machinery
35 34 import importlib.util
36 35
37 36 _deactivated = False
38 37
39 38 class _lazyloaderex(importlib.util.LazyLoader):
40 39 """This is a LazyLoader except it also follows the _deactivated global and
41 40 the ignore list.
42 41 """
43 42 def exec_module(self, module):
44 43 """Make the module load lazily."""
45 44 if _deactivated or module.__name__ in ignore:
46 45 self.loader.exec_module(module)
47 46 else:
48 47 super().exec_module(module)
49 48
50 49 # This is 3.6+ because with Python 3.5 it isn't possible to lazily load
51 50 # extensions. See the discussion in https://python.org/sf/26186 for more.
52 51 _extensions_loader = _lazyloaderex.factory(
53 52 importlib.machinery.ExtensionFileLoader)
54 53 _bytecode_loader = _lazyloaderex.factory(
55 54 importlib.machinery.SourcelessFileLoader)
56 55 _source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
57 56
58 57 def _makefinder(path):
59 58 return importlib.machinery.FileFinder(
60 59 path,
61 60 # This is the order in which loaders are passed in in core Python.
62 61 (_extensions_loader, importlib.machinery.EXTENSION_SUFFIXES),
63 62 (_source_loader, importlib.machinery.SOURCE_SUFFIXES),
64 63 (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
65 64 )
66 65
67 66 ignore = []
68 67
69 68 def init(ignorelist):
70 69 global ignore
71 70 ignore = ignorelist
72 71
73 72 def isenabled():
74 73 return _makefinder in sys.path_hooks and not _deactivated
75 74
76 75 def disable():
77 76 try:
78 77 while True:
79 78 sys.path_hooks.remove(_makefinder)
80 79 except ValueError:
81 80 pass
82 81
83 82 def enable():
84 if os.environ.get('HGDEMANDIMPORT') != 'disable':
85 sys.path_hooks.insert(0, _makefinder)
83 sys.path_hooks.insert(0, _makefinder)
86 84
87 85 @contextlib.contextmanager
88 86 def deactivated():
89 87 # This implementation is a bit different from Python 2's. Python 3
90 88 # maintains a per-package finder cache in sys.path_importer_cache (see
91 89 # PEP 302). This means that we can't just call disable + enable.
92 90 # If we do that, in situations like:
93 91 #
94 92 # demandimport.enable()
95 93 # ...
96 94 # from foo.bar import mod1
97 95 # with demandimport.deactivated():
98 96 # from foo.bar import mod2
99 97 #
100 98 # mod2 will be imported lazily. (The converse also holds -- whatever finder
101 99 # first gets cached will be used.)
102 100 #
103 101 # Instead, have a global flag the LazyLoader can use.
104 102 global _deactivated
105 103 demandenabled = isenabled()
106 104 if demandenabled:
107 105 _deactivated = True
108 106 try:
109 107 yield
110 108 finally:
111 109 if demandenabled:
112 110 _deactivated = False
General Comments 0
You need to be logged in to leave comments. Login now