omlish 0.0.0.dev458__py3-none-any.whl → 0.0.0.dev460__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,27 @@
1
1
  """
2
2
  TODO:
3
+ - _ProxyImports
3
4
  - if already imported just return?
5
+ - no, need sub-imports..
6
+ - seal on first use? or just per module? can't seal roots and still be usable
7
+ - only if not hasattr?
8
+ - audit for deadlock risk - does importlib._bootstrap do it for us? do we need a global _ProxyImporter lock? would only
9
+ happen on reification
10
+ - ProxyImportError
11
+
12
+ See:
13
+ - https://peps.python.org/pep-0810/
14
+ - https://github.com/LazyImportsCabal/cpython/tree/lazy
15
+ - https://developers.facebook.com/blog/post/2022/06/15/python-lazy-imports-with-cinder/
16
+ - https://engineering.fb.com/2024/01/18/developer-tools/lazy-imports-cinder-machine-learning-meta/
17
+ - https://www.hudsonrivertrading.com/hrtbeat/inside-hrts-python-fork/
18
+ - https://bugreports.qt.io/browse/PYSIDE-2404
19
+ - https://scientific-python.org/specs/spec-0001/
20
+ - https://github.com/scientific-python/lazy-loader
4
21
  """
5
- import contextlib
6
22
  import functools
7
23
  import importlib.util
24
+ import threading
8
25
  import types
9
26
  import typing as ta
10
27
 
@@ -13,138 +30,340 @@ from .capture import ImportCapture
13
30
  from .capture import _new_import_capture_hook
14
31
 
15
32
 
33
+ _ProxyImporterModuleAttr: ta.TypeAlias = ta.Literal[
34
+ 'child', # 'outranks' proxy_attr - all child attrs must be proxy_attrs but not vice versa
35
+ 'proxy_attr',
36
+ 'pending_child',
37
+ 'pending_attr',
38
+ ]
39
+
40
+
16
41
  ##
17
42
 
18
43
 
19
- def proxy_import(
20
- spec: str,
21
- package: str | None = None,
22
- extras: ta.Iterable[str] | None = None,
23
- ) -> types.ModuleType:
24
- if isinstance(extras, str):
25
- raise TypeError(extras)
44
+ class _ProxyImporter:
45
+ def __init__(
46
+ self,
47
+ *,
48
+ owner_globals: ta.MutableMapping[str, ta.Any] | None = None,
49
+ ) -> None:
50
+ super().__init__()
26
51
 
