Skip to content

devboard ¤

Devboard package.

A development dashboard for your projects.

Classes:

Functions:

  • get_parser

    Return the CLI argument parser.

  • main

    Run the main program.

Checkbox dataclass ¤

Checkbox(checked: bool = False)

A checkbox, added to rows to make them selectable.

Methods:

  • check

    Uncheck the checkbox.

  • toggle

    Toggle the checkbox.

  • uncheck

    Uncheck the checkbox.

Attributes:

checked class-attribute instance-attribute ¤

checked: bool = False

Whether the checkbox is checked.

check ¤

check() -> None

Uncheck the checkbox.

Source code in src/devboard/_internal/datatable.py
48
49
50
def check(self) -> None:
    """Uncheck the checkbox."""
    self.checked = True

toggle ¤

toggle() -> bool

Toggle the checkbox.

Source code in src/devboard/_internal/datatable.py
56
57
58
59
def toggle(self) -> bool:
    """Toggle the checkbox."""
    self.checked = not self.checked
    return self.checked

uncheck ¤

uncheck() -> None

Uncheck the checkbox.

Source code in src/devboard/_internal/datatable.py
52
53
54
def uncheck(self) -> None:
    """Uncheck the checkbox."""
    self.checked = False

Column ¤

Bases: Container, ModalMixin, NotifyMixin

A Devboard column.

Methods:

Attributes:

DEFAULT_CLASSES class-attribute instance-attribute ¤

DEFAULT_CLASSES = 'box'

Textual CSS classes.

HEADERS class-attribute instance-attribute ¤

HEADERS: tuple[str, ...] = ()

The data table headers.

THREADED class-attribute instance-attribute ¤

THREADED: bool = True

Whether actions of this column should run in the background.

TITLE class-attribute instance-attribute ¤

TITLE: str = ''

The title of the column.

app instance-attribute ¤

app: App

Textual application.

table property ¤

table: DataTable

Data table.

action_apply ¤

action_apply(action: str = 'default') -> None

Apply an action to selected rows.

Source code in src/devboard/_internal/board.py
80
81
82
83
84
85
86
87
88
89
90
def action_apply(self, action: str = "default") -> None:
    """Apply an action to selected rows."""
    selected_rows = [cast("Row", row) for row in self.table.selected_rows]
    if not selected_rows:
        selected_rows.append(cast("Row", self.table.current_row))
    if self.THREADED:
        for row in selected_rows:
            self.run_worker(partial(self.apply, action=action, row=row), thread=True)
    else:
        for row in selected_rows:
            self.apply(action=action, row=row)

apply ¤

apply(action: str, row: Row) -> None

Apply action on given row.

Source code in src/devboard/_internal/board.py
156
157
158
def apply(self, action: str, row: Row) -> None:  # noqa: ARG002
    """Apply action on given row."""
    return

compose ¤

compose() -> ComposeResult

Compose column widgets.

Source code in src/devboard/_internal/board.py
72
73
74
75
def compose(self) -> ComposeResult:
    """Compose column widgets."""
    yield Static("▶ " + self.TITLE, classes="column-title")
    yield DataTable(id="table")

list_projects ¤

list_projects() -> Iterable[Project]

List projects for this column.

Source code in src/devboard/_internal/board.py
147
148
149
def list_projects(self) -> Iterable[Project]:
    """List projects for this column."""
    return ()

modal ¤

modal(text: str) -> None

Push a modal.

Source code in src/devboard/_internal/modal.py
68
69
70
def modal(self, text: str) -> None:
    """Push a modal."""
    self.app.push_screen(Modal(text=text))

notify_error ¤

notify_error(message: str, timeout: float = 3.0) -> None

Notify error.

Source code in src/devboard/_internal/notifications.py
40
41
42
def notify_error(self, message: str, timeout: float = 3.0) -> None:
    """Notify error."""
    self.app.notify(f"[b red]ERROR[/]  {message}", severity="error", timeout=timeout)

notify_info ¤

notify_info(message: str, timeout: float = 3.0) -> None

Notify information.

Source code in src/devboard/_internal/notifications.py
28
29
30
def notify_info(self, message: str, timeout: float = 3.0) -> None:
    """Notify information."""
    self.app.notify(f"[b blue]INFO[/]  {message}", severity="information", timeout=timeout)

notify_success ¤

notify_success(message: str, timeout: float = 3.0) -> None

Notify success.

Source code in src/devboard/_internal/notifications.py
32
33
34
def notify_success(self, message: str, timeout: float = 3.0) -> None:
    """Notify success."""
    self.app.notify(f"[b green]SUCCESS[/]  {message}", severity="information", timeout=timeout)

