socketwrapper 0.0.2__tar.gz → 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.
Files changed (23) hide show
  1. {socketwrapper-0.0.2/socketwrapper.egg-info → socketwrapper-0.1.0}/PKG-INFO +141 -20
  2. socketwrapper-0.1.0/README.md +407 -0
  3. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/pyproject.toml +21 -3
  4. socketwrapper-0.1.0/socketwrapper/__init__.py +696 -0
  5. socketwrapper-0.1.0/socketwrapper/_io.py +527 -0
  6. socketwrapper-0.1.0/socketwrapper/_nt.py +473 -0
  7. socketwrapper-0.1.0/socketwrapper/_utils.py +381 -0
  8. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper/_varint.py +6 -1
  9. socketwrapper-0.1.0/socketwrapper/framing.py +406 -0
  10. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper/protocols.py +19 -6
  11. {socketwrapper-0.0.2 → socketwrapper-0.1.0/socketwrapper.egg-info}/PKG-INFO +141 -20
  12. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/SOURCES.txt +2 -0
  13. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/requires.txt +3 -1
  14. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/zip-safe +1 -1
  15. socketwrapper-0.0.2/README.md +0 -287
  16. socketwrapper-0.0.2/socketwrapper/__init__.py +0 -476
  17. socketwrapper-0.0.2/socketwrapper/_utils.py +0 -298
  18. socketwrapper-0.0.2/socketwrapper/framing.py +0 -255
  19. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/LICENSE +0 -0
  20. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/setup.cfg +0 -0
  21. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper/py.typed +0 -0
  22. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/dependency_links.txt +0 -0
  23. {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketwrapper
3
- Version: 0.0.2
3
+ Version: 0.1.0
4
4
  Summary: high level socket and pipe wrappers
5
5
  Author: Felipe A Hernandez
6
6
  Author-email: ergoithz@gmail.com
@@ -27,11 +27,12 @@ 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
33
34
  Project-URL: donations, https://ko-fi.com/s26me
34
- Keywords: socket,pipe,ipc,asyncio
35
+ Keywords: socket,pipe,ipc,asyncio,multiprocessing
35
36
  Classifier: Framework :: AsyncIO
36
37
  Classifier: Intended Audience :: Developers
37
38
  Classifier: License :: OSI Approved :: MIT License
@@ -49,30 +50,49 @@ Requires-Dist: coverage; extra == "dev"
49
50
  Requires-Dist: msgpack; extra == "dev"
50
51
  Requires-Dist: ruff; extra == "dev"
51
52
  Requires-Dist: wheel; extra == "dev"
52
- Requires-Dist: yapf; extra == "dev"
53
+ Requires-Dist: uvloop; sys_platform != "win32" and extra == "dev"
53
54
  Dynamic: license-file
54
55
 
55
56
  # socketwrapper
56
57
 
57
- This package provides high level wrappers for sockets and pipes:
58
+ High level wrappers for socket and pipe IO, thread-safe, asyncio-native and supporting multiprocessing.
59
+
60
+ Features:
58
61
 
59
62
  - Thread-safe within both threads and asyncio realms, and between them.
60
63
  - Managed sync `recv` and `send` operations with timeouts.
61
64
  - Native asyncio `recv_async` and `send_async` operations.
62
- - Pluggable message protocol (headered variable length data) supporting variable-length header parsing, payload serialization and deserialization.
63
- - Pluggable I/O protocol (OS file descriptor still required).
65
+ - Pluggable message frame and serialization protocols for stream sockets, and serialization for pipes, datagram sockets and Windows message pipes.
66
+ - Pluggable I/O (backed by supported OS descriptors).
64
67
 
65
- Builtin message protocols (framing):
68
+ Built-in message protocols (framing):
66
69
 
67
70
  - `socketwrapper.framing.VarIntBytes`: varint-headered bytes (default with `framing=True`).
68
- - `socketwrapper.framing.MultiprocessingBytes` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
69
- - `socketwrapper.framing.Multiprocessing` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
70
- - `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data stream.
71
+ - `socketwrapper.framing.MultiprocessingBytes`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
72
+ - `socketwrapper.framing.Multiprocessing`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
73
+ - `socketwrapper.framing.MultiprocessingPipeBytes`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` bytes, or alias of `socketwrapper.framing.MultiprocessingBytes` in other platforms.
74
+ - `socketwrapper.framing.MultiprocessingPipe`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` pickled data, or alias of `socketwrapper.framing.Multiprocessing` in other platforms.
75
+ - `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data.
76
+
77
+ ## Limitations on Windows
78
+
79
+ > 🛈 Due platform limitations, `sockerwrapper` has to resort on polling for non-overlapped pipes, and also [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes when not using [asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop). See [Python documentation about asyncio Platform Support](https://docs.python.org/3/library/asyncio-platforms.html#asyncio-platform-support) for details.
80
+
81
+ > ⚠ Due performance considerations, avoid:
82
+ > - Wrapping anonymous pipes (as returned by [os.pipe](https://docs.python.org/3/library/os.html#os.pipe)) whenever possible.
83
+ > - Asynchronous operations on overlapped named pipes when using [asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop).
84
+
85
+ For your convenience, `socketwrapper.pipe` will create overlapped named pipes on Windows.
86
+
87
+ [asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop) (default on Windows) works with both sockets and [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes. Non-overlapped pipes will use polling.
88
+
89
+ [asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop) only natively supports wrapping sockets when manually configured as event loop, all pipes will use polling.
71
90
 
72
91
  ## Motivation
73
92
 
93
+ - I just wanted to send/recv from sockets (and pipes) for IPC without having to dig up 40 years worth of quirks, with and without asyncio.
74
94
  - There aren't a ton of high level asyncio socket wrappers out there providing all we need for IPC: header/payload message logic and support for both sockets and pipes.
75
- - Most implementations got either hardcoded messaging protocols or require a fixed-size header.
95
+ - Most implementations got either hardcoded messaging protocols or require a fixed-size header, or just hardcode their own socket initialization like [asyncio.streams](https://docs.python.org/3.14/library/asyncio-stream.html).
76
96
  - No implementation was thread-safe between both asyncio and threading realms.
77
97
  - No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
78
98
 
@@ -89,13 +109,66 @@ uv pip install 'socketwrapper[msgpack]'
89
109
 
90
110
  ## Changelog
91
111
 
92
- ### v0.0.2 - 2025.11.05
112
+ ### 0.1.0 - 2026.01.22
113
+
114
+ #### Breaking
115
+
116
+ - Type `SendPayload` is no longer exposed.
117
+ - `SizedBuffer` protocol is removed, as it was causing typing annoyances.
118
+ - `SocketLike` protocol now requires `recv_into`, `get_inheritable` and `set_inheritable` implementations.
119
+
120
+ #### Features
121
+
122
+ - Added support for Windows sockets and pipes.
123
+ - Added support for datagram sockets.
124
+ - Added support for blind `recv` operations without explicit size (omitted or `None`), returning the first chunk of data for stream sockets, or a whole datagram for datagram sockets whatever its size.
125
+ - Added `asyncio` event loop feature detection with multiple strategies.
126
+ - Added `StreamEOFError` exception (inheriting and replacing [EOFError](https://docs.python.org/3.14/library/exceptions.html#EOFError)), with a `data` attribute exposing partial result.
127
+ - Added `StreamTimeoutError` exception (inheriting and replacing [TimeoutError](https://docs.python.org/3.14/library/exceptions.html#TimeoutError)), with a `data` attribute exposing partial result.
128
+ - Added `StreamCancelledError`: exception (inheriting and replacing [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError)), with `data` attribute exposing partial result.
129
+ - For **Python 3.12** this is an alias of [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError) due [cpython#113848](https://github.com/python/cpython/issues/113848).
130
+
131
+
132
+ #### Changes
133
+
134
+ - For datagram socket-likes (`protocols.ExtendedSocketLike.type` defined as anything other than `socket.SOCK_STREAM`):
135
+ - Protocol `protocols.SocketLike.recv` requires handing new `flags=socket.MSG_PEEK` parameter.
136
+ - Protocol `protocols.SocketLike.recv_into` requires handing both new `bufsize=0` and `flags=socket.MSG_PEEK` parameters.
137
+ - Helper `socketwrapper.pipe`creates overlapped named pipes on **Windows** instead of `os.pipe` anonymous pipes to support asynchronous IO.
138
+ - Improved compatibility for third party asyncio event loops ([uvloop](https://github.com/MagicStack/uvloop) tested).
139
+ - SocketWrappers are now serializable by [multiprocessing](https://docs.python.org/3/library/multiprocessing.html).
140
+ - Optimize `framing.MsgPack` headless stream read operation logic.
141
+
142
+ #### Bugfixes
143
+
144
+ - `SocketWriter.send` and `SocketWriter.send_async` now return its written byte count.
145
+ - Synchronous recv zero timeout `recv(..., timeout=0)` is now handled consistently.
146
+ - `socketwrapper.socketpair` now always returns full duplex socketwrappers.
147
+
148
+ ### 0.0.3 - 2025.11.08
149
+
150
+ #### Features
151
+
152
+ - Optional protocol `protocols.ExtendedSocketLike.recv_into`.
153
+ - `recv` operations can now use `sock.recv_into` (if available) to reduce memory allocation overhead.
154
+
155
+ ### 0.0.2 - 2025.11.05
156
+
157
+ #### Breaking
93
158
 
94
- - Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
95
- - New: optional msgpack support (`msgpack` extra).
96
- - Fix: busy reads no longer blocking asyncio event loop.
97
- - Typing: expose `SizedBuffer` and deprecate `SendPayload`.
98
- - Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
159
+ - `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
160
+
161
+ #### Features
162
+ - Optional msgpack support (`msgpack` extra).
163
+
164
+ #### Changes
165
+
166
+ - `SizedBuffer` type is now exposed and deprecate `SendPayload` (removal expected in `0.1.0`).
167
+ - `SocketWriter.send` and `SocketWriter.send_async` parameter `data` type is now `SizedBuffer`.
168
+
169
+ #### Bugfixes
170
+
171
+ - Busy reads no longer blocking asyncio event loop.
99
172
 
100
173
  ## Documentation
101
174
 
@@ -103,23 +176,28 @@ None other than this README, life's too short and I'm too busy with real life, i
103
176
 
104
177
  ### Puggable I/O: SocketLike protocol
105
178
 
106
- The `socketwrapper.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
179
+ The `protocols.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
107
180
 
108
181
  ```py
109
182
  @typing.runtime_checkable
110
183
  class SocketLike(typing.Protocol):
111
184
  """Protocol for socket-like objects accepted by socketwrapper socket classes."""
112
185
 
186
+ def close(self) -> None: ...
113
187
  def fileno(self) -> int: ...
188
+ def recv(self, bufsize: int, flags: int = 0, /) -> bytes: ...
189
+ def recv_into(self, buffer: collections.abc.Buffer, nbytes: int = 0, flags: int = 0, /) -> int: ...
114
190
  def send(self, data: collections.abc.Buffer, /) -> int: ...
115
- def recv(self, bufsize: int, /) -> bytes: ...
116
191
  def settimeout(self, timeout: float | None, /) -> None: ...
117
- def close(self) -> None: ...
192
+ def get_inheritable(self) -> bool: ...
193
+ def set_inheritable(self, inheritable: bool, /) -> None: ...
194
+
118
195
  ```
119
196
 
120
197
  Special attention to:
121
198
  - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
122
199
  - [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout) must support `settimeout(.0)` ([non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError) for any other value will be handled, relying on [selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector) for synchronous operations.
200
+ - Unless `SocketLike.type` is [socket.SOCK_STREAM](https://docs.python.org/3.14/library/socket.html#socket.SOCK_STREAM) (or missing), [recv](https://docs.python.org/3.14/library/socket.html#socket.socket.recv) and [recv_into](https://docs.python.org/3.14/library/socket.html#socket.socket.recv_into) `flags` must support `socket.MSG_PEEK`.
123
201
 
124
202
  ## Usage
125
203
 
@@ -339,3 +417,46 @@ Sending 1048576 bytes!
339
417
  > 100%
340
418
  Received 1048576 bytes!
341
419
  ```
420
+
421
+ #### Connection handling (`socket.accept`)
422
+
423
+ The connection concept is purposely left out of this library (may change in the future), reasoning:
424
+ - Already existing methods for accepting connections: [socket.accept](https://docs.python.org/3/library/socket.html#socket.socket.accept) and [loop.sock_accept](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.loop.sock_accept), both simple and standard.
425
+ - `socketwrapper` can be already used to wrap the connected socket (thread safe and way simpler than [asyncio Streaming Protocols](https://docs.python.org/3.14/library/asyncio-protocol.html#streaming-protocols)).
426
+ - Server sockets aren't usually shared outside the connection loop between sync and async contexts (unnecessary `CrossLock`).
427
+
428
+ So just wrap accepted connection sockets with `socketwrapper.SocketDuplex`.
429
+
430
+ ```python
431
+ import asyncio
432
+ import socket
433
+ import socketwrapper
434
+
435
+ async def listen(server: socket.socket) -> None:
436
+ loop = asyncio.get_running_loop()
437
+ while True:
438
+ sock, addr = await loop.sock_accept(server)
439
+ with socketwrapper.SocketDuplex(sock) as sock:
440
+ await sock.send_async(b'message')
441
+
442
+ async def main() -> None:
443
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
444
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
445
+ server.bind(('localhost', 8080))
446
+ server.listen(2)
447
+ server.setblocking(False) # asyncio requires a non-blocking server
448
+ listener = asyncio.create_task(listen(server))
449
+
450
+ client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
451
+ client.connect(('localhost', 8080))
452
+ with socketwrapper.SocketDuplex(client) as client:
453
+ print(await client.recv_async())
454
+
455
+ listener.cancel() # stop connection handler
456
+ await asyncio.gather(listener, return_exceptions=True)
457
+ server.close()
458
+
459
+ if __name__ == '__main__':
460
+ asyncio.run(main())
461
+
462
+ ```
@@ -0,0 +1,407 @@
1
+ # socketwrapper
2
+
3
+ High level wrappers for socket and pipe IO, thread-safe, asyncio-native and supporting multiprocessing.
4
+
5
+ Features:
6
+
7
+ - Thread-safe within both threads and asyncio realms, and between them.
8
+ - Managed sync `recv` and `send` operations with timeouts.
9
+ - Native asyncio `recv_async` and `send_async` operations.
10
+ - Pluggable message frame and serialization protocols for stream sockets, and serialization for pipes, datagram sockets and Windows message pipes.
11
+ - Pluggable I/O (backed by supported OS descriptors).
12
+
13
+ Built-in message protocols (framing):
14
+
15
+ - `socketwrapper.framing.VarIntBytes`: varint-headered bytes (default with `framing=True`).
16
+ - `socketwrapper.framing.MultiprocessingBytes`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
17
+ - `socketwrapper.framing.Multiprocessing`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
18
+ - `socketwrapper.framing.MultiprocessingPipeBytes`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` bytes, or alias of `socketwrapper.framing.MultiprocessingBytes` in other platforms.
19
+ - `socketwrapper.framing.MultiprocessingPipe`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` pickled data, or alias of `socketwrapper.framing.Multiprocessing` in other platforms.
20
+ - `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data.
21
+
22
+ ## Limitations on Windows
23
+
24
+ > 🛈 Due platform limitations, `sockerwrapper` has to resort on polling for non-overlapped pipes, and also [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes when not using [asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop). See [Python documentation about asyncio Platform Support](https://docs.python.org/3/library/asyncio-platforms.html#asyncio-platform-support) for details.
25
+
26
+ > ⚠ Due performance considerations, avoid:
27
+ > - Wrapping anonymous pipes (as returned by [os.pipe](https://docs.python.org/3/library/os.html#os.pipe)) whenever possible.
28
+ > - Asynchronous operations on overlapped named pipes when using [asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop).
29
+
30
+ For your convenience, `socketwrapper.pipe` will create overlapped named pipes on Windows.
31
+
32
+ [asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop) (default on Windows) works with both sockets and [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes. Non-overlapped pipes will use polling.
33
+
34
+ [asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop) only natively supports wrapping sockets when manually configured as event loop, all pipes will use polling.
35
+
36
+ ## Motivation
37
+
38
+ - I just wanted to send/recv from sockets (and pipes) for IPC without having to dig up 40 years worth of quirks, with and without asyncio.
39
+ - There aren't a ton of high level asyncio socket wrappers out there providing all we need for IPC: header/payload message logic and support for both sockets and pipes.
40
+ - Most implementations got either hardcoded messaging protocols or require a fixed-size header, or just hardcode their own socket initialization like [asyncio.streams](https://docs.python.org/3.14/library/asyncio-stream.html).
41
+ - No implementation was thread-safe between both asyncio and threading realms.
42
+ - No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
43
+
44
+ ## Installation
45
+
46
+ ```sh
47
+ uv pip install socketwrapper
48
+ ```
49
+
50
+ Or with optional [msgpack](https://pypi.org/project/msgpack/) support.
51
+ ```sh
52
+ uv pip install 'socketwrapper[msgpack]'
53
+ ```
54
+
55
+ ## Changelog
56
+
57
+ ### 0.1.0 - 2026.01.22
58
+
59
+ #### Breaking
60
+
61
+ - Type `SendPayload` is no longer exposed.
62
+ - `SizedBuffer` protocol is removed, as it was causing typing annoyances.
63
+ - `SocketLike` protocol now requires `recv_into`, `get_inheritable` and `set_inheritable` implementations.
64
+
65
+ #### Features
66
+
67
+ - Added support for Windows sockets and pipes.
68
+ - Added support for datagram sockets.
69
+ - Added support for blind `recv` operations without explicit size (omitted or `None`), returning the first chunk of data for stream sockets, or a whole datagram for datagram sockets whatever its size.
70
+ - Added `asyncio` event loop feature detection with multiple strategies.
71
+ - Added `StreamEOFError` exception (inheriting and replacing [EOFError](https://docs.python.org/3.14/library/exceptions.html#EOFError)), with a `data` attribute exposing partial result.
72
+ - Added `StreamTimeoutError` exception (inheriting and replacing [TimeoutError](https://docs.python.org/3.14/library/exceptions.html#TimeoutError)), with a `data` attribute exposing partial result.
73
+ - Added `StreamCancelledError`: exception (inheriting and replacing [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError)), with `data` attribute exposing partial result.
74
+ - For **Python 3.12** this is an alias of [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError) due [cpython#113848](https://github.com/python/cpython/issues/113848).
75
+
76
+
77
+ #### Changes
78
+
79
+ - For datagram socket-likes (`protocols.ExtendedSocketLike.type` defined as anything other than `socket.SOCK_STREAM`):
80
+ - Protocol `protocols.SocketLike.recv` requires handing new `flags=socket.MSG_PEEK` parameter.
81
+ - Protocol `protocols.SocketLike.recv_into` requires handing both new `bufsize=0` and `flags=socket.MSG_PEEK` parameters.
82
+ - Helper `socketwrapper.pipe`creates overlapped named pipes on **Windows** instead of `os.pipe` anonymous pipes to support asynchronous IO.
83
+ - Improved compatibility for third party asyncio event loops ([uvloop](https://github.com/MagicStack/uvloop) tested).
84
+ - SocketWrappers are now serializable by [multiprocessing](https://docs.python.org/3/library/multiprocessing.html).
85
+ - Optimize `framing.MsgPack` headless stream read operation logic.
86
+
87
+ #### Bugfixes
88
+
89
+ - `SocketWriter.send` and `SocketWriter.send_async` now return its written byte count.
90
+ - Synchronous recv zero timeout `recv(..., timeout=0)` is now handled consistently.
91
+ - `socketwrapper.socketpair` now always returns full duplex socketwrappers.
92
+
93
+ ### 0.0.3 - 2025.11.08
94
+
95
+ #### Features
96
+
97
+ - Optional protocol `protocols.ExtendedSocketLike.recv_into`.
98
+ - `recv` operations can now use `sock.recv_into` (if available) to reduce memory allocation overhead.
99
+
100
+ ### 0.0.2 - 2025.11.05
101
+
102
+ #### Breaking
103
+
104
+ - `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
105
+
106
+ #### Features
107
+ - Optional msgpack support (`msgpack` extra).
108
+
109
+ #### Changes
110
+
111
+ - `SizedBuffer` type is now exposed and deprecate `SendPayload` (removal expected in `0.1.0`).
112
+ - `SocketWriter.send` and `SocketWriter.send_async` parameter `data` type is now `SizedBuffer`.
113
+
114
+ #### Bugfixes
115
+
116
+ - Busy reads no longer blocking asyncio event loop.
117
+
118
+ ## Documentation
119
+
120
+ None other than this README, life's too short and I'm too busy with real life, if you need better documentation consider donating to [my ko-fi](https://ko-fi.com/s26me) stating that as a tip message, check out how my docs look like at [mstache docs](https://mstache.readthedocs.io/en/latest/) and [uactor docs](https://mstache.readthedocs.io/en/latest/).
121
+
122
+ ### Puggable I/O: SocketLike protocol
123
+
124
+ The `protocols.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
125
+
126
+ ```py
127
+ @typing.runtime_checkable
128
+ class SocketLike(typing.Protocol):
129
+ """Protocol for socket-like objects accepted by socketwrapper socket classes."""
130
+
131
+ def close(self) -> None: ...
132
+ def fileno(self) -> int: ...
133
+ def recv(self, bufsize: int, flags: int = 0, /) -> bytes: ...
134
+ def recv_into(self, buffer: collections.abc.Buffer, nbytes: int = 0, flags: int = 0, /) -> int: ...
135
+ def send(self, data: collections.abc.Buffer, /) -> int: ...
136
+ def settimeout(self, timeout: float | None, /) -> None: ...
137
+ def get_inheritable(self) -> bool: ...
138
+ def set_inheritable(self, inheritable: bool, /) -> None: ...
139
+
140
+ ```
141
+
142
+ Special attention to:
143
+ - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
144
+ - [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout) must support `settimeout(.0)` ([non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError) for any other value will be handled, relying on [selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector) for synchronous operations.
145
+ - Unless `SocketLike.type` is [socket.SOCK_STREAM](https://docs.python.org/3.14/library/socket.html#socket.SOCK_STREAM) (or missing), [recv](https://docs.python.org/3.14/library/socket.html#socket.socket.recv) and [recv_into](https://docs.python.org/3.14/library/socket.html#socket.socket.recv_into) `flags` must support `socket.MSG_PEEK`.
146
+
147
+ ## Usage
148
+
149
+ ### Simple IPC with pipe
150
+
151
+ ```python
152
+ import os
153
+ import socketwrapper
154
+
155
+ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
156
+ child_writer.inheritable = True
157
+ child_pid = os.fork() # replace with your own process fork/spawn logic
158
+ child_writer.inheritable = False # important, prevent socket leaks!
159
+
160
+ if child_pid:
161
+ print(f'Message {parent_reader.recv()!r} received')
162
+ else:
163
+ child_writer.send(b'Hello world!')
164
+ ```
165
+ ```
166
+ Message b'Hello world!' received
167
+ ```
168
+
169
+ ### Simple IPC with pipe using msgpack
170
+
171
+ ```python
172
+ import os
173
+ import socketwrapper
174
+ import socketwrapper.framing as framing
175
+
176
+ with socketwrapper.pipe(framing=framing.MsgPack()) as (parent_reader, child_writer):
177
+ child_writer.inheritable = True
178
+ child_pid = os.fork() # replace with your own process fork/spawn logic
179
+ child_writer.inheritable = False # important, prevent socket leaks!
180
+
181
+ if child_pid:
182
+ print(f'Message {parent_reader.recv()!r} received')
183
+ else:
184
+ child_writer.send({'data': b'Hello world!'})
185
+ ```
186
+ ```
187
+ Message {'data': b'Hello world!'} received
188
+ ```
189
+
190
+ ### Simple asyncio IPC with pipe
191
+
192
+ ```python
193
+ import asyncio
194
+ import os
195
+ import socketwrapper
196
+
197
+ async def parent(readable: socketwrapper.MessageReader) -> None:
198
+ print(f'Message {await readable.recv_async()!r} received')
199
+
200
+ async def child(writable: socketwrapper.MessageWriter) -> None:
201
+ await writable.send_async(b'Hello world!')
202
+
203
+ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
204
+ child_writer.inheritable = True
205
+ child_pid = os.fork() # replace with your own process fork/spawn logic
206
+ child_writer.inheritable = False # important, prevent socket leaks!
207
+
208
+ asyncio.run(parent(parent_reader) if child_pid else child(child_writer))
209
+ ```
210
+ ```
211
+ Message b'Hello world!' received
212
+ ```
213
+
214
+ ### Simple bidirectional IPC with socketpair
215
+
216
+ ```python
217
+ import os
218
+ import socketwrapper
219
+
220
+ with socketwrapper.socketpair(framing=True) as (parent_duplex, child_duplex):
221
+ child_duplex.inheritable = True
222
+ child_pid = os.fork() # replace with your own process fork/spawn logic
223
+ child_duplex.inheritable = False # important, prevent socket leaks!
224
+
225
+ if child_pid:
226
+ parent_duplex.send(b'Hello child!')
227
+ print(f'Message {parent_duplex.recv()!r} received in parent')
228
+
229
+ else:
230
+ print(f'Message {child_duplex.recv()!r} received in child')
231
+ child_duplex.send(b'Hello parent!')
232
+ ```
233
+ ```
234
+ Message b'Hello child!' received in child
235
+ Message b'Hello parent!' received in parent
236
+ ```
237
+
238
+ ### Socketwrapper with multiprocessing.Pipe and asyncio
239
+
240
+ ```py
241
+ import asyncio
242
+ import multiprocessing
243
+ import multiprocessing.connection
244
+ import socketwrapper
245
+ import socketwrapper.framing
246
+
247
+ def child(conn: multiprocessing.connection.Connection) -> None:
248
+
249
+ async def main() -> None:
250
+ with socketwrapper.MessageDuplex(conn, framing=socketwrapper.framing.MultiprocessingBytes) as child_duplex:
251
+ print(f'Message {await child_duplex.recv_async()!r} received in child')
252
+ await child_duplex.send_async(b'Hello parent!')
253
+
254
+ asyncio.run(main())
255
+
256
+ if __name__ == '__main__':
257
+ parent_conn, child_conn = multiprocessing.Pipe()
258
+ with parent_conn, child_conn:
259
+ child_process = multiprocessing.Process(target=child, args=(child_conn,))
260
+ child_process.start()
261
+
262
+ parent_conn.send_bytes(b'Hello child!')
263
+ print(f'Message {parent_conn.recv_bytes()!r} received in parent')
264
+ child_process.join(1)
265
+ ```
266
+ ```
267
+ Message b'Hello child!' received in child
268
+ Message b'Hello parent!' received in parent
269
+ ```
270
+
271
+ ### Socketwrapper for cross-interpreter communication
272
+
273
+ ```py
274
+ import concurrent.futures
275
+ import socketwrapper
276
+
277
+ def child(child_fileno: int) -> None:
278
+ child_writer = socketwrapper.MessageWriter(child_fileno)
279
+ child_writer.send(b'Hello World')
280
+
281
+ if __name__ == '__main__':
282
+ with (socketwrapper.pipe(framing=True) as (parent_reader, child_writer),
283
+ concurrent.futures.InterpreterPoolExecutor() as pool):
284
+ pool.submit(child, child_writer.fileno())
285
+ print(f'Message {parent_reader.recv()!r} received')
286
+ ```
287
+ ```
288
+ Message b'Hello World' received
289
+ ```
290
+
291
+ ### Custom socketwrapper framing with progress
292
+
293
+ ```py
294
+ import collections.abc
295
+ import io
296
+ import itertools
297
+ import os
298
+ import socketwrapper
299
+ import socketwrapper.framing
300
+
301
+ def progress(arrow: str, size: int, min_chunk: int = 1024) -> collections.abc.Generator[int, None, None]:
302
+ part_size = max(min_chunk, size // 100)
303
+ full_parts, last_size = divmod(size, part_size)
304
+ percent = 100 / (full_parts + 1 if last_size else full_parts)
305
+
306
+ for i in range(full_parts):
307
+ print(f'{arrow} {i * percent:6.2f}%')
308
+ yield part_size
309
+
310
+ if last_size:
311
+ print(f'{arrow} {full_parts * percent:6.2f}%')
312
+ yield last_size
313
+
314
+ print(f'{arrow} 100%')
315
+
316
+ class ProgressFraming(socketwrapper.framing.VarIntBytes):
317
+
318
+ @classmethod
319
+ def frames(cls, buffer: io.BytesIO) -> collections.abc.Generator[int, None, None]:
320
+ frames = super().frames(buffer)
321
+ yield from itertools.islice(frames, 2)
322
+ yield from progress('>', next(frames))
323
+
324
+ @classmethod
325
+ def dumps(cls, data: bytes) -> collections.abc.Generator[memoryview, None, None]:
326
+ buffer = memoryview(b''.join(super().dumps(data)))
327
+ for size in progress('<', len(buffer)):
328
+ chunk, buffer = buffer[:size], buffer[size:]
329
+ yield chunk
330
+
331
+ with socketwrapper.socketpair(framing=ProgressFraming) as (parent_duplex, child_duplex):
332
+ child_duplex.inheritable = True
333
+ child_pid = os.fork() # replace with your own multiprocessing fork logic
334
+ child_duplex.inheritable = False # important, prevent socket leaks!
335
+
336
+ if child_pid:
337
+ payload = os.urandom(1024) * 1024
338
+ print(f'Sending {len(payload)} bytes!')
339
+ parent_duplex.send(payload)
340
+ else:
341
+ print(f'Received {len(child_duplex.recv())} bytes!')
342
+ ```
343
+ ```
344
+ Sending 1048576 bytes!
345
+ < 0.00%
346
+ < 0.99%
347
+ < 1.98%
348
+ < 2.97%
349
+ ...
350
+ > 0.99%
351
+ < 13.86%
352
+ > 1.98%
353
+ < 14.85%
354
+ ...
355
+ > 91.09%
356
+ < 99.01%
357
+ > 92.08%
358
+ < 100%
359
+ ...
360
+ > 98.02%
361
+ > 99.01%
362
+ > 100%
363
+ Received 1048576 bytes!
364
+ ```
365
+
366
+ #### Connection handling (`socket.accept`)
367
+
368
+ The connection concept is purposely left out of this library (may change in the future), reasoning:
369
+ - Already existing methods for accepting connections: [socket.accept](https://docs.python.org/3/library/socket.html#socket.socket.accept) and [loop.sock_accept](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.loop.sock_accept), both simple and standard.
370
+ - `socketwrapper` can be already used to wrap the connected socket (thread safe and way simpler than [asyncio Streaming Protocols](https://docs.python.org/3.14/library/asyncio-protocol.html#streaming-protocols)).
371
+ - Server sockets aren't usually shared outside the connection loop between sync and async contexts (unnecessary `CrossLock`).
372
+
373
+ So just wrap accepted connection sockets with `socketwrapper.SocketDuplex`.
374
+
375
+ ```python
376
+ import asyncio
377
+ import socket
378
+ import socketwrapper
379
+
380
+ async def listen(server: socket.socket) -> None:
381
+ loop = asyncio.get_running_loop()
382
+ while True:
383
+ sock, addr = await loop.sock_accept(server)
384
+ with socketwrapper.SocketDuplex(sock) as sock:
385
+ await sock.send_async(b'message')
386
+
387
+ async def main() -> None:
388
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
389
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
390
+ server.bind(('localhost', 8080))
391
+ server.listen(2)
392
+ server.setblocking(False) # asyncio requires a non-blocking server
393
+ listener = asyncio.create_task(listen(server))
394
+
395
+ client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
396
+ client.connect(('localhost', 8080))
397
+ with socketwrapper.SocketDuplex(client) as client:
398
+ print(await client.recv_async())
399
+
400
+ listener.cancel() # stop connection handler
401
+ await asyncio.gather(listener, return_exceptions=True)
402
+ server.close()
403
+
404
+ if __name__ == '__main__':
405
+ asyncio.run(main())
406
+
407
+ ```