Skip to content

Commit f1b192c

Browse files
fedorovclaude
andcommitted
fix all 30 pre-existing mypy errors
- Add mypy overrides for missing stubs (numpy, SimpleITK, pydicom, idc_index) - Use cast() for dict[str, Any] property returns in SegmentData - Widen _triplet_setter type to accept Any for duck-typed Code objects - Move config setter adjacent to config property getter (fixes mypy no-redef) - Remove unreachable branch in _path() and _triplet_setter - Add type annotations to _iterative_dict_sort in tests - Fix SegImage(tmp_dir=...) keyword args in tests (was positional) - Add type: ignore comments for intentional dynamic typing in tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cc46d2c commit f1b192c

5 files changed

Lines changed: 54 additions & 31 deletions

File tree

pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ module = "pydcmqi.*"
106106
disallow_untyped_defs = true
107107
disallow_incomplete_defs = true
108108

109+
[[tool.mypy.overrides]]
110+
module = [
111+
"numpy",
112+
"numpy.*",
113+
"SimpleITK",
114+
"SimpleITK.*",
115+
"pydicom.*",
116+
"idc_index.*",
117+
]
118+
ignore_missing_imports = true
119+
120+
[[tool.mypy.overrides]]
121+
module = [
122+
"pydcmqi.segment",
123+
"test_segimage",
124+
]
125+
disallow_untyped_calls = false
126+
warn_unused_ignores = false
127+
109128

110129
[tool.ruff]
111130
src = ["src"]

src/pydcmqi/segimage.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -369,19 +369,22 @@ def config(self) -> SegImageDict:
369369
raise ValueError(f"Duplicate labelIDs found in {file_path}.")
370370

371371
# add each segment to the config
372-
config["segmentAttributes"] = [[s.config for s in ss] for ss in of2s.values()]
372+
config["segmentAttributes"] = [
373+
[s.config for s in ss] # type: ignore[misc]
374+
for ss in of2s.values()
375+
]
373376

374377
# return the generated config
375378
return config
376379

377-
@property
378-
def segmentation_files(self) -> list[Path]:
379-
return sorted(s.path for s in self._segments if s.path is not None)
380-
381380
@config.setter
382381
def config(self, config: SegImageDict) -> None:
383382
self.data.setConfigData(config)
384383

384+
@property
385+
def segmentation_files(self) -> list[Path]:
386+
return sorted(s.path for s in self._segments if s.path is not None)
387+
385388
@property
386389
def segments(self) -> list[Segment]:
387390
return self._segments

src/pydcmqi/segment.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Any
2+
from typing import Any, cast
33

44
import numpy as np
55
import SimpleITK as sitk
@@ -120,19 +120,19 @@ def _triplet_factory(self, key: str) -> Triplet:
120120
else:
121121
t = Triplet.empty()
122122
setattr(self, f"__tpf_{key}", t)
123-
return getattr(self, f"__tpf_{key}")
123+
return cast(Triplet, getattr(self, f"__tpf_{key}"))
124124

125125
@property
126126
def label(self) -> str:
127-
return self._data["SegmentLabel"]
127+
return cast(str, self._data["SegmentLabel"])
128128

129129
@label.setter
130130
def label(self, label: str) -> None:
131131
self._data["SegmentLabel"] = label
132132

133133
@property
134134
def description(self) -> str:
135-
return self._data["SegmentDescription"]
135+
return cast(str, self._data["SegmentDescription"])
136136

137137
@description.setter
138138
def description(self, description: str) -> None:
@@ -148,29 +148,31 @@ def rgb(self, rgb: tuple[int, int, int]) -> None:
148148

149149
@property
150150
def labelID(self) -> int:
151-
return self._data["labelID"]
151+
return cast(int, self._data["labelID"])
152152

153153
@labelID.setter
154154
def labelID(self, labelID: int) -> None:
155155
self._data["labelID"] = labelID
156156