notify_warning ¤

notify_warning(message: str, timeout: float = 3.0) -> None

Notify warning.

Source code in src/devboard/_internal/notifications.py
36
37
38
def notify_warning(self, message: str, timeout: float = 3.0) -> None:
    """Notify warning."""
    self.app.notify(f"[b yellow]WARNING[/]  {message}", severity="warning", timeout=timeout)

populate_rows staticmethod ¤

populate_rows(project: Project) -> list[tuple[Any, ...]]

Populate rows for this column.

Source code in src/devboard/_internal/board.py
151
152
153
154
@staticmethod
def populate_rows(project: Project) -> list[tuple[Any, ...]]:  # noqa: ARG004
    """Populate rows for this column."""
    return []

update ¤

update() -> None

Update the column (ask the app to recompute its data).

Source code in src/devboard/_internal/board.py
100
101
102
103
104
def update(self) -> None:
    """Update the column (ask the app to recompute its data)."""
    scan = getattr(self.app, "scan", None)
    if scan is not None:
        scan([self])

DataTable ¤

Bases: SelectableRowsDataTable

A Devboard data table.

Methods:

Attributes:

BINDINGS class-attribute instance-attribute ¤

BINDINGS: ClassVar = [
    Binding(
        "space",
        "toggle_select_row",
        "Toggle select",
        show=False,
    ),
    Binding(
        "ctrl+a, *",
        "toggle_select_all",
        "Toggle select all",
        show=False,
    ),
    Binding(
        "exclamation_mark",
        "reverse_select",
        "Reverse select",
        show=False,
    ),
    Binding(
        "shift+up",
        "toggle_select_up",
        "Expand select up",
        show=False,
    ),
    Binding(
        "shift+down",
        "toggle_select_down",
        "Expand select down",
        show=False,
    ),
]

Key bindings for selecting rows.

ROW class-attribute instance-attribute ¤

ROW = Row

The class to instantiate rows.

current_row property ¤

current_row: SelectableRow

Currently selected row.

selectable_rows property ¤

selectable_rows: Iterator[SelectableRow]

Rows, as selectable ones.

selected_rows property ¤

selected_rows: Iterator[SelectableRow]

Selected rows.

action_reverse_select ¤

action_reverse_select() -> None

Reverse selection.

Source code in src/devboard/_internal/datatable.py
189
190
191
192
193
def action_reverse_select(self) -> None:
    """Reverse selection."""
    for row in self.selectable_rows:
        row.toggle_select()
    self.force_refresh()

action_toggle_select_all ¤

action_toggle_select_all() -> None

Toggle-select all rows.

Source code in src/devboard/_internal/datatable.py
178
179
180
181
182
183
184
185
186
187
def action_toggle_select_all(self) -> None:
    """Toggle-select all rows."""
    rows = list(self.selectable_rows)
    if all(row.selected for row in rows):
        for row in rows:
            row.unselect()
    else:
        for row in rows:
            row.select()
    self.force_refresh()

action_toggle_select_down ¤

action_toggle_select_down() -> None

Toggle selection down.

Source code in src/devboard/_internal/datatable.py
207
208
209
210
211
212
213
214
215
216
217
def action_toggle_select_down(self) -> None:
    """Toggle selection down."""
    try:
        row = self.current_row
        next_row = row.next
    except CellDoesNotExist:
        pass
    else:
        next_row.toggle_select()
        self.move_cursor(row=next_row.index)
        self.force_refresh()

action_toggle_select_row ¤

action_toggle_select_row() -> None

Toggle-select current row.

Source code in src/devboard/_internal/datatable.py
169
170
171
172
173
174
175
176
def action_toggle_select_row(self) -> None:
    """Toggle-select current row."""
    try:
        row = self.current_row
    except CellDoesNotExist:
        return
    row.toggle_select()
    self.force_refresh()

action_toggle_select_up ¤

action_toggle_select_up() -> None

Toggle selection up.

Source code in src/devboard/_internal/datatable.py
195
196
197
198
199
200
201
202
203
204
205
def action_toggle_select_up(self) -> None:
    """Toggle selection up."""
    try:
        row = self.current_row
        previous_row = row.previous
    except CellDoesNotExist:
        pass
    else:
        previous_row.toggle_select()
        self.move_cursor(row=previous_row.index)
        self.force_refresh()

add_rows ¤

add_rows(rows: Iterable[Iterable]) -> list[RowKey]

Add rows.

