socketwrapper 0.0.2__tar.gz → 0.0.3__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.
- {socketwrapper-0.0.2/socketwrapper.egg-info → socketwrapper-0.0.3}/PKG-INFO +20 -4
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/README.md +18 -3
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/pyproject.toml +2 -1
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper/__init__.py +26 -16
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper/_utils.py +77 -30
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper/framing.py +1 -2
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper/protocols.py +7 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3/socketwrapper.egg-info}/PKG-INFO +20 -4
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/LICENSE +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/setup.cfg +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper/_varint.py +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper/py.typed +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper.egg-info/SOURCES.txt +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper.egg-info/dependency_links.txt +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper.egg-info/requires.txt +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper.egg-info/top_level.txt +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.0.3}/socketwrapper.egg-info/zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socketwrapper
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: high level socket and pipe wrappers
|
|
5
5
|
Author: Felipe A Hernandez
|
|
6
6
|
Author-email: ergoithz@gmail.com
|
|
@@ -27,6 +27,7 @@ License: MIT License
|
|
|
27
27
|
SOFTWARE.
|
|
28
28
|
|
|
29
29
|
Project-URL: homepage, https://gitlab.com/ergoithz/socketwrapper
|
|
30
|
+
Project-URL: repository, https://gitlab.com/ergoithz/socketwrapper
|
|
30
31
|
Project-URL: issue-tracker, https://gitlab.com/ergoithz/socketwrapper/-/issues
|
|
31
32
|
Project-URL: release-notes, https://gitlab.com/ergoithz/socketwrapper/-/releases
|
|
32
33
|
Project-URL: issue-new, https://gitlab.com/ergoithz/socketwrapper/-/issues/new
|
|
@@ -89,12 +90,17 @@ uv pip install 'socketwrapper[msgpack]'
|
|
|
89
90
|
|
|
90
91
|
## Changelog
|
|
91
92
|
|
|
92
|
-
###
|
|
93
|
+
### 0.0.3 - 2025.11.08
|
|
94
|
+
|
|
95
|
+
- New: optional support for `recv_into` in socket-like objects.
|
|
96
|
+
- Optimization: recv operations will now use `sock.recv_into` (if available) to reduce memory allocation overhead.
|
|
97
|
+
|
|
98
|
+
### 0.0.2 - 2025.11.05
|
|
93
99
|
|
|
94
100
|
- Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
|
|
95
101
|
- New: optional msgpack support (`msgpack` extra).
|
|
96
102
|
- Fix: busy reads no longer blocking asyncio event loop.
|
|
97
|
-
- Typing: expose `SizedBuffer` and deprecate `SendPayload
|
|
103
|
+
- Typing: expose `SizedBuffer` and deprecate `SendPayload` (removal expected in `0.1.0`).
|
|
98
104
|
- Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
|
|
99
105
|
|
|
100
106
|
## Documentation
|
|
@@ -103,7 +109,9 @@ None other than this README, life's too short and I'm too busy with real life, i
|
|
|
103
109
|
|
|
104
110
|
### Puggable I/O: SocketLike protocol
|
|
105
111
|
|
|
106
|
-
The `
|
|
112
|
+
The `protocols.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
113
|
+
|
|
114
|
+
Additional methods defined in `protocols.OptionalSocketLike` can be optionally implemented enabling optimized code paths.
|
|
107
115
|
|
|
108
116
|
```py
|
|
109
117
|
@typing.runtime_checkable
|
|
@@ -115,6 +123,14 @@ class SocketLike(typing.Protocol):
|
|
|
115
123
|
def recv(self, bufsize: int, /) -> bytes: ...
|
|
116
124
|
def settimeout(self, timeout: float | None, /) -> None: ...
|
|
117
125
|
def close(self) -> None: ...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@typing.runtime_checkable
|
|
129
|
+
class OptionalSocketLike(SocketLike, typing.Protocol):
|
|
130
|
+
"""Protocol for optional socket-like object methods accepted by socketwrapper socket classes."""
|
|
131
|
+
|
|
132
|
+
def recv_into(self, buffer: collections.abc.Buffer, /) -> bytes: ...
|
|
133
|
+
|
|
118
134
|
```
|
|
119
135
|
|
|
120
136
|
Special attention to:
|
|
@@ -35,12 +35,17 @@ uv pip install 'socketwrapper[msgpack]'
|
|
|
35
35
|
|
|
36
36
|
## Changelog
|
|
37
37
|
|
|
38
|
-
###
|
|
38
|
+
### 0.0.3 - 2025.11.08
|
|
39
|
+
|
|
40
|
+
- New: optional support for `recv_into` in socket-like objects.
|
|
41
|
+
- Optimization: recv operations will now use `sock.recv_into` (if available) to reduce memory allocation overhead.
|
|
42
|
+
|
|
43
|
+
### 0.0.2 - 2025.11.05
|
|
39
44
|
|
|
40
45
|
- Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
|
|
41
46
|
- New: optional msgpack support (`msgpack` extra).
|
|
42
47
|
- Fix: busy reads no longer blocking asyncio event loop.
|
|
43
|
-
- Typing: expose `SizedBuffer` and deprecate `SendPayload
|
|
48
|
+
- Typing: expose `SizedBuffer` and deprecate `SendPayload` (removal expected in `0.1.0`).
|
|
44
49
|
- Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
|
|
45
50
|
|
|
46
51
|
## Documentation
|
|
@@ -49,7 +54,9 @@ None other than this README, life's too short and I'm too busy with real life, i
|
|
|
49
54
|
|
|
50
55
|
### Puggable I/O: SocketLike protocol
|
|
51
56
|
|
|
52
|
-
The `
|
|
57
|
+
The `protocols.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
58
|
+
|
|
59
|
+
Additional methods defined in `protocols.OptionalSocketLike` can be optionally implemented enabling optimized code paths.
|
|
53
60
|
|
|
54
61
|
```py
|
|
55
62
|
@typing.runtime_checkable
|
|
@@ -61,6 +68,14 @@ class SocketLike(typing.Protocol):
|
|
|
61
68
|
def recv(self, bufsize: int, /) -> bytes: ...
|
|
62
69
|
def settimeout(self, timeout: float | None, /) -> None: ...
|
|
63
70
|
def close(self) -> None: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@typing.runtime_checkable
|
|
74
|
+
class OptionalSocketLike(SocketLike, typing.Protocol):
|
|
75
|
+
"""Protocol for optional socket-like object methods accepted by socketwrapper socket classes."""
|
|
76
|
+
|
|
77
|
+
def recv_into(self, buffer: collections.abc.Buffer, /) -> bytes: ...
|
|
78
|
+
|
|
64
79
|
```
|
|
65
80
|
|
|
66
81
|
Special attention to:
|
|
@@ -5,7 +5,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'socketwrapper'
|
|
7
7
|
description = 'high level socket and pipe wrappers'
|
|
8
|
-
version = '0.0.
|
|
8
|
+
version = '0.0.3'
|
|
9
9
|
requires-python = '>=3.12'
|
|
10
10
|
keywords = ['socket', 'pipe', 'ipc', 'asyncio']
|
|
11
11
|
readme = { file = 'README.md', content-type = 'text/markdown' }
|
|
@@ -48,6 +48,7 @@ dev = [
|
|
|
48
48
|
|
|
49
49
|
[project.urls]
|
|
50
50
|
homepage = 'https://gitlab.com/ergoithz/socketwrapper'
|
|
51
|
+
repository = 'https://gitlab.com/ergoithz/socketwrapper'
|
|
51
52
|
issue-tracker = 'https://gitlab.com/ergoithz/socketwrapper/-/issues'
|
|
52
53
|
release-notes = 'https://gitlab.com/ergoithz/socketwrapper/-/releases'
|
|
53
54
|
issue-new = 'https://gitlab.com/ergoithz/socketwrapper/-/issues/new'
|
|
@@ -23,7 +23,7 @@ from . import _utils, framing, protocols
|
|
|
23
23
|
__all__ = (
|
|
24
24
|
# type aliases and protocols
|
|
25
25
|
'MessageFraming',
|
|
26
|
-
'SendPayload', #
|
|
26
|
+
'SendPayload', # TODO(0.1.0): remove, deprecated since 0.0.2
|
|
27
27
|
'SizedBuffer',
|
|
28
28
|
'SocketLike',
|
|
29
29
|
'SocketOrDescriptor',
|
|
@@ -50,7 +50,7 @@ DEFAULT_FRAMING = framing.VarIntBytes
|
|
|
50
50
|
|
|
51
51
|
SocketLike = protocols.SocketLike
|
|
52
52
|
MessageFraming = protocols.MessageFraming
|
|
53
|
-
SocketOrDescriptor = SocketLike | protocols.HasFileno | int
|
|
53
|
+
SocketOrDescriptor = protocols.SocketLike | protocols.ExtendedSocketLike | protocols.HasFileno | int
|
|
54
54
|
SizedBuffer = protocols.SizedBuffer
|
|
55
55
|
|
|
56
56
|
RecvSize = _utils.RecvSize
|
|
@@ -58,7 +58,7 @@ SendPayload = _utils.SendPayload
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
class DescriptorSocket:
|
|
61
|
-
"""Generic socket-like accepting any file descriptor (
|
|
61
|
+
"""Generic socket-like accepting any file descriptor (usually pipes) as input."""
|
|
62
62
|
|
|
63
63
|
def __init__(self, fd: int) -> None:
|
|
64
64
|
"""Initialize."""
|
|
@@ -76,6 +76,16 @@ class DescriptorSocket:
|
|
|
76
76
|
"""Read data from descriptor."""
|
|
77
77
|
return os.read(self._fd, bufsize)
|
|
78
78
|
|
|
79
|
+
if sys.version_info < (3, 14):
|
|
80
|
+
def recv_into(self, buffer: collections.abc.Buffer) -> int:
|
|
81
|
+
"""Read data from descriptor into buffer."""
|
|
82
|
+
return os.readv(self._fd, (buffer,))
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
def recv_into(self, buffer: collections.abc.Buffer) -> int:
|
|
86
|
+
"""Read data from descriptor into buffer."""
|
|
87
|
+
return os.readinto(self._fd, buffer)
|
|
88
|
+
|
|
79
89
|
def settimeout(self, timeout: float | None) -> None:
|
|
80
90
|
"""Set blocking or non-blocking based on timeout being 0 or None."""
|
|
81
91
|
if timeout:
|
|
@@ -262,7 +272,7 @@ KSW = typing_extensions.TypeVar('KSW', bound=SocketWriter, default=SocketWriter)
|
|
|
262
272
|
class MessageWrapper(_utils.ClosingContext, typing_extensions.Generic[R, W, KS]):
|
|
263
273
|
"""Base class for message socket wrappers supporting asynchronous operations."""
|
|
264
274
|
|
|
265
|
-
_wrapper
|
|
275
|
+
_wrapper = SocketWrapper
|
|
266
276
|
_raw: KS
|
|
267
277
|
framing: MessageFraming[R, W]
|
|
268
278
|
|
|
@@ -293,7 +303,7 @@ class MessageWrapper(_utils.ClosingContext, typing_extensions.Generic[R, W, KS])
|
|
|
293
303
|
class MessageReader(MessageWrapper[R, typing.Any, KSR], typing_extensions.Generic[R, KSR]):
|
|
294
304
|
"""Readable message socket wrapper."""
|
|
295
305
|
|
|
296
|
-
_wrapper
|
|
306
|
+
_wrapper = SocketReader
|
|
297
307
|
|
|
298
308
|
def recv(self, timeout: float | None = None) -> R:
|
|
299
309
|
"""Receive data from socket."""
|
|
@@ -307,7 +317,7 @@ class MessageReader(MessageWrapper[R, typing.Any, KSR], typing_extensions.Generi
|
|
|
307
317
|
class MessageWriter(MessageWrapper[typing.Any, W, KSW], typing_extensions.Generic[W, KSW]):
|
|
308
318
|
"""Writable message socket wrapper."""
|
|
309
319
|
|
|
310
|
-
_wrapper
|
|
320
|
+
_wrapper = SocketWriter
|
|
311
321
|
|
|
312
322
|
def send(self, data: W, timeout: float | None = None) -> None:
|
|
313
323
|
"""Send data to socket."""
|
|
@@ -321,7 +331,7 @@ class MessageWriter(MessageWrapper[typing.Any, W, KSW], typing_extensions.Generi
|
|
|
321
331
|
class MessageDuplex[R, W](MessageWriter[W, SocketDuplex], MessageReader[R, SocketDuplex]):
|
|
322
332
|
"""Duplex (both readable and writable) message socket wrapper."""
|
|
323
333
|
|
|
324
|
-
_wrapper
|
|
334
|
+
_wrapper = SocketDuplex
|
|
325
335
|
|
|
326
336
|
|
|
327
337
|
# TODO: move to namespace
|
|
@@ -445,9 +455,9 @@ def _framing(framing: MessageFraming[DR, DW] | bool) -> MessageFraming[DR, DW] |
|
|
|
445
455
|
return DEFAULT_FRAMING if framing is True else framing if framing else None
|
|
446
456
|
|
|
447
457
|
|
|
448
|
-
def _socketlike(sock:
|
|
458
|
+
def _socketlike[S: SocketLike](sock: S | protocols.HasFileno | int) -> S | socket.socket | DescriptorSocket:
|
|
449
459
|
"""Use or initialize socketlike, reporting if it's native or not."""
|
|
450
|
-
if isinstance(sock, SocketLike):
|
|
460
|
+
if isinstance(sock, protocols.SocketLike):
|
|
451
461
|
return sock # socket object, use straight
|
|
452
462
|
|
|
453
463
|
fd, dup = (sock, False) if isinstance(sock, int) else (sock.fileno(), True)
|
|
@@ -459,18 +469,18 @@ def _socketlike(sock: SocketOrDescriptor) -> SocketLike:
|
|
|
459
469
|
maybe_socket = False
|
|
460
470
|
|
|
461
471
|
if maybe_socket:
|
|
462
|
-
|
|
472
|
+
sck = socket.socket(fileno=fd)
|
|
463
473
|
try:
|
|
464
|
-
|
|
474
|
+
sck.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)
|
|
465
475
|
if dup: # obj backed by socket, duplicate to not interfere with original ref lifetime
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return
|
|
476
|
+
sck2 = sck.dup()
|
|
477
|
+
sck.detach()
|
|
478
|
+
return sck2
|
|
469
479
|
|
|
470
480
|
except OSError:
|
|
471
|
-
|
|
481
|
+
sck.detach()
|
|
472
482
|
|
|
473
483
|
else:
|
|
474
|
-
return
|
|
484
|
+
return sck # socket descriptor, use directly
|
|
475
485
|
|
|
476
486
|
return DescriptorSocket(fd) # assume pipe-like
|
|
@@ -8,6 +8,7 @@ import errno
|
|
|
8
8
|
import io
|
|
9
9
|
import os
|
|
10
10
|
import socket
|
|
11
|
+
import sys
|
|
11
12
|
import threading
|
|
12
13
|
import time
|
|
13
14
|
import types
|
|
@@ -20,6 +21,9 @@ from . import protocols
|
|
|
20
21
|
RecvSize = collections.abc.Callable[[io.BytesIO], collections.abc.Iterable[int]] | int
|
|
21
22
|
SendPayload = collections.abc.Iterable[protocols.SizedBuffer] | protocols.SizedBuffer
|
|
22
23
|
|
|
24
|
+
COPY_BUFFER_MINSIZE = 65536 # 64KB
|
|
25
|
+
COPY_BUFFER_THRESHOLD = 512 if os.getenv('PYTHONMALLOC') in {None, 'default', 'pymalloc'} else 0
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
class ClosingContext(abc.ABC):
|
|
25
29
|
|
|
@@ -103,7 +107,7 @@ class ContextPair[A: contextlib.AbstractContextManager, B: contextlib.AbstractCo
|
|
|
103
107
|
class CrossLock:
|
|
104
108
|
"""A higher level lock for both sync and async."""
|
|
105
109
|
|
|
106
|
-
__slots__
|
|
110
|
+
__slots__ = '_aiolocks', '_aiowaits', '_busy', '_lock'
|
|
107
111
|
|
|
108
112
|
_aiolocks: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, asyncio.Lock]
|
|
109
113
|
_aiowaits: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, asyncio.Future]
|
|
@@ -190,41 +194,79 @@ def safe_timeout(sock: protocols.SocketLike, timeout: float | None) -> bool:
|
|
|
190
194
|
"""Check if timeout if supported by setting it and handling ValueError."""
|
|
191
195
|
try:
|
|
192
196
|
sock.settimeout(timeout)
|
|
197
|
+
|
|
193
198
|
except ValueError:
|
|
194
199
|
return False
|
|
200
|
+
|
|
195
201
|
return True
|
|
196
202
|
|
|
197
203
|
|
|
198
|
-
def reader(
|
|
199
|
-
sock: protocols.SocketLike, buffer: io.BytesIO, size: RecvSize, *,
|
|
204
|
+
def reader( # noqa: C901
|
|
205
|
+
sock: protocols.SocketLike | protocols.ExtendedSocketLike, buffer: io.BytesIO, size: RecvSize, *,
|
|
200
206
|
throttle: bool = False,
|
|
201
207
|
) -> collections.abc.Iterator[bool]:
|
|
202
|
-
"""
|
|
208
|
+
r"""
|
|
209
|
+
Fully consume bytes from the socket.
|
|
210
|
+
|
|
211
|
+
Implementation notes
|
|
212
|
+
====================
|
|
213
|
+
|
|
214
|
+
1. We yield `True` for when the consumer should wait for data.
|
|
215
|
+
2. We rely on ``pymalloc`` speed for small strings (512B) and whenever
|
|
216
|
+
:meth:`protocols.OptionalSocketLike.recv_into` isn't available.
|
|
217
|
+
3. We use a copy buffer for bigger reads instead of writing directly via
|
|
218
|
+
:meth:`io.BytesIO.getbuffer` because of issues with preallocation:
|
|
219
|
+
- :meth:`io.BytesIO.truncate` doesn't extend https://github.com/python/cpython/issues/71448
|
|
220
|
+
- ``buffer.write(b'\0' * size)`` is slower.
|
|
221
|
+
- ``buffer.seek(size - 1, os.SEEK_END) + buffer.write(b'\0')`` breaks smart resizing.
|
|
222
|
+
4. Our copy buffer can grow by duplicating its size on each partial read
|
|
223
|
+
until its size is above the source socket-like buffer size.
|
|
224
|
+
|
|
225
|
+
"""
|
|
203
226
|
buff_write = buffer.write
|
|
204
227
|
sock_recv = sock.recv
|
|
228
|
+
sock_recv_into = getattr(sock, 'recv_into', None) # optional
|
|
205
229
|
tell = buffer.tell
|
|
206
230
|
seek = buffer.seek
|
|
207
231
|
seek_end = os.SEEK_END
|
|
208
232
|
sizes = (size,) if isinstance(size, int) else size(buffer)
|
|
233
|
+
bufskip, bufview = (
|
|
234
|
+
(COPY_BUFFER_THRESHOLD, memoryview(bytearray(COPY_BUFFER_MINSIZE))) if sock_recv_into else
|
|
235
|
+
(sys.maxsize, b'')
|
|
236
|
+
)
|
|
209
237
|
for sz in sizes:
|
|
210
|
-
if sz
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
if sz < 1:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# FIXME: rework implementation if cpython ever fixes https://github.com/python/cpython/issues/71448
|
|
242
|
+
pos = tell()
|
|
243
|
+
seek(0, seek_end)
|
|
244
|
+
while True:
|
|
245
|
+
try:
|
|
246
|
+
if sz > bufskip: # use buffer for long strings (if recv_into is available)
|
|
247
|
+
written = sock_recv_into(bufview[:sz])
|
|
248
|
+
if written == len(bufview):
|
|
249
|
+
chunk, bufview = bufview, memoryview(bytearray(len(bufview) * 2))
|
|
250
|
+
|
|
251
|
+
elif written:
|
|
252
|
+
chunk = bufview[:written]
|
|
253
|
+
|
|
254
|
+
else:
|
|
216
255
|
raise EOFError
|
|
217
256
|
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
elif not (chunk := sock_recv(sz)): # use pymalloc (faster than buffers for small strings)
|
|
258
|
+
raise EOFError
|
|
220
259
|
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
if (sz := sz - buff_write(chunk)) < 1:
|
|
261
|
+
break
|
|
223
262
|
|
|
224
|
-
|
|
225
|
-
yield
|
|
263
|
+
if throttle:
|
|
264
|
+
yield False
|
|
265
|
+
|
|
266
|
+
except BlockingIOError:
|
|
267
|
+
yield True
|
|
226
268
|
|
|
227
|
-
|
|
269
|
+
seek(pos)
|
|
228
270
|
|
|
229
271
|
|
|
230
272
|
def writer( # noqa: C901
|
|
@@ -244,22 +286,27 @@ def writer( # noqa: C901
|
|
|
244
286
|
sock_send = sock.send
|
|
245
287
|
chunks = (data,) if isinstance(data, protocols.SizedBuffer) else data
|
|
246
288
|
for chunk in chunks:
|
|
247
|
-
if chunk:
|
|
248
|
-
|
|
249
|
-
while (offset := safe_write(chunk)) < 1: # write first chunk without slicing
|
|
250
|
-
yield True
|
|
289
|
+
if not chunk:
|
|
290
|
+
continue
|
|
251
291
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
292
|
+
written = 0
|
|
293
|
+
while (written := safe_write(chunk)) < 1: # write first chunk without slicing
|
|
294
|
+
yield True
|
|
295
|
+
|
|
296
|
+
sz = len(chunk) - written
|
|
297
|
+
if sz < 1:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
with memoryview(chunk) as buffer: # turn slicing into zero-copy
|
|
301
|
+
while True:
|
|
302
|
+
if (written := safe_write(buffer[-sz:])) < 1:
|
|
303
|
+
yield True
|
|
257
304
|
|
|
258
|
-
|
|
259
|
-
|
|
305
|
+
elif (sz := sz - written) < 1:
|
|
306
|
+
break
|
|
260
307
|
|
|
261
|
-
|
|
262
|
-
|
|
308
|
+
elif throttle:
|
|
309
|
+
yield False
|
|
263
310
|
|
|
264
311
|
|
|
265
312
|
@contextlib.contextmanager
|
|
@@ -4,7 +4,6 @@ import abc
|
|
|
4
4
|
import collections.abc
|
|
5
5
|
import io
|
|
6
6
|
import os
|
|
7
|
-
import typing
|
|
8
7
|
|
|
9
8
|
from . import _varint, protocols
|
|
10
9
|
|
|
@@ -178,7 +177,7 @@ try:
|
|
|
178
177
|
packer: PackerTypes
|
|
179
178
|
unpacker: UnpackerTypes
|
|
180
179
|
|
|
181
|
-
_typedefs
|
|
180
|
+
_typedefs = (
|
|
182
181
|
# headsize, headskip, varsized, recurse, factor
|
|
183
182
|
*((0, 0, False, False, 1) for _ in range(0x80)), # 0x00-0x80: fixint (+)
|
|
184
183
|
*((i, 0, False, True, 2) for i in range(16)), # 0x80-0x90: fixmap
|
|
@@ -24,6 +24,13 @@ class SocketLike(typing.Protocol):
|
|
|
24
24
|
def close(self) -> None: ...
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
@typing.runtime_checkable
|
|
28
|
+
class ExtendedSocketLike(SocketLike, typing.Protocol):
|
|
29
|
+
"""Protocol for optional socket-like object methods accepted by socketwrapper socket classes."""
|
|
30
|
+
|
|
31
|
+
def recv_into(self, buffer: collections.abc.Buffer, /) -> bytes: ...
|
|
32
|
+
|
|
33
|
+
|
|
27
34
|
@typing.runtime_checkable
|
|
28
35
|
class HasFileno(typing.Protocol):
|
|
29
36
|
"""Protocol for objects backed by a file descriptor."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socketwrapper
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: high level socket and pipe wrappers
|
|
5
5
|
Author: Felipe A Hernandez
|
|
6
6
|
Author-email: ergoithz@gmail.com
|
|
@@ -27,6 +27,7 @@ License: MIT License
|
|
|
27
27
|
SOFTWARE.
|
|
28
28
|
|
|
29
29
|
Project-URL: homepage, https://gitlab.com/ergoithz/socketwrapper
|
|
30
|
+
Project-URL: repository, https://gitlab.com/ergoithz/socketwrapper
|
|
30
31
|
Project-URL: issue-tracker, https://gitlab.com/ergoithz/socketwrapper/-/issues
|
|
31
32
|
Project-URL: release-notes, https://gitlab.com/ergoithz/socketwrapper/-/releases
|
|
32
33
|
Project-URL: issue-new, https://gitlab.com/ergoithz/socketwrapper/-/issues/new
|
|
@@ -89,12 +90,17 @@ uv pip install 'socketwrapper[msgpack]'
|
|
|
89
90
|
|
|
90
91
|
## Changelog
|
|
91
92
|
|
|
92
|
-
###
|
|
93
|
+
### 0.0.3 - 2025.11.08
|
|
94
|
+
|
|
95
|
+
- New: optional support for `recv_into` in socket-like objects.
|
|
96
|
+
- Optimization: recv operations will now use `sock.recv_into` (if available) to reduce memory allocation overhead.
|
|
97
|
+
|
|
98
|
+
### 0.0.2 - 2025.11.05
|
|
93
99
|
|
|
94
100
|
- Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
|
|
95
101
|
- New: optional msgpack support (`msgpack` extra).
|
|
96
102
|
- Fix: busy reads no longer blocking asyncio event loop.
|
|
97
|
-
- Typing: expose `SizedBuffer` and deprecate `SendPayload
|
|
103
|
+
- Typing: expose `SizedBuffer` and deprecate `SendPayload` (removal expected in `0.1.0`).
|
|
98
104
|
- Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
|
|
99
105
|
|
|
100
106
|
## Documentation
|
|
@@ -103,7 +109,9 @@ None other than this README, life's too short and I'm too busy with real life, i
|
|
|
103
109
|
|
|
104
110
|
### Puggable I/O: SocketLike protocol
|
|
105
111
|
|
|
106
|
-
The `
|
|
112
|
+
The `protocols.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
113
|
+
|
|
114
|
+
Additional methods defined in `protocols.OptionalSocketLike` can be optionally implemented enabling optimized code paths.
|
|
107
115
|
|
|
108
116
|
```py
|
|
109
117
|
@typing.runtime_checkable
|
|
@@ -115,6 +123,14 @@ class SocketLike(typing.Protocol):
|
|
|
115
123
|
def recv(self, bufsize: int, /) -> bytes: ...
|
|
116
124
|
def settimeout(self, timeout: float | None, /) -> None: ...
|
|
117
125
|
def close(self) -> None: ...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@typing.runtime_checkable
|
|
129
|
+
class OptionalSocketLike(SocketLike, typing.Protocol):
|
|
130
|
+
"""Protocol for optional socket-like object methods accepted by socketwrapper socket classes."""
|
|
131
|
+
|
|
132
|
+
def recv_into(self, buffer: collections.abc.Buffer, /) -> bytes: ...
|
|
133
|
+
|
|
118
134
|
```
|
|
119
135
|
|
|
120
136
|
Special attention to:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|