27
- omod = None
52
+ self._owner_globals = owner_globals
53
+
54
+ self._owner_name: str | None = owner_globals.get('__name__') if owner_globals else None
55
+
56
+ # NOTE: Import machinery may be reentrant for things like gc ops and signal handling:
57
+ # TODO: audit for reentrancy this lol
58
+ # https://github.com/python/cpython/blob/72f25a8d9a5673d39c107cf522465a566b979ed5/Lib/importlib/_bootstrap.py#L233-L237 # noqa
59
+ self._lock = threading.RLock()
60
+
61
+ self._modules_by_name: dict[str, _ProxyImporter._Module] = {}
62
+ self._modules_by_proxy_obj: dict[types.ModuleType, _ProxyImporter._Module] = {}
63
+
64
+ class _Module:
65
+ def __init__(
66
+ self,
67
+ name: str,
68
+ getattr_handler: ta.Callable[['_ProxyImporter._Module', str], ta.Any],
69
+ *,
70
+ parent: ta.Optional['_ProxyImporter._Module'] = None,
71
+ ) -> None:
72
+ super().__init__()
73
+
74
+ self.name = name
75
+ self.parent = parent
76
+
77
+ self.base_name = name.rpartition('.')[2]
78
+ self.root: _ProxyImporter._Module = parent.root if parent is not None else self # noqa
79
+
80
+ self.children: dict[str, _ProxyImporter._Module] = {}
81
+ self.descendants: set[_ProxyImporter._Module] = set()
82
+
83
+ self.proxy_obj = types.ModuleType(f'<{self.__class__.__qualname__}: {name}>')
84
+ self.proxy_obj.__file__ = None
85
+ self.proxy_obj.__getattr__ = functools.partial(getattr_handler, self) # type: ignore[method-assign]
86
+
87
+ self.pending_children: set[str] = set()
88
+ self.pending_attrs: set[str] = set()
89
+
90
+ real_obj: types.ModuleType | None = None
91
+
92
+ def __repr__(self) -> str:
93
+ return f'{self.__class__.__name__}<{self.name}{"!" if self.real_obj is not None else ""}>'
94
+
95
+ def find_attr(self, attr: str) -> _ProxyImporterModuleAttr | None:
96
+ is_child = attr in self.children
97
+ is_proxy_attr = attr in self.proxy_obj.__dict__
98
+ is_pending_child = attr in self.pending_children
99
+ is_pending_attr = attr in self.pending_attrs
100
+
101
+ if is_child:
102
+ if (
103
+ not is_proxy_attr or
104
+ is_pending_child or
105
+ is_pending_attr
106
+ ):
107
+ raise RuntimeError
108
+ return 'child'
109
+
110
+ elif is_proxy_attr:
111
+ if (
112
+ is_pending_child or
113
+ is_pending_attr
114
+ ):
115
+ raise RuntimeError
116
+ return 'proxy_attr'
117
+
118
+ elif is_pending_child:
119
+ if (
120
+ is_child or
121
+ is_proxy_attr or
122
+ is_pending_attr
123
+ ):
124
+ raise RuntimeError
125
+ return 'pending_child'
126
+
127
+ elif is_pending_attr:
128
+ if (
129
+ is_child or
130
+ is_proxy_attr or
131
+ is_pending_child
132
+ ):
133
+ raise RuntimeError
134
+ return 'pending_attr'
28
135
 
29
- def __getattr__(att): # noqa
30
- nonlocal omod
31
- if omod is None:
32
- omod = importlib.import_module(spec, package=package)
33
- if extras:
34
- for x in extras:
35
- importlib.import_module(f'{spec}.{x}', package=package)
36
- return getattr(omod, att)
136
+ else:
137
+ return None
37
138
 
38
- lmod = types.ModuleType(spec)
39
- lmod.__getattr__ = __getattr__ # type: ignore[method-assign]
40
- return lmod
139
+ #
41
140
 
141
+ def _get_or_make_module_locked(self, name: str) -> _Module:
142
+ try:
143
+ return self._modules_by_name[name]
144
+ except KeyError:
145
+ pass
42
146
 
43
- #
147
+ parent: _ProxyImporter._Module | None = None
148
+ if '.' in name:
149
+ rest, _, attr = name.rpartition('.')
150
+ parent = self._get_or_make_module_locked(rest)
44
151
 
152
+ fa = parent.find_attr(attr)
153
+ if not (fa == 'pending_child' or fa is None):
154
+ raise RuntimeError
45
155
 
46
- def auto_proxy_import(
47
- mod_globals: ta.MutableMapping[str, ta.Any],
48
- *,
49
- disable: bool = False,
156
+ if (ro := parent.real_obj) is not None and attr not in ro.__dict__:
157
+ raise NotImplementedError
50
158
 
51
- unreferenced_callback: ta.Callable[[ta.Mapping[str, ta.Sequence[str | None]]], None] | None = None,
52
- raise_unreferenced: bool = False,
159
+ module = self._modules_by_name[name] = _ProxyImporter._Module(
160
+ name,
161
+ self._handle_module_getattr,
162
+ )
53
163
 
54
- _stack_offset: int = 0,
55
- ) -> ta.ContextManager[ImportCapture]:
56
- inst = ImportCapture(
57
- mod_globals,
58
- _hook=_new_import_capture_hook(
59
- mod_globals,
60
- stack_offset=_stack_offset + 1,
61
- ),
62
- disable=disable,
63
- )
164
+ self._modules_by_name[name] = module
165
+ self._modules_by_proxy_obj[module.proxy_obj] = module
64
166
 
