Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/lazy_importer/_imports.py: 73%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

199 statements  

1"""Implements deferred imports using documented Python protocols.""" 

2 

3from __future__ import annotations 

4 

5import builtins 

6import contextlib 

7import importlib.util 

8import operator 

9import threading 

10import types 

11 

12# Type checkers recognize this flag without importing typing at runtime. 

13TYPE_CHECKING = False 

14if TYPE_CHECKING: 

15 from collections.abc import Callable, Generator, Mapping, Sequence 

16 from typing import Any, Final 

17 

18 

19class _Request: 

20 __slots__ = ("name", "namespace", "fromlist", "attribute", "path") 

21 

22 name: str 

23 namespace: dict[str, Any] 

24 fromlist: tuple[str, ...] 

25 attribute: str | None 

26 path: tuple[str, ...] 

27 

28 def __init__( 

29 self, 

30 name: str, 

31 namespace: dict[str, Any], 

32 fromlist: tuple[str, ...], 

33 attribute: str | None = None, 

34 path: tuple[str, ...] = (), 

35 ) -> None: 

36 object.__setattr__(self, "name", name) 

37 object.__setattr__(self, "namespace", namespace) 

38 object.__setattr__(self, "fromlist", fromlist) 

39 object.__setattr__(self, "attribute", attribute) 

40 object.__setattr__(self, "path", path) 

41 

42 def __setattr__(self, name: str, value: Any) -> None: 

43 raise AttributeError("request is immutable") 

44 

45 def __delattr__(self, name: str) -> None: 

46 raise AttributeError("request is immutable") 

47 

48 def load(self) -> Any: 

49 with _HOOK.suspended(): 

50 module = builtins.__import__( 

51 self.name, self.namespace, self.namespace, self.fromlist, 0 

52 ) 

53 if self.attribute is not None: 

54 try: 

55 return getattr(module, self.attribute) 

56 except AttributeError as exc: 

57 raise ImportError( 

58 f"cannot import name {self.attribute!r} " 

59 f"from {module.__name__!r}" 

60 ) from exc 

61 for component in self.path: 

62 module = getattr(module, component) 

63 return module 

64 

65 

66class _Proxy: 

67 def __init__(self, request: _Request) -> None: 

68 object.__setattr__(self, "_request", request) 

69 object.__setattr__(self, "_children", {}) 

70 object.__setattr__(self, "_namespaces", [request.namespace]) 

71 object.__setattr__(self, "_lock", threading.RLock()) 

72 object.__setattr__(self, "_resolved", ()) 

73 object.__setattr__(self, "_resolving", threading.local()) 

74 

75 def _resolve(self) -> Any: 

76 resolved = object.__getattribute__(self, "_resolved") 

77 if resolved: 

78 return resolved[0] 

79 resolving = object.__getattribute__(self, "_resolving") 

80 request = object.__getattribute__(self, "_request") 

81 if getattr(resolving, "active", False): 

82 raise ImportError(f"circular lazy import of {request.name!r}") 

83 resolving.active = True 

84 try: 

85 value = request.load() 

86 finally: 

87 resolving.active = False 

88 # Never hold a proxy lock while running import hooks or module code: 

89 # another thread importing that module may need this same proxy. 

90 with object.__getattribute__(self, "_lock"): 

91 resolved = object.__getattribute__(self, "_resolved") 

92 if resolved: 

93 return resolved[0] 

94 object.__setattr__(self, "_resolved", (value,)) 

95 if isinstance(value, types.ModuleType): 

96 for name, child in object.__getattribute__(self, "_children").items(): 

97 if name not in vars(value): 

98 vars(value)[name] = child 

99 object.__getattribute__(child, "_namespaces").append( 

100 vars(value) 

101 ) 

102 for namespace in object.__getattribute__(self, "_namespaces"): 

103 for name, binding in tuple(namespace.items()): 

104 if binding is self and namespace.get(name) is self: 

105 namespace[name] = value 

106 return value 

107 

108 def __getattribute__(self, name: str) -> Any: 

109 if name == "__mro_entries__": 

110 return object.__getattribute__(self, name) 

111 resolved = object.__getattribute__(self, "_resolved") 

112 if resolved: 

113 return getattr(resolved[0], name) 

114 children = object.__getattribute__(self, "_children") 

115 if name in children: 

116 return children[name] 

117 return getattr(_Proxy._resolve(self), name) 

118 

119 def __setattr__(self, name: str, value: Any) -> None: 

120 setattr(_Proxy._resolve(self), name, value) 

121 

122 def __delattr__(self, name: str) -> None: 

123 delattr(_Proxy._resolve(self), name) 

124 

