omlish 0.0.0.dev6__py3-none-any.whl → 0.0.0.dev8__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.
Files changed (108) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +0 -9
  4. omlish/asyncs/anyio.py +40 -0
  5. omlish/bootstrap.py +737 -0
  6. omlish/check.py +1 -1
  7. omlish/collections/__init__.py +4 -0
  8. omlish/collections/exceptions.py +2 -0
  9. omlish/collections/utils.py +38 -9
  10. omlish/configs/strings.py +2 -0
  11. omlish/dataclasses/__init__.py +7 -0
  12. omlish/dataclasses/impl/descriptors.py +95 -0
  13. omlish/dataclasses/impl/reflect.py +1 -1
  14. omlish/dataclasses/utils.py +23 -0
  15. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  16. omlish/diag/procfs.py +1 -1
  17. omlish/diag/threads.py +131 -48
  18. omlish/docker.py +16 -1
  19. omlish/fnpairs.py +0 -4
  20. omlish/{serde → formats}/dotenv.py +3 -0
  21. omlish/{serde → formats}/yaml.py +2 -2
  22. omlish/graphs/trees.py +1 -1
  23. omlish/http/consts.py +6 -0
  24. omlish/http/sessions.py +2 -2
  25. omlish/inject/__init__.py +4 -0
  26. omlish/inject/binder.py +3 -3
  27. omlish/inject/elements.py +1 -1
  28. omlish/inject/impl/injector.py +57 -27
  29. omlish/inject/impl/origins.py +2 -0
  30. omlish/inject/origins.py +3 -0
  31. omlish/inject/utils.py +18 -0
  32. omlish/iterators.py +69 -2
  33. omlish/lang/__init__.py +16 -7
  34. omlish/lang/classes/restrict.py +10 -0
  35. omlish/lang/contextmanagers.py +1 -1
  36. omlish/lang/descriptors.py +3 -3
  37. omlish/lang/imports.py +67 -0
  38. omlish/lang/iterables.py +40 -0
  39. omlish/lang/maybes.py +3 -0
  40. omlish/lang/objects.py +38 -0
  41. omlish/lang/strings.py +25 -0
  42. omlish/lang/sys.py +9 -0
  43. omlish/lang/typing.py +37 -0
  44. omlish/lite/__init__.py +1 -0
  45. omlish/lite/cached.py +18 -0
  46. omlish/lite/check.py +29 -0
  47. omlish/lite/contextmanagers.py +18 -0
  48. omlish/lite/json.py +30 -0
  49. omlish/lite/logs.py +121 -0
  50. omlish/lite/marshal.py +318 -0
  51. omlish/lite/reflect.py +49 -0
  52. omlish/lite/runtime.py +18 -0
  53. omlish/lite/secrets.py +19 -0
  54. omlish/lite/strings.py +25 -0
  55. omlish/lite/subprocesses.py +112 -0
  56. omlish/logs/__init__.py +13 -9
  57. omlish/logs/configs.py +17 -22
  58. omlish/logs/formatters.py +3 -48
  59. omlish/marshal/__init__.py +28 -0
  60. omlish/marshal/any.py +5 -5
  61. omlish/marshal/base.py +27 -11
  62. omlish/marshal/base64.py +24 -9
  63. omlish/marshal/dataclasses.py +34 -28
  64. omlish/marshal/datetimes.py +74 -18
  65. omlish/marshal/enums.py +14 -8
  66. omlish/marshal/exceptions.py +11 -1
  67. omlish/marshal/factories.py +59 -74
  68. omlish/marshal/forbidden.py +35 -0
  69. omlish/marshal/global_.py +11 -4
  70. omlish/marshal/iterables.py +21 -24
  71. omlish/marshal/mappings.py +23 -26
  72. omlish/marshal/numbers.py +51 -0
  73. omlish/marshal/optionals.py +11 -12
  74. omlish/marshal/polymorphism.py +86 -21
  75. omlish/marshal/primitives.py +4 -5
  76. omlish/marshal/standard.py +13 -8
  77. omlish/marshal/uuids.py +4 -5
  78. omlish/matchfns.py +218 -0
  79. omlish/os.py +64 -0
  80. omlish/reflect/__init__.py +39 -0
  81. omlish/reflect/isinstance.py +38 -0
  82. omlish/reflect/ops.py +84 -0
  83. omlish/reflect/subst.py +110 -0
  84. omlish/reflect/types.py +275 -0
  85. omlish/secrets/__init__.py +18 -2
  86. omlish/secrets/crypto.py +132 -0
  87. omlish/secrets/marshal.py +36 -7
  88. omlish/secrets/openssl.py +207 -0
  89. omlish/secrets/secrets.py +260 -8
  90. omlish/secrets/subprocesses.py +42 -0
  91. omlish/sql/dbs.py +6 -5
  92. omlish/sql/exprs.py +12 -0
  93. omlish/sql/secrets.py +10 -0
  94. omlish/term.py +1 -1
  95. omlish/testing/pytest/plugins/switches.py +54 -19
  96. omlish/text/glyphsplit.py +5 -0
  97. omlish-0.0.0.dev8.dist-info/METADATA +50 -0
  98. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/RECORD +105 -78
  99. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/WHEEL +1 -1
  100. omlish/logs/filters.py +0 -11
  101. omlish/reflect.py +0 -470
  102. omlish-0.0.0.dev6.dist-info/METADATA +0 -34
  103. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  104. /omlish/{serde → formats}/__init__.py +0 -0
  105. /omlish/{serde → formats}/json.py +0 -0
  106. /omlish/{serde → formats}/props.py +0 -0
  107. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/LICENSE +0 -0
  108. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/top_level.txt +0 -0
