Skip to content

Commit 84d955d

Browse files
authored
Merge pull request #127 from ap--/support-0.6.0
Support 0.6.0 and more tests
2 parents e6cda3a + d14e1d6 commit 84d955d

File tree

10 files changed

+153
-37
lines changed

10 files changed

+153
-37
lines changed

.github/workflows/run_pytests.yaml

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,47 @@ on:
77
tags: v[0-9]+.[0-9]+.[0-9]+
88
pull_request: {}
99

10-
env:
11-
QUPATH_VERSION: 0.5.1
12-
1310
jobs:
1411
# RUN PYTEST ON PAQUO SOURCE
1512
tests:
16-
name: pytest ${{ matrix.os }}::py${{ matrix.python-version }}
13+
name: pytest ${{ matrix.os }}::qp${{ matrix.qupath-version }}::py${{ matrix.python-version }}
1714
runs-on: ${{ matrix.os }}
1815
strategy:
1916
max-parallel: 8
2017
fail-fast: false
2118
matrix:
2219
os: [ubuntu-latest, macos-latest, windows-latest]
23-
python-version: ["3.12"]
20+
python-version: ["3.13"]
21+
qupath-version: ["0.6.0"]
2422
include:
2523
# we'll test the python support on ubuntu
24+
- os: ubuntu-latest
25+
python-version: '3.12'
26+
qupath-version: '0.6.0'
2627
- os: ubuntu-latest
2728
python-version: '3.11'
29+
qupath-version: '0.6.0'
2830
- os: ubuntu-latest
29-
python-version: '3.12'
31+
python-version: '3.10'
32+
qupath-version: '0.6.0'
3033
- os: ubuntu-latest
3134
python-version: '3.9'
35+
qupath-version: '0.6.0'
3236
- os: ubuntu-latest
3337
python-version: '3.8'
38+
qupath-version: '0.6.0'
39+
- os: ubuntu-latest
40+
python-version: '3.13'
41+
qupath-version: '0.5.1'
42+
- os: ubuntu-latest
43+
python-version: '3.13'
44+
qupath-version: '0.4.4'
45+
- os: ubuntu-latest
46+
python-version: '3.13'
47+
qupath-version: '0.3.2'
48+
- os: ubuntu-latest
49+
python-version: '3.13'
50+
qupath-version: '0.2.3'
3451
steps:
3552
- uses: actions/checkout@v3
3653
with:
@@ -49,13 +66,13 @@ jobs:
4966
CACHE_NUMBER: 0
5067
with:
5168
path: ./qupath/download
52-
key: ${{ runner.os }}-qupath-v${{ env.CACHE_NUMBER }}
69+
key: ${{ runner.os }}-qupath-v${{ matrix.qupath-version }}-c${{ env.CACHE_NUMBER }}
5370
- name: Install qupath and set PAQUO_QUPATH_DIR
5471
shell: bash
5572
run: |
5673
python -c "import os; os.makedirs('qupath/download', exist_ok=True)"
5774
python -c "import os; os.makedirs('qupath/apps', exist_ok=True)"
58-
python -m paquo get_qupath --install-path ./qupath/apps --download-path ./qupath/download ${{ env.QUPATH_VERSION }} | grep -v "^#" | sed "s/^/PAQUO_QUPATH_DIR=/" >> $GITHUB_ENV
75+
python -m paquo get_qupath --install-path ./qupath/apps --download-path ./qupath/download ${{ matrix.qupath-version }} | grep -v "^#" | sed "s/^/PAQUO_QUPATH_DIR=/" >> $GITHUB_ENV
5976
- name: Test with pytest
6077
run: |
6178
pytest --cov=./paquo --cov-report=xml