Automatically insert a column with checkboxes in position 0.

Source code in src/devboard/_internal/datatable.py
149
150
151
152
153
154
def add_rows(self, rows: Iterable[Iterable]) -> list[RowKey]:
    """Add rows.

    Automatically insert a column with checkboxes in position 0.
    """
    return super().add_rows((Checkbox(), *row) for row in rows)

clear ¤

clear(columns: bool = True) -> SelectableRowsDataTable

Clear rows and optionally columns.

When clearing columns, automatically re-add a column for checkboxes.

Source code in src/devboard/_internal/datatable.py
156
157
158
159
160
161
162
163
164
def clear(self, columns: bool = True) -> SelectableRowsDataTable:  # noqa: FBT001,FBT002
    """Clear rows and optionally columns.

    When clearing columns, automatically re-add a column for checkboxes.
    """
    super().clear(columns)
    if columns:
        self.add_column("", key="checkbox")
    return self

force_refresh ¤

force_refresh() -> None

Force refresh table.

Source code in src/devboard/_internal/datatable.py
222
223
224
225
226
227
def force_refresh(self) -> None:
    """Force refresh table."""
    # HACK: Without such increment, the table is refreshed
    # only when focus changes to another column.
    self._update_count += 1
    self.refresh()

Devboard ¤

Devboard(
    *args: Any,
    board: str | Path | None = None,
    background_tasks: bool = True,
    workers: int | None = None,
    **kwargs: Any,
)

Bases: App, ModalMixin

The Devboard application.

Parameters:

  • board ¤

    (str | Path | None, default: None ) –

    The board to display (name or file path).

  • background_tasks ¤

    (bool, default: True ) –

    Whether to fetch repositories in the background after the initial scan. Disabling this also disables the on-disk cache (useful for tests and screenshots).

  • workers ¤

    (int | None, default: None ) –

    How many projects to scan concurrently. Overrides the workers config setting.

Methods:

Attributes:

Source code in src/devboard/_internal/app.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def __init__(
    self,
    *args: Any,
    board: str | Path | None = None,
    background_tasks: bool = True,
    workers: int | None = None,
    **kwargs: Any,
) -> None:
    """Initialize the app.

    Parameters:
        board: The board to display (name or file path).
        background_tasks: Whether to fetch repositories in the background after the initial scan.
            Disabling this also disables the on-disk cache (useful for tests and screenshots).
        workers: How many projects to scan concurrently. Overrides the `workers` config setting.
    """
    super().__init__(*args, **kwargs)
    self._board: str | Path | None = board
    self._board_key: str = str(board)
    self._config_file: Path = Path(user_config_dir(), "devboard", "config.toml")
    self._background_tasks: bool = background_tasks
    self._scan_workers: int | None = workers
    self._scanning: bool = False

BINDINGS class-attribute instance-attribute ¤

BINDINGS: ClassVar = [
    Binding("F5, ctrl+r", "refresh", "Refresh"),
    Binding("question_mark", "show_help", "Help"),
    Binding(
        "ctrl+q, q, escape", "exit", "Exit", key_display="Q"
    ),
]

Application key bindings.

CSS_PATH class-attribute instance-attribute ¤

CSS_PATH = Path(__file__).parent / 'devboard.tcss'

Path to the CSS file.

app instance-attribute ¤

app: App

Textual application.

action_exit ¤

action_exit() -> None

Exit application.

Source code in src/devboard/_internal/app.py
133
134
135
136
def action_exit(self) -> None:
    """Exit application."""
    self.workers.cancel_all()
    self.exit()

action_refresh ¤

action_refresh() -> None

Refresh all columns.

Source code in src/devboard/_internal/app.py
129
130
131
def action_refresh(self) -> None:
    """Refresh all columns."""
    self.scan()

action_show_help ¤

action_show_help() -> None

Show help.

Source code in src/devboard/_internal/app.py
119
120
121
122
123
124
125
126
127
def action_show_help(self) -> None:
    """Show help."""
    lines = ["# Main keys\n\n"]
    lines.extend(self._bindings_help(Devboard))
    lines.extend(self._bindings_help(DataTable, search_up=True))
    for column in self.query(Column):
        lines.append(f"\n\n# {column.__class__.TITLE}\n\n")
        lines.extend(self._bindings_help(column.__class__))
    self.push_screen(Modal(text=Markdown("\n".join(lines))))

compose ¤

compose() -> ComposeResult

Compose the layout.

