Skip to content

Commit 2f2f314

Browse files
committed
update titles
1 parent 54cec67 commit 2f2f314

File tree

6 files changed

+310
-1
lines changed

6 files changed

+310
-1
lines changed

docs/api/await_complete.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,13 @@
22
title: "textual.await_complete"
33
---
44

5+
This object is returned by methods that do work in the *background*.
6+
You can await the return value if you need to know when that work has completed.
7+
If you ignore it, Textual will wait for the work to be done before handling the next message.
8+
9+
!!! note
10+
11+
You are unlikely to need to explicitly create these objects yourself.
12+
13+
514
::: textual.await_complete

docs/api/await_remove.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,14 @@
22
title: "textual.await_remove"
33
---
44

5+
6+
This object is returned by [`Widget.remove()`][textual.widget.Widget.remove], and other methods which remove widgets.
7+
You can await the return value if you need to know exactly when the widgets have been removed.
8+
If you ignore it, Textual will wait for the widgets to be removed before handling the next message.
9+
10+
!!! note
11+
12+
You are unlikely to need to explicitly create these objects yourself.
13+
14+
515
::: textual.await_remove

docs/guide/workers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ You can also create workers which will *not* immediately exit on exception, by s
100100

101101
### Worker lifetime
102102

103-
Workers are managed by a single [WorkerManager][textual._worker_manager.WorkerManager] instance, which you can access via `app.workers`.
103+
Workers are managed by a single [WorkerManager][textual.worker_manager.WorkerManager] instance, which you can access via `app.workers`.
104104
This is a container-like object which you iterate over to see your active workers.
105105

106106
Workers are tied to the DOM node (widget, screen, or app) where they are created.

src/textual/map_geometry.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from typing import NamedTuple
4+
5+
from textual.geometry import Region, Size, Spacing
6+
7+
8+
class MapGeometry(NamedTuple):
9+
"""Defines the absolute location of a Widget."""
10+
11+
region: Region
12+
"""The (screen) [region][textual.geometry.Region] occupied by the widget."""
13+
order: tuple[tuple[int, int, int], ...]
14+
"""Tuple of tuples defining the painting order of the widget.
15+
16+
Each successive triple represents painting order information with regards to
17+
ancestors in the DOM hierarchy and the last triple provides painting order
18+
information for this specific widget.
19+
"""
20+
clip: Region
21+
"""A [region][textual.geometry.Region] to clip the widget by (if a Widget is within a container)."""
22+
virtual_size: Size
23+
"""The virtual [size][textual.geometry.Size] (scrollable area) of a widget if it is a container."""
24+
container_size: Size
25+
"""The container [size][textual.geometry.Size] (area not occupied by scrollbars)."""
26+
virtual_region: Region
27+
"""The [region][textual.geometry.Region] relative to the container (but not necessarily visible)."""
28+
dock_gutter: Spacing
29+
"""Space from the container reserved by docked widgets."""
30+
31+
@property
32+
def visible_region(self) -> Region:
33+
"""The Widget region after clipping."""
34+
return self.clip.intersection(self.region)

src/textual/system_commands.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""A command palette command provider for Textual system commands.
2+
3+
This is a simple command provider that makes the most obvious application
4+
actions available via the [command palette][textual.command.CommandPalette].
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from .command import DiscoveryHit, Hit, Hits, Provider
10+
from .types import IgnoreReturnCallbackType
11+
12+
13+
class SystemCommands(Provider):
14+
"""A [source][textual.command.Provider] of command palette commands that run app-wide tasks.
15+
16+
Used by default in [`App.COMMANDS`][textual.app.App.COMMANDS].
17+
"""
18+
19+
@property
20+
def _system_commands(self) -> tuple[tuple[str, IgnoreReturnCallbackType, str], ...]:
21+
"""The system commands to reveal to the command palette."""
22+
return (
23+
(
24+
"Toggle light/dark mode",
25+
self.app.action_toggle_dark,
26+
"Toggle the application between light and dark mode",
27+
),
28+
(
29+
"Quit the application",
30+
self.app.action_quit,
31+
"Quit the application as soon as possible",
32+
),
33+
(
34+
"Ring the bell",
35+
self.app.action_bell,
36+
"Ring the terminal's 'bell'",
37+
),
38+
)
39+
40+
async def discover(self) -> Hits:
41+
"""Handle a request for the discovery commands for this provider.
42+
43+
Yields:
44+
Commands that can be discovered.
45+
"""
46+
for name, runnable, help_text in self._system_commands:
47+
yield DiscoveryHit(
48+
name,
49+
runnable,
50+
help=help_text,
51+
)
52+
53+
async def search(self, query: str) -> Hits:
54+
"""Handle a request to search for system commands that match the query.
55+
56+
Args:
57+
query: The user input to be matched.
58+
59+
Yields:
60+
Command hits for use in the command palette.
61+
"""
62+
# We're going to use Textual's builtin fuzzy matcher to find
63+
# matching commands.
64+
matcher = self.matcher(query)
65+
66+
# Loop over all applicable commands, find those that match and offer
67+
# them up to the command palette.
68+
for name, runnable, help_text in self._system_commands:
69+
if (match := matcher.match(name)) > 0:
70+
yield Hit(
71+
match,
72+
matcher.highlight(name),
73+
runnable,
74+
help=help_text,
75+
)

