omlish 0.0.0.dev447__py3-none-any.whl → 0.0.0.dev493__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.

Files changed (265) hide show
  1. omlish/.omlish-manifests.json +12 -0
  2. omlish/README.md +199 -0
  3. omlish/__about__.py +21 -16
  4. omlish/argparse/all.py +17 -9
  5. omlish/argparse/cli.py +16 -3
  6. omlish/argparse/utils.py +21 -0
  7. omlish/asyncs/asyncio/rlock.py +110 -0
  8. omlish/asyncs/asyncio/sync.py +43 -0
  9. omlish/asyncs/asyncio/utils.py +2 -0
  10. omlish/asyncs/sync.py +25 -0
  11. omlish/bootstrap/_marshal.py +1 -1
  12. omlish/bootstrap/diag.py +12 -21
  13. omlish/bootstrap/main.py +2 -5
  14. omlish/bootstrap/sys.py +27 -28
  15. omlish/cexts/__init__.py +0 -0
  16. omlish/cexts/include/omlish/omlish.hh +1 -0
  17. omlish/collections/__init__.py +13 -1
  18. omlish/collections/attrregistry.py +210 -0
  19. omlish/collections/cache/impl.py +1 -0
  20. omlish/collections/identity.py +1 -0
  21. omlish/collections/mappings.py +28 -0
  22. omlish/collections/trie.py +5 -1
  23. omlish/collections/utils.py +77 -0
  24. omlish/concurrent/all.py +2 -1
  25. omlish/concurrent/futures.py +25 -0
  26. omlish/concurrent/threadlets.py +1 -1
  27. omlish/daemons/reparent.py +2 -3
  28. omlish/daemons/spawning.py +2 -3
  29. omlish/dataclasses/__init__.py +2 -0
  30. omlish/dataclasses/impl/api/classes/decorator.py +3 -0
  31. omlish/dataclasses/impl/api/classes/make.py +3 -0
  32. omlish/dataclasses/impl/concerns/repr.py +15 -2
  33. omlish/dataclasses/impl/configs.py +97 -37
  34. omlish/dataclasses/impl/generation/compilation.py +21 -19
  35. omlish/dataclasses/impl/generation/globals.py +1 -0
  36. omlish/dataclasses/impl/generation/ops.py +1 -0
  37. omlish/dataclasses/impl/generation/plans.py +2 -17
  38. omlish/dataclasses/impl/generation/processor.py +106 -25
  39. omlish/dataclasses/impl/processing/base.py +8 -0
  40. omlish/dataclasses/impl/processing/driving.py +19 -7
  41. omlish/dataclasses/specs.py +1 -0
  42. omlish/dataclasses/tools/modifiers.py +5 -0
  43. omlish/diag/_pycharm/runhack.py +1 -1
  44. omlish/diag/cmds/__init__.py +0 -0
  45. omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
  46. omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
  47. omlish/diag/{ps.py → cmds/ps.py} +6 -6
  48. omlish/diag/pycharm.py +16 -2
  49. omlish/diag/pydevd.py +58 -40
  50. omlish/diag/replserver/console.py +1 -1
  51. omlish/dispatch/__init__.py +18 -12
  52. omlish/dispatch/methods.py +50 -140
  53. omlish/dom/rendering.py +1 -1
  54. omlish/formats/dotenv.py +1 -1
  55. omlish/formats/json/stream/__init__.py +13 -0
  56. omlish/funcs/guard.py +225 -0
  57. omlish/graphs/dot/rendering.py +1 -1
  58. omlish/http/all.py +44 -4
  59. omlish/http/clients/asyncs.py +33 -27
  60. omlish/http/clients/base.py +17 -1
  61. omlish/http/clients/coro/__init__.py +0 -0
  62. omlish/http/clients/coro/sync.py +171 -0
  63. omlish/http/clients/default.py +208 -29
  64. omlish/http/clients/executor.py +56 -0
  65. omlish/http/clients/httpx.py +72 -11
  66. omlish/http/clients/middleware.py +181 -0
  67. omlish/http/clients/sync.py +33 -27
  68. omlish/http/clients/syncasync.py +49 -0
  69. omlish/http/clients/urllib.py +6 -3
  70. omlish/http/coro/client/connection.py +15 -6
  71. omlish/http/coro/io.py +2 -0
  72. omlish/http/flasky/__init__.py +40 -0
  73. omlish/http/flasky/_compat.py +2 -0
  74. omlish/http/flasky/api.py +82 -0
  75. omlish/http/flasky/app.py +203 -0
  76. omlish/http/flasky/cvs.py +59 -0
  77. omlish/http/flasky/requests.py +20 -0
  78. omlish/http/flasky/responses.py +23 -0
  79. omlish/http/flasky/routes.py +23 -0
  80. omlish/http/flasky/types.py +15 -0
  81. omlish/http/urls.py +67 -0
  82. omlish/inject/__init__.py +57 -29
  83. omlish/inject/_dataclasses.py +5148 -0
  84. omlish/inject/binder.py +11 -52
  85. omlish/inject/eagers.py +2 -0
  86. omlish/inject/elements.py +27 -0
  87. omlish/inject/helpers/__init__.py +0 -0
  88. omlish/inject/{utils.py → helpers/constfn.py} +3 -3
  89. omlish/inject/{tags.py → helpers/id.py} +2 -2
  90. omlish/inject/helpers/late.py +76 -0
  91. omlish/inject/{managed.py → helpers/managed.py} +10 -10
  92. omlish/inject/helpers/multis.py +143 -0
  93. omlish/inject/helpers/wrappers.py +54 -0
  94. omlish/inject/impl/elements.py +54 -21
  95. omlish/inject/impl/injector.py +29 -25
  96. omlish/inject/impl/inspect.py +10 -1
  97. omlish/inject/impl/maysync.py +3 -4
  98. omlish/inject/impl/multis.py +3 -0
  99. omlish/inject/impl/sync.py +3 -4
  100. omlish/inject/injector.py +31 -2
  101. omlish/inject/inspect.py +35 -0
  102. omlish/inject/maysync.py +2 -4
  103. omlish/inject/multis.py +8 -0
  104. omlish/inject/overrides.py +3 -3
  105. omlish/inject/privates.py +6 -0
  106. omlish/inject/providers.py +3 -2
  107. omlish/inject/sync.py +5 -4
  108. omlish/io/buffers.py +118 -0
  109. omlish/io/readers.py +29 -0
  110. omlish/iterators/transforms.py +2 -2
  111. omlish/lang/__init__.py +180 -97
  112. omlish/lang/_asyncs.cc +186 -0
  113. omlish/lang/asyncs.py +17 -0
  114. omlish/lang/casing.py +11 -0
  115. omlish/lang/contextmanagers.py +28 -4
  116. omlish/lang/functions.py +31 -22
  117. omlish/lang/imports/_capture.cc +11 -9
  118. omlish/lang/imports/capture.py +363 -170
  119. omlish/lang/imports/proxy.py +455 -152
  120. omlish/lang/lazyglobals.py +22 -9
  121. omlish/lang/params.py +17 -0
  122. omlish/lang/recursion.py +0 -1
  123. omlish/lang/sequences.py +124 -0
  124. omlish/lifecycles/README.md +30 -0
  125. omlish/lifecycles/__init__.py +87 -13
  126. omlish/lifecycles/_dataclasses.py +1388 -0
  127. omlish/lifecycles/base.py +178 -64
  128. omlish/lifecycles/contextmanagers.py +113 -4
  129. omlish/lifecycles/controller.py +150 -87
  130. omlish/lifecycles/injection.py +143 -0
  131. omlish/lifecycles/listeners.py +56 -0
  132. omlish/lifecycles/managed.py +142 -0
  133. omlish/lifecycles/manager.py +218 -93
  134. omlish/lifecycles/states.py +2 -0
  135. omlish/lifecycles/transitions.py +3 -0
  136. omlish/lifecycles/unwrap.py +57 -0
  137. omlish/lite/abstract.py +54 -24
  138. omlish/lite/asyncs.py +2 -2
  139. omlish/lite/attrops.py +2 -0
  140. omlish/lite/contextmanagers.py +4 -4
  141. omlish/lite/dataclasses.py +44 -0
  142. omlish/lite/maybes.py +8 -0
  143. omlish/lite/maysync.py +1 -5
  144. omlish/lite/pycharm.py +1 -1
  145. omlish/lite/typing.py +24 -0
  146. omlish/logs/_amalg.py +1 -1
  147. omlish/logs/all.py +25 -11
  148. omlish/logs/asyncs.py +73 -0
  149. omlish/logs/base.py +101 -12
  150. omlish/logs/contexts.py +4 -1
  151. omlish/logs/lists.py +125 -0
  152. omlish/logs/modules.py +19 -1
  153. omlish/logs/std/loggers.py +6 -1
  154. omlish/logs/std/noisy.py +11 -9
  155. omlish/logs/{standard.py → std/standard.py} +3 -4
  156. omlish/logs/utils.py +17 -2
  157. omlish/manifests/loading.py +2 -1
  158. omlish/marshal/__init__.py +33 -13
  159. omlish/marshal/_dataclasses.py +2774 -0
  160. omlish/marshal/base/configs.py +12 -0
  161. omlish/marshal/base/contexts.py +36 -21
  162. omlish/marshal/base/funcs.py +8 -11
  163. omlish/marshal/base/options.py +8 -0
  164. omlish/marshal/base/registries.py +146 -44
  165. omlish/marshal/base/types.py +40 -16
  166. omlish/marshal/composite/iterables.py +33 -20
  167. omlish/marshal/composite/literals.py +20 -18
  168. omlish/marshal/composite/mappings.py +36 -23
  169. omlish/marshal/composite/maybes.py +29 -19
  170. omlish/marshal/composite/newtypes.py +16 -16
  171. omlish/marshal/composite/optionals.py +14 -14
  172. omlish/marshal/composite/special.py +15 -15
  173. omlish/marshal/composite/unions/__init__.py +0 -0
  174. omlish/marshal/composite/unions/literals.py +93 -0
  175. omlish/marshal/composite/unions/primitives.py +103 -0
  176. omlish/marshal/factories/invalidate.py +18 -68
  177. omlish/marshal/factories/method.py +26 -0
  178. omlish/marshal/factories/moduleimport/factories.py +22 -65
  179. omlish/marshal/factories/multi.py +13 -25
  180. omlish/marshal/factories/recursive.py +42 -56
  181. omlish/marshal/factories/typecache.py +29 -74
  182. omlish/marshal/factories/typemap.py +42 -43
  183. omlish/marshal/objects/dataclasses.py +129 -106
  184. omlish/marshal/objects/marshal.py +18 -14
  185. omlish/marshal/objects/namedtuples.py +48 -42
  186. omlish/marshal/objects/unmarshal.py +19 -15
  187. omlish/marshal/polymorphism/marshal.py +9 -11
  188. omlish/marshal/polymorphism/metadata.py +16 -5
  189. omlish/marshal/polymorphism/standard.py +13 -1
  190. omlish/marshal/polymorphism/unions.py +15 -105
  191. omlish/marshal/polymorphism/unmarshal.py +9 -10
  192. omlish/marshal/singular/enums.py +14 -18
  193. omlish/marshal/standard.py +10 -6
  194. omlish/marshal/trivial/any.py +1 -1
  195. omlish/marshal/trivial/forbidden.py +21 -26
  196. omlish/metadata.py +23 -1
  197. omlish/os/forkhooks.py +4 -0
  198. omlish/os/pidfiles/pinning.py +2 -2
  199. omlish/reflect/__init__.py +43 -26
  200. omlish/reflect/ops.py +10 -1
  201. omlish/reflect/types.py +1 -0
  202. omlish/secrets/marshal.py +1 -1
  203. omlish/specs/jmespath/__init__.py +12 -3
  204. omlish/specs/jmespath/_dataclasses.py +2893 -0
  205. omlish/specs/jmespath/ast.py +1 -1
  206. omlish/specs/jsonrpc/__init__.py +13 -0
  207. omlish/specs/jsonrpc/_marshal.py +32 -23
  208. omlish/specs/jsonrpc/conns.py +10 -7
  209. omlish/specs/jsonschema/_marshal.py +1 -1
  210. omlish/specs/jsonschema/keywords/__init__.py +7 -0
  211. omlish/specs/jsonschema/keywords/_dataclasses.py +1644 -0
  212. omlish/specs/openapi/_marshal.py +31 -22
  213. omlish/sql/__init__.py +24 -5
  214. omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
  215. omlish/sql/api/dbapi.py +1 -1
  216. omlish/sql/dbapi/__init__.py +15 -0
  217. omlish/sql/{dbapi.py → dbapi/drivers.py} +2 -2
  218. omlish/sql/queries/__init__.py +3 -0
  219. omlish/sql/queries/_marshal.py +2 -2
  220. omlish/sql/queries/rendering.py +1 -1
  221. omlish/sql/tabledefs/_marshal.py +1 -1
  222. omlish/subprocesses/base.py +4 -0
  223. omlish/subprocesses/editor.py +1 -1
  224. omlish/sync.py +155 -21
  225. omlish/term/alt.py +60 -0
  226. omlish/term/confirm.py +8 -8
  227. omlish/term/pager.py +235 -0
  228. omlish/term/terminfo.py +935 -0
  229. omlish/term/termstate.py +67 -0
  230. omlish/term/vt100/terminal.py +0 -3
  231. omlish/testing/pytest/plugins/asyncs/fixtures.py +4 -1
  232. omlish/testing/pytest/plugins/asyncs/plugin.py +2 -0
  233. omlish/testing/pytest/plugins/skips.py +2 -1
  234. omlish/testing/unittest/main.py +3 -3
  235. omlish/text/docwrap/__init__.py +3 -0
  236. omlish/text/docwrap/__main__.py +11 -0
  237. omlish/text/docwrap/api.py +83 -0
  238. omlish/text/docwrap/cli.py +91 -0
  239. omlish/text/docwrap/groups.py +84 -0
  240. omlish/text/docwrap/lists.py +167 -0
  241. omlish/text/docwrap/parts.py +146 -0
  242. omlish/text/docwrap/reflowing.py +106 -0
  243. omlish/text/docwrap/rendering.py +151 -0
  244. omlish/text/docwrap/utils.py +11 -0
  245. omlish/text/docwrap/wrapping.py +59 -0
  246. omlish/text/filecache.py +2 -2
  247. omlish/text/lorem.py +6 -0
  248. omlish/text/parts.py +2 -2
  249. omlish/text/textwrap.py +51 -0
  250. omlish/typedvalues/marshal.py +85 -59
  251. omlish/typedvalues/values.py +2 -1
  252. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/METADATA +36 -32
  253. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/RECORD +260 -199
  254. omlish/dataclasses/impl/generation/mangling.py +0 -18
  255. omlish/funcs/match.py +0 -227
  256. omlish/lifecycles/abstract.py +0 -86
  257. omlish/marshal/factories/match.py +0 -34
  258. omlish/marshal/factories/simple.py +0 -28
  259. /omlish/inject/{impl → helpers}/proxy.py +0 -0
  260. /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
  261. /omlish/sql/{abc.py → dbapi/abc.py} +0 -0
  262. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/WHEEL +0 -0
  263. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/entry_points.txt +0 -0
  264. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/licenses/LICENSE +0 -0
  265. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,28 @@
