Skip to content

We could have an ABC for synchronous channels #3310

Open

Description

@Zac-HD

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?

Activity

  1. A5rocks commented on Aug 4, 2025

    @A5rocks
    Contributor

    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...

  2. A5rocks commented on Aug 4, 2025

    @A5rocks
    Contributor

    I think allowing subclassing is conceptually the wrong approach.

    1. NEVERMIND, I accidentally read "sync context manager" as "sync iterator". My objections don't hold for it, though IMO (3) still applies a bit.
    2. 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.
    3. 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.
    4. 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?

  3. Zac-HD commented on Aug 20, 2025

    @Zac-HD
    MemberAuthor

    (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().

  4. A5rocks commented on Aug 20, 2025

    @A5rocks
    Contributor

    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!).

    I'm still not really seeing the use case...

  5. Zac-HD commented on Aug 20, 2025

    @Zac-HD
    MemberAuthor

    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.

  6. A5rocks commented on Aug 20, 2025

    @A5rocks
    Contributor

    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?

  7. Zac-HD commented on Aug 20, 2025

    @Zac-HD
    MemberAuthor

    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.

  8. A5rocks commented on Aug 21, 2025

    @A5rocks
    Contributor

    I mentioned .clone() because it was removed from ReceiveChannel's public API. Specifically referencing #1115 which was informed by #719.

    Concrete proposal: there's a BufferingReceiveChannel (naming is hard...) that inherits ReceiveChannel and adds:

    • receive_nowait
    • .close + sync contextmanager
    • .clone maybe? See Fine-tuning channels #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...)

  9. changed the title [-]`@as_safe_channel` could return a `MemoryReceiveChannel`[/-] [+]We could have an ABC for synchronous channels[/+] on Oct 3, 2025
  10. A5rocks commented on Mar 4, 2026

    @A5rocks
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions