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
@@ -9,10 +9,13 @@ Insufficient alt impls:
9
9
 
10
10
  Possible alt impls:
11
11
  - aot static analysis, codegen, compare, if valid skip ctxmgr body and inject proxies, otherwise warn and run
12
+ - or just gen code inline, if ta.TYPE_CHECKING: .... else: # @omlish-generate-auto-proxy-import/init
13
+ - generate classic `foo = _lang.proxy_import('.foo', __package__)` blocks
12
14
  """
13
15
  import builtins
14
16
  import contextlib
15
17
  import functools
18
+ import importlib.util
16
19
  import sys
17
20
  import threading
18
21
  import types
@@ -44,14 +47,27 @@ class ImportCaptureErrors:
44
47
  return f'{self.__class__.__qualname__}(module={self.module!r}, name={self.name!r})'
45
48
 
46
49
  class ImportError(ImportCaptureError): # noqa
47
- def __init__(self, module: str, from_list: ta.Sequence[str] | None) -> None:
50
+ def __init__(
51
+ self,
52
+ name: str,
53
+ *,
54
+ level: int | None = None,
55
+ from_list: ta.Sequence[str] | None,
56
+ ) -> None:
48
57
  super().__init__()
49
58
 
50
- self.module = module
59
+ self.name = name
60
+ self.level = level
51
61
  self.from_list = from_list
52
62
 
53
63
  def __repr__(self) -> str:
54
- return f'{self.__class__.__qualname__}(module={self.module!r}, from_list={self.from_list!r})'
64
+ return ''.join([
65
+ f'{self.__class__.__qualname__}(',
66
+ f'name={self.name!r}',
67
+ *([f', level={self.level!r}'] if self.level is not None else []),
68
+ *([f', from_list={self.from_list!r}'] if self.from_list is not None else []),
69
+ ')',
70
+ ])
55
71
 
56
72
  class ImportStarForbiddenError(ImportError):
57
73
  pass
@@ -60,7 +76,7 @@ class ImportCaptureErrors:
60
76
  pass
61
77
 
62
78
  class UnreferencedImportsError(ImportCaptureError):
63
- def __init__(self, unreferenced: ta.Mapping[str, ta.Sequence[str | None]]) -> None:
79
+ def __init__(self, unreferenced: ta.Sequence[str]) -> None:
64
80
  super().__init__()
65
81
 
66
82
  self.unreferenced = unreferenced
@@ -76,136 +92,138 @@ class ImportCaptureErrors:
76
92
 
77
93
 
78
94
  class _ImportCaptureHook:
79
- class ModuleSpec(ta.NamedTuple):
80
- name: str
81
- level: int
82
-
83
- def __str__(self) -> str:
84
- return f'{"." * self.level}{self.name}'
85
-
86
- def __repr__(self) -> str:
87
- return repr(str(self))
88
-
89
95
  def __init__(
90
96
  self,
91
97
  *,
98
+ package: str | None = None,
92
99
  forbid_uncaptured_imports: bool = False,
93
100
  ) -> None:
94
101
  super().__init__()
95
102
 
103
+ self._package = package
96
104
  self._forbid_uncaptured_imports = forbid_uncaptured_imports
97
105
 
98
- self._modules_by_spec: dict[_ImportCaptureHook.ModuleSpec, _ImportCaptureHook._Module] = {}
106
+ self._modules_by_name: dict[str, _ImportCaptureHook._Module] = {}
99
107
  self._modules_by_module_obj: dict[types.ModuleType, _ImportCaptureHook._Module] = {}
100
108
 
101
- self._attrs: dict[_ImportCaptureHook._ModuleAttr, tuple[_ImportCaptureHook._Module, str]] = {}
102
-
103
109
  #
104
110
 
105
- class _ModuleAttr:
111
+ class _Module:
106
112
  def __init__(
107
113
  self,
108
- module: '_ImportCaptureHook._Module',
109
114
  name: str,
115
+ getattr_handler: ta.Callable[['_ImportCaptureHook._Module', str], ta.Any],
116
+ *,
117
+ parent: ta.Optional['_ImportCaptureHook._Module'] = None,
110
118
  ) -> None:
111
119
  super().__init__()
112
120
 
113
- self.__module = module
114
- self.__name = name
121
+ if name.startswith('.'):
122
+ raise ImportCaptureError
115
123
 
116
- def __repr__(self) -> str:
117
- return f'<{self.__class__.__name__}: {f"{self.__module.spec}:{self.__name}"!r}>'
124
+ self.name = name
125
+ self.parent = parent
118
126
 
119
- class _Module:
120
- def __init__(
121
- self,
122
- spec: '_ImportCaptureHook.ModuleSpec',
123
- *,
124
- getattr_handler: ta.Callable[['_ImportCaptureHook._Module', str], ta.Any] | None = None,
125
- ) -> None:
126
- super().__init__()
127
+ self.base_name = name.rpartition('.')[2]
128
+ self.root: _ImportCaptureHook._Module = parent.root if parent is not None else self # noqa
127
129
 
128
- self.spec = spec
130
+ self.children: dict[str, _ImportCaptureHook._Module] = {}
131
+ self.descendants: set[_ImportCaptureHook._Module] = set()
129
132
 
130
- self.module_obj = types.ModuleType(f'<{self.__class__.__qualname__}: {spec!r}>')
131
- if getattr_handler is not None:
132
- self.module_obj.__getattr__ = functools.partial(getattr_handler, self) # type: ignore[method-assign] # noqa
133
+ self.module_obj = types.ModuleType(f'<{self.__class__.__qualname__}: {name}>')
134
+ self.module_obj.__file__ = None
135
+ self.module_obj.__getattr__ = functools.partial(getattr_handler, self) # type: ignore[method-assign] # noqa
133
136
  self.initial_module_dict = dict(self.module_obj.__dict__)
134
137
 
135
- self.contents: dict[str, _ImportCaptureHook._ModuleAttr | types.ModuleType] = {}
136
- self.imported_whole = False
138
+ self.explicit = False
139
+ self.immediate = False
137
140
 
138
141
  def __repr__(self) -> str:
139
- return f'{self.__class__.__name__}({self.spec!r})'
142
+ return f'{self.__class__.__name__}<{self.name}{"!" if self.immediate else "+" if self.explicit else ""}>'
140
143
 
141
- def _get_or_make_module(self, spec: ModuleSpec) -> _Module:
144
+ def set_explicit(self) -> None:
145
+ cur: _ImportCaptureHook._Module | None = self
146
+ while cur is not None and not cur.explicit:
147
+ cur.explicit = True
148
+ cur = cur.parent
149
+
150
+ #
151
+
152
+ @property
153
+ def _modules(self) -> ta.Sequence[_Module]:
154
+ return sorted(self._modules_by_name.values(), key=lambda m: m.name)
155
+
156
+ def _get_or_make_module(self, name: str) -> _Module:
142
157
  try:
143
- return self._modules_by_spec[spec]
158
+ return self._modules_by_name[name]
144
159
  except KeyError:
145
160
  pass
146
161
 
147
- module = self._Module(
148
- spec,
149
- getattr_handler=self._handle_module_getattr,
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)
168
+
169
+ module = _ImportCaptureHook._Module(
170
+ name,
171
+ self._handle_module_getattr,
172
+ parent=parent,
150
173
  )
151
- self._modules_by_spec[spec] = module
174
+ self._modules_by_name[name] = module
152
175
  self._modules_by_module_obj[module.module_obj] = module
176
+
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)
181
+
153
182
  return module
154
183
 
155
- def _handle_module_getattr(self, module: _Module, attr: str) -> ta.Any:
156
- if attr in module.contents:
157
- raise ImportCaptureErrors.AttrError(str(module.spec), attr)
184
+ def _make_child_module(self, module: _Module, attr: str) -> _Module:
185
+ if attr in module.children:
186
+ raise ImportCaptureErrors.AttrError(module.name, attr)
158
187
 
159
- v: _ImportCaptureHook._ModuleAttr | types.ModuleType
160
- if not module.spec.name:
161
- if not module.spec.level:
162
- raise ImportCaptureError
163
- cs = _ImportCaptureHook.ModuleSpec(attr, module.spec.level)
164
- cm = self._get_or_make_module(cs)
165
- cm.imported_whole = True
166
- v = cm.module_obj
188
+ return self._get_or_make_module(f'{module.name}.{attr}')
167
189
 
168
- else:
169
- ma = _ImportCaptureHook._ModuleAttr(module, attr)
170
- self._attrs[ma] = (module, attr)
171
- v = ma
190
+ #
191
+
192
+ def _handle_module_getattr(self, module: _Module, attr: str) -> ta.Any:
193
+ if not module.explicit:
194
+ raise ImportCaptureErrors.AttrError(module.name, attr)
172
195
 
173
- module.contents[attr] = v
174
- setattr(module.module_obj, attr, v)
175
- return v
196
+ return self._make_child_module(module, attr).module_obj
176
197
 
177
198
  def _handle_import(
178
199
  self,
179
- module: _Module,
200
+ name: str,
180
201
  *,
181
202
  from_list: ta.Sequence[str] | None,
182
- ) -> None:
183
- if from_list is None:
184
- if module.spec.level or not module.spec.name:
185
- raise ImportCaptureError
203
+ ) -> types.ModuleType:
204
+ module = self._get_or_make_module(name)
186
205
 
187
- module.imported_whole = True
206
+ if from_list is None:
207
+ module.set_explicit()
208
+ module.root.immediate = True
209
+ return module.root.module_obj
188
210
 
189
211
  else:
190
212
  for attr in from_list:
191
213
  if attr == '*':
192
- raise ImportCaptureErrors.ImportStarForbiddenError(str(module.spec), from_list)
214
+ raise ImportCaptureErrors.ImportStarForbiddenError(module.name, from_list=from_list)
215
+
216
+ if (cm := module.children.get(attr)) is None:
217
+ cm = self._make_child_module(module, attr)
218
+ cm.set_explicit()
219
+ cm.immediate = True
220
+ continue
193
221
 
194
222
  x = getattr(module.module_obj, attr)
223
+ if x is not cm.module_obj or x not in self._modules_by_module_obj:
224
+ raise ImportCaptureErrors.AttrError(module.name, attr)
195
225
 
196
- bad = False
197
- if x is not module.contents.get(attr):
198
- bad = True
199
- if isinstance(x, _ImportCaptureHook._ModuleAttr):
200
- if self._attrs[x] != (module, attr):
201
- bad = True
202
- elif isinstance(x, types.ModuleType):
203
- if x not in self._modules_by_module_obj:
204
- bad = True
205
- else:
206
- bad = True
207
- if bad:
208
- raise ImportCaptureErrors.AttrError(str(module.spec), attr)
226
+ return module.module_obj
209
227
 
210
228
  #
211
229
 
@@ -225,16 +243,16 @@ class _ImportCaptureHook:
225
243
  ):
226
244
  return None
227
245
 
228
- spec = _ImportCaptureHook.ModuleSpec(name, level)
229
- module = self._get_or_make_module(spec)
246
+ if level:
247
+ if not self._package:
248
+ raise ImportCaptureError
249
+ name = importlib.util.resolve_name(('.' * level) + name, self._package)
230
250
 
231
- self._handle_import(
232
- module,
251
+ return self._handle_import(
252
+ name,
233
253
  from_list=from_list,
234
254
  )
235
255
 
236
- return module.module_obj
237
-
238
256
  @ta.final
239
257
  @contextlib.contextmanager
240
258
  def hook_context(
@@ -269,18 +287,24 @@ class _ImportCaptureHook:
269
287
  self,
270
288
  mod_globals: ta.MutableMapping[str, ta.Any], # noqa
271
289
  ) -> None:
272
- for m in self._modules_by_spec.values():
290
+ for m in self._modules_by_name.values():
291
+ if m.immediate and not m.explicit:
292
+ raise ImportCaptureError
293
+
294
+ if not m.explicit and m.children:
295
+ raise ImportCaptureError
296
+
273
297
  for a, o in m.module_obj.__dict__.items():
274
298
  try:
275
299
  i = m.initial_module_dict[a]
276
300
 
277
301
  except KeyError:
278
- if o is not m.contents[a]:
279
- raise ImportCaptureErrors.AttrError(str(m.spec), a) from None
302
+ if o is not m.children[a].module_obj:
303
+ raise ImportCaptureErrors.AttrError(m.name, a) from None
280
304
 
281
305
  else:
282
306
  if o != i:
283
- raise ImportCaptureErrors.AttrError(str(m.spec), a)
307
+ raise ImportCaptureErrors.AttrError(m.name, a)
284
308
 
285
309
  #
286
310
 
@@ -290,24 +314,20 @@ class _ImportCaptureHook:
290
314
  *,
291
315
  collect_unreferenced: bool = False,
292
316
  ) -> 'ImportCapture.Captured':
293
- dct: dict[_ImportCaptureHook._Module, list[tuple[str | None, str]]] = {}
294
-
295
- rem_whole_mods: set[_ImportCaptureHook._Module] = set()
296
- rem_mod_attrs: set[_ImportCaptureHook._ModuleAttr] = set()
317
+ rem_explicit_mods: set[_ImportCaptureHook._Module] = set()
297
318
  if collect_unreferenced:
298
- rem_whole_mods.update([m for m in self._modules_by_spec.values() if m.imported_whole])
299
- rem_mod_attrs.update(self._attrs)
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
+ )
300
324
 
301
- for attr, obj in mod_globals.items():
302
- if isinstance(obj, _ImportCaptureHook._ModuleAttr):
303
- try:
304
- m, a = self._attrs[obj]
305
- except KeyError:
306
- raise ImportCaptureErrors.AttrError(None, attr) from None
307
- dct.setdefault(m, []).append((a, attr))
308
- rem_mod_attrs.discard(obj)
325
+ #
326
+
327
+ dct: dict[_ImportCaptureHook._Module, list[tuple[str | None, str]]] = {}
309
328
 
310
- elif isinstance(obj, _ImportCaptureHook._Module):
329
+ for attr, obj in mod_globals.items():
330
+ if isinstance(obj, _ImportCaptureHook._Module):
311
331
  raise ImportCaptureErrors.AttrError(None, attr) from None
312
332
 
313
333
  elif isinstance(obj, types.ModuleType):
@@ -315,41 +335,80 @@ class _ImportCaptureHook:
315
335
  m = self._modules_by_module_obj[obj]
316
336
  except KeyError:
317
337
  continue
318
- if not m.imported_whole:
319
- raise RuntimeError(f'ImportCapture module {m.spec!r} not imported_whole')
320
- dct.setdefault(m, []).append((None, attr))
321
- rem_whole_mods.discard(m)
322
-
323
- lst: list[ImportCapture.Import] = []
324
- for m, ts in dct.items():
325
- if not m.spec.name:
326
- if not m.spec.level:
327
- raise ImportCaptureError
328
- for imp_attr, as_attr in ts:
329
- if not imp_attr:
330
- raise RuntimeError
331
- lst.append(ImportCapture.Import(
332
- '.' * m.spec.level + imp_attr,
333
- [(None, as_attr)],
334
- ))
335
338
 
336
- else:
337
- lst.append(ImportCapture.Import(
338
- str(m.spec),
339
- ts,
340
- ))
341
-
342
- unreferenced: dict[str, list[str | None]] | None = None
343
- if collect_unreferenced and (rem_whole_mods or rem_mod_attrs):
344
- unreferenced = {}
345
- for m in rem_whole_mods:
346
- unreferenced.setdefault(str(m.spec), []).append(None)
347
- for ma in rem_mod_attrs:
348
- m, a = self._attrs[ma]
349
- unreferenced.setdefault(str(m.spec), []).append(a)
339
+ if m.explicit:
340
+ dct.setdefault(m, []).append((None, attr))
341
+ if m in rem_explicit_mods:
342
+ # Remove everything reachable from this root *except* items imported immediately, such as
343
+ # `from x import y` - those still need to be immediately reachable.
344
+ rem_explicit_mods -= {dm for dm in m.descendants if not dm.immediate}
345
+ rem_explicit_mods.remove(m)
346
+
347
+ else:
348
+ p = m.parent
349
+ if p is None or not p.explicit:
350
+ raise ImportCaptureError
351
+ dct.setdefault(p, []).append((m.base_name, attr))
352
+
353
+ #
354
+
355
+ mods: dict[str, ImportCapture.Module] = {}
356
+
357
+ def build_import_module(m: _ImportCaptureHook._Module) -> ImportCapture.Module:
358
+ children: dict[str, ImportCapture.Module] = {}
359
+ attrs: list[str] = []
360
+ for cm in sorted(m.children.values(), key=lambda cm: cm.name):
361
+ if not cm.explicit:
362
+ attrs.append(cm.base_name)
363
+ else:
364
+ children[cm.base_name] = build_import_module(cm)
365
+
366
+ mod = ImportCapture.Module(
367
+ m.name,
368
+ children or None,
369
+ attrs or None,
370
+ )
371
+
372
+ if m.parent is None:
373
+ mod.parent = None
374
+ for c in children.values():
375
+ c.parent = mod
376
+
377
+ mods[mod.name] = mod
378
+ return mod
379
+
380
+ root_mods: dict[str, ImportCapture.Module] = {
381
+ m.base_name: build_import_module(m)
382
+ for m in self._modules_by_name.values()
383
+ if m.parent is None
384
+ }
385
+
386
+ mods = dict(sorted(mods.items(), key=lambda t: t[0]))
387
+ root_mods = dict(sorted(root_mods.items(), key=lambda t: t[0]))
388
+
389
+ #
390
+
391
+ imps: list[ImportCapture.Import] = []
392
+
393
+ for m, ts in sorted(dct.items(), key=lambda t: t[0].name):
394
+ imps.append(ImportCapture.Import(
395
+ mods[m.name],
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,
398
+ ))
399
+
400
+ #
401
+
402
+ unreferenced: list[str] | None = None
403
+ if collect_unreferenced and rem_explicit_mods:
404
+ unreferenced = sorted(m.name for m in rem_explicit_mods)
350
405
 
351
406
  return ImportCapture.Captured(
352
- lst,
407
+ {i.module.name: i for i in imps},
408
+
409
+ mods,
410
+ root_mods,
411
+
353
412
  unreferenced,
354
413
  )
355
414
 
@@ -358,6 +417,14 @@ class _ImportCaptureHook:
358
417
 
359
418
 
360
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
+
361
428
  def _new_import(
362
429
  self,
363
430
  old_import,
@@ -377,8 +444,9 @@ class _AbstractBuiltinsImportCaptureHook(_ImportCaptureHook):
377
444
 
378
445
  if self._forbid_uncaptured_imports:
379
446
  raise ImportCaptureErrors.UncapturedImportForbiddenError(
380
- str(_ImportCaptureHook.ModuleSpec(name, level)),
381
- fromlist,
447
+ name,
448
+ level=level,
449
+ from_list=fromlist,
382
450
  )
383
451
 
384
452
  return old_import(
@@ -513,11 +581,24 @@ class _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook(_AbstractBuiltinsImport
513
581
  #
514
582
 
515
583
 
516
- _capture: ta.Any = None
517
- try:
518
- from . import _capture # type: ignore
519
- except ImportError:
520
- 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
521
602
 
522
603
 
523
604
  class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
@@ -537,7 +618,7 @@ class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
537
618
  frame: types.FrameType,
538
619
  new_builtins: dict[str, ta.Any],
539
620
  ) -> bool:
540
- 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
541
622
 
542
623
  @contextlib.contextmanager
543
624
  def _hook_context(
@@ -567,39 +648,155 @@ class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
567
648
  #
568
649
 
569
650
 
651
+ _CAPTURE_IMPLS: ta.Mapping[str, type[_AbstractBuiltinsImportCaptureHook]] = {
652
+ 'cext': _FrameBuiltinsImportCaptureHook,
653
+ 'somewhat_safe': _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook,
654
+ 'unsafe': _UnsafeGlobalBuiltinsImportCaptureHook,
655
+ }
656
+
657
+
570
658
  def _new_import_capture_hook(
571
659
  mod_globals: ta.MutableMapping[str, ta.Any], # noqa
572
660
  *,
573
661
  stack_offset: int = 0,
662
+ capture_impl: str | None = None,
574
663
  **kwargs: ta.Any,
575
664
  ) -> '_ImportCaptureHook':
576
- frame: types.FrameType | None = sys._getframe(1 + stack_offset) # noqa
577
- if frame is None or frame.f_globals is not mod_globals:
578
- 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
670
+
671
+ kwargs.setdefault('package', mod_globals.get('__package__'))
579
672
 
580
- if _capture is not None:
581
- 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
582
680
 
583
- return _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook(**kwargs)
681
+ return cls(**kwargs)
584
682
 
585
683
 
586
684
  ##
587
685
 
588
686
 
687
+ ImportCaptureModuleKind: ta.TypeAlias = ta.Literal[
688
+ 'parent',
689
+ 'terminal',
690
+ 'leaf',
691
+ ]
692
+
693
+
589
694
  class ImportCapture:
590
- class Import(ta.NamedTuple):
591
- spec: str
592
- attrs: ta.Sequence[tuple[str | None, str]]
695
+ @ta.final
696
+ class Module:
697
+ def __init__(
698
+ self,
699
+ name: str,
700
+ children: ta.Mapping[str, 'ImportCapture.Module'] | None = None,
701
+ attrs: ta.Sequence[str] | None = None,
702
+ ) -> None:
703
+ self.name = name
704
+ self.children = children
705
+ self.attrs = attrs
706
+
707
+ self.base_name = name.rpartition('.')[2]
708
+
709
+ if not self.children and not self.attrs:
710
+ self.kind = 'leaf'
711
+ elif not self.children or all(c.kind == 'leaf' for c in self.children.values()):
712
+ self.kind = 'terminal'
713
+ else:
714
+ self.kind = 'parent'
715
+
716
+ parent: ta.Optional['ImportCapture.Module']
717
+
718
+ kind: ImportCaptureModuleKind
719
+
720
+ def __repr__(self) -> str:
721
+ return ''.join([
722
+ f'{self.__class__.__name__}(',
723
+ f'{self.name!r}',
724
+ f', :{self.kind}',
725
+ *([f', children=[{", ".join(map(repr, self.children))}]'] if self.children else []),
726
+ *([f', attrs={self.attrs!r}'] if self.attrs else []),
727
+ ')',
728
+ ])
729
+
730
+ _root: 'ImportCapture.Module'
731
+
732
+ @property
733
+ def root(self) -> 'ImportCapture.Module':
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
744
+
745
+ @ta.final
746
+ class Import:
747
+ def __init__(
748
+ self,
749
+ module: 'ImportCapture.Module',
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
752
+ ) -> None:
753
+ self.module = module
754
+ self.as_ = as_
755
+ self.attrs = attrs
593
756
 
594
- class Captured(ta.NamedTuple):
595
- imports: ta.Sequence['ImportCapture.Import']
596
- unreferenced: ta.Mapping[str, ta.Sequence[str | None]] | None
757
+ def __repr__(self) -> str:
758
+ return ''.join([
759
+ f'{self.__class__.__name__}(',
760
+ f'{self.module.name!r}',
761
+ *([f', as_={self.as_!r}'] if self.as_ else []),
762
+ *([f', attrs={self.attrs!r}'] if self.attrs else []),
763
+ ')',
764
+ ])
765
+
766
+ @ta.final
767
+ class Captured:
768
+ def __init__(
769
+ self,
770
+
771
+ imports: ta.Mapping[str, 'ImportCapture.Import'],
772
+
773
+ modules: ta.Mapping[str, 'ImportCapture.Module'],
774
+ root_modules: ta.Mapping[str, 'ImportCapture.Module'],
775
+
776
+ unreferenced: ta.Sequence[str] | None,
777
+ ) -> None:
778
+ self.imports = imports
779
+
780
+ self.modules = modules
781
+ self.root_modules = root_modules
782
+
783
+ self.unreferenced = unreferenced
597
784
 
598
785
  @property
599
786
  def attrs(self) -> ta.Iterator[str]:
600
- for pi in self.imports:
601
- for _, a in pi.attrs:
602
- yield a
787
+ for pi in self.imports.values():
788
+ if pi.as_:
789
+ yield from pi.as_
790
+ if pi.attrs:
791
+ for _, a in pi.attrs:
792
+ yield a
793
+
794
+ EMPTY_CAPTURED: ta.ClassVar[Captured] = Captured(
795
+ {},
796
+ {},
797
+ {},
798
+ None,
799
+ )
603
800
 
604
801
  #
605
802
 
@@ -649,7 +846,7 @@ class ImportCapture:
649
846
  def capture(
650
847
  self,
651
848
  *,
652
- unreferenced_callback: ta.Callable[[ta.Mapping[str, ta.Sequence[str | None]]], None] | None = None,
849
+ unreferenced_callback: ta.Callable[[ta.Sequence[str]], None] | None = None,
653
850
  raise_unreferenced: bool = False,
654
851
  ) -> ta.Iterator[ta.Self]:
655
852
  if self._result_ is not None:
@@ -657,10 +854,7 @@ class ImportCapture:
657
854
 
658
855
  if self._disabled:
659
856
  self._result_ = ImportCapture._Result(
660
- ImportCapture.Captured(
661
- [],
662
- None,
663
- ),
857
+ ImportCapture.EMPTY_CAPTURED,
664
858
  )
665
859
  yield self
666
860
  return
@@ -681,9 +875,8 @@ class ImportCapture:
681
875
  if raise_unreferenced:
682
876
  raise ImportCaptureErrors.UnreferencedImportsError(blt.unreferenced)
683
877
 
684
- for pi in blt.imports:
685
- for _, a in pi.attrs:
686
- del self._mod_globals[a]
878
+ for a in blt.attrs:
879
+ del self._mod_globals[a]
687
880
 
688
881
  self._result_ = ImportCapture._Result(
689
882
  blt,