157157
@property
158158
def segmentAlgorithmName(self) -> str:
159-
return self._data["SegmentAlgorithmName"]
159+
return cast(str, self._data["SegmentAlgorithmName"])
160160

161161
@segmentAlgorithmName.setter
162162
def segmentAlgorithmName(self, segmentAlgorithmName: str) -> None:
163163
self._data["SegmentAlgorithmName"] = segmentAlgorithmName
164164

165165
@property
166166
def segmentAlgorithmType(self) -> str:
167-
return self._data["SegmentAlgorithmType"]
167+
return cast(str, self._data["SegmentAlgorithmType"])
168168

169169
@segmentAlgorithmType.setter
170170
def segmentAlgorithmType(self, segmentAlgorithmType: str) -> None:
171171
self._data["SegmentAlgorithmType"] = segmentAlgorithmType
172172

173-
def _triplet_setter(self, key: str, value: tuple[str, str, str] | Triplet) -> None:
173+
def _triplet_setter(
174+
self, key: str, value: tuple[str, str, str] | Triplet | Any
175+
) -> None:
174176
if isinstance(value, Triplet):
175177
pass
176178
elif isinstance(value, tuple):
@@ -343,7 +345,8 @@ def isLabelRange(self, start: int, end: int) -> bool:
343345

344346
@property
345347
def binary(self) -> np.ndarray:
346-
return self.numpy == self.labelID
348+
result: np.ndarray = self.numpy == self.labelID
349+
return result
347350

348351
def saveAsBinary(self, path: str | Path) -> None:
349352
# make sure path is a Path object

src/pydcmqi/triplet.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
def _path(path: str | Path) -> Path:
1010
if isinstance(path, Path):
1111
return path
12-
if isinstance(path, str):
13-
return Path(path)
14-
msg = "Invalid path type."
15-
raise ValueError(msg)
12+
return Path(path)
1613

1714

1815
class Triplet:

tests/test_segimage.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from idc_index import index
77

8-
from pydcmqi import DcmqiError, SegImage, SegmentData, Triplet
8+
from pydcmqi import DcmqiError, SegImage, SegmentData, Triplet, TripletDict
99

1010
TEST_DIR = Path(__file__).resolve().parent / "test_data"
1111

@@ -17,7 +17,7 @@
1717
# helper function to sort dictionaries
1818
# ONLY FOR FILE EXPORT
1919
# DICTS ARE NOT PERSISTENTLY ORDERED IN PYTHON
20-
def _iterative_dict_sort(d):
20+
def _iterative_dict_sort(d: object) -> object:
2121
if isinstance(d, list):
2222
return [_iterative_dict_sort(v) for v in d]
2323
if isinstance(d, dict):
@@ -36,7 +36,7 @@ def test_triplet_from_tuple(self):
3636
assert t.valid
3737

