socketwrapper 0.0.1__tar.gz → 0.0.2__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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketwrapper
3
- Version: 0.0.1
4
- Summary: high level socket and pipe wrapper
3
+ Version: 0.0.2
4
+ Summary: high level socket and pipe wrappers
5
5
  Author: Felipe A Hernandez
6
6
  Author-email: ergoithz@gmail.com
7
7
  License: MIT License
@@ -41,9 +41,12 @@ Classifier: Topic :: System :: Networking
41
41
  Requires-Python: >=3.12
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
- Requires-Dist: typing-extensions>=4.15; python_version < "3.13"
44
+ Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
45
+ Provides-Extra: msgpack
46
+ Requires-Dist: msgpack; extra == "msgpack"
45
47
  Provides-Extra: dev
46
48
  Requires-Dist: coverage; extra == "dev"
49
+ Requires-Dist: msgpack; extra == "dev"
47
50
  Requires-Dist: ruff; extra == "dev"
48
51
  Requires-Dist: wheel; extra == "dev"
49
52
  Requires-Dist: yapf; extra == "dev"
@@ -52,24 +55,26 @@ Dynamic: license-file
52
55
  # socketwrapper
53
56
 
54
57
  This package provides high level wrappers for sockets and pipes:
55
- - Thread-safe within both and between threads and asyncio realms.
56
- - Managed sync recv and send operations with timeouts.
57
- - Native asyncio recv_async and send_async operations.
58
- - Pluggable message protocol (headered variable length data) supporting
59
- header parsing, serialization and deserialization.
60
- - Pluggable I/O protocol (file descriptor still required due asyncio).
61
58
 
62
- ## Motivation
59
+ - Thread-safe within both threads and asyncio realms, and between them.
60
+ - Managed sync `recv` and `send` operations with timeouts.
61
+ - 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).
63
64
 
64
- There aren't a ton of high level asyncio socket wrappers out there providing
65
- all we need for IPC: header/payload logic and support for both sockets and pipes.
65
+ Builtin message protocols (framing):
66
66
 
67
- Most implementations either don't have pluggable messaging protocols or it
68
- doesn't support variable-size headers.
67
+ - `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.
69
71
 
70
- No other implementation is thread-safe across both asyncio and threading realms.
72
+ ## Motivation
71
73
 
72
- No other implementation directly supports wrapping multiprocessing Connections.
74
+ - 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.
76
+ - No implementation was thread-safe between both asyncio and threading realms.
77
+ - No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
73
78
 
74
79
  ## Installation
75
80
 
@@ -77,18 +82,28 @@ No other implementation directly supports wrapping multiprocessing Connections.
77
82
  uv pip install socketwrapper
78
83
  ```
79
84
 
