diff --git a/.github/workflows/run_pytests.yaml b/.github/workflows/run_pytests.yaml
index 9ce4442..e6cbd06 100644
--- a/.github/workflows/run_pytests.yaml
+++ b/.github/workflows/run_pytests.yaml
@@ -8,7 +8,7 @@ on:
pull_request: {}
env:
- QUPATH_VERSION: 0.4.3
+ QUPATH_VERSION: 0.5.0
jobs:
# RUN PYTEST ON PAQUO SOURCE
@@ -16,19 +16,21 @@ jobs:
name: pytest ${{ matrix.os }}::py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
- max-parallel: 7
+ max-parallel: 8
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.11"]
+ python-version: ["3.12"]
include:
# we'll test the python support on ubuntu
- os: ubuntu-latest
- python-version: '3.10'
+ python-version: '3.11'
- os: ubuntu-latest
- python-version: 3.9
+ python-version: '3.12'
- os: ubuntu-latest
- python-version: 3.8
+ python-version: '3.9'
+ - os: ubuntu-latest
+ python-version: '3.8'
steps:
- uses: actions/checkout@v3
with:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 04d2c6c..adc9349 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,17 +1,17 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- repo: https://github.com/asottile/pyupgrade
- rev: v3.9.0
+ rev: v3.15.1
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/pycqa/isort
- rev: 5.12.0
+ rev: 5.13.2
hooks:
- id: isort
exclude: ^examples|^extras|^docs|tests.*|setup.py
@@ -21,21 +21,21 @@ repos:
# - id: black
# language_version: python3
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: 'v1.4.1'
+ rev: 'v1.8.0'
hooks:
- id: mypy
exclude: ^examples|^extras|^docs|tests.*
additional_dependencies: [packaging, ome-types]
- repo: https://github.com/PyCQA/flake8
- rev: '6.0.0'
+ rev: '7.0.0'
hooks:
- id: flake8
additional_dependencies:
- - flake8-typing-imports==1.12.0
+ - flake8-typing-imports==1.15.0
language_version: python3
exclude: "^(build|docs|examples|extras|setup.py)|tests[/]"
- repo: https://github.com/PyCQA/bandit
- rev: '1.7.5'
+ rev: '1.7.7'
hooks:
- id: bandit
args: ["-lll", "--ini", ".bandit"]
diff --git a/README.md b/README.md
index 37a1e1c..7582559 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ unintuitive, slow or if its documentation is confusing, it's a bug in
tracker!
Development
-[happens on github](https://github.com/bayer-science-for-a-better-life/paquo)
+[happens on GitHub](https://github.com/bayer-science-for-a-better-life/paquo)
:octocat:
## Documentation
@@ -54,18 +54,18 @@ paquo to use that version. Currently, paquo supports every version of QuPath fro
`0.2.0` to the most recent. _(We even support older `0.2.0-mX` versions but no guarantees)._
```shell
-> paquo get_qupath --install-path "/some/path/on/your/machine" 0.4.3
-# downloading: https://github.com/qupath/qupath/releases/download/v0.4.3/QuPath-0.4.3-Linux.tar.xz
+> paquo get_qupath --install-path "/some/path/on/your/machine" 0.5.0
+# downloading: https://github.com/qupath/qupath/releases/download/v0.5.0/QuPath-0.4.3-Linux.tar.xz
# progress ................... OK
-# extracting: [...]/QuPath-0.4.3-Linux.tar.xz
-# available at: /some/path/on/your/machine/QuPath-0.4.3
+# extracting: [...]/QuPath-0.5.0-Linux.tar.xz
+# available at: /some/path/on/your/machine/QuPath-0.5.0
#
# use via environment variable:
-# $ export PAQUO_QUPATH_DIR=/some/path/on/your/machine/QuPath-0.4.3
+# $ export PAQUO_QUPATH_DIR=/some/path/on/your/machine/QuPath-0.5.0
#
# use via .paquo.toml config file:
-# qupath_dir="/some/path/on/your/machine/QuPath-0.4.3"
-/some/path/on/your/machine/QuPath-0.4.3
+# qupath_dir="/some/path/on/your/machine/QuPath-0.5.0"
+/some/path/on/your/machine/QuPath-0.5.0
```
@@ -88,9 +88,9 @@ so go ahead and hack.
- When contributing code, please try to use Pull Requests.
- tests go hand in hand with modules on ```tests``` packages at the same level. We use ```pytest```.
-You can setup your IDE to help you adhering to these guidelines.
+You can set up your IDE to help you to adhere to these guidelines.
-_([Santi](https://github.com/sdvillal) is happy to help you setting up pycharm in 5 minutes)_
+_([Santi](https://github.com/sdvillal) is happy to help you to set up pycharm in 5 minutes)_
## Acknowledgements
diff --git a/paquo/__main__.py b/paquo/__main__.py
index 9bee0e9..d453361 100644
--- a/paquo/__main__.py
+++ b/paquo/__main__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import argparse
import functools
import os
@@ -8,6 +10,7 @@
from itertools import repeat
from logging.config import dictConfig
from pathlib import Path
+from typing import Callable
from paquo._cli import DirectoryType
from paquo._cli import argument
@@ -270,14 +273,14 @@ def quickview(args, subparser):
f_annotations = None
if args.annotations:
- def f_annotations(name):
+ def f_annotations(name): # noqa
if name != image.name:
return []
return list(args.annotations)
f_annotations_cmd = None
if args.annotations_cmd:
- def f_annotations_cmd(name):
+ def f_annotations_cmd(name): # noqa
import shlex
_cmd = shlex.split(f"{args.annotations_cmd} {name}")
print("annotations", _cmd)
@@ -286,7 +289,7 @@ def f_annotations_cmd(name):
cmd = None
if f_annotations_cmd or f_annotations:
- def cmd(name):
+ def cmd(name): # noqa
names = []
if f_annotations:
names.extend(f_annotations(name))
@@ -332,13 +335,16 @@ def _download_cb(it, name):
finally:
print("")
- file = download_qupath(
- args.version,
- path=args.download_path,
- callback=_download_cb,
- system=system,
- ssl_verify=not args.no_ssl_verify
- )
+ try:
+ file = download_qupath(
+ args.version,
+ path=args.download_path,
+ callback=_download_cb,
+ system=system,
+ ssl_verify=not args.no_ssl_verify
+ )
+ except Exception:
+ raise SystemExit(1)
print("# extracting:", file)
app = extract_qupath(file, args.install_path, system=system)
print("# available at:", app)
diff --git a/paquo/_utils.py b/paquo/_utils.py
index de749da..171ab68 100644
--- a/paquo/_utils.py
+++ b/paquo/_utils.py
@@ -126,12 +126,23 @@ def download_qupath(
else:
raise ValueError(f"unsupported platform.system() == {system!r}")
- if "rc" not in version:
+ if not version.startswith("v"):
+ version = f"v{version}"
+
+ if Version(version) > Version("0.4.4"):
+ if system == "Darwin":
+ if platform.machine() == "arm64":
+ _sys = "Mac-arm64"
+ else:
+ _sys = "Mac-x64"
name = f"QuPath-{version}-{_sys}"
else:
- name = f"QuPath-{version}"
+ if "rc" not in version:
+ name = f"QuPath-{version[1:]}-{_sys}"
+ else:
+ name = f"QuPath-{version[1:]}"
- url = f"https://github.com/qupath/qupath/releases/download/v{version}/{name}.{ext}"
+ url = f"https://github.com/qupath/qupath/releases/download/{version}/{name}.{ext}"
chunk_size = 10 * 1024 * 1024
@@ -153,6 +164,7 @@ def download_qupath(
for chunk in callback(iter(lambda: f.read(chunk_size), b""), name=url):
tmp.write(chunk)
except Exception:
+ print("# error requesting:", url, file=sys.stderr)
try:
os.unlink(out_fn)
except OSError:
@@ -168,7 +180,7 @@ def extract_qupath(file, destination, system=None):
# normalize QuPath App dirname
m = re.match(
- r"QuPath-(?P[0-9]+[.][0-9]+[.][0-9]+(-rc[0-9]+|-m[0-9]+)?)",
+ r"QuPath-v?(?P[0-9]+[.][0-9]+[.][0-9]+(-rc[0-9]+|-m[0-9]+)?)",
fn,
)
@@ -236,9 +248,12 @@ def extract_qupath(file, destination, system=None):
pth = os.path.join(tmp_dir, name)
if name.startswith("QuPath") and os.path.isdir(pth):
break
+ if name.startswith("QuPath") and name.endswith(".exe") and os.path.isfile(pth):
+ pth = tmp_dir
+ break
else:
raise RuntimeError("no qupath extracted?")
- shutil.move(os.path.join(tmp_dir, name), qp_dst)
+ shutil.move(pth, qp_dst)
return qp_dst
else:
diff --git a/paquo/images.py b/paquo/images.py
index e86c5ba..32b95d0 100644
--- a/paquo/images.py
+++ b/paquo/images.py
@@ -548,7 +548,8 @@ def downsample_levels(self) -> List[Dict[str, float]]:
The available downsample levels can differ dependent
on which image backend is used by QuPath
"""
- md = self._image_server.getMetadata()
+ with redirect(stdout=True, stderr=True):
+ md = self._image_server.getMetadata()
levels = []
for level in range(int(md.nLevels())):
resolution_level = md.getLevel(level)
diff --git a/paquo/java.py b/paquo/java.py
index 55da01f..c0f3be0 100644
--- a/paquo/java.py
+++ b/paquo/java.py
@@ -119,7 +119,12 @@ def supports_newer_addobject_and_pathclass(self) -> bool:
PathAnnotationObject = JClass("qupath.lib.objects.PathAnnotationObject")
PathClass = JClass('qupath.lib.objects.classes.PathClass')
-PathClassFactory = JClass('qupath.lib.objects.classes.PathClassFactory')
+
+if not compatibility.supports_newer_addobject_and_pathclass():
+ PathClassFactory = JClass('qupath.lib.objects.classes.PathClassFactory')
+else:
+ PathClassFactory = None
+
PathDetectionObject = JClass("qupath.lib.objects.PathDetectionObject")
PathIO = JClass("qupath.lib.io.PathIO")
PathObjectHierarchy = JClass('qupath.lib.objects.hierarchy.PathObjectHierarchy')
diff --git a/paquo/tests/test_images.py b/paquo/tests/test_images.py
index acd423a..f7f1a9e 100644
--- a/paquo/tests/test_images.py
+++ b/paquo/tests/test_images.py
@@ -1,5 +1,6 @@
import platform
import shutil
+import sys
import tempfile
from contextlib import nullcontext
from operator import itemgetter
@@ -23,7 +24,11 @@ def image_entry(svs_small):
@pytest.fixture(scope='function')
def removable_svs_small(svs_small):
- with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
+ kw = {} if sys.version_info < (3, 10) else {"ignore_cleanup_errors": True}
+ with tempfile.TemporaryDirectory(
+ prefix='paquo-',
+ **kw
+ ) as tmpdir:
new_path = Path(tmpdir) / svs_small.name
shutil.copy(svs_small, new_path)
yield new_path
@@ -32,21 +37,35 @@ def removable_svs_small(svs_small):
@pytest.fixture(scope='function')
def project_with_removed_image(removable_svs_small):
with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
- qp = QuPathProject(tmpdir, mode='x')
- _ = qp.add_image(removable_svs_small, image_type=QuPathImageType.BRIGHTFIELD_H_E)
- qp.save()
- removable_svs_small.unlink()
- yield qp.path
+ with QuPathProject(tmpdir, mode='x') as qp:
+ _ = qp.add_image(removable_svs_small, image_type=QuPathImageType.BRIGHTFIELD_H_E)
+ qp.save()
+ path = qp.path
+ try:
+ removable_svs_small.unlink()
+ except PermissionError:
+ if platform.system() == "Windows":
+ pytest.xfail("Windows QuPath==0.5.0 permission issue")
+ else:
+ raise
+ yield path
@pytest.fixture(scope='function')
def project_with_removed_image_without_image_data(removable_svs_small):
with tempfile.TemporaryDirectory(prefix='paquo-') as tmpdir:
- qp = QuPathProject(tmpdir, mode='x')
- _ = qp.add_image(removable_svs_small)
- qp.save()
- removable_svs_small.unlink()
- yield qp.path
+ with QuPathProject(tmpdir, mode='x') as qp:
+ _ = qp.add_image(removable_svs_small)
+ qp.save()
+ path = qp.path
+ try:
+ removable_svs_small.unlink()
+ except PermissionError:
+ if platform.system() == "Windows":
+ pytest.xfail("Windows QuPath==0.5.0 permission issue")
+ else:
+ raise
+ yield path
def test_image_entry_return_hierarchy(image_entry):
@@ -74,10 +93,6 @@ def test_image_properties_from_image_server(image_entry):
assert image_entry.num_z_slices == 1
-@pytest.mark.xfail(
- platform.uname().machine == "arm64",
- reason="QuPath-vendored openslide not working on arm64"
-)
def test_image_downsample_levels(image_entry):
levels = [
{'downsample': 1.0,
@@ -85,11 +100,14 @@ def test_image_downsample_levels(image_entry):
'width': 2220},
# todo: when openslide can be used by qupath, this downsample level
# in the test image disappears. investigate...
- # {'downsample': 3.865438534407666,
- # 'height': 768,
- # 'width': 574},
+ {'downsample': 3.865438534407666,
+ 'height': 768,
+ 'width': 574},
]
- assert image_entry.downsample_levels == levels
+ assert (
+ image_entry.downsample_levels == levels
+ or image_entry.downsample_levels == levels[:1]
+ )
def test_metadata_interface(image_entry):
diff --git a/paquo/tests/test_projects.py b/paquo/tests/test_projects.py
index 31841c2..0d3b6f0 100644
--- a/paquo/tests/test_projects.py
+++ b/paquo/tests/test_projects.py
@@ -8,6 +8,7 @@
import pytest
+from paquo._utils import QuPathVersion
from paquo.images import ImageProvider, QuPathProjectImageEntry, QuPathImageType
from paquo.projects import QuPathProject
@@ -284,7 +285,7 @@ def test_project_save_image_data(new_project, svs_small):
assert (entry.entry_path / "data.qpdata").is_file()
-def test_project_delete_image_file_when_opened(new_project, svs_small):
+def test_project_delete_image_file_when_opened(new_project, svs_small, qupath_version):
# prepare new image to be deleted
new_svs_small = new_project.path.parent / f"image_be_gone{svs_small.suffix}"
shutil.copy(svs_small, new_svs_small)
@@ -314,7 +315,16 @@ def test_project_delete_image_file_when_opened(new_project, svs_small):
elif qupath_uses == "OPENSLIDE":
- os.unlink(new_svs_small)
+ if (
+ qupath_version >= QuPathVersion("0.5.0")
+ and platform.system() == "Windows"
+ ):
+ cm = pytest.raises(PermissionError)
+ else:
+ cm = nullcontext()
+
+ with cm:
+ os.unlink(new_svs_small)
else: # pragma: no cover
raise ValueError('...')
@@ -389,7 +399,13 @@ def test_project_image_uri_update_try_relative(tmp_path, svs_small):
# NOW move the location
location_1 = tmp_path / "location_1"
- shutil.move(location_0, location_1)
+ try:
+ shutil.move(location_0, location_1)
+ except PermissionError:
+ if platform.system() == "Windows":
+ pytest.xfail("Windows QuPath==0.5.0 quirk with permissions")
+ else:
+ raise
with QuPathProject(location_1 / "project", mode='r') as qp:
entry, = qp.images
diff --git a/setup.cfg b/setup.cfg
index 8611f09..85e7026 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,6 +20,7 @@ classifiers =
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
Topic :: Scientific/Engineering
Topic :: Scientific/Engineering :: Visualization
Topic :: Scientific/Engineering :: Information Analysis