diff --git a/.github/workflows/check_pull_request_title.yml b/.github/workflows/check_pull_request_title.yml index f333e0c56..ce96270a7 100644 --- a/.github/workflows/check_pull_request_title.yml +++ b/.github/workflows/check_pull_request_title.yml @@ -1,8 +1,13 @@ -name: "Check PR title" +name: Check PR title + on: pull_request: types: [edited, opened, synchronize, reopened] +permissions: + pull-requests: read + statuses: write # Required to update commit status (e.g. pass/fail) + jobs: pr-title-check: runs-on: ubuntu-latest @@ -14,12 +19,22 @@ jobs: - uses: naveenk1223/action-pr-title@master with: - # Valid titles: "Do something" + # ^ Start of string + # [A-Z] First character must be an uppercase ASCII letter + # [a-zA-Z]* Followed by zero or more ASCII letters + # (? list[Subject]: # The number of files for each modality is not the same # E.g. 581 for T1, 578 for T2 # Let's just use the first modality as reference for now @@ -137,7 +137,7 @@ def _get_subjects_list(root, modalities): subjects.append(Subject(**images_dict)) return subjects - def _download(self, root, modalities): + def _download(self, root: Path, modalities: Sequence[str]) -> None: """Download the IXI data if it does not exist already.""" for modality in modalities: modality_dir = root / modality @@ -195,7 +195,7 @@ def __init__( super().__init__(subjects_list, transform=transform, **kwargs) @staticmethod - def _get_subjects_list(root): + def _get_subjects_list(root: Path) -> list[Subject]: image_paths = sglob(root / 'image', '*.nii.gz') label_paths = sglob(root / 'label', '*.nii.gz') if not (image_paths and label_paths): @@ -214,7 +214,7 @@ def _get_subjects_list(root): subjects.append(Subject(**subject_dict)) return subjects - def _download(self, root): + def _download(self, root: Path) -> None: """Download the tiny IXI data if it doesn't exist already.""" if root.is_dir(): # assume it's been downloaded print('Root directory for IXITiny found:', root) # noqa: T201 @@ -234,9 +234,9 @@ def _download(self, root): shutil.rmtree(ixi_tiny_dir) -def sglob(directory, pattern): - return sorted(Path(directory).glob(pattern)) +def sglob(directory: Path, pattern: str) -> list[Path]: + return sorted(directory.glob(pattern)) -def get_subject_id(path): +def get_subject_id(path: Path) -> str: return '-'.join(path.name.split('-')[:-1]) diff --git a/src/torchio/utils.py b/src/torchio/utils.py index e90e7fac0..4a06e46d0 100644 --- a/src/torchio/utils.py +++ b/src/torchio/utils.py @@ -25,6 +25,9 @@ from .types import TypeNumber from .types import TypePath +ITK_SNAP = 'ITK-SNAP' +SLICER = 'Slicer' + def to_tuple( value: Any, @@ -344,44 +347,64 @@ def add_images_from_batch( def guess_external_viewer() -> Path | None: """Guess the path to an executable that could be used to visualize images. - Currently, it looks for 1) ITK-SNAP and 2) 3D Slicer. Implemented - for macOS and Windows. + It looks for 1) ITK-SNAP and 2) 3D Slicer. """ if 'SITK_SHOW_COMMAND' in os.environ: return Path(os.environ['SITK_SHOW_COMMAND']) - platform = sys.platform - itk = 'ITK-SNAP' - slicer = 'Slicer' - if platform == 'darwin': + + if (platform := sys.platform) == 'darwin': + return _guess_macos_viewer() + elif platform == 'win32': + return _guess_windows_viewer() + elif 'linux' in platform: + return _guess_linux_viewer() + else: + return None + + +def _guess_macos_viewer() -> Path | None: + def _get_app_path(app_name: str) -> Path: app_path = '/Applications/{}.app/Contents/MacOS/{}' - itk_snap_path = Path(app_path.format(2 * (itk,))) + return Path(app_path.format(2 * (app_name,))) + + if (itk_snap_path := _get_app_path(ITK_SNAP)).is_file(): + return itk_snap_path + elif (slicer_path := _get_app_path(SLICER)).is_file(): + return slicer_path + else: + return None + + +def _guess_windows_viewer() -> Path | None: + def _get_app_path(app_dirs: list[Path], bin_name: str) -> Path: + app_dir = app_dirs[-1] + app_path = app_dir / bin_name + if app_path.is_file(): + return app_path + + program_files_dir = Path(os.environ['ProgramW6432']) + itk_snap_dirs = list(program_files_dir.glob(f'{ITK_SNAP}*')) + slicer_dirs = list(program_files_dir.glob(f'{SLICER}*')) + + if itk_snap_dirs: + itk_snap_path = _get_app_path(itk_snap_dirs, 'bin/itk-snap.exe') if itk_snap_path.is_file(): return itk_snap_path - slicer_path = Path(app_path.format(2 * (slicer,))) + elif slicer_dirs: + slicer_path = _get_app_path(slicer_dirs, 'slicer.exe') if slicer_path.is_file(): return slicer_path - elif platform == 'win32': - program_files_dir = Path(os.environ['ProgramW6432']) - itk_snap_dirs = list(program_files_dir.glob('ITK-SNAP*')) - if itk_snap_dirs: - itk_snap_dir = itk_snap_dirs[-1] - itk_snap_path = itk_snap_dir / 'bin/itk-snap.exe' - if itk_snap_path.is_file(): - return itk_snap_path - slicer_dirs = list(program_files_dir.glob('Slicer*')) - if slicer_dirs: - slicer_dir = slicer_dirs[-1] - slicer_path = slicer_dir / 'slicer.exe' - if slicer_path.is_file(): - return slicer_path - elif 'linux' in platform: - itk_snap_which = shutil.which('itksnap') - if itk_snap_which is not None: - return Path(itk_snap_which) - slicer_which = shutil.which('Slicer') - if slicer_which is not None: - return Path(slicer_which) - return None # for mypy + else: + return None + + +def _guess_linux_viewer() -> Path | None: + if (itk_snap_which := shutil.which('itksnap')) is not None: + return Path(itk_snap_which) + elif (slicer_which := shutil.which('Slicer')) is not None: + return Path(slicer_which) + else: + return None def parse_spatial_shape(shape):