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

1"""The Textual application.""" 

2 

3from __future__ import annotations 

4 

5import os 

6from functools import partial 

7from multiprocessing import Pool 

8from typing import TYPE_CHECKING, Any, Iterable 

9 

10from textual import work 

11from textual.containers import Container 

12from textual.widgets import Static 

13 

14from devboard.datatable import SelectableRow, SelectableRowsDataTable 

15from devboard.modal import ModalMixin 

16from devboard.notifications import NotifyMixin 

17from devboard.projects import Project 

18 

19if TYPE_CHECKING: 

20 from textual.app import ComposeResult 

21 

22DEBUG = os.getenv("DEBUG", "0") == "1" 

23 

24 

25class Row(SelectableRow): 

26 """A Devboard row.""" 

27 

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") 

35 

36 

37class DataTable(SelectableRowsDataTable): 

38 """A Devboard data table.""" 

39 

40 ROW = Row 

41 """The class to instantiate rows.""" 

42 

43 

44class Column(Container, ModalMixin, NotifyMixin): # type: ignore[misc] 

45 """A Devboard column.""" 

46 

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.""" 

55 

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") 

63 

64 def on_mount(self) -> None: 

65 """Fill data table.""" 

66 self.update() 

67 

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] 

80 

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] 

88 

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) 

98 

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 

115 

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 

126 

127 # -------------------------------------------------- 

128 # Methods to implement in subclasses. 

129 # -------------------------------------------------- 

130 def list_projects(self) -> Iterable[Project]: 

131 """List projects for this column.""" 

132 return () 

133 

134 @staticmethod 

135 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # noqa: ARG004 

136 """Populate rows for this column.""" 

137 return [] 

138 

139 def apply(self, action: str, row: Row) -> None: # noqa: ARG002 

140 """Apply action on given row.""" 

141 return