Source code in src/devboard/_internal/app.py
103
104
105
106
107
108
109
110
def compose(self) -> ComposeResult:
    """Compose the layout."""
    for column in self._load_columns():
        if isinstance(column, Column):
            yield column
        else:
            yield column()
    yield Footer()

fetch_all ¤

fetch_all() -> None

Run git fetch in all projects, in background.

Source code in src/devboard/_internal/app.py
160
161
162
163
164
165
166
@work(thread=True)
def fetch_all(self) -> None:
    """Run `git fetch` in all projects, in background."""
    projects: set[Project] = set()
    for column in self.query(Column):
        projects |= set(column.list_projects())
    self._fetch_projects(projects)

modal ¤

modal(text: str) -> None

Push a modal.

Source code in src/devboard/_internal/modal.py
68
69
70
def modal(self, text: str) -> None:
    """Push a modal."""
    self.app.push_screen(Modal(text=text))

on_mount ¤

on_mount() -> None

Populate columns, then run background tasks.

Source code in src/devboard/_internal/app.py
112
113
114
def on_mount(self) -> None:
    """Populate columns, then run background tasks."""
    self.scan(initial=True)

scan ¤

scan(
    columns: Iterable[Column] | None = None,
    *,
    initial: bool = False,
) -> None

Recompute columns data in the background.

A single scan feeds all columns: each project is read once, by a small pool of threads, and the resulting rows are dispatched to every column as they arrive.

Parameters:

  • columns ¤

    (Iterable[Column] | None, default: None ) –

    The columns to update (all of them by default).

  • initial ¤

    (bool, default: False ) –

    Whether this is the initial scan at startup, which additionally displays cached data, saves fresh data to the cache, and triggers the background fetch when these features are enabled.

Source code in src/devboard/_internal/app.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def scan(self, columns: Iterable[Column] | None = None, *, initial: bool = False) -> None:
    """Recompute columns data in the background.

    A single scan feeds all columns: each project is read once,
    by a small pool of threads, and the resulting rows are dispatched
    to every column as they arrive.

    Parameters:
        columns: The columns to update (all of them by default).
        initial: Whether this is the initial scan at startup, which additionally
            displays cached data, saves fresh data to the cache, and triggers
            the background fetch when these features are enabled.
    """
    if self._scanning:
        return
    self._scanning = True
    column_list = list(columns) if columns is not None else list(self.query(Column))
    self.run_worker(partial(self._scan, column_list, initial=initial), thread=True)

Modal ¤

Modal(*args: Any, text: Any, **kwargs: Any)

Bases: ModalScreen

A modal screen.

Methods:

  • compose

    Screen composition.

  • on_key

    Dismiss on any unbound key.

Attributes:

  • text

    Text content.

Source code in src/devboard/_internal/modal.py
36
37
38
39
40
41
42
43
def __init__(self, *args: Any, text: Any, **kwargs: Any) -> None:
    """Initialize the screen."""
    super().__init__(*args, **kwargs)
    if isinstance(text, str):
        self.text = Text.from_ansi(text)
        """Text content."""
    else:
        self.text = text

text instance-attribute ¤

text = Text.from_ansi(text)

Text content.

compose ¤

compose() -> ComposeResult

Screen composition.

Source code in src/devboard/_internal/modal.py
45
46
47
def compose(self) -> ComposeResult:
    """Screen composition."""
    yield VerticalScroll(Static(self.text), id="modal-contents")

on_key ¤

on_key(event: Key) -> None

Dismiss on any unbound key.

Source code in src/devboard/_internal/modal.py
49
50
51
52
53
54
55
56
57
58
59
def on_key(self, event: Key) -> None:
    """Dismiss on any unbound key."""
    active_bindings = getattr(self.app, "active_bindings", None)
    if active_bindings is None:  # Textual < 1.0.
        legacy_binding_chain = getattr(self.app, "_modal_binding_chain")  # noqa: B009
        is_bound = any(bindings.keys.get(event.key) for _, bindings in legacy_binding_chain)
    else:
        is_bound = event.key in active_bindings
    if not is_bound:
        event.stop()
        self.dismiss()

ModalMixin ¤

Mixin class to add a modal method.

Methods:

  • modal

    Push a modal.

Attributes:

  • app (App) –

    Textual application.

app instance-attribute ¤

app: App

Textual application.

modal ¤

modal(text: str) -> None

Push a modal.

Source code in src/devboard/_internal/modal.py
68
69
70
def modal(self, text: str) -> None:
    """Push a modal."""
    self.app.push_screen(Modal(text=text))

NotifyMixin ¤

Mixin class to add notify methods.

Methods:

