tressed 0.0.1__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.
tressed/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __all__ = []
tressed/__version__.py ADDED
@@ -0,0 +1,13 @@
1
+ __all__ = ["__version__"]
2
+
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ __version__: str
6
+
7
+
8
+ def __getattr__(name: str) -> str:
9
+ if name == "__version__":
10
+ from importlib.metadata import version
11
+
12
+ return version("tressed")
13
+ raise AttributeError(f"Package 'tressed.__version__' has no attribute '{name}'")
tressed/exceptions.py ADDED
@@ -0,0 +1,13 @@
1
+ __all__ = ["TressedError", "TressedTypeError", "TressedValueError"]
2
+
3
+
4
+ class TressedError(Exception):
5
+ pass
6
+
7
+
8
+ class TressedTypeError(TressedError, TypeError):
9
+ pass
10
+
11
+
12
+ class TressedValueError(TressedError, ValueError):
13
+ pass
@@ -0,0 +1,14 @@
1
+ TYPE_CHECKING = False
2
+ if TYPE_CHECKING:
3
+ from tressed.loader.loader import Loader
4
+
5
+ __all__ = ["Loader"]
6
+
7
+
8
+ def __getattr__(name: str) -> type[Loader]:
9
+ if name == "Loader":
10
+ from tressed.loader.loader import Loader
11
+
12
+ return Loader
13
+
14
+ raise AttributeError(f"Package 'tressed.loader' has no attribute '{name}'")
@@ -0,0 +1,152 @@
1
+ # TODO: Use python3.15+ lazy imports instead
2
+ # TODO: Switch to TypeForm https://peps.python.org/pep-0747/ once available
3
+ from __future__ import annotations
4
+
5
+ from tressed.exceptions import TressedTypeError, TressedValueError
6
+
7
+ TYPE_CHECKING = False
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Mapping
10
+ from typing import Any, Final
11
+
12
+ from tressed.loader.types import TypeLoaderFn, TypePath
13
+ from tressed.predicates import TypePredicate
14
+
15
+
16
+ __all__ = [
17
+ "Loader",
18
+ ]
19
+
20
+
21
+ def _default_type_loaders():
22
+ from tressed.loader.loaders import load_simple_scalar
23
+
24
+ return {
25
+ bool: load_simple_scalar,
26
+ int: load_simple_scalar,
27
+ float: load_simple_scalar,
28
+ str: load_simple_scalar,
29
+ type(None): load_simple_scalar,
30
+ }
31
+
32
+
33
+ def _default_type_mappers(specialize: bool) -> Mapping[TypePredicate, TypeLoaderFn]:
34
+ from tressed.loader.loaders import (
35
+ load_dataclass,
36
+ load_simple_collection,
37
+ load_tuple,
38
+ )
39
+ from tressed.predicates import (
40
+ is_dataclass_type,
41
+ is_frozenset_type,
42
+ is_homogeneous_tuple_type,
43
+ is_list_type,
44
+ is_set_type,
45
+ is_tuple_type,
46
+ )
47
+
48
+ load_tuple_ = load_tuple
49
+ load_simple_collection_ = load_simple_collection
50
+ if specialize:
51
+ from tressed.loader.specializer import SpecializingLoader
52
+ from tressed.loader.specializers import (
53
+ specialize_load_simple_collection,
54
+ specialize_load_tuple,
55
+ )
56
+
57
+ load_tuple_ = SpecializingLoader(load_tuple, specialize_load_tuple)
58
+ load_simple_collection_ = SpecializingLoader(
59
+ load_simple_collection, specialize_load_simple_collection
60
+ )
61
+
62
+ # Note that the order matters as some predicates match several types,
63
+ # put the most specific match first.
64
+ return {
65
+ is_homogeneous_tuple_type: load_simple_collection_,
66
+ is_tuple_type: load_tuple_,
67
+ is_list_type: load_simple_collection_,
68
+ is_set_type: load_simple_collection_,
69
+ is_frozenset_type: load_simple_collection_,
70
+ is_dataclass_type: load_dataclass,
71
+ }
72
+
73
+
74
+ type Alias = str
75
+
76
+
77
+ class Loader:
78
+ def __init__(
79
+ self,
80
+ type_loaders: Mapping[type, TypeLoaderFn] | None = None,
81
+ type_mappers: Mapping[TypePredicate, TypeLoaderFn] | None = None,
82
+ enable_specialization: bool = False,
83
+ alias_field: str = "alias",
84
+ ) -> None:
85
+ # Map a type to its loader
86
+ if type_loaders is None:
87
+ self._type_loaders: dict[type, TypeLoaderFn] = _default_type_loaders()
88
+ else:
89
+ self._type_loaders = dict(type_loaders)
90
+
91
+ # Mapping of type predicate to a loader
92
+ if type_mappers is None:
93
+ self._type_mappers: Mapping[TypePredicate, TypeLoaderFn] = (
94
+ _default_type_mappers(enable_specialization)
95
+ )
96
+ else:
97
+ self._type_mappers = dict(type_mappers)
98
+
99
+ self._alias_field: Final = alias_field
100
+ # Mapping of (type, type_form, name) to alias
101
+ self._alias_cache: dict[tuple[type, TypePath, str], Alias] = {}
102
+
103
+ def _resolve_alias_no_cache[T](
104
+ self, type_form: type[T], type_path: TypePath, name: str
105
+ ) -> Alias:
106
+ # TODO: Also have a type mapper for alias resolvers
107
+ if fields := getattr(type_form, "__dataclass_fields__", None):
108
+ field = fields[name]
109
+ alias = field.metadata.get(self._alias_field)
110
+ if alias is None:
111
+ return name
112
+ return alias
113
+
114
+ def _resolve_alias[T](
115
+ self, type_form: type[T], type_path: TypePath, name: str
116
+ ) -> Alias:
117
+ cache_key = (type_form, type_path, name)
118
+ if (alias := self._alias_cache.get(cache_key)) is not None:
119
+ return alias
120
+
121
+ alias = self._resolve_alias_no_cache(type_form, type_path, name)
122
+ self._alias_cache[cache_key] = alias
123
+ return alias
124
+
125
+ def _load[T](self, value: Any, type_form: type[T], type_path: TypePath) -> T:
126
+ if (type_loader := self._type_loaders.get(type_form)) is None:
127
+ for type_predicate, type_loader in self._type_mappers.items():
128
+ if type_predicate(type_form):
129
+ break
130
+ else:
131
+ type_loader = None
132
+ if type_loader is None:
133
+ raise TressedTypeError(
134
+ f"Unhandled type form {type_form!r} at path {type_path!r} for value {value!r}"
135
+ )
136
+
137
+ # Cache lookup for next time
138
+ self._type_loaders[type_form] = type_loader
139
+
140
+ try:
141
+ return type_loader(value, type_form, type_path, self) # type: ignore[call-non-callable]
142
+ except TressedValueError:
143
+ raise
144
+ except Exception as e:
145
+ error = TressedValueError(
146
+ f"Failed to load value {value!r} at path {type_path!r} into type form {type_form!r}"
147
+ )
148
+ error.add_note(f"{type(e)}: {e}")
149
+ raise error from e
150
+
151
+ def load[T](self, value: Any, type_form: type[T]) -> T:
152
+ return self._load(value, type_form, ())
@@ -0,0 +1,80 @@
1
+ from tressed.exceptions import TressedValueError
2
+
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Any
6
+
7
+ from tressed.loader.types import LoaderProtocol, TypePath
8
+
9
+ __all__ = [
10
+ "load_simple_scalar",
11
+ "load_simple_collection",
12
+ "load_tuple",
13
+ "load_dataclass",
14
+ ]
15
+
16
+
17
+ def load_simple_scalar[T](
18
+ value: Any, type_form: type[T], type_path: TypePath, loader: LoaderProtocol
19
+ ) -> T:
20
+ if type(value) is type_form:
21
+ return value
22
+ raise TressedValueError(
23
+ f"Cannot to load value {value!r} at path {type_path!r} into {type_form.__name___}"
24
+ )
25
+
26
+
27
+ def load_simple_collection[T](
28
+ value: Any, type_form: type[T], type_path: TypePath, loader: LoaderProtocol
29
+ ) -> T:
30
+ from tressed.predicates import get_args, get_origin
31
+
32
+ origin = get_origin(type_form)
33
+ num_expected_args = 2 if origin is tuple else 1
34
+ args = get_args(type_form)
35
+ if origin is None or args is None or len(args) != num_expected_args:
36
+ raise TressedValueError(
37
+ f"Cannot to load value {value!r} at path {type_path!r}, "
38
+ f"{getattr(type_form, '__name__', type_form)} is not a homogeneous generic type"
39
+ )
40
+
41
+ item_type = args[0]
42
+ return origin(
43
+ loader._load(item, item_type, (*type_path, pos))
44
+ for pos, item in enumerate(value)
45
+ )
46
+
47
+
48
+ def load_tuple[*Ts](
49
+ value: Any, type_form: tuple[*Ts], type_path: TypePath, loader: LoaderProtocol
50
+ ) -> tuple[*Ts]:
51
+ from tressed.predicates import get_args, get_origin
52
+
53
+ origin = get_origin(type_form)
54
+ args = get_args(type_form)
55
+ if origin is None or args is None:
56
+ raise TressedValueError(
57
+ f"Cannot to load value {value!r} at path {type_path!r}, "
58
+ f"{getattr(type_form, '__name__', type_form)} is not a generic type"
59
+ )
60
+
61
+ return tuple(
62
+ loader._load(item, args[pos], (*type_path, pos))
63
+ for pos, item in enumerate(value)
64
+ )
65
+
66
+
67
+ def load_dataclass[T](
68
+ value: Any, type_form: type[T], type_path: TypePath, loader: LoaderProtocol
69
+ ) -> T:
70
+ from dataclasses import MISSING, fields
71
+
72
+ loaded = {}
73
+
74
+ for field in fields(type_form):
75
+ field_name = field.name
76
+ alias = loader._resolve_alias(type_form, type_path, field_name)
77
+ if (field_value := value.get(alias, MISSING)) is not MISSING:
78
+ loaded[field_name] = field_value
79
+
80
+ return type_form(**loaded)
@@ -0,0 +1,52 @@
1
+ TYPE_CHECKING = False
2
+ if TYPE_CHECKING:
3
+ from typing import Any
4
+
5
+ from tressed.loader.types import LoaderProtocol, TypePath
6
+
7
+ __all__ = ["SpecializingLoader"]
8
+
9
+
10
+ class SpecializingLoader:
11
+ def __init__(self, loader, specializer) -> None:
12
+ self._loader = loader
13
+ self._specialized_loaders: dict = {}
14
+ self._specializer = specializer
15
+ self._threshold: int = 3
16
+
17
+ def __call__[T](
18
+ self,
19
+ value: Any,
20
+ type_form: type[T],
21
+ type_path: TypePath,
22
+ loader: LoaderProtocol,
23
+ ) -> T:
24
+ key = (type_form, type_path)
25
+ specialized_loader = self._specialized_loaders.get(key)
26
+ if type(specialized_loader) is type(None) or type(specialized_loader) is int:
27
+ if type(specialized_loader) is int and specialized_loader > 0:
28
+ count = specialized_loader + 1
29
+ else:
30
+ count = 1
31
+ self._specialized_loaders[key] = count
32
+ specialized_loader = None
33
+
34
+ # Specialize!
35
+ if count > self._threshold:
36
+ specializer_fn_code = self._specializer(*key)
37
+ if specializer_fn_code is None:
38
+ # zero means we can't specialize this
39
+ self._specialized_loaders[key] = 0
40
+ else:
41
+ globals_ = {}
42
+ locals_ = {}
43
+
44
+ exec(specializer_fn_code, globals_, locals_)
45
+
46
+ specialized_loader = locals_["__specialized_fn"]
47
+ self._specialized_loaders[key] = specialized_loader
48
+
49
+ if specialized_loader is None:
50
+ return self._loader(value, type_form, type_path, loader)
51
+ else:
52
+ return specialized_loader(value, loader)
@@ -0,0 +1,219 @@
1
+ TYPE_CHECKING = False
2
+ if TYPE_CHECKING:
3
+ from collections.abc import Iterable
4
+ from typing import Final
5
+
6
+ from tressed.loader.types import TypeLoaderFn, TypePath, TypePathItem
7
+
8
+ __all__ = [
9
+ "specialize_load_tuple",
10
+ "specialize_load_simple_collection",
11
+ ]
12
+
13
+
14
+ class Codegen:
15
+ __slots__ = ("fn_name", "indent", "_parts", "_cached_code")
16
+
17
+ def __init__(self, fn_name: str = "__specialized_fn") -> None:
18
+ self.fn_name: Final = fn_name
19
+ self.indent: Final = " "
20
+
21
+ self._parts = []
22
+ self._cached_code: str | None = None
23
+
24
+ def code(self) -> str:
25
+ if cached_code := self._cached_code:
26
+ return cached_code
27
+ import io
28
+
29
+ builder = io.StringIO()
30
+ builder.write(f"def {self.fn_name}(value, loader):\n")
31
+ for line in self._parts:
32
+ builder.write(line)
33
+ builder.write("\n")
34
+ cached_code = builder.getvalue()
35
+
36
+ self._cached_code = cached_code
37
+ self._parts.clear()
38
+ return cached_code
39
+
40
+ def exec(self) -> TypeLoaderFn:
41
+ # NOTE: This relies on the source being executed being only trusted and validated inputs.
42
+ globals_ = {}
43
+ locals_ = {}
44
+ exec(self.code(), globals_, locals_)
45
+ return locals_[self.fn_name]
46
+
47
+ def _emit(self, part: str) -> None:
48
+ self._parts.append(part)
49
+
50
+ def _emit_indent(self, indent: int = 0) -> None:
51
+ self._emit(self.indent * (indent + 1))
52
+
53
+ def _emit_newline(self) -> None:
54
+ self._emit("\n")
55
+
56
+ def _emit_line(self, line: str, indent: int = 0) -> None:
57
+ self._emit_indent(indent)
58
+ self._emit(line)
59
+ self._emit_newline()
60
+
61
+ def emit_unpack_args(
62
+ self, ident: str, args: tuple[type, ...]
63
+ ) -> tuple[tuple[str, type], ...]:
64
+ # TODO: Allocate identifiers!
65
+ items = tuple((f"_item_{pos}", type_form) for pos, type_form in enumerate(args))
66
+ match items:
67
+ case []:
68
+ pass
69
+ case [(item_ident, _)]:
70
+ self._emit_line(f"{item_ident}, = {ident}")
71
+ case [*_]:
72
+ self._emit_line(
73
+ f"{', '.join(item_ident for item_ident, _ in items)} = {ident}"
74
+ )
75
+ return items
76
+
77
+ def emit_load_fn(self, ident: str = "_load") -> str:
78
+ self._emit_line(f"{ident} = loader._load")
79
+ return ident
80
+
81
+ def emit_load(
82
+ self,
83
+ loader_ident: str,
84
+ ident: str,
85
+ type_form: type,
86
+ type_path: TypePath,
87
+ type_path_item: TypePathItem,
88
+ ) -> str:
89
+ # TODO: Allocate identifiers!
90
+ loaded_ident = f"{ident}_loaded"
91
+ # TODO: Handle type forms!
92
+ self._emit_line(
93
+ f"{loaded_ident} = _load({ident}, {type_form.__qualname__}, {_type_path_repr(*type_path, type_path_item)})"
94
+ )
95
+ return loaded_ident
96
+
97
+ def emit_tuple(self, idents: Iterable[str], *, compact: bool = False) -> None:
98
+ self._emit("(")
99
+ if not compact:
100
+ self._emit_newline()
101
+ for ident in idents:
102
+ if not compact:
103
+ self._emit_indent(indent=1)
104
+ self._emit(f"{ident},")
105
+ if not compact:
106
+ self._emit_newline()
107
+ if not compact:
108
+ self._emit_indent()
109
+ self._emit(")")
110
+ self._emit_newline()
111
+
112
+ def emit_return(self) -> None:
113
+ self._emit_indent()
114
+ self._emit("return ")
115
+
116
+
117
+ def specialize_load_tuple[T](
118
+ type_form: type[T],
119
+ type_path: TypePath,
120
+ ) -> str | None:
121
+ """
122
+ Generate specialized function for given type at given path.
123
+ """
124
+ from tressed.predicates import get_args
125
+
126
+ args = get_args(type_form)
127
+ assert args is not None
128
+
129
+ codegen = Codegen()
130
+ items = codegen.emit_unpack_args("value", args)
131
+ load_fn = codegen.emit_load_fn()
132
+ loaded = []
133
+ for pos, (item, arg_type_form) in enumerate(items):
134
+ loaded.append(codegen.emit_load(load_fn, item, arg_type_form, type_path, pos))
135
+ codegen.emit_return()
136
+ codegen.emit_tuple(loaded)
137
+ return codegen.code()
138
+
139
+
140
+ def _type_form_repr(type_form) -> str:
141
+ from tressed.predicates import get_args, get_origin
142
+
143
+ origin = get_origin(type_form)
144
+ if origin is not None:
145
+ args = get_args(type_form)
146
+ assert args is not None
147
+ return _generic_type_repr(origin, args)
148
+
149
+ return type_form.__qualname__
150
+
151
+
152
+ def _generic_type_repr(origin: type, args: tuple[type, ...]) -> str:
153
+ if len(args) == 0:
154
+ args_str = "()"
155
+ else:
156
+ args_str = ", ".join(map(_type_form_repr, args))
157
+ return f"{origin.__qualname__}[{args_str}]"
158
+
159
+
160
+ class Ident:
161
+ __slots__ = ("name",)
162
+
163
+ def __init__(self, name: str) -> None:
164
+ self.name = name
165
+
166
+
167
+ def _type_path_repr(*args: TypePathItem | Ident) -> str:
168
+ items = []
169
+ for arg in args:
170
+ match arg:
171
+ case Ident():
172
+ items.append(arg.name)
173
+ case _:
174
+ items.append(repr(arg))
175
+
176
+ match items:
177
+ case [item]:
178
+ # Special case for single item tuple
179
+ return f"({item},)"
180
+ case _:
181
+ return f"({', '.join(items)})"
182
+
183
+
184
+ def specialize_load_simple_collection[T](
185
+ type_form: type[T],
186
+ type_path: TypePath,
187
+ ) -> str | None:
188
+ """
189
+ Generate specialized function for given type at given path.
190
+ """
191
+ from tressed.predicates import get_args, get_origin
192
+
193
+ origin = get_origin(type_form)
194
+ assert origin is not None
195
+
196
+ args = get_args(type_form)
197
+ assert args is not None
198
+ arg_type_form = args[0]
199
+
200
+ codegen = Codegen()
201
+
202
+ load_fn = codegen.emit_load_fn()
203
+ codegen.emit_return()
204
+ if origin is list:
205
+ open, close = "[", "]"
206
+ elif origin is set:
207
+ open, close = "{", "}"
208
+ elif origin is frozenset:
209
+ open, close = "({", "})"
210
+ else:
211
+ open, close = f"{origin.__qualname__}([", "])"
212
+
213
+ codegen._emit(f"""{open}
214
+ {load_fn}(item, {_type_form_repr(arg_type_form)}, {_type_path_repr(*type_path, Ident("pos"))})
215
+ for pos, item
216
+ in enumerate(value)
217
+ {close}
218
+ """)
219
+ return codegen.code()
@@ -0,0 +1,26 @@
1
+ __all__ = []
2
+
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from collections.abc import Callable
6
+ from typing import Any, Protocol
7
+
8
+ type TypePathItem = str | int
9
+ type TypePath = tuple[TypePathItem, ...]
10
+
11
+ class LoaderProtocol(Protocol):
12
+ def _load[T](
13
+ self, value: Any, type_form: type[T], type_path: TypePath
14
+ ) -> T: ...
15
+ def _resolve_alias[T](
16
+ self, type_form: type[T], type_path: TypePath, name: str
17
+ ) -> str: ...
18
+
19
+ type TypeLoaderFn[T] = Callable[[Any, type[T], TypePath, LoaderProtocol], T]
20
+
21
+ __all__ += [
22
+ "TypePathItem",
23
+ "TypePath",
24
+ "LoaderProtocol",
25
+ "TypeLoaderFn",
26
+ ]
tressed/predicates.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ Type predicates.
3
+
4
+ Do not import modules if not absolutely required. Instead, take advantage of the fact
5
+ that a type from a module can only be instantiated if it already has been imported.
6
+
7
+ For example to check if a type is an instance of foo.Bar, do not do:
8
+
9
+ def is_foo_bar[T: type](type_form: T) -> bool:
10
+ import foo
11
+
12
+ return subclass(t, foo.Bar)
13
+
14
+ But instead:
15
+
16
+ def is_foo_bar[T: type](type_form: T) -> bool:
17
+ if foo := sys.modules.get("foo"):
18
+ retur issubclass(t, foo.Bar)
19
+ """
20
+
21
+ import sys
22
+
23
+ TYPE_CHECKING = False
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable
26
+ from typing import Any
27
+
28
+ __all__ = [
29
+ "get_origin",
30
+ "get_args",
31
+ "is_tuple_type",
32
+ "is_homogeneous_tuple_type",
33
+ "is_list_type",
34
+ "is_set_type",
35
+ "is_frozenset_type",
36
+ "is_dataclass_type",
37
+ ]
38
+
39
+ if TYPE_CHECKING:
40
+ # TODO: Use actual typing.TypeForm once it has made its way into python.
41
+ # Experimental support is coming in mypy 1.19, the PEP (747) is not accepted yet as of the time of writing.
42
+ type TypeForm = Any
43
+ type TypePredicate[T: TypeForm] = Callable[[T], bool]
44
+
45
+ __all__ += [
46
+ "TypeForm",
47
+ "TypePredicate",
48
+ ]
49
+
50
+
51
+ def get_origin(type_form: TypeForm) -> type | None:
52
+ # Avoid importing typing module if possible.
53
+ # TODO: Handle annotated, generic etc...
54
+ return getattr(type_form, "__origin__", None)
55
+
56
+
57
+ def get_args(type_form: TypeForm) -> tuple[type, ...] | None:
58
+ # Avoid importing typing module if possible.
59
+ # TODO: Handle annotated, generic etc...
60
+ return getattr(type_form, "__args__", None)
61
+
62
+
63
+ def is_homogeneous_tuple_type(type_form: TypeForm) -> bool:
64
+ """
65
+ The given type form matches tuple[T, ...] or typing.Tuple[T, ...]
66
+ """
67
+ origin = get_origin(type_form)
68
+ if origin is None:
69
+ return False
70
+ if origin is tuple or (
71
+ (typing := sys.modules.get("typing")) and origin is typing.Tuple
72
+ ):
73
+ args = get_args(type_form)
74
+ if args is None:
75
+ return False
76
+ return len(args) == 2 and args[1] == Ellipsis
77
+ return False
78
+
79
+
80
+ def is_tuple_type(type_form: TypeForm) -> bool:
81
+ origin = get_origin(type_form)
82
+ if origin is None:
83
+ return False
84
+ if origin is tuple:
85
+ return True
86
+ if typing := sys.modules.get("typing"):
87
+ return origin is typing.Tuple
88
+ return False
89
+
90
+
91
+ def is_list_type(type_form: TypeForm) -> bool:
92
+ origin = get_origin(type_form)
93
+ if origin is None:
94
+ return False
95
+ if origin is list:
96
+ return True
97
+ if typing := sys.modules.get("typing"):
98
+ return origin is typing.List
99
+ return False
100
+
101
+
102
+ def is_set_type(type_form: TypeForm) -> bool:
103
+ origin = get_origin(type_form)
104
+ if origin is None:
105
+ return False
106
+ if origin is set:
107
+ return True
108
+ if typing := sys.modules.get("typing"):
109
+ return origin is typing.Set
110
+ return False
111
+
112
+
113
+ def is_frozenset_type(type_form: TypeForm) -> bool:
114
+ origin = get_origin(type_form)
115
+ if origin is None:
116
+ return False
117
+ if origin is frozenset:
118
+ return True
119
+ if typing := sys.modules.get("typing"):
120
+ return origin is typing.FrozenSet
121
+ return False
122
+
123
+
124
+ def is_dataclass_type(type_form: TypeForm) -> bool:
125
+ if dataclasses := sys.modules.get("dataclasses"):
126
+ # We want to match only dataclass types, not instances
127
+ return dataclasses.is_dataclass(type_form) and isinstance(type_form, type)
128
+ return False
tressed/py.typed ADDED
File without changes
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: tressed
3
+ Version: 0.0.1
4
+ Summary: Glue types together
5
+ Author-email: Thomas Cellerier <thomascellerier@gmail.com>
6
+ Project-URL: Homepage, https://github.com/thomascellerier/tressed
7
+ Project-URL: Repository, https://github.com/thomascellerier/tressed.git
8
+ Requires-Python: >=3.14
9
+ Description-Content-Type: text/markdown
10
+
11
+ # tressed
12
+
13
+ Serialize and deserialize types, in any order.
14
+
15
+ ## Usage
16
+
17
+ ```Python
18
+ from dataclasses import dataclass, field
19
+ from tressed.loader import Loader
20
+
21
+ @dataclass
22
+ class SomeDataclass:
23
+ foo: str
24
+ bar: str = "bar"
25
+ baz: list[int] = field(default_factory=lambda: [1, 2, 3])
26
+ bar_bar: tuple[int, str] = field(metadata={"alias": "barBar"}, kw_only=True)
27
+
28
+ value = {
29
+ "foo": "foo",
30
+ "barBar": (2, "humbug"),
31
+ }
32
+
33
+ loader = Loader()
34
+ loaded = loader.load(value, SomeDataclass)
35
+
36
+ assert loaded == SomeDataclass(
37
+ foo="foo",
38
+ bar="bar",
39
+ baz=[1, 2, 3],
40
+ bar_bar=(2, "humbug"),
41
+ )
42
+ ```
43
+
44
+ ## Supported types
45
+
46
+ The following types are supported out of the box:
47
+
48
+ - bool
49
+ - int
50
+ - float
51
+ - str
52
+ - tuple[T1, ..., Tn], typing.Tuple[T1, ..., Tn]
53
+ - tuple[T, ...], typing.Tuple[T, ...] a.k.a homogeneous tuple
54
+ - list[T], typing.List[T]
55
+ - set[T], typing.Set[T]
56
+ - frozenset[T], typing.FrozenSet[T]
57
+ - dataclasses.dataclass
58
+
59
+ It is easy to add support for custom types as needed when creating a loader.
60
+
61
+ ## Installation
62
+
63
+ Using pip:
64
+ ```bash
65
+ pip install tressed
66
+ ```
67
+
68
+ Or using uv:
69
+ ```bash
70
+ uv add tressed
71
+ ```
72
+
73
+ Then just import tressed and get coding.
74
+ ```Python
75
+ import tressed
76
+ ```
77
+
78
+ ## Goals
79
+
80
+ - Provide easy serialization and deserialization to and from built-in and standard library types.
81
+ - Provide easy way to add support for custom types.
82
+ - Provide easy yet powerful support for serializaiton and deserialization aliases.
83
+ - Pure python, no additional runtime dependencies beyond the standard library.
84
+ - Do as little work as possible to be suitable for CLIs, for example using lazy imports as much as possible.
85
+
86
+ ### Stretch goals
87
+ - Add first class support for concurrent loading and dumping, with support for asyncio and threading, for example deserializing a list of M objects on N threads.
88
+ - Use code generation internally similarly to dataclasses. Instead of doing metaprogramming each time, generate an actual function for the type that does (de)serialization. Interpret the first time, compile the second, using source or ast module, and then reuse the compiled function.
@@ -0,0 +1,15 @@
1
+ tressed/__init__.py,sha256=da1PTClDMl-IBkrSvq6JC1lnS-K_BASzCvxVhNxN5Ls,13
2
+ tressed/__version__.py,sha256=dX3yXfRP2ZDFjTPZdcDtG6g9WiTrOTTdjSs-s_yTDCI,322
3
+ tressed/exceptions.py,sha256=WfPu_fPLH7TUC6njmNpQ3hYXRblMdKAM1X4maFDJPgI,232
4
+ tressed/predicates.py,sha256=ErdPm94LnguB2MO0_6wqlCRLohIbaubPlyIaU_jMZNc,3452
5
+ tressed/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ tressed/loader/__init__.py,sha256=qkO2MxvVUN_9Mi2YUNFssWLV-Qn5y7X0QgHJdl4ss70,331
7
+ tressed/loader/loader.py,sha256=041veJW-Uwd9Se5DDKXaJu4gDDH3J2yAkF2KYLO_0yA,5085
8
+ tressed/loader/loaders.py,sha256=xsq5ONJctuij0gXf2qFoMT-P2yLwLIDEo9F-k1egcBM,2377
9
+ tressed/loader/specializer.py,sha256=u7r7qv1kOS61sguYD6q6TOwx7tzM1GAUxxdoCXzfEzE,1755
10
+ tressed/loader/specializers.py,sha256=sO54CaVFcCTymZ3py8_eH1MP4dxMPv-44G1wb-6a5Jk,6086
11
+ tressed/loader/types.py,sha256=xEuPWB9V0M4LkhF7bLx-upQqlzHUoGl1zkgnYT1IuAg,683
12
+ tressed-0.0.1.dist-info/METADATA,sha256=WglxjqLLfIxncmenKdMPChlrb3_ilUcP5OCqiOah8Os,2411
13
+ tressed-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ tressed-0.0.1.dist-info/top_level.txt,sha256=R3Xw_AtSlP9ol3NXmxHi7TMTSsY60TP6L_70Z--oG9g,8
15
+ tressed-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ tressed