Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update supported python versions #112

Merged
merged 3 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
- published

env:
MINIMUM_PYTHON_VERSION: '3.8'
MINIMUM_PYTHON_VERSION: '3.9'

concurrency:
group: ${{ github.head_ref || github.run_id }}
Expand Down Expand Up @@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v5.0.0
hooks:
- id: check-docstring-first
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
rev: v3.18.0
hooks:
- id: pyupgrade
language: python
args: [--py38-plus]
args: [--py39-plus]
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/ambv/black
rev: 23.9.1
rev: 24.10.0
hooks:
- id: black
language: python
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
rev: 7.1.1
hooks:
- id: flake8
language: python
Expand All @@ -35,7 +35,7 @@ repos:
- yesqa
exclude: ^panimg/contrib/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.6.0'
rev: 'v1.11.2'
hooks:
- id: mypy
additional_dependencies:
Expand Down
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# History

## 0.14.0 (2024-10-15)

* Removed support for Python 3.8
* Added support for Python 3.12 and 3.13

## 0.13.2 (2023-10-23)

* Fix DICOM-WSI conversion issue where not all levels were converted correctly
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
**NOT FOR CLINICAL USE**

Conversion of medical images to MHA and TIFF.
Requires Python 3.8, 3.9, 3.10 or 3.11.
Requires Python 3.9, 3.10, 3.11, 3.12 or 3.13.
`libvips-dev` and `libopenslide-dev` must be installed on your system.

Under the hood we use:
Expand Down
3 changes: 1 addition & 2 deletions panimg/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
from typing import Dict, List


