Skip to content

Commit 2a0116d

Browse files
authored
Merge branch 'dev' into fix/ghsa-download-integrity
2 parents 43de468 + 434c094 commit 2a0116d

7 files changed

Lines changed: 181 additions & 35 deletions

File tree

.pre-commit-config.yaml

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ repos:
1313
hooks:
1414
- id: end-of-file-fixer
1515
- id: trailing-whitespace
16+
- id: check-ast
1617
- id: check-yaml
1718
- id: check-docstring-first
1819
- id: check-executables-have-shebangs
1920
- id: check-toml
2021
- id: check-case-conflict
22+
- id: check-illegal-windows-names
2123
- id: check-added-large-files
2224
args: ['--maxkb=1024']
2325
- id: detect-private-key
@@ -26,8 +28,9 @@ repos:
2628
args: ['--autofix', '--no-sort-keys', '--indent=4']
2729
- id: end-of-file-fixer
2830
- id: mixed-line-ending
31+
2932
- repo: https://github.com/astral-sh/ruff-pre-commit
30-
rev: v0.15.20
33+
rev: v0.16.5
3134
hooks:
3235
- id: ruff-check
3336
args: ["--fix"]
@@ -37,8 +40,25 @@ repos:
3740
^monai/_version.py
3841
)
3942
40-
- repo: https://github.com/hadialqattan/pycln
41-
rev: v2.6.0
43+
- repo: https://github.com/psf/black-pre-commit-mirror
44+
rev: 26.5.1 # Black version, keep synced with MONAI requirements
4245
hooks:
43-
- id: pycln
44-
args: [--config=pyproject.toml]
46+
- id: black
47+
language_version: python3
48+
# black will be given individual file names and so will ignore the excludes in pyproject.toml
49+
exclude: |
50+
(?x)(
51+
^versioneer.py|
52+
^monai/_version.py
53+
)
54+
55+
- repo: https://github.com/pycqa/isort
56+
rev: 9.0.1 # isort version, keep synced with MONAI requirements
57+
hooks:
58+
- id: isort
59+
name: isort (python)
60+
exclude: |
61+
(?x)(
62+
^versioneer.py|
63+
^monai/_version.py
64+
)

monai/auto3dseg/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,14 @@ def algo_from_json(filename: str, template_path: PathLike | None = None, **kwarg
493493
if state_template_path:
494494
algo_config["template_path"] = state_template_path
495495

496+
warnings.warn(
497+
f"Loading {filename}: the file's `_target_` value is resolved to an imported callable and "
498+
"invoked, and template directories from the file may be added to `sys.path`; only load "
499+
"algo_object.json files from a source you trust "
500+
"(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-2wx3-8x3w-r8qv).",
501+
stacklevel=2,
502+
)
503+
496504
parser = ConfigParser(algo_config)
497505
algo = parser.get_parsed_content()
498506
used_template_path = path

monai/data/image_reader.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ def _get_affine(self, img, lps_to_ras: bool = True):
347347
affine: np.ndarray = np.eye(sr + 1)
348348
affine[:sr, :sr] = direction[:sr, :sr] @ np.diag(spacing[:sr])
349349
affine[:sr, -1] = origin[:sr]
350+
350351
if lps_to_ras:
351352
affine = orientation_ras_lps(affine)
352353
return affine
@@ -752,13 +753,25 @@ def _get_affine(self, metadata: dict, lps_to_ras: bool = True):
752753
stacklevel=2,
753754
)
754755
return affine
756+
757+
def _raise_if_not_finite(values: Sequence[Any], tag: str) -> None:
758+
if not np.isfinite(tuple(values)).all():
759+
raise ValueError(
760+
f"PydicomReader: cannot derive affine matrix because DICOM tag {tag} "
761+
f"has a non-finite value: {values}."
762+
)
763+
755764
# "00200037" is the tag of `ImageOrientationPatient`
756765
rx, ry, rz, cx, cy, cz = metadata["00200037"]["Value"]
766+
_raise_if_not_finite((rx, ry, rz, cx, cy, cz), "ImageOrientationPatient (0020,0037)")
757767
# "00200032" is the tag of `ImagePositionPatient`
758768
sx, sy, sz = metadata["00200032"]["Value"]
769+
_raise_if_not_finite((sx, sy, sz), "ImagePositionPatient (0020,0032)")
759770
# "00280030" is the tag of `PixelSpacing`
760771
spacing = metadata["00280030"]["Value"] if "00280030" in metadata else (1.0, 1.0)
772+
_raise_if_not_finite(tuple(spacing), "PixelSpacing (0028,0030)")
761773
dr, dc = metadata.get("spacing", spacing)[:2]
774+
_raise_if_not_finite((dr, dc), "spacing")
762775
affine[0, 0] = cx * dr
763776
affine[0, 1] = rx * dc
764777
affine[0, 3] = sx
@@ -773,12 +786,16 @@ def _get_affine(self, metadata: dict, lps_to_ras: bool = True):
773786
# 3d
774787
if "lastImagePositionPatient" in metadata:
775788
t1n, t2n, t3n = metadata["lastImagePositionPatient"]
789+
_raise_if_not_finite((t1n, t2n, t3n), "lastImagePositionPatient")
776790
n = metadata[MetaKeys.SPATIAL_SHAPE][-1]
777791
if n > 1:
778792
affine[0, 2] = (t1n - sx) / (n - 1)
779793
affine[1, 2] = (t2n - sy) / (n - 1)
780794
affine[2, 2] = (t3n - sz) / (n - 1)
781795