1
1
  """
2
2
  TODO:
3
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
4
22
  """
5
- import contextlib
6
23
  import functools
7
24
  import importlib.util
25
+ import threading
8
26
  import types
9
27
  import typing as ta
10
28
 
@@ -13,138 +31,349 @@ from .capture import ImportCapture
13
31
  from .capture import _new_import_capture_hook
14
32
 
15
33
 
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
+ ]
40
+
41
+
16
42
  ##
17
43
 
18
44
 
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)
45
+ class _ProxyImporter:
46
+ def __init__(
47
+ self,
48
+ *,
49
+ owner_globals: ta.MutableMapping[str, ta.Any] | None = None,
50
+ ) -> None:
51
+ super().__init__()
26
52
 
27
- omod = None
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'
28
136
 
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)
137
+ else:
138
+ return None
37
139
 
38
- lmod = types.ModuleType(spec)
39
- lmod.__getattr__ = __getattr__ # type: ignore[method-assign]
40
- return lmod
140
+ #
41
141
 
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
42
147
 
43
- #
148
+ parent: _ProxyImporter._Module | None = None
149
+ if '.' in name:
150
+ rest, _, attr = name.rpartition('.')
151
+ parent = self._get_or_make_module_locked(rest)
44
152
 
153
+ fa = parent.find_attr(attr)
154
+ if not (fa == 'pending_child' or fa is None):
155
+ raise RuntimeError
45
156
 