src/textual/worker_manager.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""
2+
A class to manage [workers](/guide/workers) for an app.
3+
4+
You access this object via [App.workers][textual.app.App.workers] or [Widget.workers][textual.dom.DOMNode.workers].
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import asyncio
10+
from collections import Counter
11+
from operator import attrgetter
12+
from typing import TYPE_CHECKING, Any, Iterable, Iterator
13+
14+
import rich.repr
15+
16+
from .worker import Worker, WorkerState, WorkType
17+
18+
if TYPE_CHECKING:
19+
from .app import App
20+
from .dom import DOMNode
21+
22+
23+
@rich.repr.auto(angular=True)
24+
class WorkerManager:
25+
"""An object to manager a number of workers.
26+
27+
You will not have to construct this class manually, as widgets, screens, and apps
28+
have a worker manager accessibly via a `workers` attribute.
29+
"""
30+
31+
def __init__(self, app: App) -> None:
32+
"""Initialize a worker manager.
33+
34+
Args:
35+
app: An App instance.
36+
"""
37+
self._app = app
38+
"""A reference to the app."""
39+
self._workers: set[Worker] = set()
40+
"""The workers being managed."""
41+
42+
def __rich_repr__(self) -> rich.repr.Result:
43+
counter: Counter[WorkerState] = Counter()
44+
counter.update(worker.state for worker in self._workers)
45+
for state, count in sorted(counter.items()):
46+
yield state.name, count
47+
48+
def __iter__(self) -> Iterator[Worker[Any]]:
49+
return iter(sorted(self._workers, key=attrgetter("_created_time")))
50+
51+
def __reversed__(self) -> Iterator[Worker[Any]]:
52+
return iter(
53+
sorted(self._workers, key=attrgetter("_created_time"), reverse=True)
54+
)
55+
56+
def __bool__(self) -> bool:
57+
return bool(self._workers)
58+
59+
def __len__(self) -> int:
60+
return len(self._workers)
61+
62+
def __contains__(self, worker: object) -> bool:
63+
return worker in self._workers
64+
65+
def add_worker(
66+
self, worker: Worker, start: bool = True, exclusive: bool = True
67+
) -> None:
68+
"""Add a new worker.
69+
70+
Args:
71+
worker: A Worker instance.
72+
start: Start the worker if True, otherwise the worker must be started manually.
73+
exclusive: Cancel all workers in the same group as `worker`.
74+
"""
75+
if exclusive and worker.group:
76+
self.cancel_group(worker.node, worker.group)
77+
self._workers.add(worker)
78+
if start:
79+
worker._start(self._app, self._remove_worker)
80+
81+
def _new_worker(
82+
self,
83+
work: WorkType,
84+
node: DOMNode,
85+
*,
86+
name: str | None = "",
87+
group: str = "default",
88+
description: str = "",
89+
exit_on_error: bool = True,
90+
start: bool = True,
91+
exclusive: bool = False,
92+
thread: bool = False,
93+
) -> Worker:
94+
"""Create a worker from a function, coroutine, or awaitable.
95+
96+
Args:
97+
work: A callable, a coroutine, or other awaitable.
98+
name: A name to identify the worker.
99+
group: The worker group.
100+
description: A description of the worker.
101+
exit_on_error: Exit the app if the worker raises an error. Set to `False` to suppress exceptions.
102+
start: Automatically start the worker.
103+
exclusive: Cancel all workers in the same group.
104+
thread: Mark the worker as a thread worker.
105+
106+
Returns:
107+
A Worker instance.
108+
"""
109+
worker: Worker[Any] = Worker(
110+
node,
111+
work,
112+
name=name or getattr(work, "__name__", "") or "",
113+
group=group,
114+
description=description or repr(work),
115+
exit_on_error=exit_on_error,
116+
thread=thread,
117+
)
118+
self.add_worker(worker, start=start, exclusive=exclusive)
119+
return worker
120+
121+
def _remove_worker(self, worker: Worker) -> None:
122+
"""Remove a worker from the manager.
123+
124+
Args:
125+
worker: A Worker instance.
126+
"""
127+
self._workers.discard(worker)
128+
129+
def start_all(self) -> None:
130+
"""Start all the workers."""
131+
for worker in self._workers:
132+
worker._start(self._app, self._remove_worker)
133+
134+
def cancel_all(self) -> None:
135+
"""Cancel all workers."""
136+
for worker in self._workers:
137+
worker.cancel()
138+
139+
def cancel_group(self, node: DOMNode, group: str) -> list[Worker]:
140+
"""Cancel a single group.
141+
142+
Args:
143+
node: Worker DOM node.
144+
group: A group name.
145+
146+
Returns:
147+
A list of workers that were cancelled.
148+
"""
149+
workers = [
150+
worker
151+
for worker in self._workers
152+
if (worker.group == group and worker.node == node)
153+
]
154+
for worker in workers:
155+
worker.cancel()
156+
return workers
157+
158+
def cancel_node(self, node: DOMNode) -> list[Worker]:
159+
"""Cancel all workers associated with a given node
160+
161+
Args:
162+
node: A DOM node (widget, screen, or App).
163+
164+
Returns:
165+
List of cancelled workers.
166+
"""
167+
workers = [worker for worker in self._workers if worker.node == node]
168+
for worker in workers:
169+
worker.cancel()
170+
return workers
171+
172+
async def wait_for_complete(self, workers: Iterable[Worker] | None = None) -> None:
173+
"""Wait for workers to complete.
174+
175+
Args:
176+
workers: An iterable of workers or None to wait for all workers in the manager.
177+
"""
178+
try:
179+
await asyncio.gather(*[worker.wait() for worker in (workers or self)])
180+
except asyncio.CancelledError:
181+
pass

0 commit comments

Comments
 (0)