paquo/classes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def __init__(self,
8383
if color is not None:
8484
java_color = QuPathColor.from_any(color).to_java_rgba() # use rgba?
8585

86-
if compatibility.supports_newer_addobject_and_pathclass():
86+
if compatibility.supports_newer_addobject_and_pathclass:
8787
path_class = PathClass.getInstance(java_parent, name, java_color)
8888
else:
8989
path_class = PathClassFactory.getDerivedPathClass(java_parent, name, java_color)

paquo/hierarchy.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ def __ior__(self, other: Iterable[Any]) -> "PathObjectProxy": # type: ignore
109109
if self._readonly:
110110
raise OSError("project in readonly mode")
111111
path_objects = [x.java_object for x in other]
112+
if compatibility.supports_addobjects:
113+
add_objects = self._java_hierarchy.addObjects
114+
else:
115+
add_objects = self._java_hierarchy.addPathObjects
112116
try:
113-
self._java_hierarchy.addPathObjects(path_objects)
117+
add_objects(path_objects)
114118
finally:
115119
self._list_invalidate_cache()
116120
return self
@@ -128,7 +132,7 @@ def __isub__(self, other: Iterable[Any]) -> "PathObjectProxy": # type: ignore
128132
self._list_invalidate_cache()
129133
return self
130134

131-
if compatibility.supports_newer_addobject_and_pathclass():
135+
if compatibility.supports_newer_addobject_and_pathclass:
132136
def add(self, x: PathROIObjectType) -> None:
133137
"""adds a new path object to the proxy"""
134138
if self._mask:
@@ -445,7 +449,8 @@ def load_geojson(
445449
if not isinstance(geojson, list):
446450
raise TypeError("requires a geojson list")
447451

448-
requires_annotation_json_fix = compatibility.requires_annotation_json_fix()
452+
requires_annotation_json_fix = compatibility.requires_annotation_json_fix
453+
requires_objecttype_json_fix = compatibility.requires_objecttype_json_fix
449454

450455
aos = []
451456
skipped: "CounterType[str]" = collections.Counter()
@@ -483,6 +488,12 @@ def load_geojson(
483488
object_id = "PathAnnotationObject"
484489
annotation['id'] = object_id
485490

491+
if (
492+
requires_objecttype_json_fix
493+
and 'object_type' not in properties
494+
):
495+
properties['object_type'] = object_type
496+
486497
gson = GsonTools.getInstance()
487498
if object_type == "annotation":
488499
java_obj = gson.fromJson(String(json.dumps(annotation)), PathAnnotationObject)

paquo/images.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from paquo.java import IOException
3535
from paquo.java import NoSuchFileException
3636
from paquo.java import PathIO
37+
from paquo.java import RuntimeException
3738
from paquo.java import String
3839
from paquo.java import URISyntaxException
3940
from paquo.java import compatibility
@@ -431,7 +432,7 @@ def _image_data(self):
431432
try:
432433
return self.java_object.readImageData()
433434
# from java land
434-
except IOException: # pragma: no cover
435+
except (IOException, RuntimeException): # pragma: no cover
435436
image_data_fn = self.entry_path / "data.qpdata"
436437
try:
437438
image_data = PathIO.readImageData(
@@ -448,7 +449,10 @@ def _properties(self):
448449

449450
@cached_property
450451
def _image_server(self):
451-
server = self._image_data.getServer()
452+
try:
453+
server = self._image_data.getServer()
454+
except RuntimeException:
455+
server = None
452456
if not server:
453457
_log.warning("recovering readonly from server.json")
454458
try:
@@ -592,7 +596,7 @@ def hierarchy(self) -> QuPathPathObjectHierarchy:
592596
else:
593597
try:
594598
h = self._image_data.getHierarchy()
595-
except OSError:
599+
except (OSError, RuntimeError):
596600
_log.warning("could not open image data. loading annotation hierarchy from project.")
597601
h = self.java_object.readHierarchy()
598602

@@ -676,7 +680,10 @@ def _repr_html_(self, compact=False, index=0):
676680
@property
677681
def uri(self):
678682
"""the image entry uri"""
679-
uris = self.java_object.getServerURIs()
683+
if compatibility.supports_get_uris:
684+
uris = self.java_object.getURIs()
685+
else:
686+
uris = self.java_object.getServerURIs()
680687
if len(uris) == 0:
681688
raise RuntimeError("no server") # pragma: no cover
682689
elif len(uris) > 1:

paquo/java.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import warnings
2+
from functools import cached_property
23

34
from paquo._config import settings
45
from paquo._config import to_kwargs
@@ -43,6 +44,7 @@ class _Compatibility:
4344
def __init__(self, version: "QuPathVersion | None") -> None:
4445
self.version = version
4546

47+
@cached_property
4648
def requires_missing_classes_json_fix(self) -> bool:
4749
# older QuPaths crash on project load when classes.json is missing
4850
# see: https://github.com/qupath/qupath/commit/be861cea80b9a8ef300e30d7985fd69791c2432e
@@ -51,6 +53,7 @@ def requires_missing_classes_json_fix(self) -> bool:
5153
else:
5254
return self.version <= QuPathVersion("0.2.0")
5355

56+
@cached_property
5457
def requires_annotation_json_fix(self) -> bool:
5558
# annotations changed between QuPath "0.2.3" and "0.3.x"
5659
# see: https://github.com/qupath/qupath/commit/fef5c43ce3f67e0e062677c407b395ef3e6e27c3
@@ -59,6 +62,15 @@ def requires_annotation_json_fix(self) -> bool:
5962
else:
6063
return self.version <= QuPathVersion("0.2.3")
6164

65+
@cached_property
66+
def requires_objecttype_json_fix(self) -> bool:
67+
# annotations changed between QuPath "0.3.0" and "0.4.x"
68+
if self.version is None:
69+
return False
70+
else:
71+
return QuPathVersion("0.3.0") <= self.version < QuPathVersion("0.4.0")
72+
73+
@cached_property
6274
def supports_image_server_recovery(self) -> bool:
6375
# image_server server.json files are only guaranteed to be written since QuPath "0.2.0"
6476
# see: https://github.com/qupath/qupath/commit/39abee3012da9252ea988308848c5d802164e060
@@ -67,6 +79,7 @@ def supports_image_server_recovery(self) -> bool:
6779
else:
6880
return self.version >= QuPathVersion("0.2.0")
6981

82+
@cached_property
7083
def supports_logmanager(self) -> bool:
7184
# the logmanager class was only added with 0.2.0-m10
7285
# see: https://github.com/qupath/qupath/commit/15b844703b686f7a9a64c50194ebe22fc46924a5
@@ -75,6 +88,7 @@ def supports_logmanager(self) -> bool:
7588
else:
7689
return self.version >= QuPathVersion("0.2.0-m10")
7790

91+
@cached_property
7892
def supports_newer_addobject_and_pathclass(self) -> bool:
7993
# PathObjectHierarchy.addPathObject and .addPathObjectWithoutUpdate are deprecated
8094
# PathClassFactory is deprecated too
@@ -84,6 +98,30 @@ def supports_newer_addobject_and_pathclass(self) -> bool:
8498
else:
8599
return self.version >= QuPathVersion("0.4.0")
86100

101+
@cached_property
102+
def supports_addobjects(self) -> bool:
103+
# PathObjectHierarchy.addPathObjects was removed
104+
if self.version is None:
105+
return False
106+
else:
107+
return self.version >= QuPathVersion("0.6.0")
108+
109+
@cached_property
110+
def supports_get_uris(self) -> bool:
111+
# .getServerURIs was removed
112+
if self.version is None:
113+
return False
114+
else:
115+
return self.version >= QuPathVersion("0.6.0")
116+
117+
@cached_property
118+
def supports_newer_measurements_interface(self) -> bool:
119+
# .putMeasurement is gone?
120+
if self.version is None:
121+
return False
122+
else:
123+
return self.version >= QuPathVersion("0.6.0")
124+
87125

88126
compatibility = _Compatibility(qupath_version)
89127

@@ -112,15 +150,15 @@ def supports_newer_addobject_and_pathclass(self) -> bool:
112150
ImageServers = JClass('qupath.lib.images.servers.ImageServers') # NOTE: this is needed to make QuPath v0.3.0-rc1 work
113151
ImageServerProvider = JClass('qupath.lib.images.servers.ImageServerProvider')
114152

115-
if compatibility.supports_logmanager():
153+
if compatibility.supports_logmanager:
116154
LogManager = JClass('qupath.lib.gui.logging.LogManager')
117155
else:
118156
LogManager = None
119157

120158
PathAnnotationObject = JClass("qupath.lib.objects.PathAnnotationObject")
121159
PathClass = JClass('qupath.lib.objects.classes.PathClass')
122160

123-
if not compatibility.supports_newer_addobject_and_pathclass():
161+
if not compatibility.supports_newer_addobject_and_pathclass:
124162
PathClassFactory = JClass('qupath.lib.objects.classes.PathClassFactory')
125163
else:
126164
PathClassFactory = None
@@ -157,6 +195,7 @@ def supports_newer_addobject_and_pathclass(self) -> bool:
157195
IllegalArgumentException = JClass('java.lang.IllegalArgumentException')
158196
FileNotFoundException = JClass('java.io.FileNotFoundException')
159197
NoSuchFileException = JClass('java.nio.file.NoSuchFileException')
198+
RuntimeException = JClass('java.lang.RuntimeException')
160199

161200

162201
def __getattr__(name):

paquo/pathobjects.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from paquo.java import String
2929
from paquo.java import WKBReader
3030
from paquo.java import WKBWriter
31+
from paquo.java import compatibility
3132

3233
__all__ = [
3334
"fix_geojson_geometry",
@@ -91,7 +92,10 @@ def __init__(
9192
def __setitem__(self, k: str, v: float) -> None:
9293
if not isinstance(v, float):
9394
raise TypeError(f"value must be float, got: {type(v).__name__}")
94-
self._measurement_list.putMeasurement(k, v)
95+
if compatibility.supports_newer_measurements_interface:
96+
self._measurement_list.put(k, v)
97+
else:
98+
self._measurement_list.putMeasurement(k, v)
9599
if self._update_callback:
96100
self._update_callback()
97101

@@ -105,15 +109,23 @@ def __delitem__(self, v: str) -> None:
105109
def __getitem__(self, k: Union[str, int]) -> float:
106110
if not isinstance(k, (int, str)):
107111
raise KeyError(f"unsupported key of type {type(k)}")
108-
value = float(self._measurement_list.getMeasurementValue(k))
112+
if compatibility.supports_newer_measurements_interface:
113+
if isinstance(k, int):
114+
k = str(self._measurement_list.getMeasurementNames()[k])
115+
value = float(self._measurement_list.get(k))
116+
else:
117+
value = float(self._measurement_list.getMeasurementValue(k))
109118
if math.isnan(value) and k not in self:
110119
raise KeyError(k)
111120
return value
112121

113122
def __contains__(self, item: object) -> bool:
114123
if not isinstance(item, str):
115124
return False
116-
return bool(self._measurement_list.containsNamedMeasurement(item))
125+
if compatibility.supports_newer_measurements_interface:
126+
return bool(self._measurement_list.containsKey(item))
127+
else:
128+
return bool(self._measurement_list.containsNamedMeasurement(item))
117129

118130
def __len__(self) -> int:
119131
return int(self._measurement_list.size())

paquo/projects.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def __init__(self,
219219

220220
if _exists:
221221
cm: Callable[..., ContextManager[Any]]
222-
if compatibility.requires_missing_classes_json_fix():
222+
if compatibility.requires_missing_classes_json_fix:
223223
@contextmanager
224224
def cm(is_readonly):
225225
classes_json = p.parent.joinpath("classifiers", "classes.json")
@@ -453,8 +453,12 @@ def update_image_paths(self, *, try_relative: bool = False, **rebase_kwargs) ->
453453
uri2uri[URI(old_uri)] = URI(new_uri)
454454

455455
# update uris if possible
456-
for image in self.images:
457-
image.java_object.updateServerURIs(uri2uri)
456+
if compatibility.supports_get_uris:
457+
for image in self.images:
458+
image.java_object.updateURIs(uri2uri)
459+
else:
460+
for image in self.images:
461+
image.java_object.updateServerURIs(uri2uri)
458462

459463
@redirect(stderr=True, stdout=True)
460464
def remove_image(

paquo/tests/test_images.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def test_readonly_recovery_hierarchy(project_with_removed_image_without_image_da
175175

176176
def test_readonly_recovery_image_server(project_with_removed_image):
177177
from paquo.java import compatibility
178-
if not compatibility.supports_image_server_recovery():
178+
if not compatibility.supports_image_server_recovery:
179179
pytest.skip(f"unsupported in {compatibility.version}")
180180

181181
with QuPathProject(project_with_removed_image, mode='r') as qp:

paquo/tests/test_projects.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def test_project_delete_image_file_when_opened(new_project, svs_small, qupath_ve
299299
qupath_uses = "OPENSLIDE"
300300

301301
if qupath_uses == "BIOFORMATS": # pragma: no cover
302-
if platform.system() == "Windows":
302+
if platform.system() == "Windows" and qupath_version < QuPathVersion("0.6.0"):
303303
# NOTE: on Windows because you can't delete files that have open
304304
# file handles. In this test we're deleting the file opened by
305305
# the ImageServer on the java side. (this is happening
@@ -316,7 +316,7 @@ def test_project_delete_image_file_when_opened(new_project, svs_small, qupath_ve
316316
elif qupath_uses == "OPENSLIDE":
317317

318318
if (
319-
qupath_version >= QuPathVersion("0.5.0")
319+
QuPathVersion("0.5.0") <= qupath_version < QuPathVersion("0.6.0")
320320
and platform.system() == "Windows"
321321
):
322322
cm = pytest.raises(PermissionError)

0 commit comments

Comments
 (0)