You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
We could have an ABC for synchronous channels #3310
Our RecvChanWrapper class could be a subclass of MemoryReceiveChannel, which would be lovely in that it allows use with synchronous context managers and the .close() method.
(.receive_nowait() is part of the API, but the implementation would have to be simply raise WouldBlock!)
...of course, MemoryReceiveChannel is currently marked @final, which would make this harder. This isn't the only case where I'd like to be able to subclass it though - my async_map(fn, memory_recv_chan) util would be nicer to use if I could return a (subclasses of) memory channel there too. Maybe this points us towards having a more general ReceiveChannelWrapper type, which can be subclassed and customized? Although for type-checkers we'd want that to be a subclass...
Hmm, I'm not entirely convinced MemorySendChannel/MemoryReceiveChannel are useful descriptions of anything other than what they currently are? Like, what are you going to do, send an object synchronously using a socket? (And if you want "cancellation but fast" forceful closure already covers that and I'm not sure making a sync version would be a good concept)
I suppose "send an object synchronously but not have a buffer" does work though. I should think about this a bit more...
I think allowing subclassing is conceptually the wrong approach.
NEVERMIND, I accidentally read "sync context manager" as "sync iterator". My objections don't hold for it, though IMO (3) still applies a bit.
as_safe_channel probably shouldn't return a MemoryReceiveChannel -- the point of the helper is to make things safer, which using it synchronously does not encourage.
having 3 levels of how something can close (synchronously, while cancelled, and gracefully) on an ABC is quite a bit. I guess reality is very nuanced here so having more ways is better, but I would prefer this detail to be chosen on any concrete impls.
if someone wants to write a method and signal (to a typechecker) that it works with anything implementing a sync close + async iteration, protocols work. I'm not sure what story Trio should offer with protocols, as in: should we export some useful ones? For now, we don't.
All that said, I'm simply a -0. It's not like allowing subclassing would be necessarily bad, I would simply rather less API to think about than more. I'm open to being convinced out of my opinions!
EDIT: why do we support closure on as_safe_channel anyways?
(We support closure on as_safe_channel because it's a standard part of the Channel interface, and interop is useful.)
Without subclassing, how can I make async_map(fn, recv_chan) return a MemoryReceiveChannel when passed one, without adding a background nursery which wouldn't otherwise be necessary?
If you could subclass it's pretty easy; just wrap around the original channel and apply the map/filter/etc. in receive_nowait().
I'm not sure whether returning a MemoryReceiveChannel is necessary:
if using a type checker, you could make a protocol of the things you like on MemoryReceiveChannel and return that (it sounds like memory channel methods + .close()?)
otherwise, I guess duck type? I guess if you want to pass isinstance that doesn't work -- is that common? (I wouldn't know...)
I guess it would make sense that isinstance(async_map(fn, recv_chan), type(recv_chan)), but that requires only supporting a single type of channel or dynamically making subclasses (oh no!).
Protocols don't play particularly nicely with type-based protocols for cloudpickle-like serialization, unfortunately.
I'm not aiming for isinstance(async_map(fn, recv_chan), type(recv_chan)), just that if you started with something that could be used in a synchronous context manager or .close() method, you keep that (in practice I'd always return one of two channel-wrapper types). I guess we could therefore add an intermediate type between trio.abc.*Channel and Memory*Channel, which matches the interface of the latter? I do feel like fighting against convenient nominal typing is mostly a losing battle, though.
That may be a better idea, especially if we decide not to match the interface of the latter! I think the logic goes: memory receive channels need a memory send channel -> use open_memory_channel not MemoryReceiveChannel.__init__ -> no public constructor -> no subclassing.
For instance, probably .clone() and .statistics() are bad ideas to keep around, maybe other things too?
I'd definitely want .clone(), it's indispensible for multi-producer or multi-consumer patterns. Also fairly easy to support, so long as the clone stays in the same thread and trio loop.
Counterpoint to all my above thinking: there are actually use cases for subclassing memory channels! Someone on Gitter wanted a channel where they could discard items in the buffer. Obviously we would like them to have everything in memory channels plus this extra new method, but as is they literally cannot do that.
If we actually allowed subclassing memory channels, they would have to rely on implementation details (the attributes of the deque and the parking lot) though, so I'm not completely sure.
Activity
A5rocks commentedon Aug 4, 2025
Hmm, I'm not entirely convinced MemorySendChannel/MemoryReceiveChannel are useful descriptions of anything other than what they currently are? Like, what are you going to do, send an object synchronously using a socket? (And if you want "cancellation but fast" forceful closure already covers that and I'm not sure making a sync version would be a good concept)
I suppose "send an object synchronously but not have a buffer" does work though. I should think about this a bit more...
A5rocks commentedon Aug 4, 2025
I think allowing subclassing is conceptually the wrong approach.
as_safe_channelprobably shouldn't return aMemoryReceiveChannel-- the point of the helper is to make things safer, which using it synchronously does not encourage.All that said, I'm simply a -0. It's not like allowing subclassing would be necessarily bad, I would simply rather less API to think about than more. I'm open to being convinced out of my opinions!
EDIT: why do we support closure on
as_safe_channelanyways?Zac-HD commentedon Aug 20, 2025
(We support closure on
as_safe_channelbecause it's a standard part of the Channel interface, and interop is useful.)Without subclassing, how can I make
async_map(fn, recv_chan)return aMemoryReceiveChannelwhen passed one, without adding a background nursery which wouldn't otherwise be necessary?If you could subclass it's pretty easy; just wrap around the original channel and apply the map/filter/etc. in
receive_nowait().A5rocks commentedon Aug 20, 2025
I'm not sure whether returning a
MemoryReceiveChannelis necessary:MemoryReceiveChanneland return that (it sounds like memory channel methods +.close()?)isinstancethat doesn't work -- is that common? (I wouldn't know...)I guess it would make sense that
isinstance(async_map(fn, recv_chan), type(recv_chan)), but that requires only supporting a single type of channel or dynamically making subclasses (oh no!).I'm still not really seeing the use case...
Zac-HD commentedon Aug 20, 2025
Protocols don't play particularly nicely with type-based protocols for cloudpickle-like serialization, unfortunately.
I'm not aiming for
isinstance(async_map(fn, recv_chan), type(recv_chan)), just that if you started with something that could be used in a synchronous context manager or .close() method, you keep that (in practice I'd always return one of two channel-wrapper types). I guess we could therefore add an intermediate type betweentrio.abc.*ChannelandMemory*Channel, which matches the interface of the latter? I do feel like fighting against convenient nominal typing is mostly a losing battle, though.A5rocks commentedon Aug 20, 2025
That may be a better idea, especially if we decide not to match the interface of the latter! I think the logic goes: memory receive channels need a memory send channel -> use
open_memory_channelnotMemoryReceiveChannel.__init__-> no public constructor -> no subclassing.For instance, probably
.clone()and.statistics()are bad ideas to keep around, maybe other things too?Zac-HD commentedon Aug 20, 2025
I'd definitely want
.clone(), it's indispensible for multi-producer or multi-consumer patterns. Also fairly easy to support, so long as the clone stays in the same thread and trio loop..statistics()feels more optional though.A5rocks commentedon Aug 21, 2025
I mentioned
.clone()because it was removed fromReceiveChannel's public API. Specifically referencing #1115 which was informed by #719.Concrete proposal: there's a
BufferingReceiveChannel(naming is hard...) that inheritsReceiveChanneland adds:receive_nowait.close+ sync contextmanager.clonemaybe? See Fine-tuning channels #719 for mention of how it's annoying to implement.Then
MemoryReceiveChannelinherits from that. + similar forBufferingSendChannel, w/receive_nowaitreplaced bysend_nowait.(naming it after "buffering" makes it sound really evil though...)
[-]`@as_safe_channel` could return a `MemoryReceiveChannel`[/-][+]We could have an ABC for synchronous channels[/+]A5rocks commentedon Mar 4, 2026
Counterpoint to all my above thinking: there are actually use cases for subclassing memory channels! Someone on Gitter wanted a channel where they could discard items in the buffer. Obviously we would like them to have everything in memory channels plus this extra new method, but as is they literally cannot do that.
If we actually allowed subclassing memory channels, they would have to rely on implementation details (the attributes of the deque and the parking lot) though, so I'm not completely sure.