Skip to content

Commit 66688c6

Browse files
Sec/dependencies (#332)
* sec(deps): Don't use override-dependencies as this is not respected by uvx, but use regular dependency trees * chore(gui): Don't use windowed mode for launchpad if on Python 3.14 * chore(wsi): Reject running wsi dicom commands on Python 3.14, given transitive dependency of highdicom not yet supported on that Python version
1 parent 9905601 commit 66688c6

6 files changed

Lines changed: 378 additions & 259 deletions

File tree

pyproject.toml

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ classifiers = [
7171
"Natural Language :: English",
7272
]
7373

74+
# WARNING: The upper bound of requires-python is *not* respected by uvx
7475
requires-python = ">=3.11, <3.15"
7576

7677
dependencies = [
@@ -96,7 +97,7 @@ dependencies = [
9697
"duckdb>=1.4.2,<=2",
9798
"google-cloud-storage>=3.6.0,<4",
9899
"crc32c>=2.8,<3", # TODO(Helmut): Remove and back to google_crc32c when that supports Python 3.14
99-
"highdicom>=0.26.1,<1",
100+
"highdicom>=0.26.1,<1; python_version < '3.14'", # transitive dependency pyjpegls not yet supporting Python 3.14
100101
"html-sanitizer>=2.6.0,<3",
101102
"httpx>=0.28.1,<1",
102103
"idc-index-data==23.0.1",
@@ -124,15 +125,32 @@ dependencies = [
124125
"truststore>=0.10.4,<1",
125126
"urllib3>=2.6.1,<3",
126127
"wsidicom>=0.28.1,<1",
128+
# Transitive overrides
129+
# WARNING: one cannot negate or downgrade a dependency required here. use override-dependencies for that.
130+
"rfc3987; sys_platform == 'never'", # GPLv3
131+
"h11>=0.16.0", # CVE-2025-43859
132+
"tornado>=6.5.0", # CVE-2025-47287
133+
"urllib3>=2.5.0", # CVE-2025-50181, CVE-2025-50182,
134+
"pillow>=11.3.0", # CVE-2025-48379,
135+
"aiohttp>=3.12.14", # CVE-2025-53643
136+
"starlette>=0.47.2", # CVE-2025-54121
137+
"starlette>=0.49.1", # GHSA-7f5h-v6xp-fcq8
138+
"lxml>=6.0.2", # For python 3.14 pre-built wheels
127139
]
128140

129141
[project.optional-dependencies]
130142
pyinstaller = ["pyinstaller>=6.14.0,<7"]
131-
jupyter = ["jupyter>=1.1.1,<2"]
143+
jupyter = [
144+
"jupyter>=1.1.1,<2",
145+
# Transitive overrides
146+
# WARNING: one cannot negate or downgrade a dependency required here. use override-dependencies for that.
147+
"jupyter-core>=5.8.1", # CVE-2025-30167
148+
"jupyterlab>=4.4.9", # CVE-2025-59842
149+
]
132150
marimo = [
133151
"cloudpathlib>=0.23.0,<1",
134152
"ipython>=9.8.0,<10",
135-
"marimo>=0.18.3,<1",
153+
"marimo>=0.18.4,<1",
136154
"matplotlib>=3.10.7,<4",
137155
"shapely>=2.1.0,<3",
138156
]
@@ -185,27 +203,18 @@ dev = [
185203
"types-pyyaml>=6.0.12.20250915,<7",
186204
"types-requests>=2.32.4.20250913,<3",
187205
"watchdog>=6.0.0,<7",
206+
# Transitive overrides
207+
# WARNING: one cannot negate or downgrade a dependency required here. use override-dependencies for that.
208+
"pip>=5.3", # CVE-2025-8869
209+
"uv>=0.9.7", # CVE-2025-54368, GHSA-w476-p2h3-79g9, GHSA-pqhf-p39g-3x64
210+
"fonttools>=4.60.2", # CVE-2025-66034 (GHSA-768j-98cg-p3fv), dep of matplotlib
188211
]
189212

190213
[tool.uv]
191214
required-version = ">=0.9.7" # CVE-2025-54368, GHSA-w476-p2h3-79g9, GHSA-pqhf-p39g-3x64
215+
# WARNING: override-dependencies is *not* respected by uvx
192216
override-dependencies = [ # https://github.com/astral-sh/uv/issues/4422
193-
"rfc3987; sys_platform == 'never'", # GPLv3
194-
"h11>=0.16.0", # CVE-2025-43859
195-
"tornado>=6.5.0", # CVE-2025-47287
196-
"jupyter-core>=5.8.1", # CVE-2025-30167
197-
"urllib3>=2.5.0", # CVE-2025-50181, CVE-2025-50182,
198-
"pillow>=11.3.0", # CVE-2025-48379,
199-
"aiohttp>=3.12.14", # CVE-2025-53643
200-
"starlette>=0.47.2", # CVE-2025-54121
201-
"uv>=0.9.7", # CVE-2025-54368, GHSA-w476-p2h3-79g9, GHSA-pqhf-p39g-3x64
202-
"jupyterlab>=4.4.9", # CVE-2025-59842
203-
"pip>=5.3", # CVE-2025-8869
204-
"starlette>=0.49.1", # GHSA-7f5h-v6xp-fcq8
205-
"fonttools>=4.60.2", # CVE-2025-66034 (GHSA-768j-98cg-p3fv), dep of matplotlib
206-
"pyjpegls; python_version < '3.14'", # No Python 3.14 support yet
207-
"pytest>=9.0.1", # pytest-md-report depends on pytest<9 unnecessarily
208-
"lxml>=6.0.2", # For python 3.14 pre-built wheels
217+
"pytest>=9.0.1", # pytest-md-report depends on pytest<9 unnecessarily
209218
]
210219

211220
[tool.uv.sources]

src/aignostics/utils/_gui.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ def gui_run( # noqa: PLR0913, PLR0917
7373
native = False
7474
show = True
7575

76+
# On Windows with python 3.14 don't use native mode due to pythonnet not yet
77+
# supported
78+
if native and platform.system() == "Windows" and platform.python_version_tuple() >= ("3", "14"):
79+
native = False
80+
show = True
81+
7682
gui_register_pages()
7783

7884
ui.run(

src/aignostics/wsi/_cli.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@
1212
from ._service import Service
1313
from ._utils import print_slide_info, print_study_info
1414

15+
# Python version for highdicom compatibility check
16+
_PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
17+
_HIGHDICOM_UNSUPPORTED_VERSIONS = {"3.14"}
18+
19+
20+
def _check_highdicom_available() -> bool:
21+
"""Check if highdicom is available (not supported on Python 3.14+).
22+
23+
Returns:
24+
True if highdicom can be imported, False otherwise.
25+
"""
26+
try:
27+
from ._pydicom_handler import PydicomHandler # noqa: PLC0415, F401
28+
29+
return True
30+
except ImportError:
31+
return False
32+
33+
34+
def _print_highdicom_unsupported_error() -> None:
35+
"""Print error message when highdicom is not available."""
36+
console.print(f"[red]This command requires 'highdicom' which is not available on Python {_PYTHON_VERSION}.[/red]")
37+
console.print("[yellow]Please run with Python 3.13 or earlier:[/yellow]")
38+
console.print("[green] uvx -p 3.13 aignostics wsi dicom <command> ...[/green]")
39+
sys.exit(1)
40+
41+
1542
cli = typer.Typer(name="wsi", help="Operations on whole slide images.")
1643

1744

@@ -107,6 +134,10 @@ def dicom_inspect(
107134
summary: Annotated[bool, typer.Option(help="Show only summary information")] = False,
108135
) -> None: # pylint: disable=W0613
109136
"""Inspect DICOM files at any hierarchy level."""
137+
if not _check_highdicom_available():
138+
_print_highdicom_unsupported_error()
139+
return
140+
110141
from ._pydicom_handler import PydicomHandler # noqa: PLC0415
111142

112143
try:
@@ -139,6 +170,10 @@ def dicom_geojson_import(
139170
geojson_path: Annotated[Path, typer.Argument(help="Path to the GeoJSON file", exists=True)],
140171
) -> None: # pylint: disable=W0613
141172
"""Import GeoJSON annotations into DICOM ANN instance."""
173+
if not _check_highdicom_available():
174+
_print_highdicom_unsupported_error()
175+
return
176+
142177
from ._pydicom_handler import PydicomHandler # noqa: PLC0415
143178

144179
try:

tests/aignostics/cli_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ def mock_app_mount(path, app_instance):
213213
"show_welcome_message parameter should be True when native is False"
214214
)
215215
assert mock_ui_run_args["show"] is True, "show parameter should be True when native is False"
216+
elif platform.system() == "Windows" and platform.python_version_tuple() >= ("3", "14"):
217+
assert mock_ui_run_args["native"] is False, "native parameter should be False on Windows"
218+
assert mock_ui_run_args["show_welcome_message"] is True, (
219+
"show_welcome_message parameter should be True when native is False"
220+
)
221+
assert mock_ui_run_args["show"] is True, "show parameter should be True when native is False"
216222
else:
217223
assert mock_ui_run_args["native"] is True, "native parameter should be True on platforms other than Linux"
218224
assert mock_ui_run_args["show_welcome_message"] is False, (

tests/aignostics/wsi/cli_test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
THUMBNAIL_UID = "1.3.6.1.4.1.5962.99.1.1038911754.1238045814.1637421484298.15.0"
1616
SMALL_PYRAMIDAL_STUDY_UID = "Study: 2.25.150973379448125660359643882019624926008"
1717

18+
# Skip tests requiring highdicom on Python 3.14+ (highdicom not yet supported)
19+
SKIP_HIGHDICOM_PYTHON_314 = pytest.mark.skipif(
20+
sys.version_info[:2] >= (3, 14),
21+
reason="highdicom is not available on Python 3.14+",
22+
)
23+
1824

1925
@pytest.mark.integration
2026
@pytest.mark.timeout(timeout=60 * 5)
@@ -36,6 +42,7 @@ def test_inspect_openslide_dicom(runner: CliRunner, record_property) -> None:
3642
)
3743

3844

45+
@SKIP_HIGHDICOM_PYTHON_314
3946
@pytest.mark.skipif(
4047
sys.platform == "win32" and platform.machine().lower() in {"arm64", "aarch64"} and sys.version_info[:2] == (3, 12),
4148
reason="Skipping on Windows ARM with Python 3.12.x",
@@ -57,6 +64,7 @@ def test_inspect_pydicom_directory_non_verbose(runner: CliRunner, record_propert
5764
)
5865

5966

67+
@SKIP_HIGHDICOM_PYTHON_314
6068
@pytest.mark.skipif(
6169
platform.system() == "Windows"
6270
and platform.machine().lower() in {"arm64", "aarch64"}
@@ -84,6 +92,7 @@ def test_inspect_pydicom_directory_verbose(runner: CliRunner, record_property) -
8492
)
8593

8694

95+
@SKIP_HIGHDICOM_PYTHON_314
8796
@pytest.mark.skipif(
8897
platform.system() == "Windows"
8998
and platform.machine().lower() in {"arm64", "aarch64"}
@@ -101,6 +110,7 @@ def test_inspect_pydicom_single_file_non_verbose(runner: CliRunner, record_prope
101110
assert SMALL_PYRAMIDAL_STUDY_UID in result.output
102111

103112

113+
@SKIP_HIGHDICOM_PYTHON_314
104114
@pytest.mark.skipif(
105115
platform.system() == "Windows"
106116
and platform.machine().lower() in {"arm64", "aarch64"}
@@ -125,6 +135,7 @@ def test_inspect_pydicom_single_file_verbose(runner: CliRunner, record_property)
125135
)
126136

127137

138+
@SKIP_HIGHDICOM_PYTHON_314
128139
@pytest.mark.skipif(
129140
platform.system() == "Windows"
130141
and platform.machine().lower() in {"arm64", "aarch64"}
@@ -165,6 +176,7 @@ def test_wsi_inspect_error_handling(runner: CliRunner, record_property) -> None:
165176
assert str(file_path) in normalize_output(result.output)
166177

167178

179+
@SKIP_HIGHDICOM_PYTHON_314
168180
@pytest.mark.integration
169181
@pytest.mark.timeout(timeout=60 * 5)
170182
def test_wsi_dicom_inspect_error_handling(runner: CliRunner, record_property) -> None:
@@ -184,6 +196,7 @@ def test_wsi_dicom_inspect_error_handling(runner: CliRunner, record_property) ->
184196
assert "Invalid DICOM structure" in normalize_output(result.output)
185197

186198

199+
@SKIP_HIGHDICOM_PYTHON_314
187200
@pytest.mark.integration
188201
@pytest.mark.timeout(timeout=60 * 5)
189202
def test_wsi_dicom_geojson_import_error_handling(runner: CliRunner, record_property) -> None:

0 commit comments

Comments
 (0)