class ValidationError(Exception):
Expand All @@ -13,6 +12,6 @@ class UnconsumedFilesException(Exception): # noqa: N818
the unconsumed file.
"""

def __init__(self, *args, file_errors: Dict[Path, List[str]]):
def __init__(self, *args, file_errors: dict[Path, list[str]]):
super().__init__(*args)
self.file_errors = file_errors
2 changes: 1 addition & 1 deletion panimg/image_builders/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable
from collections.abc import Iterable

from panimg.image_builders.dicom import image_builder_dicom
from panimg.image_builders.fallback import image_builder_fallback
Expand Down
21 changes: 11 additions & 10 deletions panimg/image_builders/dicom.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging
from collections import defaultdict
from collections.abc import Iterator
from math import isclose
from pathlib import Path
from typing import Any, DefaultDict, Dict, Iterator, List, Set, Tuple
from typing import Any, DefaultDict

import numpy as np
import pydicom
Expand Down Expand Up @@ -248,7 +249,7 @@ def _read_pixel_values(self, filename: Path, rescale: bool) -> np.ndarray:
else:
return pixel_array

def _shape(self, samples_per_pixel: int) -> Tuple[int, ...]:
def _shape(self, samples_per_pixel: int) -> tuple[int, ...]:
pixel_dims = [
self.n_slices,
self.ref_header.Rows,
Expand Down Expand Up @@ -375,7 +376,7 @@ def read(self) -> SimpleITKImage:


def get_dicom_headers_by_study(
files: Set[Path], file_errors: DefaultDict[Path, List[str]]
files: set[Path], file_errors: DefaultDict[Path, list[str]]
):
"""
Gets all headers from dicom files found in path.
Expand All @@ -393,9 +394,9 @@ def get_dicom_headers_by_study(
A dictionary of headers for all dicom image files found within path,
grouped by study id.
"""
study_key_type = Tuple[str, ...]
studies: Dict[study_key_type, Dict[str, Any]] = {}
indices: Dict[str, Dict[study_key_type, int]] = {}
study_key_type = tuple[str, ...]
studies: dict[study_key_type, dict[str, Any]] = {}
indices: dict[str, dict[study_key_type, int]] = {}

for file in files:
if not file.is_file():
Expand Down Expand Up @@ -443,8 +444,8 @@ def get_dicom_headers_by_study(


def _find_valid_dicom_files(
files: Set[Path], file_errors: DefaultDict[Path, List[str]]
) -> List[DicomDataset]:
files: set[Path], file_errors: DefaultDict[Path, list[str]]
) -> list[DicomDataset]:
"""
Gets the headers for all dicom files on path and validates them.

Expand Down Expand Up @@ -529,7 +530,7 @@ def _find_valid_dicom_files(
return result


def image_builder_dicom(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
def image_builder_dicom(*, files: set[Path]) -> Iterator[SimpleITKImage]:
"""
Constructs image objects by inspecting files in a directory.

Expand All @@ -546,7 +547,7 @@ def image_builder_dicom(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
- a list files associated with the detected images
- path->error message map describing what is wrong with a given file
"""
file_errors: DefaultDict[Path, List[str]] = defaultdict(list)
file_errors: DefaultDict[Path, list[str]] = defaultdict(list)

studies = _find_valid_dicom_files(files=files, file_errors=file_errors)
for dicom_ds in studies:
Expand Down
7 changes: 4 additions & 3 deletions panimg/image_builders/fallback.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import defaultdict
from collections.abc import Iterator
from pathlib import Path
from typing import DefaultDict, Iterator, List, Set
from typing import DefaultDict

import numpy as np
import SimpleITK
Expand All @@ -15,7 +16,7 @@ def format_error(message: str) -> str:
return f"Fallback image builder: {message}"


def image_builder_fallback(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
def image_builder_fallback(*, files: set[Path]) -> Iterator[SimpleITKImage]:
"""
Constructs image objects by inspecting files in a directory.

Expand All @@ -33,7 +34,7 @@ def image_builder_fallback(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
- a list files associated with the detected images
- path->error message map describing what is wrong with a given file
"""
file_errors: DefaultDict[Path, List[str]] = defaultdict(list)
file_errors: DefaultDict[Path, list[str]] = defaultdict(list)

for file in files:
try:
Expand Down
10 changes: 6 additions & 4 deletions panimg/image_builders/metaio_mhd_mha.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@

See: https://itk.org/Wiki/MetaIO/Documentation
"""

from collections import defaultdict
from collections.abc import Iterator, Mapping
from pathlib import Path
from typing import DefaultDict, Dict, Iterator, List, Mapping, Set, Union
from typing import DefaultDict, Union

from panimg.exceptions import UnconsumedFilesException, ValidationError
from panimg.image_builders.metaio_utils import load_sitk_image, parse_mh_header
from panimg.models import SimpleITKImage


def image_builder_mhd( # noqa: C901
*, files: Set[Path]
*, files: set[Path]
) -> Iterator[SimpleITKImage]:
"""
Constructs image objects by inspecting files in a directory.
Expand All @@ -31,11 +33,11 @@ def image_builder_mhd( # noqa: C901
- files associated with the detected images
- path->error message map describing what is wrong with a given file
"""
file_errors: DefaultDict[Path, List[str]] = defaultdict(list)
file_errors: DefaultDict[Path, list[str]] = defaultdict(list)

element_data_file_key = "ElementDataFile"

def detect_mhd_file(headers: Dict[str, str], path: Path) -> bool:
def detect_mhd_file(headers: dict[str, str], path: Path) -> bool:
try:
data_file = headers[element_data_file_key]
except KeyError:
Expand Down
7 changes: 4 additions & 3 deletions panimg/image_builders/metaio_nifti.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import defaultdict
from collections.abc import Iterator
from pathlib import Path
from typing import DefaultDict, Iterator, List, Set
from typing import DefaultDict

from panimg.exceptions import UnconsumedFilesException, ValidationError
from panimg.image_builders.metaio_utils import load_sitk_image
Expand All @@ -11,7 +12,7 @@ def format_error(message: str) -> str:
return f"NifTI image builder: {message}"


def image_builder_nifti(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
def image_builder_nifti(*, files: set[Path]) -> Iterator[SimpleITKImage]:
"""
Constructs image objects from files in NifTI format (nii/nii.gz)

Expand All @@ -28,7 +29,7 @@ def image_builder_nifti(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
- a list files associated with the detected images
- path->error message map describing what is wrong with a given file
"""
file_errors: DefaultDict[Path, List[str]] = defaultdict(list)
file_errors: DefaultDict[Path, list[str]] = defaultdict(list)

for file in files:
if not (file.name.endswith(".nii") or file.name.endswith(".nii.gz")):
Expand Down
7 changes: 4 additions & 3 deletions panimg/image_builders/metaio_nrrd.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import re
from collections import defaultdict
from collections.abc import Iterator
from pathlib import Path
from typing import DefaultDict, Iterator, List, Set
from typing import DefaultDict

from panimg.exceptions import UnconsumedFilesException, ValidationError
from panimg.image_builders.metaio_utils import load_sitk_image
Expand Down Expand Up @@ -70,7 +71,7 @@ def format_error(message: str) -> str:
return f"NRRD image builder: {message}"


def image_builder_nrrd(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
def image_builder_nrrd(*, files: set[Path]) -> Iterator[SimpleITKImage]:
"""
Constructs image objects from files in NRRD format (nrrd)

Expand All @@ -92,7 +93,7 @@ def image_builder_nrrd(*, files: Set[Path]) -> Iterator[SimpleITKImage]:
- a list files associated with the detected images
- path->error message map describing what is wrong with a given file
"""
file_errors: DefaultDict[Path, List[str]] = defaultdict(list)
file_errors: DefaultDict[Path, list[str]] = defaultdict(list)

for file in files:
try:
Expand Down
26 changes: 13 additions & 13 deletions panimg/image_builders/metaio_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from pathlib import Path
from typing import Any, Dict, List
from typing import Any

import SimpleITK

Expand Down Expand Up @@ -70,18 +70,18 @@
**{md.keyword: md.match_pattern for md in EXTRA_METADATA},
}

HEADERS_MATCHING_NUM_TIMEPOINTS: List[str] = ["Exposures", "ContentTimes"]
HEADERS_MATCHING_NUM_TIMEPOINTS: list[str] = ["Exposures", "ContentTimes"]

HEADERS_MATCHING_WINDOW_SETTINGS: List[str] = ["WindowCenter", "WindowWidth"]
HEADERS_MATCHING_WINDOW_SETTINGS: list[str] = ["WindowCenter", "WindowWidth"]

HEADERS_WITH_LISTING: List[str] = [
HEADERS_WITH_LISTING: list[str] = [
"TransformMatrix",
"Offset",
"CenterOfRotation",
"ElementSpacing",
]

EXPECTED_HEADERS: List[str] = [
EXPECTED_HEADERS: list[str] = [
"ObjectType",
"NDims",
"BinaryData",
Expand All @@ -100,7 +100,7 @@
]


def parse_mh_header(file: Path) -> Dict[str, str]:
def parse_mh_header(file: Path) -> dict[str, str]:
"""
Attempts to parse the headers of an mhd file.

Expand All @@ -125,7 +125,7 @@ def parse_mh_header(file: Path) -> Dict[str, str]:
# attempt to limit number of read headers to prevent overflow attacks
read_line_limit = 10000

result: Dict[str, str] = {}
result: dict[str, str] = {}
with file.open("rb") as f:
lines = True
while lines:
Expand Down Expand Up @@ -155,7 +155,7 @@ def parse_mh_header(file: Path) -> Dict[str, str]:
return result


def extract_key_value_pairs(line: str) -> Dict[str, str]:
def extract_key_value_pairs(line: str) -> dict[str, str]:
line = line.rstrip("\n\r")
if line.strip() and "=" in line:
key, value = line.split("=", 1)
Expand All @@ -165,13 +165,13 @@ def extract_key_value_pairs(line: str) -> Dict[str, str]:


def extract_header_listing(
property: str, headers: Dict[str, str], dtype: type = float
) -> List[Any]:
property: str, headers: dict[str, str], dtype: type = float
) -> list[Any]:
return [dtype(e) for e in headers[property].strip().split(" ")]


def resolve_mh_data_file_path(
headers: Dict[str, str], is_mha: bool, mhd_file: Path
headers: dict[str, str], is_mha: bool, mhd_file: Path
) -> Path:
if is_mha:
data_file_path = mhd_file
Expand All @@ -186,7 +186,7 @@ def resolve_mh_data_file_path(

def validate_and_clean_additional_mh_headers(
reader: SimpleITK.ImageFileReader,
) -> Dict[str, str]:
) -> dict[str, str]:
cleaned_headers = {}
for key in reader.GetMetaDataKeys():
value = reader.GetMetaData(key)
Expand Down Expand Up @@ -255,7 +255,7 @@ def validate_list_data_matches_num_timepoints(


def add_additional_mh_headers_to_sitk_image(
sitk_image: SimpleITK.Image, cleaned_headers: Dict[str, str]
sitk_image: SimpleITK.Image, cleaned_headers: dict[str, str]
):
for header in ADDITIONAL_HEADERS:
if header in cleaned_headers:
Expand Down
Loading
Loading