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 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
@@ -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']
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from .core import *
2
+ from .core import __version__
3
+ from . import ext
4
+
5
+ __all__ = (
6
+ *core.__all__,
7
+ 'ext',
8
+ )
9
+
10
+ del core
@@ -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,6 @@
1
+ rPickle's py.typed
2
+
3
+ Why is it called 'rPickle'?
4
+ Because it has a interesting meaning. The 'r' refers 'Rain' - yeah, which's in 'Rainwalker'.
5
+ And 'Pickle'? You know its meaning, so I don't descript again.
6
+ Happy pickling!
@@ -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,4 @@
1
+
2
+ [dev]
3
+ pytest>=7.0
4
+ pytest-benchmark
@@ -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']