Adapt Logic on Getting the Event Loop in Application.run_polling/webhook to Python 3.14 (#4875)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: harshil21 <37377066+harshil21@users.noreply.github.com>
This commit is contained in:
Copilot
2025-08-02 17:18:53 +02:00
committed by GitHub
parent 94da8a8d67
commit cea812dabd
4 changed files with 116 additions and 29 deletions
+4 -1
View File
@@ -97,4 +97,7 @@ pyvenv.cfg
Scripts/
# environment manager:
.mise.toml
.mise.toml
# Support for uv.lock will come in a future PR. See #4796
uv.lock
@@ -0,0 +1,5 @@
bugfixes = "Adapt logic on getting the event loop in ``Application.run_polling/webhook`` to Python 3.14"
[[pull_requests]]
uid = "4875"
author_uid = "harshil21"
closes_threads = ["4874"]
+9 -4
View File
@@ -1025,10 +1025,15 @@ class Application(
bootstrap_retries: int,
close_loop: bool = True,
) -> None:
# Calling get_event_loop() should still be okay even in py3.10+ as long as there is a
# running event loop, or we are in the main thread, which are the intended use cases.
# See the docs of get_event_loop() and get_running_loop() for more info
loop = asyncio.get_event_loop()
# Try to get the running event loop first, and if there isn't one, create a new one.
# This handles the Python 3.14+ behavior where get_event_loop() raises RuntimeError
# when there's no current event loop in the main thread.
try:
loop = asyncio.get_event_loop()
except RuntimeError:
# No running event loop, create and set a new one
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if stop_signals is DEFAULT_NONE and platform.system() != "Windows":
stop_signals = (signal.SIGINT, signal.SIGTERM, signal.SIGABRT)
+98 -24
View File
@@ -1504,7 +1504,7 @@ class TestApplication:
thread.start()
with caplog.at_level(logging.DEBUG):
app.run_polling(drop_pending_updates=True, close_loop=False)
thread.join()
thread.join(timeout=10)
assert len(assertions) == 8
for key, value in assertions.items():
@@ -1558,7 +1558,7 @@ class TestApplication:
thread = Thread(target=thread_target)
thread.start()
app.run_polling(drop_pending_updates=True, close_loop=False)
thread.join()
thread.join(timeout=10)
assert events == ["init", "post_init", "start_polling"], "Wrong order of events detected!"
@pytest.mark.skipif(
@@ -1603,7 +1603,7 @@ class TestApplication:
thread = Thread(target=thread_target)
thread.start()
app.run_polling(drop_pending_updates=True, close_loop=False)
thread.join()
thread.join(timeout=10)
assert events == [
"updater.shutdown",
"shutdown",
@@ -1655,7 +1655,7 @@ class TestApplication:
thread = Thread(target=thread_target)
thread.start()
app.run_polling(drop_pending_updates=True, close_loop=False)
thread.join()
thread.join(timeout=10)
assert events == [
"updater.stop",
"stop",
@@ -1704,7 +1704,7 @@ class TestApplication:
thread = Thread(target=thread_target)
thread.start()
app.run_polling(close_loop=False)
thread.join()
thread.join(timeout=10)
assert set(self.received.keys()) == set(updater_signature.parameters.keys())
for name, param in updater_signature.parameters.items():
@@ -1720,7 +1720,7 @@ class TestApplication:
thread = Thread(target=thread_target)
thread.start()
app.run_polling(close_loop=False, **expected)
thread.join()
thread.join(timeout=10)
assert set(self.received.keys()) == set(updater_signature.parameters.keys())
assert self.received.pop("error_callback", None)
@@ -1779,7 +1779,7 @@ class TestApplication:
drop_pending_updates=True,
close_loop=False,
)
thread.join()
thread.join(timeout=10)
assert len(assertions) == 7
for key, value in assertions.items():
@@ -1844,7 +1844,7 @@ class TestApplication:
drop_pending_updates=True,
close_loop=False,
)
thread.join()
thread.join(timeout=10)
assert events == ["init", "post_init", "start_webhook"], "Wrong order of events detected!"
@pytest.mark.skipif(
@@ -1900,7 +1900,7 @@ class TestApplication:
drop_pending_updates=True,
close_loop=False,
)
thread.join()
thread.join(timeout=10)
assert events == [
"updater.shutdown",
"shutdown",
@@ -1963,7 +1963,7 @@ class TestApplication:
drop_pending_updates=True,
close_loop=False,
)
thread.join()
thread.join(timeout=10)
assert events == [
"updater.stop",
"stop",
@@ -2014,7 +2014,7 @@ class TestApplication:
thread = Thread(target=thread_target)
thread.start()
app.run_webhook(close_loop=False)
thread.join()
thread.join(timeout=10)
assert set(self.received.keys()) == set(updater_signature.parameters.keys()) - {"self"}
for name, param in updater_signature.parameters.items():
@@ -2027,7 +2027,7 @@ class TestApplication:
thread = Thread(target=thread_target)
thread.start()
app.run_webhook(close_loop=False, **expected)
thread.join()
thread.join(timeout=10)
assert set(self.received.keys()) == set(expected.keys())
assert self.received == expected
@@ -2359,8 +2359,7 @@ class TestApplication:
thread.join(timeout=10)
assert not thread.is_alive(), "Test took to long to run. Aborting"
@pytest.mark.flaky(3, 1) # loop.call_later will error the test when a flood error is received
def test_signal_handlers(self, app, monkeypatch):
def test_signal_handlers(self, offline_bot, monkeypatch):
# this test should make sure that signal handlers are set by default on Linux + Mac,
# and not on Windows.
@@ -2368,19 +2367,30 @@ class TestApplication:
def signal_handler_test(*args, **kwargs):
# args[0] is the signal, [1] the callback
received_signals.append(args[0])
received_signals.append(args[1])
app = ApplicationBuilder().bot(offline_bot).application_class(PytestApplication).build()
# Mock the necessary methods to avoid network calls
monkeypatch.setattr(app.bot, "get_updates", empty_get_updates)
monkeypatch.setattr(app.bot, "delete_webhook", return_true)
loop = asyncio.get_event_loop()
monkeypatch.setattr(loop, "add_signal_handler", signal_handler_test)
monkeypatch.setattr(app.bot, "get_updates", empty_get_updates)
monkeypatch.setattr(loop.__class__, "add_signal_handler", signal_handler_test)
def abort_app():
raise SystemExit
# Mock initialize to exit quickly after testing signal handler setup
original_initialize = app.initialize
loop.call_later(0.6, abort_app)
async def quick_initialize(*args, **kwargs):
await original_initialize(*args, **kwargs)
# Exit quickly by raising an exception after successful initialization
raise TelegramError("Test completed successfully")
app.run_polling(close_loop=False)
monkeypatch.setattr(app, "initialize", quick_initialize)
with pytest.raises(TelegramError, match="Test completed successfully"):
app.run_polling(close_loop=False)
if platform.system() == "Windows":
assert received_signals == []
@@ -2388,8 +2398,8 @@ class TestApplication:
assert received_signals == [signal.SIGINT, signal.SIGTERM, signal.SIGABRT]
received_signals.clear()
loop.call_later(0.8, abort_app)
app.run_webhook(port=49152, webhook_url="example.com", close_loop=False)
with pytest.raises(TelegramError, match="Test completed successfully"):
app.run_webhook(port=49152, webhook_url="example.com", close_loop=False)
if platform.system() == "Windows":
assert received_signals == []
@@ -2540,7 +2550,7 @@ class TestApplication:
close_loop=False,
)
thread.join()
thread.join(timeout=10)
assert len(assertions) == 5
for key, value in assertions.items():
@@ -2714,3 +2724,67 @@ class TestApplication:
# check that exactly those handlers were checked that were configured when
# add_error_handler was called
assert called_handlers == {"remove_handler"}
@pytest.mark.skipif(
sys.version_info < (3, 14),
reason="Only relevant for Python 3.14+ where get_event_loop() raises RuntimeError",
)
def test_run_polling_no_event_loop_python314(self, offline_bot, monkeypatch):
"""Test that run_polling works when no event loop exists (Python 3.14+ scenario).
This simulates the Python 3.14+ behavior where get_event_loop() raises RuntimeError
when there's no current event loop in the main thread. The fix should create a new
event loop in this case.
"""
# Track if our test ran and whether any exceptions occurred
exception_captured = None
def thread_target():
nonlocal exception_captured
try:
# Intentionally DON'T set an event loop to simulate Python 3.14 scenario
# Note: the existing test_run_polling_webhook_bootstrap_retries DOES set one
app = (
ApplicationBuilder()
.bot(offline_bot)
.application_class(PytestApplication)
.build()
)
# Mock the necessary methods to avoid network calls
monkeypatch.setattr(app.bot, "get_updates", empty_get_updates)
monkeypatch.setattr(app.bot, "delete_webhook", return_true)
# Mock initialize to exit quickly after testing event loop creation
original_initialize = app.initialize
async def quick_initialize(*args, **kwargs):
await original_initialize(*args, **kwargs)
# Exit quickly by raising an exception after successful initialization
raise TelegramError("Test completed successfully")
monkeypatch.setattr(app, "initialize", quick_initialize)
# This should work - the key is that it creates an event loop and doesn't
# raise RuntimeError about no current event loop (Python 3.14+ issue)
with pytest.raises(TelegramError, match="Test completed successfully"):
app.run_polling(
bootstrap_retries=0,
close_loop=True,
stop_signals=None, # Can't use signals in threads
drop_pending_updates=True,
)
# If we get here, the event loop was created successfully
except Exception as e:
exception_captured = e
thread = Thread(target=thread_target)
thread.start()
thread.join(timeout=10)
assert not thread.is_alive(), "Test took too long to run"
# If there was an unexpected exception, fail the test
if exception_captured:
raise exception_captured