- Notifications
You must be signed in to change notification settings - Fork5.7k
Description
Originally proposed in#2388, rewriting here to summarize.
Motivation
ConversationHandler
currently reacts to updates that happenwhile in a state. This means that a callback needs to "prepare" everything that the next state expects to have happend (e.g. send a message the user needs to react to). This can sometimes make it a bit hard to follow the logic of "what needs to be done where" and can also lead to code duplication in the cases where
- multiple states lead to the same next state, thus they all need to do the same preparations
- nested conversations are used that lead back to a parents state thats also reached directly by parent states. Here the child conversation needs to do the same preparations as the parent conversation.
In the setting of nested conversations this also leads to the parent conversation and the child conversation being dependent on each other, making child conversations less reusable for different parents.
Proposed feature
Allow to run code when "entering" a state. This could be either a simplecallback(update, context)
or (which I would prefer) a list of handlers, just like currently forstates
. The logic would then be something along the lines:
defhandle_update(self,update): ...new_state=selected_handler(update,context)ifforhandlerinself.state_entries.get(new_state, []):ifhandler.check_update(update):handler.handle_update(update,context)break
The entry handlers could be specified via aentries
keyword (or whatever naming) that accepts a dictionary just likestates
does.
Of course it should be purely optional, because a) backwards compatibility and b) simple conversations will still do fine without it.
Integration withrun_async
When a state-handler runs asynchronously, we only get a promise and hence don't know the next state. what we could do is add a methodadd_done_callback
toext.utils.Promise
that makes sure the corresponding callback is run whenPromise.run
is done.
This is exactly what's provided byasyncio.Task/Future
, so this has a high chance of not being an issue in the context of#2288.
Current alternatives
What one can currently do to reduce code duplication is ofc to extract the duplicated code to a standalone function and call that wherever needed. In the setting of child conversations that should be reusable by different parents, one can use a factory pattern, i.e. have a method
defbuild_child_conversation(extracted_function_1,extracted_function_2, …)->ConversationHandler: …
that inserts the passed functions into the logic of the child conversation. This allows for the highest level of encapsulation. If that's not needed, one can also just directly use the corresponding functions of the parent conversation directly.
While those soltions a viable, they require some manual work and still lead to some extend of code duplication.
Summary & Things to consider
One can surely argue thatConversationHandler
is already complex enough and that the added benefit is rather small and mostly interesting for very advanced users who like to design their code as clean as possible. On the other hand I can imagine that this can help some users follow the logic ofConversationHandler
more easily and promoting clean code structure is nothing to be ashamed of.
Basically I think this is a reasonable feature to have, but would not be opposed to disregarding it in favor of maintainability.
Ragarding the integration withrun_async
, it may be worth to just wait for v14 to be out, before tackling this (if we decide to do so).