Skip to content

Commit 6d12c7f

Browse files
authored
Merge pull request #37 from benoitc/feature/channel-close-method
Add close() method to Channel and ByteChannel
2 parents ea7bdf0 + b5d7932 commit 6d12c7f

4 files changed

Lines changed: 114 additions & 0 deletions

File tree

c_src/py_callback.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2856,6 +2856,38 @@ static PyObject *erlang_channel_is_closed_impl(PyObject *self, PyObject *args) {
28562856
}
28572857
}
28582858

2859+
/**
2860+
* @brief Close a channel from Python
2861+
*
2862+
* Usage: erlang._channel_close(channel_ref)
2863+
* Returns: True on success
2864+
* Raises: TypeError if invalid reference
2865+
*/
2866+
static PyObject *erlang_channel_close_impl(PyObject *self, PyObject *args) {
2867+
(void)self;
2868+
PyObject *capsule;
2869+
2870+
if (!PyArg_ParseTuple(args, "O", &capsule)) {
2871+
return NULL;
2872+
}
2873+
2874+
if (!PyCapsule_CheckExact(capsule)) {
2875+
PyErr_SetString(PyExc_TypeError, "expected channel reference");
2876+
return NULL;
2877+
}
2878+
2879+
py_channel_t *channel = (py_channel_t *)PyCapsule_GetPointer(capsule, CHANNEL_CAPSULE_NAME);
2880+
if (channel == NULL) {
2881+
PyErr_SetString(PyExc_ValueError, "invalid channel reference");
2882+
return NULL;
2883+
}
2884+
2885+
/* Close the channel - this wakes any waiting receivers */
2886+
channel_close(channel);
2887+
2888+
Py_RETURN_TRUE;
2889+
}
2890+
28592891
/* ============================================================================
28602892
* ByteChannel Methods (raw bytes, no term conversion)
28612893
* ============================================================================ */
@@ -3328,6 +3360,10 @@ static PyMethodDef ErlangModuleMethods[] = {
33283360
"Check if channel is closed.\n"
33293361
"Usage: erlang._channel_is_closed(channel_ref)\n"
33303362
"Returns: True if closed, False otherwise."},
3363+
{"_channel_close", erlang_channel_close_impl, METH_VARARGS,
3364+
"Close a channel.\n"
3365+
"Usage: erlang._channel_close(channel_ref)\n"
3366+
"Returns: True. Wakes any waiting receivers."},
33313367
/* ByteChannel methods (raw bytes, no term conversion) */
33323368
{"_byte_channel_try_receive_bytes", erlang_byte_channel_try_receive_bytes_impl, METH_VARARGS,
33333369
"ByteChannel receive (non-blocking, raw bytes).\n"

docs/channel.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,27 @@ msg = await ch.async_receive()
172172

173173
**Raises:** `ChannelClosed` when the channel is closed.
174174

175+
#### `close()`
176+
177+
Close the channel from Python. Wakes any waiting receivers.
178+
179+
```python
180+
ch.close() # Signal no more data will be sent
181+
```
182+
183+
Safe to call multiple times.
184+
185+
#### Context Manager
186+
187+
Channels support the `with` statement for automatic cleanup:
188+
189+
```python
190+
with Channel(channel_ref) as ch:
191+
for msg in ch:
192+
process(msg)
193+
# channel automatically closed on exit
194+
```
195+
175196
#### Iteration
176197

177198
```python
@@ -461,6 +482,15 @@ def process_bytes(channel_ref):
461482

462483
# Send bytes back
463484
ch.send_bytes(b"response data")
485+
486+
# Close when done
487+
ch.close()
488+
489+
# Or use context manager for automatic cleanup
490+
with ByteChannel(channel_ref) as ch:
491+
for chunk in ch:
492+
process(chunk)
493+
# channel automatically closed
464494
```
465495

466496
### Async Python API

priv/_erlang_impl/_byte_channel.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,5 +307,29 @@ def _is_closed(self) -> bool:
307307
except Exception:
308308
return True
309309

310+
def close(self):
311+
"""Close the byte channel.
312+
313+
Closes the channel and wakes any waiting receivers with ByteChannelClosed.
314+
Safe to call multiple times - subsequent calls have no effect.
315+
316+
Example:
317+
byte_channel.close() # Signal no more data
318+
"""
319+
import erlang
320+
try:
321+
erlang._channel_close(self._ref)
322+
except Exception:
323+
pass # Ignore errors on close (may already be closed)
324+
325+
def __enter__(self):
326+
"""Context manager entry - returns self."""
327+
return self
328+
329+
def __exit__(self, exc_type, exc_val, exc_tb):
330+
"""Context manager exit - closes the channel."""
331+
self.close()
332+
return False
333+
310334
def __repr__(self):
311335
return f"<ByteChannel ref={self._ref!r}>"

priv/_erlang_impl/_channel.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,30 @@ def _is_closed(self):
286286
except Exception:
287287
return True
288288

289+
def close(self):
290+
"""Close the channel.
291+
292+
Closes the channel and wakes any waiting receivers with ChannelClosed.
293+
Safe to call multiple times - subsequent calls have no effect.
294+
295+
Example:
296+
channel.close() # Signal no more data
297+
"""
298+
import erlang
299+
try:
300+
erlang._channel_close(self._ref)
301+
except Exception:
302+
pass # Ignore errors on close (may already be closed)
303+
304+
def __enter__(self):
305+
"""Context manager entry - returns self."""
306+
return self
307+
308+
def __exit__(self, exc_type, exc_val, exc_tb):
309+
"""Context manager exit - closes the channel."""
310+
self.close()
311+
return False
312+
289313
def __repr__(self):
290314
return f"<Channel ref={self._ref!r}>"
291315

0 commit comments

Comments
 (0)