Attributes:

  • app (App) –

    Textual application.

app instance-attribute ¤

app: App

Textual application.

notify_error ¤

notify_error(message: str, timeout: float = 3.0) -> None

Notify error.

Source code in src/devboard/_internal/notifications.py
40
41
42
def notify_error(self, message: str, timeout: float = 3.0) -> None:
    """Notify error."""
    self.app.notify(f"[b red]ERROR[/]  {message}", severity="error", timeout=timeout)

notify_info ¤

notify_info(message: str, timeout: float = 3.0) -> None

Notify information.

Source code in src/devboard/_internal/notifications.py
28
29
30
def notify_info(self, message: str, timeout: float = 3.0) -> None:
    """Notify information."""
    self.app.notify(f"[b blue]INFO[/]  {message}", severity="information", timeout=timeout)

notify_success ¤

notify_success(message: str, timeout: float = 3.0) -> None

Notify success.

Source code in src/devboard/_internal/notifications.py
32
33
34
def notify_success(self, message: str, timeout: float = 3.0) -> None:
    """Notify success."""
    self.app.notify(f"[b green]SUCCESS[/]  {message}", severity="information", timeout=timeout)

notify_warning ¤

notify_warning(message: str, timeout: float = 3.0) -> None

Notify warning.

Source code in src/devboard/_internal/notifications.py
36
37
38
def notify_warning(self, message: str, timeout: float = 3.0) -> None:
    """Notify warning."""
    self.app.notify(f"[b yellow]WARNING[/]  {message}", severity="warning", timeout=timeout)

Project ¤

Project(path: Path)

A class representing development projects.

It is instantiated with a path, and then provides many utility properties and methods.

Methods:

  • __lt__

    Ordering is based on the project name.

  • checkout

    Checkout branch, restore previous one when exiting.

  • delete

    Delete branch.

  • fetch

    Fetch.

  • lock

    Lock project.

  • pull

    Pull branch.

  • push

    Push branch.

  • unlock

    Unlock project.

  • unpulled

    Number of unpulled commits (compared to the branch upstream), per branch.

  • unpushed

    Number of unpushed commits (compared to the branch upstream), per branch.

  • unreleased

    List unreleased commits (commits since the latest tag reachable from the branch).

Attributes:

  • DEFAULT_BRANCHES (tuple[str, ...]) –

    Name of common default branches. Mainly useful to compute unreleased commits.

  • LOCKS (dict[Project, Lock]) –

    Locks for projects, to avoid concurrent operations.

  • branch (Head) –

    Currently checked out branch.

  • default_branch (str) –

    Default branch (or main branch), as checked out when cloning.

  • is_dirty (bool) –

    Whether the project is in a "dirty" state (uncommitted modifications).

  • latest_tag (TagReference) –

    Latest tag (by creation date).

  • name (str) –

    Name of the project.

  • path (Path) –

    Path of the project on the file-system.

  • repo (Repo) –

    GitPython's Repo object (cached per instance).

  • status (Status) –

    Status of the project.

  • status_line (str) –

    Status of the project, as a string.

Source code in src/devboard/_internal/projects.py
67
68
69
def __init__(self, path: Path) -> None:
    self.path: Path = path
    """Path of the project on the file-system."""

DEFAULT_BRANCHES class-attribute ¤

DEFAULT_BRANCHES: tuple[str, ...] = ('main', 'master')

Name of common default branches. Mainly useful to compute unreleased commits.

LOCKS class-attribute ¤

Locks for projects, to avoid concurrent operations.

branch property ¤

branch: Head

Currently checked out branch.

default_branch property ¤

default_branch: str

Default branch (or main branch), as checked out when cloning.

is_dirty property ¤

is_dirty: bool

Whether the project is in a "dirty" state (uncommitted modifications).

latest_tag property ¤

latest_tag: TagReference

Latest tag (by creation date).

Raises:

name property ¤

name: str

Name of the project.

path instance-attribute ¤

path: Path = path

Path of the project on the file-system.

repo cached property ¤

repo: Repo

GitPython's Repo object (cached per instance).

status property ¤

status: Status

Status of the project.

Computed from a single git status --porcelain call, which is much cheaper than diffing index and work tree separately. Each file is counted once, in the first matching category: untracked, renamed, added, deleted, type-changed, modified.

status_line property ¤

status_line: str

Status of the project, as a string.

__lt__ ¤

__lt__(other: object) -> bool

Ordering is based on the project name.

Total ordering is implemented on projects so they can be sorted in the application tables.