46
- def auto_proxy_import(
47
- mod_globals: ta.MutableMapping[str, ta.Any],
48
- *,
49
- disable: bool = False,
157
+ if (ro := parent.real_obj) is not None and attr not in ro.__dict__:
158
+ raise NotImplementedError
50
159
 
51
- unreferenced_callback: ta.Callable[[ta.Mapping[str, ta.Sequence[str | None]]], None] | None = None,
52
- raise_unreferenced: bool = False,
160
+ module = self._modules_by_name[name] = _ProxyImporter._Module(
161
+ name,
162
+ self._handle_module_getattr,
163
+ )
53
164
 
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
- )
165
+ self._modules_by_name[name] = module
166
+ self._modules_by_proxy_obj[module.proxy_obj] = module
64
167
 
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
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)
72
173
 
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)
174
+ return module
77
175
 
78
- return inner()
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
79
187
 
188
+ if (ro := module.real_obj) is not None and n in ro.__dict__:
189
+ raise NotImplementedError
80
190
 
81
- ##
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
82
195
 
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
83
200
 
84
- class _ProxyInit:
85
- class NamePackage(ta.NamedTuple):
86
- name: str
87
- package: str
201
+ #
88
202
 
89
- class _Import(ta.NamedTuple):
90
- spec: str
91
- attr: str | None
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)
92
207
 