125 def __call__(self, *args: Any, **kwargs: Any) -> Any: 

126 target = _Proxy._resolve(self) 

127 args = tuple(resolve(arg) for arg in args) 

128 kwargs = {name: resolve(value) for name, value in kwargs.items()} 

129 return target(*args, **kwargs) 

130 

131 def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: 

132 return (_Proxy._resolve(self),) 

133 

134 

135# Special methods are looked up on the type, bypassing __getattribute__. 

136# Keep the supported protocol surface explicit, including reflected operators. 

137_PROTOCOLS: Final = ( 

138 "repr", 

139 "str", 

140 "bytes", 

141 "format", 

142 "bool", 

143 "hash", 

144 "len", 

145 "iter", 

146 "next", 

147 "reversed", 

148 "contains", 

149 "getitem", 

150 "setitem", 

151 "delitem", 

152 "enter", 

153 "exit", 

154 "await", 

155 "aiter", 

156 "anext", 

157 "aenter", 

158 "aexit", 

159 "int", 

160 "float", 

161 "complex", 

162 "index", 

163 "round", 

164 "trunc", 

165 "floor", 

166 "ceil", 

167 "neg", 

168 "pos", 

169 "abs", 

170 "invert", 

171 "lt", 

172 "le", 

173 "eq", 

174 "ne", 

175 "gt", 

176 "ge", 

177 "add", 

178 "sub", 

179 "mul", 

180 "matmul", 

181 "truediv", 

182 "floordiv", 

183 "mod", 

184 "divmod", 

185 "pow", 

186 "lshift", 

187 "rshift", 

188 "and", 

189 "xor", 

190 "or", 

191 "radd", 

192 "rsub", 

193 "rmul", 

194 "rmatmul", 

195 "rtruediv", 

196 "rfloordiv", 

197 "rmod", 

198 "rdivmod", 

199 "rpow", 

200 "rlshift", 

201 "rrshift", 

202 "rand", 

203 "rxor", 

204 "ror", 

205 "iadd", 

206 "isub", 

207 "imul", 

208 "imatmul", 

209 "itruediv", 

210 "ifloordiv", 

211 "imod", 

212 "ipow", 

213 "ilshift", 

214 "irshift", 

215 "iand", 

216 "ixor", 

217 "ior", 

218) 

219 

220 

221_OPERATIONS: Final[Mapping[str, Callable[..., Any]]] = { 

222 "__bool__": bool, 

223 "__str__": str, 

224 "__repr__": repr, 

225 "__bytes__": bytes, 

226 "__format__": format, 

227 "__hash__": hash, 

228 "__len__": len, 

229 "__iter__": iter, 

230 "__next__": next, 

231 "__reversed__": reversed, 

232 "__int__": int, 

233 "__float__": float, 

234 "__complex__": complex, 

235 "__round__": round, 

236 "__abs__": abs, 

237 "__contains__": operator.contains, 

238 "__pow__": pow, 

239 "__divmod__": divmod, 

240} 

241 

242 

243_REFLECTED: Final[Mapping[str, str]] = { 

244 f"__r{name}__": f"__{name}__" 

245 for name in ( 

246 "add", 

247 "sub", 

248 "mul", 

249 "matmul", 

250 "truediv", 

251 "floordiv", 

252 "mod", 

253 "divmod", 

254 "pow", 

255 "lshift", 

256 "rshift", 

257 "and", 

258 "xor", 

259 "or", 

260 ) 

261} 

262 

263 

264def _forward(method: str) -> Callable[..., Any]: 

265 def operation(self: _Proxy, *args: Any, **kwargs: Any) -> Any: 

266 value = _Proxy._resolve(self) 

267 args = tuple(resolve(arg) for arg in args) 

268 operation = _OPERATIONS.get(method) 

269 if operation is not None: 

270 return operation(value, *args, **kwargs) 

271 operation = getattr(operator, method, None) 

272 if operation is not None: 

273 return operation(value, *args, **kwargs) 

274 if method in _REFLECTED: 

275 operation = _OPERATIONS.get(_REFLECTED[method]) 

276 if operation is None: 

277 operation = getattr(operator, _REFLECTED[method]) 

278 return operation(args[0], value, *args[1:], **kwargs) 

279 return getattr(type(value), method)(value, *args, **kwargs) 

280 

281 return operation 

282 

283 

284for _protocol in _PROTOCOLS: 

285 setattr(_Proxy, f"__{_protocol}__", _forward(f"__{_protocol}__")) 

286 

287 

288def resolve(value: Any, /) -> Any: 