Source code in src/devboard/_internal/projects.py
74
75
76
77
78
79
80
81
def __lt__(self, other: object) -> bool:
    """Ordering is based on the project name.

    Total ordering is implemented on projects so they can be sorted in the application tables.
    """
    if not isinstance(other, Project):
        return NotImplemented
    return self.name < other.name

checkout ¤

checkout(branch: str | None) -> Iterator[None]

Checkout branch, restore previous one when exiting.

Source code in src/devboard/_internal/projects.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
@contextmanager
def checkout(self, branch: str | None) -> Iterator[None]:
    """Checkout branch, restore previous one when exiting."""
    if not branch:
        yield
        return
    current = self.branch
    if branch == current:
        yield
        return
    self.repo.branches[branch].checkout()
    try:
        yield
    finally:
        current.checkout()

delete ¤

delete(branch: str) -> None

Delete branch.

Source code in src/devboard/_internal/projects.py
222
223
224
def delete(self, branch: str) -> None:
    """Delete branch."""
    self.repo.delete_head(branch, force=True)

fetch ¤

fetch() -> None

Fetch.

Source code in src/devboard/_internal/projects.py
244
245
246
247
248
249
def fetch(self) -> None:
    """Fetch."""
    with suppress(AttributeError, GitCommandError):
        self.repo.remotes.origin.fetch()
    with suppress(AttributeError, GitCommandError):
        self.repo.remotes.upstream.fetch()

lock ¤

lock() -> bool

Lock project.

Source code in src/devboard/_internal/projects.py
263
264
265
def lock(self) -> bool:
    """Lock project."""
    return self.LOCKS[self].acquire(blocking=False)

pull ¤

pull(branch: str | None = None) -> None

Pull branch.

Source code in src/devboard/_internal/projects.py
212
213
214
215
def pull(self, branch: str | None = None) -> None:
    """Pull branch."""
    with self.checkout(branch):
        self.repo.remotes.origin.pull()

push ¤

push(branch: str | None = None) -> None

Push branch.

Source code in src/devboard/_internal/projects.py
217
218
219
220
def push(self, branch: str | None = None) -> None:
    """Push branch."""
    with self.checkout(branch):
        self.repo.remotes.origin.push()

unlock ¤

unlock() -> None

Unlock project.

Source code in src/devboard/_internal/projects.py
267
268
269
def unlock(self) -> None:
    """Unlock project."""
    self.LOCKS[self].release()

unpulled ¤

unpulled() -> dict[str, int]

Number of unpulled commits (compared to the branch upstream), per branch.

Source code in src/devboard/_internal/projects.py
173
174
175
def unpulled(self) -> dict[str, int]:
    """Number of unpulled commits (compared to the branch upstream), per branch."""
    return {branch: behind for branch, (_, behind) in self._tracking.items()}

unpushed ¤

unpushed() -> dict[str, int]

Number of unpushed commits (compared to the branch upstream), per branch.

Source code in src/devboard/_internal/projects.py
169
170
171
def unpushed(self) -> dict[str, int]:
    """Number of unpushed commits (compared to the branch upstream), per branch."""
    return {branch: ahead for branch, (ahead, _) in self._tracking.items()}

unreleased ¤

unreleased(branch: str | None = None) -> list[Commit]

List unreleased commits (commits since the latest tag reachable from the branch).

Source code in src/devboard/_internal/projects.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def unreleased(self, branch: str | None = None) -> list[Commit]:
    """List unreleased commits (commits since the latest tag reachable from the branch)."""
    if branch is None:
        try:
            branch = self.default_branch
        except ValueError:
            return []
    try:
        latest_tag = self.repo.git.describe(branch, tags=True, abbrev=0)
    except GitCommandError:
        rev = branch  # No tag reachable: everything is unreleased.
    else:
        rev = f"{latest_tag}..{branch}"
    try:
        return list(self.repo.iter_commits(rev))
    except GitCommandError:
        return []

Row dataclass ¤

Row(table: SelectableRowsDataTable, key: RowKey)

Bases: SelectableRow

A Devboard row.

Methods:

Attributes:

app property ¤

app: App

Textual application.

checkbox property ¤

checkbox: Checkbox

Row checkbox.

data property ¤

data: list

Row data (without checkbox).

index property ¤

index: int

Row index.

key instance-attribute ¤

key: RowKey

The row key.

next property ¤

Next row (down).

previous property ¤

previous: SelectableRow

Previous row (up).

project property ¤

project: Project

Devboard project.

selected property ¤

selected: bool

Whether this row is selected.

table instance-attribute ¤

The data table containing this row.

