##// END OF EJS Templates
hgdemandimport: disable on Python 3.5...
Gregory Szorc -
r44576:c5e0a9b9 default
parent child Browse files
Show More
@@ -1,128 +1,131
1 # demandimportpy3 - global demand-loading of modules for Mercurial
1 # demandimportpy3 - global demand-loading of modules for Mercurial
2 #
2 #
3 # Copyright 2017 Facebook Inc.
3 # Copyright 2017 Facebook Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Lazy loading for Python 3.6 and above.
8 """Lazy loading for Python 3.6 and above.
9
9
10 This uses the new importlib finder/loader functionality available in Python 3.5
10 This uses the new importlib finder/loader functionality available in Python 3.5
11 and up. The code reuses most of the mechanics implemented inside importlib.util,
11 and up. The code reuses most of the mechanics implemented inside importlib.util,
12 but with a few additions:
12 but with a few additions:
13
13
14 * Allow excluding certain modules from lazy imports.
14 * Allow excluding certain modules from lazy imports.
15 * Expose an interface that's substantially the same as demandimport for
15 * Expose an interface that's substantially the same as demandimport for
16 Python 2.
16 Python 2.
17
17
18 This also has some limitations compared to the Python 2 implementation:
18 This also has some limitations compared to the Python 2 implementation:
19
19
20 * Much of the logic is per-package, not per-module, so any packages loaded
20 * Much of the logic is per-package, not per-module, so any packages loaded
21 before demandimport is enabled will not be lazily imported in the future. In
21 before demandimport is enabled will not be lazily imported in the future. In
22 practice, we only expect builtins to be loaded before demandimport is
22 practice, we only expect builtins to be loaded before demandimport is
23 enabled.
23 enabled.
24 """
24 """
25
25
26 # This line is unnecessary, but it satisfies test-check-py3-compat.t.
26 # This line is unnecessary, but it satisfies test-check-py3-compat.t.
27 from __future__ import absolute_import
27 from __future__ import absolute_import
28
28
29 import contextlib
29 import contextlib
30 import importlib.abc
30 import importlib.abc
31 import importlib.machinery
31 import importlib.machinery
32 import importlib.util
32 import importlib.util
33 import sys
33 import sys
34
34
35 from . import tracing
35 from . import tracing
36
36
37 _deactivated = False
37 _deactivated = False
38
38
39 # Python 3.5's LazyLoader doesn't work for some reason.
40 # https://bugs.python.org/issue26186 is a known issue with extension
41 # importing. But it appears to not have a meaningful effect with
42 # Mercurial.
43 _supported = sys.version_info[0:2] >= (3, 6)
44
39
45
40 class _lazyloaderex(importlib.util.LazyLoader):
46 class _lazyloaderex(importlib.util.LazyLoader):
41 """This is a LazyLoader except it also follows the _deactivated global and
47 """This is a LazyLoader except it also follows the _deactivated global and
42 the ignore list.
48 the ignore list.
43 """
49 """
44
50
45 def exec_module(self, module):
51 def exec_module(self, module):
46 """Make the module load lazily."""
52 """Make the module load lazily."""
47 with tracing.log('demandimport %s', module):
53 with tracing.log('demandimport %s', module):
48 if _deactivated or module.__name__ in ignores:
54 if _deactivated or module.__name__ in ignores:
49 self.loader.exec_module(module)
55 self.loader.exec_module(module)
50 else:
56 else:
51 super().exec_module(module)
57 super().exec_module(module)
52
58
53
59
54 # This is 3.6+ because with Python 3.5 it isn't possible to lazily load
55 # extensions. See the discussion in https://bugs.python.org/issue26186 for more.
56 if sys.version_info[0:2] >= (3, 6):
57 _extensions_loader = _lazyloaderex.factory(
60 _extensions_loader = _lazyloaderex.factory(
58 importlib.machinery.ExtensionFileLoader
61 importlib.machinery.ExtensionFileLoader
59 )
62 )
60 else:
61 _extensions_loader = importlib.machinery.ExtensionFileLoader
62
63 _bytecode_loader = _lazyloaderex.factory(
63 _bytecode_loader = _lazyloaderex.factory(
64 importlib.machinery.SourcelessFileLoader
64 importlib.machinery.SourcelessFileLoader
65 )
65 )
66 _source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
66 _source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
67
67
68
68
69 def _makefinder(path):
69 def _makefinder(path):
70 return importlib.machinery.FileFinder(
70 return importlib.machinery.FileFinder(
71 path,
71 path,
72 # This is the order in which loaders are passed in in core Python.
72 # This is the order in which loaders are passed in in core Python.
73 (_extensions_loader, importlib.machinery.EXTENSION_SUFFIXES),
73 (_extensions_loader, importlib.machinery.EXTENSION_SUFFIXES),
74 (_source_loader, importlib.machinery.SOURCE_SUFFIXES),
74 (_source_loader, importlib.machinery.SOURCE_SUFFIXES),
75 (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
75 (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
76 )
76 )
77
77
78
78
79 ignores = set()
79 ignores = set()
80
80
81
81
82 def init(ignoreset):
82 def init(ignoreset):
83 global ignores
83 global ignores
84 ignores = ignoreset
84 ignores = ignoreset
85
85
86
86
87 def isenabled():
87 def isenabled():
88 return _makefinder in sys.path_hooks and not _deactivated
88 return _makefinder in sys.path_hooks and not _deactivated
89
89
90
90
91 def disable():
91 def disable():
92 try:
92 try:
93 while True:
93 while True:
94 sys.path_hooks.remove(_makefinder)
94 sys.path_hooks.remove(_makefinder)
95 except ValueError:
95 except ValueError:
96 pass
96 pass
97
97
98
98
99 def enable():
99 def enable():
100 if not _supported:
101 return
102
100 sys.path_hooks.insert(0, _makefinder)
103 sys.path_hooks.insert(0, _makefinder)
101
104
102
105
103 @contextlib.contextmanager
106 @contextlib.contextmanager
104 def deactivated():
107 def deactivated():
105 # This implementation is a bit different from Python 2's. Python 3
108 # This implementation is a bit different from Python 2's. Python 3
106 # maintains a per-package finder cache in sys.path_importer_cache (see
109 # maintains a per-package finder cache in sys.path_importer_cache (see
107 # PEP 302). This means that we can't just call disable + enable.
110 # PEP 302). This means that we can't just call disable + enable.
108 # If we do that, in situations like:
111 # If we do that, in situations like:
109 #
112 #
110 # demandimport.enable()
113 # demandimport.enable()
111 # ...
114 # ...
112 # from foo.bar import mod1
115 # from foo.bar import mod1
113 # with demandimport.deactivated():
116 # with demandimport.deactivated():
114 # from foo.bar import mod2
117 # from foo.bar import mod2
115 #
118 #
116 # mod2 will be imported lazily. (The converse also holds -- whatever finder
119 # mod2 will be imported lazily. (The converse also holds -- whatever finder
117 # first gets cached will be used.)
120 # first gets cached will be used.)
118 #
121 #
119 # Instead, have a global flag the LazyLoader can use.
122 # Instead, have a global flag the LazyLoader can use.
120 global _deactivated
123 global _deactivated
121 demandenabled = isenabled()
124 demandenabled = isenabled()
122 if demandenabled:
125 if demandenabled:
123 _deactivated = True
126 _deactivated = True
124 try:
127 try:
125 yield
128 yield
126 finally:
129 finally:
127 if demandenabled:
130 if demandenabled:
128 _deactivated = False
131 _deactivated = False
@@ -1,234 +1,238
1 from __future__ import absolute_import, print_function
1 from __future__ import absolute_import, print_function
2
2
3 from mercurial import demandimport
3 from mercurial import demandimport
4
4
5 demandimport.enable()
5 demandimport.enable()
6
6
7 import os
7 import os
8 import subprocess
8 import subprocess
9 import sys
9 import sys
10 import types
10 import types
11
11
12 # Don't import pycompat because it has too many side-effects.
12 # Don't import pycompat because it has too many side-effects.
13 ispy3 = sys.version_info[0] >= 3
13 ispy3 = sys.version_info[0] >= 3
14
14
15 # Only run if demandimport is allowed
15 # Only run if demandimport is allowed
16 if subprocess.call(
16 if subprocess.call(
17 ['python', '%s/hghave' % os.environ['TESTDIR'], 'demandimport']
17 ['python', '%s/hghave' % os.environ['TESTDIR'], 'demandimport']
18 ):
18 ):
19 sys.exit(80)
19 sys.exit(80)
20
20
21 # We rely on assert, which gets optimized out.
21 # We rely on assert, which gets optimized out.
22 if sys.flags.optimize:
22 if sys.flags.optimize:
23 sys.exit(80)
23 sys.exit(80)
24
24
25 # The demand importer doesn't work on Python 3.5.
26 if sys.version_info[0:2] == (3, 5):
27 sys.exit(80)
28
25 if ispy3:
29 if ispy3:
26 from importlib.util import _LazyModule
30 from importlib.util import _LazyModule
27
31
28 try:
32 try:
29 from importlib.util import _Module as moduletype
33 from importlib.util import _Module as moduletype
30 except ImportError:
34 except ImportError:
31 moduletype = types.ModuleType
35 moduletype = types.ModuleType
32 else:
36 else:
33 moduletype = types.ModuleType
37 moduletype = types.ModuleType
34
38
35 if os.name != 'nt':
39 if os.name != 'nt':
36 try:
40 try:
37 import distutils.msvc9compiler
41 import distutils.msvc9compiler
38
42
39 print(
43 print(
40 'distutils.msvc9compiler needs to be an immediate '
44 'distutils.msvc9compiler needs to be an immediate '
41 'importerror on non-windows platforms'
45 'importerror on non-windows platforms'
42 )
46 )
43 distutils.msvc9compiler
47 distutils.msvc9compiler
44 except ImportError:
48 except ImportError:
45 pass
49 pass
46
50
47 import re
51 import re
48
52
49 rsub = re.sub
53 rsub = re.sub
50
54
51
55
52 def f(obj):
56 def f(obj):
53 l = repr(obj)
57 l = repr(obj)
54 l = rsub("0x[0-9a-fA-F]+", "0x?", l)
58 l = rsub("0x[0-9a-fA-F]+", "0x?", l)
55 l = rsub("from '.*'", "from '?'", l)
59 l = rsub("from '.*'", "from '?'", l)
56 l = rsub("'<[a-z]*>'", "'<whatever>'", l)
60 l = rsub("'<[a-z]*>'", "'<whatever>'", l)
57 return l
61 return l
58
62
59
63
60 demandimport.disable()
64 demandimport.disable()
61 os.environ['HGDEMANDIMPORT'] = 'disable'
65 os.environ['HGDEMANDIMPORT'] = 'disable'
62 # this enable call should not actually enable demandimport!
66 # this enable call should not actually enable demandimport!
63 demandimport.enable()
67 demandimport.enable()
64 from mercurial import node
68 from mercurial import node
65
69
66 # We use assert instead of a unittest test case because having imports inside
70 # We use assert instead of a unittest test case because having imports inside
67 # functions changes behavior of the demand importer.
71 # functions changes behavior of the demand importer.
68 if ispy3:
72 if ispy3:
69 assert not isinstance(node, _LazyModule)
73 assert not isinstance(node, _LazyModule)
70 else:
74 else:
71 assert f(node) == "<module 'mercurial.node' from '?'>", f(node)
75 assert f(node) == "<module 'mercurial.node' from '?'>", f(node)
72
76
73 # now enable it for real
77 # now enable it for real
74 del os.environ['HGDEMANDIMPORT']
78 del os.environ['HGDEMANDIMPORT']
75 demandimport.enable()
79 demandimport.enable()
76
80
77 # Test access to special attributes through demandmod proxy
81 # Test access to special attributes through demandmod proxy
78 assert 'mercurial.error' not in sys.modules
82 assert 'mercurial.error' not in sys.modules
79 from mercurial import error as errorproxy
83 from mercurial import error as errorproxy
80
84
81 if ispy3:
85 if ispy3:
82 # unsure why this isn't lazy.
86 # unsure why this isn't lazy.
83 assert not isinstance(f, _LazyModule)
87 assert not isinstance(f, _LazyModule)
84 assert f(errorproxy) == "<module 'mercurial.error' from '?'>", f(errorproxy)
88 assert f(errorproxy) == "<module 'mercurial.error' from '?'>", f(errorproxy)
85 else:
89 else:
86 assert f(errorproxy) == "<unloaded module 'error'>", f(errorproxy)
90 assert f(errorproxy) == "<unloaded module 'error'>", f(errorproxy)
87
91
88 doc = ' '.join(errorproxy.__doc__.split()[:3])
92 doc = ' '.join(errorproxy.__doc__.split()[:3])
89 assert doc == 'Mercurial exceptions. This', doc
93 assert doc == 'Mercurial exceptions. This', doc
90 assert errorproxy.__name__ == 'mercurial.error', errorproxy.__name__
94 assert errorproxy.__name__ == 'mercurial.error', errorproxy.__name__
91
95
92 # __name__ must be accessible via __dict__ so the relative imports can be
96 # __name__ must be accessible via __dict__ so the relative imports can be
93 # resolved
97 # resolved
94 name = errorproxy.__dict__['__name__']
98 name = errorproxy.__dict__['__name__']
95 assert name == 'mercurial.error', name
99 assert name == 'mercurial.error', name
96
100
97 if ispy3:
101 if ispy3:
98 assert not isinstance(errorproxy, _LazyModule)
102 assert not isinstance(errorproxy, _LazyModule)
99 assert f(errorproxy) == "<module 'mercurial.error' from '?'>", f(errorproxy)
103 assert f(errorproxy) == "<module 'mercurial.error' from '?'>", f(errorproxy)
100 else:
104 else:
101 assert f(errorproxy) == "<proxied module 'error'>", f(errorproxy)
105 assert f(errorproxy) == "<proxied module 'error'>", f(errorproxy)
102
106
103 import os
107 import os
104
108
105 if ispy3:
109 if ispy3:
106 assert not isinstance(os, _LazyModule)
110 assert not isinstance(os, _LazyModule)
107 assert f(os) == "<module 'os' from '?'>", f(os)
111 assert f(os) == "<module 'os' from '?'>", f(os)
108 else:
112 else:
109 assert f(os) == "<unloaded module 'os'>", f(os)
113 assert f(os) == "<unloaded module 'os'>", f(os)
110
114
111 assert f(os.system) == '<built-in function system>', f(os.system)
115 assert f(os.system) == '<built-in function system>', f(os.system)
112 assert f(os) == "<module 'os' from '?'>", f(os)
116 assert f(os) == "<module 'os' from '?'>", f(os)
113
117
114 assert 'mercurial.utils.procutil' not in sys.modules
118 assert 'mercurial.utils.procutil' not in sys.modules
115 from mercurial.utils import procutil
119 from mercurial.utils import procutil
116
120
117 if ispy3:
121 if ispy3:
118 assert isinstance(procutil, _LazyModule)
122 assert isinstance(procutil, _LazyModule)
119 assert f(procutil) == "<module 'mercurial.utils.procutil' from '?'>", f(
123 assert f(procutil) == "<module 'mercurial.utils.procutil' from '?'>", f(
120 procutil
124 procutil
121 )
125 )
122 else:
126 else:
123 assert f(procutil) == "<unloaded module 'procutil'>", f(procutil)
127 assert f(procutil) == "<unloaded module 'procutil'>", f(procutil)
124
128
125 assert f(procutil.system) == '<function system at 0x?>', f(procutil.system)
129 assert f(procutil.system) == '<function system at 0x?>', f(procutil.system)
126 assert procutil.__class__ == moduletype, procutil.__class__
130 assert procutil.__class__ == moduletype, procutil.__class__
127 assert f(procutil) == "<module 'mercurial.utils.procutil' from '?'>", f(
131 assert f(procutil) == "<module 'mercurial.utils.procutil' from '?'>", f(
128 procutil
132 procutil
129 )
133 )
130 assert f(procutil.system) == '<function system at 0x?>', f(procutil.system)
134 assert f(procutil.system) == '<function system at 0x?>', f(procutil.system)
131
135
132 assert 'mercurial.hgweb' not in sys.modules
136 assert 'mercurial.hgweb' not in sys.modules
133 from mercurial import hgweb
137 from mercurial import hgweb
134
138
135 if ispy3:
139 if ispy3:
136 assert not isinstance(hgweb, _LazyModule)
140 assert not isinstance(hgweb, _LazyModule)
137 assert f(hgweb) == "<module 'mercurial.hgweb' from '?'>", f(hgweb)
141 assert f(hgweb) == "<module 'mercurial.hgweb' from '?'>", f(hgweb)
138 assert isinstance(hgweb.hgweb_mod, _LazyModule)
142 assert isinstance(hgweb.hgweb_mod, _LazyModule)
139 assert (
143 assert (
140 f(hgweb.hgweb_mod) == "<module 'mercurial.hgweb.hgweb_mod' from '?'>"
144 f(hgweb.hgweb_mod) == "<module 'mercurial.hgweb.hgweb_mod' from '?'>"
141 ), f(hgweb.hgweb_mod)
145 ), f(hgweb.hgweb_mod)
142 else:
146 else:
143 assert f(hgweb) == "<unloaded module 'hgweb'>", f(hgweb)
147 assert f(hgweb) == "<unloaded module 'hgweb'>", f(hgweb)
144 assert f(hgweb.hgweb_mod) == "<unloaded module 'hgweb_mod'>", f(
148 assert f(hgweb.hgweb_mod) == "<unloaded module 'hgweb_mod'>", f(
145 hgweb.hgweb_mod
149 hgweb.hgweb_mod
146 )
150 )
147
151
148 assert f(hgweb) == "<module 'mercurial.hgweb' from '?'>", f(hgweb)
152 assert f(hgweb) == "<module 'mercurial.hgweb' from '?'>", f(hgweb)
149
153
150 import re as fred
154 import re as fred
151
155
152 if ispy3:
156 if ispy3:
153 assert not isinstance(fred, _LazyModule)
157 assert not isinstance(fred, _LazyModule)
154 assert f(fred) == "<module 're' from '?'>"
158 assert f(fred) == "<module 're' from '?'>"
155 else:
159 else:
156 assert f(fred) == "<unloaded module 're'>", f(fred)
160 assert f(fred) == "<unloaded module 're'>", f(fred)
157
161
158 import re as remod
162 import re as remod
159
163
160 if ispy3:
164 if ispy3:
161 assert not isinstance(remod, _LazyModule)
165 assert not isinstance(remod, _LazyModule)
162 assert f(remod) == "<module 're' from '?'>"
166 assert f(remod) == "<module 're' from '?'>"
163 else:
167 else:
164 assert f(remod) == "<unloaded module 're'>", f(remod)
168 assert f(remod) == "<unloaded module 're'>", f(remod)
165
169
166 import sys as re
170 import sys as re
167
171
168 if ispy3:
172 if ispy3:
169 assert not isinstance(re, _LazyModule)
173 assert not isinstance(re, _LazyModule)
170 assert f(re) == "<module 'sys' (built-in)>"
174 assert f(re) == "<module 'sys' (built-in)>"
171 else:
175 else:
172 assert f(re) == "<unloaded module 'sys'>", f(re)
176 assert f(re) == "<unloaded module 'sys'>", f(re)
173
177
174 if ispy3:
178 if ispy3:
175 assert not isinstance(fred, _LazyModule)
179 assert not isinstance(fred, _LazyModule)
176 assert f(fred) == "<module 're' from '?'>", f(fred)
180 assert f(fred) == "<module 're' from '?'>", f(fred)
177 else:
181 else:
178 assert f(fred) == "<unloaded module 're'>", f(fred)
182 assert f(fred) == "<unloaded module 're'>", f(fred)
179
183
180 assert f(fred.sub) == '<function sub at 0x?>', f(fred.sub)
184 assert f(fred.sub) == '<function sub at 0x?>', f(fred.sub)
181
185
182 if ispy3:
186 if ispy3:
183 assert not isinstance(fred, _LazyModule)
187 assert not isinstance(fred, _LazyModule)
184 assert f(fred) == "<module 're' from '?'>", f(fred)
188 assert f(fred) == "<module 're' from '?'>", f(fred)
185 else:
189 else:
186 assert f(fred) == "<proxied module 're'>", f(fred)
190 assert f(fred) == "<proxied module 're'>", f(fred)
187
191
188 remod.escape # use remod
192 remod.escape # use remod
189 assert f(remod) == "<module 're' from '?'>", f(remod)
193 assert f(remod) == "<module 're' from '?'>", f(remod)
190
194
191 if ispy3:
195 if ispy3:
192 assert not isinstance(re, _LazyModule)
196 assert not isinstance(re, _LazyModule)
193 assert f(re) == "<module 'sys' (built-in)>"
197 assert f(re) == "<module 'sys' (built-in)>"
194 assert f(type(re.stderr)) == "<class '_io.TextIOWrapper'>", f(
198 assert f(type(re.stderr)) == "<class '_io.TextIOWrapper'>", f(
195 type(re.stderr)
199 type(re.stderr)
196 )
200 )
197 assert f(re) == "<module 'sys' (built-in)>"
201 assert f(re) == "<module 'sys' (built-in)>"
198 else:
202 else:
199 assert f(re) == "<unloaded module 'sys'>", f(re)
203 assert f(re) == "<unloaded module 'sys'>", f(re)
200 assert f(re.stderr) == "<open file '<whatever>', mode 'w' at 0x?>", f(
204 assert f(re.stderr) == "<open file '<whatever>', mode 'w' at 0x?>", f(
201 re.stderr
205 re.stderr
202 )
206 )
203 assert f(re) == "<proxied module 'sys'>", f(re)
207 assert f(re) == "<proxied module 'sys'>", f(re)
204
208
205 assert 'telnetlib' not in sys.modules
209 assert 'telnetlib' not in sys.modules
206 import telnetlib
210 import telnetlib
207
211
208 if ispy3:
212 if ispy3:
209 assert not isinstance(telnetlib, _LazyModule)
213 assert not isinstance(telnetlib, _LazyModule)
210 assert f(telnetlib) == "<module 'telnetlib' from '?'>"
214 assert f(telnetlib) == "<module 'telnetlib' from '?'>"
211 else:
215 else:
212 assert f(telnetlib) == "<unloaded module 'telnetlib'>", f(telnetlib)
216 assert f(telnetlib) == "<unloaded module 'telnetlib'>", f(telnetlib)
213
217
214 try:
218 try:
215 from telnetlib import unknownattr
219 from telnetlib import unknownattr
216
220
217 assert False, (
221 assert False, (
218 'no demandmod should be created for attribute of non-package '
222 'no demandmod should be created for attribute of non-package '
219 'module:\ntelnetlib.unknownattr = %s' % f(unknownattr)
223 'module:\ntelnetlib.unknownattr = %s' % f(unknownattr)
220 )
224 )
221 except ImportError as inst:
225 except ImportError as inst:
222 assert rsub(r"'", '', str(inst)).startswith(
226 assert rsub(r"'", '', str(inst)).startswith(
223 'cannot import name unknownattr'
227 'cannot import name unknownattr'
224 )
228 )
225
229
226 from mercurial import util
230 from mercurial import util
227
231
228 # Unlike the import statement, __import__() function should not raise
232 # Unlike the import statement, __import__() function should not raise
229 # ImportError even if fromlist has an unknown item
233 # ImportError even if fromlist has an unknown item
230 # (see Python/import.c:import_module_level() and ensure_fromlist())
234 # (see Python/import.c:import_module_level() and ensure_fromlist())
231 assert 'zipfile' not in sys.modules
235 assert 'zipfile' not in sys.modules
232 zipfileimp = __import__('zipfile', globals(), locals(), ['unknownattr'])
236 zipfileimp = __import__('zipfile', globals(), locals(), ['unknownattr'])
233 assert f(zipfileimp) == "<module 'zipfile' from '?'>", f(zipfileimp)
237 assert f(zipfileimp) == "<module 'zipfile' from '?'>", f(zipfileimp)
234 assert not util.safehasattr(zipfileimp, 'unknownattr')
238 assert not util.safehasattr(zipfileimp, 'unknownattr')
General Comments 0
You need to be logged in to leave comments. Login now