Skip to content

Commit 573b879

Browse files
authored
Merge pull request #31 from richardkoehler/feat/add-typing
Add type checking
2 parents 8af3efa + 4989c5d commit 573b879

18 files changed

Lines changed: 243 additions & 133 deletions

.github/workflows/ci.yml

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI - Lint
1+
name: CI - Lint and Type Check
22

33
on:
44
push:
@@ -17,21 +17,21 @@ jobs:
1717
- name: Checkout repository
1818
uses: actions/checkout@v6
1919

20-
- name: Set up uv with Python
20+
- name: Set up uv
2121
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57
2222
with:
2323
python-version: "3.14"
2424
enable-cache: true
2525

26-
- name: Sync dependencies (locked, dev)
26+
- name: Sync dependencies
2727
run: |
2828
uv sync --locked --dev
2929
30-
- name: Run ruff (as in pre-commit)
30+
- name: Lint
3131
run: |
3232
uv run ruff check --exclude tests .
3333
34-
- name: Run ruff format (check)
34+
- name: Format
3535
run: |
3636
uv run ruff format --check --exclude tests .
3737
@@ -41,3 +41,25 @@ jobs:
4141
# run: |
4242
# uv run pytest
4343

44+
type-check:
45+
name: Type Check
46+
runs-on: ubuntu-latest
47+
48+
steps:
49+
- name: Checkout repository
50+
uses: actions/checkout@v6
51+
52+
- name: Set up uv
53+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57
54+
with:
55+
python-version: "3.14"
56+
enable-cache: true
57+
58+
- name: Sync dependencies
59+
run: |
60+
uv sync --locked --dev
61+
62+
- name: Type check
63+
run: |
64+
uv run ty check src
65+

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ repos:
1010
- id: ruff-format
1111
- id: ruff-check
1212
args: ["--fix", "--exclude", "tests"]
13+
14+
- repo: local
15+
hooks:
16+
- id: ty-check
17+
name: ty check (src)
18+
entry: uv run ty check src
19+
language: system
20+
pass_filenames: false

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ dev = [
3737
"ruff>=0.1.0",
3838
# "mypy>=1.8.0",
3939
"pre-commit>=3.6",
40+
"ty>=0.0.30",
41+
"pandas-stubs>=3.0.0.260204",
4042
]
4143
build = [
4244
"pyinstaller>=6.15.0",
@@ -121,3 +123,6 @@ strict_optional = true
121123
[[tool.mypy.overrides]]
122124
module = "PySide6.*"
123125
ignore_missing_imports = true
126+
127+
[tool.ty.rules]
128+
unresolved-attribute = "ignore"

src/clinical_dbs_annotator/controllers/wizard_controller.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -476,12 +476,17 @@ def undo_last_session_entry(self, view) -> None:
476476

477477
# Read the TSV file and filter out rows with the last block_id
478478
file_path = self.session_data.file_path
479+
if file_path is None:
480+
QMessageBox.warning(
481+
view, "Error", "No file path is associated with this session."
482+
)
483+
return
479484
rows_to_keep = []
480485
rows_to_delete = []
481486

482487
with open(file_path, newline="", encoding="utf-8") as f:
483488
reader = csv.DictReader(f, delimiter="\t")
484-
fieldnames = reader.fieldnames
489+
fieldnames = list(reader.fieldnames or [])
485490

486491
for row in reader:
487492
block_id = row.get("block_id", "")
@@ -528,11 +533,11 @@ def close_session(self, parent) -> None:
528533
parent,
529534
"Confirm Close Session",
530535
"Are you sure you want to close the current session? The session will be saved before closing.",
531-
QMessageBox.Ok | QMessageBox.Cancel,
532-
QMessageBox.Cancel,
536+
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
537+
QMessageBox.StandardButton.Cancel,
533538
)
534539

535-
if reply == QMessageBox.Ok:
540+
if reply == QMessageBox.StandardButton.Ok:
536541
self.session_data.close_file()
537542
parent.close()
538543