93
- def __init__(
94
- self,
95
- lazy_globals: LazyGlobals,
96
- name_package: NamePackage,
97
- ) -> None:
98
- super().__init__()
208
+ def _retrieve_from_module_locked(self, module: _Module, attr: str) -> ta.Any:
209
+ fa = module.find_attr(attr)
210
+
211
+ if fa == 'child' or fa == 'proxy_attr':
212
+ return module.proxy_obj.__dict__[attr]
213
+
214
+ val: ta.Any
215
+
216
+ if fa == 'pending_child':
217
+ if module.name == self._owner_name:
218
+ val = importlib.import_module(f'{module.name}.{attr}')
219
+
220
+ else:
221
+ mod = __import__(
222
+ module.name,
223
+ self._owner_globals or {},
224
+ {},
225
+ [attr],
226
+ 0,
227
+ )
228
+
229
+ val = getattr(mod, attr)
230
+
231
+ module.pending_children.remove(attr)
232
+
233
+ elif fa == 'pending_attr' or fa is None:
234
+ if module.name == self._owner_name:
235
+ raise NotImplementedError
99
236
 
100
- self._lazy_globals = lazy_globals
101
- self._name_package = name_package
237
+ if (ro := module.real_obj) is None:
238
+ ro = module.real_obj = importlib.import_module(module.name)
102
239
 
