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