Repository: python-telegram-bot/python-telegram-bot
Pull request: Add run_async parameter to ConversationHandler
- Add `run_async` parameter to `ConversationHandler`
- When `run_async` is set to `True`, overrides all handlers in this `ConversationHandler` with `run_async` equals to `True`
- Closes #2283 

Problem statement:
[FEATURE] ConversationHandler with option to override all handlers' run async option
#### Is your feature request related to a problem? Please describe.
I have a long `ConversationHandler` in which I want all of its handlers to be able to run asynchronously. To achieve this, I have to set every handler in this `ConversationHandler` with `run_async=True`. I found this a bit repetitive and thought that there might be an easier way.

#### Describe the solution you'd like
I think it would be nice if there is an option in `ConversationHandler` like `all_handlers_run_async: bool`. When it is set to `True`, then all the handlers in this `ConversationHandler` would be overridden with `run_async=True`. 

We can also give a bit more flexibility by adding options like `all_entry_points_run_async`, `all_states_run_async` and `all_fallbacks_run_async`, where it only overrides a subset of handlers with `run_async=True`

#### Describe alternatives you've considered
N/A

#### Additional context
N/A

Let me know what you think and Happy New Year!

Hints from issue discussion:
@zeshuaro I like the diversity of this idea and see no downside to that. If you want to have all your handlers async anyway, consider checking out <a href="https://python-telegram-bot.readthedocs.io/en/stable/telegram.ext.defaults.html#telegram.ext.Defaults.run_async">ext.Defaults.run_async</a>.
@Poolitzer Thanks for the suggestion, that might actually solve my problem. I'll check it out!

And if you think it's worth adding the extra options to `ConversationHandler`, I'm more than happy to give it a go.
Hi. I think a `run_async` parameter allowing to make __every__ handler in a CH async would be a great addition. The hanledrs settings should be overridden in `__init__` directly and a corresponding note should be added in the docstring.
If you're up for it, you.re very welcome to PR  @zeshuaro :)

Regarding further specifications like `all_entry_points_run_async`: imho those are rather specific use cases and better suited for ptbcontrib than the main lib. Ofc you're also welcome to PR over there. In that case Pool had the idea to define a class `ConversationHandlerAsyncOptions` (or similar naming) that can be passed to CH containing all the settings instead of adding multiple args to CH (similar to the `Defaults` setup)

Commit message: Add run_async parameter

Patch to review:
diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py
index fdcf8748b5a..0e78d61aa62 100644
--- a/telegram/ext/conversationhandler.py
+++ b/telegram/ext/conversationhandler.py
@@ -99,6 +99,11 @@ class ConversationHandler(Handler[Update]):
         :attr:`END` to end the *parent* conversation from within the nested one. For an example on
         nested :class:`ConversationHandler` s, see our `examples`_.
 
+        Note that setting :attr:`run_async` to :obj:`True` will override the attribute
+        :attr:`run_async` in all the handlers used in this :class:`ConversationHandler` and set
+        them to :obj:`True`. This includes handlers used in :attr:`entry_points`, :attr:`states`,
+        and :attr:`fallback`.
+
     .. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples
 
     Args:
@@ -136,6 +141,9 @@ class ConversationHandler(Handler[Update]):
         map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
             used to instruct a nested conversationhandler to transition into a mapped state on
             its parent conversationhandler in place of a specified nested state.
+        run_async (:obj:`bool`, optional): If set to :obj:`True`, all the handlers in this
+            :class:`ConversationHandler` will be set with :attr:`run_async` equals to :obj:`True`.
+            Default is :obj:`False`.
 
     Raises:
         ValueError
@@ -168,6 +176,8 @@ class ConversationHandler(Handler[Update]):
         map_to_parent (Dict[:obj:`object`, :obj:`object`]): Optional. A :obj:`dict` that can be
             used to instruct a nested conversationhandler to transition into a mapped state on
             its parent conversationhandler in place of a specified nested state.
+        run_async (:obj:`bool`): If set to :obj:`True`, all the handlers in this
+            :class:`ConversationHandler` will be set with :attr:`run_async` equals to :obj:`True`.
 
     """
 
@@ -192,8 +202,9 @@ def __init__(
         name: str = None,
         persistent: bool = False,
         map_to_parent: Dict[object, object] = None,
+        run_async: bool = False,
     ):
-        self.run_async = False
+        self.run_async = run_async
 
         self._entry_points = entry_points
         self._states = states
@@ -229,7 +240,7 @@ def __init__(
                 "since message IDs are not globally unique."
             )
 
-        all_handlers = list()
+        all_handlers: List[Handler] = list()
         all_handlers.extend(entry_points)
         all_handlers.extend(fallbacks)
 
@@ -263,6 +274,10 @@ def __init__(
                     )
                     break
 
+        if self.run_async:
+            for handler in all_handlers:
+                handler.run_async = True
+
     @property
     def entry_points(self) -> List[Handler]:
         return self._entry_points
diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py
index 8436d2c491a..8aa63a5cfa1 100644
--- a/tests/test_conversationhandler.py
+++ b/tests/test_conversationhandler.py
@@ -1429,3 +1429,35 @@ def test_callback(u, c):
         assert self.current_state[user1.id] == self.STOPPING
         assert handler.conversations.get((0, user1.id)) is None
         assert not self.test_flag
+
+    def test_conversation_handler_run_async_true(self, dp):
+        conv_handler = ConversationHandler(
+            entry_points=self.entry_points,
+            states=self.states,
+            fallbacks=self.fallbacks,
+            run_async=True,
+        )
+
+        all_handlers = conv_handler.entry_points + conv_handler.fallbacks
+        for state_handlers in conv_handler.states.values():
+            all_handlers += state_handlers
+
+        for handler in all_handlers:
+            assert handler.run_async
+
+    def test_conversation_handler_run_async_false(self, dp):
+        conv_handler = ConversationHandler(
+            entry_points=[CommandHandler('start', self.start_end, run_async=True)],
+            states=self.states,
+            fallbacks=self.fallbacks,
+        )
+
+        for handler in conv_handler.entry_points:
+            assert handler.run_async
+
+        all_handlers = conv_handler.fallbacks
+        for state_handlers in conv_handler.states.values():
+            all_handlers += state_handlers
+
+        for handler in all_handlers:
+            assert not handler.run_async.value


Code review comments:
diff_hunk: @@ -136,6 +141,9 @@ class ConversationHandler(Handler[Update]):
         map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
             used to instruct a nested conversationhandler to transition into a mapped state on
             its parent conversationhandler in place of a specified nested state.
+        run_async (:obj:`bool`, optional): If set to :obj:`True`, all the handlers in this
+            :class:`ConversationHandler` will be set with :attr:`run_async` equals to :obj:`True`.
path: telegram/ext/conversationhandler.py
text: @user1:
The wording is somewhat hard to understand (same below). suggestion:

```suggestion
        run_async (:obj:`bool`, optional): Pass :obj:`True` to override the :attr:`Handler.run_async` setting of all handlers (in :attr:`entry_points`, :attr:`states` and :attr:`fallbacks:`.
```

I guess this formulation would also make above note unnecessary …

@author:
Updated with new commits