src/clinical_dbs_annotator/logging_config.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import threading
66
from logging.handlers import RotatingFileHandler
77
from pathlib import Path
8+
from types import TracebackType
89

910
from PySide6.QtCore import (
1011
QMessageLogContext,
@@ -21,8 +22,24 @@
2122
_crash_log_file = None
2223

2324

25+
def _safe_exc_info(
26+
exc_type: type[BaseException],
27+
exc_value: BaseException | None,
28+
exc_traceback: TracebackType | None,
29+
) -> (
30+
tuple[type[BaseException], BaseException, TracebackType | None]
31+
| tuple[None, None, None]
32+
):
33+
"""Normalize exception tuple for logging APIs that require non-optional exception values."""
34+
if exc_value is None:
35+
return (None, None, None)
36+
return (exc_type, exc_value, exc_traceback)
37+
38+
2439
def _install_exception_hooks() -> None:
25-
def exc_hook(exc_type, exc, tb):
40+
def exc_hook(
41+
exc_type: type[BaseException], exc: BaseException, tb: TracebackType | None
42+
) -> None:
2643
logging.getLogger("uncaught").critical(
2744
"Uncaught exception",
2845
exc_info=(exc_type, exc, tb),
@@ -33,7 +50,7 @@ def exc_hook(exc_type, exc, tb):
3350
def thread_exc_hook(args: threading.ExceptHookArgs) -> None:
3451
logging.getLogger("uncaught").critical(
3552
"Uncaught thread exception",
36-
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
53+
exc_info=_safe_exc_info(args.exc_type, args.exc_value, args.exc_traceback),
3754
)
3855

3956
threading.excepthook = thread_exc_hook
@@ -42,7 +59,7 @@ def unraisable_hook(args: sys.UnraisableHookArgs) -> None:
4259
logging.getLogger("uncaught").error(
4360
"Unraisable exception in %r",
4461
args.object,
45-
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
62+
exc_info=_safe_exc_info(args.exc_type, args.exc_value, args.exc_traceback),
4663
)
4764

4865
sys.unraisablehook = unraisable_hook

src/clinical_dbs_annotator/models/session_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def open_file_append(
128128
try:
129129
with open(file_path, newline="", encoding="utf-8") as f:
130130
reader = csv.DictReader(f, delimiter="\t")
131-
existing_fieldnames = reader.fieldnames
131+
existing_fieldnames = list(reader.fieldnames or [])
132132
except Exception:
133133
logger.warning(
134134
"Failed to read existing TSV headers, using defaults: %s",
@@ -388,7 +388,7 @@ def open_simple_file_append(self, filepath: str) -> None:
388388
try:
389389
with open(filepath, newline="", encoding="utf-8") as f:
390390
reader = csv.DictReader(f, delimiter="\t")
391-
fieldnames = reader.fieldnames
391+
fieldnames = list(reader.fieldnames or [])
392392
except Exception:
393393
logger.warning(
394394
"Failed reading annotation TSV headers, using defaults: %s",

src/clinical_dbs_annotator/ui/widgets.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import typing
99

10-
from PySide6.QtCore import QEvent, QSize, Qt, Signal
10+
from PySide6.QtCore import QByteArray, QEvent, QSize, Qt, Signal
1111
from PySide6.QtGui import QIcon, QPixmap
1212
from PySide6.QtWidgets import (
1313
QFrame,
@@ -168,10 +168,10 @@ def _create_arrow_button(self, direction: str, double: bool) -> QPushButton:
168168
"""
169169
btn = QPushButton()
170170
btn.setIcon(create_arrow_icon(direction, double))
171-
btn.setIconSize(QSize(*ICON_SIZES["increment"]))
172-
btn.setFixedSize(
173-
BUTTON_SIZES["increment"]["width"], BUTTON_SIZES["increment"]["height"]
174-
)
171+
icon_width, icon_height = typing.cast(tuple[int, int], ICON_SIZES["increment"])
172+
btn.setIconSize(QSize(icon_width, icon_height))
173+
increment_size = typing.cast(dict[str, int], BUTTON_SIZES["increment"])
174+
btn.setFixedSize(increment_size["width"], increment_size["height"])
175175
btn.setCursor(Qt.PointingHandCursor)
176176
btn.setStyleSheet(
177177
"""
@@ -353,7 +353,7 @@ def _create_lr_arrow_icon(self, direction: str, double: bool) -> QIcon:
353353
"""
354354

355355
pixmap = QPixmap()
356-
pixmap.loadFromData(bytes(svg, encoding="utf-8"), "SVG")
356+
pixmap.loadFromData(QByteArray(svg.encode("utf-8")))
357357
return QIcon(pixmap)
358358

359359
def _create_x_icon(self) -> QIcon:
@@ -366,7 +366,7 @@ def _create_x_icon(self) -> QIcon:
366366
</svg>
367367
"""
368368
pixmap = QPixmap()
369-
pixmap.loadFromData(bytes(svg, encoding="utf-8"), "SVG")
369+
pixmap.loadFromData(QByteArray(svg.encode("utf-8")))
370370
return QIcon(pixmap)
371371

372372
@typing.override
@@ -437,26 +437,22 @@ def _toggle_disabled(self) -> None:
437437
self.valueChanged.emit(self._value)
438438

439439
# Public API (mirrors old InteractiveProgressBar)
440-
@typing.override
441-
def setMinimum(self, value: int) -> None:
440+
def setMinimum(self, value: int) -> None: # noqa: N802
442441
"""Set the minimum internal value."""
443442
self._minimum = int(value)
444443
self.progress_bar.setMinimum(int(value))
445444

446-
@typing.override
447-
def setMaximum(self, value: int) -> None:
445+
def setMaximum(self, value: int) -> None: # noqa: N802
448446
"""Set the maximum internal value."""
449447
self._maximum = int(value)
450448
self.progress_bar.setMaximum(int(value))
451449

452-
@typing.override
453-
def setValue(self, value: int) -> None:
450+
def setValue(self, value: int) -> None: # noqa: N802
454451
"""Set the current value and update the progress bar."""
455452
self._value = int(value)
456453
self.progress_bar.setValue(int(value))
457454

458-
@typing.override
459-
def setFormat(self, format_str: str) -> None:
455+
def setFormat(self, format_str: str) -> None: # noqa: N802
460456
"""Set the text format displayed on the progress bar."""
461457
self.progress_bar.setFormat(format_str)
462458

@@ -477,8 +473,7 @@ def value(self) -> int:
477473
"""Return the current internal value."""
478474
return self._value
479475

480-
@typing.override
481-
def isDisabled(self) -> bool:
476+
def isDisabled(self) -> bool: # noqa: N802
482477
"""Return True if the widget is in disabled state."""
483478
return self._disabled
484479

src/clinical_dbs_annotator/utils/graphics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
and button animations.
66
"""
77

8-
from PySide6.QtCore import QRectF, Qt, QTimer
8+
from PySide6.QtCore import QByteArray, QRectF, Qt, QTimer
99
from PySide6.QtGui import QIcon, QPainter, QPainterPath, QPixmap
1010

1111
from ..config import BUTTON_PULSE_COUNT, BUTTON_PULSE_DURATION
@@ -54,7 +54,7 @@ def create_arrow_icon(direction: str = "up", double: bool = False) -> QIcon:
5454
"""
5555

5656
pixmap = QPixmap()
57-
pixmap.loadFromData(bytes(svg, encoding="utf-8"), "SVG")
57+
pixmap.loadFromData(QByteArray(svg.encode("utf-8")))
5858
return QIcon(pixmap)
5959

6060

0 commit comments

Comments
 (0)