socketwrapper 0.0.3__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 (22) hide show
  1. {socketwrapper-0.0.3/socketwrapper.egg-info → socketwrapper-0.1.0}/PKG-INFO +134 -29
  2. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/README.md +131 -26
  3. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/pyproject.toml +20 -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.3 → socketwrapper-0.1.0}/socketwrapper/_varint.py +6 -1
  9. socketwrapper-0.1.0/socketwrapper/framing.py +406 -0
  10. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/socketwrapper/protocols.py +13 -7
  11. {socketwrapper-0.0.3 → socketwrapper-0.1.0/socketwrapper.egg-info}/PKG-INFO +134 -29
  12. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/socketwrapper.egg-info/SOURCES.txt +2 -0
  13. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/socketwrapper.egg-info/requires.txt +3 -1
  14. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/socketwrapper.egg-info/zip-safe +1 -1
  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.0}/LICENSE +0 -0
  19. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/setup.cfg +0 -0
  20. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/socketwrapper/py.typed +0 -0
  21. {socketwrapper-0.0.3 → socketwrapper-0.1.0}/socketwrapper.egg-info/dependency_links.txt +0 -0
  22. {socketwrapper-0.0.3 → 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.3
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
@@ -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,66 @@ uv pip install 'socketwrapper[msgpack]'
90
109
 
91
110
  ## Changelog
92
111
 
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
+
93
148
  ### 0.0.3 - 2025.11.08
94
149
 
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.
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.
97
154
 
98
155
  ### 0.0.2 - 2025.11.05
99
156
 
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`.
157
+ #### Breaking
158
+
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.
105
172
 
106
173
  ## Documentation
107
174
 
@@ -111,31 +178,26 @@ None other than this README, life's too short and I'm too busy with real life, i
111
178
 
112
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`.
113
180
 
114
- Additional methods defined in `protocols.OptionalSocketLike` can be optionally implemented enabling optimized code paths.
115
-
116
181
  ```py
117
182
  @typing.runtime_checkable
118
183
  class SocketLike(typing.Protocol):
119
184
  """Protocol for socket-like objects accepted by socketwrapper socket classes."""
120
185
 
186
+ def close(self) -> None: ...
121
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: ...
122
190
  def send(self, data: collections.abc.Buffer, /) -> int: ...
123
- def recv(self, bufsize: int, /) -> bytes: ...
124
191
  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: ...
192
+ def get_inheritable(self) -> bool: ...
193
+ def set_inheritable(self, inheritable: bool, /) -> None: ...
133
194
 
134
195
  ```
135
196
 
136
197
  Special attention to:
137
198
  - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
138
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`.
139
201
 
140
202
  ## Usage
141
203
 
@@ -355,3 +417,46 @@ Sending 1048576 bytes!
355
417
  > 100%
356
418
  Received 1048576 bytes!
357
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
+ ```
@@ -1,24 +1,43 @@
1
1
  # socketwrapper
2
2
 
3
- This package provides high level wrappers for sockets and pipes:
3
+ High level wrappers for socket and pipe IO, thread-safe, asyncio-native and supporting multiprocessing.
4
+
5
+ Features:
4
6
 
5
7
  - Thread-safe within both threads and asyncio realms, and between them.
6
8
  - Managed sync `recv` and `send` operations with timeouts.
7
9
  - Native asyncio `recv_async` and `send_async` operations.
8
- - Pluggable message protocol (headered variable length data) supporting variable-length header parsing, payload serialization and deserialization.
9
- - Pluggable I/O protocol (OS file descriptor still required).
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).
10
12
 
11
- Builtin message protocols (framing):
13
+ Built-in message protocols (framing):
12
14
 
13
15
  - `socketwrapper.framing.VarIntBytes`: varint-headered bytes (default with `framing=True`).
