##// END OF EJS Templates
demandimport: ensure lazyloaderex sets loader attributes (issue6725)...
Jason R. Coombs -
r50483:7236f11d default
parent child Browse files
Show More
@@ -1,172 +1,176
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 import contextlib
27 27 import importlib.util
28 28 import sys
29 29
30 30 from . import tracing
31 31
32 32 _deactivated = False
33 33
34 34
35 35 class _lazyloaderex(importlib.util.LazyLoader):
36 36 """This is a LazyLoader except it also follows the _deactivated global and
37 37 the ignore list.
38 38 """
39 39
40 40 def exec_module(self, module):
41 41 """Make the module load lazily."""
42 42 with tracing.log('demandimport %s', module):
43 43 if _deactivated or module.__name__ in ignores:
44 # Reset the loader on the module as super() does (issue6725)
45 module.__spec__.loader = self.loader
46 module.__loader__ = self.loader
47
44 48 self.loader.exec_module(module)
45 49 else:
46 50 super().exec_module(module)
47 51
48 52
49 53 class LazyFinder:
50 54 """A wrapper around a ``MetaPathFinder`` that makes loaders lazy.
51 55
52 56 ``sys.meta_path`` finders have their ``find_spec()`` called to locate a
53 57 module. This returns a ``ModuleSpec`` if found or ``None``. The
54 58 ``ModuleSpec`` has a ``loader`` attribute, which is called to actually
55 59 load a module.
56 60
57 61 Our class wraps an existing finder and overloads its ``find_spec()`` to
58 62 replace the ``loader`` with our lazy loader proxy.
59 63
60 64 We have to use __getattribute__ to proxy the instance because some meta
61 65 path finders don't support monkeypatching.
62 66 """
63 67
64 68 __slots__ = ("_finder",)
65 69
66 70 def __init__(self, finder):
67 71 object.__setattr__(self, "_finder", finder)
68 72
69 73 def __repr__(self):
70 74 return "<LazyFinder for %r>" % object.__getattribute__(self, "_finder")
71 75
72 76 # __bool__ is canonical Python 3. But check-code insists on __nonzero__ being
73 77 # defined via `def`.
74 78 def __nonzero__(self):
75 79 return bool(object.__getattribute__(self, "_finder"))
76 80
77 81 __bool__ = __nonzero__
78 82
79 83 def __getattribute__(self, name):
80 84 if name in ("_finder", "find_spec"):
81 85 return object.__getattribute__(self, name)
82 86
83 87 return getattr(object.__getattribute__(self, "_finder"), name)
84 88
85 89 def __delattr__(self, name):
86 90 return delattr(object.__getattribute__(self, "_finder"))
87 91
88 92 def __setattr__(self, name, value):
89 93 return setattr(object.__getattribute__(self, "_finder"), name, value)
90 94
91 95 def find_spec(self, fullname, path, target=None):
92 96 finder = object.__getattribute__(self, "_finder")
93 97 try:
94 98 find_spec = finder.find_spec
95 99 except AttributeError:
96 100 loader = finder.find_module(fullname, path)
97 101 if loader is None:
98 102 spec = None
99 103 else:
100 104 spec = importlib.util.spec_from_loader(fullname, loader)
101 105 else:
102 106 spec = find_spec(fullname, path, target)
103 107
104 108 # Lazy loader requires exec_module().
105 109 if (
106 110 spec is not None
107 111 and spec.loader is not None
108 112 and getattr(spec.loader, "exec_module", None)
109 113 ):
110 114 spec.loader = _lazyloaderex(spec.loader)
111 115
112 116 return spec
113 117
114 118
115 119 ignores = set()
116 120
117 121
118 122 def init(ignoreset):
119 123 global ignores
120 124 ignores = ignoreset
121 125
122 126
123 127 def isenabled():
124 128 return not _deactivated and any(
125 129 isinstance(finder, LazyFinder) for finder in sys.meta_path
126 130 )
127 131
128 132
129 133 def disable():
130 134 new_finders = []
131 135 for finder in sys.meta_path:
132 136 new_finders.append(
133 137 finder._finder if isinstance(finder, LazyFinder) else finder
134 138 )
135 139 sys.meta_path[:] = new_finders
136 140
137 141
138 142 def enable():
139 143 new_finders = []
140 144 for finder in sys.meta_path:
141 145 new_finders.append(
142 146 LazyFinder(finder) if not isinstance(finder, LazyFinder) else finder
143 147 )
144 148 sys.meta_path[:] = new_finders
145 149
146 150
147 151 @contextlib.contextmanager
148 152 def deactivated():
149 153 # This implementation is a bit different from Python 2's. Python 3
150 154 # maintains a per-package finder cache in sys.path_importer_cache (see
151 155 # PEP 302). This means that we can't just call disable + enable.
152 156 # If we do that, in situations like:
153 157 #
154 158 # demandimport.enable()
155 159 # ...
156 160 # from foo.bar import mod1
157 161 # with demandimport.deactivated():
158 162 # from foo.bar import mod2
159 163 #
160 164 # mod2 will be imported lazily. (The converse also holds -- whatever finder
161 165 # first gets cached will be used.)
162 166 #
163 167 # Instead, have a global flag the LazyLoader can use.
164 168 global _deactivated
165 169 demandenabled = isenabled()
166 170 if demandenabled:
167 171 _deactivated = True
168 172 try:
169 173 yield
170 174 finally:
171 175 if demandenabled:
172 176 _deactivated = False
@@ -1,236 +1,244
1 1 from mercurial import demandimport
2 2
3 3 demandimport.enable()
4 4
5 5 import os
6 6 import subprocess
7 7 import sys
8 8 import types
9 9
10 10 # Don't import pycompat because it has too many side-effects.
11 11 ispy3 = sys.version_info[0] >= 3
12 12
13 13 # Only run if demandimport is allowed
14 14 if subprocess.call(
15 15 [os.environ['PYTHON'], '%s/hghave' % os.environ['TESTDIR'], 'demandimport']
16 16 ):
17 17 sys.exit(80)
18 18
19 19 # We rely on assert, which gets optimized out.
20 20 if sys.flags.optimize:
21 21 sys.exit(80)
22 22
23 23 # The demand importer doesn't work on Python 3.5.
24 24 if sys.version_info[0:2] == (3, 5):
25 25 sys.exit(80)
26 26
27 27 if ispy3:
28 28 from importlib.util import _LazyModule
29 29
30 30 try:
31 31 from importlib.util import _Module as moduletype
32 32 except ImportError:
33 33 moduletype = types.ModuleType
34 34 else:
35 35 moduletype = types.ModuleType
36 36
37 37 if os.name != 'nt':
38 38 try:
39 39 import distutils.msvc9compiler
40 40
41 41 print(
42 42 'distutils.msvc9compiler needs to be an immediate '
43 43 'importerror on non-windows platforms'
44 44 )
45 45 distutils.msvc9compiler
46 46 except ImportError:
47 47 pass
48 48
49 49 import re
50 50
51 51 rsub = re.sub
52 52
53 53
54 54 def f(obj):
55 55 l = repr(obj)
56 56 l = rsub("0x[0-9a-fA-F]+", "0x?", l)
57 57 l = rsub("from '.*'", "from '?'", l)
58 58 l = rsub("'<[a-z]*>'", "'<whatever>'", l)
59 59 return l
60 60
61 61
62 62 demandimport.disable()
63 63 os.environ['HGDEMANDIMPORT'] = 'disable'
64 64 # this enable call should not actually enable demandimport!
65 65 demandimport.enable()
66 66 from mercurial import node
67 67
68 68 # We use assert instead of a unittest test case because having imports inside
69 69 # functions changes behavior of the demand importer.
70 70 if ispy3:
71 71 assert not isinstance(node, _LazyModule)
72 72 else:
73 73 assert f(node) == "<module 'mercurial.node' from '?'>", f(node)
74 74
75 75 # now enable it for real
76 76 del os.environ['HGDEMANDIMPORT']
77 77 demandimport.enable()
78 78
79 79 # Test access to special attributes through demandmod proxy
80 80 assert 'mercurial.error' not in sys.modules
81 81 from mercurial import error as errorproxy
82 82
83 83 if ispy3:
84 84 # unsure why this isn't lazy.
85 85 assert not isinstance(f, _LazyModule)
86 86 assert f(errorproxy) == "<module 'mercurial.error' from '?'>", f(errorproxy)
87 87 else:
88 88 assert f(errorproxy) == "<unloaded module 'error'>", f(errorproxy)
89 89
90 90 doc = ' '.join(errorproxy.__doc__.split()[:3])
91 91 assert doc == 'Mercurial exceptions. This', doc
92 92 assert errorproxy.__name__ == 'mercurial.error', errorproxy.__name__
93 93
94 94 # __name__ must be accessible via __dict__ so the relative imports can be
95 95 # resolved
96 96 name = errorproxy.__dict__['__name__']
97 97 assert name == 'mercurial.error', name
98 98
99 99 if ispy3:
100 100 assert not isinstance(errorproxy, _LazyModule)
101 101 assert f(errorproxy) == "<module 'mercurial.error' from '?'>", f(errorproxy)
102 102 else:
103 103 assert f(errorproxy) == "<proxied module 'error'>", f(errorproxy)
104 104
105 105 import os
106 106
107 107 if ispy3:
108 108 assert not isinstance(os, _LazyModule)
109 109 assert f(os) == "<module 'os' from '?'>", f(os)
110 110 else:
111 111 assert f(os) == "<unloaded module 'os'>", f(os)
112 112
113 113 assert f(os.system) == '<built-in function system>', f(os.system)
114 114 assert f(os) == "<module 'os' from '?'>", f(os)
115 115
116 116 assert 'mercurial.utils.procutil' not in sys.modules
117 117 from mercurial.utils import procutil
118 118
119 119 if ispy3:
120 120 assert isinstance(procutil, _LazyModule)
121 121 assert f(procutil) == "<module 'mercurial.utils.procutil' from '?'>", f(
122 122 procutil
123 123 )
124 124 else:
125 125 assert f(procutil) == "<unloaded module 'procutil'>", f(procutil)
126 126
127 127 assert f(procutil.system) == '<function system at 0x?>', f(procutil.system)
128 128 assert procutil.__class__ == moduletype, procutil.__class__
129 129 assert f(procutil) == "<module 'mercurial.utils.procutil' from '?'>", f(
130 130 procutil
131 131 )
132 132 assert f(procutil.system) == '<function system at 0x?>', f(procutil.system)
133 133
134 134 assert 'mercurial.hgweb' not in sys.modules
135 135 from mercurial import hgweb
136 136
137 137 if ispy3:
138 138 assert isinstance(hgweb, _LazyModule)
139 139 assert f(hgweb) == "<module 'mercurial.hgweb' from '?'>", f(hgweb)
140 140 assert isinstance(hgweb.hgweb_mod, _LazyModule)
141 141 assert (
142 142 f(hgweb.hgweb_mod) == "<module 'mercurial.hgweb.hgweb_mod' from '?'>"
143 143 ), f(hgweb.hgweb_mod)
144 144 else:
145 145 assert f(hgweb) == "<unloaded module 'hgweb'>", f(hgweb)
146 146 assert f(hgweb.hgweb_mod) == "<unloaded module 'hgweb_mod'>", f(
147 147 hgweb.hgweb_mod
148 148 )
149 149
150 150 assert f(hgweb) == "<module 'mercurial.hgweb' from '?'>", f(hgweb)
151 151
152 152 import re as fred
153 153
154 154 if ispy3:
155 155 assert not isinstance(fred, _LazyModule)
156 156 assert f(fred) == "<module 're' from '?'>"
157 157 else:
158 158 assert f(fred) == "<unloaded module 're'>", f(fred)
159 159
160 160 import re as remod
161 161
162 162 if ispy3:
163 163 assert not isinstance(remod, _LazyModule)
164 164 assert f(remod) == "<module 're' from '?'>"
165 165 else:
166 166 assert f(remod) == "<unloaded module 're'>", f(remod)
167 167
168 168 import sys as re
169 169
170 170 if ispy3:
171 171 assert not isinstance(re, _LazyModule)
172 172 assert f(re) == "<module 'sys' (built-in)>"
173 173 else:
174 174 assert f(re) == "<unloaded module 'sys'>", f(re)
175 175
176 176 if ispy3:
177 177 assert not isinstance(fred, _LazyModule)
178 178 assert f(fred) == "<module 're' from '?'>", f(fred)
179 179 else:
180 180 assert f(fred) == "<unloaded module 're'>", f(fred)
181 181
182 182 assert f(fred.sub) == '<function sub at 0x?>', f(fred.sub)
183 183
184 184 if ispy3:
185 185 assert not isinstance(fred, _LazyModule)
186 186 assert f(fred) == "<module 're' from '?'>", f(fred)
187 187 else:
188 188 assert f(fred) == "<proxied module 're'>", f(fred)
189 189
190 190 remod.escape # use remod
191 191 assert f(remod) == "<module 're' from '?'>", f(remod)
192 192
193 193 if ispy3:
194 194 assert not isinstance(re, _LazyModule)
195 195 assert f(re) == "<module 'sys' (built-in)>"
196 196 assert f(type(re.stderr)) == "<class '_io.TextIOWrapper'>", f(
197 197 type(re.stderr)
198 198 )
199 199 assert f(re) == "<module 'sys' (built-in)>"
200 200 else:
201 201 assert f(re) == "<unloaded module 'sys'>", f(re)
202 202 assert f(re.stderr) == "<open file '<whatever>', mode 'w' at 0x?>", f(
203 203 re.stderr
204 204 )
205 205 assert f(re) == "<proxied module 'sys'>", f(re)
206 206
207 207 assert 'telnetlib' not in sys.modules
208 208 import telnetlib
209 209
210 210 if ispy3:
211 211 assert isinstance(telnetlib, _LazyModule)
212 212 assert f(telnetlib) == "<module 'telnetlib' from '?'>"
213 213 else:
214 214 assert f(telnetlib) == "<unloaded module 'telnetlib'>", f(telnetlib)
215 215
216 216 try:
217 217 from telnetlib import unknownattr
218 218
219 219 assert False, (
220 220 'no demandmod should be created for attribute of non-package '
221 221 'module:\ntelnetlib.unknownattr = %s' % f(unknownattr)
222 222 )
223 223 except ImportError as inst:
224 224 assert rsub(r"'", '', str(inst)).startswith(
225 225 'cannot import name unknownattr'
226 226 )
227 227
228 228 from mercurial import util
229 229
230 230 # Unlike the import statement, __import__() function should not raise
231 231 # ImportError even if fromlist has an unknown item
232 232 # (see Python/import.c:import_module_level() and ensure_fromlist())
233 233 assert 'ftplib' not in sys.modules
234 234 zipfileimp = __import__('ftplib', globals(), locals(), ['unknownattr'])
235 235 assert f(zipfileimp) == "<module 'ftplib' from '?'>", f(zipfileimp)
236 236 assert not util.safehasattr(zipfileimp, 'unknownattr')
237
238
239 # test deactivation for issue6725
240 del sys.modules['telnetlib']
241 with demandimport.deactivated():
242 import telnetlib
243 assert telnetlib.__loader__ == telnetlib.__spec__.loader
244 assert telnetlib.__loader__.get_resource_reader
General Comments 0
You need to be logged in to leave comments. Login now