remove ¤

remove() -> None

Remove row from the table.

Source code in src/devboard/_internal/datatable.py
112
113
114
def remove(self) -> None:
    """Remove row from the table."""
    self.table.remove_row(self.key)

select ¤

select() -> None

Select this row.

Source code in src/devboard/_internal/datatable.py
95
96
97
def select(self) -> None:
    """Select this row."""
    self.checkbox.check()

toggle_select ¤

toggle_select() -> bool

Toggle-select this row.

Source code in src/devboard/_internal/datatable.py
103
104
105
def toggle_select(self) -> bool:
    """Toggle-select this row."""
    return self.checkbox.toggle()

unselect ¤

unselect() -> None

Unselect this row.

Source code in src/devboard/_internal/datatable.py
 99
100
101
def unselect(self) -> None:
    """Unselect this row."""
    self.checkbox.uncheck()

SelectableRow dataclass ¤

SelectableRow(table: SelectableRowsDataTable, key: RowKey)

A selectable row.

Methods:

Attributes:

app property ¤

app: App

Textual application.

checkbox property ¤

checkbox: Checkbox

Row checkbox.

data property ¤

data: list

Row data (without checkbox).

index property ¤

index: int

Row index.

key instance-attribute ¤

key: RowKey

The row key.

next property ¤

Next row (down).

previous property ¤

previous: SelectableRow

Previous row (up).

selected property ¤

selected: bool

Whether this row is selected.

table instance-attribute ¤

The data table containing this row.

remove ¤

remove() -> None

Remove row from the table.

Source code in src/devboard/_internal/datatable.py
112
113
114
def remove(self) -> None:
    """Remove row from the table."""
    self.table.remove_row(self.key)

select ¤

select() -> None

Select this row.

Source code in src/devboard/_internal/datatable.py
95
96
97
def select(self) -> None:
    """Select this row."""
    self.checkbox.check()

toggle_select ¤

toggle_select() -> bool

Toggle-select this row.

Source code in src/devboard/_internal/datatable.py
103
104
105
def toggle_select(self) -> bool:
    """Toggle-select this row."""
    return self.checkbox.toggle()

unselect ¤

unselect() -> None

Unselect this row.

Source code in src/devboard/_internal/datatable.py
 99
100
101
def unselect(self) -> None:
    """Unselect this row."""
    self.checkbox.uncheck()

SelectableRowsDataTable ¤

Bases: DataTable

Data table with selectable rows.

Methods:

Attributes:

BINDINGS class-attribute instance-attribute ¤

BINDINGS: ClassVar = [
    Binding(
        "space",
        "toggle_select_row",
        "Toggle select",
        show=False,
    ),
    Binding(
        "ctrl+a, *",
        "toggle_select_all",
        "Toggle select all",
        show=False,
    ),
    Binding(
        "exclamation_mark",
        "reverse_select",
        "Reverse select",
        show=False,
    ),
    Binding(
        "shift+up",
        "toggle_select_up",
        "Expand select up",
        show=False,
    ),
    Binding(
        "shift+down",
        "toggle_select_down",
        "Expand select down",
        show=False,
    ),
]

Key bindings for selecting rows.

ROW class-attribute instance-attribute ¤

The class to instantiate selectable rows.

current_row property ¤

current_row: SelectableRow

Currently selected row.

selectable_rows property ¤

selectable_rows: Iterator[SelectableRow]

Rows, as selectable ones.

selected_rows property ¤

selected_rows: Iterator[SelectableRow]

Selected rows.

action_reverse_select ¤

action_reverse_select() -> None

Reverse selection.

Source code in src/devboard/_internal/datatable.py
189
190
191
192
193
def action_reverse_select(self) -> None:
    """Reverse selection."""
    for row in self.selectable_rows:
        row.toggle_select()
    self.force_refresh()

action_toggle_select_all ¤

action_toggle_select_all() -> None

Toggle-select all rows.

Source code in src/devboard/_internal/datatable.py
178
179
180
181
182
183
184
185
186
187
def action_toggle_select_all(self) -> None:
    """Toggle-select all rows."""
    rows = list(self.selectable_rows)
    if all(row.selected for row in rows):
        for row in rows:
            row.unselect()
    else:
        for row in rows:
            row.select()
    self.force_refresh()

action_toggle_select_down ¤

action_toggle_select_down() -> None

Toggle selection down.