103
- self._imps_by_attr: dict[str, _ProxyInit._Import] = {}
104
- self._mods_by_spec: dict[str, ta.Any] = {}
240
+ val = getattr(ro, attr)
105
241
 
106
- @property
107
- def name_package(self) -> NamePackage:
108
- return self._name_package
242
+ if fa == 'pending_attr':
243
+ module.pending_attrs.remove(attr)
244
+
245
+ else:
246
+ raise TypeError(fa)
247
+
248
+ setattr(module.proxy_obj, attr, val)
249
+ return val
250
+
251
+ #
252
+
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)
109
256
 
110
257
  def add(
111
258
  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))
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
+ )
281
+
282
+ return m.proxy_obj
283
+
284
+ def get_module(self, name: str) -> types.ModuleType:
285
+ try:
286
+ return self._modules_by_name[name].proxy_obj
287
+ except KeyError:
288
+ pass
119
289
 
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))
290
+ with self._lock:
291
+ return self._get_or_make_module_locked(name).proxy_obj
123
292
 
124
- def _import_module(self, name: str) -> ta.Any:
125
- return importlib.import_module(name, package=self._name_package.package)
293
+ def lookup(self, spec: str) -> ta.Any:
294
+ if '.' not in spec:
295
+ return self._modules_by_name[spec].proxy_obj
126
296
 