65
- @contextlib.contextmanager
66
- def inner() -> ta.Iterator[ImportCapture]:
67
- with inst.capture(
68
- unreferenced_callback=unreferenced_callback,
69
- raise_unreferenced=raise_unreferenced,
70
- ):
71
- yield inst
167
+ if parent is not None:
168
+ parent.pending_children.discard(module.base_name)
169
+ parent.children[module.base_name] = module
170
+ setattr(parent.proxy_obj, module.base_name, module.proxy_obj)
171
+ parent.root.descendants.add(module)
72
172
 
73
- pkg = mod_globals.get('__package__')
74
- for pi in inst.captured.imports:
75
- for sa, ma in pi.attrs:
76
- mod_globals[ma] = proxy_import(pi.spec + (('.' + sa) if sa is not None else ''), pkg)
173
+ return module
77
174
 
78
- return inner()
175
+ def _extend_module_locked(
176
+ self,
177
+ module: _Module,
178
+ *,
179
+ children: ta.Iterable[str] | None = None,
180
+ attrs: ta.Iterable[str] | None = None,
181
+ ) -> None:
182
+ for l in (children, attrs):
183
+ for n in l or ():
184
+ if n in module.proxy_obj.__dict__:
185
+ raise NotImplementedError
79
186
 
187
+ if (ro := module.real_obj) is not None and n in ro.__dict__:
188
+ raise NotImplementedError
80
189
 
81
- ##
190
+ for n in children or ():
191
+ fa = module.find_attr(n)
192
+ if not (fa == 'pending_child' or fa is None):
193
+ raise RuntimeError
82
194
 
195
+ for n in attrs or ():
196
+ fa = module.find_attr(n)
197
+ if not (fa == 'pending_attr' or fa is None):
198
+ raise RuntimeError
83
199
 
84
- class _ProxyInit:
85
- class NamePackage(ta.NamedTuple):
86
- name: str
87
- package: str
200
+ #
88
201
 
89
- class _Import(ta.NamedTuple):
90
- spec: str
91
- attr: str | None
202
+ if children:
203
+ module.pending_children.update(n for n in children if n not in module.children)
204
+ if attrs:
205
+ module.pending_attrs.update(attrs)
92
206
 
93
- def __init__(
94
- self,
95
- lazy_globals: LazyGlobals,
96
- name_package: NamePackage,
97
- ) -> None:
98
- super().__init__()
207
+ def _retrieve_from_module_locked(self, module: _Module, attr: str) -> ta.Any:
208
+ fa = module.find_attr(attr)
209
+
210
+ if fa == 'child' or fa == 'proxy_attr':
211
+ return module.proxy_obj.__dict__[attr]
212
+
213
+ val: ta.Any
214
+
215
+ if fa == 'pending_child':
216
+ if module.name == self._owner_name:
217
+ val = importlib.import_module(f'{module.name}.{attr}')
218
+
219
+ else:
220
+ mod = __import__(
221
+ module.name,
222
+ self._owner_globals or {},
223
+ {},
224
+ [attr],
225
+ 0,
226
+ )
227
+
228
+ val = getattr(mod, attr)
229
+
230
+ module.pending_children.remove(attr)
99
231
 
100
- self._lazy_globals = lazy_globals
101
- self._name_package = name_package
232
+ elif fa == 'pending_attr' or fa is None:
233
+ if module.name == self._owner_name:
234
+ raise NotImplementedError
102
235
 
103
- self._imps_by_attr: dict[str, _ProxyInit._Import] = {}
104
- self._mods_by_spec: dict[str, ta.Any] = {}
236
+ if (ro := module.real_obj) is None:
237
+ ro = module.real_obj = importlib.import_module(module.name)
105
238
 
106
- @property
107
- def name_package(self) -> NamePackage:
108
- return self._name_package
239
+ val = getattr(ro, attr)
240
+
241
+ if fa == 'pending_attr':
242
+ module.pending_attrs.remove(attr)
243
+
244
+ else:
245
+ raise TypeError(fa)
246
+
247
+ setattr(module.proxy_obj, attr, val)
248
+ return val
249
+
250
+ #
251
+
252
+ def _handle_module_getattr(self, module: _Module, attr: str) -> ta.Any:
253
+ with self._lock:
254
+ return self._retrieve_from_module_locked(module, attr)
109
255
 