289 """Returns the imported object, or the argument if it isn't a lazy proxy. 

290 

291 Resolves fallback proxies explicitly for identity checks, type checks and 

292 APIs that require concrete objects. Failed imports can be retried. 

293 """ 

294 if type(value) is _Proxy: 

295 return _Proxy._resolve(value) 

296 return value 

297 

298 

299class _FromImport: 

300 def __init__(self, request: _Request) -> None: 

301 self._request = request 

302 

303 def __getattribute__(self, name: str) -> _Proxy: 

304 request = object.__getattribute__(self, "_request") 

305 return _Proxy( 

306 _Request( 

307 request.name, 

308 request.namespace, 

309 (name,), 

310 attribute=name, 

311 path=request.path, 

312 ) 

313 ) 

314 

315 

316class _ThreadState(threading.local): 

317 def __init__(self) -> None: 

318 self.depth = 0 

319 self.suspensions = 0 

320 

321 

322class _ImportHook: 

323 def __init__(self) -> None: 

324 self._lock = threading.RLock() 

325 self._state = _ThreadState() 

326 self._users = 0 

327 self._previous: Callable[..., Any] = builtins.__import__ 

328 # Store the bound method so installation and restoration use identity. 

329 self._callback = self._import 

330 

331 @contextlib.contextmanager 

332 def suspended(self) -> Generator[None, None, None]: 

333 self._state.suspensions += 1 

334 try: 

335 yield 

336 finally: 

337 self._state.suspensions -= 1 

338 

339 @contextlib.contextmanager 

340 def activate(self) -> Generator[None, None, None]: 

341 with self._lock: 

342 if self._users == 0: 

343 self._previous = builtins.__import__ 

344 vars(builtins)["__import__"] = self._callback 

345 self._users += 1 

346 self._state.depth += 1 

347 try: 

348 yield 

349 finally: 

350 self._state.depth -= 1 

351 with self._lock: 

352 self._users -= 1 

353 if self._users == 0 and builtins.__import__ is self._callback: 

354 builtins.__import__ = self._previous 

355 

356 def _import( 

357 self, 

358 name: str, 

359 globals: dict[str, Any] | None = None, 

360 locals: Mapping[str, Any] | None = None, 

361 fromlist: Sequence[str] | None = (), 

362 level: int = 0, 

363 ) -> Any: 

364 state = self._state 

365 if ( 

366 state.depth 

367 and not state.suspensions 

368 and globals is not None 

369 and "*" not in (fromlist or ()) 

370 and name != "__future__" 

371 ): 

372 absolute = name 

373 if level: 

374 package = globals.get("__package__") 

375 if package is None: 

376 spec = globals.get("__spec__") 

377 if spec is not None: 

378 package = spec.parent 

379 else: 

380 package = globals.get("__name__", "") 

381 if "__path__" not in globals: 

382 package = package.rpartition(".")[0] 

383 absolute = importlib.util.resolve_name("." * level + name, package) 

384 # Membership may run user code that imports its own dependencies. 

385 with self.suspended(): 

386 selected = absolute in globals.get("__lazy_modules__", ()) 

387 if selected: 

388 request = _Request(absolute, globals, tuple(fromlist or ())) 

389 if fromlist: 

390 return _FromImport(request) 

391 existing = globals.get(name.partition(".")[0]) 

392 root = _Proxy(request) 

393 if type(existing) is _Proxy: 

394 object.__getattribute__(root, "_children").update( 

395 object.__getattribute__(existing, "_children") 

396 ) 

397 parent = root 

398 components = name.split(".") 

399 for index, component in enumerate(components[1:], 1): 

400 children = object.__getattribute__(parent, "_children") 

401 child = _Proxy( 

402 _Request( 

403 request.name, 

404 request.namespace, 

405 request.fromlist, 

406 attribute=request.attribute, 

407 path=tuple(components[1 : index + 1]), 

408 ) 

409 ) 

410 if component in children: 

411 object.__getattribute__(child, "_children").update( 

412 object.__getattribute__(children[component], "_children") 

413 ) 

414 object.__getattribute__(parent, "_children")[component] = child 

415 parent = child 

416 return root 

417 with self.suspended(): 

418 return self._previous(name, globals, locals, fromlist, level) 

419 

420 

421_HOOK: Final = _ImportHook() 

422 

423 

424def lazy_importer() -> contextlib.AbstractContextManager[None]: 

425 """Returns a context that enables the importing module's __lazy_modules__. 

426 

427 Each import checks membership in its module's current declaration. The 

428 fallback context is single-use and applies to imports on the current thread. 

429 Module-level blocks containing only imports avoid differences from native 

430 lazy imports described in the README. 

431 """ 

432 return _HOOK.activate()