We could have an ABC for synchronous channels
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...
Thoughts?
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_channelprobably shouldn't return aMemoryReceiveChannel-- 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
MemoryReceiveChanneland return that (it sounds like memory channel methods +.close()?) - otherwise, I guess duck type? I guess if you want to pass
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...
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.
.statistics() feels more optional though.
I mentioned .clone() because it was removed from ReceiveChannel's public API. Specifically referencing https://github.com/python-trio/trio/pull/1115 which was informed by https://github.com/python-trio/trio/issues/719.
Concrete proposal: there's a BufferingReceiveChannel (naming is hard...) that inherits ReceiveChannel and adds:
-
receive_nowait -
.close+ sync contextmanager -
.clonemaybe? See https://github.com/python-trio/trio/issues/719 for mention of how it's annoying to implement.
Then MemoryReceiveChannel inherits from that. + similar for BufferingSendChannel, w/ receive_nowait replaced by send_nowait.
(naming it after "buffering" makes it sound really evil though...)