110
256
  def add(
111
257
  self,
112
- package: str,
113
- attrs: ta.Iterable[tuple[str | None, str]],
114
- ) -> None:
115
- for imp_attr, as_attr in attrs:
116
- if imp_attr is None:
117
- self._imps_by_attr[as_attr] = self._Import(package, None)
118
- self._lazy_globals.set_fn(as_attr, functools.partial(self.get, as_attr))
258
+ module: str,
259
+ *,
260
+ children: ta.Iterable[str] | None = None,
261
+ attrs: ta.Iterable[str] | None = None,
262
+ ) -> types.ModuleType:
263
+ if isinstance(children, str):
264
+ raise TypeError(children)
265
+
266
+ # Leaf modules get collapsed into parents' pending_children
267
+ if not children and not attrs and '.' in module:
268
+ module, _, child = module.rpartition('.')
269
+ children = [child]
270
+
271
+ with self._lock:
272
+ m = self._get_or_make_module_locked(module)
273
+
274
+ if children or attrs:
275
+ self._extend_module_locked(
276
+ m,
277
+ children=children,
278
+ attrs=attrs,
279
+ )
280
+
281
+ return m.proxy_obj
282
+
283
+ def get_module(self, name: str) -> types.ModuleType:
284
+ try:
285
+ return self._modules_by_name[name].proxy_obj
286
+ except KeyError:
287
+ pass
119
288
 
120
- else:
121
- self._imps_by_attr[as_attr] = self._Import(package, imp_attr)
122
- self._lazy_globals.set_fn(as_attr, functools.partial(self.get, as_attr))
289
+ with self._lock:
290
+ return self._get_or_make_module_locked(name).proxy_obj
123
291
 
124
- def _import_module(self, name: str) -> ta.Any:
125
- return importlib.import_module(name, package=self._name_package.package)
292
+ def lookup(self, spec: str) -> ta.Any:
293
+ if '.' not in spec:
294
+ return self._modules_by_name[spec].proxy_obj
126
295
 
127
- def get(self, attr: str) -> ta.Any:
128
296
  try:
129
- imp = self._imps_by_attr[attr]
297
+ module = self._modules_by_name[spec]
130
298
  except KeyError:
131
- raise AttributeError(attr) # noqa
299
+ pass
300
+ else:
301
+ return module.proxy_obj
132
302
 
133
- val: ta.Any
303
+ rest, _, attr = spec.rpartition('.')
304
+ module = self._modules_by_name[rest]
305
+ return getattr(module.proxy_obj, attr)
134
306
 
135
- if imp.attr is None:
136
- val = self._import_module(imp.spec)
137
307
 
138
- else:
139
- try:
140
- mod = self._mods_by_spec[imp.spec]
141
- except KeyError:
142
- mod = self._import_module(imp.spec)
143
- self._mods_by_spec[imp.spec] = mod
308
+ #
144
309
 
145
- val = getattr(mod, imp.attr)
146
310
 
147
- return val
311
+ _MODULE_PROXY_IMPORTER_GLOBAL_NAME = '__proxy_importer__'
312
+
313
+
314
+ def _get_module_proxy_importer(mod_globals: ta.MutableMapping[str, ta.Any]) -> _ProxyImporter:
315
+ """Assumed to only be called in a module body - no locking is done."""
316
+
317
+ pi: _ProxyImporter
318
+ try:
319
+ pi = mod_globals[_MODULE_PROXY_IMPORTER_GLOBAL_NAME]
320
+
321
+ except KeyError:
322
+ pi = _ProxyImporter(
323
+ owner_globals=mod_globals,
324
+ )
325
+ mod_globals[_MODULE_PROXY_IMPORTER_GLOBAL_NAME] = pi
326
+
327
+ else:
328
+ if pi.__class__ is not _ProxyImporter:
329
+ raise TypeError(pi)
330
+
331
+ if pi._owner_globals is not mod_globals: # noqa
332
+ raise RuntimeError
333
+
334
+ return pi
335
+
336
+
337
+ ##
338
+
339
+
340
+ def proxy_import(
341
+ spec: str,
342
+ package: str | None = None,
343
+ extras: ta.Iterable[str] | None = None,
344
+ ) -> types.ModuleType:
345
+ """'Legacy' proxy import mechanism."""
346
+
347
+ if isinstance(extras, str):
348
+ raise TypeError(extras)
349
+
350
+ omod = None
351
+
352
+ def __getattr__(att): # noqa
353
+ nonlocal omod
354
+ if omod is None:
355
+ omod = importlib.import_module(spec, package=package)
356
+ if extras:
357
+ for x in extras:
358
+ importlib.import_module(f'{spec}.{x}', package=package)
359
+ return getattr(omod, att)
360
+
361
+ lmod = types.ModuleType(spec)
362
+ lmod.__getattr__ = __getattr__ # type: ignore[method-assign]
363
+ return lmod
364
+
365
+
366
+ #
148
367
 