3838
def test_triplet_from_dict(self):
39-
d = {
39+
d: TripletDict = {
4040
"CodeMeaning": "Anatomical Structure",
4141
"CodeValue": "123037004",
4242
"CodingSchemeDesignator": "SCT",
@@ -68,7 +68,7 @@ def test_empty_triplet(self):
6868
class TestSegmentData:
6969
def test_triplet_property_from_tuple(self):
7070
d = SegmentData()
71-
d.segmentedPropertyCategory = ("Anatomical Structure", "123037004", "SCT")
71+
d.segmentedPropertyCategory = ("Anatomical Structure", "123037004", "SCT") # type: ignore[assignment]
7272

7373
assert isinstance(d.segmentedPropertyCategory, Triplet)
7474
assert d.segmentedPropertyCategory.label == "Anatomical Structure"
@@ -170,7 +170,7 @@ def test_tmp_dir(self):
170170
ValueError,
171171
match="Invalid tmp_dir, must be either None for default, a Path or a string.",
172172
):
173-
_ = SegImage(tmp_dir=1)
173+
_ = SegImage(tmp_dir=1) # type: ignore[arg-type]
174174

175175

176176
class TestSegimageRead:
@@ -189,7 +189,7 @@ def setup_class(self):
189189
self.out_dir.mkdir(parents=True, exist_ok=True)
190190

191191
# initialize a SegImage instance used in multiple tests
192-
self.segimg = SegImage(self.tmp_dir)
192+
self.segimg = SegImage(tmp_dir=self.tmp_dir)
193193

194194
# initialize idc index client
195195
client = index.IDCClient()
@@ -477,12 +477,13 @@ def setup_class(self):
477477
self.lung_seg_file = self.out_dir / "pydcmqi-1.nii.gz"
478478
self.tumor_seg_file = self.out_dir / "pydcmqi-2.nii.gz"
479479

480-
# extract nifit files from segmentation if not already done
480+
# extract nifti files from segmentation if not already done
481481
if (
482482
not self.dseg_config_file.exists()
483483
or not self.lung_seg_file.exists()
484484
or not self.tumor_seg_file.exists()
485485
):
486+
self.segimg = SegImage(tmp_dir=self.tmp_dir)
486487
self.segimg.load(self.seg_file, output_dir=self.out_dir)
487488

488489
def test_write(self):
@@ -493,7 +494,7 @@ def test_write(self):
493494
config = json.load(f)
494495

495496
# initialize a SegImage instance used in multiple tests
496-
segimg = SegImage(self.tmp_dir)
497+
segimg = SegImage(tmp_dir=self.tmp_dir)
497498

498499
# specify segimg data
499500
segimg.data.bodyPartExamined = "LUNG"
@@ -513,13 +514,13 @@ def test_write(self):
513514
lung.data.labelID = 1
514515
lung.data.segmentAlgorithmName = "BAMF-Lung-FDG-PET-CT"
515516
lung.data.segmentAlgorithmType = "AUTOMATIC"
516-
lung.data.segmentedPropertyCategory = (
517+
lung.data.segmentedPropertyCategory = ( # type: ignore[assignment]
517518
"Anatomical Structure",
518519
"123037004",
519520
"SCT",
520521
)
521-
lung.data.segmentedPropertyType = ("Lung", "39607008", "SCT")
522-
lung.data.segmentedPropertyTypeModifier = ("Right and left", "51440002", "SCT")
522+
lung.data.segmentedPropertyType = ("Lung", "39607008", "SCT") # type: ignore[assignment]
523+
lung.data.segmentedPropertyTypeModifier = ("Right and left", "51440002", "SCT") # type: ignore[assignment]
523524

524525
lung.setFile(self.lung_seg_file, labelID=1)
525526

@@ -531,7 +532,7 @@ def test_write(self):
531532
tumor.data.labelID = 2
532533
tumor.data.segmentAlgorithmName = "BAMF-Lung-FDG-PET-CT"
533534
tumor.data.segmentAlgorithmType = "AUTOMATIC"
534-
tumor.data.segmentedPropertyCategory = ("Radiologic Finding", "C35869", "NCIt")
535+
tumor.data.segmentedPropertyCategory = ("Radiologic Finding", "C35869", "NCIt") # type: ignore[assignment]
535536
tumor.data.segmentedPropertyType.label = "FDG-Avid Tumor"
536537
tumor.data.segmentedPropertyType.code = "C168968"
537538
tumor.data.segmentedPropertyType.scheme = "NCIt"
@@ -574,7 +575,7 @@ def test_triplet_setter_rejects_invalid_type(self):
574575

575576
d = SegmentData()
576577
with pytest.raises(TypeError, match="Expected Triplet, tuple, or Code-like"):
577-
d.segmentedPropertyCategory = 42
578+
d.segmentedPropertyCategory = 42 # type: ignore[assignment]
578579

579580
def test_segment_data_validation_error(self):
580581
d = SegmentData()

0 commit comments

Comments
 (0)