127
- def get(self, attr: str) -> ta.Any:
128
297
  try:
129
- imp = self._imps_by_attr[attr]
298
+ module = self._modules_by_name[spec]
130
299
  except KeyError:
131
- raise AttributeError(attr) # noqa
300
+ pass
301
+ else:
302
+ return module.proxy_obj
132
303
 
133
- val: ta.Any
304
+ rest, _, attr = spec.rpartition('.')
305
+ module = self._modules_by_name[rest]
306
+ return getattr(module.proxy_obj, attr)
134
307
 
135
- if imp.attr is None:
136
- val = self._import_module(imp.spec)
137
308
 
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
309
+ #
144
310
 
145
- val = getattr(mod, imp.attr)
146
311
 
147
- return val
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
+ *,
346
+ no_cache: bool = False,
347
+ ) -> types.ModuleType:
348
+ """'Legacy' proxy import mechanism."""
349
+
350
+ if isinstance(extras, str):
351
+ raise TypeError(extras)
352
+
353
+ omod = None
354
+
355
+ def __getattr__(att): # noqa
356
+ nonlocal omod
357
+
358
+ if omod is None:
359
+ omod = importlib.import_module(spec, package=package)
360
+ if extras:
361
+ for x in extras:
362
+ importlib.import_module(f'{spec}.{x}', package=package)
363
+
364
+ v = getattr(omod, att)
365
+
366
+ if not no_cache:
367
+ setattr(lmod, att, v)
368
+
369
+ return v
370
+
371
+ lmod = types.ModuleType(spec)
372
+ lmod.__getattr__ = __getattr__ # type: ignore[method-assign]
373
+ return lmod
374
+
375
+
376
+ #
148
377
 
149
378
 