14
- - `socketwrapper.framing.MultiprocessingBytes` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
15
- - `socketwrapper.framing.Multiprocessing` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
16
- - `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data stream.
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.
17
35
 
18
36
  ## Motivation
19
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.
20
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.
21
- - Most implementations got either hardcoded messaging protocols or require a fixed-size header.
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).
22
41
  - No implementation was thread-safe between both asyncio and threading realms.
23
42
  - No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
24
43
 
@@ -35,18 +54,66 @@ uv pip install 'socketwrapper[msgpack]'
35
54
 
36
55
  ## Changelog
37
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
+
38
93
  ### 0.0.3 - 2025.11.08
39
94
 
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.
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.
42
99
 
43
100
  ### 0.0.2 - 2025.11.05
44
101
 
45
- - Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
46
- - New: optional msgpack support (`msgpack` extra).
47
- - Fix: busy reads no longer blocking asyncio event loop.
48
- - Typing: expose `SizedBuffer` and deprecate `SendPayload` (removal expected in `0.1.0`).
49
- - Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
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.
50
117
 
51
118
  ## Documentation
52
119
 
@@ -56,31 +123,26 @@ None other than this README, life's too short and I'm too busy with real life, i
56
123
 
57
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`.
58
125
 
59
- Additional methods defined in `protocols.OptionalSocketLike` can be optionally implemented enabling optimized code paths.
60
-
61
126
  ```py
62
127
  @typing.runtime_checkable
63
128
  class SocketLike(typing.Protocol):
64
129
  """Protocol for socket-like objects accepted by socketwrapper socket classes."""
65
130
 
131
+ def close(self) -> None: ...
66
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: ...
67
135
  def send(self, data: collections.abc.Buffer, /) -> int: ...
68
- def recv(self, bufsize: int, /) -> bytes: ...
69
136
  def settimeout(self, timeout: float | None, /) -> None: ...
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: ...
137
+ def get_inheritable(self) -> bool: ...
138
+ def set_inheritable(self, inheritable: bool, /) -> None: ...
78
139
 
79
140
  ```
80
141
 
81
142
  Special attention to:
82
143
  - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
83
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`.
84
146
 
85
147
  ## Usage
86
148
 
@@ -300,3 +362,46 @@ Sending 1048576 bytes!
300
362
  > 100%
301
363
  Received 1048576 bytes!
302
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
+ ```
@@ -5,9 +5,9 @@ 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.3'
8
+ version = '0.1.0'
9
9
  requires-python = '>=3.12'
10
- keywords = ['socket', 'pipe', 'ipc', 'asyncio']
10
+ keywords = ['socket', 'pipe', 'ipc', 'asyncio', 'multiprocessing']
11
11
  readme = { file = 'README.md', content-type = 'text/markdown' }
12
12
  license = { file = 'LICENSE' }
13
13
  classifiers = [
@@ -43,7 +43,7 @@ dev = [
43
43
  'msgpack',
44
44
  'ruff',
45
45
  'wheel',
46
- 'yapf',
46
+ 'uvloop ; sys_platform != "win32"',
47
47
  ]
48
48
 
49
49
  [project.urls]
@@ -61,9 +61,22 @@ packages = [
61
61
  zip-safe = true
62
62
 
63
63
  [tool.ruff]
64
+ exclude = [
65
+ 'examples/*',
66
+ ]
64
67
  line-length = 120
65
68
  target-version = "py312"
66
69
 
70
+ [tool.coverage.run]
71
+ source = [
72
+ "socketwrapper",
73
+ ]
74
+ branch = true
75
+ relative_files = true
76
+
77
+ [tool.coverage.report]
78
+ fail_under = 90
79
+
67
80
  [tool.ruff.lint]
68
81
  preview = true
69
82
  select = [
@@ -136,6 +149,7 @@ ignore = [
136
149
  'SIM105', # replacing every except/pass with contextlib.suppress
137
150
  # Maintainability
138
151
  'RUF022', # unordered __all__ for annotated groups
152
+ 'RUF067', # reexport-only __init__ (WTF!)
139
153
  ]
140
154
 
141
155
  [tool.ruff.lint.flake8-errmsg]
@@ -161,6 +175,9 @@ convention = 'pep257'
161
175
  inline-quotes = 'single'
162
176
  multiline-quotes = 'single'
163
177
 
178
+ [tool.ruff.format]
179
+ quote-style = 'single'
180
+
164
181
  [tool.yapf]
165
182
  indent_closing_brackets = true
166
183
  join_multiple_lines = false