omlish 0.0.0.dev21__py3-none-any.whl → 0.0.0.dev23__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 +17 -8
- omlish/asyncs/bridge.py +35 -19
- omlish/bootstrap/__init__.py +39 -0
- omlish/bootstrap/base.py +3 -1
- omlish/bootstrap/diag.py +51 -2
- omlish/bootstrap/main.py +2 -1
- omlish/bootstrap/marshal.py +18 -0
- omlish/check.py +123 -33
- omlish/concurrent/__init__.py +11 -0
- omlish/concurrent/executors.py +52 -0
- omlish/{concurrent.py → concurrent/futures.py} +0 -44
- omlish/concurrent/threadlets.py +91 -0
- omlish/diag/asts.py +132 -0
- omlish/diag/pycharm.py +80 -0
- omlish/docker.py +3 -0
- omlish/genmachine.py +58 -0
- omlish/lang/functions.py +3 -0
- omlish/logs/__init__.py +4 -0
- omlish/logs/handlers.py +10 -0
- omlish/marshal/__init__.py +4 -0
- omlish/marshal/base64.py +4 -0
- omlish/marshal/primitives.py +6 -0
- omlish/marshal/standard.py +4 -0
- omlish/marshal/unions.py +101 -0
- omlish/matchfns.py +3 -3
- omlish/secrets/openssl.py +2 -2
- omlish/sql/__init__.py +18 -0
- omlish/sql/qualifiedname.py +82 -0
- {omlish-0.0.0.dev21.dist-info → omlish-0.0.0.dev23.dist-info}/METADATA +13 -9
- {omlish-0.0.0.dev21.dist-info → omlish-0.0.0.dev23.dist-info}/RECORD +33 -23
- {omlish-0.0.0.dev21.dist-info → omlish-0.0.0.dev23.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev21.dist-info → omlish-0.0.0.dev23.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev21.dist-info → omlish-0.0.0.dev23.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
import concurrent.futures as cf
|
2
|
+
import contextlib
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
|
6
|
+
T = ta.TypeVar('T')
|
7
|
+
P = ta.ParamSpec('P')
|
8
|
+
|
9
|
+
|
10
|
+
class ImmediateExecutor(cf.Executor):
|
11
|
+
|
12
|
+
def __init__(self, *, immediate_exceptions: bool = False) -> None:
|
13
|
+
super().__init__()
|
14
|
+
self._immediate_exceptions = immediate_exceptions
|
15
|
+
|
16
|
+
def submit(
|
17
|
+
self,
|
18
|
+
fn: ta.Callable[P, T],
|
19
|
+
/,
|
20
|
+
*args: P.args,
|
21
|
+
**kwargs: P.kwargs,
|
22
|
+
) -> cf.Future[T]:
|
23
|
+
future: ta.Any = cf.Future()
|
24
|
+
try:
|
25
|
+
result = fn(*args, **kwargs)
|
26
|
+
future.set_result(result)
|
27
|
+
except Exception as e:
|
28
|
+
if self._immediate_exceptions:
|
29
|
+
raise
|
30
|
+
future.set_exception(e)
|
31
|
+
return future
|
32
|
+
|
33
|
+
|
34
|
+
@contextlib.contextmanager
|
35
|
+
def new_executor(
|
36
|
+
max_workers: int | None = None,
|
37
|
+
cls: type[cf.Executor] = cf.ThreadPoolExecutor,
|
38
|
+
*,
|
39
|
+
immediate_exceptions: bool = False,
|
40
|
+
**kwargs: ta.Any,
|
41
|
+
) -> ta.Generator[cf.Executor, None, None]:
|
42
|
+
if max_workers == 0:
|
43
|
+
yield ImmediateExecutor(
|
44
|
+
immediate_exceptions=immediate_exceptions,
|
45
|
+
)
|
46
|
+
|
47
|
+
else:
|
48
|
+
with cls( # type: ignore
|
49
|
+
max_workers,
|
50
|
+
**kwargs,
|
51
|
+
) as exe:
|
52
|
+
yield exe
|
@@ -1,11 +1,9 @@
|
|
1
1
|
import concurrent.futures as cf
|
2
|
-
import contextlib
|
3
2
|
import time
|
4
3
|
import typing as ta
|
5
4
|
|
6
5
|
|
7
6
|
T = ta.TypeVar('T')
|
8
|
-
P = ta.ParamSpec('P')
|
9
7
|
|
10
8
|
|
11
9
|
##
|
@@ -137,45 +135,3 @@ def wait_dependent_futures(
|
|
137
135
|
|
138
136
|
futs_by_fn = {fn: fut for fut, fn in fns_by_fut.items()}
|
139
137
|
return futs_by_fn
|
140
|
-
|
141
|
-
|
142
|
-
##
|
143
|
-
|
144
|
-
|
145
|
-
class ImmediateExecutor(cf.Executor):
|
146
|
-
|
147
|
-
def __init__(self, *, immediate_exceptions: bool = False) -> None:
|
148
|
-
super().__init__()
|
149
|
-
self._immediate_exceptions = immediate_exceptions
|
150
|
-
|
151
|
-
def submit(self, fn: ta.Callable[P, T], /, *args: P.args, **kwargs: P.kwargs) -> cf.Future[T]:
|
152
|
-
future: ta.Any = cf.Future()
|
153
|
-
try:
|
154
|
-
result = fn(*args, **kwargs)
|
155
|
-
future.set_result(result)
|
156
|
-
except Exception as e:
|
157
|
-
if self._immediate_exceptions:
|
158
|
-
raise
|
159
|
-
future.set_exception(e)
|
160
|
-
return future
|
161
|
-
|
162
|
-
|
163
|
-
@contextlib.contextmanager
|
164
|
-
def new_executor(
|
165
|
-
max_workers: int | None = None,
|
166
|
-
cls: type[cf.Executor] = cf.ThreadPoolExecutor,
|
167
|
-
*,
|
168
|
-
immediate_exceptions: bool = False,
|
169
|
-
**kwargs: ta.Any,
|
170
|
-
) -> ta.Generator[cf.Executor, None, None]:
|
171
|
-
if max_workers == 0:
|
172
|
-
yield ImmediateExecutor(
|
173
|
-
immediate_exceptions=immediate_exceptions,
|
174
|
-
)
|
175
|
-
|
176
|
-
else:
|
177
|
-
with cls( # type: ignore
|
178
|
-
max_workers,
|
179
|
-
**kwargs,
|
180
|
-
) as exe:
|
181
|
-
yield exe
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import abc
|
2
|
+
import dataclasses as dc
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from omlish import lang
|
6
|
+
|
7
|
+
|
8
|
+
if ta.TYPE_CHECKING:
|
9
|
+
import greenlet
|
10
|
+
else:
|
11
|
+
greenlet = lang.proxy_import('greenlet')
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
class Threadlet(abc.ABC):
|
18
|
+
"""Not safe to identity-key - use `underlying`."""
|
19
|
+
|
20
|
+
def __hash__(self):
|
21
|
+
raise TypeError('use `underlying`')
|
22
|
+
|
23
|
+
def __eq__(self, other):
|
24
|
+
raise TypeError('use `underlying`')
|
25
|
+
|
26
|
+
@property
|
27
|
+
@abc.abstractmethod
|
28
|
+
def underlying(self) -> ta.Any:
|
29
|
+
raise NotImplementedError
|
30
|
+
|
31
|
+
@property
|
32
|
+
@abc.abstractmethod
|
33
|
+
def parent(self) -> ta.Optional['Threadlet']:
|
34
|
+
raise NotImplementedError
|
35
|
+
|
36
|
+
@property
|
37
|
+
@abc.abstractmethod
|
38
|
+
def dead(self) -> bool:
|
39
|
+
raise NotImplementedError
|
40
|
+
|
41
|
+
@abc.abstractmethod
|
42
|
+
def switch(self, *args: ta.Any, **kwargs: ta.Any) -> ta.Any:
|
43
|
+
raise NotImplementedError
|
44
|
+
|
45
|
+
@abc.abstractmethod
|
46
|
+
def throw(self, ex: Exception) -> ta.Any:
|
47
|
+
raise NotImplementedError
|
48
|
+
|
49
|
+
|
50
|
+
class Threadlets(abc.ABC):
|
51
|
+
@abc.abstractmethod
|
52
|
+
def spawn(self, fn: ta.Callable[[], None]) -> Threadlet:
|
53
|
+
raise NotImplementedError
|
54
|
+
|
55
|
+
@abc.abstractmethod
|
56
|
+
def get_current(self) -> Threadlet:
|
57
|
+
raise NotImplementedError
|
58
|
+
|
59
|
+
|
60
|
+
##
|
61
|
+
|
62
|
+
|
63
|
+
@dc.dataclass(frozen=True)
|
64
|
+
class GreenletThreadlet(Threadlet):
|
65
|
+
g: 'greenlet.greenlet'
|
66
|
+
|
67
|
+
@property
|
68
|
+
def underlying(self) -> 'greenlet.greenlet':
|
69
|
+
return self.g
|
70
|
+
|
71
|
+
@property
|
72
|
+
def parent(self) -> ta.Optional['GreenletThreadlet']:
|
73
|
+
return GreenletThreadlet(self.g.parent)
|
74
|
+
|
75
|
+
@property
|
76
|
+
def dead(self) -> bool:
|
77
|
+
return self.g.dead
|
78
|
+
|
79
|
+
def switch(self, *args: ta.Any, **kwargs: ta.Any) -> ta.Any:
|
80
|
+
return self.g.switch(*args, **kwargs)
|
81
|
+
|
82
|
+
def throw(self, ex: Exception) -> ta.Any:
|
83
|
+
return self.g.throw(ex)
|
84
|
+
|
85
|
+
|
86
|
+
class GreenletThreadlets(Threadlets):
|
87
|
+
def spawn(self, fn: ta.Callable[[], None]) -> Threadlet:
|
88
|
+
return GreenletThreadlet(greenlet.greenlet(fn))
|
89
|
+
|
90
|
+
def get_current(self) -> Threadlet:
|
91
|
+
return GreenletThreadlet(greenlet.getcurrent())
|
omlish/diag/asts.py
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
import ast
|
2
|
+
import dataclasses as dc
|
3
|
+
import inspect
|
4
|
+
import pprint
|
5
|
+
import textwrap
|
6
|
+
import types
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from .. import lang
|
10
|
+
|
11
|
+
|
12
|
+
if ta.TYPE_CHECKING:
|
13
|
+
import executing
|
14
|
+
else:
|
15
|
+
executing = lang.proxy_import('executing')
|
16
|
+
|
17
|
+
|
18
|
+
class ArgsRenderer:
|
19
|
+
"""
|
20
|
+
TODO:
|
21
|
+
- kwargs
|
22
|
+
- recursion
|
23
|
+
- whatever pytest looks like
|
24
|
+
- make sure not leaking sensitive data
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
origin: types.TracebackType | types.FrameType | None = None,
|
30
|
+
back: int = 1,
|
31
|
+
) -> None:
|
32
|
+
super().__init__()
|
33
|
+
|
34
|
+
self._origin = origin
|
35
|
+
|
36
|
+
self._frame: types.FrameType | None
|
37
|
+
if isinstance(origin, types.TracebackType):
|
38
|
+
self._frame = origin.tb_frame
|
39
|
+
elif isinstance(origin, types.FrameType):
|
40
|
+
self._frame = origin
|
41
|
+
elif origin is None:
|
42
|
+
frame = inspect.currentframe()
|
43
|
+
for _ in range(back + 1):
|
44
|
+
if frame is None:
|
45
|
+
break
|
46
|
+
frame = frame.f_back
|
47
|
+
self._frame = frame
|
48
|
+
else:
|
49
|
+
raise TypeError(origin)
|
50
|
+
|
51
|
+
def _get_indented_text(
|
52
|
+
self,
|
53
|
+
src: executing.Source,
|
54
|
+
node: ast.AST,
|
55
|
+
) -> str:
|
56
|
+
result = src.asttokens().get_text(node)
|
57
|
+
if '\n' in result:
|
58
|
+
result = ' ' * node.first_token.start[1] + result # type: ignore
|
59
|
+
result = textwrap.dedent(result)
|
60
|
+
result = result.strip()
|
61
|
+
return result
|
62
|
+
|
63
|
+
def _val_to_string(self, obj: ta.Any) -> str:
|
64
|
+
s = pprint.pformat(obj)
|
65
|
+
s = s.replace('\\n', '\n')
|
66
|
+
return s
|
67
|
+
|
68
|
+
def _is_literal_expr(self, s: str) -> bool:
|
69
|
+
try:
|
70
|
+
ast.literal_eval(s)
|
71
|
+
except Exception: # noqa
|
72
|
+
return False
|
73
|
+
return True
|
74
|
+
|
75
|
+
@dc.dataclass(frozen=True)
|
76
|
+
class RenderedArg:
|
77
|
+
val: str
|
78
|
+
expr: str
|
79
|
+
is_literal_expr: bool
|
80
|
+
|
81
|
+
def __str__(self) -> str:
|
82
|
+
if self.is_literal_expr:
|
83
|
+
return self.val
|
84
|
+
else:
|
85
|
+
return f'({self.expr} = {self.val})'
|
86
|
+
|
87
|
+
def render_args(
|
88
|
+
self,
|
89
|
+
*vals: ta.Any,
|
90
|
+
) -> ta.Sequence[RenderedArg] | None:
|
91
|
+
if self._frame is None:
|
92
|
+
return None
|
93
|
+
|
94
|
+
call_node = executing.Source.executing(self._frame).node
|
95
|
+
if not isinstance(call_node, ast.Call):
|
96
|
+
return None
|
97
|
+
|
98
|
+
source = executing.Source.for_frame(self._frame)
|
99
|
+
|
100
|
+
exprs = [
|
101
|
+
self._get_indented_text(source, arg) # noqa
|
102
|
+
for arg in call_node.args
|
103
|
+
]
|
104
|
+
if len(exprs) != len(vals):
|
105
|
+
return None
|
106
|
+
|
107
|
+
return [
|
108
|
+
self.RenderedArg(
|
109
|
+
val=self._val_to_string(val),
|
110
|
+
expr=expr,
|
111
|
+
is_literal_expr=self._is_literal_expr(expr),
|
112
|
+
)
|
113
|
+
for val, expr in zip(vals, exprs)
|
114
|
+
]
|
115
|
+
|
116
|
+
@classmethod
|
117
|
+
def smoketest(cls) -> bool:
|
118
|
+
def bar(z):
|
119
|
+
return z + 1
|
120
|
+
|
121
|
+
def foo(x, y):
|
122
|
+
return cls().render_args(x, y)
|
123
|
+
|
124
|
+
r = foo(1, bar(2))
|
125
|
+
if r is None:
|
126
|
+
return False
|
127
|
+
|
128
|
+
x, y = r
|
129
|
+
return (
|
130
|
+
x == cls.RenderedArg(val='1', expr='1', is_literal_expr=True) and
|
131
|
+
y == cls.RenderedArg(val='3', expr='bar(2)', is_literal_expr=False)
|
132
|
+
)
|
omlish/diag/pycharm.py
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
import os.path
|
2
|
+
import plistlib
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from .. import check
|
8
|
+
from .. import lang
|
9
|
+
|
10
|
+
|
11
|
+
if ta.TYPE_CHECKING:
|
12
|
+
docker = lang.proxy_import('omlish.docker')
|
13
|
+
else:
|
14
|
+
from omlish import docker
|
15
|
+
|
16
|
+
|
17
|
+
##
|
18
|
+
|
19
|
+
|
20
|
+
PYCHARM_HOME = '/Applications/PyCharm.app'
|
21
|
+
|
22
|
+
|
23
|
+
def read_pycharm_info_plist() -> ta.Mapping[str, ta.Any] | None:
|
24
|
+
plist_file = os.path.join(PYCHARM_HOME, 'Contents', 'Info.plist')
|
25
|
+
if not os.path.isfile(plist_file):
|
26
|
+
return None
|
27
|
+
|
28
|
+
with open(plist_file, 'rb') as f:
|
29
|
+
root = plistlib.load(f)
|
30
|
+
|
31
|
+
return root
|
32
|
+
|
33
|
+
|
34
|
+
@lang.cached_function
|
35
|
+
def get_pycharm_version() -> str | None:
|
36
|
+
plist = read_pycharm_info_plist()
|
37
|
+
if plist is None:
|
38
|
+
return None
|
39
|
+
|
40
|
+
ver = check.non_empty_str(plist['CFBundleVersion'])
|
41
|
+
check.state(ver.startswith('PY-'))
|
42
|
+
return ver[3:]
|
43
|
+
|
44
|
+
|
45
|
+
##
|
46
|
+
|
47
|
+
|
48
|
+
def pycharm_remote_debugger_attach(
|
49
|
+
host: str | None,
|
50
|
+
port: int,
|
51
|
+
*,
|
52
|
+
version: str | None = None,
|
53
|
+
) -> None:
|
54
|
+
# if version is None:
|
55
|
+
# version = get_pycharm_version()
|
56
|
+
# check.non_empty_str(version)
|
57
|
+
|
58
|
+
if host is None:
|
59
|
+
if sys.platform == 'linux' and docker.is_likely_in_docker():
|
60
|
+
host = docker.DOCKER_FOR_MAC_HOSTNAME
|
61
|
+
else:
|
62
|
+
host = 'localhost'
|
63
|
+
|
64
|
+
try:
|
65
|
+
import pydevd_pycharm # noqa
|
66
|
+
except ImportError:
|
67
|
+
subprocess.check_call([
|
68
|
+
sys.executable,
|
69
|
+
'-mpip',
|
70
|
+
'install',
|
71
|
+
'pydevd-pycharm' + (f'~={version}' if version is not None else ''),
|
72
|
+
])
|
73
|
+
|
74
|
+
import pydevd_pycharm # noqa
|
75
|
+
pydevd_pycharm.settrace(
|
76
|
+
host,
|
77
|
+
port=port,
|
78
|
+
stdoutToServer=True,
|
79
|
+
stderrToServer=True,
|
80
|
+
)
|
omlish/docker.py
CHANGED
omlish/genmachine.py
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
"""
|
2
|
+
https://github.com/pytransitions/transitions
|
3
|
+
"""
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
|
7
|
+
I = ta.TypeVar('I')
|
8
|
+
O = ta.TypeVar('O')
|
9
|
+
|
10
|
+
# MachineGen: ta.TypeAlias = ta.Generator[ta.Iterable[O] | None, I, ta.Optional[MachineGen[I, O]]]
|
11
|
+
MachineGen: ta.TypeAlias = ta.Generator[ta.Any, ta.Any, ta.Any]
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
class IllegalStateError(Exception):
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
class GenMachine(ta.Generic[I, O]):
|
22
|
+
"""
|
23
|
+
Generator-powered state machine. Generators are sent an `I` object and yield any number of `O` objects in response,
|
24
|
+
until they yield a `None` by accepting new input. Generators may return a new generator to switch states, or return
|
25
|
+
`None` to terminate.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, initial: MachineGen) -> None:
|
29
|
+
super().__init__()
|
30
|
+
self._advance(initial)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def state(self) -> str | None:
|
34
|
+
if self._gen is not None:
|
35
|
+
return self._gen.gi_code.co_qualname
|
36
|
+
return None
|
37
|
+
|
38
|
+
def __repr__(self) -> str:
|
39
|
+
return f'{self.__class__.__name__}@{hex(id(self))[2:]}<{self.state}>'
|
40
|
+
|
41
|
+
_gen: MachineGen | None
|
42
|
+
|
43
|
+
def _advance(self, gen: MachineGen) -> None:
|
44
|
+
self._gen = gen
|
45
|
+
if (n := next(self._gen)) is not None: # noqa
|
46
|
+
raise IllegalStateError
|
47
|
+
|
48
|
+
def __call__(self, i: I) -> ta.Iterable[O]:
|
49
|
+
if self._gen is None:
|
50
|
+
raise IllegalStateError
|
51
|
+
try:
|
52
|
+
while (o := self._gen.send(i)) is not None:
|
53
|
+
yield from o
|
54
|
+
except StopIteration as s:
|
55
|
+
if s.value is None:
|
56
|
+
self._gen = None
|
57
|
+
return None
|
58
|
+
self._advance(s.value)
|
omlish/lang/functions.py
CHANGED
omlish/logs/__init__.py
CHANGED
omlish/logs/handlers.py
ADDED
omlish/marshal/__init__.py
CHANGED
omlish/marshal/base64.py
CHANGED
omlish/marshal/primitives.py
CHANGED
@@ -9,6 +9,9 @@ from .base import Unmarshaler
|
|
9
9
|
from .values import Value
|
10
10
|
|
11
11
|
|
12
|
+
##
|
13
|
+
|
14
|
+
|
12
15
|
PRIMITIVE_TYPES: tuple[type, ...] = (
|
13
16
|
bool,
|
14
17
|
int,
|
@@ -19,6 +22,9 @@ PRIMITIVE_TYPES: tuple[type, ...] = (
|
|
19
22
|
)
|
20
23
|
|
21
24
|
|
25
|
+
##
|
26
|
+
|
27
|
+
|
22
28
|
class PrimitiveMarshalerUnmarshaler(Marshaler, Unmarshaler):
|
23
29
|
def marshal(self, ctx: MarshalContext, o: ta.Any) -> Value:
|
24
30
|
if isinstance(o, PRIMITIVE_TYPES):
|
omlish/marshal/standard.py
CHANGED
@@ -27,6 +27,8 @@ from .optionals import OptionalMarshalerFactory
|
|
27
27
|
from .optionals import OptionalUnmarshalerFactory
|
28
28
|
from .primitives import PRIMITIVE_MARSHALER_FACTORY
|
29
29
|
from .primitives import PRIMITIVE_UNMARSHALER_FACTORY
|
30
|
+
from .unions import PrimitiveUnionMarshalerFactory
|
31
|
+
from .unions import PrimitiveUnionUnmarshalerFactory
|
30
32
|
from .uuids import UUID_MARSHALER_FACTORY
|
31
33
|
from .uuids import UUID_UNMARSHALER_FACTORY
|
32
34
|
|
@@ -37,6 +39,7 @@ from .uuids import UUID_UNMARSHALER_FACTORY
|
|
37
39
|
STANDARD_MARSHALER_FACTORIES: list[MarshalerFactory] = [
|
38
40
|
PRIMITIVE_MARSHALER_FACTORY,
|
39
41
|
OptionalMarshalerFactory(),
|
42
|
+
PrimitiveUnionMarshalerFactory(),
|
40
43
|
DataclassMarshalerFactory(),
|
41
44
|
EnumMarshalerFactory(),
|
42
45
|
NUMBERS_MARSHALER_FACTORY,
|
@@ -66,6 +69,7 @@ def new_standard_marshaler_factory() -> MarshalerFactory:
|
|
66
69
|
STANDARD_UNMARSHALER_FACTORIES: list[UnmarshalerFactory] = [
|
67
70
|
PRIMITIVE_UNMARSHALER_FACTORY,
|
68
71
|
OptionalUnmarshalerFactory(),
|
72
|
+
PrimitiveUnionUnmarshalerFactory(),
|
69
73
|
DataclassUnmarshalerFactory(),
|
70
74
|
EnumUnmarshalerFactory(),
|
71
75
|
NUMBERS_UNMARSHALER_FACTORY,
|
omlish/marshal/unions.py
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
import dataclasses as dc
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from .. import check
|
5
|
+
from .. import matchfns as mfs
|
6
|
+
from .. import reflect as rfl
|
7
|
+
from .base import MarshalContext
|
8
|
+
from .base import Marshaler
|
9
|
+
from .base import MarshalerFactory
|
10
|
+
from .base import UnmarshalContext
|
11
|
+
from .base import Unmarshaler
|
12
|
+
from .base import UnmarshalerFactory
|
13
|
+
from .values import Value
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
class MatchUnionMarshaler(Marshaler):
|
20
|
+
mmf: mfs.MultiMatchFn[[UnmarshalContext, Value], ta.Any]
|
21
|
+
|
22
|
+
def marshal(self, ctx: MarshalContext, o: ta.Any) -> Value:
|
23
|
+
try:
|
24
|
+
m = self.mmf.match(ctx, o)
|
25
|
+
except mfs.AmbiguousMatchesError:
|
26
|
+
raise ValueError(o) # noqa
|
27
|
+
return m.fn(ctx, o)
|
28
|
+
|
29
|
+
|
30
|
+
class MatchUnionUnmarshaler(Unmarshaler):
|
31
|
+
mmf: mfs.MultiMatchFn[[UnmarshalContext, Value], ta.Any]
|
32
|
+
|
33
|
+
def unmarshal(self, ctx: UnmarshalContext, v: Value) -> ta.Any:
|
34
|
+
try:
|
35
|
+
m = self.mmf.match(ctx, v)
|
36
|
+
except mfs.AmbiguousMatchesError:
|
37
|
+
raise ValueError(v) # noqa
|
38
|
+
return m.fn(ctx, v)
|
39
|
+
|
40
|
+
|
41
|
+
##
|
42
|
+
|
43
|
+
|
44
|
+
PRIMITIVE_UNION_TYPES: tuple[type, ...] = (
|
45
|
+
float,
|
46
|
+
int,
|
47
|
+
str,
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
#
|
52
|
+
|
53
|
+
|
54
|
+
@dc.dataclass(frozen=True)
|
55
|
+
class PrimitiveUnionMarshaler(Marshaler):
|
56
|
+
tys: ta.Sequence[type]
|
57
|
+
|
58
|
+
def marshal(self, ctx: MarshalContext, o: ta.Any) -> Value:
|
59
|
+
raise NotImplementedError
|
60
|
+
|
61
|
+
|
62
|
+
@dc.dataclass(frozen=True)
|
63
|
+
class PrimitiveUnionMarshalerFactory(MarshalerFactory):
|
64
|
+
tys: ta.Sequence[type] = PRIMITIVE_UNION_TYPES
|
65
|
+
|
66
|
+
def guard(self, ctx: MarshalContext, rty: rfl.Type) -> bool:
|
67
|
+
return isinstance(rty, rfl.Union) and all(a in self.tys for a in rty.args)
|
68
|
+
|
69
|
+
def fn(self, ctx: MarshalContext, rty: rfl.Type) -> Marshaler:
|
70
|
+
args = check.isinstance(rty, rfl.Union).args
|
71
|
+
return PrimitiveUnionMarshaler([t for t in self.tys if t in args])
|
72
|
+
|
73
|
+
|
74
|
+
#
|
75
|
+
|
76
|
+
|
77
|
+
@dc.dataclass(frozen=True)
|
78
|
+
class PrimitiveUnionUnmarshaler(Unmarshaler):
|
79
|
+
tys: ta.Sequence[type]
|
80
|
+
|
81
|
+
def unmarshal(self, ctx: UnmarshalContext, v: Value) -> ta.Any:
|
82
|
+
raise NotImplementedError
|
83
|
+
|
84
|
+
|
85
|
+
@dc.dataclass(frozen=True)
|
86
|
+
class PrimitiveUnionUnmarshalerFactory(UnmarshalerFactory):
|
87
|
+
tys: ta.Sequence[type] = PRIMITIVE_UNION_TYPES
|
88
|
+
|
89
|
+
def guard(self, ctx: UnmarshalContext, rty: rfl.Type) -> bool:
|
90
|
+
return isinstance(rty, rfl.Union) and all(a in self.tys for a in rty.args)
|
91
|
+
|
92
|
+
def fn(self, ctx: UnmarshalContext, rty: rfl.Type) -> Unmarshaler:
|
93
|
+
args = check.isinstance(rty, rfl.Union).args
|
94
|
+
return PrimitiveUnionUnmarshaler([t for t in self.tys if t in args])
|
95
|
+
|
96
|
+
|
97
|
+
#
|
98
|
+
|
99
|
+
|
100
|
+
PRIMITIVE_UNION_MARSHALER_FACTORY = PrimitiveUnionMarshalerFactory()
|
101
|
+
PRIMITIVE_UNION_UNMARSHALER_FACTORY = PrimitiveUnionUnmarshalerFactory()
|