Skip to content

Commit 878cbb0

Browse files
authored
fix: enable opening artifacts in qupath on Windows (#335)
1 parent 0fc5990 commit 878cbb0

2 files changed

Lines changed: 49 additions & 37 deletions

File tree

src/aignostics/qupath/_service.py

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
PROJECT_FILENAME = "project.qpproj"
4444
ANNOTATIONS_BATCH_SIZE = 500000
45+
JSON_SUFFIX = ".json"
4546

4647

4748
class QuPathVersion(BaseModel):
@@ -1124,40 +1125,46 @@ def add(
11241125
progress_callable(progress)
11251126

11261127
# We communicate via file I/O with the Groovy script running within QuPath
1127-
with (
1128-
tempfile.NamedTemporaryFile(mode="w", suffix=".json", encoding="utf-8") as paths_file,
1129-
tempfile.NamedTemporaryFile(mode="w", suffix=".json", encoding="utf-8") as output_file,
1130-
):
1131-
try:
1132-
json.dump([str(path.resolve()) for path in paths], paths_file.file)
1133-
paths_file.flush()
1128+
# Use delete=False to avoid Windows file locking issues - the file must be closed
1129+
# before QuPath can read it, but NamedTemporaryFile keeps files locked while open on Windows
1130+
paths_file = tempfile.NamedTemporaryFile(mode="w", suffix=JSON_SUFFIX, encoding="utf-8", delete=False) # noqa: SIM115
1131+
output_file = tempfile.NamedTemporaryFile(mode="w", suffix=JSON_SUFFIX, encoding="utf-8", delete=False) # noqa: SIM115
1132+
try:
1133+
# Write paths and close file so QuPath can read it
1134+
json.dump([str(path.resolve()) for path in paths], paths_file)
1135+
paths_file.close()
1136+
output_file.close()
11341137

1135-
pid = Service.execute_qupath(
1136-
script=Service._find_groovy_script("add"),
1137-
script_args=[str(project), str(paths_file.name), str(output_file.name)],
1138-
)
1138+
pid = Service.execute_qupath(
1139+
script=Service._find_groovy_script("add"),
1140+
script_args=[str(project), paths_file.name, output_file.name],
1141+
)
11391142

1140-
if not pid:
1141-
message = "Failed to execute QuPath script for adding images."
1142-
logger.error(message)
1143-
raise RuntimeError(message) # noqa: TRY301
1144-
1145-
with Path(output_file.name).open("r", encoding="utf-8") as f:
1146-
result_data = json.load(f)
1147-
added_count = int(result_data.get("added_count", 0))
1148-
errors = result_data.get("errors", [])
1149-
for error in errors:
1150-
logger.warning(f"QuPath add script error: {error}")
1151-
1152-
if progress_callable:
1153-
progress.status = AddProgressState.COMPLETED
1154-
progress_callable(progress)
1155-
1156-
return added_count
1157-
except Exception as e:
1158-
message = f"Failed to add images to QuPath project: {e!s}"
1159-
logger.exception(message)
1160-
raise RuntimeError(message) from e
1143+
if not pid:
1144+
message = "Failed to execute QuPath script for adding images."
1145+
logger.error(message)
1146+
raise RuntimeError(message) # noqa: TRY301
1147+
1148+
with Path(output_file.name).open("r", encoding="utf-8") as f:
1149+
result_data = json.load(f)
1150+
added_count = int(result_data.get("added_count", 0))
1151+
errors = result_data.get("errors", [])
1152+
for error in errors:
1153+
logger.warning(f"QuPath add script error: {error}")
1154+
1155+
if progress_callable:
1156+
progress.status = AddProgressState.COMPLETED
1157+
progress_callable(progress)
1158+
1159+
return added_count
1160+
except Exception as e:
1161+
message = f"Failed to add images to QuPath project: {e!s}"
1162+
logger.exception(message)
1163+
raise RuntimeError(message) from e
1164+
finally:
1165+
# Clean up temp files
1166+
Path(paths_file.name).unlink(missing_ok=True)
1167+
Path(output_file.name).unlink(missing_ok=True)
11611168

11621169
@staticmethod
11631170
def annotate(
@@ -1246,9 +1253,12 @@ def inspect(project: Path) -> QuPathProject:
12461253
RuntimeError: If there is an error inspecting the project.
12471254
"""
12481255
# We communicate via file I/O with the Groovy script running within QuPath
1249-
with tempfile.NamedTemporaryFile(mode="w+", suffix=".json", encoding="utf-8") as temp_file:
1250-
output_file = Path(temp_file.name).resolve()
1251-
1256+
# Use delete=False to avoid Windows file locking issues - the file must be closed
1257+
# before QuPath can write to it, but NamedTemporaryFile keeps files locked while open on Windows
1258+
temp_file = tempfile.NamedTemporaryFile(mode="w+", suffix=JSON_SUFFIX, encoding="utf-8", delete=False) # noqa: SIM115
1259+
output_file = Path(temp_file.name).resolve()
1260+
temp_file.close()
1261+
try:
12521262
pid = Service.execute_qupath(
12531263
quiet=True,
12541264
project=project,
@@ -1267,3 +1277,5 @@ def inspect(project: Path) -> QuPathProject:
12671277
raise RuntimeError(message)
12681278

12691279
return QuPathProject.model_validate_json(output_file.read_text(encoding="utf-8"))
1280+
finally:
1281+
output_file.unlink(missing_ok=True)

tests/aignostics/qupath/gui_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ async def test_gui_qupath_install_and_launch(
149149
@pytest.mark.e2e
150150
@pytest.mark.long_running
151151
@pytest.mark.skipif(
152-
(platform.system() == "Linux" and platform.machine() in {"aarch64", "arm64"}) or platform.system() == "Windows",
153-
reason="QuPath is not supported on ARM64 Linux; Windows support is not fully tested yet",
152+
(platform.system() == "Linux" and platform.machine() in {"aarch64", "arm64"}),
153+
reason="QuPath is not supported on ARM64 Linux",
154154
)
155155
@pytest.mark.timeout(timeout=60 * 15)
156156
@pytest.mark.sequential

0 commit comments

Comments
 (0)