omlish 0.0.0.dev459__py3-none-any.whl → 0.0.0.dev461__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.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev459'
2
- __revision__ = 'a12178d42c88b1d69bca74aaf2cd0f1fd8106a47'
1
+ __version__ = '0.0.0.dev461'
2
+ __revision__ = '7ebd7a07f84e80a8d1855b8a182da756dbb7d6ec'
3
3
 
4
4
 
5
5
  #
@@ -76,7 +76,7 @@ static PyMethodDef capture_methods[] = {
76
76
 
77
77
  static struct PyModuleDef_Slot capture_slots[] = {
78
78
  {Py_mod_gil, Py_MOD_GIL_NOT_USED},
79
- {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED},
79
+ {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
80
80
  {0, NULL}
81
81
  };
82
82
 
@@ -120,30 +120,27 @@ class _ImportCaptureHook:
120
120
 
121
121
  if name.startswith('.'):
122
122
  raise ImportCaptureError
123
+
123
124
  self.name = name
124
- self.base_name = name.rpartition('.')[2]
125
125
  self.parent = parent
126
126
 
127
+ self.base_name = name.rpartition('.')[2]
128
+ self.root: _ImportCaptureHook._Module = parent.root if parent is not None else self # noqa
129
+
130
+ self.children: dict[str, _ImportCaptureHook._Module] = {}
131
+ self.descendants: set[_ImportCaptureHook._Module] = set()
132
+
127
133
  self.module_obj = types.ModuleType(f'<{self.__class__.__qualname__}: {name}>')
128
134
  self.module_obj.__file__ = None
129
135
  self.module_obj.__getattr__ = functools.partial(getattr_handler, self) # type: ignore[method-assign] # noqa
130
136
  self.initial_module_dict = dict(self.module_obj.__dict__)
131
137
 
132
- self.children: dict[str, _ImportCaptureHook._Module] = {}
133
- self.descendants: set[_ImportCaptureHook._Module] = set()
134
138
  self.explicit = False
135
139
  self.immediate = False
136
140
 
137
141
  def __repr__(self) -> str:
138
142
  return f'{self.__class__.__name__}<{self.name}{"!" if self.immediate else "+" if self.explicit else ""}>'
139
143
 
140
- @property
141
- def root(self) -> '_ImportCaptureHook._Module':
142
- out = self
143
- while out.parent is not None:
144
- out = out.parent
145
- return out
146
-
147
144
  def set_explicit(self) -> None:
148
145
  cur: _ImportCaptureHook._Module | None = self
149
146
  while cur is not None and not cur.explicit:
@@ -157,35 +154,32 @@ class _ImportCaptureHook:
157
154
  return sorted(self._modules_by_name.values(), key=lambda m: m.name)
158
155
 
159
156
  def _get_or_make_module(self, name: str) -> _Module:
160
- def rec(name: str) -> _ImportCaptureHook._Module: # noqa
161
- try:
162
- return self._modules_by_name[name]
163
- except KeyError:
164
- pass
165
-
166
- parent: _ImportCaptureHook._Module | None = None
167
- if '.' in name:
168
- rest, _, attr = name.rpartition('.')
169
- parent = rec(rest)
170
- if attr in parent.children:
171
- raise ImportCaptureErrors.AttrError(rest, attr)
157
+ try:
158
+ return self._modules_by_name[name]
159
+ except KeyError:
160
+ pass
172
161
 
173
- module = self._Module(
174
- name,
175
- self._handle_module_getattr,
176
- parent=parent,
177
- )
178
- self._modules_by_name[name] = module
179
- self._modules_by_module_obj[module.module_obj] = module
162
+ parent: _ImportCaptureHook._Module | None = None
163
+ if '.' in name:
164
+ rest, _, attr = name.rpartition('.')
165
+ parent = self._get_or_make_module(rest)
166
+ if attr in parent.children:
167
+ raise ImportCaptureErrors.AttrError(rest, attr)
180
168
 
181
- if parent is not None:
182
- parent.children[attr] = module # noqa
183
- setattr(parent.module_obj, attr, module.module_obj)
184
- parent.root.descendants.add(module)
169
+ module = _ImportCaptureHook._Module(
170
+ name,
171
+ self._handle_module_getattr,
172
+ parent=parent,
173
+ )
174
+ self._modules_by_name[name] = module
175
+ self._modules_by_module_obj[module.module_obj] = module
185
176
 
186
- return module
177
+ if parent is not None:
178
+ parent.children[module.base_name] = module
179
+ setattr(parent.module_obj, module.base_name, module.module_obj)
180
+ parent.root.descendants.add(module)
187
181
 
188
- return rec(name)
182
+ return module
189
183
 
190
184
  def _make_child_module(self, module: _Module, attr: str) -> _Module:
191
185
  if attr in module.children:
@@ -322,7 +316,11 @@ class _ImportCaptureHook:
322
316
  ) -> 'ImportCapture.Captured':
323
317
  rem_explicit_mods: set[_ImportCaptureHook._Module] = set()
324
318
  if collect_unreferenced:
325
- rem_explicit_mods.update([m for m in self._modules_by_name.values() if m.immediate])
319
+ rem_explicit_mods.update(
320
+ m for m in self._modules_by_name.values()
321
+ if m.immediate
322
+ and m.parent is not None # No good way to tell if user did `import a.b.c` or `import a.b.c as c`
323
+ )
326
324
 
327
325
  #
328
326
 
@@ -395,8 +393,8 @@ class _ImportCaptureHook:
395
393
  for m, ts in sorted(dct.items(), key=lambda t: t[0].name):
396
394
  imps.append(ImportCapture.Import(
397
395
  mods[m.name],
398
- [r for l, r in ts if l is None],
399
- [(l, r) for l, r in ts if l is not None],
396
+ [r for l, r in ts if l is None] or None,
397
+ [(l, r) for l, r in ts if l is not None] or None,
400
398
  ))
401
399
 
402
400
  #
@@ -419,6 +417,14 @@ class _ImportCaptureHook:
419
417
 
420
418
 
421
419
  class _AbstractBuiltinsImportCaptureHook(_ImportCaptureHook):
420
+ def __init__(
421
+ self,
422
+ *,
423
+ _frame: types.FrameType | None = None,
424
+ **kwargs: ta.Any,
425
+ ) -> None:
426
+ super().__init__(**kwargs)
427
+
422
428
  def _new_import(
423
429
  self,
424
430
  old_import,
@@ -575,11 +581,24 @@ class _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook(_AbstractBuiltinsImport
575
581
  #
576
582
 
577
583
 
578
- _capture: ta.Any = None
579
- try:
580
- from . import _capture # type: ignore
581
- except ImportError:
582
- pass
584
+ _cext_: ta.Any
585
+
586
+
587
+ def _cext() -> ta.Any:
588
+ global _cext_
589
+ try:
590
+ return _cext_
591
+ except NameError:
592
+ pass
593
+
594
+ cext: ta.Any
595
+ try:
596
+ from . import _capture as cext # type: ignore
597
+ except ImportError:
598
+ cext = None
599
+
600
+ _cext_ = cext
601
+ return cext
583
602
 
584
603
 
585
604
  class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
@@ -599,7 +618,7 @@ class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
599
618
  frame: types.FrameType,
600
619
  new_builtins: dict[str, ta.Any],
601
620
  ) -> bool:
602
- return _capture._set_frame_builtins(frame, frame.f_builtins, new_builtins) # noqa
621
+ return _cext()._set_frame_builtins(frame, frame.f_builtins, new_builtins) # noqa
603
622
 
604
623
  @contextlib.contextmanager
605
624
  def _hook_context(
@@ -629,22 +648,37 @@ class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
629
648
  #
630
649
 
631
650
 
651
+ _CAPTURE_IMPLS: ta.Mapping[str, type[_AbstractBuiltinsImportCaptureHook]] = {
652
+ 'cext': _FrameBuiltinsImportCaptureHook,
653
+ 'somewhat_safe': _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook,
654
+ 'unsafe': _UnsafeGlobalBuiltinsImportCaptureHook,
655
+ }
656
+
657
+
632
658
  def _new_import_capture_hook(
633
659
  mod_globals: ta.MutableMapping[str, ta.Any], # noqa
634
660
  *,
635
661
  stack_offset: int = 0,
662
+ capture_impl: str | None = None,
636
663
  **kwargs: ta.Any,
637
664
  ) -> '_ImportCaptureHook':
638
- frame: types.FrameType | None = sys._getframe(1 + stack_offset) # noqa
639
- if frame is None or frame.f_globals is not mod_globals:
640
- raise ImportCaptureError("Can't find importing frame")
665
+ if '_frame' not in kwargs:
666
+ frame: types.FrameType | None = sys._getframe(1 + stack_offset) # noqa
667
+ if frame is None or frame.f_globals is not mod_globals:
668
+ raise ImportCaptureError("Can't find importing frame")
669
+ kwargs['_frame'] = frame
641
670
 
642
671
  kwargs.setdefault('package', mod_globals.get('__package__'))
643
672
 
644
- if _capture is not None:
645
- return _FrameBuiltinsImportCaptureHook(_frame=frame, **kwargs)
673
+ cls: type[_AbstractBuiltinsImportCaptureHook]
674
+ if capture_impl is not None:
675
+ cls = _CAPTURE_IMPLS[capture_impl]
676
+ elif _cext() is not None:
677
+ cls = _FrameBuiltinsImportCaptureHook
678
+ else:
679
+ cls = _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook
646
680
 
647
- return _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook(**kwargs)
681
+ return cls(**kwargs)
648
682
 
649
683
 
650
684
  ##
@@ -693,20 +727,28 @@ class ImportCapture:
693
727
  ')',
694
728
  ])
695
729
 
730
+ _root: 'ImportCapture.Module'
731
+
696
732
  @property
697
733
  def root(self) -> 'ImportCapture.Module':
698
- out = self
699
- while out.parent is not None:
700
- out = out.parent
701
- return out
734
+ try:
735
+ return self._root
736
+ except AttributeError:
737
+ pass
738
+
739
+ root = self
740
+ while root.parent is not None:
741
+ root = root.parent
742
+ self._root = root
743
+ return root
702
744
 
703
745
  @ta.final
704
746
  class Import:
705
747
  def __init__(
706
748
  self,
707
749
  module: 'ImportCapture.Module',
708
- as_: ta.Sequence[str],
709
- attrs: ta.Sequence[tuple[str, str]], # ('foo', 'bar') -> `import foo as bar` - explicitly not a dict
750
+ as_: ta.Sequence[str] | None,
751
+ attrs: ta.Sequence[tuple[str, str]] | None, # ('foo', 'bar') -> `import foo as bar` - explicitly not a dict # noqa
710
752
  ) -> None:
711
753
  self.module = module
712
754
  self.as_ = as_
@@ -743,9 +785,11 @@ class ImportCapture:
743
785
  @property
744
786
  def attrs(self) -> ta.Iterator[str]:
745
787
  for pi in self.imports.values():
746
- yield from pi.as_
747
- for _, a in pi.attrs:
748
- yield a
788
+ if pi.as_:
789
+ yield from pi.as_
790
+ if pi.attrs:
791
+ for _, a in pi.attrs:
792
+ yield a
749
793
 
750
794
  EMPTY_CAPTURED: ta.ClassVar[Captured] = Captured(
751
795
  {},
@@ -1,6 +1,28 @@
1
- import contextlib
1
+ """
2
+ TODO:
3
+ - if already imported just return?
4
+ - no, need sub-imports..
5
+ - seal on first use? or just per module? can't seal roots and still be usable
6
+ - only if not hasattr?
7
+ - audit for deadlock risk - does importlib._bootstrap do it for us? do we need a global _ProxyImporter lock? would only
8
+ happen on reification
9
+ - ProxyImportError
10
+ - detect import reification in own module body - user is failing to properly 'hands-off' lazy import
11
+ - bonus points detect when done specifically for a type annotation
12
+
13
+ See:
14
+ - https://peps.python.org/pep-0810/
15
+ - https://github.com/LazyImportsCabal/cpython/tree/lazy
16
+ - https://developers.facebook.com/blog/post/2022/06/15/python-lazy-imports-with-cinder/
17
+ - https://engineering.fb.com/2024/01/18/developer-tools/lazy-imports-cinder-machine-learning-meta/
18
+ - https://www.hudsonrivertrading.com/hrtbeat/inside-hrts-python-fork/
19
+ - https://bugreports.qt.io/browse/PYSIDE-2404
20
+ - https://scientific-python.org/specs/spec-0001/
21
+ - https://github.com/scientific-python/lazy-loader
22
+ """
2
23
  import functools
3
24
  import importlib.util
25
+ import threading
4
26
  import types
5
27
  import typing as ta
6
28
 
@@ -9,179 +31,340 @@ from .capture import ImportCapture
9
31
  from .capture import _new_import_capture_hook
10
32
 
11
33
 
12
- ##
34
+ _ProxyImporterModuleAttr: ta.TypeAlias = ta.Literal[
35
+ 'child', # 'outranks' proxy_attr - all child attrs must be proxy_attrs but not vice versa
36
+ 'proxy_attr',
37
+ 'pending_child',
38
+ 'pending_attr',
39
+ ]
13
40
 
14
41
 
15
- def _translate_old_style_import_capture(
16
- cap: ImportCapture.Captured,
17
- ) -> ta.Mapping[str, ta.Sequence[tuple[str | None, str]]]:
18
- dct: dict[str, list[tuple[str | None, str]]] = {}
42
+ ##
19
43
 
20
- for ci in cap.imports.values():
21
- if ci.module.kind == 'leaf':
22
- if (p := ci.module.parent) is None:
23
- raise NotImplementedError
24
44
 
25
- if ci.attrs:
26
- raise NotImplementedError
45
+ class _ProxyImporter:
46
+ def __init__(
47
+ self,
48
+ *,
49
+ owner_globals: ta.MutableMapping[str, ta.Any] | None = None,
50
+ ) -> None:
51
+ super().__init__()
27
52
 
28
- for a in ci.as_:
29
- dct.setdefault(p.name, []).append(
30
- (ci.module.base_name, a),
31
- )
53
+ self._owner_globals = owner_globals
54
+
55
+ self._owner_name: str | None = owner_globals.get('__name__') if owner_globals else None
56
+
57
+ # NOTE: Import machinery may be reentrant for things like gc ops and signal handling:
58
+ # TODO: audit for reentrancy this lol
59
+ # https://github.com/python/cpython/blob/72f25a8d9a5673d39c107cf522465a566b979ed5/Lib/importlib/_bootstrap.py#L233-L237 # noqa
60
+ self._lock = threading.RLock()
61
+
62
+ self._modules_by_name: dict[str, _ProxyImporter._Module] = {}
63
+ self._modules_by_proxy_obj: dict[types.ModuleType, _ProxyImporter._Module] = {}
64
+
65
+ class _Module:
66
+ def __init__(
67
+ self,
68
+ name: str,
69
+ getattr_handler: ta.Callable[['_ProxyImporter._Module', str], ta.Any],
70
+ *,
71
+ parent: ta.Optional['_ProxyImporter._Module'] = None,
72
+ ) -> None:
73
+ super().__init__()
74
+
75
+ self.name = name
76
+ self.parent = parent
77
+
78
+ self.base_name = name.rpartition('.')[2]
79
+ self.root: _ProxyImporter._Module = parent.root if parent is not None else self # noqa
80
+
81
+ self.children: dict[str, _ProxyImporter._Module] = {}
82
+ self.descendants: set[_ProxyImporter._Module] = set()
83
+
84
+ self.proxy_obj = types.ModuleType(f'<{self.__class__.__qualname__}: {name}>')
85
+ self.proxy_obj.__file__ = None
86
+ self.proxy_obj.__getattr__ = functools.partial(getattr_handler, self) # type: ignore[method-assign]
87
+
88
+ self.pending_children: set[str] = set()
89
+ self.pending_attrs: set[str] = set()
90
+
91
+ real_obj: types.ModuleType | None = None
92
+
93
+ def __repr__(self) -> str:
94
+ return f'{self.__class__.__name__}<{self.name}{"!" if self.real_obj is not None else ""}>'
95
+
96
+ def find_attr(self, attr: str) -> _ProxyImporterModuleAttr | None:
97
+ is_child = attr in self.children
98
+ is_proxy_attr = attr in self.proxy_obj.__dict__
99
+ is_pending_child = attr in self.pending_children
100
+ is_pending_attr = attr in self.pending_attrs
101
+
102
+ if is_child:
103
+ if (
104
+ not is_proxy_attr or
105
+ is_pending_child or
106
+ is_pending_attr
107
+ ):
108
+ raise RuntimeError
109
+ return 'child'
110
+
111
+ elif is_proxy_attr:
112
+ if (
113
+ is_pending_child or
114
+ is_pending_attr
115
+ ):
116
+ raise RuntimeError
117
+ return 'proxy_attr'
118
+
119
+ elif is_pending_child:
120
+ if (
121
+ is_child or
122
+ is_proxy_attr or
123
+ is_pending_attr
124
+ ):
125
+ raise RuntimeError
126
+ return 'pending_child'
127
+
128
+ elif is_pending_attr:
129
+ if (
130
+ is_child or
131
+ is_proxy_attr or
132
+ is_pending_child
133
+ ):
134
+ raise RuntimeError
135
+ return 'pending_attr'
32
136
 
33
- elif ci.module.kind == 'terminal':
34
- if ci.module.children:
35
- raise NotImplementedError
137
+ else:
138
+ return None
36
139
 
37
- for a in ci.as_:
38
- dct.setdefault(ci.module.name, []).append(
39
- (None, a),
40
- )
140
+ #
41
141
 
42
- for sa, da in ci.attrs:
43
- dct.setdefault(ci.module.name, []).append(
44
- (sa, da),
45
- )
142
+ def _get_or_make_module_locked(self, name: str) -> _Module:
143
+ try:
144
+ return self._modules_by_name[name]
145
+ except KeyError:
146
+ pass
46
147
 
47
- else:
48
- raise NotImplementedError
148
+ parent: _ProxyImporter._Module | None = None
149
+ if '.' in name:
150
+ rest, _, attr = name.rpartition('.')
151
+ parent = self._get_or_make_module_locked(rest)
49
152
 
50
- return dct
153
+ fa = parent.find_attr(attr)
154
+ if not (fa == 'pending_child' or fa is None):
155
+ raise RuntimeError
51
156
 
157
+ if (ro := parent.real_obj) is not None and attr not in ro.__dict__:
158
+ raise NotImplementedError
52
159
 
53
- ##
160
+ module = self._modules_by_name[name] = _ProxyImporter._Module(
161
+ name,
162
+ self._handle_module_getattr,
163
+ )
54
164
 
165
+ self._modules_by_name[name] = module
166
+ self._modules_by_proxy_obj[module.proxy_obj] = module
55
167
 
56
- def proxy_import(
57
- spec: str,
58
- package: str | None = None,
59
- extras: ta.Iterable[str] | None = None,
60
- ) -> types.ModuleType:
61
- if isinstance(extras, str):
62
- raise TypeError(extras)
168
+ if parent is not None:
169
+ parent.pending_children.discard(module.base_name)
170
+ parent.children[module.base_name] = module
171
+ setattr(parent.proxy_obj, module.base_name, module.proxy_obj)
172
+ parent.root.descendants.add(module)
63
173
 
64
- omod = None
174
+ return module
65
175
 
66
- def __getattr__(att): # noqa
67
- nonlocal omod
68
- if omod is None:
69
- omod = importlib.import_module(spec, package=package)
70
- if extras:
71
- for x in extras:
72
- importlib.import_module(f'{spec}.{x}', package=package)
73
- return getattr(omod, att)
176
+ def _extend_module_locked(
177
+ self,
178
+ module: _Module,
179
+ *,
180
+ children: ta.Iterable[str] | None = None,
181
+ attrs: ta.Iterable[str] | None = None,
182
+ ) -> None:
183
+ for l in (children, attrs):
184
+ for n in l or ():
185
+ if n in module.proxy_obj.__dict__:
186
+ raise NotImplementedError
74
187
 
75
- lmod = types.ModuleType(spec)
76
- lmod.__getattr__ = __getattr__ # type: ignore[method-assign]
77
- return lmod
188
+ if (ro := module.real_obj) is not None and n in ro.__dict__:
189
+ raise NotImplementedError
78
190
 
191
+ for n in children or ():
192
+ fa = module.find_attr(n)
193
+ if not (fa == 'pending_child' or fa is None):
194
+ raise RuntimeError
79
195
 
80
- #
196
+ for n in attrs or ():
197
+ fa = module.find_attr(n)
198
+ if not (fa == 'pending_attr' or fa is None):
199
+ raise RuntimeError
81
200
 
201
+ #
82
202
 
83
- def auto_proxy_import(
84
- mod_globals: ta.MutableMapping[str, ta.Any],
85
- *,
86
- disable: bool = False,
203
+ if children:
204
+ module.pending_children.update(n for n in children if n not in module.children)
205
+ if attrs:
206
+ module.pending_attrs.update(attrs)
87
207
 
88
- unreferenced_callback: ta.Callable[[ta.Sequence[str]], None] | None = None,
89
- raise_unreferenced: bool = False,
208
+ def _retrieve_from_module_locked(self, module: _Module, attr: str) -> ta.Any:
209
+ fa = module.find_attr(attr)
90
210
 
91
- _stack_offset: int = 0,
92
- ) -> ta.ContextManager[ImportCapture]:
93
- inst = ImportCapture(
94
- mod_globals,
95
- _hook=_new_import_capture_hook(
96
- mod_globals,
97
- stack_offset=_stack_offset + 1,
98
- ),
99
- disable=disable,
100
- )
211
+ if fa == 'child' or fa == 'proxy_attr':
212
+ return module.proxy_obj.__dict__[attr]
101
213
 
102
- @contextlib.contextmanager
103
- def inner() -> ta.Iterator[ImportCapture]:
104
- with inst.capture(
105
- unreferenced_callback=unreferenced_callback,
106
- raise_unreferenced=raise_unreferenced,
107
- ):
108
- yield inst
214
+ val: ta.Any
109
215
 
110
- for spec, attrs in _translate_old_style_import_capture(inst.captured).items():
111
- for sa, ma in attrs:
112
- mod_globals[ma] = proxy_import(spec + (('.' + sa) if sa is not None else ''))
216
+ if fa == 'pending_child':
217
+ if module.name == self._owner_name:
218
+ val = importlib.import_module(f'{module.name}.{attr}')
113
219
 
114
- return inner()
220
+ else:
221
+ mod = __import__(
222
+ module.name,
223
+ self._owner_globals or {},
224
+ {},
225
+ [attr],
226
+ 0,
227
+ )
115
228
 
229
+ val = getattr(mod, attr)
116
230
 
117
- ##
231
+ module.pending_children.remove(attr)
118
232
 
233
+ elif fa == 'pending_attr' or fa is None:
234
+ if module.name == self._owner_name:
235
+ raise NotImplementedError
119
236
 
120
- class _ProxyInit:
121
- class NamePackage(ta.NamedTuple):
122
- name: str
123
- package: str
237
+ if (ro := module.real_obj) is None:
238
+ ro = module.real_obj = importlib.import_module(module.name)
124
239
 
125
- class _Import(ta.NamedTuple):
126
- name: str
127
- attr: str | None
240
+ val = getattr(ro, attr)
128
241
 
129
- def __init__(
130
- self,
131
- lazy_globals: LazyGlobals,
132
- name_package: NamePackage,
133
- ) -> None:
134
- super().__init__()
242
+ if fa == 'pending_attr':
243
+ module.pending_attrs.remove(attr)
135
244
 
136
- self._lazy_globals = lazy_globals
137
- self._name_package = name_package
245
+ else:
246
+ raise TypeError(fa)
247
+
248
+ setattr(module.proxy_obj, attr, val)
249
+ return val
138
250
 
139
- self._imps_by_attr: dict[str, _ProxyInit._Import] = {}
251
+ #
140
252
 
141
- @property
142
- def name_package(self) -> NamePackage:
143
- return self._name_package
253
+ def _handle_module_getattr(self, module: _Module, attr: str) -> ta.Any:
254
+ with self._lock:
255
+ return self._retrieve_from_module_locked(module, attr)
144
256
 
145
257
  def add(
146
258
  self,
147
- name: str,
148
- attrs: ta.Iterable[tuple[str | None, str]],
149
- ) -> None:
150
- for imp_attr, as_attr in attrs:
151
- if imp_attr is None:
152
- self._imps_by_attr[as_attr] = self._Import(name, None)
153
- self._lazy_globals.set_fn(as_attr, functools.partial(self.get, as_attr))
259
+ module: str,
260
+ *,
261
+ children: ta.Iterable[str] | None = None,
262
+ attrs: ta.Iterable[str] | None = None,
263
+ ) -> types.ModuleType:
264
+ if isinstance(children, str):
265
+ raise TypeError(children)
266
+
267
+ # Leaf modules get collapsed into parents' pending_children
268
+ if not children and not attrs and '.' in module:
269
+ module, _, child = module.rpartition('.')
270
+ children = [child]
271
+
272
+ with self._lock:
273
+ m = self._get_or_make_module_locked(module)
274
+
275
+ if children or attrs:
276
+ self._extend_module_locked(
277
+ m,
278
+ children=children,
279
+ attrs=attrs,
280
+ )
154
281
 
155
- else:
156
- self._imps_by_attr[as_attr] = self._Import(name, imp_attr)
157
- self._lazy_globals.set_fn(as_attr, functools.partial(self.get, as_attr))
282
+ return m.proxy_obj
158
283
 
159
- def get(self, attr: str) -> ta.Any:
284
+ def get_module(self, name: str) -> types.ModuleType:
160
285
  try:
161
- imp = self._imps_by_attr[attr]
286
+ return self._modules_by_name[name].proxy_obj
162
287
  except KeyError:
163
- raise AttributeError(attr) # noqa
164
-
165
- val: ta.Any
288
+ pass
166
289
 
167
- if imp.attr is None:
168
- val = importlib.import_module(imp.name)
290
+ with self._lock:
291
+ return self._get_or_make_module_locked(name).proxy_obj
169
292
 
170
- elif imp.name == self._name_package.name:
171
- val = importlib.import_module(f'{imp.name}.{imp.attr}')
293
+ def lookup(self, spec: str) -> ta.Any:
294
+ if '.' not in spec:
295
+ return self._modules_by_name[spec].proxy_obj
172
296
 
297
+ try:
298
+ module = self._modules_by_name[spec]
299
+ except KeyError:
300
+ pass
173
301
  else:
174
- mod = __import__(
175
- imp.name,
176
- self._lazy_globals._globals, # noqa
177
- {},
178
- [imp.attr],
179
- 0,
180
- )
302
+ return module.proxy_obj
181
303
 
182
- val = getattr(mod, imp.attr)
304
+ rest, _, attr = spec.rpartition('.')
305
+ module = self._modules_by_name[rest]
306
+ return getattr(module.proxy_obj, attr)
183
307
 
184
- return val
308
+
309
+ #
310
+
311
+
312
+ _MODULE_PROXY_IMPORTER_GLOBAL_NAME = '__proxy_importer__'
313
+
314
+
315
+ def _get_module_proxy_importer(mod_globals: ta.MutableMapping[str, ta.Any]) -> _ProxyImporter:
316
+ """Assumed to only be called in a module body - no locking is done."""
317
+
318
+ pi: _ProxyImporter
319
+ try:
320
+ pi = mod_globals[_MODULE_PROXY_IMPORTER_GLOBAL_NAME]
321
+
322
+ except KeyError:
323
+ pi = _ProxyImporter(
324
+ owner_globals=mod_globals,
325
+ )
326
+ mod_globals[_MODULE_PROXY_IMPORTER_GLOBAL_NAME] = pi
327
+
328
+ else:
329
+ if pi.__class__ is not _ProxyImporter:
330
+ raise TypeError(pi)
331
+
332
+ if pi._owner_globals is not mod_globals: # noqa
333
+ raise RuntimeError
334
+
335
+ return pi
336
+
337
+
338
+ ##
339
+
340
+
341
+ def proxy_import(
342
+ spec: str,
343
+ package: str | None = None,
344
+ extras: ta.Iterable[str] | None = None,
345
+ ) -> types.ModuleType:
346
+ """'Legacy' proxy import mechanism."""
347
+
348
+ if isinstance(extras, str):
349
+ raise TypeError(extras)
350
+
351
+ omod = None
352
+
353
+ def __getattr__(att): # noqa
354
+ nonlocal omod
355
+ if omod is None:
356
+ omod = importlib.import_module(spec, package=package)
357
+ if extras:
358
+ for x in extras:
359
+ importlib.import_module(f'{spec}.{x}', package=package)
360
+ return getattr(omod, att)
361
+
362
+ lmod = types.ModuleType(spec)
363
+ lmod.__getattr__ = __getattr__ # type: ignore[method-assign]
364
+ return lmod
365
+
366
+
367
+ #
185
368
 
186
369
 
187
370
  def proxy_init(
@@ -225,76 +408,143 @@ def proxy_init(
225
408
 
226
409
  #
227
410
 
228
- init_name_package = _ProxyInit.NamePackage(
229
- init_globals['__name__'],
230
- init_globals['__package__'],
411
+ pi = _get_module_proxy_importer(init_globals)
412
+ lg = LazyGlobals.install(init_globals)
413
+
414
+ pi.add(
415
+ name,
416
+ children=[r for l, r in al if r is not None],
231
417
  )
232
418
 
233
- pi: _ProxyInit
234
- try:
235
- pi = init_globals['__proxy_init__']
419
+ for imp_attr, as_attr in al:
420
+ lg.set_fn(as_attr, functools.partial(pi.lookup, name if imp_attr is None else f'{name}.{imp_attr}'))
236
421
 
237
- except KeyError:
238
- pi = _ProxyInit(
239
- LazyGlobals.install(init_globals),
240
- init_name_package,
422
+
423
+ ##
424
+
425
+
426
+ class _AutoProxy:
427
+ def __init__(
428
+ self,
429
+ mod_globals: ta.MutableMapping[str, ta.Any],
430
+ *,
431
+ disable: bool = False,
432
+ eager: bool = False,
433
+
434
+ unreferenced_callback: ta.Callable[[ta.Sequence[str]], None] | None = None,
435
+ raise_unreferenced: bool = False,
436
+
437
+ update_exports: bool = False,
438
+
439
+ _stack_offset: int = 0,
440
+ _capture_impl: str | None = None,
441
+ ) -> None:
442
+ super().__init__()
443
+
444
+ self._mod_globals = mod_globals
445
+
446
+ self._disabled = disable
447
+ self._eager = eager
448
+
449
+ self._unreferenced_callback = unreferenced_callback
450
+ self._raise_unreferenced = raise_unreferenced
451
+
452
+ self._update_exports = update_exports
453
+
454
+ self._ic = ImportCapture(
455
+ mod_globals,
456
+ _hook=_new_import_capture_hook(
457
+ mod_globals,
458
+ stack_offset=_stack_offset + 1,
459
+ capture_impl=_capture_impl,
460
+ ),
461
+ disable=disable,
241
462
  )
242
- init_globals['__proxy_init__'] = pi
463
+ self._icc: ta.Any = None
243
464
 
244
- else:
245
- if pi.name_package != init_name_package:
246
- raise Exception(f'Wrong init name: {pi.name_package=} != {init_name_package=}')
465
+ def __enter__(self) -> ImportCapture:
466
+ if self._icc is not None:
467
+ raise RuntimeError
247
468
 
248
- pi.add(name, al)
469
+ self._icc = self._ic.capture(
470
+ unreferenced_callback=self._unreferenced_callback,
471
+ raise_unreferenced=self._raise_unreferenced,
472
+ )
249
473
 
474
+ return self._icc.__enter__() # noqa
250
475
 
251
- #
476
+ def __exit__(self, exc_type, exc_val, exc_tb):
477
+ if self._icc is None:
478
+ raise RuntimeError
252
479
 
480
+ self._icc.__exit__(exc_type, exc_val, exc_tb)
253
481
 
254
- def auto_proxy_init(
255
- init_globals: ta.MutableMapping[str, ta.Any],
256
- *,
257
- disable: bool = False,
258
- eager: bool = False,
259
-
260
- unreferenced_callback: ta.Callable[[ta.Sequence[str]], None] | None = None,
261
- raise_unreferenced: bool = False,
262
-
263
- update_exports: bool = False,
264
-
265
- _stack_offset: int = 0,
266
- ) -> ta.ContextManager[ImportCapture]:
267
- inst = ImportCapture(
268
- init_globals,
269
- _hook=_new_import_capture_hook(
270
- init_globals,
271
- stack_offset=_stack_offset + 1,
272
- ),
273
- disable=disable,
274
- )
482
+ if not self._disabled and exc_type is None:
483
+ self._install()
484
+
485
+ # @abc.abstractmethod
486
+ def _install(self) -> None:
487
+ raise NotImplementedError
488
+
489
+
490
+ @ta.final
491
+ class _AutoProxyImport(_AutoProxy):
492
+ def _install(self) -> None:
493
+ cap = self._ic.captured
494
+
495
+ for cm in cap.modules.values():
496
+ if cm.attrs:
497
+ raise RuntimeError
498
+
499
+ pi = _get_module_proxy_importer(self._mod_globals)
275
500
 
276
- @contextlib.contextmanager
277
- def inner() -> ta.Iterator[ImportCapture]:
278
- with inst.capture(
279
- unreferenced_callback=unreferenced_callback,
280
- raise_unreferenced=raise_unreferenced,
281
- ):
282
- yield inst
283
-
284
- for spec, attrs in _translate_old_style_import_capture(inst.captured).items():
285
- proxy_init(
286
- init_globals,
287
- spec,
288
- attrs,
501
+ for cm in cap.modules.values():
502
+ pi.add(
503
+ cm.name,
504
+ children=cm.children,
289
505
  )
290
506
 
291
- if eager:
292
- lg = LazyGlobals.install(init_globals)
507
+ for ci in cap.imports.values():
508
+ pm = pi.get_module(ci.module.name)
509
+ for a in ci.as_ or ():
510
+ self._mod_globals[a] = pm
293
511
 
294
- for a in inst.captured.attrs:
512
+ if self._eager:
513
+ for ci in cap.imports.values():
514
+ pi.lookup(ci.module.name)
515
+
516
+ if self._update_exports:
517
+ self._ic.update_exports()
518
+
519
+
520
+ @ta.final
521
+ class _AutoProxyInit(_AutoProxy):
522
+ def _install(self) -> None:
523
+ cap = self._ic.captured
524
+
525
+ pi = _get_module_proxy_importer(self._mod_globals)
526
+ lg = LazyGlobals.install(self._mod_globals)
527
+
528
+ for cm in cap.modules.values():
529
+ pi.add(
530
+ cm.name,
531
+ children=cm.children,
532
+ attrs=cm.attrs,
533
+ )
534
+
535
+ for ci in cap.imports.values():
536
+ for a in ci.as_ or ():
537
+ lg.set_fn(a, functools.partial(pi.lookup, ci.module.name))
538
+ for sa, da in ci.attrs or ():
539
+ lg.set_fn(da, functools.partial(pi.lookup, f'{ci.module.name}.{sa}'))
540
+
541
+ if self._eager:
542
+ for a in cap.attrs:
295
543
  lg.get(a)
296
544
 
297
- if update_exports:
298
- inst.update_exports()
545
+ if self._update_exports:
546
+ self._ic.update_exports()
547
+
299
548
 
300
- return inner()
549
+ auto_proxy_import = _AutoProxyImport
550
+ auto_proxy_init = _AutoProxyInit
@@ -1,3 +1,4 @@
1
+ import threading
1
2
  import typing as ta
2
3
 
3
4
 
@@ -15,6 +16,10 @@ class AmbiguousLazyGlobalsFallbackError(Exception):
15
16
  return f'{self.__class__.__name__}({self.attr!r}, {self.fallbacks!r})'
16
17
 
17
18
 
19
+ _LAZY_GLOBALS_LOCK = threading.RLock()
20
+
21
+
22
+ @ta.final
18
23
  class LazyGlobals:
19
24
  def __init__(
20
25
  self,
@@ -22,8 +27,6 @@ class LazyGlobals:
22
27
  globals: ta.MutableMapping[str, ta.Any] | None = None, # noqa
23
28
  update_globals: bool = False,
24
29
  ) -> None:
25
- super().__init__()
26
-
27
30
  self._globals = globals
28
31
  self._update_globals = update_globals
29
32
 
@@ -37,18 +40,28 @@ class LazyGlobals:
37
40
  except KeyError:
38
41
  pass
39
42
  else:
40
- if not isinstance(xga, cls):
43
+ if xga.__class__ is not cls:
41
44
  raise RuntimeError(f'Module already has __getattr__ hook: {xga}') # noqa
42
45
  return xga
43
46
 
44
- lm = cls(
45
- globals=globals,
46
- update_globals=True,
47
- )
47
+ with _LAZY_GLOBALS_LOCK:
48
+ try:
49
+ xga = globals['__getattr__']
50
+ except KeyError:
51
+ pass
52
+ else:
53
+ if xga.__class__ is not cls:
54
+ raise RuntimeError(f'Module already has __getattr__ hook: {xga}') # noqa
55
+ return xga
56
+
57
+ lm = cls(
58
+ globals=globals,
59
+ update_globals=True,
60
+ )
48
61
 
49
- globals['__getattr__'] = lm
62
+ globals['__getattr__'] = lm
50
63
 
51
- return lm
64
+ return lm
52
65
 
53
66
  def set_fn(self, attr: str, fn: ta.Callable[[], ta.Any]) -> 'LazyGlobals':
54
67
  self._attr_fns[attr] = fn
@@ -5,9 +5,6 @@ import typing as ta
5
5
  from ...lite.check import check
6
6
 
7
7
 
8
- T = ta.TypeVar('T')
9
-
10
-
11
8
  ##
12
9
 
13
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omlish
3
- Version: 0.0.0.dev459
3
+ Version: 0.0.0.dev461
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License-Expression: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.omlish-manifests.json,sha256=FLw7xkPiSXuImZgqSP8BwrEib2R1doSzUPLUkc-QUIA,8410
2
- omlish/__about__.py,sha256=xtL6Mzn2wu2_IspeSZ_MxiJmXyGvptjs7xRjcNyq79E,3613
2
+ omlish/__about__.py,sha256=LlWiGEazhGs4KJP-X_qmIvAXOXB4LxUSsqQz07ymSCM,3613
3
3
  omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
4
4
  omlish/c3.py,sha256=ZNIMl1kwg3qdei4DiUrJPQe5M81S1e76N-GuNSwLBAE,8683
5
5
  omlish/cached.py,sha256=MLap_p0rdGoDIMVhXVHm1tsbcWobJF0OanoodV03Ju8,542
@@ -425,7 +425,7 @@ omlish/lang/errors.py,sha256=shcS-NCnEUudF8qC_SmO2TQyjivKlS4TDjaz_faqQ0c,44
425
425
  omlish/lang/functions.py,sha256=tZL7Yi5Oy34lvzP6HhWmV5q1eg5-mk3FrWEjsmhKRhY,5707
426
426
  omlish/lang/generators.py,sha256=nJiSmDpsfPiypGzJ8qlOO7-BUnCsrAeDow9mhtGgBio,5196
427
427
  omlish/lang/iterables.py,sha256=o_s8ouaJrdUqEVl2_bzJk5CVdabmrywXY0gPn7xss3w,3371
428
- omlish/lang/lazyglobals.py,sha256=YfPtWgNEa0ULtbIiQIQ12pbafUwd6INQRw_PFcAabjo,2282
428
+ omlish/lang/lazyglobals.py,sha256=5v4S0YSUrxdZFC6znrWolfeheArMa_DEJEcz46XCQN4,2676
429
429
  omlish/lang/maybes.py,sha256=8GUpqJvyx9y5PQBQBBx6yTSE5WKsMbXMHPt4_vojKUw,209
430
430
  omlish/lang/maysync.py,sha256=S2Q_rGC4AxRa1UsGJdSzZsYpgOcX9Y8ZmYGA9v8OWn8,1635
431
431
  omlish/lang/objects.py,sha256=JMOZU5I6sK4rJm04898dOJkvef-lDIh5O3k6tlLbBJw,4616
@@ -451,11 +451,11 @@ omlish/lang/classes/restrict.py,sha256=xHLIK20MQ_jJPQ7JVzMNhyN4Xc4eLBgrcxqDnTbeK
451
451
  omlish/lang/classes/simple.py,sha256=3AJSs-plVg2flq4SC6I39LxP0nBaB241puv3D5YCP5I,2973
452
452
  omlish/lang/classes/virtual.py,sha256=J4y-uiv1RaP2rfFeptXqQ1a4MRek0TMlAFFraO_lzhs,3397
453
453
  omlish/lang/imports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
454
- omlish/lang/imports/_capture.cc,sha256=cp-L5yFGjftfBa12sKNz0Q5Yhh54a88DH7VEN1oa5hU,2313
455
- omlish/lang/imports/capture.py,sha256=z91F2CVcBDX4trhd2oRB5YL_d_Nu9KBTjzhy98N9YDQ,26170
454
+ omlish/lang/imports/_capture.cc,sha256=_jm7RuA8lB2RfQJOO6GhT8aq5zYlw36nKW4ndVBLduQ,2311
455
+ omlish/lang/imports/capture.py,sha256=pyON5qUw9IuzzOcIHh4QNjlzAEjIGb9DwI7Z8yLDUfw,27265
456
456
  omlish/lang/imports/conditional.py,sha256=R-E47QD95mMonPImWlrde3rnJrFKCCkYz71c94W05sc,1006
457
457
  omlish/lang/imports/lazy.py,sha256=Eefs9hkj5surMdwgxX_Q3BOqPcox10v0sKT5rKIQknc,808
458
- omlish/lang/imports/proxy.py,sha256=tIIChKvl5TCZp_vg6D7WR7NxnN0J-L-bqjp0DYvgUuA,7562
458
+ omlish/lang/imports/proxy.py,sha256=hATzpaZFmPO7oNOQOeFdxAC_uCFhxy3FyfDYn-SvpUQ,16051
459
459
  omlish/lang/imports/resolving.py,sha256=DeRarn35Fryg5JhVhy8wbiC9lvr58AnllI9B_reswUE,2085
460
460
  omlish/lang/imports/traversal.py,sha256=pbFQIa880NGjSfcLsno2vE_G41_CLwDHb-7gWg2J3BI,2855
461
461
  omlish/lifecycles/__init__.py,sha256=zOuvV4pErPwxcKUSgshmME2Duw9GrjwckpNmW3FPKng,810
@@ -763,7 +763,7 @@ omlish/term/progressbar.py,sha256=nCnTX1dUNCGle9FzzeTBuoxQaellErOQkyBsKVrGG1I,36
763
763
  omlish/term/vt100/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
764
764
  omlish/term/vt100/c.py,sha256=93HARU6Dd1rVF-n8cAyXqkEqleYLcofuLgSUNd6-GbU,3537
765
765
  omlish/term/vt100/states.py,sha256=OxPUxfFTcfz56MhtDgIigEApChOtN6XO1g6R2H08mu4,8303
766
- omlish/term/vt100/terminal.py,sha256=KUlg331ele7P6SHsBKdbpdQFDKsxSply1Ds27NkppTs,9359
766
+ omlish/term/vt100/terminal.py,sha256=VHbZs5mHZPBizxseWKcRH77QAjE4bWDvuHi3NM6PvbA,9337
767
767
  omlish/testing/__init__.py,sha256=h8GMv0PhVyMECsQoYuqoBzHjYO6UJyXBZvnozzUg9M8,171
768
768
  omlish/testing/testing.py,sha256=tlBeqVmzGhjRbWygJt3mZljq662qR4nAaGNE2VoAey4,3617
769
769
  omlish/testing/pytest/__init__.py,sha256=i4ti6Q2rVYJ-XBk9UYDfUUagCrEDTC5jOeSykBjYYZQ,234
@@ -826,9 +826,9 @@ omlish/typedvalues/marshal.py,sha256=2xqX6JllhtGpmeYkU7C-qzgU__0x-vd6CzYbAsocQlc
826
826
  omlish/typedvalues/of_.py,sha256=UXkxSj504WI2UrFlqdZJbu2hyDwBhL7XVrc2qdR02GQ,1309
827
827
  omlish/typedvalues/reflect.py,sha256=PAvKW6T4cW7u--iX80w3HWwZUS3SmIZ2_lQjT65uAyk,1026
828
828
  omlish/typedvalues/values.py,sha256=ym46I-q2QJ_6l4UlERqv3yj87R-kp8nCKMRph0xQ3UA,1307
829
- omlish-0.0.0.dev459.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
830
- omlish-0.0.0.dev459.dist-info/METADATA,sha256=vA7Ker_93at2wRMI8zaoBhaoAnE8BmZqvBEDDGy9gEg,19003
831
- omlish-0.0.0.dev459.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
832
- omlish-0.0.0.dev459.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
833
- omlish-0.0.0.dev459.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
834
- omlish-0.0.0.dev459.dist-info/RECORD,,
829
+ omlish-0.0.0.dev461.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
830
+ omlish-0.0.0.dev461.dist-info/METADATA,sha256=77pvoCEFopTc7xv_Ej86VMdyzb88vIfaZA22GkYO16c,19003
831
+ omlish-0.0.0.dev461.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
832
+ omlish-0.0.0.dev461.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
833
+ omlish-0.0.0.dev461.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
834
+ omlish-0.0.0.dev461.dist-info/RECORD,,