omlish 0.0.0.dev18__py3-none-any.whl → 0.0.0.dev20__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 (46) hide show
  1. omlish/__about__.py +3 -3
  2. omlish/asyncs/anyio.py +13 -6
  3. omlish/bootstrap/__init__.py +3 -0
  4. omlish/bootstrap/__main__.py +4 -0
  5. omlish/bootstrap/base.py +38 -0
  6. omlish/bootstrap/diag.py +128 -0
  7. omlish/bootstrap/harness.py +81 -0
  8. omlish/bootstrap/main.py +183 -0
  9. omlish/bootstrap/sys.py +361 -0
  10. omlish/dataclasses/__init__.py +2 -0
  11. omlish/dataclasses/impl/metadata.py +2 -1
  12. omlish/dataclasses/utils.py +45 -0
  13. omlish/defs.py +29 -5
  14. omlish/fnpairs.py +1 -1
  15. omlish/formats/json.py +1 -0
  16. omlish/lang/descriptors.py +2 -0
  17. omlish/lite/logs.py +2 -2
  18. omlish/logs/configs.py +11 -23
  19. omlish/logs/noisy.py +15 -0
  20. omlish/marshal/__init__.py +4 -0
  21. omlish/marshal/dataclasses.py +16 -3
  22. omlish/marshal/helpers.py +22 -0
  23. omlish/marshal/objects.py +33 -14
  24. omlish/multiprocessing.py +32 -0
  25. omlish/specs/__init__.py +0 -0
  26. omlish/specs/jsonschema/__init__.py +0 -0
  27. omlish/specs/jsonschema/keywords/__init__.py +42 -0
  28. omlish/specs/jsonschema/keywords/base.py +86 -0
  29. omlish/specs/jsonschema/keywords/core.py +26 -0
  30. omlish/specs/jsonschema/keywords/metadata.py +22 -0
  31. omlish/specs/jsonschema/keywords/parse.py +69 -0
  32. omlish/specs/jsonschema/keywords/render.py +47 -0
  33. omlish/specs/jsonschema/keywords/validation.py +68 -0
  34. omlish/specs/jsonschema/schemas/__init__.py +0 -0
  35. omlish/specs/jsonschema/schemas/draft202012/__init__.py +0 -0
  36. omlish/specs/jsonschema/schemas/draft202012/vocabularies/__init__.py +0 -0
  37. omlish/specs/jsonschema/types.py +10 -0
  38. omlish/testing/pytest/plugins/__init__.py +8 -3
  39. omlish/testing/pytest/plugins/depskip.py +81 -0
  40. omlish/testing/pytest/plugins/utils.py +14 -0
  41. {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev20.dist-info}/METADATA +3 -3
  42. {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev20.dist-info}/RECORD +45 -22
  43. omlish/bootstrap.py +0 -746
  44. {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev20.dist-info}/LICENSE +0 -0
  45. {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev20.dist-info}/WHEEL +0 -0
  46. {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev20.dist-info}/top_level.txt +0 -0
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev18'
2
- __revision__ = '534f2a8b308f6514a06a41bba629c71a05497327'
1
+ __version__ = '0.0.0.dev20'
2
+ __revision__ = '56a3421b375112818e306f2c17d467c7aac1d26b'
3
3
 
4
4
 
5
5
  #
@@ -88,7 +88,7 @@ class Project(ProjectBase):
88
88
  'sqlx': [
89
89
  'sqlean.py ~= 3.45; python_version < "3.13"',
90
90
 
91
- 'duckdb ~= 1.0',
91
+ 'duckdb ~= 1.1',
92
92
  ],
93
93
 
94
94
  'testing': [
omlish/asyncs/anyio.py CHANGED
@@ -23,6 +23,7 @@ async def killer(shutdown: anyio.Event, sleep_s: float) -> None:
23
23
  shutdown.set()
24
24
 
25
25
  """ # noqa
26
+ import dataclasses as dc
26
27
  import signal
27
28
  import typing as ta
28
29
 
@@ -44,6 +45,12 @@ StapledByteStream: ta.TypeAlias = anyio.streams.stapled.StapledByteStream
44
45
  StapledObjectStream: ta.TypeAlias = anyio.streams.stapled.StapledObjectStream
45
46
 
46
47
 
48
+ @dc.dataclass(eq=False)
49
+ class MemoryStapledObjectStream(StapledObjectStream[T]):
50
+ send_stream: MemoryObjectSendStream[T]
51
+ receive_stream: MemoryObjectReceiveStream[T]
52
+
53
+
47
54
  ##
48
55
 
49
56
 
@@ -143,8 +150,8 @@ def split_memory_object_streams(
143
150
  return tup
144
151
 
145
152
 
146
- def create_stapled_memory_object_stream(max_buffer_size: float = 0) -> StapledObjectStream:
147
- return StapledObjectStream(*anyio.create_memory_object_stream(max_buffer_size))
153
+ def create_stapled_memory_object_stream(max_buffer_size: float = 0) -> MemoryStapledObjectStream:
154
+ return MemoryStapledObjectStream(*anyio.create_memory_object_stream(max_buffer_size))
148
155
 
149
156
 
150
157
  # FIXME: https://github.com/python/mypy/issues/15238
@@ -158,9 +165,9 @@ def create_memory_object_stream[T](max_buffer_size: float = 0) -> tuple[
158
165
 
159
166
  def staple_memory_object_stream(
160
167
  *args: anyio.create_memory_object_stream[T],
161
- ) -> StapledObjectStream[T]:
168
+ ) -> MemoryStapledObjectStream[T]:
162
169
  send, receive = args
163
- return StapledObjectStream(
170
+ return MemoryStapledObjectStream(
164
171
  check.isinstance(send, MemoryObjectSendStream), # type: ignore
165
172
  check.isinstance(receive, MemoryObjectReceiveStream), # type: ignore
166
173
  )
@@ -168,9 +175,9 @@ def staple_memory_object_stream(
168
175
 
169
176
  # FIXME: https://github.com/python/mypy/issues/15238
170
177
  # FIXME: https://youtrack.jetbrains.com/issues?q=tag:%20%7BPEP%20695%7D
171
- def staple_memory_object_stream2[T](max_buffer_size: float = 0) -> StapledObjectStream[T]:
178
+ def staple_memory_object_stream2[T](max_buffer_size: float = 0) -> MemoryStapledObjectStream[T]:
172
179
  send, receive = anyio.create_memory_object_stream[T](max_buffer_size)
173
- return StapledObjectStream(
180
+ return MemoryStapledObjectStream(
174
181
  check.isinstance(send, MemoryObjectSendStream), # type: ignore
175
182
  check.isinstance(receive, MemoryObjectReceiveStream), # type: ignore
176
183
  )
@@ -0,0 +1,3 @@
1
+ from .harness import ( # noqa
2
+ bootstrap,
3
+ )
@@ -0,0 +1,4 @@
1
+ if __name__ == '__main__':
2
+ from .main import _main
3
+
4
+ _main()
@@ -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
@@ -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
@@ -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())