rPickle 0.1.0__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.
- rpickle-0.1.0/LICENSE +21 -0
- rpickle-0.1.0/PKG-INFO +119 -0
- rpickle-0.1.0/README.md +100 -0
- rpickle-0.1.0/pyproject.toml +34 -0
- rpickle-0.1.0/setup.cfg +4 -0
- rpickle-0.1.0/src/rPickle/__init__.py +10 -0
- rpickle-0.1.0/src/rPickle/core.py +370 -0
- rpickle-0.1.0/src/rPickle/ext.py +44 -0
- rpickle-0.1.0/src/rPickle/py.typed +6 -0
- rpickle-0.1.0/src/rPickle.egg-info/PKG-INFO +119 -0
- rpickle-0.1.0/src/rPickle.egg-info/SOURCES.txt +16 -0
- rpickle-0.1.0/src/rPickle.egg-info/dependency_links.txt +1 -0
- rpickle-0.1.0/src/rPickle.egg-info/requires.txt +4 -0
- rpickle-0.1.0/src/rPickle.egg-info/top_level.txt +1 -0
- rpickle-0.1.0/tests/test_basic.py +41 -0
- rpickle-0.1.0/tests/test_containers.py +45 -0
- rpickle-0.1.0/tests/test_errors.py +28 -0
- rpickle-0.1.0/tests/test_extensions.py +45 -0
rpickle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rainwalker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rpickle-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rPickle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A safe and efficient Python serialization library
|
|
5
|
+
Author-email: Rainwalker <lmx1w3r@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: serialization,pickle,binary
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-benchmark; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# rPickle
|
|
21
|
+
|
|
22
|
+
A safe and efficient Python serialization library.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- 🔒 **Safe** - No arbitrary code execution, unlike `pickle`
|
|
27
|
+
- ⚡ **Fast** - Optimized binary format
|
|
28
|
+
- 📦 **Compact** - Small serialized size
|
|
29
|
+
- 🔄 **Circular references** - Handles self-referential structures
|
|
30
|
+
- 🎨 **Extensible** - Custom type support via extensions
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.15+ (preview)
|
|
35
|
+
- Python 3.10+ (full)
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install rPickle
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
```python
|
|
45
|
+
import rPickle
|
|
46
|
+
|
|
47
|
+
# Serialize
|
|
48
|
+
data = {'name': 'Alice', 'scores': [95, 87, 92]}
|
|
49
|
+
packed = rPickle.dumps(data)
|
|
50
|
+
|
|
51
|
+
# Deserialize
|
|
52
|
+
restored = rPickle.loads(packed)
|
|
53
|
+
print(restored) # {'name': 'Alice', 'scores': [95, 87, 92]}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### Core Function
|
|
59
|
+
|
|
60
|
+
| Function | Description |
|
|
61
|
+
| --------------- | ------------------------- |
|
|
62
|
+
| dumps(obj) | Serialize object to bytes |
|
|
63
|
+
| loads(data) | Deserialize from bytes |
|
|
64
|
+
| dump(obj, file) | Serialize to file |
|
|
65
|
+
| load(file) | Deserialize from file |
|
|
66
|
+
|
|
67
|
+
## Extensions
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from datetime import datetime
|
|
71
|
+
|
|
72
|
+
data = {'created': datetime.now()}
|
|
73
|
+
packed = rPickle.dumps(data, extensions=rPickle.datetime_ex)
|
|
74
|
+
restored = rPickle.loads(packed, extensions=rPickle.datetime_ex)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Custom Extensions
|
|
78
|
+
|
|
79
|
+
Add support for your own types using the `extensions` parameter.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from datetime import datetime
|
|
83
|
+
|
|
84
|
+
# 1. Define dump function (type → bytes)
|
|
85
|
+
def dump_datetime(dt: datetime) -> bytes:
|
|
86
|
+
return dt.timestamp().to_bytes(8, 'little')
|
|
87
|
+
|
|
88
|
+
# 2. Define load function (bytes → type)
|
|
89
|
+
def load_datetime(data: bytes) -> datetime:
|
|
90
|
+
timestamp = int.from_bytes(data, 'little')
|
|
91
|
+
return datetime.fromtimestamp(timestamp)
|
|
92
|
+
|
|
93
|
+
# 3. Register your extension
|
|
94
|
+
my_extensions = {
|
|
95
|
+
datetime: (load_datetime, dump_datetime) # (load, dump)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# 4. Use it
|
|
99
|
+
data = {'created': datetime.now()}
|
|
100
|
+
packed = rPickle.dumps(data, extensions=my_extensions)
|
|
101
|
+
restored = rPickle.loads(packed, extensions=my_extensions)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Built-in Extensions
|
|
105
|
+
|
|
106
|
+
rPickle includes some ready-to-use extensions
|
|
107
|
+
|
|
108
|
+
## Supported Types
|
|
109
|
+
- `None`, `bool`, `int`, `float`, `complex`, `str`
|
|
110
|
+
- `bytes`, `bytearray`
|
|
111
|
+
- `list`, `tuple`, `set`, `frozenset`
|
|
112
|
+
- `dict`, `frozendict` (Python 3.15+)
|
|
113
|
+
- `range`, `slice`
|
|
114
|
+
- `Ellipsis`, `NotImplemented`
|
|
115
|
+
- Circular references
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
rpickle-0.1.0/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# rPickle
|
|
2
|
+
|
|
3
|
+
A safe and efficient Python serialization library.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **Safe** - No arbitrary code execution, unlike `pickle`
|
|
8
|
+
- ⚡ **Fast** - Optimized binary format
|
|
9
|
+
- 📦 **Compact** - Small serialized size
|
|
10
|
+
- 🔄 **Circular references** - Handles self-referential structures
|
|
11
|
+
- 🎨 **Extensible** - Custom type support via extensions
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Python 3.15+ (preview)
|
|
16
|
+
- Python 3.10+ (full)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install rPickle
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
```python
|
|
26
|
+
import rPickle
|
|
27
|
+
|
|
28
|
+
# Serialize
|
|
29
|
+
data = {'name': 'Alice', 'scores': [95, 87, 92]}
|
|
30
|
+
packed = rPickle.dumps(data)
|
|
31
|
+
|
|
32
|
+
# Deserialize
|
|
33
|
+
restored = rPickle.loads(packed)
|
|
34
|
+
print(restored) # {'name': 'Alice', 'scores': [95, 87, 92]}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### Core Function
|
|
40
|
+
|
|
41
|
+
| Function | Description |
|
|
42
|
+
| --------------- | ------------------------- |
|
|
43
|
+
| dumps(obj) | Serialize object to bytes |
|
|
44
|
+
| loads(data) | Deserialize from bytes |
|
|
45
|
+
| dump(obj, file) | Serialize to file |
|
|
46
|
+
| load(file) | Deserialize from file |
|
|
47
|
+
|
|
48
|
+
## Extensions
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from datetime import datetime
|
|
52
|
+
|
|
53
|
+
data = {'created': datetime.now()}
|
|
54
|
+
packed = rPickle.dumps(data, extensions=rPickle.datetime_ex)
|
|
55
|
+
restored = rPickle.loads(packed, extensions=rPickle.datetime_ex)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Custom Extensions
|
|
59
|
+
|
|
60
|
+
Add support for your own types using the `extensions` parameter.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from datetime import datetime
|
|
64
|
+
|
|
65
|
+
# 1. Define dump function (type → bytes)
|
|
66
|
+
def dump_datetime(dt: datetime) -> bytes:
|
|
67
|
+
return dt.timestamp().to_bytes(8, 'little')
|
|
68
|
+
|
|
69
|
+
# 2. Define load function (bytes → type)
|
|
70
|
+
def load_datetime(data: bytes) -> datetime:
|
|
71
|
+
timestamp = int.from_bytes(data, 'little')
|
|
72
|
+
return datetime.fromtimestamp(timestamp)
|
|
73
|
+
|
|
74
|
+
# 3. Register your extension
|
|
75
|
+
my_extensions = {
|
|
76
|
+
datetime: (load_datetime, dump_datetime) # (load, dump)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# 4. Use it
|
|
80
|
+
data = {'created': datetime.now()}
|
|
81
|
+
packed = rPickle.dumps(data, extensions=my_extensions)
|
|
82
|
+
restored = rPickle.loads(packed, extensions=my_extensions)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Built-in Extensions
|
|
86
|
+
|
|
87
|
+
rPickle includes some ready-to-use extensions
|
|
88
|
+
|
|
89
|
+
## Supported Types
|
|
90
|
+
- `None`, `bool`, `int`, `float`, `complex`, `str`
|
|
91
|
+
- `bytes`, `bytearray`
|
|
92
|
+
- `list`, `tuple`, `set`, `frozenset`
|
|
93
|
+
- `dict`, `frozendict` (Python 3.15+)
|
|
94
|
+
- `range`, `slice`
|
|
95
|
+
- `Ellipsis`, `NotImplemented`
|
|
96
|
+
- Circular references
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['setuptools>=61.0', 'wheel']
|
|
3
|
+
build-backend = 'setuptools.build_meta'
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = 'rPickle'
|
|
7
|
+
dynamic = ['version']
|
|
8
|
+
description = 'A safe and efficient Python serialization library'
|
|
9
|
+
readme = 'README.md'
|
|
10
|
+
license = {text = 'MIT'}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = 'Rainwalker', email = 'lmx1w3r@outlook.com'},
|
|
13
|
+
]
|
|
14
|
+
keywords = ['serialization', 'pickle', 'binary']
|
|
15
|
+
classifiers = [
|
|
16
|
+
'Development Status :: 4 - Beta',
|
|
17
|
+
'Intended Audience :: Developers',
|
|
18
|
+
'License :: OSI Approved :: MIT License',
|
|
19
|
+
'Programming Language :: Python',
|
|
20
|
+
'Topic :: Software Development :: Libraries :: Python Modules',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.dynamic]
|
|
24
|
+
version = {attr = "rPickle.__version__"}
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ['pytest>=7.0', 'pytest-benchmark']
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ['src']
|
|
31
|
+
exclude = ['tests*']
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
rPickle = ['py.typed']
|
rpickle-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'''
|
|
2
|
+
rPickle - A safe and efficient Python serialization library.
|
|
3
|
+
|
|
4
|
+
Binary format with:
|
|
5
|
+
- Circular reference support
|
|
6
|
+
- Extensible type system
|
|
7
|
+
'''
|
|
8
|
+
|
|
9
|
+
from typing import BinaryIO as _BinaryIO, Any as _Any, Callable as _Callable
|
|
10
|
+
import struct as _struct
|
|
11
|
+
import builtins as _builtins
|
|
12
|
+
from io import BytesIO as _BytesIO
|
|
13
|
+
from sys import version_info as _ver_info
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
_version = 1
|
|
17
|
+
_python_version = (3, 15)
|
|
18
|
+
|
|
19
|
+
__version__ = '0.1.0'
|
|
20
|
+
|
|
21
|
+
_MAGIC = b'RPkl'
|
|
22
|
+
|
|
23
|
+
class VersionError(Exception):
|
|
24
|
+
def __init__(self, text: str = f"Cannot load because Python or rPickle is not the newest version. Need Python {'.'.join(map(str, _python_version))}+ or rPickle {__version__}+"):
|
|
25
|
+
self.text = text
|
|
26
|
+
super().__init__(text)
|
|
27
|
+
|
|
28
|
+
if _ver_info < (3, 15):
|
|
29
|
+
class frozendict:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
raise VersionError()
|
|
32
|
+
|
|
33
|
+
class _LOAD:
|
|
34
|
+
__slots__ = ('buf', 'objs_id', 'extensions')
|
|
35
|
+
|
|
36
|
+
struct_compile = {
|
|
37
|
+
'd' : _struct.Struct('<d'),
|
|
38
|
+
'2d': _struct.Struct('<2d'),
|
|
39
|
+
'bB': _struct.Struct('<bB'),
|
|
40
|
+
'h' : _struct.Struct('<h'),
|
|
41
|
+
'b' : _struct.Struct('<b'),
|
|
42
|
+
'i' : _struct.Struct('<i'),
|
|
43
|
+
'f' : _struct.Struct('<f'),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def __init__(self, f: _BinaryIO, extensions: dict[type, _Callable[[bytes], _Any]] | None = None):
|
|
47
|
+
self.buf: _BytesIO = f
|
|
48
|
+
magic = f.read(4)
|
|
49
|
+
if magic != _MAGIC: raise ValueError("invalid data")
|
|
50
|
+
version = int.from_bytes(f.read(2), 'little')
|
|
51
|
+
if version != _version: warnings.warn('may not be compatible', RuntimeWarning)
|
|
52
|
+
py_ver_maj, py_ver_min = f.read(2)
|
|
53
|
+
py_ver = py_ver_maj, py_ver_min
|
|
54
|
+
if py_ver > _python_version: warnings.warn('may not be compatible', RuntimeWarning)
|
|
55
|
+
self.objs_id = []
|
|
56
|
+
self.extensions = {} if extensions is None else {k.__module__ + k.__qualname__: v for k, v in extensions.items()}
|
|
57
|
+
|
|
58
|
+
def VALUE(self) -> _Any:
|
|
59
|
+
now = self.buf.read(1)[0]
|
|
60
|
+
match now:
|
|
61
|
+
case 0x0 | 0x1: return bool(now)
|
|
62
|
+
case 0x2 : return None
|
|
63
|
+
case 0x3:
|
|
64
|
+
value, = self.struct_compile['d'].unpack(self.buf.read(8))
|
|
65
|
+
self.objs_id.append(value)
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
case 0x4:
|
|
69
|
+
idx = len(self.objs_id)
|
|
70
|
+
self.objs_id.append(None)
|
|
71
|
+
flag = self.buf.read(1)[0]
|
|
72
|
+
has_real, has_imag = flag >> 4, flag & 1
|
|
73
|
+
value = complex(self.VALUE() if has_real else 0, self.VALUE() if has_imag else 0)
|
|
74
|
+
self.objs_id[idx] = value
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
case 0x5:
|
|
78
|
+
signed, length_of_length = self.struct_compile['bB'].unpack(self.buf.read(2))
|
|
79
|
+
length = int.from_bytes(self.buf.read(length_of_length), 'little')
|
|
80
|
+
value = signed * int.from_bytes(self.buf.read(length), 'little')
|
|
81
|
+
self.objs_id.append(value)
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
case 0x6:
|
|
85
|
+
length_of_length = self.buf.read(1)[0]
|
|
86
|
+
length = int.from_bytes(self.buf.read(length_of_length), 'little')
|
|
87
|
+
value = self.buf.read(length).decode()
|
|
88
|
+
self.objs_id.append(value)
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
case 0x7:
|
|
92
|
+
idx = len(self.objs_id)
|
|
93
|
+
self.objs_id.append(None)
|
|
94
|
+
value = (*self.LIST([]),) # Using unpacking instead of tuple creator because it's faster.
|
|
95
|
+
self.objs_id[idx] = value
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
case 0x8:
|
|
99
|
+
value = []
|
|
100
|
+
self.objs_id.append(value)
|
|
101
|
+
value = self.LIST(value)
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
case 0x9:
|
|
105
|
+
value = set()
|
|
106
|
+
self.objs_id.append(value)
|
|
107
|
+
value = self.SET(value)
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
case 0xA:
|
|
111
|
+
value = {}
|
|
112
|
+
self.objs_id.append(value)
|
|
113
|
+
value = self.DICT(value)
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
case 0xB | 0xC as code:
|
|
117
|
+
length_of_length = self.buf.read(1)[0]
|
|
118
|
+
length = int.from_bytes(self.buf.read(length_of_length), 'little')
|
|
119
|
+
if code == 0xB:
|
|
120
|
+
value = self.buf.read(length)
|
|
121
|
+
else:
|
|
122
|
+
value = bytearray(length)
|
|
123
|
+
self.buf.readinto(value)
|
|
124
|
+
self.objs_id.append(value)
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
case 0xD:
|
|
128
|
+
value, = self.struct_compile['h'].unpack(self.buf.read(2))
|
|
129
|
+
return value
|
|
130
|
+
|
|
131
|
+
case 0xE:
|
|
132
|
+
value = (self.buf.read(1)[0] ^ 0x80) - 0x80
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
case 0xF:
|
|
136
|
+
value, = self.struct_compile['i'].unpack(self.buf.read(4))
|
|
137
|
+
self.objs_id.append(value)
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
case 0x10:
|
|
141
|
+
idx = len(self.objs_id)
|
|
142
|
+
self.objs_id.append(None)
|
|
143
|
+
value = frozenset(self.SET(set()))
|
|
144
|
+
self.objs_id[idx] = value
|
|
145
|
+
return value
|
|
146
|
+
|
|
147
|
+
case 0x11:
|
|
148
|
+
length = self.buf.read(1)[0]
|
|
149
|
+
value = self.buf.read(length).decode()
|
|
150
|
+
self.objs_id.append(value)
|
|
151
|
+
return value
|
|
152
|
+
|
|
153
|
+
case 0x12 | 0x13 as code:
|
|
154
|
+
idx = len(self.objs_id)
|
|
155
|
+
self.objs_id.append(None)
|
|
156
|
+
value = (range if code == 0x12 else slice)(self.VALUE(), self.VALUE(), self.VALUE())
|
|
157
|
+
self.objs_id[idx] = value
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
case 0x14: return ...
|
|
161
|
+
case 0x15: return _builtins.NotImplemented
|
|
162
|
+
case 0x16:
|
|
163
|
+
idx = len(self.objs_id)
|
|
164
|
+
self.objs_id.append(None)
|
|
165
|
+
value = frozendict(self.DICT(value))
|
|
166
|
+
self.objs_id[idx] = value
|
|
167
|
+
return value
|
|
168
|
+
|
|
169
|
+
case 0xFE:
|
|
170
|
+
length = self.buf.read(1)[0]
|
|
171
|
+
value = int.from_bytes(self.buf.read(length), 'little')
|
|
172
|
+
return self.objs_id[value]
|
|
173
|
+
|
|
174
|
+
case 0xFF:
|
|
175
|
+
length = self.buf.read(1)[0]
|
|
176
|
+
name = self.buf.read(length).decode()
|
|
177
|
+
if name in self.extensions:
|
|
178
|
+
length_of_length = self.buf.read(1)[0]
|
|
179
|
+
length = int.from_bytes(self.buf.read(length_of_length), 'little')
|
|
180
|
+
content = self.buf.read(length)
|
|
181
|
+
value = self.extensions[name][0](content)
|
|
182
|
+
self.objs_id.append(value)
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
def LIST(self, value: list) -> list:
|
|
186
|
+
length_of_length = self.buf.read(1)[0]
|
|
187
|
+
length = int.from_bytes(self.buf.read(length_of_length), 'little')
|
|
188
|
+
value += [None] * length
|
|
189
|
+
for item in range(length): value[item] = self.VALUE()
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
def SET(self, value: set) -> set:
|
|
193
|
+
length_of_length = self.buf.read(1)[0]
|
|
194
|
+
length = int.from_bytes(self.buf.read(length_of_length), 'little')
|
|
195
|
+
value_add = value.add
|
|
196
|
+
for _ in range(length): value_add(self.VALUE())
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
def DICT(self, value: dict) -> dict:
|
|
200
|
+
length_of_length = self.buf.read(1)[0]
|
|
201
|
+
length = int.from_bytes(self.buf.read(length_of_length), 'little')
|
|
202
|
+
for _ in range(length): value[self.VALUE()] = self.VALUE()
|
|
203
|
+
# WARNING: right VALUE() will execute first, then left VALUE()
|
|
204
|
+
# We know: Python evaluates expressions from left to right.
|
|
205
|
+
# Notice that while evaluating an assignment, the right-hand side is evaluated before the left-hand side.
|
|
206
|
+
return value
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def load(f: _BinaryIO, extensions: dict[type, _Callable[[bytes], _Any]] | None = None) -> _Any:
|
|
210
|
+
'''load data from file'''
|
|
211
|
+
return _LOAD(f, extensions).VALUE()
|
|
212
|
+
|
|
213
|
+
def loads(source: bytes | bytearray, extensions: dict[type, _Callable[[bytes], _Any]] | None = None) -> _Any:
|
|
214
|
+
'''load data from byte-like'''
|
|
215
|
+
return load(_BytesIO(source), extensions)
|
|
216
|
+
|
|
217
|
+
def dump(obj: _Any, f: _BinaryIO, extensions: dict[type, _Callable[[_Any], bytes]] | None = None):
|
|
218
|
+
'''dump data to file'''
|
|
219
|
+
f.write(_MAGIC)
|
|
220
|
+
f.write(_version.to_bytes(2, 'little'))
|
|
221
|
+
f.write(_ver_info.major.to_bytes())
|
|
222
|
+
f.write(_ver_info.minor.to_bytes())
|
|
223
|
+
if extensions is None: extensions = {}
|
|
224
|
+
stack = [obj]
|
|
225
|
+
obm = {
|
|
226
|
+
tuple : b'\x07',
|
|
227
|
+
list : b'\x08',
|
|
228
|
+
set : b'\x09',
|
|
229
|
+
frozenset : b'\x10',
|
|
230
|
+
|
|
231
|
+
dict : b'\x0A',
|
|
232
|
+
frozendict: b'\x16',
|
|
233
|
+
|
|
234
|
+
bytes : b'\x0B',
|
|
235
|
+
bytearray : b'\x0C',
|
|
236
|
+
|
|
237
|
+
range : b'\x12',
|
|
238
|
+
slice : b'\x13',
|
|
239
|
+
}
|
|
240
|
+
struct_compile = {
|
|
241
|
+
'cd' : _struct.Struct('<cd'),
|
|
242
|
+
'cbB': _struct.Struct('<cbB'),
|
|
243
|
+
'ch' : _struct.Struct('<ch'),
|
|
244
|
+
'cb' : _struct.Struct('<cb'),
|
|
245
|
+
'ci' : _struct.Struct('<ci'),
|
|
246
|
+
'2c' : _struct.Struct('<2c'),
|
|
247
|
+
'cB' : _struct.Struct('<cB'),
|
|
248
|
+
}
|
|
249
|
+
objs_id: dict[int, int] = {}
|
|
250
|
+
id_count = 0
|
|
251
|
+
while stack:
|
|
252
|
+
obj = stack.pop()
|
|
253
|
+
obj_id = id(obj)
|
|
254
|
+
if obj_id in objs_id:
|
|
255
|
+
f.write(b'\xFE')
|
|
256
|
+
content = objs_id[obj_id].to_bytes(8, 'little').rstrip(b'\x00')
|
|
257
|
+
f.write(len(content).to_bytes())
|
|
258
|
+
f.write(content)
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
if not (
|
|
262
|
+
isinstance(obj, bool)
|
|
263
|
+
or obj is None
|
|
264
|
+
or obj is ...
|
|
265
|
+
or obj is _builtins.NotImplemented
|
|
266
|
+
or (isinstance(obj, int) and -32768 <= obj < 32768)
|
|
267
|
+
):
|
|
268
|
+
objs_id[obj_id] = id_count
|
|
269
|
+
id_count += 1
|
|
270
|
+
|
|
271
|
+
match obj:
|
|
272
|
+
case bool() : f.write(bytes((obj,)))
|
|
273
|
+
case None : f.write(b'\x02')
|
|
274
|
+
case float():
|
|
275
|
+
f.write(struct_compile['cd'].pack(b'\x03', obj))
|
|
276
|
+
|
|
277
|
+
case complex():
|
|
278
|
+
f.write(b'\x04')
|
|
279
|
+
has_real, has_imag = bool(obj.real), bool(obj.imag)
|
|
280
|
+
f.write(bytes((has_real << 4 | has_imag,)))
|
|
281
|
+
stack += (obj.imag, obj.real)[not has_imag : has_real + 1]
|
|
282
|
+
|
|
283
|
+
case int():
|
|
284
|
+
if -128 <= obj < 128: # very short
|
|
285
|
+
f.write(struct_compile['cb'].pack(b'\x0E', obj))
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
elif -32768 <= obj < 32768: # short
|
|
289
|
+
f.write(struct_compile['ch'].pack(b'\x0D', obj))
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
elif -2147483648 <= obj < 2147483648 : # int
|
|
293
|
+
f.write(struct_compile['ci'].pack(b'\x0F', obj))
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
num = abs(obj)
|
|
297
|
+
digits = num.to_bytes((num.bit_length() + 7) >> 3, 'little')
|
|
298
|
+
f.write(struct_compile['2c'].pack(b'\x05', b'\xFF' if obj < 0 else b'\x01')) # signed
|
|
299
|
+
length_of_length = len(digits).to_bytes(8, 'little').rstrip(b'\x00') # length
|
|
300
|
+
f.write(len(length_of_length).to_bytes()) # length of length
|
|
301
|
+
f.write(length_of_length)
|
|
302
|
+
f.write(digits)
|
|
303
|
+
|
|
304
|
+
case str():
|
|
305
|
+
content = obj.encode()
|
|
306
|
+
if len(content) < 256:
|
|
307
|
+
f.write(struct_compile['cB'].pack(b'\x11', len(content)))
|
|
308
|
+
f.write(content)
|
|
309
|
+
else:
|
|
310
|
+
f.write(b'\x06')
|
|
311
|
+
length = len(content).to_bytes(8, 'little').rstrip(b'\x00')
|
|
312
|
+
f.write(len(length).to_bytes())
|
|
313
|
+
f.write(length)
|
|
314
|
+
f.write(content)
|
|
315
|
+
|
|
316
|
+
case tuple() | list() | set() | frozenset():
|
|
317
|
+
f.write(obm[type(obj)])
|
|
318
|
+
length = len(obj).to_bytes(8, 'little').rstrip(b'\x00')
|
|
319
|
+
f.write(len(length).to_bytes())
|
|
320
|
+
f.write(length)
|
|
321
|
+
stack += [None] * len(obj)
|
|
322
|
+
stack[-1:-1-len(obj):-1] = obj
|
|
323
|
+
|
|
324
|
+
case dict() | frozendict():
|
|
325
|
+
f.write(obm[type(obj)])
|
|
326
|
+
length = len(obj).to_bytes(8, 'little').rstrip(b'\x00')
|
|
327
|
+
f.write(len(length).to_bytes())
|
|
328
|
+
f.write(length)
|
|
329
|
+
stack += [None] * (len(obj) << 1)
|
|
330
|
+
stack[-1:-1-(len(obj)<<1):-1] = (item for key, value in obj.items() for item in (value, key))
|
|
331
|
+
# To maintain order, this genexpr must reverse (key, value) to (value, key)
|
|
332
|
+
# stack will become [..., k3, v3, k2, v2, k1, v1]
|
|
333
|
+
# When stack pop, it'll pop v1 first, then pop k1, and so on
|
|
334
|
+
|
|
335
|
+
case bytes() | bytearray():
|
|
336
|
+
f.write(obm[type(obj)])
|
|
337
|
+
length = len(obj).to_bytes(8, 'little').rstrip(b'\x00')
|
|
338
|
+
f.write(len(length).to_bytes())
|
|
339
|
+
f.write(length)
|
|
340
|
+
f.write(obj)
|
|
341
|
+
|
|
342
|
+
case range() | slice():
|
|
343
|
+
f.write(obm[type(obj)])
|
|
344
|
+
stack += (obj.step, obj.stop, obj.start)
|
|
345
|
+
|
|
346
|
+
case _ if obj is ...: f.write(b'\x14')
|
|
347
|
+
case _ if obj is _builtins.NotImplemented: f.write(b'\x15')
|
|
348
|
+
case _:
|
|
349
|
+
if type(obj) in extensions:
|
|
350
|
+
f.write(b'\xFF')
|
|
351
|
+
name = (type(obj).__module__ + type(obj).__qualname__).encode()
|
|
352
|
+
f.write(len(name).to_bytes())
|
|
353
|
+
f.write(name)
|
|
354
|
+
value = extensions[type(obj)][1](obj)
|
|
355
|
+
length = len(value).to_bytes(8, 'little').rstrip(b'\x00')
|
|
356
|
+
f.write(len(length).to_bytes())
|
|
357
|
+
f.write(length)
|
|
358
|
+
f.write(value)
|
|
359
|
+
else:
|
|
360
|
+
raise TypeError(f"Unsupported type: {type(obj).__name__}")
|
|
361
|
+
|
|
362
|
+
def dumps(obj: _Any, extensions: dict[type, _Callable[[_Any], bytes]] | None = None) -> bytes:
|
|
363
|
+
'''dump data to byte-like'''
|
|
364
|
+
buf = _BytesIO()
|
|
365
|
+
dump(obj, buf, extensions)
|
|
366
|
+
return buf.getvalue()
|
|
367
|
+
|
|
368
|
+
__all__ = (
|
|
369
|
+
'loads', 'dumps', 'load', 'dump', 'VersionError',
|
|
370
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"rPickle's built-in extensions"
|
|
2
|
+
|
|
3
|
+
import struct as _struct
|
|
4
|
+
from .core import dumps, loads
|
|
5
|
+
|
|
6
|
+
# datetime
|
|
7
|
+
from datetime import datetime as _datetime
|
|
8
|
+
|
|
9
|
+
def _datetime_d(value: _datetime) -> bytes: return _struct.pack('<d', value.timestamp())
|
|
10
|
+
def _datetime_l(code: bytes) -> _datetime: return _datetime.fromtimestamp(_struct.unpack('<d', code)[0])
|
|
11
|
+
|
|
12
|
+
datetime_ext = {_datetime: (_datetime_l, _datetime_d)}
|
|
13
|
+
|
|
14
|
+
# Decimal
|
|
15
|
+
from decimal import Decimal as _Decimal
|
|
16
|
+
|
|
17
|
+
def _Decimal_d(value: _Decimal) -> bytes: return str(value).encode('ascii')
|
|
18
|
+
def _Decimal_l(code: bytes) -> _Decimal: return _Decimal(code.decode('ascii'))
|
|
19
|
+
|
|
20
|
+
Decimal_ext = {_Decimal: (_Decimal_l, _Decimal_d)}
|
|
21
|
+
|
|
22
|
+
# UUID
|
|
23
|
+
from uuid import UUID as _UUID
|
|
24
|
+
|
|
25
|
+
def _UUID_d(value: _UUID) -> bytes: return value.bytes
|
|
26
|
+
def _UUID_l(code: bytes) -> _UUID: return _UUID(bytes=code)
|
|
27
|
+
|
|
28
|
+
UUID_ext = {_UUID: (_UUID_l, _UUID_d)}
|
|
29
|
+
|
|
30
|
+
#Fraction
|
|
31
|
+
from fractions import Fraction as _Fraction
|
|
32
|
+
|
|
33
|
+
def _Fraction_d(value: _Fraction) -> bytes: return dumps((value.numerator, value.denominator))
|
|
34
|
+
def _Fraction_l(code: bytes) -> _Fraction: return _Fraction(*loads(code))
|
|
35
|
+
|
|
36
|
+
Fraction_ext = {_Fraction: (_Fraction_l, _Fraction_d)}
|
|
37
|
+
|
|
38
|
+
# others
|
|
39
|
+
__all__ = (
|
|
40
|
+
'datetime_ext',
|
|
41
|
+
'Decimal_ext',
|
|
42
|
+
'UUID_ext',
|
|
43
|
+
'Fraction_ext',
|
|
44
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rPickle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A safe and efficient Python serialization library
|
|
5
|
+
Author-email: Rainwalker <lmx1w3r@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: serialization,pickle,binary
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-benchmark; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# rPickle
|
|
21
|
+
|
|
22
|
+
A safe and efficient Python serialization library.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- 🔒 **Safe** - No arbitrary code execution, unlike `pickle`
|
|
27
|
+
- ⚡ **Fast** - Optimized binary format
|
|
28
|
+
- 📦 **Compact** - Small serialized size
|
|
29
|
+
- 🔄 **Circular references** - Handles self-referential structures
|
|
30
|
+
- 🎨 **Extensible** - Custom type support via extensions
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.15+ (preview)
|
|
35
|
+
- Python 3.10+ (full)
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install rPickle
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
```python
|
|
45
|
+
import rPickle
|
|
46
|
+
|
|
47
|
+
# Serialize
|
|
48
|
+
data = {'name': 'Alice', 'scores': [95, 87, 92]}
|
|
49
|
+
packed = rPickle.dumps(data)
|
|
50
|
+
|
|
51
|
+
# Deserialize
|
|
52
|
+
restored = rPickle.loads(packed)
|
|
53
|
+
print(restored) # {'name': 'Alice', 'scores': [95, 87, 92]}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### Core Function
|
|
59
|
+
|
|
60
|
+
| Function | Description |
|
|
61
|
+
| --------------- | ------------------------- |
|
|
62
|
+
| dumps(obj) | Serialize object to bytes |
|
|
63
|
+
| loads(data) | Deserialize from bytes |
|
|
64
|
+
| dump(obj, file) | Serialize to file |
|
|
65
|
+
| load(file) | Deserialize from file |
|
|
66
|
+
|
|
67
|
+
## Extensions
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from datetime import datetime
|
|
71
|
+
|
|
72
|
+
data = {'created': datetime.now()}
|
|
73
|
+
packed = rPickle.dumps(data, extensions=rPickle.datetime_ex)
|
|
74
|
+
restored = rPickle.loads(packed, extensions=rPickle.datetime_ex)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Custom Extensions
|
|
78
|
+
|
|
79
|
+
Add support for your own types using the `extensions` parameter.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from datetime import datetime
|
|
83
|
+
|
|
84
|
+
# 1. Define dump function (type → bytes)
|
|
85
|
+
def dump_datetime(dt: datetime) -> bytes:
|
|
86
|
+
return dt.timestamp().to_bytes(8, 'little')
|
|
87
|
+
|
|
88
|
+
# 2. Define load function (bytes → type)
|
|
89
|
+
def load_datetime(data: bytes) -> datetime:
|
|
90
|
+
timestamp = int.from_bytes(data, 'little')
|
|
91
|
+
return datetime.fromtimestamp(timestamp)
|
|
92
|
+
|
|
93
|
+
# 3. Register your extension
|
|
94
|
+
my_extensions = {
|
|
95
|
+
datetime: (load_datetime, dump_datetime) # (load, dump)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# 4. Use it
|
|
99
|
+
data = {'created': datetime.now()}
|
|
100
|
+
packed = rPickle.dumps(data, extensions=my_extensions)
|
|
101
|
+
restored = rPickle.loads(packed, extensions=my_extensions)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Built-in Extensions
|
|
105
|
+
|
|
106
|
+
rPickle includes some ready-to-use extensions
|
|
107
|
+
|
|
108
|
+
## Supported Types
|
|
109
|
+
- `None`, `bool`, `int`, `float`, `complex`, `str`
|
|
110
|
+
- `bytes`, `bytearray`
|
|
111
|
+
- `list`, `tuple`, `set`, `frozenset`
|
|
112
|
+
- `dict`, `frozendict` (Python 3.15+)
|
|
113
|
+
- `range`, `slice`
|
|
114
|
+
- `Ellipsis`, `NotImplemented`
|
|
115
|
+
- Circular references
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/rPickle/__init__.py
|
|
5
|
+
src/rPickle/core.py
|
|
6
|
+
src/rPickle/ext.py
|
|
7
|
+
src/rPickle/py.typed
|
|
8
|
+
src/rPickle.egg-info/PKG-INFO
|
|
9
|
+
src/rPickle.egg-info/SOURCES.txt
|
|
10
|
+
src/rPickle.egg-info/dependency_links.txt
|
|
11
|
+
src/rPickle.egg-info/requires.txt
|
|
12
|
+
src/rPickle.egg-info/top_level.txt
|
|
13
|
+
tests/test_basic.py
|
|
14
|
+
tests/test_containers.py
|
|
15
|
+
tests/test_errors.py
|
|
16
|
+
tests/test_extensions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rPickle
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import rPickle
|
|
2
|
+
|
|
3
|
+
class TestBasicTypes:
|
|
4
|
+
def test_none(self):
|
|
5
|
+
assert rPickle.loads(rPickle.dumps(None)) is None
|
|
6
|
+
|
|
7
|
+
def test_bool(self):
|
|
8
|
+
assert rPickle.loads(rPickle.dumps(True)) is True
|
|
9
|
+
assert rPickle.loads(rPickle.dumps(False)) is False
|
|
10
|
+
|
|
11
|
+
def test_int(self):
|
|
12
|
+
for n in [-1, 0, 1, 127, 128, 32767, 32768, 2**31-1, 2**31]:
|
|
13
|
+
assert rPickle.loads(rPickle.dumps(n)) == n
|
|
14
|
+
|
|
15
|
+
def test_float(self):
|
|
16
|
+
for f in [0.0, 1.5, -3.14, float('inf'), float('-inf')]:
|
|
17
|
+
assert rPickle.loads(rPickle.dumps(f)) == f
|
|
18
|
+
|
|
19
|
+
def test_complex(self):
|
|
20
|
+
for c in [1+2j, 3.5-4j, complex(0, 1)]:
|
|
21
|
+
assert rPickle.loads(rPickle.dumps(c)) == c
|
|
22
|
+
|
|
23
|
+
def test_str(self):
|
|
24
|
+
s = "Hello 世界 \n\t\r \"' \\"
|
|
25
|
+
assert rPickle.loads(rPickle.dumps(s)) == s
|
|
26
|
+
|
|
27
|
+
def test_bytes(self):
|
|
28
|
+
b = b"\x00\x01\x02\xFF"
|
|
29
|
+
assert rPickle.loads(rPickle.dumps(b)) == b
|
|
30
|
+
|
|
31
|
+
def test_bytearray(self):
|
|
32
|
+
ba = bytearray(b"\x00\x01\x02\xFF")
|
|
33
|
+
result = rPickle.loads(rPickle.dumps(ba))
|
|
34
|
+
assert result == ba
|
|
35
|
+
assert isinstance(result, bytearray)
|
|
36
|
+
|
|
37
|
+
def test_ellipsis(self):
|
|
38
|
+
assert rPickle.loads(rPickle.dumps(...)) is ...
|
|
39
|
+
|
|
40
|
+
def test_notimplemented(self):
|
|
41
|
+
assert rPickle.loads(rPickle.dumps(NotImplemented)) is NotImplemented
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import rPickle
|
|
2
|
+
|
|
3
|
+
class TestContainers:
|
|
4
|
+
def test_list(self):
|
|
5
|
+
data = [1, 2, 3, "a", None]
|
|
6
|
+
assert rPickle.loads(rPickle.dumps(data)) == data
|
|
7
|
+
|
|
8
|
+
def test_tuple(self):
|
|
9
|
+
data = (1, 2, 3, "a", None)
|
|
10
|
+
assert rPickle.loads(rPickle.dumps(data)) == data
|
|
11
|
+
|
|
12
|
+
def test_dict(self):
|
|
13
|
+
data = {"a": 1, "b": [2, 3], "c": None}
|
|
14
|
+
assert rPickle.loads(rPickle.dumps(data)) == data
|
|
15
|
+
|
|
16
|
+
def test_set(self):
|
|
17
|
+
data = {1, 2, 3}
|
|
18
|
+
assert rPickle.loads(rPickle.dumps(data)) == data
|
|
19
|
+
|
|
20
|
+
def test_frozenset(self):
|
|
21
|
+
data = frozenset([1, 2, 3])
|
|
22
|
+
assert rPickle.loads(rPickle.dumps(data)) == data
|
|
23
|
+
|
|
24
|
+
def test_nested(self):
|
|
25
|
+
data = [{"a": (1, 2)}, {b"key": {3, 4}}]
|
|
26
|
+
assert rPickle.loads(rPickle.dumps(data)) == data
|
|
27
|
+
|
|
28
|
+
class TestCircularReferences:
|
|
29
|
+
def test_self_list(self):
|
|
30
|
+
a = []
|
|
31
|
+
a.append(a)
|
|
32
|
+
b = rPickle.loads(rPickle.dumps(a))
|
|
33
|
+
assert b[0] is b
|
|
34
|
+
|
|
35
|
+
def test_self_dict(self):
|
|
36
|
+
d = {}
|
|
37
|
+
d["self"] = d
|
|
38
|
+
e = rPickle.loads(rPickle.dumps(d))
|
|
39
|
+
assert e["self"] is e
|
|
40
|
+
|
|
41
|
+
def test_shared_reference(self):
|
|
42
|
+
inner = [1, 2]
|
|
43
|
+
outer = [inner, inner]
|
|
44
|
+
result = rPickle.loads(rPickle.dumps(outer))
|
|
45
|
+
assert result[0] is result[1]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import rPickle
|
|
3
|
+
|
|
4
|
+
class TestErrors:
|
|
5
|
+
def test_invalid_data(self):
|
|
6
|
+
with pytest.raises(ValueError, match="invalid data"):
|
|
7
|
+
rPickle.loads(b'not rpickle data')
|
|
8
|
+
|
|
9
|
+
def test_unsupported_type(self):
|
|
10
|
+
class Custom:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
with pytest.raises(TypeError, match="Unsupported type: Custom"):
|
|
14
|
+
rPickle.dumps(Custom())
|
|
15
|
+
|
|
16
|
+
def test_extension_without_load(self):
|
|
17
|
+
"""测试扩展格式错误时应该报错"""
|
|
18
|
+
class MyType:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
# 创建错误的扩展(dump 函数缺失)
|
|
22
|
+
bad_ext = {MyType: (None, None)} # load 和 dump 都是 None
|
|
23
|
+
|
|
24
|
+
obj = MyType()
|
|
25
|
+
|
|
26
|
+
# dumps 时应该抛出 TypeError
|
|
27
|
+
with pytest.raises(TypeError):
|
|
28
|
+
rPickle.dumps(obj, extensions=bad_ext)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import rPickle
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
|
|
7
|
+
class TestBuiltinExtensions:
|
|
8
|
+
def test_datetime(self):
|
|
9
|
+
dt = datetime(2026, 6, 12, 12, 0, 0)
|
|
10
|
+
packed = rPickle.dumps(dt, extensions=rPickle.ext.datetime_ext)
|
|
11
|
+
restored = rPickle.loads(packed, extensions=rPickle.ext.datetime_ext)
|
|
12
|
+
assert restored == dt
|
|
13
|
+
|
|
14
|
+
def test_decimal(self):
|
|
15
|
+
d = Decimal('3.14159')
|
|
16
|
+
packed = rPickle.dumps(d, extensions=rPickle.ext.Decimal_ext)
|
|
17
|
+
restored = rPickle.loads(packed, extensions=rPickle.ext.Decimal_ext)
|
|
18
|
+
assert restored == d
|
|
19
|
+
|
|
20
|
+
def test_uuid(self):
|
|
21
|
+
u = uuid4()
|
|
22
|
+
packed = rPickle.dumps(u, extensions=rPickle.ext.UUID_ext)
|
|
23
|
+
restored = rPickle.loads(packed, extensions=rPickle.ext.UUID_ext)
|
|
24
|
+
assert restored == u
|
|
25
|
+
|
|
26
|
+
def test_fraction(self):
|
|
27
|
+
f = Fraction(3, 4)
|
|
28
|
+
packed = rPickle.dumps(f, extensions=rPickle.ext.Fraction_ext)
|
|
29
|
+
restored = rPickle.loads(packed, extensions=rPickle.ext.Fraction_ext)
|
|
30
|
+
assert restored == f
|
|
31
|
+
|
|
32
|
+
def test_multiple_extensions(self):
|
|
33
|
+
data = {
|
|
34
|
+
'dt': datetime(2026, 6, 12, 12, 0, 0),
|
|
35
|
+
'dec': Decimal('3.14'),
|
|
36
|
+
'frac': Fraction(2, 3),
|
|
37
|
+
}
|
|
38
|
+
ext = (rPickle.ext.datetime_ext |
|
|
39
|
+
rPickle.ext.Decimal_ext |
|
|
40
|
+
rPickle.ext.Fraction_ext)
|
|
41
|
+
packed = rPickle.dumps(data, extensions=ext)
|
|
42
|
+
restored = rPickle.loads(packed, extensions=ext)
|
|
43
|
+
assert restored['dt'] == data['dt']
|
|
44
|
+
assert restored['dec'] == data['dec']
|
|
45
|
+
assert restored['frac'] == data['frac']
|