avoine 0.1.0a1__tar.gz

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.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: avoine
3
+ Version: 0.1.0a1
4
+ Summary: Tiny serialisation and deserialisation library
5
+ Author: classabbyamp
6
+ Author-email: classabbyamp <dev@placeviolette.net>
7
+ License-Expression: CC0-1.0
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: File Formats
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.12
17
+ Project-URL: homepage, https://codeberg.org/classabbyamp/avoine
18
+ Project-URL: issues, https://codeberg.org/classabbyamp/avoine/issues
19
+ Project-URL: source, https://codeberg.org/classabbyamp/avoine
20
+ Description-Content-Type: text/markdown
21
+
22
+ # avoine
23
+
24
+ A tiny serialisation and deserialisation library.
25
+
26
+ `avoine` does not implement any serialisation or deserialisation itself. Instead,
27
+ it relies on the common Python pattern of `load` and `dump` functions (for file-like
28
+ objects) and `loads` and `dumps` functions (for string-like objects).
29
+
30
+ To configure serialisation and deserialisation, use the `avoine` class:
31
+
32
+ ```py
33
+ import json
34
+ import tomllib
35
+
36
+ from avoine import avoine
37
+
38
+ # define a format for deserialisation (load -> from file, loads -> from string/bytes)
39
+ # and serialisation (dump -> from file, dumps -> from string/bytes) and set it as default
40
+ avoine.register("json", default=True, load=json.load, loads=json.loads, dump=json.dump, dumps=json.dumps)
41
+
42
+ # not all methods are required
43
+ avoine.register("toml", load=tomllib.load)
44
+
45
+ # register can be used again to update a format definition
46
+ avoine.register("toml", loads=tomllib.loads)
47
+
48
+ # the default format is what is used when the format is left unspecified
49
+ # it can be set or unset on its own
50
+ avoine.default = "toml"
51
+ avoine.default = None
52
+
53
+ # and retrieved with
54
+ print(avoine.default)
55
+
56
+ # you can unregister a format too
57
+ avoine.unregister("toml", default="json")
58
+
59
+ # the list of currently-registered formats can be retrieved with
60
+ avoine.formats()
61
+
62
+ # or for a specific method with
63
+ avoine.formats("load")
64
+
65
+ # all configuration can be reset with
66
+ avoine.clear()
67
+ ```
68
+
69
+ Then, in your code, you can define dataclasses and add the `AvoineBase` mixin class,
70
+ which will allow you to serialise/deserialise them from any registered format anywhere:
71
+
72
+ ```py
73
+ from dataclasses import dataclass
74
+ from avoine import AvoineBase
75
+
76
+ @dataclass
77
+ class MyStruct(AvoineBase):
78
+ foo: str
79
+ bar: MyInnerStruct
80
+
81
+ @dataclass
82
+ class MyInnerStruct(AvoineBase):
83
+ baz: list[str]
84
+ opt: str | None = None
85
+
86
+ with open("mystruct.json") as f:
87
+ data = MyStruct.load(f)
88
+ # kwargs for the underlying loader/dumper are passed down to them
89
+ data.dumps(indent=2)
90
+
91
+ with open("mystruct.toml") as f:
92
+ print(MyStruct.load(f, format="toml"))
93
+ ```
94
+
95
+ Custom formats can be created by writing functions with the following signatures:
96
+
97
+ ```py
98
+ def load(fp: SupportsRead[AnyStr], **kwargs) -> dict[str, Any]:
99
+ ...
100
+
101
+ def loads(s: AnyStr, **kwargs) -> dict[str, Any]:
102
+ ...
103
+
104
+ def dump(obj: Any, fp: SupportsWrite[AnyStr], **kwargs) -> None:
105
+ ...
106
+
107
+ def dumps(obj: Any, **kwargs) -> AnyStr:
108
+ ...
109
+ ```
110
+
111
+ Notes:
112
+
113
+ - `AnyStr` can be `str`, `bytes`, or both
114
+ - `kwargs` are optional
115
+
116
+ If a set of loading/dumping functions for a format do not match these signatures,
117
+ wrapper functions can be written to transform them to a compatible signature.
118
+
119
+ ## License
120
+
121
+ avoine is in the public domain.
122
+
123
+ To the extent possible under law, classabbyamp has waived all copyright and related or neighboring rights to this work.
124
+
125
+ http://creativecommons.org/publicdomain/zero/1.0/
@@ -0,0 +1,104 @@
1
+ # avoine
2
+
3
+ A tiny serialisation and deserialisation library.
4
+
5
+ `avoine` does not implement any serialisation or deserialisation itself. Instead,
6
+ it relies on the common Python pattern of `load` and `dump` functions (for file-like
7
+ objects) and `loads` and `dumps` functions (for string-like objects).
8
+
9
+ To configure serialisation and deserialisation, use the `avoine` class:
10
+
11
+ ```py
12
+ import json
13
+ import tomllib
14
+
15
+ from avoine import avoine
16
+
17
+ # define a format for deserialisation (load -> from file, loads -> from string/bytes)
18
+ # and serialisation (dump -> from file, dumps -> from string/bytes) and set it as default
19
+ avoine.register("json", default=True, load=json.load, loads=json.loads, dump=json.dump, dumps=json.dumps)
20
+
21
+ # not all methods are required
22
+ avoine.register("toml", load=tomllib.load)
23
+
24
+ # register can be used again to update a format definition
25
+ avoine.register("toml", loads=tomllib.loads)
26
+
27
+ # the default format is what is used when the format is left unspecified
28
+ # it can be set or unset on its own
29
+ avoine.default = "toml"
30
+ avoine.default = None
31
+
32
+ # and retrieved with
33
+ print(avoine.default)
34
+
35
+ # you can unregister a format too
36
+ avoine.unregister("toml", default="json")
37
+
38
+ # the list of currently-registered formats can be retrieved with
39
+ avoine.formats()
40
+
41
+ # or for a specific method with
42
+ avoine.formats("load")
43
+
44
+ # all configuration can be reset with
45
+ avoine.clear()
46
+ ```
47
+
48
+ Then, in your code, you can define dataclasses and add the `AvoineBase` mixin class,
49
+ which will allow you to serialise/deserialise them from any registered format anywhere:
50
+
51
+ ```py
52
+ from dataclasses import dataclass
53
+ from avoine import AvoineBase
54
+
55
+ @dataclass
56
+ class MyStruct(AvoineBase):
57
+ foo: str
58
+ bar: MyInnerStruct
59
+
60
+ @dataclass
61
+ class MyInnerStruct(AvoineBase):
62
+ baz: list[str]
63
+ opt: str | None = None
64
+
65
+ with open("mystruct.json") as f:
66
+ data = MyStruct.load(f)
67
+ # kwargs for the underlying loader/dumper are passed down to them
68
+ data.dumps(indent=2)
69
+
70
+ with open("mystruct.toml") as f:
71
+ print(MyStruct.load(f, format="toml"))
72
+ ```
73
+
74
+ Custom formats can be created by writing functions with the following signatures:
75
+
76
+ ```py
77
+ def load(fp: SupportsRead[AnyStr], **kwargs) -> dict[str, Any]:
78
+ ...
79
+
80
+ def loads(s: AnyStr, **kwargs) -> dict[str, Any]:
81
+ ...
82
+
83
+ def dump(obj: Any, fp: SupportsWrite[AnyStr], **kwargs) -> None:
84
+ ...
85
+
86
+ def dumps(obj: Any, **kwargs) -> AnyStr:
87
+ ...
88
+ ```
89
+
90
+ Notes:
91
+
92
+ - `AnyStr` can be `str`, `bytes`, or both
93
+ - `kwargs` are optional
94
+
95
+ If a set of loading/dumping functions for a format do not match these signatures,
96
+ wrapper functions can be written to transform them to a compatible signature.
97
+
98
+ ## License
99
+
100
+ avoine is in the public domain.
101
+
102
+ To the extent possible under law, classabbyamp has waived all copyright and related or neighboring rights to this work.
103
+
104
+ http://creativecommons.org/publicdomain/zero/1.0/
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "avoine"
3
+ version = "0.1.0a1"
4
+ description = "Tiny serialisation and deserialisation library"
5
+ license = "CC0-1.0"
6
+ readme = "README.md"
7
+ authors = [
8
+ { name = "classabbyamp", email = "dev@placeviolette.net" }
9
+ ]
10
+
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: File Formats",
19
+ "Typing :: Typed",
20
+ ]
21
+
22
+ requires-python = ">=3.12"
23
+ dependencies = []
24
+
25
+ [project.urls]
26
+ homepage = "https://codeberg.org/classabbyamp/avoine"
27
+ source = "https://codeberg.org/classabbyamp/avoine"
28
+ issues = "https://codeberg.org/classabbyamp/avoine/issues"
29
+
30
+ [build-system]
31
+ requires = ["uv_build>=0.9.13,<0.10.0"]
32
+ build-backend = "uv_build"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ { include-group = "test" },
37
+ ]
38
+ test = [
39
+ "pytest>=9.0.2",
40
+ "pytest-cov>=7.0.0",
41
+ ]
@@ -0,0 +1,2 @@
1
+ from .avoine import avoine, AvoineBase
2
+ __all__ = ["avoine", "AvoineBase"]
@@ -0,0 +1,179 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, asdict
3
+ from typing import TYPE_CHECKING, Any, AnyStr, Concatenate, Literal, Self
4
+
5
+ if TYPE_CHECKING:
6
+ from _typeshed import DataclassInstance, SupportsRead, SupportsWrite
7
+ else:
8
+ DataclassInstance = object
9
+
10
+ type Load[AnyStr: (str, bytes)] = Callable[Concatenate[SupportsRead[AnyStr], ...], dict[str, Any]]
11
+ type Loads[AnyStr: (str, bytes)] = Callable[Concatenate[AnyStr, ...], dict[str, Any]]
12
+
13
+ type Dump[AnyStr: (str, bytes)] = Callable[Concatenate[Any, SupportsWrite[AnyStr], ...], None]
14
+ type Dumps[AnyStr: (str, bytes)] = Callable[Concatenate[Any, ...], AnyStr]
15
+
16
+
17
+ @dataclass
18
+ class AvoineFormat:
19
+ load: Load | None = None
20
+ loads: Loads | None = None
21
+ dump: Dump | None = None
22
+ dumps: Dumps | None = None
23
+
24
+
25
+ class Avoine:
26
+ """Manages state for avoine"""
27
+
28
+ __fmts: dict[str, AvoineFormat] = {}
29
+ __default_fmt: str | None = None
30
+
31
+ def register(self, name: str, *, default: bool = False,
32
+ load: Load | None = None,
33
+ loads: Loads | None = None,
34
+ dump: Dump | None = None,
35
+ dumps: Dumps | None = None):
36
+ """Register a loading/dumping format
37
+
38
+ :param name: the name of the format
39
+ :param default: make this format the default (used when unspecified)
40
+ :param load: the loader from file-like object function
41
+ :param loads: the loader from string-like object function
42
+ :param dump: the dumper to file-like object function
43
+ :param dumps: the dumper to string-like object function
44
+ """
45
+ if name not in self.__fmts:
46
+ self.__fmts[name] = AvoineFormat()
47
+ self.__fmts[name].load = load
48
+ self.__fmts[name].loads = loads
49
+ self.__fmts[name].dump = dump
50
+ self.__fmts[name].dumps = dumps
51
+ if default:
52
+ self.__default_fmt = name
53
+
54
+ def unregister(self, name: str, default: str | None = None):
55
+ """Unregisters a format
56
+
57
+ :param name: the name of the format
58
+ :param default: the name of the new default format
59
+ """
60
+ if name in self.__fmts:
61
+ del self.__fmts[name]
62
+ if self.default == name:
63
+ self.default = None
64
+ if default:
65
+ self.default = default
66
+
67
+ def clear(self):
68
+ """Clear all registered formats"""
69
+ self.__fmts = {}
70
+ self.__default_fmt = None
71
+
72
+ def formats(self, method: Literal["load", "loads", "dump", "dumps"] | None = None) -> list[str]:
73
+ """List all registered formats
74
+
75
+ :param method: list all registered formats that implement this method
76
+ :raises ValueError: if the method is unknown
77
+ """
78
+ match method:
79
+ case "load" | "loads" | "dump" | "dumps":
80
+ return [k for k, v in self.__fmts.items() if getattr(v, method) is not None]
81
+ case None:
82
+ return list(self.__fmts.keys())
83
+ case _:
84
+ raise ValueError(f"unknown method: {method}")
85
+
86
+ @property
87
+ def default(self) -> str | None:
88
+ """Get the default format for loading/dumping
89
+
90
+ :return: the name of the default format (or `None` if not set)
91
+ """
92
+ return self.__default_fmt
93
+
94
+ @default.setter
95
+ def default(self, format: str | None = None):
96
+ """Set the default format for loading/dumping
97
+
98
+ :param format: the new default format
99
+ :raises ValueError: if the format is not registered
100
+ """
101
+ if format is not None and format not in self.__fmts:
102
+ raise ValueError(f"unknown format: {format}")
103
+ self.__default_fmt = format
104
+
105
+ def _get_format(self, format: str | None = None) -> AvoineFormat:
106
+ if (not format and not (format := self.default)):
107
+ raise NotImplementedError(f"no default format set, format must be specified")
108
+ if format not in self.__fmts:
109
+ raise ValueError(f"unknown format: {format}")
110
+ return self.__fmts[format]
111
+
112
+
113
+ class AvoineBase(DataclassInstance):
114
+ """Mixin to add loading/dumping capabilities to a dataclass"""
115
+
116
+ @classmethod
117
+ def load(cls, fp: SupportsRead[AnyStr], format: str | None = None, **kwargs) -> Self:
118
+ """Load data from a file-like object
119
+
120
+ Exceptions from the underlying loader are passed up.
121
+
122
+ :param fp: the file-like object to read from
123
+ :param format: the format to use for loading
124
+ :param kwargs: keyword arguments passed to the underlying loader
125
+ :raises ValueError: if the format is not registered
126
+ :raises NotImplementedError: if loading from file-like object is not supported for the specified format
127
+ """
128
+ if (load := avoine._get_format(format).load) is None:
129
+ raise NotImplementedError(f"loading from {format} file is not supported")
130
+ return cls(**load(fp, **kwargs))
131
+
132
+ @classmethod
133
+ def loads(cls, s: AnyStr, format: str | None = None, **kwargs) -> Self:
134
+ """Load data from a string-like object
135
+
136
+ Exceptions from the underlying loader are passed up.
137
+
138
+ :param s: the string-like object to read from
139
+ :param format: the format to use for loading
140
+ :param kwargs: keyword arguments passed to the underlying loader
141
+ :raises ValueError: if the format is not registered
142
+ :raises NotImplementedError: if loading from string-like object is not supported for the specified format
143
+ """
144
+ if (loads := avoine._get_format(format).loads) is None:
145
+ raise NotImplementedError(f"loading from {format} string is not supported")
146
+ return cls(**loads(s, **kwargs))
147
+
148
+ def dump(self, fp: SupportsWrite[AnyStr], format: str | None = None, **kwargs):
149
+ """Dump data to a file-like object
150
+
151
+ Exceptions from the underlying dumper are passed up.
152
+
153
+ :param fp: the file-like object to write to
154
+ :param format: the format to use for dumping
155
+ :param kwargs: keyword arguments passed to the underlying dumper
156
+ :raises ValueError: if the format is not registered
157
+ :raises NotImplementedError: if dumping to file-like object is not supported for the specified format
158
+ """
159
+ if (dump := avoine._get_format(format).dump) is None:
160
+ raise NotImplementedError(f"dumping to {format} file is not supported")
161
+ dump(asdict(self), fp, **kwargs)
162
+
163
+ def dumps(self, format: str | None = None, **kwargs) -> AnyStr:
164
+ """Dump data to a string-like object
165
+
166
+ Exceptions from the underlying dumper are passed up.
167
+
168
+ :param format: the format to use for dumping
169
+ :param kwargs: keyword arguments passed to the underlying dumper
170
+ :raises ValueError: if the format is not registered
171
+ :raises NotImplementedError: if dumping to string-like object is not supported for the specified format
172
+ :return: the dumped data
173
+ """
174
+ if (dumps := avoine._get_format(format).dumps) is None:
175
+ raise NotImplementedError(f"dumping to {format} string is not supported")
176
+ return dumps(asdict(self), **kwargs)
177
+
178
+
179
+ avoine = Avoine()
File without changes