From ddc3e9a3e55fa130cfc33667c58c44232f3e76d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20N=C3=BCrnberg?= Date: Mon, 15 Jul 2024 15:54:45 +0200 Subject: [PATCH] update code syntax --- src/pydcmqi/segimage.py | 101 ++++++++++++++++++---------------------- src/pydcmqi/types.py | 6 +-- tests/test_segimage.py | 53 +++++++++++---------- 3 files changed, 74 insertions(+), 86 deletions(-) diff --git a/src/pydcmqi/segimage.py b/src/pydcmqi/segimage.py index 9dd5d9d..4e81f2b 100644 --- a/src/pydcmqi/segimage.py +++ b/src/pydcmqi/segimage.py @@ -1,13 +1,12 @@ from __future__ import annotations import json -import os import shutil import subprocess import tempfile from collections import OrderedDict from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any import numpy as np import SimpleITK as sitk @@ -17,7 +16,7 @@ # --== helper and utility functions ==-- -def get_min_max_values(image: sitk.Image) -> Tuple[float, float]: +def get_min_max_values(image: sitk.Image) -> tuple[float, float]: filter = sitk.MinimumMaximumImageFilter() filter.Execute(image) return filter.GetMinimum(), filter.GetMaximum() @@ -28,7 +27,9 @@ def _path(path: str | Path) -> Path: return Path(path) if isinstance(path, Path): return path - raise ValueError("Invalid path type.") + + msg = "Invalid path type." + raise ValueError(msg) # --== class definitions ==-- @@ -58,7 +59,7 @@ def fromDict(d: TripletDict) -> Triplet: return Triplet(d["CodeMeaning"], d["CodeValue"], d["CodingSchemeDesignator"]) @staticmethod - def fromTuple(t: Tuple[str, str, str]) -> Triplet: + def fromTuple(t: tuple[str, str, str]) -> Triplet: """ Create a LabeledTriplet from a tuple. @@ -183,9 +184,6 @@ def validate(data: dict) -> bool: or Triplet.fromDict(data["AnatomicRegionModifierSequence"]).valid, ] - print("required_fields", required_fields) - print("optional_fields", optional_fields) - return all(required_fields) and all(optional_fields) def getConfigData(self, bypass_validation: bool = False) -> dict: @@ -194,8 +192,7 @@ def getConfigData(self, bypass_validation: bool = False) -> dict: @property def data(self) -> SegmentDict: - d = self._bake_data() - return d + return self._bake_data() def __getitem__(self, key: str) -> Any: return self._data[key] @@ -229,11 +226,11 @@ def description(self, description: str) -> None: self._data["SegmentDescription"] = description @property - def rgb(self) -> Tuple[int]: + def rgb(self) -> tuple[int, int, int]: return tuple(self._data["recommendedDisplayRGBValue"]) @rgb.setter - def rgb(self, rgb: Tuple[int]) -> None: + def rgb(self, rgb: tuple[int, int, int]) -> None: self._data["recommendedDisplayRGBValue"] = list(rgb) @property @@ -260,7 +257,7 @@ def segmentAlgorithmType(self) -> str: def segmentAlgorithmType(self, segmentAlgorithmType: str) -> None: self._data["SegmentAlgorithmType"] = segmentAlgorithmType - def _triplet_setter(self, key: str, value: Union[Tuple[str, str, str], Triplet]): + def _triplet_setter(self, key: str, value: tuple[str, str, str] | Triplet): if isinstance(value, tuple): value = Triplet.fromTuple(value) assert isinstance(value, Triplet) @@ -271,9 +268,7 @@ def segmentedPropertyCategory(self) -> Triplet: return self._triplet_factory("SegmentedPropertyCategoryCodeSequence") @segmentedPropertyCategory.setter - def segmentedPropertyCategory( - self, value: Union[Tuple[str, str, str], Triplet] - ) -> None: + def segmentedPropertyCategory(self, value: tuple[str, str, str] | Triplet) -> None: self._triplet_setter("SegmentedPropertyCategoryCodeSequence", value) @property @@ -281,9 +276,7 @@ def segmentedPropertyType(self) -> Triplet: return self._triplet_factory("SegmentedPropertyTypeCodeSequence") @segmentedPropertyType.setter - def segmentedPropertyType( - self, value: Union[Tuple[str, str, str], Triplet] - ) -> None: + def segmentedPropertyType(self, value: tuple[str, str, str] | Triplet) -> None: self._triplet_setter("SegmentedPropertyTypeCodeSequence", value) @property @@ -292,7 +285,7 @@ def segmentedPropertyTypeModifier(self) -> Triplet: @segmentedPropertyTypeModifier.setter def segmentedPropertyTypeModifier( - self, value: Union[Tuple[str, str, str], Triplet] + self, value: tuple[str, str, str] | Triplet ) -> None: self._triplet_setter("SegmentedPropertyTypeModifierCodeSequence", value) @@ -305,7 +298,7 @@ def anatomicRegion(self) -> Triplet: return self._triplet_factory("AnatomicRegionSequence") @anatomicRegion.setter - def anatomicRegion(self, value: Union[Tuple[str, str, str], Triplet]) -> None: + def anatomicRegion(self, value: tuple[str, str, str] | Triplet) -> None: self._triplet_setter("AnatomicRegionSequence", value) @property @@ -317,9 +310,7 @@ def anatomicRegionModifier(self) -> Triplet: return self._triplet_factory("AnatomicRegionModifierSequence") @anatomicRegionModifier.setter - def anatomicRegionModifier( - self, value: Union[Tuple[str, str, str], Triplet] - ) -> None: + def anatomicRegionModifier(self, value: tuple[str, str, str] | Triplet) -> None: self._triplet_setter("AnatomicRegionModifierSequence", value) @property @@ -329,11 +320,11 @@ def hasAnatomicRegionModifier(self) -> bool: class Segment: def __init__(self) -> None: - self.path: Optional[Path] = None + self.path: Path | None = None self.data = SegmentData() - self._cached_itk: Optional[sitk.Image] = None - self._cached_numpy: Optional[np.ndarray] = None + self._cached_itk: sitk.Image | None = None + self._cached_numpy: np.ndarray | None = None @property def config(self) -> dict: @@ -352,7 +343,7 @@ def labelID(self, labelID: int) -> None: self.data.labelID = labelID def setFile( - self, path: Union[str, Path], labelID: int, diable_sanity_check: bool = False + self, path: str | Path, labelID: int, diable_sanity_check: bool = False ) -> None: # make sure path is a Path object path = _path(path) @@ -365,12 +356,10 @@ def setFile( if not path.is_file(): raise ValueError(f"Path is not a file: {path}") - # check file has as many labels as expected - try: - image = sitk.ReadImage(str(path)) - except Exception: - raise ValueError(f"Could not read image: {path}") + # read image + image = sitk.ReadImage(str(path)) + # check file has as many labels as expected if image.GetNumberOfComponentsPerPixel() != 1: raise ValueError( f"Image must have only one component per pixel: {path}" @@ -414,8 +403,8 @@ def isMultiLabel(self) -> bool: def isLabel(self, label: int) -> bool: return label in np.unique(self.numpy) - def isLabelSet(self, labels: List[int]) -> bool: - return all([self.isLabel(l) for l in labels]) + def isLabelSet(self, labels: list[int]) -> bool: + return all(self.isLabel(label) for label in labels) def isLabelRange(self, start: int, end: int) -> bool: return self.isLabelSet(list(range(start, end + 1))) @@ -424,7 +413,7 @@ def isLabelRange(self, start: int, end: int) -> bool: def binary(self) -> np.ndarray: return self.numpy == self.labelID - def saveAsBinary(self, path: Union[str, Path]) -> None: + def saveAsBinary(self, path: str | Path) -> None: # make sure path is a Path object path = _path(path) @@ -523,15 +512,15 @@ def asdict(self) -> SegImageDict: class SegImageFiles: def __init__(self) -> None: - self._dicomseg: Optional[Path] = None - self._config: Optional[Path] = None + self._dicomseg: Path | None = None + self._config: Path | None = None @property - def dicomseg(self) -> Optional[Path]: + def dicomseg(self) -> Path | None: return self._dicomseg @property - def config(self) -> Optional[Path]: + def config(self) -> Path | None: return self._config @@ -543,7 +532,7 @@ def reset(cls) -> None: cls.verbose = False def __init__( - self, verbose: Optional[bool] = None, tmp_dir: Optional[Union[Path, str]] = None + self, verbose: bool | None = None, tmp_dir: Path | str | None = None ) -> None: # set verbose if verbose is not None: @@ -566,13 +555,13 @@ def __init__( self.loaded = False self.files = SegImageFiles() - self._config: Optional[dict] = None - self._segments: List[Segment] = [] + self._config: dict | None = None + self._segments: list[Segment] = [] def load( self, - dicomseg_file: Union[Path, str], - output_dir: Optional[Union[Path, str]] = None, + dicomseg_file: Path | str, + output_dir: Path | str | None = None, ) -> bool: print(f"Converting file: {dicomseg_file} into {output_dir}.") @@ -621,7 +610,7 @@ def _import( config_file = output_dir / "pydcmqi-meta.json" # load the config file - with open(config_file) as f: + with Path.open(config_file) as f: self._config = json.load(f) # load data @@ -631,7 +620,7 @@ def _import( # load each segmentation as item for i, s in enumerate(self._config["segmentAttributes"]): # find generated export file - f = os.path.join(output_dir, f"pydcmqi-{i+1}.nii.gz") + f = output_dir / f"pydcmqi-{i+1}.nii.gz" # load all configs from segment definition for config in s: @@ -650,9 +639,9 @@ def _import( def write( self, - output_file: Union[str, Path], - dicom_dir: Union[str, Path], - export_config_to_file: Optional[Union[str, Path]] = None, + output_file: str | Path, + dicom_dir: str | Path, + export_config_to_file: str | Path | None = None, allow_overwrite: bool = False, ) -> None: # make sure the output file is a Path object @@ -693,7 +682,7 @@ def write( # store in _debug_test_meta.json meta_tmp_file = Path(self.tmp_dir) / "_debug_test_meta.json" - with open(meta_tmp_file, "w") as f: + with Path.open(meta_tmp_file, "w") as f: json.dump(config, f, indent=2) # export config file if requested @@ -733,7 +722,7 @@ def config(self) -> SegImageDict: raise ValueError(f"Segment {s} has no file specified.") # sort segments by files - f2s: Dict[str, List[Segment]] = {} + f2s: dict[str, list[Segment]] = {} for s in self._segments: p = str(s.path) if p not in f2s: @@ -743,7 +732,7 @@ def config(self) -> SegImageDict: # sort the segments by their labelID f2s = {k: sorted(v, key=lambda x: x.labelID) for k, v in f2s.items()} - # order the dicionary by it's keys + # order the dictionary by it's keys of2s = OrderedDict(sorted(f2s.items())) # check that for all files @@ -765,15 +754,15 @@ def config(self) -> SegImageDict: return config @property - def segmentation_files(self) -> List[Path]: - return sorted(set([s.path for s in self._segments])) + def segmentation_files(self) -> list[Path]: + return sorted({s.path for s in self._segments}) @config.setter def config(self, config: SegImageDict) -> None: self.data.setConfigData(config) @property - def segments(self) -> List[Segment]: + def segments(self) -> list[Segment]: return self._segments def add_segment(self, segment: Segment) -> None: diff --git a/src/pydcmqi/types.py b/src/pydcmqi/types.py index 1ee6b4c..da1dedf 100644 --- a/src/pydcmqi/types.py +++ b/src/pydcmqi/types.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, TypedDict +from typing import TypedDict class TripletDict(TypedDict): @@ -15,7 +15,7 @@ class SegmentDict(TypedDict): SegmentDescription: str SegmentAlgorithmName: str SegmentAlgorithmType: str - recommendedDisplayRGBValue: List[int] + recommendedDisplayRGBValue: list[int] SegmentedPropertyCategoryCodeSequence: TripletDict SegmentedPropertyTypeCodeSequence: TripletDict @@ -29,4 +29,4 @@ class SegImageDict(TypedDict): InstanceNumber: str SeriesDescription: str SeriesNumber: str - segmentAttributes: List[List[SegmentDict]] + segmentAttributes: list[list[SegmentDict]] diff --git a/tests/test_segimage.py b/tests/test_segimage.py index 9e7a233..301e7ae 100644 --- a/tests/test_segimage.py +++ b/tests/test_segimage.py @@ -9,7 +9,7 @@ from pydcmqi.segimage import SegImage, SegmentData, Triplet -TEST_DIR = Path(os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_data")) +TEST_DIR = Path(__file__).resolve().parent / "test_data" # force loading of the segmentation data # NOTE: only set to false to speed up manual testing. @@ -22,10 +22,9 @@ def _iterative_dict_sort(d): if isinstance(d, list): return [_iterative_dict_sort(v) for v in d] - elif isinstance(d, dict): + if isinstance(d, dict): return {k: _iterative_dict_sort(v) for k, v in sorted(d.items())} - else: - return d + return d class TestTriplets: @@ -36,7 +35,7 @@ def test_triplet_from_tuple(self): assert t.label == "Liver" assert t.code == "123037004" assert t.scheme == "SCT" - assert t.valid == True + assert t.valid def test_triplet_from_dict(self): d = { @@ -49,7 +48,7 @@ def test_triplet_from_dict(self): assert t.label == "Anatomical Structure" assert t.code == "123037004" assert t.scheme == "SCT" - assert t.valid == True + assert t.valid def test_triplet_initializer(self): t = Triplet("Anatomical Structure", "123037004", "SCT") @@ -57,7 +56,7 @@ def test_triplet_initializer(self): assert t.label == "Anatomical Structure" assert t.code == "123037004" assert t.scheme == "SCT" - assert t.valid == True + assert t.valid def test_empty_triplet(self): t = Triplet.empty() @@ -65,7 +64,7 @@ def test_empty_triplet(self): assert t.label == "" assert t.code == "" assert t.scheme == "" - assert t.valid == False + assert not t.valid class TestSegmentData: @@ -124,8 +123,8 @@ def test_verbosity_default(self): segimg = SegImage() # verbose mode is disabled by default - assert SegImage.verbose == False - assert segimg.verbose == False + assert not SegImage.verbose + assert not segimg.verbose def test_verbosity_default_global_override(self): # set globally to true @@ -136,17 +135,17 @@ def test_verbosity_default_global_override(self): segimg2 = SegImage(verbose=False) # - assert SegImage.verbose == True - assert segimg1.verbose == True - assert segimg2.verbose == False + assert SegImage.verbose + assert segimg1.verbose + assert not segimg2.verbose def test_verbosity_instance_override(self): # initialize segimg = SegImage(verbose=True) # - assert SegImage.verbose == False - assert segimg.verbose == True + assert not SegImage.verbose + assert segimg.verbose ### tmp_dir def test_tmp_dir(self): @@ -157,10 +156,10 @@ def test_tmp_dir(self): segimg4 = SegImage(tmp_dir=Path("tmp")) # all instances will have a temp dir set - assert segimg1.tmp_dir != None - assert segimg2.tmp_dir != None - assert segimg3.tmp_dir != None - assert segimg4.tmp_dir != None + assert segimg1.tmp_dir is not None + assert segimg2.tmp_dir is not None + assert segimg3.tmp_dir is not None + assert segimg4.tmp_dir is not None # the type of the tmp_dir is Path, no matter how it was initilaized assert isinstance(segimg1.tmp_dir, Path) @@ -220,7 +219,7 @@ def setup_class(self): # check download and assign files sf = [f for f in os.listdir(self.seg_dir) if f.endswith(".dcm")] assert len(sf) == 1 - self.seg_file = os.path.join(self.seg_dir, sf[0]) + self.seg_file = self.seg_dir / sf[0] def setup_method(self): # rezet segimage @@ -236,17 +235,17 @@ def setup_method(self): self.segimg.load(self.seg_file, output_dir=self.out_dir) def test_loading(self): - assert self.segimg.loaded == True + assert self.segimg.loaded # files - assert self.segimg.files.config != None + assert self.segimg.files.config is not None assert self.segimg.files.config == self.out_dir / "pydcmqi-meta.json" - assert self.segimg.files.dicomseg != None + assert self.segimg.files.dicomseg is not None assert self.segimg.files.dicomseg == self.seg_file # config - assert self.segimg._config != None - assert self.segimg.getExportedConfiguration() != None + assert self.segimg._config is not None + assert self.segimg.getExportedConfiguration() is not None def test_loaded_segment_content(self): # check @@ -470,7 +469,7 @@ def setup_class(self): # check download and assign files sf = [f for f in os.listdir(self.seg_dir) if f.endswith(".dcm")] assert len(sf) == 1 - self.seg_file = os.path.join(self.seg_dir, sf[0]) + self.seg_file = self.seg_dir / sf[0] # get the segmentation files self.dseg_config_file = self.out_dir / "pydcmqi-meta.json" @@ -489,7 +488,7 @@ def test_write(self): # NOTE: this test is not yet dynamic and only works with the specified dicomseg file. # load original config - with open(self.dseg_config_file) as f: + with Path.open(self.dseg_config_file) as f: config = json.load(f) # initialize a SegImage instance used in multiple tests