Repository: python-telegram-bot/python-telegram-bot

Issue:
[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!



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)

Resolving patch:
diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py
--- a/telegram/ext/conversationhandler.py
+++ b/telegram/ext/conversationhandler.py
@@ -136,6 +136,15 @@ 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): Pass :obj:`True` to *override* the
+            :attr:`Handler.run_async` setting of all handlers (in :attr:`entry_points`,
+            :attr:`states` and :attr:`fallbacks`).
+
+            Note:
+                If set to :obj:`True`, you should not pass a handler instance, that needs to be
+                run synchronously in another context.
+
+            .. versionadded:: 13.2
 
     Raises:
         ValueError
@@ -168,6 +177,10 @@ 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 :obj:`True`, will override the
+            :attr:`Handler.run_async` setting of all internal handlers on initialization.
+
+            .. versionadded:: 13.2
 
     """
 
@@ -192,8 +205,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 +243,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 +277,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


Test patch:
diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py
--- a/tests/test_conversationhandler.py
+++ b/tests/test_conversationhandler.py
@@ -1429,3 +1429,36 @@ 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,
+            run_async=False,
+        )
+
+        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
diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py
--- a/tests/test_jobqueue.py
+++ b/tests/test_jobqueue.py
@@ -314,7 +314,7 @@ def test_run_monthly(self, job_queue, timezone):
             next_months_days = calendar.monthrange(now.year, now.month + 1)[1]
 
         expected_reschedule_time += dtm.timedelta(this_months_days)
-        if next_months_days < this_months_days:
+        if day > next_months_days:
             expected_reschedule_time += dtm.timedelta(next_months_days)
 
         expected_reschedule_time = timezone.normalize(expected_reschedule_time)