Merge pull request #1615 from Textualize/test-freeze

Fix for test freeze
This commit is contained in:
Will McGugan
2023-01-19 06:28:01 -08:00
committed by GitHub
7 changed files with 79 additions and 11 deletions

View File

@@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed TextLog wrapping issue https://github.com/Textualize/textual/issues/1554
- Fixed issue with TextLog not writing anything before layout https://github.com/Textualize/textual/issues/1498
- Fixed an exception when populating a child class of `ListView` purely from `compose` https://github.com/Textualize/textual/issues/1588
- Fixed freeze in tests https://github.com/Textualize/textual/issues/1608
## [0.9.1] - 2022-12-30

23
src/textual/_asyncio.py Normal file
View File

@@ -0,0 +1,23 @@
"""
Compatibility layer for asyncio.
"""
from __future__ import annotations
import sys
__all__ = ["create_task"]
if sys.version_info >= (3, 8):
from asyncio import create_task
else:
import asyncio
from asyncio import create_task as _create_task
from typing import Awaitable
def create_task(coroutine: Awaitable, *, name: str | None = None) -> asyncio.Task:
"""Schedule the execution of a coroutine object in a spawn task."""
return _create_task(coroutine)

View File

@@ -1,8 +1,6 @@
from __future__ import annotations
import asyncio
from concurrent.futures import Future
from functools import partial
import inspect
import io
import os
@@ -12,8 +10,10 @@ import threading
import unicodedata
import warnings
from asyncio import Task
from concurrent.futures import Future
from contextlib import asynccontextmanager, redirect_stderr, redirect_stdout
from datetime import datetime
from functools import partial
from pathlib import Path, PurePath
from queue import Queue
from time import perf_counter
@@ -41,9 +41,10 @@ from rich.protocol import is_renderable
from rich.segment import Segment, Segments
from rich.traceback import Traceback
from . import actions, Logger, LogGroup, LogVerbosity, events, log, messages
from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages
from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction
from ._ansi_sequences import SYNC_END, SYNC_START
from ._asyncio import create_task
from ._callback import invoke
from ._context import active_app
from ._event_broker import NoHandler, extract_handler_actions
@@ -69,7 +70,6 @@ from .renderables.blank import Blank
from .screen import Screen
from .widget import AwaitMount, Widget
if TYPE_CHECKING:
from .devtools.client import DevtoolsClient
from .pilot import Pilot
@@ -859,7 +859,7 @@ class App(Generic[ReturnType], DOMNode):
)
# Launch the app in the "background"
app_task = asyncio.create_task(run_app(app))
app_task = create_task(run_app(app), name=f"run_test {app}")
# Wait until the app has performed all startup routines.
await app_ready_event.wait()
@@ -914,7 +914,9 @@ class App(Generic[ReturnType], DOMNode):
raise
pilot = Pilot(app)
auto_pilot_task = asyncio.create_task(run_auto_pilot(auto_pilot, pilot))
auto_pilot_task = create_task(
run_auto_pilot(auto_pilot, pilot), name=repr(pilot)
)
try:
await app._process_messages(
@@ -1653,6 +1655,7 @@ class App(Generic[ReturnType], DOMNode):
return []
new_widgets = list(widgets)
if before is not None or after is not None:
# There's a before or after, which means there's going to be an
# insertion, so make it easier to get the new things in the
@@ -1668,6 +1671,11 @@ class App(Generic[ReturnType], DOMNode):
if widget.children:
self._register(widget, *widget.children)
apply_stylesheet(widget)
if not self._running:
# If the app is not running, prevent awaiting of the widget tasks
return []
return list(widgets)
def _unregister(self, widget: Widget) -> None:
@@ -2111,7 +2119,9 @@ class App(Generic[ReturnType], DOMNode):
removed_widgets = self._detach_from_dom(widgets)
finished_event = asyncio.Event()
asyncio.create_task(prune_widgets_task(removed_widgets, finished_event))
create_task(
prune_widgets_task(removed_widgets, finished_event), name="prune nodes"
)
return AwaitRemove(finished_event)

View File

@@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable
from weakref import WeakSet
from . import Logger, events, log, messages
from ._asyncio import create_task
from ._callback import invoke
from ._context import NoActiveAppError, active_app
from ._time import time
@@ -304,7 +305,12 @@ class MessagePump(metaclass=MessagePumpMeta):
def _start_messages(self) -> None:
"""Start messages task."""
if self.app._running:
self._task = asyncio.create_task(self._process_messages())
self._task = create_task(
self._process_messages(), name=f"message pump {self}"
)
else:
self._closing = True
self._closed = True
async def _process_messages(self) -> None:
self._running = True

View File

@@ -7,7 +7,6 @@ Timer objects are created by [set_interval][textual.message_pump.MessagePump.set
from __future__ import annotations
import asyncio
import weakref
from asyncio import CancelledError, Event, Task
from typing import Awaitable, Callable, Union
@@ -15,6 +14,7 @@ from typing import Awaitable, Callable, Union
from rich.repr import Result, rich_repr
from . import _clock, events
from ._asyncio import create_task
from ._callback import invoke
from ._context import active_app
from ._time import sleep
@@ -89,7 +89,7 @@ class Timer:
Returns:
A Task instance for the timer.
"""
self._task = asyncio.create_task(self._run_timer())
self._task = create_task(self._run_timer(), name=self.name)
return self._task
def stop_no_wait(self) -> None:

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from asyncio import Lock, wait
from asyncio import Lock, create_task, wait
from collections import Counter
from fractions import Fraction
@@ -35,6 +36,7 @@ from rich.text import Text
from rich.traceback import Traceback
from . import errors, events, messages
from ._asyncio import create_task
from ._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction
from ._arrange import DockArrangeResult, arrange
from ._context import active_app
@@ -93,7 +95,7 @@ class AwaitMount:
async def await_mount() -> None:
if self._widgets:
aws = [
create_task(widget._mounted_event.wait())
create_task(widget._mounted_event.wait(), name="await mount")
for widget in self._widgets
]
if aws:

26
tests/test_freeze.py Normal file
View File

@@ -0,0 +1,26 @@
import pytest
from textual.app import App
from textual.screen import Screen
from textual.widgets import Footer, Header, Input
class MyScreen(Screen):
def compose(self):
yield Header()
yield Input()
yield Footer()
class MyApp(App):
def on_mount(self):
self.install_screen(MyScreen(), "myscreen")
self.push_screen("myscreen")
async def test_freeze():
"""Regression test for https://github.com/Textualize/textual/issues/1608"""
app = MyApp()
with pytest.raises(Exception):
async with app.run_test():
raise Exception("never raised")