150
379
  def proxy_init(
@@ -152,6 +381,13 @@ def proxy_init(
152
381
  spec: str,
153
382
  attrs: ta.Iterable[str | tuple[str | None, str | None] | None] | None = None,
154
383
  ) -> None:
384
+ name = importlib.util.resolve_name(
385
+ spec,
386
+ package=init_globals['__package__'] if spec.startswith('.') else None,
387
+ )
388
+
389
+ #
390
+
155
391
  if isinstance(attrs, str):
156
392
  raise TypeError(attrs)
157
393
 
@@ -181,76 +417,143 @@ def proxy_init(
181
417
 
182
418
  #
183
419
 
184
- init_name_package = _ProxyInit.NamePackage(
185
- init_globals['__name__'],
186
- init_globals['__package__'],
420
+ pi = _get_module_proxy_importer(init_globals)
421
+ lg = LazyGlobals.install(init_globals)
422
+
423
+ pi.add(
424
+ name,
425
+ children=[r for l, r in al if r is not None],
187
426
  )
188
427
 
189
- pi: _ProxyInit
190
- try:
191
- pi = init_globals['__proxy_init__']
428
+ for imp_attr, as_attr in al:
429
+ lg.set_fn(as_attr, functools.partial(pi.lookup, name if imp_attr is None else f'{name}.{imp_attr}'))
192
430
 
193
- except KeyError:
194
- pi = _ProxyInit(
195
- LazyGlobals.install(init_globals),
196
- init_name_package,
431
+
432
+ ##
433
+
434
+
435
+ class _AutoProxy:
436
+ def __init__(
437
+ self,
438
+ mod_globals: ta.MutableMapping[str, ta.Any],
439
+ *,
440
+ disable: bool = False,
441
+ eager: bool = False,
442
+
443
+ unreferenced_callback: ta.Callable[[ta.Sequence[str]], None] | None = None,
444
+ raise_unreferenced: bool = False,
445
+
446
+ update_exports: bool = False,
447
+
448
+ _stack_offset: int = 0,
449
+ _capture_impl: str | None = None,
450
+ ) -> None:
451
+ super().__init__()
452
+
453
+ self._mod_globals = mod_globals
454
+
455
+ self._disabled = disable
456
+ self._eager = eager
457
+
458
+ self._unreferenced_callback = unreferenced_callback
459
+ self._raise_unreferenced = raise_unreferenced
460
+
461
+ self._update_exports = update_exports
462
+
463
+ self._ic = ImportCapture(
464
+ mod_globals,
465
+ _hook=_new_import_capture_hook(
466
+ mod_globals,
467
+ stack_offset=_stack_offset + 1,
468
+ capture_impl=_capture_impl,
469
+ ),
470
+ disable=disable,
197
471
  )
198
- init_globals['__proxy_init__'] = pi
472
+ self._icc: ta.Any = None
199
473
 
200
- else:
201
- if pi.name_package != init_name_package:
202
- raise Exception(f'Wrong init name: {pi.name_package=} != {init_name_package=}')
474
+ def __enter__(self) -> ImportCapture:
475
+ if self._icc is not None:
476
+ raise RuntimeError
203
477
 
204
- pi.add(spec, al)
478
+ self._icc = self._ic.capture(
479
+ unreferenced_callback=self._unreferenced_callback,
480
+ raise_unreferenced=self._raise_unreferenced,
481
+ )
205
482
 
483
+ return self._icc.__enter__() # noqa
206
484
 
207
- #
485
+ def __exit__(self, exc_type, exc_val, exc_tb):
486
+ if self._icc is None:
487
+ raise RuntimeError
208
488
 
489
+ self._icc.__exit__(exc_type, exc_val, exc_tb)
209
490
 
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
- )
491
+ if not self._disabled and exc_type is None:
492
+ self._install()
493
+
494
+ # @abc.abstractmethod
495
+ def _install(self) -> None:
496
+ raise NotImplementedError
497
+
498
+
499
+ @ta.final
500
+ class _AutoProxyImport(_AutoProxy):
501
+ def _install(self) -> None:
502
+ cap = self._ic.captured
503
+
504
+ for cm in cap.modules.values():
505
+ if cm.attrs:
506
+ raise RuntimeError
507
+
508
+ pi = _get_module_proxy_importer(self._mod_globals)
231
509
 
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,
510
+ for cm in cap.modules.values():
511
+ pi.add(
512
+ cm.name,
513
+ children=cm.children,
245
514
  )
246
515
 
247
- if eager:
248
- lg = LazyGlobals.install(init_globals)
516
+ for ci in cap.imports.values():
517
+ pm = pi.get_module(ci.module.name)
518
+ for a in ci.as_ or ():
519
+ self._mod_globals[a] = pm
249
520
 
250
- for a in inst.captured.attrs:
521
+ if self._eager:
522
+ for ci in cap.imports.values():
523
+ pi.lookup(ci.module.name)
524
+
525
+ if self._update_exports:
526
+ self._ic.update_exports()
527
+
528
+
529
+ @ta.final
530
+ class _AutoProxyInit(_AutoProxy):
531
+ def _install(self) -> None:
532
+ cap = self._ic.captured
533
+
534
+ pi = _get_module_proxy_importer(self._mod_globals)
535
+ lg = LazyGlobals.install(self._mod_globals)
536
+
537
+ for cm in cap.modules.values():
538
+ pi.add(
539
+ cm.name,
540
+ children=cm.children,
541
+ attrs=cm.attrs,
542
+ )
543
+
544
+ for ci in cap.imports.values():
545
+ for a in ci.as_ or ():
546
+ lg.set_fn(a, functools.partial(pi.lookup, ci.module.name))
547
+ for sa, da in ci.attrs or ():
548
+ lg.set_fn(da, functools.partial(pi.lookup, f'{ci.module.name}.{sa}'))
549
+
550
+ if self._eager:
551
+ for a in cap.attrs:
251
552
  lg.get(a)
252
553
 
253
- if update_exports:
254
- inst.update_exports()
554
+ if self._update_exports:
555
+ self._ic.update_exports()
556
+
255
557
 
256
- return inner()
558
+ auto_proxy_import = _AutoProxyImport
559
+ auto_proxy_init = _AutoProxyInit