soia-client 1.0.0__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.
- soia_client-1.0.0.dist-info/METADATA +12 -0
- soia_client-1.0.0.dist-info/RECORD +21 -0
- soia_client-1.0.0.dist-info/WHEEL +5 -0
- soia_client-1.0.0.dist-info/top_level.txt +1 -0
- soialib/__init__.py +0 -0
- soialib/impl/__init__.py +0 -0
- soialib/impl/arrays.py +163 -0
- soialib/impl/encoding.py +41 -0
- soialib/impl/enums.py +414 -0
- soialib/impl/function_maker.py +196 -0
- soialib/impl/optionals.py +74 -0
- soialib/impl/primitives.py +210 -0
- soialib/impl/repr.py +43 -0
- soialib/impl/structs.py +718 -0
- soialib/impl/type_adapter.py +56 -0
- soialib/keyed_items.py +14 -0
- soialib/module_initializer.py +79 -0
- soialib/never.py +4 -0
- soialib/serializer.py +87 -0
- soialib/spec.py +148 -0
- soialib/timestamp.py +127 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def make_function(
|
|
7
|
+
name: str,
|
|
8
|
+
params: Sequence[Union[str, "Param"]],
|
|
9
|
+
body: Sequence[Union[str, "Line"]],
|
|
10
|
+
) -> Callable:
|
|
11
|
+
params = [Param(p, default=None) if isinstance(p, str) else p for p in params]
|
|
12
|
+
|
|
13
|
+
def make_locals() -> _Locals:
|
|
14
|
+
# First, collect all the (key, value) pairs.
|
|
15
|
+
# There may be both key duplicates and value duplicates. They will be resolved
|
|
16
|
+
# later.
|
|
17
|
+
all_pieces: list[Union[str, _Local]] = []
|
|
18
|
+
for param in params:
|
|
19
|
+
if param._default is not None:
|
|
20
|
+
all_pieces.extend(param._default._pieces)
|
|
21
|
+
for line in body:
|
|
22
|
+
if isinstance(line, LineSpan):
|
|
23
|
+
all_pieces.extend(line._pieces)
|
|
24
|
+
all_locals = (p for p in all_pieces if isinstance(p, _Local))
|
|
25
|
+
|
|
26
|
+
locals: dict[str, Any] = {}
|
|
27
|
+
reversed: dict[int, str] = {}
|
|
28
|
+
prefix_to_count: dict[str, int] = {}
|
|
29
|
+
for local in all_locals:
|
|
30
|
+
name = local.name
|
|
31
|
+
value = local.value
|
|
32
|
+
if id(value) in reversed:
|
|
33
|
+
# We already have this value, either under the same name or under a
|
|
34
|
+
# different name. Either way, we can reuse the name we already have.
|
|
35
|
+
continue
|
|
36
|
+
if name.endswith("?"):
|
|
37
|
+
# Replace the "?" suffix with a number obtained from a sequence so there
|
|
38
|
+
# is no name conflict.
|
|
39
|
+
prefix = name[0:-1]
|
|
40
|
+
count = prefix_to_count.get(prefix, 0)
|
|
41
|
+
prefix_to_count[prefix] = count + 1
|
|
42
|
+
name = prefix + str(count)
|
|
43
|
+
if name in locals:
|
|
44
|
+
raise ValueError(f"Duplicate local: name={name}; value={value}")
|
|
45
|
+
locals[name] = value
|
|
46
|
+
reversed[id(value)] = name
|
|
47
|
+
|
|
48
|
+
return _Locals(locals, reversed)
|
|
49
|
+
|
|
50
|
+
locals = make_locals()
|
|
51
|
+
|
|
52
|
+
def line_to_code(l: Union[str, Line]) -> str:
|
|
53
|
+
if isinstance(l, str):
|
|
54
|
+
return l
|
|
55
|
+
return l._to_code(locals)
|
|
56
|
+
|
|
57
|
+
body_str = "\n ".join(line_to_code(l) for l in body) if body else "pass"
|
|
58
|
+
text = f"""
|
|
59
|
+
def __create_function__({', '.join(locals.locals.keys())}):
|
|
60
|
+
def {name}({', '.join(p._to_code(locals) for p in params)}):
|
|
61
|
+
{body_str}
|
|
62
|
+
return {name}
|
|
63
|
+
"""
|
|
64
|
+
ns = {}
|
|
65
|
+
exec(text, None, ns)
|
|
66
|
+
return ns["__create_function__"](**locals.locals)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class LineSpan:
|
|
71
|
+
"An immutable span within a line of Python code."
|
|
72
|
+
|
|
73
|
+
_pieces: tuple[Union[str, "_Local"], ...]
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def join(*spans: Union[str, "LineSpan"], separator: str = "") -> "LineSpan":
|
|
77
|
+
def get_pieces(
|
|
78
|
+
span: Union[str, "LineSpan"],
|
|
79
|
+
) -> tuple[Union[str, "_Local"], ...]:
|
|
80
|
+
if isinstance(span, LineSpan):
|
|
81
|
+
return span._pieces
|
|
82
|
+
return ((span),)
|
|
83
|
+
|
|
84
|
+
# Remove empty strings.
|
|
85
|
+
spans = tuple(s for s in spans if s)
|
|
86
|
+
if len(spans) == 0:
|
|
87
|
+
return _EMPTY_LINE_SPAN
|
|
88
|
+
elif len(spans) == 1:
|
|
89
|
+
only_span = spans[0]
|
|
90
|
+
if isinstance(only_span, LineSpan):
|
|
91
|
+
return only_span
|
|
92
|
+
return LineSpan((only_span,))
|
|
93
|
+
if separator:
|
|
94
|
+
pieces = itertools.chain(
|
|
95
|
+
get_pieces(spans[0]),
|
|
96
|
+
*itertools.chain(
|
|
97
|
+
itertools.chain((separator,), get_pieces(s)) for s in spans[1:]
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
pieces = itertools.chain(*(get_pieces(s) for s in spans))
|
|
102
|
+
return LineSpan(tuple(pieces))
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def local(name: str, value: Any):
|
|
106
|
+
return LineSpan((_Local(name, value),))
|
|
107
|
+
|
|
108
|
+
def _to_code(self, locals: "_Locals") -> str:
|
|
109
|
+
ret = ""
|
|
110
|
+
for piece in self._pieces:
|
|
111
|
+
if isinstance(piece, _Local):
|
|
112
|
+
# Look up the resolved name for the value.
|
|
113
|
+
# It migth be different from `piece.name` in some edge cases.
|
|
114
|
+
ret += locals.reversed[id(piece.value)]
|
|
115
|
+
else:
|
|
116
|
+
ret += piece
|
|
117
|
+
return ret
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
_EMPTY_LINE_SPAN = LineSpan(())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# TODO: comment
|
|
124
|
+
LineSpanLike = Union[str, LineSpan]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# A type alias to use when the line span is a Python expression.
|
|
128
|
+
Expr = LineSpan
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# TODO: comment
|
|
132
|
+
ExprLike = LineSpanLike
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# A type alias to use when the line span is the whole line.
|
|
136
|
+
Line = LineSpan
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# An immutable sequence of lines.
|
|
140
|
+
BodySpan = tuple[Union[str, Line], ...]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# A type alias to use when the lines constitute the whole body of a function.
|
|
144
|
+
Body = BodySpan
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class BodyBuilder:
|
|
148
|
+
"""Arnold Schwarzenegger."""
|
|
149
|
+
|
|
150
|
+
_lines: list[LineSpan]
|
|
151
|
+
|
|
152
|
+
def __init__(self):
|
|
153
|
+
self._lines = []
|
|
154
|
+
|
|
155
|
+
def append_ln(self, *spans: Union[str, LineSpan]):
|
|
156
|
+
self._lines.append(Line.join(*spans))
|
|
157
|
+
|
|
158
|
+
def extend(self, other: Iterable[Union[str, Line]], indent: str = ""):
|
|
159
|
+
self._lines.extend(Line.join(indent, l) for l in other)
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def build(self) -> Body:
|
|
163
|
+
return tuple(self._lines)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class Param:
|
|
168
|
+
name: str
|
|
169
|
+
default: Optional[ExprLike] = None
|
|
170
|
+
|
|
171
|
+
def _to_code(self, locals: "_Locals") -> str:
|
|
172
|
+
if self._default is None:
|
|
173
|
+
return self.name
|
|
174
|
+
return f"{self.name}={self._default._to_code(locals)}"
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def _default(self) -> Optional[Expr]:
|
|
178
|
+
if self.default is None:
|
|
179
|
+
return None
|
|
180
|
+
else:
|
|
181
|
+
return Expr.join(self.default)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
Params = list[Union[str, Param]]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class _Local:
|
|
189
|
+
name: str
|
|
190
|
+
value: Any
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True)
|
|
194
|
+
class _Locals:
|
|
195
|
+
locals: dict[str, Any]
|
|
196
|
+
reversed: dict[Any, str]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
from weakref import WeakKeyDictionary
|
|
5
|
+
|
|
6
|
+
from soialib import spec
|
|
7
|
+
from soialib.impl.encoding import NULL_WIRE
|
|
8
|
+
from soialib.impl.function_maker import Expr, ExprLike
|
|
9
|
+
from soialib.impl.type_adapter import TypeAdapter
|
|
10
|
+
|
|
11
|
+
Other = TypeVar("Other")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_optional_adapter(other_adapter: TypeAdapter) -> TypeAdapter:
|
|
15
|
+
return _other_adapter_to_optional_adapter.setdefault(
|
|
16
|
+
other_adapter, _OptionalAdapter(other_adapter)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class _OptionalAdapter(TypeAdapter):
|
|
22
|
+
__slots__ = ("other_adapter",)
|
|
23
|
+
|
|
24
|
+
other_adapter: TypeAdapter
|
|
25
|
+
|
|
26
|
+
def default_expr(self) -> ExprLike:
|
|
27
|
+
return "None"
|
|
28
|
+
|
|
29
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
30
|
+
return Expr.join(
|
|
31
|
+
"(None if ",
|
|
32
|
+
arg_expr,
|
|
33
|
+
" is None else ",
|
|
34
|
+
self.other_adapter.to_frozen_expr(arg_expr),
|
|
35
|
+
")",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> Expr:
|
|
39
|
+
return Expr.join(arg_expr, " is not None")
|
|
40
|
+
|
|
41
|
+
def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
|
|
42
|
+
other_to_json = self.other_adapter.to_json_expr(in_expr, readable)
|
|
43
|
+
if other_to_json == in_expr:
|
|
44
|
+
return in_expr
|
|
45
|
+
return Expr.join(
|
|
46
|
+
"(None if ",
|
|
47
|
+
in_expr,
|
|
48
|
+
" is None else ",
|
|
49
|
+
other_to_json,
|
|
50
|
+
")",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def from_json_expr(self, json_expr: ExprLike) -> ExprLike:
|
|
54
|
+
other_from_json = self.other_adapter.from_json_expr(json_expr)
|
|
55
|
+
if other_from_json == json_expr:
|
|
56
|
+
return json_expr
|
|
57
|
+
return Expr.join(
|
|
58
|
+
"(None if ",
|
|
59
|
+
json_expr,
|
|
60
|
+
" is None else ",
|
|
61
|
+
other_from_json,
|
|
62
|
+
")",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def finalize(
|
|
66
|
+
self,
|
|
67
|
+
resolve_type_fn: Callable[[spec.Type], "TypeAdapter"],
|
|
68
|
+
) -> None:
|
|
69
|
+
self.other_adapter.finalize(resolve_type_fn)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_other_adapter_to_optional_adapter: WeakKeyDictionary[TypeAdapter, TypeAdapter] = (
|
|
73
|
+
WeakKeyDictionary()
|
|
74
|
+
)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Final, final
|
|
4
|
+
|
|
5
|
+
from soialib import spec
|
|
6
|
+
from soialib.impl.function_maker import Expr, ExprLike
|
|
7
|
+
from soialib.impl.type_adapter import TypeAdapter
|
|
8
|
+
from soialib.timestamp import Timestamp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractPrimitiveAdapter(TypeAdapter):
|
|
12
|
+
@final
|
|
13
|
+
def finalize(
|
|
14
|
+
self,
|
|
15
|
+
resolve_type_fn: Callable[[spec.Type], "TypeAdapter"],
|
|
16
|
+
) -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _BoolAdapter(AbstractPrimitiveAdapter):
|
|
21
|
+
def default_expr(self) -> ExprLike:
|
|
22
|
+
return "False"
|
|
23
|
+
|
|
24
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
25
|
+
return Expr.join("(True if ", arg_expr, " else False)")
|
|
26
|
+
|
|
27
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
|
|
28
|
+
return arg_expr
|
|
29
|
+
|
|
30
|
+
def to_json_expr(
|
|
31
|
+
self,
|
|
32
|
+
in_expr: ExprLike,
|
|
33
|
+
readable: bool,
|
|
34
|
+
) -> ExprLike:
|
|
35
|
+
if readable:
|
|
36
|
+
return Expr.join("(1 if ", in_expr, " else 0)")
|
|
37
|
+
else:
|
|
38
|
+
return in_expr
|
|
39
|
+
|
|
40
|
+
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
41
|
+
return Expr.join("(True if ", json_expr, " else False)")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
BOOL_ADAPTER: Final[TypeAdapter] = _BoolAdapter()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class _AbstractIntAdapter(AbstractPrimitiveAdapter):
|
|
49
|
+
"""Type adapter implementation for int32, int64 and uint64."""
|
|
50
|
+
|
|
51
|
+
def default_expr(self) -> ExprLike:
|
|
52
|
+
return "0"
|
|
53
|
+
|
|
54
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
55
|
+
return Expr.join("(", arg_expr, " | 0)")
|
|
56
|
+
|
|
57
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
|
|
58
|
+
return arg_expr
|
|
59
|
+
|
|
60
|
+
def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
|
|
61
|
+
return in_expr
|
|
62
|
+
|
|
63
|
+
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
64
|
+
return Expr.join("(", json_expr, " | 0)")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class _Int32Adapter(_AbstractIntAdapter):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class _Int64Adapter(_AbstractIntAdapter):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class _Uint64Adapter(_AbstractIntAdapter):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
INT32_ADAPTER: Final[TypeAdapter] = _Int32Adapter()
|
|
83
|
+
INT64_ADAPTER: Final[TypeAdapter] = _Int64Adapter()
|
|
84
|
+
UINT64_ADAPTER: Final[TypeAdapter] = _Uint64Adapter()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class _AbstractFloatAdapter(AbstractPrimitiveAdapter):
|
|
89
|
+
"""Type adapter implementation for float32 and float64."""
|
|
90
|
+
|
|
91
|
+
def default_expr(self) -> ExprLike:
|
|
92
|
+
return "0.0"
|
|
93
|
+
|
|
94
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
95
|
+
return Expr.join("(", arg_expr, " + 0.0)")
|
|
96
|
+
|
|
97
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
|
|
98
|
+
return arg_expr
|
|
99
|
+
|
|
100
|
+
def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
|
|
101
|
+
return in_expr
|
|
102
|
+
|
|
103
|
+
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
104
|
+
return Expr.join("(", json_expr, " + 0.0)")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class _Float32Adapter(_AbstractFloatAdapter):
|
|
109
|
+
"""Type adapter implementation for float32."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class _Float64Adapter(_AbstractFloatAdapter):
|
|
114
|
+
"""Type adapter implementation for float32."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
FLOAT32_ADAPTER: Final[TypeAdapter] = _Float32Adapter()
|
|
118
|
+
FLOAT64_ADAPTER: Final[TypeAdapter] = _Float64Adapter()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _TimestampAdapter(AbstractPrimitiveAdapter):
|
|
122
|
+
def default_expr(self) -> Expr:
|
|
123
|
+
return Expr.local("_EPOCH", Timestamp.EPOCH)
|
|
124
|
+
|
|
125
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
126
|
+
timestamp_local = Expr.local("Timestamp", Timestamp)
|
|
127
|
+
return Expr.join(
|
|
128
|
+
"(",
|
|
129
|
+
arg_expr,
|
|
130
|
+
" if ",
|
|
131
|
+
arg_expr,
|
|
132
|
+
".__class__ is ",
|
|
133
|
+
timestamp_local,
|
|
134
|
+
" else ",
|
|
135
|
+
timestamp_local,
|
|
136
|
+
"(unix_millis=",
|
|
137
|
+
arg_expr,
|
|
138
|
+
".unix_millis))",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> Expr:
|
|
142
|
+
return Expr.join(arg_expr, ".unix_millis")
|
|
143
|
+
|
|
144
|
+
def to_json_expr(self, in_expr: ExprLike, readable: bool) -> Expr:
|
|
145
|
+
if readable:
|
|
146
|
+
return Expr.join(in_expr, "._trj()")
|
|
147
|
+
else:
|
|
148
|
+
return Expr.join(in_expr, ".unix_millis")
|
|
149
|
+
|
|
150
|
+
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
151
|
+
fn = Expr.local("_timestamp_from_json", _timestamp_from_json)
|
|
152
|
+
return Expr.join(fn, "(", json_expr, ")")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _timestamp_from_json(json: Any) -> Timestamp:
|
|
156
|
+
if json.__class__ is int or isinstance(json, int):
|
|
157
|
+
return Timestamp(unix_millis=json)
|
|
158
|
+
else:
|
|
159
|
+
return Timestamp(unix_millis=json["unix_millis"])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
TIMESTAMP_ADAPTER: Final[TypeAdapter] = _TimestampAdapter()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _StringAdapter(AbstractPrimitiveAdapter):
|
|
166
|
+
def default_expr(self) -> ExprLike:
|
|
167
|
+
return '""'
|
|
168
|
+
|
|
169
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
170
|
+
return Expr.join("('' + ", arg_expr, ")")
|
|
171
|
+
|
|
172
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
|
|
173
|
+
return arg_expr
|
|
174
|
+
|
|
175
|
+
def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
|
|
176
|
+
return in_expr
|
|
177
|
+
|
|
178
|
+
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
179
|
+
return Expr.join("('' + (", json_expr, " or ''))")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
STRING_ADAPTER: Final[TypeAdapter] = _StringAdapter()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class _BytesAdapter(AbstractPrimitiveAdapter):
|
|
186
|
+
_fromhex_fn: Final = bytes.fromhex
|
|
187
|
+
|
|
188
|
+
def default_expr(self) -> ExprLike:
|
|
189
|
+
return 'b""'
|
|
190
|
+
|
|
191
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
192
|
+
return Expr.join("(b'' + ", arg_expr, ")")
|
|
193
|
+
|
|
194
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
|
|
195
|
+
return arg_expr
|
|
196
|
+
|
|
197
|
+
def to_json_expr(
|
|
198
|
+
self,
|
|
199
|
+
in_expr: ExprLike,
|
|
200
|
+
readable: bool,
|
|
201
|
+
) -> Expr:
|
|
202
|
+
return Expr.join(in_expr, ".hex()")
|
|
203
|
+
|
|
204
|
+
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
205
|
+
return Expr.join(
|
|
206
|
+
Expr.local("fromhex", _BytesAdapter._fromhex_fn), "(", json_expr, ")"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
BYTES_ADAPTER: Final[TypeAdapter] = _BytesAdapter()
|
soialib/impl/repr.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ReprResult:
|
|
8
|
+
repr: str
|
|
9
|
+
complex: bool
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def indented(self) -> str:
|
|
13
|
+
if complex:
|
|
14
|
+
return self.repr.replace("\n", "\n ")
|
|
15
|
+
else:
|
|
16
|
+
return self.repr
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def repr_impl(input: Any) -> ReprResult:
|
|
20
|
+
if isinstance(input, list) or isinstance(input, tuple):
|
|
21
|
+
if len(input) == 0:
|
|
22
|
+
return ReprResult("[]", complex=False)
|
|
23
|
+
elif len(input) == 1:
|
|
24
|
+
only_item_repr = repr_impl(input[0])
|
|
25
|
+
if only_item_repr.complex:
|
|
26
|
+
return ReprResult(f"[\n {only_item_repr.indented},\n]", complex=True)
|
|
27
|
+
else:
|
|
28
|
+
return ReprResult(f"[{only_item_repr.repr}]", complex=True)
|
|
29
|
+
else:
|
|
30
|
+
body = "".join(f" {repr_impl(item).indented},\n" for item in input)
|
|
31
|
+
return ReprResult(f"[\n{body}]", complex=True)
|
|
32
|
+
elif isinstance(input, str):
|
|
33
|
+
if "\n" in input:
|
|
34
|
+
# TODO: comment
|
|
35
|
+
lines = input.split("\n")
|
|
36
|
+
body = "".join(f" {repr(line)},\n" for line in lines)
|
|
37
|
+
return ReprResult(f"'\\n'.join([\n{body}])", complex=True)
|
|
38
|
+
else:
|
|
39
|
+
return ReprResult(repr(input), complex=False)
|
|
40
|
+
else:
|
|
41
|
+
r = repr(input)
|
|
42
|
+
# Complex if the representation contains '[', '(' or '\n'.
|
|
43
|
+
return ReprResult(r, complex=re.search(r"[\\[\\(\\\n]", r) is not None)
|