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.
- avoine-0.1.0a1/PKG-INFO +125 -0
- avoine-0.1.0a1/README.md +104 -0
- avoine-0.1.0a1/pyproject.toml +41 -0
- avoine-0.1.0a1/src/avoine/__init__.py +2 -0
- avoine-0.1.0a1/src/avoine/avoine.py +179 -0
- avoine-0.1.0a1/src/avoine/py.typed +0 -0
avoine-0.1.0a1/PKG-INFO
ADDED
|
@@ -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/
|
avoine-0.1.0a1/README.md
ADDED
|
@@ -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,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
|