omlish/bootstrap.py ADDED
@@ -0,0 +1,737 @@
1
+ """
2
+ TODO:
3
+ - more logging options
4
+ - a more powerful interface would be run_fn_with_bootstrap..
5
+
6
+ TODO new items:
7
+ - pydevd connect-back
8
+ - debugging / pdb
9
+ - repl server
10
+ - packaging fixups
11
+ - daemonize ( https://github.com/thesharp/daemonize/blob/master/daemonize.py )
12
+ """
13
+ # ruff: noqa: UP006 UP007
14
+ import abc
15
+ import argparse
16
+ import contextlib
17
+ import dataclasses as dc
18
+ import enum
19
+ import faulthandler
20
+ import gc
21
+ import importlib
22
+ import io
23
+ import itertools
24
+ import logging
25
+ import os
26
+ import pwd
27
+ import resource
28
+ import signal
29
+ import sys
30
+ import typing as ta
31
+
32
+ from . import lang
33
+
34
+
35
+ if ta.TYPE_CHECKING:
36
+ import cProfile # noqa
37
+ import pstats
38
+ import runpy
39
+
40
+ from . import libc
41
+ from . import logs
42
+ from . import os as osu
43
+ from .diag import threads as diagt
44
+ from .formats import dotenv
45
+
46
+ else:
47
+ cProfile = lang.proxy_import('cProfile') # noqa
48
+ pstats = lang.proxy_import('pstats')
49
+ runpy = lang.proxy_import('runpy')
50
+
51
+ libc = lang.proxy_import('.libc', __package__)
52
+ logs = lang.proxy_import('.logs', __package__)
53
+ osu = lang.proxy_import('.os', __package__)
54
+ diagt = lang.proxy_import('.diag.threads', __package__)
55
+ dotenv = lang.proxy_import('.formats.dotenv', __package__)
56
+
57
+
58
+ BootstrapConfigT = ta.TypeVar('BootstrapConfigT', bound='Bootstrap.Config')
59
+
60
+
61
+ ##
62
+
63
+
64
+ class Bootstrap(abc.ABC, ta.Generic[BootstrapConfigT]):
65
+ @dc.dataclass(frozen=True)
66
+ class Config(abc.ABC): # noqa
67
+ pass
68
+
69
+ def __init__(self, config: BootstrapConfigT) -> None:
70
+ super().__init__()
71
+ self._config = config
72
+
73
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
74
+ super().__init_subclass__(**kwargs)
75
+ if not cls.__name__.endswith('Bootstrap'):
76
+ raise NameError(cls)
77
+ if abc.ABC not in cls.__bases__ and not issubclass(cls.__dict__['Config'], Bootstrap.Config):
78
+ raise TypeError(cls)
79
+
80
+
81
+ class SimpleBootstrap(Bootstrap[BootstrapConfigT], abc.ABC):
82
+ @abc.abstractmethod
83
+ def run(self) -> None:
84
+ raise NotImplementedError
85
+
86
+
87
+ class ContextBootstrap(Bootstrap[BootstrapConfigT], abc.ABC):
88
+ @abc.abstractmethod
89
+ def enter(self) -> ta.ContextManager[None]:
90
+ raise NotImplementedError
91
+
92
+
93
+ ##
94
+
95
+
96
+ class CwdBootstrap(ContextBootstrap['CwdBootstrap.Config']):
97
+ @dc.dataclass(frozen=True)
98
+ class Config(Bootstrap.Config):
99
+ path: ta.Optional[str] = None
100
+
101
+ @contextlib.contextmanager
102
+ def enter(self) -> ta.Iterator[None]:
103
+ if self._config.path is not None:
104
+ prev = os.getcwd()
105
+ os.chdir(self._config.path)
106
+ else:
107
+ prev = None
108
+
109
+ try:
110
+ yield
111
+
112
+ finally:
113
+ if prev is not None:
114
+ os.chdir(prev)
115
+
116
+
117
+ ##
118
+
119
+
120
+ class SetuidBootstrap(SimpleBootstrap['SetuidBootstrap.Config']):
121
+ @dc.dataclass(frozen=True)
122
+ class Config(Bootstrap.Config):
123
+ user: ta.Optional[str] = None
124
+
125
+ def run(self) -> None:
126
+ if self._config.user is not None:
127
+ user = pwd.getpwnam(self._config.user)
128
+ os.setuid(user.pw_uid)
129
+
130
+
131
+ ##
132
+
133
+
134
+ class GcDebugFlag(enum.Enum):
135
+ STATS = gc.DEBUG_STATS
136
+ COLLECTABLE = gc.DEBUG_COLLECTABLE
137
+ UNCOLLECTABLE = gc.DEBUG_UNCOLLECTABLE
138
+ SAVEALL = gc.DEBUG_SAVEALL
139
+ LEAK = gc.DEBUG_LEAK
140
+
141
+
142
+ class GcBootstrap(ContextBootstrap['GcBootstrap.Config']):
143
+ @dc.dataclass(frozen=True)
144
+ class Config(Bootstrap.Config):
145
+ disable: bool = False
146
+ debug: ta.Optional[int] = None
147
+
148
+ @contextlib.contextmanager
149
+ def enter(self) -> ta.Iterator[None]:
150
+ prev_enabled = gc.isenabled()
151
+ if self._config.disable:
152
+ gc.disable()
153
+
154
+ if self._config.debug is not None:
155
+ prev_debug = gc.get_debug()
156
+ gc.set_debug(self._config.debug)
157
+ else:
158
+ prev_debug = None
159
+
160
+ try:
161
+ yield
162
+
163
+ finally:
164
+ if prev_enabled:
165
+ gc.enable()
166
+
167
+ if prev_debug is not None:
168
+ gc.set_debug(prev_debug)
169
+
170
+
171
+ ##
172
+
173
+
174
+ class NiceBootstrap(SimpleBootstrap['NiceBootstrap.Config']):
175
+ @dc.dataclass(frozen=True)
176
+ class Config(Bootstrap.Config):
177
+ nice: ta.Optional[int] = None
178
+
179
+ def run(self) -> None:
180
+ if self._config.nice is not None:
181
+ os.nice(self._config.nice)
182
+
183
+
184
+ ##
185
+
186
+
187
+ class LogBootstrap(ContextBootstrap['LogBootstrap.Config']):
188
+ @dc.dataclass(frozen=True)
189
+ class Config(Bootstrap.Config):
190
+ level: ta.Union[str, int, None] = None
191
+ json: bool = False
192
+
193
+ @contextlib.contextmanager
194
+ def enter(self) -> ta.Iterator[None]:
195
+ if self._config.level is None:
196
+ yield
197
+ return
198
+
199
+ handler = logs.configure_standard_logging(
200
+ self._config.level,
201
+ json=self._config.json,
202
+ )
203
+
204
+ try:
205
+ yield
206
+
207
+ finally:
208
+ if handler is not None:
209
+ logging.root.removeHandler(handler)
210
+
211
+
212
+ ##
213
+
214
+
215
+ class FaulthandlerBootstrap(ContextBootstrap['FaulthandlerBootstrap.Config']):
216
+ @dc.dataclass(frozen=True)
217
+ class Config(Bootstrap.Config):
218
+ enabled: ta.Optional[bool] = None
219
+
220
+ @contextlib.contextmanager
221
+ def enter(self) -> ta.Iterator[None]:
222
+ if self._config.enabled is None:
223
+ yield
224
+ return
225
+
226
+ prev = faulthandler.is_enabled()
227
+ if self._config.enabled:
228
+ faulthandler.enable()
229
+ else:
230
+ faulthandler.disable()
231
+
232
+ try:
233
+ yield
234
+
235
+ finally:
236
+ if prev:
237
+ faulthandler.enable()
238
+ else:
239
+ faulthandler.disable()
240
+
241
+
242
+ ##
243
+
244
+
245
+ SIGNALS_BY_NAME = {
246
+ a[len('SIG'):]: v # noqa
247
+ for a in dir(signal)
248
+ if a.startswith('SIG')
249
+ and not a.startswith('SIG_')
250
+ and a == a.upper()
251
+ and isinstance((v := getattr(signal, a)), int)
252
+ }
253
+
254
+
255
+ class PrctlBootstrap(SimpleBootstrap['PrctlBootstrap.Config']):
256
+ @dc.dataclass(frozen=True)
257
+ class Config(Bootstrap.Config):
258
+ dumpable: bool = False
259
+ deathsig: ta.Union[int, str, None] = None
260
+
261
+ def run(self) -> None:
262
+ if self._config.dumpable:
263
+ libc.prctl(libc.PR_SET_DUMPABLE, 1, 0, 0, 0, 0)
264
+
265
+ if self._config.deathsig is not None:
266
+ if isinstance(self._config.deathsig, int):
267
+ sig = self._config.deathsig
268
+ else:
269
+ sig = SIGNALS_BY_NAME[self._config.deathsig.upper()]
270
+ libc.prctl(libc.PR_SET_PDEATHSIG, sig, 0, 0, 0, 0)
271
+
272
+
273
+ ##
274
+
275
+
276
+ RLIMITS_BY_NAME = {
277
+ a[len('RLIMIT_'):]: v # noqa
278
+ for a in dir(resource)
279
+ if a.startswith('RLIMIT_')
280
+ and a == a.upper()
281
+ and isinstance((v := getattr(resource, a)), int)
282
+ }
283
+
284
+
285
+ class RlimitBootstrap(ContextBootstrap['RlimitBootstrap.Config']):
286
+ @dc.dataclass(frozen=True)
287
+ class Config(Bootstrap.Config):
288
+ limits: ta.Optional[ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]] = None
289
+
290
+ @contextlib.contextmanager
291
+ def enter(self) -> ta.Iterator[None]:
292
+ if not self._config.limits:
293
+ yield
294
+ return
295
+
296
+ def or_infin(l: ta.Optional[int]) -> int:
297
+ return l if l is not None else resource.RLIM_INFINITY
298
+
299
+ prev = {}
300
+ for k, (s, h) in self._config.limits.items():
301
+ i = RLIMITS_BY_NAME[k.upper()]
302
+ prev[i] = resource.getrlimit(i)
303
+ resource.setrlimit(i, (or_infin(s), or_infin(h)))
304
+
305
+ try:
306
+ yield
307
+
308
+ finally:
309
+ for i, (s, h) in prev.items():
310
+ resource.setrlimit(i, (s, h))
311
+
312
+
313
+ ##
314
+
315
+
316
+ class ImportBootstrap(SimpleBootstrap['ImportBootstrap.Config']):
317
+ @dc.dataclass(frozen=True)
318
+ class Config(Bootstrap.Config):
319
+ modules: ta.Optional[ta.Sequence[str]] = None
320
+
321
+ def run(self) -> None:
322
+ for m in self._config.modules or ():
323
+ importlib.import_module(m)
324
+
325
+
326
+ ##
327
+
328
+
329
+ class ProfilingBootstrap(ContextBootstrap['ProfilingBootstrap.Config']):
330
+ @dc.dataclass(frozen=True)
331
+ class Config(Bootstrap.Config):
332
+ enable: bool = False
333
+ builtins: bool = True
334
+
335
+ outfile: ta.Optional[str] = None
336
+
337
+ print: bool = False
338
+ sort: str = 'cumtime'
339
+ topn: int = 100
340
+
341
+ @contextlib.contextmanager
342
+ def enter(self) -> ta.Iterator[None]:
343
+ if not self._config.enable:
344
+ yield
345
+ return
346
+
347
+ prof = cProfile.Profile()
348
+ prof.enable()
349
+ try:
350
+ yield
351
+
352
+ finally:
353
+ prof.disable()
354
+ prof.create_stats()
355
+
356
+ if self._config.print:
357
+ pstats.Stats(prof) \
358
+ .strip_dirs() \
359
+ .sort_stats(self._config.sort) \
360
+ .print_stats(self._config.topn)
361
+
362
+ if self._config.outfile is not None:
363
+ prof.dump_stats(self._config.outfile)
364
+
365
+
366
+ ##
367
+
368
+
369
+ class EnvBootstrap(ContextBootstrap['EnvBootstrap.Config']):
370
+ @dc.dataclass(frozen=True)
371
+ class Config(Bootstrap.Config):
372
+ vars: ta.Optional[ta.Mapping[str, ta.Optional[str]]] = None
373
+ files: ta.Optional[ta.Sequence[str]] = None
374
+
375
+ @contextlib.contextmanager
376
+ def enter(self) -> ta.Iterator[None]:
377
+ if not (self._config.vars or self._config.files):
378
+ yield
379
+ return
380
+
381
+ new = dict(self._config.vars or {})
382
+ for f in self._config.files or ():
383
+ new.update(dotenv.dotenv_values(f, env=os.environ))
384
+
385
+ prev: ta.Dict[str, ta.Optional[str]] = {k: os.environ.get(k) for k in new}
386
+
387
+ def do(k: str, v: ta.Optional[str]) -> None:
388
+ if v is not None:
389
+ os.environ[k] = v
390
+ else:
391
+ del os.environ[k]
392
+
393
+ for k, v in new.items():
394
+ do(k, v)
395
+
396
+ try:
397
+ yield
398
+
399
+ finally:
400
+ for k, v in prev.items():
401
+ do(k, v)
402
+
403
+
404
+ ##
405
+
406
+
407
+ class ThreadDumpBootstrap(ContextBootstrap['ThreadDumpBootstrap.Config']):
408
+ @dc.dataclass(frozen=True)
409
+ class Config(Bootstrap.Config):
410
+ interval_s: ta.Optional[float] = None
411
+
412
+ on_sigquit: bool = False
413
+
414
+ @contextlib.contextmanager
415
+ def enter(self) -> ta.Iterator[None]:
416
+ if self._config.interval_s:
417
+ tdt = diagt.create_thread_dump_thread(
418
+ interval_s=self._config.interval_s,
419
+ start=True,
420
+ )
421
+ else:
422
+ tdt = None
423
+
424
+ if self._config.on_sigquit:
425
+ dump_threads_str = diagt.dump_threads_str
426
+
427
+ def handler(signum, frame):
428
+ print(dump_threads_str(), file=sys.stderr)
429
+
430
+ prev_sq = lang.just(signal.signal(signal.SIGQUIT, handler))
431
+ else:
432
+ prev_sq = lang.empty()
433
+
434
+ try:
435
+ yield
436
+
437
+ finally:
438
+ if tdt is not None:
439
+ tdt.stop_nowait()
440
+
441
+ if prev_sq.present:
442
+ signal.signal(signal.SIGQUIT, prev_sq.must())
443
+
444
+
445
+ ##
446
+
447
+
448
+ class TimebombBootstrap(ContextBootstrap['TimebombBootstrap.Config']):
449
+ @dc.dataclass(frozen=True)
450
+ class Config(Bootstrap.Config):
451
+ delay_s: ta.Optional[float] = None
452
+
453
+ @contextlib.contextmanager
454
+ def enter(self) -> ta.Iterator[None]:
455
+ if not self._config.delay_s:
456
+ yield
457
+ return
458
+
459
+ tbt = diagt.create_timebomb_thread(
460
+ self._config.delay_s,
461
+ start=True,
462
+ )
463
+ try:
464
+ yield
465
+ finally:
466
+ tbt.stop_nowait()
467
+
468
+
469
+ ##
470
+
471
+
472
+ class PidfileBootstrap(ContextBootstrap['PidfileBootstrap.Config']):
473
+ @dc.dataclass(frozen=True)
474
+ class Config(Bootstrap.Config):
475
+ path: ta.Optional[str] = None
476
+
477
+ @contextlib.contextmanager
478
+ def enter(self) -> ta.Iterator[None]:
479
+ if self._config.path is None:
480
+ yield
481
+ return
482
+
483
+ with osu.Pidfile(self._config.path) as pf:
484
+ pf.write()
485
+ yield
486
+
487
+
488
+ ##
489
+
490
+
491
+ class FdsBootstrap(SimpleBootstrap['FdsBootstrap.Config']):
492
+ @dc.dataclass(frozen=True)
493
+ class Config(Bootstrap.Config):
494
+ redirects: ta.Optional[ta.Mapping[int, ta.Union[int, str, None]]] = None
495
+
496
+ def run(self) -> None:
497
+ for dst, src in (self._config.redirects or {}).items():
498
+ if src is None:
499
+ src = '/dev/null'
500
+ if isinstance(src, int):
501
+ os.dup2(src, dst)
502
+ elif isinstance(src, str):
503
+ sfd = os.open(src, os.O_RDWR)
504
+ os.dup2(sfd, dst)
505
+ os.close(sfd)
506
+ else:
507
+ raise TypeError(src)
508
+
509
+
510
+ ##
511
+
512
+
513
+ class PrintPidBootstrap(SimpleBootstrap['PrintPidBootstrap.Config']):
514
+ @dc.dataclass(frozen=True)
515
+ class Config(Bootstrap.Config):
516
+ enable: bool = False
517
+ pause: bool = False
518
+
519
+ def run(self) -> None:
520
+ if not (self._config.enable or self._config.pause):
521
+ return
522
+ print(str(os.getpid()), file=sys.stderr)
523
+ if self._config.pause:
524
+ input()
525
+
526
+
527
+ ##
528
+
529
+
530
+ BOOTSTRAP_TYPES_BY_NAME: ta.Mapping[str, ta.Type[Bootstrap]] = { # noqa
531
+ lang.snake_case(cls.__name__[:-len('Bootstrap')]): cls
532
+ for cls in lang.deep_subclasses(Bootstrap)
533
+ if not lang.is_abstract_class(cls)
534
+ }
535
+
536
+ BOOTSTRAP_TYPES_BY_CONFIG_TYPE: ta.Mapping[ta.Type[Bootstrap.Config], ta.Type[Bootstrap]] = {
537
+ cls.Config: cls
538
+ for cls in BOOTSTRAP_TYPES_BY_NAME.values()
539
+ }
540
+
541
+
542
+ ##
543
+
544
+
545
+ class BootstrapHarness:
546
+ def __init__(self, lst: ta.Sequence[Bootstrap]) -> None:
547
+ super().__init__()
548
+ self._lst = lst
549
+
550
+ @contextlib.contextmanager
551
+ def __call__(self) -> ta.Iterator[None]:
552
+ with contextlib.ExitStack() as es:
553
+ for c in self._lst:
554
+ if isinstance(c, SimpleBootstrap):
555
+ c.run()
556
+ elif isinstance(c, ContextBootstrap):
557
+ es.enter_context(c.enter())
558
+ else:
559
+ raise TypeError(c)
560
+
561
+ yield
562
+
563
+
564
+ ##
565
+
566
+
567
+ @contextlib.contextmanager
568
+ def bootstrap(*cfgs: Bootstrap.Config) -> ta.Iterator[None]:
569
+ with BootstrapHarness([
570
+ BOOTSTRAP_TYPES_BY_CONFIG_TYPE[type(c)](c)
571
+ for c in cfgs
572
+ ])():
573
+ yield
574
+
575
+
576
+ ##
577
+
578
+
579
+ class _OrderedArgsAction(argparse.Action):
580
+ def __call__(self, parser, namespace, values, option_string=None):
581
+ if 'ordered_args' not in namespace:
582
+ setattr(namespace, 'ordered_args', [])
583
+ if self.const is not None:
584
+ value = self.const
585
+ else:
586
+ value = values
587
+ namespace.ordered_args.append((self.dest, value))
588
+
589
+
590
+ def _or_opt(ty):
591
+ return (ty, ta.Optional[ty])
592
+
593
+
594
+ def _int_or_str(v):
595
+ try:
596
+ return int(v)
597
+ except ValueError:
598
+ return v
599
+
600
+
601
+ def _add_arguments(parser: argparse.ArgumentParser) -> None:
602
+ # ta.Optional[ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]]
603
+
604
+ for cname, cls in BOOTSTRAP_TYPES_BY_NAME.items():
605
+ for fld in dc.fields(cls.Config):
606
+ aname = f'--{cname}:{fld.name}'
607
+ kw: ta.Dict[str, ta.Any] = {}
608
+
609
+ if fld.type in _or_opt(str):
610
+ pass
611
+ elif fld.type in _or_opt(bool):
612
+ kw.update(const=True, nargs=0)
613
+ elif fld.type in _or_opt(int):
614
+ kw.update(type=int)
615
+ elif fld.type in _or_opt(float):
616
+ kw.update(type=float)
617
+ elif fld.type in _or_opt(ta.Union[int, str]):
618
+ kw.update(type=_int_or_str)
619
+
620
+ elif fld.type in (
621
+ *_or_opt(ta.Sequence[str]),
622
+ *_or_opt(ta.Mapping[str, ta.Optional[str]]),
623
+ *_or_opt(ta.Mapping[int, ta.Union[int, str, None]]),
624
+ *_or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]),
625
+ ):
626
+ if aname[-1] != 's':
627
+ raise NameError(aname)
628
+ aname = aname[:-1]
629
+
630
+ else:
631
+ raise TypeError(fld)
632
+
633
+ parser.add_argument(aname, action=_OrderedArgsAction, **kw)
634
+
635
+
636
+ def _process_arguments(args: ta.Any) -> ta.Sequence[Bootstrap.Config]:
637
+ if not (oa := getattr(args, 'ordered_args', None)):
638
+ return []
639
+
640
+ cfgs: ta.List[Bootstrap.Config] = []
641
+
642
+ for cname, cargs in [
643
+ (n, list(g))
644
+ for n, g in
645
+ itertools.groupby(oa, key=lambda s: s[0].partition(':')[0])
646
+ ]:
647
+ ccls = BOOTSTRAP_TYPES_BY_NAME[cname].Config
648
+ flds = {f.name: f for f in dc.fields(ccls)}
649
+
650
+ kw: ta.Dict[str, ta.Any] = {}
651
+ for aname, aval in cargs:
652
+ k = aname.partition(':')[2]
653
+
654
+ if k not in flds:
655
+ k += 's'
656
+ fld = flds[k]
657
+
658
+ if fld.type in _or_opt(ta.Sequence[str]):
659
+ kw.setdefault(k, []).append(aval)
660
+
661
+ elif fld.type in _or_opt(ta.Mapping[str, ta.Optional[str]]):
662
+ if '=' not in aval:
663
+ kw.setdefault(k, {})[aval] = None
664
+ else:
665
+ ek, _, ev = aval.partition('=')
666
+ kw.setdefault(k, {})[ek] = ev
667
+
668
+ elif fld.type in _or_opt(ta.Mapping[int, ta.Union[int, str, None]]):
669
+ fk, _, fv = aval.partition('=')
670
+ if not fv:
671
+ kw.setdefault(k, {})[int(fk)] = None
672
+ else:
673
+ kw.setdefault(k, {})[int(fk)] = _int_or_str(fv)
674
+
675
+ elif fld.type in _or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]):
676
+ fk, _, fv = aval.partition('=')
677
+ if ',' in fv:
678
+ tl, tr = fv.split(',')
679
+ else:
680
+ tl, tr = None, None
681
+ kw.setdefault(k, {})[fk] = (_int_or_str(tl) if tl else None, _int_or_str(tr) if tr else None)
682
+
683
+ else:
684
+ raise TypeError(fld)
685
+
686
+ else:
687
+ kw[k] = aval
688
+
689
+ cfg = ccls(**kw)
690
+ cfgs.append(cfg)
691
+
692
+ return cfgs
693
+
694
+
695
+ ##
696
+
697
+
698
+ def _main() -> int:
699
+ parser = argparse.ArgumentParser()
700
+
701
+ _add_arguments(parser)
702
+
703
+ parser.add_argument('-m', '--module', action='store_true')
704
+ parser.add_argument('target')
705
+ parser.add_argument('args', nargs=argparse.REMAINDER)
706
+
707
+ args = parser.parse_args()
708
+ cfgs = _process_arguments(args)
709
+
710
+ with bootstrap(*cfgs):
711
+ tgt = args.target
712
+
713
+ if args.module:
714
+ sys.argv = [tgt, *(args.args or ())]
715
+ runpy._run_module_as_main(tgt) # type: ignore # noqa
716
+
717
+ else:
718
+ with io.open_code(tgt) as fp:
719
+ src = fp.read()
720
+
721
+ ns = dict(
722
+ __name__='__main__',
723
+ __file__=tgt,
724
+ __builtins__=__builtins__,
725
+ __spec__=None,
726
+ )
727
+
728
+ import __main__ # noqa
729
+ __main__.__dict__.clear()
730
+ __main__.__dict__.update(ns)
731
+ exec(compile(src, tgt, 'exec'), __main__.__dict__, __main__.__dict__)
732
+
733
+ return 0
734
+
735
+
736
+ if __name__ == '__main__':
737
+ sys.exit(_main())