##// END OF EJS Templates
demandimport: fix a crash in LazyFinder.__delattr__...
Matt Harbison -
r51054:48e38b17 stable
parent child Browse files
Show More
@@ -1,174 +1,174 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
28 28 import contextlib
29 29 import importlib.util
30 30 import sys
31 31
32 32 from . import tracing
33 33
34 34 _deactivated = False
35 35
36 36
37 37 class _lazyloaderex(importlib.util.LazyLoader):
38 38 """This is a LazyLoader except it also follows the _deactivated global and
39 39 the ignore list.
40 40 """
41 41
42 42 def exec_module(self, module):
43 43 """Make the module load lazily."""
44 44 with tracing.log('demandimport %s', module):
45 45 if _deactivated or module.__name__ in ignores:
46 46 self.loader.exec_module(module)
47 47 else:
48 48 super().exec_module(module)
49 49
50 50
51 51 class LazyFinder:
52 52 """A wrapper around a ``MetaPathFinder`` that makes loaders lazy.
53 53
54 54 ``sys.meta_path`` finders have their ``find_spec()`` called to locate a
55 55 module. This returns a ``ModuleSpec`` if found or ``None``. The
56 56 ``ModuleSpec`` has a ``loader`` attribute, which is called to actually
57 57 load a module.
58 58
59 59 Our class wraps an existing finder and overloads its ``find_spec()`` to
60 60 replace the ``loader`` with our lazy loader proxy.
61 61
62 62 We have to use __getattribute__ to proxy the instance because some meta
63 63 path finders don't support monkeypatching.
64 64 """
65 65
66 66 __slots__ = ("_finder",)
67 67
68 68 def __init__(self, finder):
69 69 object.__setattr__(self, "_finder", finder)
70 70
71 71 def __repr__(self):
72 72 return "<LazyFinder for %r>" % object.__getattribute__(self, "_finder")
73 73
74 74 # __bool__ is canonical Python 3. But check-code insists on __nonzero__ being
75 75 # defined via `def`.
76 76 def __nonzero__(self):
77 77 return bool(object.__getattribute__(self, "_finder"))
78 78
79 79 __bool__ = __nonzero__
80 80
81 81 def __getattribute__(self, name):
82 82 if name in ("_finder", "find_spec"):
83 83 return object.__getattribute__(self, name)
84 84
85 85 return getattr(object.__getattribute__(self, "_finder"), name)
86 86
87 87 def __delattr__(self, name):
88 return delattr(object.__getattribute__(self, "_finder"))
88 return delattr(object.__getattribute__(self, "_finder"), name)
89 89
90 90 def __setattr__(self, name, value):
91 91 return setattr(object.__getattribute__(self, "_finder"), name, value)
92 92
93 93 def find_spec(self, fullname, path, target=None):
94 94 finder = object.__getattribute__(self, "_finder")
95 95 try:
96 96 find_spec = finder.find_spec
97 97 except AttributeError:
98 98 loader = finder.find_module(fullname, path)
99 99 if loader is None:
100 100 spec = None
101 101 else:
102 102 spec = importlib.util.spec_from_loader(fullname, loader)
103 103 else:
104 104 spec = find_spec(fullname, path, target)
105 105
106 106 # Lazy loader requires exec_module().
107 107 if (
108 108 spec is not None
109 109 and spec.loader is not None
110 110 and getattr(spec.loader, "exec_module", None)
111 111 ):
112 112 spec.loader = _lazyloaderex(spec.loader)
113 113
114 114 return spec
115 115
116 116
117 117 ignores = set()
118 118
119 119
120 120 def init(ignoreset):
121 121 global ignores
122 122 ignores = ignoreset
123 123
124 124
125 125 def isenabled():
126 126 return not _deactivated and any(
127 127 isinstance(finder, LazyFinder) for finder in sys.meta_path
128 128 )
129 129
130 130
131 131 def disable():
132 132 new_finders = []
133 133 for finder in sys.meta_path:
134 134 new_finders.append(
135 135 finder._finder if isinstance(finder, LazyFinder) else finder
136 136 )
137 137 sys.meta_path[:] = new_finders
138 138
139 139
140 140 def enable():
141 141 new_finders = []
142 142 for finder in sys.meta_path:
143 143 new_finders.append(
144 144 LazyFinder(finder) if not isinstance(finder, LazyFinder) else finder
145 145 )
146 146 sys.meta_path[:] = new_finders
147 147
148 148
149 149 @contextlib.contextmanager
150 150 def deactivated():
151 151 # This implementation is a bit different from Python 2's. Python 3
152 152 # maintains a per-package finder cache in sys.path_importer_cache (see
153 153 # PEP 302). This means that we can't just call disable + enable.
154 154 # If we do that, in situations like:
155 155 #
156 156 # demandimport.enable()
157 157 # ...
158 158 # from foo.bar import mod1
159 159 # with demandimport.deactivated():
160 160 # from foo.bar import mod2
161 161 #
162 162 # mod2 will be imported lazily. (The converse also holds -- whatever finder
163 163 # first gets cached will be used.)
164 164 #
165 165 # Instead, have a global flag the LazyLoader can use.
166 166 global _deactivated
167 167 demandenabled = isenabled()
168 168 if demandenabled:
169 169 _deactivated = True
170 170 try:
171 171 yield
172 172 finally:
173 173 if demandenabled:
174 174 _deactivated = False
General Comments 0
You need to be logged in to leave comments. Login now