796+
if not np.isfinite(affine).all():
797+
raise ValueError("PydicomReader: affine matrix not finite after composition.")
798+
782799
if lps_to_ras:
783800
affine = orientation_ras_lps(affine)
784801
return affine

pyproject.toml

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ mlflow = ["mlflow>=3.15.2"]
130130
nibabel = ["nibabel"]
131131
nni = [
132132
"nni; platform_system == 'Linux' and 'arm' not in platform_machine and 'aarch' not in platform_machine",
133-
"filelock<3.12.0" # https://github.com/microsoft/nni/issues/5523
133+
"filelock<3.12.0", # https://github.com/microsoft/nni/issues/5523
134+
"typeguard<3" # https://github.com/microsoft/nni/issues/5457
134135
]
135136
onnx = ["onnx>=1.13.0", "onnxruntime; python_version <= '3.10'", "onnx_graphsurgeon", "onnxscript"]
136137
openslide = ["openslide-python", "openslide-bin"]
@@ -159,9 +160,9 @@ transformers = ["transformers>=5.5.0"] # 5.x needs the transchex BertLayer/Bert
159160
zarr = ["zarr"]
160161
# these dependencies are for testing/building only, they aren't needed for regular use so don't appear in "all"
161162
testing = [
162-
"black>=26.3.1",
163+
"black>=26.5.1",
163164
"coverage>=5.5",
164-
"isort>=5.1,<6,!=6.0.0",
165+
"isort>9.0.0",
165166
"mccabe",
166167
"packaging",
167168
"parameterized",
@@ -170,9 +171,8 @@ testing = [
170171
"pycodestyle",
171172
"pyflakes",
172173
"pyrefly>=1.0.0",
173-
"ruff>=0.14.11,<0.15",
174+
"ruff>=0.16.5",
174175
"tomli", # used in print_dependencies.py for Python<3.11
175-
"typeguard<3", # https://github.com/microsoft/nni/issues/5457
176176
"types-PyYAML",
177177
"types-setuptools"
178178
]
@@ -289,39 +289,35 @@ exclude = '''
289289
)
290290
'''
291291

292-
[tool.pycln]
293-
all = true
294-
exclude = "monai/bundle/__main__.py"
295-
296292
[tool.ruff]
297293
line-length = 120
298294
target-version = "py310"
299295

300296
[tool.ruff.lint]
301297
select = [
302-
"B", # flake8-bugbear - https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
303-
"C90", # mccabe (complexity) - https://docs.astral.sh/ruff/rules/#mccabe-c90
304-
"E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e
305-
"F", # pyflakes - https://docs.astral.sh/ruff/rules/#pyflakes-f
306-
"N", # pep8-naming - https://docs.astral.sh/ruff/rules/#pep8-naming-n
307-
"PIE", # flake8-pie - https://docs.astral.sh/ruff/rules/#flake8-pie-pie
308-
"TID", # flake8-tidy-imports - https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
309-
"W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#warning-w
310-
"NPY", # NumPy specific rules
311-
"UP", # pyupgrade
312-
"RUF100", # aka yesqa
298+
"B", # flake8-bugbear - https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
299+
"C90", # mccabe (complexity) - https://docs.astral.sh/ruff/rules/#mccabe-c90
300+
"E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e
301+
"F", # pyflakes - https://docs.astral.sh/ruff/rules/#pyflakes-f
302+
"N", # pep8-naming - https://docs.astral.sh/ruff/rules/#pep8-naming-n
303+
"PIE", # flake8-pie - https://docs.astral.sh/ruff/rules/#flake8-pie-pie
304+
"TID", # flake8-tidy-imports - https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
305+
"W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#warning-w
306+
"NPY", # NumPy specific rules - https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy
307+
"UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up
308+
"RUF100", # aka yesqa - https://docs.astral.sh/ruff/rules/unused-noqa/
309+
"F401", # unused imports - https://docs.astral.sh/ruff/rules/unused-import/
313310
]
314311
extend-ignore = [
315-
"E741", # ambiguous variable name
316-
"F401", # unused import
312+
"E741", # ambiguous variable name
317313
"NPY002", # numpy-legacy-random
318-
"E203", # whitespace before ':' (pycodestyle)
319-
"E501", # line too long (pycodestyle)
320-
"C408", # unnecessary collection call (flake8-comprehensions)
321-
"N812", # lowercase imported as non lowercase (pep8-naming)
322-
"B023", # function uses loop variable (flake8-bugbear)
323-
"B905", # zip() without an explicit strict= parameter (flake8-bugbear)
324-
"B028", # no explicit stacklevel keyword argument found (flake8-bugbear)
314+
"E203", # whitespace before ':' (pycodestyle)
315+
"E501", # line too long (pycodestyle)
316+
"C408", # unnecessary collection call (flake8-comprehensions)
317+
"N812", # lowercase imported as non lowercase (pep8-naming)
318+
"B023", # function uses loop variable (flake8-bugbear)
319+
"B905", # zip() without an explicit strict= parameter (flake8-bugbear)
320+
"B028", # no explicit stacklevel keyword argument found (flake8-bugbear)
325321
]
326322

327323
[tool.ruff.lint.per-file-ignores]
@@ -334,6 +330,8 @@ extend-ignore = [
334330
"monai/apps/detection/utils/ATSS_matcher.py" = [
335331
"N999"
336332
]
333+
"__init__.py" = ["F401"] # TODO: change importation in __init__.py files to suit F401
334+
"monai/bundle/__main__.py" = ["F401"]
337335

338336
[tool.ruff.lint.mccabe]
339337
max-complexity = 50 # todo lower this treshold when yesqa id replaced with Ruff's RUF100

runtests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ function print_style_fail_msg() {
221221
echo "${red}Check failed!${noColor}"
222222
if [ "$homedir" = "$currentdir" ]
223223
then
224-
echo "Please run auto style fixes: ${green}./runtests.sh --autofix${noColor}"
224+
echo "Please run auto style fixes if necessary: ${green}./runtests.sh --autofix${noColor}"
225225
else :
226226
fi
227227
}

tests/apps/test_auto3dseg.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212
from __future__ import annotations
1313

14+
import json
1415
import os
1516
import tempfile
1617
import unittest
18+
import warnings
1719
from copy import deepcopy
1820
from numbers import Number
1921

@@ -36,6 +38,7 @@
3638
SampleOperations,
3739
SegSummarizer,
3840
SummaryOperations,
41+
algo_from_json,
3942
datafold_read,
4043
verify_report_format,
4144
)
@@ -177,6 +180,20 @@ def __call__(self, data):
177180
return d
178181

179182

183+
class _DummyAlgo:
184+
"""Minimal stand-in for an Auto3DSeg Algo object used in warning tests."""
185+
186+
def __init__(self) -> None:
187+
self.template_path: str | None = None
188+
self.output_path = os.getcwd()
189+
190+
def load_state_dict(self, state: dict) -> None:
191+
pass
192+
193+
def get_output_path(self) -> str:
194+
return self.output_path
195+
196+
180197
class TestDataAnalyzer(unittest.TestCase):
181198
def setUp(self):
182199
self.test_dir = tempfile.TemporaryDirectory()
@@ -619,5 +636,23 @@ def tearDown(self) -> None:
619636
self.test_dir.cleanup()
620637

621638

639+
class TestAlgoFromJsonSecurityWarning(unittest.TestCase):
640+
def test_warns_about_untrusted_target(self) -> None:
641+
with tempfile.TemporaryDirectory() as tmpdir:
642+
algo_file = os.path.join(tmpdir, "algo_object.json")
643+
with open(algo_file, "w", encoding="utf-8") as f:
644+
json.dump({"_target_": f"{__name__}._DummyAlgo"}, f)
645+
646+
with warnings.catch_warnings(record=True) as caught:
647+
warnings.simplefilter("always")
648+
algo_from_json(algo_file)
649+
650+
messages = [str(w.message) for w in caught]
651+
self.assertTrue(
652+
any("algo_object.json" in msg and "trust" in msg for msg in messages),
653+
f"Keywords 'algo_object.json' and 'trust' not found in warning messages: {messages}",
654+
)
655+
656+
622657
if __name__ == "__main__":
623658
unittest.main()

tests/data/test_pydicom_reader.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import numpy as np
1717

1818
from monai.data import PydicomReader
19+
from monai.utils import MetaKeys
1920
from tests.test_utils import SkipIfNoModule
2021

2122

@@ -39,6 +40,73 @@ def test_partial_orientation_tags_warns(self):
3940
affine = reader._get_affine(metadata)
4041
np.testing.assert_array_equal(affine, np.eye(4))
4142

43+
def test_non_finite_pixel_spacing_raises(self):
44+
reader = PydicomReader()
45+
metadata = {
46+
"00200037": {"Value": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]},
47+
"00200032": {"Value": [0.0, 0.0, 0.0]},
48+
"00280030": {"Value": [np.nan, 1.0]},
49+
}
50+
with self.assertRaisesRegex(ValueError, "PixelSpacing"):
51+
reader._get_affine(metadata, lps_to_ras=False)
52+
53+
def test_non_finite_image_position_raises(self):
54+
reader = PydicomReader()
55+
metadata = {
56+
"00200037": {"Value": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]},
57+
"00200032": {"Value": [np.inf, 0.0, 0.0]},
58+
"00280030": {"Value": [1.0, 1.0]},
59+
}
60+
with self.assertRaisesRegex(ValueError, "ImagePositionPatient"):
61+
reader._get_affine(metadata, lps_to_ras=False)
62+
63+
def test_finite_values_return_affine(self):
64+
reader = PydicomReader()
65+
metadata = {
66+
"00200037": {"Value": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]},
67+
"00200032": {"Value": [10.0, 20.0, 30.0]},
68+
"00280030": {"Value": [0.5, 0.25]},
69+
}
70+
affine = reader._get_affine(metadata, lps_to_ras=False)
71+
self.assertEqual(affine.shape, (4, 4))
72+
self.assertTrue(np.all(np.isfinite(affine)))
73+
np.testing.assert_allclose(affine[0, 3], 10.0)
74+
np.testing.assert_allclose(affine[1, 3], 20.0)
75+
np.testing.assert_allclose(affine[2, 3], 30.0)
76+
77+
def test_non_finite_orientation_raises(self):
78+
reader = PydicomReader()
79+
metadata = {
80+
"00200037": {"Value": [np.nan, 0.0, 0.0, 0.0, 1.0, 0.0]},
81+
"00200032": {"Value": [0.0, 0.0, 0.0]},
82+
"00280030": {"Value": [1.0, 1.0]},
83+
}
84+
with self.assertRaisesRegex(ValueError, "ImageOrientationPatient"):
85+
reader._get_affine(metadata, lps_to_ras=False)
86+
87+
def test_non_finite_last_image_position_raises(self):
88+
reader = PydicomReader()
89+
metadata = {
90+
"00200037": {"Value": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]},
91+
"00200032": {"Value": [0.0, 0.0, 0.0]},
92+
"00280030": {"Value": [1.0, 1.0]},
93+
"lastImagePositionPatient": [0.0, 0.0, np.inf],
94+
MetaKeys.SPATIAL_SHAPE: [1, 1, 2],
95+
}
96+
with self.assertRaisesRegex(ValueError, "lastImagePositionPatient"):
97+
reader._get_affine(metadata, lps_to_ras=False)
98+
99+
def test_overflow_from_finite_inputs_raises(self):
100+
# Finite inputs whose product overflows produce a non-finite affine.
101+
reader = PydicomReader()
102+
metadata = {
103+
"00200037": {"Value": [1e308, 0.0, 0.0, 1e308, 0.0, 0.0]},
104+
"00200032": {"Value": [0.0, 0.0, 0.0]},
105+
"00280030": {"Value": [1e308, 1e308]},
106+
}
107+
with self.assertRaisesRegex(ValueError, "not finite"):
108+
reader._get_affine(metadata, lps_to_ras=False)
109+
42110

43111
if __name__ == "__main__":
44112
unittest.main()

0 commit comments

Comments
 (0)