149
368
 
150
369
  def proxy_init(
@@ -152,6 +371,13 @@ def proxy_init(
152
371
  spec: str,
153
372
  attrs: ta.Iterable[str | tuple[str | None, str | None] | None] | None = None,
154
373
  ) -> None:
374
+ name = importlib.util.resolve_name(
375
+ spec,
376
+ package=init_globals['__package__'] if spec.startswith('.') else None,
377
+ )
378
+
379
+ #
380
+
155
381
  if isinstance(attrs, str):
156
382
  raise TypeError(attrs)
157
383
 
@@ -181,76 +407,143 @@ def proxy_init(
181
407
 
182
408
  #
183
409
 
184
- init_name_package = _ProxyInit.NamePackage(
185
- init_globals['__name__'],
186
- init_globals['__package__'],
410
+ pi = _get_module_proxy_importer(init_globals)
411
+ lg = LazyGlobals.install(init_globals)
412
+
413
+ pi.add(
414
+ name,
415
+ children=[r for l, r in al if r is not None],
187
416
  )
188
417
 
189
- pi: _ProxyInit
190
- try:
191
- pi = init_globals['__proxy_init__']
418
+ for imp_attr, as_attr in al:
419
+ lg.set_fn(as_attr, functools.partial(pi.lookup, name if imp_attr is None else f'{name}.{imp_attr}'))
192
420
 
193
- except KeyError:
194
- pi = _ProxyInit(
195
- LazyGlobals.install(init_globals),
196
- init_name_package,
421
+
422
+ ##
423
+
424
+
425
+ class _AutoProxy:
426
+ def __init__(
427
+ self,
428
+ mod_globals: ta.MutableMapping[str, ta.Any],
429
+ *,
430
+ disable: bool = False,
431
+ eager: bool = False,
432
+
433
+ unreferenced_callback: ta.Callable[[ta.Sequence[str]], None] | None = None,
434
+ raise_unreferenced: bool = False,
435
+
436
+ update_exports: bool = False,
437
+
438
+ _stack_offset: int = 0,
439
+ _capture_impl: str | None = None,
440
+ ) -> None:
441
+ super().__init__()
442
+
443
+ self._mod_globals = mod_globals
444
+
445
+ self._disabled = disable
446
+ self._eager = eager
447
+
448
+ self._unreferenced_callback = unreferenced_callback
449
+ self._raise_unreferenced = raise_unreferenced
450
+
451
+ self._update_exports = update_exports
452
+
453
+ self._ic = ImportCapture(
454
+ mod_globals,
455
+ _hook=_new_import_capture_hook(
456
+ mod_globals,
457
+ stack_offset=_stack_offset + 1,
458
+ capture_impl=_capture_impl,
459
+ ),
460
+ disable=disable,
197
461
  )
198
- init_globals['__proxy_init__'] = pi
462
+ self._icc: ta.Any = None
199
463
 
200
- else:
201
- if pi.name_package != init_name_package:
202
- raise Exception(f'Wrong init name: {pi.name_package=} != {init_name_package=}')
464
+ def __enter__(self) -> ImportCapture:
465
+ if self._icc is not None:
466
+ raise RuntimeError
203
467
 
204
- pi.add(spec, al)
468
+ self._icc = self._ic.capture(
469
+ unreferenced_callback=self._unreferenced_callback,
470
+ raise_unreferenced=self._raise_unreferenced,
471
+ )
205
472
 
473
+ return self._icc.__enter__() # noqa
206
474
 
207
- #
475
+ def __exit__(self, exc_type, exc_val, exc_tb):
476
+ if self._icc is None:
477
+ raise RuntimeError
208
478
 
479
+ self._icc.__exit__(exc_type, exc_val, exc_tb)
209
480
 
210
- def auto_proxy_init(
211
- init_globals: ta.MutableMapping[str, ta.Any],
212
- *,
213
- disable: bool = False,
214
- eager: bool = False,
215
-
216
- unreferenced_callback: ta.Callable[[ta.Mapping[str, ta.Sequence[str | None]]], None] | None = None,
217
- raise_unreferenced: bool = False,
218
-
219
- update_exports: bool = False,
220
-
221
- _stack_offset: int = 0,
222
- ) -> ta.ContextManager[ImportCapture]:
223
- inst = ImportCapture(
224
- init_globals,
225
- _hook=_new_import_capture_hook(
226
- init_globals,
227
- stack_offset=_stack_offset + 1,
228
- ),
229
- disable=disable,
230
- )
481
+ if not self._disabled and exc_type is None:
482
+ self._install()
483
+
484
+ # @abc.abstractmethod
485
+ def _install(self) -> None:
486
+ raise NotImplementedError
487
+
488
+
489
+ @ta.final
490
+ class _AutoProxyImport(_AutoProxy):
491
+ def _install(self) -> None:
492
+ cap = self._ic.captured
493
+
494
+ for cm in cap.modules.values():
495
+ if cm.attrs:
496
+ raise RuntimeError
497
+
498
+ pi = _get_module_proxy_importer(self._mod_globals)
231
499
 
232
- @contextlib.contextmanager
233
- def inner() -> ta.Iterator[ImportCapture]:
234
- with inst.capture(
235
- unreferenced_callback=unreferenced_callback,
236
- raise_unreferenced=raise_unreferenced,
237
- ):
238
- yield inst
239
-
240
- for pi in inst.captured.imports:
241
- proxy_init(
242
- init_globals,
243
- pi.spec,
244
- pi.attrs,
500
+ for cm in cap.modules.values():
501
+ pi.add(
502
+ cm.name,
503
+ children=cm.children,
245
504
  )
246
505
 
247
- if eager:
248
- lg = LazyGlobals.install(init_globals)
506
+ for ci in cap.imports.values():
507
+ pm = pi.get_module(ci.module.name)
508
+ for a in ci.as_ or ():
509
+ self._mod_globals[a] = pm
249
510
 
250
- for a in inst.captured.attrs:
511
+ if self._eager:
512
+ for ci in cap.imports.values():
513
+ pi.lookup(ci.module.name)
514
+
515
+ if self._update_exports:
516
+ self._ic.update_exports()
517
+
518
+
519
+ @ta.final
520
+ class _AutoProxyInit(_AutoProxy):
521
+ def _install(self) -> None:
522
+ cap = self._ic.captured
523
+
524
+ pi = _get_module_proxy_importer(self._mod_globals)
525
+ lg = LazyGlobals.install(self._mod_globals)
526
+
527
+ for cm in cap.modules.values():
528
+ pi.add(
529
+ cm.name,
530
+ children=cm.children,
531
+ attrs=cm.attrs,
532
+ )
533
+
534
+ for ci in cap.imports.values():
535
+ for a in ci.as_ or ():
536
+ lg.set_fn(a, functools.partial(pi.lookup, ci.module.name))
537
+ for sa, da in ci.attrs or ():
538
+ lg.set_fn(da, functools.partial(pi.lookup, f'{ci.module.name}.{sa}'))
539
+
540
+ if self._eager:
541
+ for a in cap.attrs:
251
542
  lg.get(a)
252
543
 
253
- if update_exports:
254
- inst.update_exports()
544
+ if self._update_exports:
545
+ self._ic.update_exports()
546
+
255
547
 
256
- return inner()
548
+ auto_proxy_import = _AutoProxyImport
549
+ auto_proxy_init = _AutoProxyInit