mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Worker API (#2182)
* worker class * worker API tests * tidy * Decorator and more tests * type fix * error order * more tests * remove active message * move worker manager to app * cancel nodes * typing fix * revert change * typing fixes and cleanup * revert typing * test fix * cancel group * Added test for worker * comment * workers docs * Added exit_on_error * changelog * svg * refactor test * remove debug tweaks * docstrings * worker test * fix typing in run * fix 3.7 tests * blog post * fix deadlock test * words * words * words * workers docs * blog post * Apply suggestions from code review Co-authored-by: Dave Pearson <davep@davep.org> * docstring * fix and docstring * Apply suggestions from code review Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Update src/textual/widgets/_markdown.py Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Update src/textual/worker.py Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Fix black * docstring * merge * changelog --------- Co-authored-by: Dave Pearson <davep@davep.org> Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -60,6 +62,8 @@ async def test_installed_screens():
|
||||
|
||||
async def test_screens():
|
||||
app = App()
|
||||
app._loop = asyncio.get_running_loop()
|
||||
app._thread_id = threading.get_ident()
|
||||
# There should be nothing in the children since the app hasn't run yet
|
||||
assert not app._nodes
|
||||
assert not app.children
|
||||
|
||||
33
tests/test_work_decorator.py
Normal file
33
tests/test_work_decorator.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import asyncio
|
||||
|
||||
from textual import work
|
||||
from textual.app import App
|
||||
from textual.worker import Worker, WorkerState
|
||||
|
||||
|
||||
async def test_work() -> None:
|
||||
"""Test basic usage of the @work decorator."""
|
||||
states: list[WorkerState] = []
|
||||
|
||||
class WorkApp(App):
|
||||
worker: Worker
|
||||
|
||||
@work
|
||||
async def foo(self) -> str:
|
||||
await asyncio.sleep(0.1)
|
||||
return "foo"
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.worker = self.foo()
|
||||
|
||||
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
||||
states.append(event.state)
|
||||
|
||||
app = WorkApp()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await app.workers.wait_for_complete()
|
||||
result = await app.worker.wait()
|
||||
assert result == "foo"
|
||||
await pilot.pause()
|
||||
assert states == [WorkerState.PENDING, WorkerState.RUNNING, WorkerState.SUCCESS]
|
||||
155
tests/test_worker.py
Normal file
155
tests/test_worker.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from textual.app import App
|
||||
from textual.worker import (
|
||||
Worker,
|
||||
WorkerCancelled,
|
||||
WorkerFailed,
|
||||
WorkerState,
|
||||
get_current_worker,
|
||||
)
|
||||
|
||||
|
||||
async def test_initialize():
|
||||
"""Test initial values."""
|
||||
|
||||
def foo() -> str:
|
||||
return "foo"
|
||||
|
||||
app = App()
|
||||
async with app.run_test():
|
||||
worker = Worker(app, foo, name="foo", group="foo-group", description="Foo test")
|
||||
repr(worker)
|
||||
|
||||
assert worker.state == WorkerState.PENDING
|
||||
assert not worker.is_cancelled
|
||||
assert not worker.is_running
|
||||
assert not worker.is_finished
|
||||
assert worker.completed_steps == 0
|
||||
assert worker.total_steps is None
|
||||
assert worker.progress == 0.0
|
||||
assert worker.result is None
|
||||
|
||||
|
||||
async def test_run_success() -> None:
|
||||
"""Test successful runs."""
|
||||
|
||||
def foo() -> str:
|
||||
"""Regular function."""
|
||||
return "foo"
|
||||
|
||||
async def bar() -> str:
|
||||
"""Coroutine."""
|
||||
return "bar"
|
||||
|
||||
async def baz() -> str:
|
||||
"""Coroutine."""
|
||||
return "baz"
|
||||
|
||||
class RunApp(App):
|
||||
pass
|
||||
|
||||
app = RunApp()
|
||||
async with app.run_test():
|
||||
# Call regular function
|
||||
foo_worker: Worker[str] = Worker(
|
||||
app, foo, name="foo", group="foo-group", description="Foo test"
|
||||
)
|
||||
# Call coroutine function
|
||||
bar_worker: Worker[str] = Worker(
|
||||
app, bar, name="bar", group="bar-group", description="Bar test"
|
||||
)
|
||||
# Call coroutine
|
||||
baz_worker: Worker[str] = Worker(
|
||||
app, baz(), name="baz", group="baz-group", description="Baz test"
|
||||
)
|
||||
assert foo_worker.result is None
|
||||
assert bar_worker.result is None
|
||||
assert baz_worker.result is None
|
||||
foo_worker._start(app)
|
||||
bar_worker._start(app)
|
||||
baz_worker._start(app)
|
||||
assert await foo_worker.wait() == "foo"
|
||||
assert await bar_worker.wait() == "bar"
|
||||
assert await baz_worker.wait() == "baz"
|
||||
assert foo_worker.result == "foo"
|
||||
assert bar_worker.result == "bar"
|
||||
assert baz_worker.result == "baz"
|
||||
|
||||
|
||||
async def test_run_error() -> None:
|
||||
async def run_error() -> str:
|
||||
await asyncio.sleep(0.1)
|
||||
1 / 0
|
||||
return "Never"
|
||||
|
||||
class ErrorApp(App):
|
||||
pass
|
||||
|
||||
app = ErrorApp()
|
||||
async with app.run_test():
|
||||
worker: Worker[str] = Worker(app, run_error)
|
||||
worker._start(app)
|
||||
with pytest.raises(WorkerFailed):
|
||||
await worker.wait()
|
||||
|
||||
|
||||
async def test_run_cancel() -> None:
|
||||
"""Test run may be cancelled."""
|
||||
|
||||
async def run_error() -> str:
|
||||
await asyncio.sleep(0.1)
|
||||
return "Never"
|
||||
|
||||
class ErrorApp(App):
|
||||
pass
|
||||
|
||||
app = ErrorApp()
|
||||
async with app.run_test():
|
||||
worker: Worker[str] = Worker(app, run_error)
|
||||
worker._start(app)
|
||||
await asyncio.sleep(0)
|
||||
worker.cancel()
|
||||
assert worker.is_cancelled
|
||||
with pytest.raises(WorkerCancelled):
|
||||
await worker.wait()
|
||||
|
||||
|
||||
async def test_run_cancel_immediately() -> None:
|
||||
"""Edge case for cancelling immediately."""
|
||||
|
||||
async def run_error() -> str:
|
||||
await asyncio.sleep(0.1)
|
||||
return "Never"
|
||||
|
||||
class ErrorApp(App):
|
||||
pass
|
||||
|
||||
app = ErrorApp()
|
||||
async with app.run_test():
|
||||
worker: Worker[str] = Worker(app, run_error)
|
||||
worker._start(app)
|
||||
worker.cancel()
|
||||
assert worker.is_cancelled
|
||||
with pytest.raises(WorkerCancelled):
|
||||
await worker.wait()
|
||||
|
||||
|
||||
async def test_get_worker() -> None:
|
||||
"""Check get current worker."""
|
||||
|
||||
async def run_worker() -> Worker:
|
||||
worker = get_current_worker()
|
||||
return worker
|
||||
|
||||
class WorkerApp(App):
|
||||
pass
|
||||
|
||||
app = WorkerApp()
|
||||
async with app.run_test():
|
||||
worker: Worker[Worker] = Worker(app, run_worker)
|
||||
worker._start(app)
|
||||
|
||||
assert await worker.wait() is worker
|
||||
96
tests/test_worker_manager.py
Normal file
96
tests/test_worker_manager.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widget import Widget
|
||||
from textual.worker import Worker, WorkerState
|
||||
|
||||
|
||||
def test_worker_manager_init():
|
||||
app = App()
|
||||
assert isinstance(repr(app.workers), str)
|
||||
assert not bool(app.workers)
|
||||
assert len(app.workers) == 0
|
||||
assert list(app.workers) == []
|
||||
assert list(reversed(app.workers)) == []
|
||||
|
||||
|
||||
async def test_run_worker_async() -> None:
|
||||
"""Check self.run_worker"""
|
||||
worker_events: list[Worker.StateChanged] = []
|
||||
|
||||
work_result: str = ""
|
||||
|
||||
new_worker: Worker
|
||||
|
||||
class WorkerWidget(Widget):
|
||||
async def work(self) -> str:
|
||||
nonlocal work_result
|
||||
await asyncio.sleep(0.02)
|
||||
work_result = "foo"
|
||||
return "foo"
|
||||
|
||||
def on_mount(self):
|
||||
nonlocal new_worker
|
||||
new_worker = self.run_worker(self.work, start=False)
|
||||
|
||||
def on_worker_state_changed(self, event) -> None:
|
||||
worker_events.append(event)
|
||||
|
||||
class WorkerApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield WorkerWidget()
|
||||
|
||||
app = WorkerApp()
|
||||
async with app.run_test():
|
||||
assert new_worker in app.workers
|
||||
assert len(app.workers) == 1
|
||||
app.workers.start_all()
|
||||
await app.workers.wait_for_complete()
|
||||
assert len(app.workers) == 0
|
||||
|
||||
assert work_result == "foo"
|
||||
assert isinstance(worker_events[0].worker.node, WorkerWidget)
|
||||
states = [event.state for event in worker_events]
|
||||
assert states == [
|
||||
WorkerState.PENDING,
|
||||
WorkerState.RUNNING,
|
||||
WorkerState.SUCCESS,
|
||||
]
|
||||
|
||||
|
||||
async def test_run_worker_thread() -> None:
|
||||
"""Check self.run_worker"""
|
||||
worker_events: list[Worker.StateChanged] = []
|
||||
|
||||
work_result: str = ""
|
||||
|
||||
class WorkerWidget(Widget):
|
||||
def work(self) -> str:
|
||||
nonlocal work_result
|
||||
time.sleep(0.02)
|
||||
work_result = "foo"
|
||||
return "foo"
|
||||
|
||||
def on_mount(self):
|
||||
self.run_worker(self.work)
|
||||
|
||||
def on_worker_state_changed(self, event) -> None:
|
||||
worker_events.append(event)
|
||||
|
||||
class WorkerApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield WorkerWidget()
|
||||
|
||||
app = WorkerApp()
|
||||
async with app.run_test():
|
||||
await app.workers.wait_for_complete()
|
||||
|
||||
assert work_result == "foo"
|
||||
assert isinstance(worker_events[0].worker.node, WorkerWidget)
|
||||
states = [event.state for event in worker_events]
|
||||
assert states == [
|
||||
WorkerState.PENDING,
|
||||
WorkerState.RUNNING,
|
||||
WorkerState.SUCCESS,
|
||||
]
|
||||
Reference in New Issue
Block a user