Coverage for src/devboard/board.py: 70.09%
85 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 20:21 +0100
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 20:21 +0100
1"""The Textual application."""
3from __future__ import annotations
5import os
6from functools import partial
7from multiprocessing import Pool
8from typing import TYPE_CHECKING, Any, Iterable
10from textual import work
11from textual.containers import Container
12from textual.widgets import Static
14from devboard.datatable import SelectableRow, SelectableRowsDataTable
15from devboard.modal import ModalMixin
16from devboard.notifications import NotifyMixin
17from devboard.projects import Project
19if TYPE_CHECKING:
20 from textual.app import ComposeResult
22DEBUG = os.getenv("DEBUG", "0") == "1"
25class Row(SelectableRow):
26 """A Devboard row."""
28 @property
29 def project(self) -> Project:
30 """Devboard project."""
31 for val in self.data:
32 if isinstance(val, Project):
33 return val
34 raise ValueError("No project in row data")
37class DataTable(SelectableRowsDataTable):
38 """A Devboard data table."""
40 ROW = Row
41 """The class to instantiate rows."""
44class Column(Container, ModalMixin, NotifyMixin): # type: ignore[misc]
45 """A Devboard column."""
47 TITLE: str = ""
48 """The title of the column."""
49 HEADERS: tuple[str, ...] = ()
50 """The data table headers."""
51 THREADED: bool = True
52 """Whether actions of this column should run in the background."""
53 DEFAULT_CLASSES = "box"
54 """Textual CSS classes."""
56 # --------------------------------------------------
57 # Textual methods.
58 # --------------------------------------------------
59 def compose(self) -> ComposeResult:
60 """Compose column widgets."""
61 yield Static("▶ " + self.TITLE, classes="column-title")
62 yield DataTable(id="table")
64 def on_mount(self) -> None:
65 """Fill data table."""
66 self.update()
68 # --------------------------------------------------
69 # Binding actions.
70 # --------------------------------------------------
71 def action_apply(self, action: str = "default") -> None:
72 """Apply an action to selected rows."""
73 selected_rows = list(self.table.selected_rows) or [self.table.current_row]
74 if self.THREADED:
75 for row in selected_rows:
76 self.run_worker(partial(self.apply, action=action, row=row), thread=True)
77 else:
78 for row in selected_rows:
79 self.apply(action=action, row=row) # type: ignore[arg-type]
81 # --------------------------------------------------
82 # Additional methods/properties.
83 # --------------------------------------------------
84 @property
85 def table(self) -> DataTable:
86 """Data table."""
87 return self.query_one("#table") # type: ignore[return-value]
89 def update(self) -> None:
90 """Update the column (recompute data)."""
91 table = self.query_one(DataTable)
92 if table.loading: 92 ↛ 93line 92 didn't jump to line 93, because the condition on line 92 was never true
93 return
94 table.loading = True
95 table.clear(columns=True)
96 table.cursor_type = "row"
97 self._load_data(table)
99 @work(thread=True)
100 def _load_data(self, table: DataTable) -> None:
101 if rows := self._populate():
102 # TODO: Reset styles.
103 for column in self.HEADERS:
104 table.add_column(column, key=column.lower())
105 table.add_rows(rows)
106 table.sort(self.HEADERS[0].lower())
107 table.refresh(layout=True)
108 else:
109 title: Static = self.query_one(".column-title") # type: ignore[assignment]
110 self.styles.width = 3
111 title.styles.text_style = "bold"
112 title.renderable = "▼ " + self.TITLE
113 self.table.styles.display = "none"
114 table.loading = False
116 def _populate(self) -> list[tuple[Any, ...]]:
117 rows = []
118 if DEBUG: 118 ↛ 119line 118 didn't jump to line 119, because the condition on line 118 was never true
119 for project in self.list_projects():
120 rows.extend(self.populate_rows(project))
121 else:
122 with Pool() as pool:
123 for result in pool.map(self.populate_rows, self.list_projects()):
124 rows.extend(result)
125 return rows
127 # --------------------------------------------------
128 # Methods to implement in subclasses.
129 # --------------------------------------------------
130 def list_projects(self) -> Iterable[Project]:
131 """List projects for this column."""
132 return ()
134 @staticmethod
135 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # noqa: ARG004
136 """Populate rows for this column."""
137 return []
139 def apply(self, action: str, row: Row) -> None: # noqa: ARG002
140 """Apply action on given row."""
141 return