omlish 0.0.0.dev18__py3-none-any.whl → 0.0.0.dev19__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.
- omlish/__about__.py +2 -2
- omlish/bootstrap/__init__.py +3 -0
- omlish/bootstrap/__main__.py +4 -0
- omlish/bootstrap/base.py +38 -0
- omlish/bootstrap/diag.py +128 -0
- omlish/bootstrap/harness.py +81 -0
- omlish/bootstrap/main.py +183 -0
- omlish/bootstrap/sys.py +361 -0
- omlish/defs.py +29 -5
- omlish/lite/logs.py +1 -1
- omlish/logs/configs.py +11 -23
- omlish/logs/noisy.py +15 -0
- omlish/testing/pytest/plugins/__init__.py +8 -3
- omlish/testing/pytest/plugins/depskip.py +81 -0
- omlish/testing/pytest/plugins/utils.py +14 -0
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/RECORD +20 -11
- omlish/bootstrap.py +0 -746
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
omlish/bootstrap/base.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import abc
|
2
|
+
import dataclasses as dc
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
|
6
|
+
BootstrapConfigT = ta.TypeVar('BootstrapConfigT', bound='Bootstrap.Config')
|
7
|
+
|
8
|
+
|
9
|
+
##
|
10
|
+
|
11
|
+
|
12
|
+
class Bootstrap(abc.ABC, ta.Generic[BootstrapConfigT]):
|
13
|
+
@dc.dataclass(frozen=True)
|
14
|
+
class Config(abc.ABC): # noqa
|
15
|
+
pass
|
16
|
+
|
17
|
+
def __init__(self, config: BootstrapConfigT) -> None:
|
18
|
+
super().__init__()
|
19
|
+
self._config = config
|
20
|
+
|
21
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
22
|
+
super().__init_subclass__(**kwargs)
|
23
|
+
if not cls.__name__.endswith('Bootstrap'):
|
24
|
+
raise NameError(cls)
|
25
|
+
if abc.ABC not in cls.__bases__ and not issubclass(cls.__dict__['Config'], Bootstrap.Config):
|
26
|
+
raise TypeError(cls)
|
27
|
+
|
28
|
+
|
29
|
+
class SimpleBootstrap(Bootstrap[BootstrapConfigT], abc.ABC):
|
30
|
+
@abc.abstractmethod
|
31
|
+
def run(self) -> None:
|
32
|
+
raise NotImplementedError
|
33
|
+
|
34
|
+
|
35
|
+
class ContextBootstrap(Bootstrap[BootstrapConfigT], abc.ABC):
|
36
|
+
@abc.abstractmethod
|
37
|
+
def enter(self) -> ta.ContextManager[None]:
|
38
|
+
raise NotImplementedError
|
omlish/bootstrap/diag.py
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# ruff: noqa: UP007
|
2
|
+
import contextlib
|
3
|
+
import dataclasses as dc
|
4
|
+
import signal
|
5
|
+
import sys
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from .. import lang
|
9
|
+
from .base import Bootstrap
|
10
|
+
from .base import ContextBootstrap
|
11
|
+
|
12
|
+
|
13
|
+
if ta.TYPE_CHECKING:
|
14
|
+
import cProfile # noqa
|
15
|
+
import pstats
|
16
|
+
|
17
|
+
from ..diag import threads as diagt
|
18
|
+
|
19
|
+
else:
|
20
|
+
cProfile = lang.proxy_import('cProfile') # noqa
|
21
|
+
pstats = lang.proxy_import('pstats')
|
22
|
+
|
23
|
+
diagt = lang.proxy_import('..diag.threads', __package__)
|
24
|
+
|
25
|
+
|
26
|
+
##
|
27
|
+
|
28
|
+
|
29
|
+
class ProfilingBootstrap(ContextBootstrap['ProfilingBootstrap.Config']):
|
30
|
+
@dc.dataclass(frozen=True)
|
31
|
+
class Config(Bootstrap.Config):
|
32
|
+
enable: bool = False
|
33
|
+
builtins: bool = True
|
34
|
+
|
35
|
+
outfile: ta.Optional[str] = None
|
36
|
+
|
37
|
+
print: bool = False
|
38
|
+
sort: str = 'cumtime'
|
39
|
+
topn: int = 100
|
40
|
+
|
41
|
+
@contextlib.contextmanager
|
42
|
+
def enter(self) -> ta.Iterator[None]:
|
43
|
+
if not self._config.enable:
|
44
|
+
yield
|
45
|
+
return
|
46
|
+
|
47
|
+
prof = cProfile.Profile()
|
48
|
+
prof.enable()
|
49
|
+
try:
|
50
|
+
yield
|
51
|
+
|
52
|
+
finally:
|
53
|
+
prof.disable()
|
54
|
+
prof.create_stats()
|
55
|
+
|
56
|
+
if self._config.print:
|
57
|
+
pstats.Stats(prof) \
|
58
|
+
.strip_dirs() \
|
59
|
+
.sort_stats(self._config.sort) \
|
60
|
+
.print_stats(self._config.topn)
|
61
|
+
|
62
|
+
if self._config.outfile is not None:
|
63
|
+
prof.dump_stats(self._config.outfile)
|
64
|
+
|
65
|
+
|
66
|
+
##
|
67
|
+
|
68
|
+
|
69
|
+
class ThreadDumpBootstrap(ContextBootstrap['ThreadDumpBootstrap.Config']):
|
70
|
+
@dc.dataclass(frozen=True)
|
71
|
+
class Config(Bootstrap.Config):
|
72
|
+
interval_s: ta.Optional[float] = None
|
73
|
+
|
74
|
+
on_sigquit: bool = False
|
75
|
+
|
76
|
+
@contextlib.contextmanager
|
77
|
+
def enter(self) -> ta.Iterator[None]:
|
78
|
+
if self._config.interval_s:
|
79
|
+
tdt = diagt.create_thread_dump_thread(
|
80
|
+
interval_s=self._config.interval_s,
|
81
|
+
start=True,
|
82
|
+
)
|
83
|
+
else:
|
84
|
+
tdt = None
|
85
|
+
|
86
|
+
if self._config.on_sigquit:
|
87
|
+
dump_threads_str = diagt.dump_threads_str
|
88
|
+
|
89
|
+
def handler(signum, frame):
|
90
|
+
print(dump_threads_str(), file=sys.stderr)
|
91
|
+
|
92
|
+
prev_sq = lang.just(signal.signal(signal.SIGQUIT, handler))
|
93
|
+
else:
|
94
|
+
prev_sq = lang.empty()
|
95
|
+
|
96
|
+
try:
|
97
|
+
yield
|
98
|
+
|
99
|
+
finally:
|
100
|
+
if tdt is not None:
|
101
|
+
tdt.stop_nowait()
|
102
|
+
|
103
|
+
if prev_sq.present:
|
104
|
+
signal.signal(signal.SIGQUIT, prev_sq.must())
|
105
|
+
|
106
|
+
|
107
|
+
##
|
108
|
+
|
109
|
+
|
110
|
+
class TimebombBootstrap(ContextBootstrap['TimebombBootstrap.Config']):
|
111
|
+
@dc.dataclass(frozen=True)
|
112
|
+
class Config(Bootstrap.Config):
|
113
|
+
delay_s: ta.Optional[float] = None
|
114
|
+
|
115
|
+
@contextlib.contextmanager
|
116
|
+
def enter(self) -> ta.Iterator[None]:
|
117
|
+
if not self._config.delay_s:
|
118
|
+
yield
|
119
|
+
return
|
120
|
+
|
121
|
+
tbt = diagt.create_timebomb_thread(
|
122
|
+
self._config.delay_s,
|
123
|
+
start=True,
|
124
|
+
)
|
125
|
+
try:
|
126
|
+
yield
|
127
|
+
finally:
|
128
|
+
tbt.stop_nowait()
|
@@ -0,0 +1,81 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- more logging options
|
4
|
+
- a more powerful interface would be run_fn_with_bootstrap..
|
5
|
+
- ./python -m gprof2dot -f pstats prof.pstats | dot -Tpdf -o prof.pstats.pdf && open prof.pstats.pdf
|
6
|
+
- multiprocess profiling - afterfork, suffix with pid
|
7
|
+
|
8
|
+
TODO diag:
|
9
|
+
- yappi
|
10
|
+
- stackscope
|
11
|
+
- https://github.com/pythonspeed/filprofiler
|
12
|
+
- https://pypi.org/project/guppy3/
|
13
|
+
- https://pypi.org/project/memory-profiler/
|
14
|
+
- https://pypi.org/project/Pympler/
|
15
|
+
|
16
|
+
TODO new items:
|
17
|
+
- pydevd connect-back
|
18
|
+
- debugging / pdb
|
19
|
+
- repl server
|
20
|
+
- packaging fixups
|
21
|
+
- daemonize ( https://github.com/thesharp/daemonize/blob/master/daemonize.py )
|
22
|
+
"""
|
23
|
+
# ruff: noqa: UP006
|
24
|
+
import contextlib
|
25
|
+
import typing as ta
|
26
|
+
|
27
|
+
from .. import lang
|
28
|
+
from . import diag # noqa
|
29
|
+
from . import sys # noqa
|
30
|
+
from .base import Bootstrap
|
31
|
+
from .base import ContextBootstrap
|
32
|
+
from .base import SimpleBootstrap
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
38
|
+
BOOTSTRAP_TYPES_BY_NAME: ta.Mapping[str, ta.Type[Bootstrap]] = { # noqa
|
39
|
+
lang.snake_case(cls.__name__[:-len('Bootstrap')]): cls
|
40
|
+
for cls in lang.deep_subclasses(Bootstrap)
|
41
|
+
if not lang.is_abstract_class(cls)
|
42
|
+
}
|
43
|
+
|
44
|
+
BOOTSTRAP_TYPES_BY_CONFIG_TYPE: ta.Mapping[ta.Type[Bootstrap.Config], ta.Type[Bootstrap]] = {
|
45
|
+
cls.Config: cls
|
46
|
+
for cls in BOOTSTRAP_TYPES_BY_NAME.values()
|
47
|
+
}
|
48
|
+
|
49
|
+
|
50
|
+
##
|
51
|
+
|
52
|
+
|
53
|
+
class BootstrapHarness:
|
54
|
+
def __init__(self, lst: ta.Sequence[Bootstrap]) -> None:
|
55
|
+
super().__init__()
|
56
|
+
self._lst = lst
|
57
|
+
|
58
|
+
@contextlib.contextmanager
|
59
|
+
def __call__(self) -> ta.Iterator[None]:
|
60
|
+
with contextlib.ExitStack() as es:
|
61
|
+
for c in self._lst:
|
62
|
+
if isinstance(c, SimpleBootstrap):
|
63
|
+
c.run()
|
64
|
+
elif isinstance(c, ContextBootstrap):
|
65
|
+
es.enter_context(c.enter())
|
66
|
+
else:
|
67
|
+
raise TypeError(c)
|
68
|
+
|
69
|
+
yield
|
70
|
+
|
71
|
+
|
72
|
+
##
|
73
|
+
|
74
|
+
|
75
|
+
@contextlib.contextmanager
|
76
|
+
def bootstrap(*cfgs: Bootstrap.Config) -> ta.Iterator[None]:
|
77
|
+
with BootstrapHarness([
|
78
|
+
BOOTSTRAP_TYPES_BY_CONFIG_TYPE[type(c)](c)
|
79
|
+
for c in cfgs
|
80
|
+
])():
|
81
|
+
yield
|
omlish/bootstrap/main.py
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import argparse
|
3
|
+
import dataclasses as dc
|
4
|
+
import io
|
5
|
+
import itertools
|
6
|
+
import sys
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from .. import lang
|
10
|
+
from .base import Bootstrap
|
11
|
+
from .harness import BOOTSTRAP_TYPES_BY_NAME
|
12
|
+
from .harness import bootstrap
|
13
|
+
|
14
|
+
|
15
|
+
if ta.TYPE_CHECKING:
|
16
|
+
import runpy
|
17
|
+
|
18
|
+
else:
|
19
|
+
runpy = lang.proxy_import('runpy')
|
20
|
+
|
21
|
+
|
22
|
+
##
|
23
|
+
|
24
|
+
|
25
|
+
class _OrderedArgsAction(argparse.Action):
|
26
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
27
|
+
if 'ordered_args' not in namespace:
|
28
|
+
setattr(namespace, 'ordered_args', [])
|
29
|
+
if self.const is not None:
|
30
|
+
value = self.const
|
31
|
+
else:
|
32
|
+
value = values
|
33
|
+
namespace.ordered_args.append((self.dest, value))
|
34
|
+
|
35
|
+
|
36
|
+
def _or_opt(ty):
|
37
|
+
return (ty, ta.Optional[ty])
|
38
|
+
|
39
|
+
|
40
|
+
def _int_or_str(v):
|
41
|
+
try:
|
42
|
+
return int(v)
|
43
|
+
except ValueError:
|
44
|
+
return v
|
45
|
+
|
46
|
+
|
47
|
+
def _add_arguments(parser: argparse.ArgumentParser) -> None:
|
48
|
+
# ta.Optional[ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]]
|
49
|
+
|
50
|
+
for cname, cls in BOOTSTRAP_TYPES_BY_NAME.items():
|
51
|
+
for fld in dc.fields(cls.Config):
|
52
|
+
aname = f'--{cname}:{fld.name}'
|
53
|
+
kw: ta.Dict[str, ta.Any] = {}
|
54
|
+
|
55
|
+
if fld.type in _or_opt(str):
|
56
|
+
pass
|
57
|
+
elif fld.type in _or_opt(bool):
|
58
|
+
kw.update(const=True, nargs=0)
|
59
|
+
elif fld.type in _or_opt(int):
|
60
|
+
kw.update(type=int)
|
61
|
+
elif fld.type in _or_opt(float):
|
62
|
+
kw.update(type=float)
|
63
|
+
elif fld.type in _or_opt(ta.Union[int, str]):
|
64
|
+
kw.update(type=_int_or_str)
|
65
|
+
|
66
|
+
elif fld.type in (
|
67
|
+
*_or_opt(ta.Sequence[str]),
|
68
|
+
*_or_opt(ta.Mapping[str, ta.Optional[str]]),
|
69
|
+
*_or_opt(ta.Mapping[int, ta.Union[int, str, None]]),
|
70
|
+
*_or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]),
|
71
|
+
):
|
72
|
+
if aname[-1] != 's':
|
73
|
+
raise NameError(aname)
|
74
|
+
aname = aname[:-1]
|
75
|
+
|
76
|
+
else:
|
77
|
+
raise TypeError(fld)
|
78
|
+
|
79
|
+
parser.add_argument(aname, action=_OrderedArgsAction, **kw)
|
80
|
+
|
81
|
+
|
82
|
+
def _process_arguments(args: ta.Any) -> ta.Sequence[Bootstrap.Config]:
|
83
|
+
if not (oa := getattr(args, 'ordered_args', None)):
|
84
|
+
return []
|
85
|
+
|
86
|
+
cfgs: ta.List[Bootstrap.Config] = []
|
87
|
+
|
88
|
+
for cname, cargs in [
|
89
|
+
(n, list(g))
|
90
|
+
for n, g in
|
91
|
+
itertools.groupby(oa, key=lambda s: s[0].partition(':')[0])
|
92
|
+
]:
|
93
|
+
ccls = BOOTSTRAP_TYPES_BY_NAME[cname].Config
|
94
|
+
flds = {f.name: f for f in dc.fields(ccls)}
|
95
|
+
|
96
|
+
kw: ta.Dict[str, ta.Any] = {}
|
97
|
+
for aname, aval in cargs:
|
98
|
+
k = aname.partition(':')[2]
|
99
|
+
|
100
|
+
if k not in flds:
|
101
|
+
k += 's'
|
102
|
+
fld = flds[k]
|
103
|
+
|
104
|
+
if fld.type in _or_opt(ta.Sequence[str]):
|
105
|
+
kw.setdefault(k, []).append(aval)
|
106
|
+
|
107
|
+
elif fld.type in _or_opt(ta.Mapping[str, ta.Optional[str]]):
|
108
|
+
if '=' not in aval:
|
109
|
+
kw.setdefault(k, {})[aval] = None
|
110
|
+
else:
|
111
|
+
ek, _, ev = aval.partition('=')
|
112
|
+
kw.setdefault(k, {})[ek] = ev
|
113
|
+
|
114
|
+
elif fld.type in _or_opt(ta.Mapping[int, ta.Union[int, str, None]]):
|
115
|
+
fk, _, fv = aval.partition('=')
|
116
|
+
if not fv:
|
117
|
+
kw.setdefault(k, {})[int(fk)] = None
|
118
|
+
else:
|
119
|
+
kw.setdefault(k, {})[int(fk)] = _int_or_str(fv)
|
120
|
+
|
121
|
+
elif fld.type in _or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]):
|
122
|
+
fk, _, fv = aval.partition('=')
|
123
|
+
if ',' in fv:
|
124
|
+
tl, tr = fv.split(',')
|
125
|
+
else:
|
126
|
+
tl, tr = None, None
|
127
|
+
kw.setdefault(k, {})[fk] = (_int_or_str(tl) if tl else None, _int_or_str(tr) if tr else None)
|
128
|
+
|
129
|
+
else:
|
130
|
+
raise TypeError(fld)
|
131
|
+
|
132
|
+
else:
|
133
|
+
kw[k] = aval
|
134
|
+
|
135
|
+
cfg = ccls(**kw)
|
136
|
+
cfgs.append(cfg)
|
137
|
+
|
138
|
+
return cfgs
|
139
|
+
|
140
|
+
|
141
|
+
##
|
142
|
+
|
143
|
+
|
144
|
+
def _main() -> int:
|
145
|
+
parser = argparse.ArgumentParser()
|
146
|
+
|
147
|
+
_add_arguments(parser)
|
148
|
+
|
149
|
+
parser.add_argument('-m', '--module', action='store_true')
|
150
|
+
parser.add_argument('target')
|
151
|
+
parser.add_argument('args', nargs=argparse.REMAINDER)
|
152
|
+
|
153
|
+
args = parser.parse_args()
|
154
|
+
cfgs = _process_arguments(args)
|
155
|
+
|
156
|
+
with bootstrap(*cfgs):
|
157
|
+
tgt = args.target
|
158
|
+
|
159
|
+
if args.module:
|
160
|
+
sys.argv = [tgt, *(args.args or ())]
|
161
|
+
runpy._run_module_as_main(tgt) # type: ignore # noqa
|
162
|
+
|
163
|
+
else:
|
164
|
+
with io.open_code(tgt) as fp:
|
165
|
+
src = fp.read()
|
166
|
+
|
167
|
+
ns = dict(
|
168
|
+
__name__='__main__',
|
169
|
+
__file__=tgt,
|
170
|
+
__builtins__=__builtins__,
|
171
|
+
__spec__=None,
|
172
|
+
)
|
173
|
+
|
174
|
+
import __main__ # noqa
|
175
|
+
__main__.__dict__.clear()
|
176
|
+
__main__.__dict__.update(ns)
|
177
|
+
exec(compile(src, tgt, 'exec'), __main__.__dict__, __main__.__dict__)
|
178
|
+
|
179
|
+
return 0
|
180
|
+
|
181
|
+
|
182
|
+
if __name__ == '__main__':
|
183
|
+
sys.exit(_main())
|