Source code in src/devboard/_internal/datatable.py
207
208
209
210
211
212
213
214
215
216
217
def action_toggle_select_down(self) -> None:
    """Toggle selection down."""
    try:
        row = self.current_row
        next_row = row.next
    except CellDoesNotExist:
        pass
    else:
        next_row.toggle_select()
        self.move_cursor(row=next_row.index)
        self.force_refresh()

action_toggle_select_row ¤

action_toggle_select_row() -> None

Toggle-select current row.

Source code in src/devboard/_internal/datatable.py
169
170
171
172
173
174
175
176
def action_toggle_select_row(self) -> None:
    """Toggle-select current row."""
    try:
        row = self.current_row
    except CellDoesNotExist:
        return
    row.toggle_select()
    self.force_refresh()

action_toggle_select_up ¤

action_toggle_select_up() -> None

Toggle selection up.

Source code in src/devboard/_internal/datatable.py
195
196
197
198
199
200
201
202
203
204
205
def action_toggle_select_up(self) -> None:
    """Toggle selection up."""
    try:
        row = self.current_row
        previous_row = row.previous
    except CellDoesNotExist:
        pass
    else:
        previous_row.toggle_select()
        self.move_cursor(row=previous_row.index)
        self.force_refresh()

add_rows ¤

add_rows(rows: Iterable[Iterable]) -> list[RowKey]

Add rows.

Automatically insert a column with checkboxes in position 0.

Source code in src/devboard/_internal/datatable.py
149
150
151
152
153
154
def add_rows(self, rows: Iterable[Iterable]) -> list[RowKey]:
    """Add rows.

    Automatically insert a column with checkboxes in position 0.
    """
    return super().add_rows((Checkbox(), *row) for row in rows)

clear ¤

clear(columns: bool = True) -> SelectableRowsDataTable

Clear rows and optionally columns.

When clearing columns, automatically re-add a column for checkboxes.

Source code in src/devboard/_internal/datatable.py
156
157
158
159
160
161
162
163
164
def clear(self, columns: bool = True) -> SelectableRowsDataTable:  # noqa: FBT001,FBT002
    """Clear rows and optionally columns.

    When clearing columns, automatically re-add a column for checkboxes.
    """
    super().clear(columns)
    if columns:
        self.add_column("", key="checkbox")
    return self

force_refresh ¤

force_refresh() -> None

Force refresh table.

Source code in src/devboard/_internal/datatable.py
222
223
224
225
226
227
def force_refresh(self) -> None:
    """Force refresh table."""
    # HACK: Without such increment, the table is refreshed
    # only when focus changes to another column.
    self._update_count += 1
    self.refresh()

Status dataclass ¤

Status(
    added: list[Path],
    deleted: list[Path],
    modified: list[Path],
    renamed: list[Path],
    typechanged: list[Path],
    untracked: list[Path],
)

Git status data.

Attributes:

added instance-attribute ¤

added: list[Path]

Added files.

deleted instance-attribute ¤

deleted: list[Path]

Deleted files.

modified instance-attribute ¤

modified: list[Path]

Modified files.

renamed instance-attribute ¤

renamed: list[Path]

Renamed files.

typechanged instance-attribute ¤

typechanged: list[Path]

Type-changed files.

untracked instance-attribute ¤

untracked: list[Path]

Untracked files.

get_parser ¤

get_parser() -> ArgumentParser

Return the CLI argument parser.

Returns:

Source code in src/devboard/_internal/cli.py
51
52
53
54
55
56
57
58
59
60
61
62
def get_parser() -> argparse.ArgumentParser:
    """Return the CLI argument parser.

    Returns:
        An argparse parser.
    """
    parser = argparse.ArgumentParser(prog="devboard")
    parser.add_argument("--show-config-dir", action="store_true", help="Show Devboard's configuration directory.")
    parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug._get_version()}")
    parser.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.")
    parser.add_argument("board", nargs="?", default=None, help="Board name or path.")
    return parser

main ¤

main(args: list[str] | None = None) -> int

Run the main program.

This function is executed when you type devboard or python -m devboard.

Parameters:

  • args ¤

    (list[str] | None, default: None ) –

    Arguments passed from the command line.

Returns:

  • int

    An exit code.

Source code in src/devboard/_internal/cli.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def main(args: list[str] | None = None) -> int:
    """Run the main program.

    This function is executed when you type `devboard` or `python -m devboard`.

    Parameters:
        args: Arguments passed from the command line.

    Returns:
        An exit code.
    """
    parser = get_parser()
    opts = parser.parse_args(args=args)
    if opts.show_config_dir:
        print(user_config_dir(appname="devboard"))
        return 0
    app = Devboard(board=opts.board)
    app.run()
    return 0