85
+ Or with optional [msgpack](https://pypi.org/project/msgpack/) support.
86
+ ```sh
87
+ uv pip install 'socketwrapper[msgpack]'
88
+ ```
89
+
90
+ ## Changelog
91
+
92
+ ### v0.0.2 - 2025.11.05
93
+
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`.
99
+
80
100
  ## Documentation
81
101
 
82
- None other than this README, life's too short and I'm too busy with real life,
83
- if you need better documentation consider donating to
84
- [my ko-fi](https://ko-fi.com/s26me) stating that as the tip message,
85
- check out how my docs look like at [mstache docs](https://mstache.readthedocs.io/en/latest/)
86
- and [uactor docs](https://mstache.readthedocs.io/en/latest/).
102
+ 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/).
87
103
 
88
104
  ### Puggable I/O: SocketLike protocol
89
105
 
90
- The `socketwrapper.SocketLike` protocol, an small subset the socket interface,
91
- is all what's required for any object to be wrap-able by `socketwrapper`.
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`.
92
107
 
93
108
  ```py
94
109
  @typing.runtime_checkable
@@ -103,15 +118,8 @@ class SocketLike(typing.Protocol):
103
118
  ```
104
119
 
105
120
  Special attention to:
106
- - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno)
107
- has to be a valid OS file descriptor.
108
- - [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout)
109
- only requires support for `settimeout(.0)` (
110
- [non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)
111
- ), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError)
112
- for any other value is fully supported in which case
113
- [selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector)
114
- will be used for synchronous operations.
121
+ - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
122
+ - [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.
115
123
 
116
124
  ## Usage
117
125
 
@@ -131,11 +139,31 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
131
139
  else:
132
140
  child_writer.send(b'Hello world!')
133
141
  ```
134
-
135
142
  ```
136
143
  Message b'Hello world!' received
137
144
  ```
138
145
 
146
+ ### Simple IPC with pipe using msgpack
147
+
148
+ ```python
149
+ import os
150
+ import socketwrapper
151
+ import socketwrapper.framing as framing
152
+
153
+ with socketwrapper.pipe(framing=framing.MsgPack()) as (parent_reader, child_writer):
154
+ child_writer.inheritable = True
155
+ child_pid = os.fork() # replace with your own process fork/spawn logic
156
+ child_writer.inheritable = False # important, prevent socket leaks!
157
+
158
+ if child_pid:
159
+ print(f'Message {parent_reader.recv()!r} received')
160
+ else:
161
+ child_writer.send({'data': b'Hello world!'})
162
+ ```
163
+ ```
164
+ Message {'data': b'Hello world!'} received
165
+ ```
166
+
139
167
  ### Simple asyncio IPC with pipe
140
168
 
141
169
  ```python
@@ -156,7 +184,6 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
156
184
 
157
185
  asyncio.run(parent(parent_reader) if child_pid else child(child_writer))
158
186
  ```
159
-
160
187
  ```
161
188
  Message b'Hello world!' received
162
189
  ```
@@ -180,7 +207,6 @@ with socketwrapper.socketpair(framing=True) as (parent_duplex, child_duplex):
180
207
  print(f'Message {child_duplex.recv()!r} received in child')
181
208
  child_duplex.send(b'Hello parent!')
182
209
  ```
183
-
184
210
  ```
185
211
  Message b'Hello child!' received in child
186
212
  Message b'Hello parent!' received in parent
@@ -204,25 +230,46 @@ def child(conn: multiprocessing.connection.Connection) -> None:
204
230
 
205
231
  asyncio.run(main())
206
232
 
207
- parent_conn, child_conn = multiprocessing.Pipe()
208
- with parent_conn, child_conn:
209
- child_process = multiprocessing.Process(target=child, args=(child_conn,))
210
- child_process.start()
233
+ if __name__ == '__main__':
234
+ parent_conn, child_conn = multiprocessing.Pipe()
235
+ with parent_conn, child_conn:
236
+ child_process = multiprocessing.Process(target=child, args=(child_conn,))
237
+ child_process.start()
211
238
 
212
- parent_conn.send_bytes(b'Hello child!')
213
- print(f'Message {parent_conn.recv_bytes()!r} received in parent')
214
- child_process.join(1)
239
+ parent_conn.send_bytes(b'Hello child!')
240
+ print(f'Message {parent_conn.recv_bytes()!r} received in parent')
241
+ child_process.join(1)
215
242
  ```
216
-
217
243
  ```
218
244
  Message b'Hello child!' received in child
219
245
  Message b'Hello parent!' received in parent
220
246
  ```
221
247
 
248
+ ### Socketwrapper for cross-interpreter communication
249
+
250
+ ```py
251
+ import concurrent.futures
252
+ import socketwrapper
253
+
254
+ def child(child_fileno: int) -> None:
255
+ child_writer = socketwrapper.MessageWriter(child_fileno)
256
+ child_writer.send(b'Hello World')
257
+
258
+ if __name__ == '__main__':
259
+ with (socketwrapper.pipe(framing=True) as (parent_reader, child_writer),
260
+ concurrent.futures.InterpreterPoolExecutor() as pool):
261
+ pool.submit(child, child_writer.fileno())
262
+ print(f'Message {parent_reader.recv()!r} received')
263
+ ```
264
+ ```
265
+ Message b'Hello World' received
266
+ ```
267
+
222
268
  ### Custom socketwrapper framing with progress
223
269
 
224
270
  ```py
225
271
  import collections.abc
272
+ import io
226
273
  import itertools
227
274
  import os
228
275
  import socketwrapper
@@ -246,7 +293,7 @@ def progress(arrow: str, size: int, min_chunk: int = 1024) -> collections.abc.Ge
246
293
  class ProgressFraming(socketwrapper.framing.VarIntBytes):
247
294
 
248
295
  @classmethod
249
- def frames(cls, buffer: bytearray) -> collections.abc.Generator[int, None, None]:
296
+ def frames(cls, buffer: io.BytesIO) -> collections.abc.Generator[int, None, None]:
250
297
  frames = super().frames(buffer)
251
298
  yield from itertools.islice(frames, 2)
252
299
  yield from progress('>', next(frames))
@@ -1,24 +1,26 @@
1
1
  # socketwrapper
2
2
 
3
3
  This package provides high level wrappers for sockets and pipes:
4
- - Thread-safe within both and between threads and asyncio realms.
5
- - Managed sync recv and send operations with timeouts.
6
- - Native asyncio recv_async and send_async operations.
7
- - Pluggable message protocol (headered variable length data) supporting
8
- header parsing, serialization and deserialization.
9
- - Pluggable I/O protocol (file descriptor still required due asyncio).
10
4
 
11
- ## Motivation
5
+ - Thread-safe within both threads and asyncio realms, and between them.
6
+ - Managed sync `recv` and `send` operations with timeouts.
7
+ - 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).
12
10
 
13
- There aren't a ton of high level asyncio socket wrappers out there providing
14
- all we need for IPC: header/payload logic and support for both sockets and pipes.
11
+ Builtin message protocols (framing):
15
12
 
16
- Most implementations either don't have pluggable messaging protocols or it
17
- doesn't support variable-size headers.
13
+ - `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.
18
17
 
19
- No other implementation is thread-safe across both asyncio and threading realms.
18
+ ## Motivation
20
19
 
21
- No other implementation directly supports wrapping multiprocessing Connections.
20
+ - 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.
22
+ - No implementation was thread-safe between both asyncio and threading realms.
23
+ - No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
22
24
 
23
25
  ## Installation
24
26
 
@@ -26,18 +28,28 @@ No other implementation directly supports wrapping multiprocessing Connections.
26
28
  uv pip install socketwrapper
27
29
  ```
28
30
 
31
+ Or with optional [msgpack](https://pypi.org/project/msgpack/) support.
32
+ ```sh
33
+ uv pip install 'socketwrapper[msgpack]'
34
+ ```
35
+
36
+ ## Changelog
37
+
38
+ ### v0.0.2 - 2025.11.05
39
+
40
+ - Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
41
+ - New: optional msgpack support (`msgpack` extra).
42
+ - Fix: busy reads no longer blocking asyncio event loop.
43
+ - Typing: expose `SizedBuffer` and deprecate `SendPayload`.
44
+ - Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
45
+
29
46
  ## Documentation
30
47
 
31
- None other than this README, life's too short and I'm too busy with real life,
32
- if you need better documentation consider donating to
33
- [my ko-fi](https://ko-fi.com/s26me) stating that as the tip message,
34
- check out how my docs look like at [mstache docs](https://mstache.readthedocs.io/en/latest/)
35
- and [uactor docs](https://mstache.readthedocs.io/en/latest/).
48
+ 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/).
36
49
 
37
50
  ### Puggable I/O: SocketLike protocol
38
51
 
39
- The `socketwrapper.SocketLike` protocol, an small subset the socket interface,
40
- is all what's required for any object to be wrap-able by `socketwrapper`.
52
+ The `socketwrapper.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
41
53
 
42
54
  ```py
43
55
  @typing.runtime_checkable
@@ -52,15 +64,8 @@ class SocketLike(typing.Protocol):
52
64
  ```
53
65
 
54
66
  Special attention to:
55
- - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno)
56
- has to be a valid OS file descriptor.
57
- - [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout)
58
- only requires support for `settimeout(.0)` (
59
- [non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)
60
- ), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError)
61
- for any other value is fully supported in which case
62
- [selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector)
63
- will be used for synchronous operations.
67
+ - [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
68
+ - [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.
64
69
 
65
70
  ## Usage
66
71
 
@@ -80,11 +85,31 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
80
85
  else:
81
86
  child_writer.send(b'Hello world!')
82
87
  ```
83
-
84
88
  ```
85
89
  Message b'Hello world!' received
86
90
  ```
87
91
 
92
+ ### Simple IPC with pipe using msgpack
93
+
94
+ ```python
95
+ import os
96
+ import socketwrapper
97
+ import socketwrapper.framing as framing
98
+
99
+ with socketwrapper.pipe(framing=framing.MsgPack()) as (parent_reader, child_writer):
100
+ child_writer.inheritable = True
101
+ child_pid = os.fork() # replace with your own process fork/spawn logic
102
+ child_writer.inheritable = False # important, prevent socket leaks!
103
+
104
+ if child_pid:
105
+ print(f'Message {parent_reader.recv()!r} received')
106
+ else:
107
+ child_writer.send({'data': b'Hello world!'})
108
+ ```
109
+ ```
110
+ Message {'data': b'Hello world!'} received
111
+ ```
112
+
88
113
  ### Simple asyncio IPC with pipe
89
114
 
90
115
  ```python
@@ -105,7 +130,6 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
105
130
 
106
131
  asyncio.run(parent(parent_reader) if child_pid else child(child_writer))
107
132
  ```
108
-
109
133
  ```
110
134
  Message b'Hello world!' received
111
135
  ```
@@ -129,7 +153,6 @@ with socketwrapper.socketpair(framing=True) as (parent_duplex, child_duplex):
129
153
  print(f'Message {child_duplex.recv()!r} received in child')
130
154
  child_duplex.send(b'Hello parent!')
131
155
  ```
132
-
133
156
  ```
134
157
  Message b'Hello child!' received in child
135
158
  Message b'Hello parent!' received in parent
@@ -153,25 +176,46 @@ def child(conn: multiprocessing.connection.Connection) -> None:
153
176
 
154
177
  asyncio.run(main())
155
178
 
156
- parent_conn, child_conn = multiprocessing.Pipe()
157
- with parent_conn, child_conn:
158
- child_process = multiprocessing.Process(target=child, args=(child_conn,))
159
- child_process.start()
179
+ if __name__ == '__main__':
180
+ parent_conn, child_conn = multiprocessing.Pipe()
181
+ with parent_conn, child_conn:
182
+ child_process = multiprocessing.Process(target=child, args=(child_conn,))
183
+ child_process.start()
160
184
 
161
- parent_conn.send_bytes(b'Hello child!')
162
- print(f'Message {parent_conn.recv_bytes()!r} received in parent')
163
- child_process.join(1)
185
+ parent_conn.send_bytes(b'Hello child!')
186
+ print(f'Message {parent_conn.recv_bytes()!r} received in parent')
187
+ child_process.join(1)
164
188
  ```
165
-
166
189
  ```
167
190
  Message b'Hello child!' received in child
168
191
  Message b'Hello parent!' received in parent
169
192
  ```
170
193
 
194
+ ### Socketwrapper for cross-interpreter communication
195
+
196
+ ```py
197
+ import concurrent.futures
198
+ import socketwrapper
199
+
200
+ def child(child_fileno: int) -> None:
201
+ child_writer = socketwrapper.MessageWriter(child_fileno)
202
+ child_writer.send(b'Hello World')
203
+
204
+ if __name__ == '__main__':
205
+ with (socketwrapper.pipe(framing=True) as (parent_reader, child_writer),
206
+ concurrent.futures.InterpreterPoolExecutor() as pool):
207
+ pool.submit(child, child_writer.fileno())
208
+ print(f'Message {parent_reader.recv()!r} received')
209
+ ```
210
+ ```
211
+ Message b'Hello World' received
212
+ ```
213
+
171
214
  ### Custom socketwrapper framing with progress
172
215
 
173
216
  ```py
174
217
  import collections.abc
218
+ import io
175
219
  import itertools
176
220
  import os
177
221
  import socketwrapper
@@ -195,7 +239,7 @@ def progress(arrow: str, size: int, min_chunk: int = 1024) -> collections.abc.Ge
195
239
  class ProgressFraming(socketwrapper.framing.VarIntBytes):
196
240
 
197
241
  @classmethod
198
- def frames(cls, buffer: bytearray) -> collections.abc.Generator[int, None, None]:
242
+ def frames(cls, buffer: io.BytesIO) -> collections.abc.Generator[int, None, None]:
199
243
  frames = super().frames(buffer)
200
244
  yield from itertools.islice(frames, 2)
201
245
  yield from progress('>', next(frames))
@@ -4,8 +4,8 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'socketwrapper'
7
- description = 'high level socket and pipe wrapper'
8
- version = '0.0.1'
7
+ description = 'high level socket and pipe wrappers'
8
+ version = '0.0.2'
9
9
  requires-python = '>=3.12'
10
10
  keywords = ['socket', 'pipe', 'ipc', 'asyncio']
11
11
  readme = { file = 'README.md', content-type = 'text/markdown' }
@@ -22,7 +22,7 @@ authors = [
22
22
  { name = 'Felipe A Hernandez' },
23
23
  { email = 'ergoithz@gmail.com' },
24
24
  ]
25
- dependencies = ['typing-extensions >= 4.15 ; python_version < "3.13"']
25
+ dependencies = ['typing-extensions >= 4.6 ; python_version < "3.13"']
26
26
 
27
27
  [tool.setuptools.package-data]
28
28
  socketwrapper = ["py.typed"]
@@ -37,8 +37,10 @@ exclude = [
37
37
  pythonVersion = '3.12'
38
38
 
39
39
  [project.optional-dependencies]
40
+ msgpack = ['msgpack']
40
41
  dev = [
41
42
  'coverage',
43
+ 'msgpack',
42
44
  'ruff',
43
45
  'wheel',
44
46
  'yapf',
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import collections.abc
5
+ import io
5
6
  import os
6
7
  import selectors
7
8
  import socket
@@ -22,8 +23,8 @@ from . import _utils, framing, protocols
22
23
  __all__ = (
23
24
  # type aliases and protocols
24
25
  'MessageFraming',
25
- 'RecvSize',
26
- 'SendPayload',
26
+ 'SendPayload', # FIXME(docs): deprecated since 0.0.2
27
+ 'SizedBuffer',
27
28
  'SocketLike',
28
29
  'SocketOrDescriptor',
29
30
 
@@ -47,10 +48,11 @@ __all__ = (
47
48
  DEFAULT_SOCKETPAIR_FAMILY = getattr(socket, 'AF_UNIX', socket.AF_INET)
48
49
  DEFAULT_FRAMING = framing.VarIntBytes
49
50
 
50
-
51
51
  SocketLike = protocols.SocketLike
52
52
  MessageFraming = protocols.MessageFraming
53
53
  SocketOrDescriptor = SocketLike | protocols.HasFileno | int
54
+ SizedBuffer = protocols.SizedBuffer
55
+
54
56
  RecvSize = _utils.RecvSize
55
57
  SendPayload = _utils.SendPayload
56
58
 
@@ -137,7 +139,7 @@ class SocketWrapper(_utils.ClosingContext):
137
139
  """Configure socket and consume processor."""
138
140
  timeout = _utils.timeout_checker(deadline, 'timeout during transmission') if deadline else None
139
141
  wait = self._sock.settimeout if timeout else None
140
- if self._duplex or _utils.capture(ValueError, self._sock.settimeout, timeout() if timeout else None):
142
+ if self._duplex or not _utils.safe_timeout(self._sock, timeout() if timeout else None):
141
143
  self._sock.settimeout(.0)
142
144
  wait = self._selector(write=write)
143
145
 
@@ -155,33 +157,23 @@ class SocketWrapper(_utils.ClosingContext):
155
157
 
156
158
  async def _consume_async(self, processor: collections.abc.Iterable, *, write: bool = False) -> None:
157
159
  """Configure socket as non-blocking and consume processor."""
158
-
159
- def notify() -> None:
160
- """Pass buffer to target and update future (and clean up) on success or error."""
161
- try:
162
- if next(iterator, True):
163
- future.set_result(None)
164
- remove_io(self)
165
-
166
- except asyncio.InvalidStateError:
167
- remove_io(self)
168
-
169
- except Exception as e:
170
- future.set_exception(e)
171
- remove_io(self)
172
-
173
160
  loop = asyncio.get_event_loop()
174
- future = loop.create_future()
175
- iterator = iter(processor)
176
161
  add_io, remove_io = (
177
162
  (loop.add_writer, loop.remove_writer) if write else
178
163
  (loop.add_reader, loop.remove_reader)
179
164
  )
180
165
 
181
166
  self._sock.settimeout(.0)
182
- add_io(self, notify)
167
+ event = asyncio.Event()
168
+ add_io(self, event.set)
183
169
  try:
184
- await future
170
+ for empty in processor:
171
+ if empty:
172
+ event.clear()
173
+ await event.wait()
174
+
175
+ else:
176
+ await asyncio.sleep(0)
185
177
 
186
178
  finally:
187
179
  remove_io(self)
@@ -197,29 +189,29 @@ class SocketReader(SocketWrapper):
197
189
  super().__init__(sock)
198
190
  self._rlock = _utils.CrossLock()
199
191
 
200
- def _recv(self, bufsize: RecvSize, timeout: float | None = None) -> bytearray:
192
+ def _recv(self, bufsize: RecvSize, timeout: float | None = None) -> io.BytesIO:
201
193
  """Receive data from socket as bytearray."""
202
194
  with _utils.lock_timeout(self._rlock, timeout=timeout) as deadline:
203
- buffer = bytearray()
195
+ buffer = io.BytesIO()
204
196
  reader = _utils.reader(self._sock, buffer, bufsize)
205
197
  self._consume(reader, deadline)
206
198
  return buffer
207
199
 
208
- async def _recv_async(self, bufsize: RecvSize) -> bytearray:
200
+ async def _recv_async(self, bufsize: RecvSize) -> io.BytesIO:
209
201
  """Receive data from socket (async) as bytearray."""
210
202
  async with self._rlock:
211
- buffer = bytearray()
212
- reader = _utils.reader(self._sock, buffer, bufsize)
203
+ buffer = io.BytesIO()
204
+ reader = _utils.reader(self._sock, buffer, bufsize, throttle=True)
213
205
  await self._consume_async(reader)
214
206
  return buffer
215
207
 
216
208
  def recv(self, bufsize: int, timeout: float | None = None) -> bytes:
217
209
  """Receive data from socket as bytes."""
218
- return bytes(self._recv(bufsize, timeout))
210
+ return self._recv(bufsize, timeout).getvalue()
219
211
 
220
212
  async def recv_async(self, bufsize: int) -> bytes:
221
213
  """Receive data from socket (async) as bytes."""
222
- return bytes(await self._recv_async(bufsize))
214
+ return (await self._recv_async(bufsize)).getvalue()
223
215
 
224
216
 
225
217
  class SocketWriter(SocketWrapper):
@@ -232,18 +224,26 @@ class SocketWriter(SocketWrapper):
232
224
  super().__init__(sock)
233
225
  self._wlock = _utils.CrossLock()
234
226
 
235
- def send(self, data: SendPayload, timeout: float | None = None) -> None:
227
+ def _send(self, data: SendPayload, timeout: float | None = None) -> None:
236
228
  """Send data to socket."""
237
229
  with _utils.lock_timeout(self._wlock, timeout=timeout) as deadline:
238
230
  writer = _utils.writer(self._sock, data)
239
231
  self._consume(writer, deadline, write=True)
240
232
 
241
- async def send_async(self, data: SendPayload) -> None:
233
+ async def _send_async(self, data: SendPayload) -> None:
242
234
  """Send data to socket (async)."""
243
235
  async with self._wlock:
244
- writer = _utils.writer(self._sock, data)
236
+ writer = _utils.writer(self._sock, data, throttle=True)
245
237
  await self._consume_async(writer, write=True)
246
238
 
239
+ def send(self, data: SizedBuffer, timeout: float | None = None) -> None:
240
+ """Send data to socket."""
241
+ self._send(data, timeout=timeout)
242
+
243
+ async def send_async(self, data: SizedBuffer) -> None:
244
+ """Send data to socket (async)."""
245
+ await self._send_async(data)
246
+
247
247
 
248
248
  class SocketDuplex(SocketWriter, SocketReader):
249
249
  """Duplex (both readable and writable) socket wrapper."""
@@ -311,11 +311,11 @@ class MessageWriter(MessageWrapper[typing.Any, W, KSW], typing_extensions.Generi
311
311
 
312
312
  def send(self, data: W, timeout: float | None = None) -> None:
313
313
  """Send data to socket."""
314
- self._raw.send(self.framing.dumps(data), timeout=timeout)
314
+ self._raw._send(self.framing.dumps(data), timeout=timeout)
315
315
 
316
316
  async def send_async(self, data: W) -> None:
317
317
  """Send data to socket (async)."""
318
- await self._raw.send_async(self.framing.dumps(data))
318
+ await self._raw._send_async(self.framing.dumps(data))
319
319
 
320
320
 
321
321
  class MessageDuplex[R, W](MessageWriter[W, SocketDuplex], MessageReader[R, SocketDuplex]):