[med-svn] [Git][med-team/openslide-python][upstream] New upstream version 1.4.6
Étienne Mollier (@emollier)
gitlab at salsa.debian.org
Sat Jun 27 12:35:55 BST 2026
Étienne Mollier pushed to branch upstream at Debian Med / openslide-python
Commits:
bd248eda by Étienne Mollier at 2026-06-27T13:01:03+02:00
New upstream version 1.4.6
- - - - -
25 changed files:
- CHANGELOG.md
- PKG-INFO
- README.md
- doc/index.rst
- doc/jekyll_fix.py
- examples/deepzoom/deepzoom_multiserver.py
- examples/deepzoom/deepzoom_server.py
- examples/deepzoom/deepzoom_tile.py
- examples/deepzoom/static/jquery.js
- examples/deepzoom/static/openseadragon.js
- examples/deepzoom/templates/files.html
- examples/deepzoom/templates/slide-fullpage.html
- examples/deepzoom/templates/slide-multipane.html
- openslide/__init__.py
- openslide/_convert.c
- openslide/_version.py
- openslide/deepzoom.py
- openslide/lowlevel.py
- openslide_python.egg-info/PKG-INFO
- pyproject.toml
- tests/common.py
- tests/test_base.py
- tests/test_deepzoom.py
- tests/test_imageslide.py
- tests/test_openslide.py
Changes:
=====================================
CHANGELOG.md
=====================================
@@ -1,5 +1,13 @@
# Notable Changes in OpenSlide Python
+## Version 1.4.6, 2026-06-07
+
+### Changes
+
+* Add attribute for `openslide.barcode` property
+* examples: Update OpenSeadragon to 6.0.2
+
+
## Version 1.4.3, 2025-12-03
### New features
=====================================
PKG-INFO
=====================================
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: openslide-python
-Version: 1.4.3
+Version: 1.4.6
Summary: Python interface to OpenSlide
Maintainer-email: OpenSlide project <openslide-users at lists.andrew.cmu.edu>
License-Expression: LGPL-2.1-only AND BSD-3-Clause AND MIT AND LicenseRef-Public-Domain
@@ -51,8 +51,10 @@ closest to a desired zoom level.
OpenSlide can read virtual slides in several formats:
* [Aperio][] (`.svs`, `.tif`)
+* [ARGOS][] (`.avs`)
* [DICOM][] (`.dcm`)
* [Hamamatsu][] (`.ndpi`, `.vms`, `.vmu`)
+* [Huron][] (`.tif`)
* [Leica][] (`.scn`)
* [MIRAX][] (`.mrxs`)
* [Philips][] (`.tiff`)
@@ -64,8 +66,10 @@ OpenSlide can read virtual slides in several formats:
[OpenSlide]: https://openslide.org/
[Aperio]: https://openslide.org/formats/aperio/
+[ARGOS]: https://openslide.org/formats/argos/
[DICOM]: https://openslide.org/formats/dicom/
[Hamamatsu]: https://openslide.org/formats/hamamatsu/
+[Huron]: https://openslide.org/formats/huron/
[Leica]: https://openslide.org/formats/leica/
[MIRAX]: https://openslide.org/formats/mirax/
[Philips]: https://openslide.org/formats/philips/
=====================================
README.md
=====================================
@@ -14,8 +14,10 @@ closest to a desired zoom level.
OpenSlide can read virtual slides in several formats:
* [Aperio][] (`.svs`, `.tif`)
+* [ARGOS][] (`.avs`)
* [DICOM][] (`.dcm`)
* [Hamamatsu][] (`.ndpi`, `.vms`, `.vmu`)
+* [Huron][] (`.tif`)
* [Leica][] (`.scn`)
* [MIRAX][] (`.mrxs`)
* [Philips][] (`.tiff`)
@@ -27,8 +29,10 @@ OpenSlide can read virtual slides in several formats:
[OpenSlide]: https://openslide.org/
[Aperio]: https://openslide.org/formats/aperio/
+[ARGOS]: https://openslide.org/formats/argos/
[DICOM]: https://openslide.org/formats/dicom/
[Hamamatsu]: https://openslide.org/formats/hamamatsu/
+[Huron]: https://openslide.org/formats/huron/
[Leica]: https://openslide.org/formats/leica/
[MIRAX]: https://openslide.org/formats/mirax/
[Philips]: https://openslide.org/formats/philips/
=====================================
doc/index.rst
=====================================
@@ -16,8 +16,10 @@ closest to a desired zoom level.
OpenSlide can read virtual slides in several formats:
* Aperio_ (``.svs``, ``.tif``)
+* ARGOS_ (``.avs``)
* DICOM_ (``.dcm``)
* Hamamatsu_ (``.ndpi``, ``.vms``, ``.vmu``)
+* Huron_ (``.tif``)
* Leica_ (``.scn``)
* MIRAX_ (``.mrxs``)
* Philips_ (``.tiff``)
@@ -32,8 +34,10 @@ Public License, version 2.1`_.
.. _OpenSlide: https://openslide.org/
.. _Aperio: https://openslide.org/formats/aperio/
+.. _ARGOS: https://openslide.org/formats/argos/
.. _DICOM: https://openslide.org/formats/dicom/
.. _Hamamatsu: https://openslide.org/formats/hamamatsu/
+.. _Huron: https://openslide.org/formats/huron/
.. _Leica: https://openslide.org/formats/leica/
.. _MIRAX: https://openslide.org/formats/mirax/
.. _Philips: https://openslide.org/formats/philips/
@@ -286,26 +290,38 @@ Standard properties
The :mod:`openslide` module provides attributes containing the names of
some commonly-used OpenSlide properties.
-.. data:: PROPERTY_NAME_COMMENT
+.. data:: PROPERTY_NAME_BACKGROUND_COLOR
- The name of the property containing a slide's comment, if any.
+ The name of the property containing a slide's background color, if any.
+ It is represented as an RGB hex triplet.
-.. data:: PROPERTY_NAME_VENDOR
+.. data:: PROPERTY_NAME_BARCODE
- The name of the property containing an identification of the vendor.
+ The name of the property containing a slide's barcode, if any.
-.. data:: PROPERTY_NAME_QUICKHASH1
+.. data:: PROPERTY_NAME_BOUNDS_HEIGHT
- The name of the property containing the "quickhash-1" sum.
+ The name of the property containing the height of the rectangle bounding
+ the non-empty region of the slide, if available.
-.. data:: PROPERTY_NAME_BACKGROUND_COLOR
+.. data:: PROPERTY_NAME_BOUNDS_WIDTH
- The name of the property containing a slide's background color, if any.
- It is represented as an RGB hex triplet.
+ The name of the property containing the width of the rectangle bounding
+ the non-empty region of the slide, if available.
-.. data:: PROPERTY_NAME_OBJECTIVE_POWER
+.. data:: PROPERTY_NAME_BOUNDS_X
- The name of the property containing a slide's objective power, if known.
+ The name of the property containing the X coordinate of the rectangle
+ bounding the non-empty region of the slide, if available.
+
+.. data:: PROPERTY_NAME_BOUNDS_Y
+
+ The name of the property containing the Y coordinate of the rectangle
+ bounding the non-empty region of the slide, if available.
+
+.. data:: PROPERTY_NAME_COMMENT
+
+ The name of the property containing a slide's comment, if any.
.. data:: PROPERTY_NAME_MPP_X
@@ -317,25 +333,17 @@ some commonly-used OpenSlide properties.
The name of the property containing the number of microns per pixel in
the Y dimension of level 0, if known.
-.. data:: PROPERTY_NAME_BOUNDS_X
-
- The name of the property containing the X coordinate of the rectangle
- bounding the non-empty region of the slide, if available.
-
-.. data:: PROPERTY_NAME_BOUNDS_Y
+.. data:: PROPERTY_NAME_OBJECTIVE_POWER
- The name of the property containing the Y coordinate of the rectangle
- bounding the non-empty region of the slide, if available.
+ The name of the property containing a slide's objective power, if known.
-.. data:: PROPERTY_NAME_BOUNDS_WIDTH
+.. data:: PROPERTY_NAME_QUICKHASH1
- The name of the property containing the width of the rectangle bounding
- the non-empty region of the slide, if available.
+ The name of the property containing the "quickhash-1" sum.
-.. data:: PROPERTY_NAME_BOUNDS_HEIGHT
+.. data:: PROPERTY_NAME_VENDOR
- The name of the property containing the height of the rectangle bounding
- the non-empty region of the slide, if available.
+ The name of the property containing an identification of the vendor.
Exceptions
=====================================
doc/jekyll_fix.py
=====================================
@@ -38,7 +38,7 @@ DIRS = {
}
FILES = {
# Added in Sphinx 5.0.0, scheduled to be removed in Sphinx 6
- 'static/_sphinx_javascript_frameworks_compat.js': 'static/sphinx_javascript_frameworks_compat.js', # noqa: E501
+ 'static/_sphinx_javascript_frameworks_compat.js': 'static/sphinx_javascript_frameworks_compat.js',
}
REWRITE_EXTENSIONS = {'.html', '.js'}
=====================================
examples/deepzoom/deepzoom_multiserver.py
=====================================
@@ -28,20 +28,16 @@ from io import BytesIO
import os
from pathlib import Path, PurePath
from threading import Lock
-from typing import TYPE_CHECKING, Any, Literal
+from typing import Any, Literal, TypeAlias
import zlib
-from PIL import Image, ImageCms
from flask import Flask, Response, abort, make_response, render_template, url_for
-
-if TYPE_CHECKING:
- # Python 3.10+
- from typing import TypeAlias
+from PIL import Image, ImageCms
if os.name == 'nt':
_dll_path = os.getenv('OPENSLIDE_PATH')
if _dll_path is not None:
- with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore] # noqa: E501
+ with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore]
import openslide
else:
import openslide
@@ -68,17 +64,16 @@ SRGB_PROFILE_BYTES = zlib.decompress(
)
SRGB_PROFILE = ImageCms.getOpenProfile(BytesIO(SRGB_PROFILE_BYTES))
-if TYPE_CHECKING:
- ColorMode: TypeAlias = Literal[
- 'default',
- 'absolute-colorimetric',
- 'perceptual',
- 'relative-colorimetric',
- 'saturation',
- 'embed',
- 'ignore',
- ]
- Transform: TypeAlias = Callable[[Image.Image], None]
+ColorMode: TypeAlias = Literal[
+ 'default',
+ 'absolute-colorimetric',
+ 'perceptual',
+ 'relative-colorimetric',
+ 'saturation',
+ 'embed',
+ 'ignore',
+]
+Transform: TypeAlias = Callable[[Image.Image], None]
class DeepZoomMultiServer(Flask):
@@ -192,7 +187,7 @@ def create_app(
icc_profile=tile.info.get('icc_profile'),
)
resp = make_response(buf.getvalue())
- resp.mimetype = 'image/%s' % format
+ resp.mimetype = f'image/{format}'
return resp
return app
=====================================
examples/deepzoom/deepzoom_server.py
=====================================
@@ -27,21 +27,17 @@ from io import BytesIO
import os
from pathlib import Path
import re
-from typing import TYPE_CHECKING, Any, Literal
+from typing import Any, Literal, TypeAlias
from unicodedata import normalize
import zlib
-from PIL import Image, ImageCms
from flask import Flask, Response, abort, make_response, render_template, url_for
-
-if TYPE_CHECKING:
- # Python 3.10+
- from typing import TypeAlias
+from PIL import Image, ImageCms
if os.name == 'nt':
_dll_path = os.getenv('OPENSLIDE_PATH')
if _dll_path is not None:
- with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore] # noqa: E501
+ with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore]
import openslide
else:
import openslide
@@ -70,17 +66,16 @@ SRGB_PROFILE_BYTES = zlib.decompress(
)
SRGB_PROFILE = ImageCms.getOpenProfile(BytesIO(SRGB_PROFILE_BYTES))
-if TYPE_CHECKING:
- ColorMode: TypeAlias = Literal[
- 'default',
- 'absolute-colorimetric',
- 'perceptual',
- 'relative-colorimetric',
- 'saturation',
- 'embed',
- 'ignore',
- ]
- Transform: TypeAlias = Callable[[Image.Image], None]
+ColorMode: TypeAlias = Literal[
+ 'default',
+ 'absolute-colorimetric',
+ 'perceptual',
+ 'relative-colorimetric',
+ 'saturation',
+ 'embed',
+ 'ignore',
+]
+Transform: TypeAlias = Callable[[Image.Image], None]
class DeepZoomServer(Flask):
@@ -193,7 +188,7 @@ def create_app(
icc_profile=tile.info.get('icc_profile'),
)
resp = make_response(buf.getvalue())
- resp.mimetype = 'image/%s' % format
+ resp.mimetype = f'image/{format}'
return resp
return app
=====================================
examples/deepzoom/deepzoom_tile.py
=====================================
@@ -34,20 +34,16 @@ from pathlib import Path
import re
import shutil
import sys
-from typing import TYPE_CHECKING, Literal
+from typing import TYPE_CHECKING, Literal, TypeAlias
from unicodedata import normalize
import zlib
from PIL import Image, ImageCms
-if TYPE_CHECKING:
- # Python 3.10+
- from typing import TypeAlias
-
if os.name == 'nt':
_dll_path = os.getenv('OPENSLIDE_PATH')
if _dll_path is not None:
- with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore] # noqa: E501
+ with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore]
import openslide
else:
import openslide
@@ -76,20 +72,21 @@ SRGB_PROFILE_BYTES = zlib.decompress(
)
SRGB_PROFILE = ImageCms.getOpenProfile(BytesIO(SRGB_PROFILE_BYTES))
+ColorMode: TypeAlias = Literal[
+ 'default',
+ 'absolute-colorimetric',
+ 'perceptual',
+ 'relative-colorimetric',
+ 'saturation',
+ 'embed',
+ 'ignore',
+]
+Transform: TypeAlias = Callable[[Image.Image], None]
if TYPE_CHECKING:
- ColorMode: TypeAlias = Literal[
- 'default',
- 'absolute-colorimetric',
- 'perceptual',
- 'relative-colorimetric',
- 'saturation',
- 'embed',
- 'ignore',
- ]
+ # Python 3.12+
TileQueue: TypeAlias = multiprocessing.queues.JoinableQueue[
tuple[str | None, int, tuple[int, int], Path] | None
]
- Transform: TypeAlias = Callable[[Image.Image], None]
class TileWorker(Process):
@@ -231,8 +228,7 @@ class DeepZoomImageTiler:
count, total = self._processed, self._dz.tile_count
if count % 100 == 0 or count == total:
print(
- "Tiling %s: wrote %d/%d tiles"
- % (self._associated or 'slide', count, total),
+ f'Tiling {self._associated or "slide"}: wrote {count}/{total} tiles',
end='\r',
file=sys.stderr,
)
@@ -320,7 +316,7 @@ class DeepZoomStaticTiler:
base = VIEWER_SLIDE_NAME
else:
base = self._slugify(associated)
- return '%s.dzi' % base
+ return f'{base}.dzi'
def _write_html(self) -> None:
import jinja2
=====================================
examples/deepzoom/static/jquery.js
=====================================
The diff for this file was not included because it is too large.
=====================================
examples/deepzoom/static/openseadragon.js
=====================================
The diff for this file was not included because it is too large.
=====================================
examples/deepzoom/templates/files.html
=====================================
@@ -1,7 +1,7 @@
<!doctype html>
<title>Available Slides</title>
-<style type="text/css">
+<style>
li {
list-style-type: none;
margin: 0.4em 0;
=====================================
examples/deepzoom/templates/slide-fullpage.html
=====================================
@@ -2,7 +2,7 @@
<meta charset="utf-8">
<title>{{ slide_filename }}</title>
-<style type="text/css">
+<style>
html {
overflow: hidden;
}
@@ -22,10 +22,10 @@ div#view {
<div id="view"></div>
-<script type="text/javascript" src="{{ url_for('static', filename='jquery.js') }}"></script>
-<script type="text/javascript" src="{{ url_for('static', filename='openseadragon.js') }}"></script>
-<script type="text/javascript" src="{{ url_for('static', filename='openseadragon-scalebar.js') }}"></script>
-<script type="text/javascript">
+<script src="{{ url_for('static', filename='jquery.js') }}"></script>
+<script src="{{ url_for('static', filename='openseadragon.js') }}"></script>
+<script src="{{ url_for('static', filename='openseadragon-scalebar.js') }}"></script>
+<script>
$(function() {
var viewer = new OpenSeadragon({
id: "view",
=====================================
examples/deepzoom/templates/slide-multipane.html
=====================================
@@ -3,7 +3,7 @@
<title>Slide Viewer</title>
<meta name="viewport" content="user-scalable=no">
-<style type="text/css">
+<style>
html {
overflow: hidden;
}
@@ -110,10 +110,10 @@ div#properties dd {
{% endif %}
</div>
-<script type="text/javascript" src="static/jquery.js"></script>
-<script type="text/javascript" src="static/openseadragon.js"></script>
-<script type="text/javascript" src="static/openseadragon-scalebar.js"></script>
-<script type="text/javascript">
+<script src="static/jquery.js"></script>
+<script src="static/openseadragon.js"></script>
+<script src="static/openseadragon-scalebar.js"></script>
+<script>
$(function() {
var dzi_data = {{ dzi_data|default('{}')|safe }};
var viewer = new OpenSeadragon({
=====================================
openslide/__init__.py
=====================================
@@ -35,30 +35,27 @@ from PIL import Image, ImageCms
from openslide import lowlevel
# Re-exports for the benefit of library users
-from openslide._version import ( # noqa: F401 module-imported-but-unused
- __version__ as __version__,
-)
+from openslide._version import __version__ as __version__
+from openslide.lowlevel import OpenSlideError as OpenSlideError
from openslide.lowlevel import (
OpenSlideUnsupportedFormatError as OpenSlideUnsupportedFormatError,
)
-from openslide.lowlevel import ( # noqa: F401 module-imported-but-unused
- OpenSlideVersionError as OpenSlideVersionError,
-)
-from openslide.lowlevel import OpenSlideError as OpenSlideError
+from openslide.lowlevel import OpenSlideVersionError as OpenSlideVersionError
__library_version__ = lowlevel.get_version()
-PROPERTY_NAME_COMMENT = 'openslide.comment'
-PROPERTY_NAME_VENDOR = 'openslide.vendor'
-PROPERTY_NAME_QUICKHASH1 = 'openslide.quickhash-1'
PROPERTY_NAME_BACKGROUND_COLOR = 'openslide.background-color'
-PROPERTY_NAME_OBJECTIVE_POWER = 'openslide.objective-power'
-PROPERTY_NAME_MPP_X = 'openslide.mpp-x'
-PROPERTY_NAME_MPP_Y = 'openslide.mpp-y'
+PROPERTY_NAME_BARCODE = 'openslide.barcode'
+PROPERTY_NAME_BOUNDS_HEIGHT = 'openslide.bounds-height'
+PROPERTY_NAME_BOUNDS_WIDTH = 'openslide.bounds-width'
PROPERTY_NAME_BOUNDS_X = 'openslide.bounds-x'
PROPERTY_NAME_BOUNDS_Y = 'openslide.bounds-y'
-PROPERTY_NAME_BOUNDS_WIDTH = 'openslide.bounds-width'
-PROPERTY_NAME_BOUNDS_HEIGHT = 'openslide.bounds-height'
+PROPERTY_NAME_COMMENT = 'openslide.comment'
+PROPERTY_NAME_MPP_X = 'openslide.mpp-x'
+PROPERTY_NAME_MPP_Y = 'openslide.mpp-y'
+PROPERTY_NAME_OBJECTIVE_POWER = 'openslide.objective-power'
+PROPERTY_NAME_QUICKHASH1 = 'openslide.quickhash-1'
+PROPERTY_NAME_VENDOR = 'openslide.vendor'
_T = TypeVar('_T')
@@ -173,7 +170,9 @@ class AbstractSlide(metaclass=ABCMeta):
"""Return a PIL.Image containing an RGB thumbnail of the image.
size: the maximum size of the thumbnail."""
- downsample = max(dim / thumb for dim, thumb in zip(self.dimensions, size))
+ downsample = max(
+ dim / thumb for dim, thumb in zip(self.dimensions, size, strict=True)
+ )
level = self.get_best_level_for_downsample(downsample)
tile = self.read_region((0, 0), level, self.level_dimensions[level])
# Apply on solid background
@@ -394,9 +393,7 @@ class ImageSlide(AbstractSlide):
If the file format is not recognized, return None."""
try:
with Image.open(filename) as img:
- # img currently resolves as Any
- # https://github.com/python-pillow/Pillow/pull/8362
- return img.format # type: ignore[no-any-return]
+ return img.format
except OSError:
return None
@@ -459,27 +456,32 @@ class ImageSlide(AbstractSlide):
if self._image is None:
raise ValueError('Cannot read from a closed slide')
if level != 0:
- raise OpenSlideError("Invalid level")
+ raise OpenSlideError('Invalid level')
if ['fail' for s in size if s < 0]:
- raise OpenSlideError(f"Size {size} must be non-negative")
+ raise OpenSlideError(f'Size {size} must be non-negative')
# Any corner of the requested region may be outside the bounds of
# the image. Create a transparent tile of the correct size and
# paste the valid part of the region into the correct location.
image_topleft = [
- max(0, min(l, limit - 1)) for l, limit in zip(location, self._image.size)
+ max(0, min(l, limit - 1))
+ for l, limit in zip(location, self._image.size, strict=True)
]
image_bottomright = [
max(0, min(l + s - 1, limit - 1))
- for l, s, limit in zip(location, size, self._image.size)
+ for l, s, limit in zip(location, size, self._image.size, strict=True)
]
- tile = Image.new("RGBA", size, (0,) * 4)
+ tile = Image.new('RGBA', size, (0,) * 4)
if not [
- 'fail' for tl, br in zip(image_topleft, image_bottomright) if br - tl < 0
+ 'fail'
+ for tl, br in zip(image_topleft, image_bottomright, strict=True)
+ if br - tl < 0
]: # "< 0" not a typo
# Crop size is greater than zero in both dimensions.
# PIL thinks the bottom right is the first *excluded* pixel
crop_box = tuple(image_topleft + [d + 1 for d in image_bottomright])
- tile_offset = tuple(il - l for il, l in zip(image_topleft, location))
+ tile_offset = tuple(
+ il - l for il, l in zip(image_topleft, location, strict=True)
+ )
assert len(crop_box) == 4 and len(tile_offset) == 2
crop = self._image.crop(crop_box)
tile.paste(crop, tile_offset)
@@ -502,12 +504,12 @@ def open_slide(filename: lowlevel.Filename) -> OpenSlide | ImageSlide:
if __name__ == '__main__':
import sys
- print("OpenSlide vendor:", OpenSlide.detect_format(sys.argv[1]))
- print("PIL format:", ImageSlide.detect_format(sys.argv[1]))
+ print('OpenSlide vendor:', OpenSlide.detect_format(sys.argv[1]))
+ print('PIL format:', ImageSlide.detect_format(sys.argv[1]))
with open_slide(sys.argv[1]) as _slide:
- print("Dimensions:", _slide.dimensions)
- print("Levels:", _slide.level_count)
- print("Level dimensions:", _slide.level_dimensions)
- print("Level downsamples:", _slide.level_downsamples)
- print("Properties:", _slide.properties)
- print("Associated images:", _slide.associated_images)
+ print('Dimensions:', _slide.dimensions)
+ print('Levels:', _slide.level_count)
+ print('Level dimensions:', _slide.level_dimensions)
+ print('Level downsamples:', _slide.level_downsamples)
+ print('Properties:', _slide.properties)
+ print('Associated images:', _slide.associated_images)
=====================================
openslide/_convert.c
=====================================
@@ -48,9 +48,9 @@ argb2rgba(PY_UINT32_T *buf, Py_ssize_t len)
u8 g = 255 * ((val >> 8) & 0xff) / a;
u8 b = 255 * ((val >> 0) & 0xff) / a;
#ifdef WORDS_BIGENDIAN
- val = r << 24 | g << 16 | b << 8 | a;
+ val = (PY_UINT32_T) r << 24 | g << 16 | b << 8 | a;
#else
- val = a << 24 | b << 16 | g << 8 | r;
+ val = (PY_UINT32_T) a << 24 | b << 16 | g << 8 | r;
#endif
buf[cur] = val;
break;
=====================================
openslide/_version.py
=====================================
@@ -21,4 +21,4 @@
This module is an implementation detail. The package version should be
obtained from openslide.__version__."""
-__version__ = '1.4.3'
+__version__ = '1.4.6'
=====================================
openslide/deepzoom.py
=====================================
@@ -26,17 +26,13 @@ from __future__ import annotations
from io import BytesIO
import math
-from typing import TYPE_CHECKING
+from typing import TypeGuard
from xml.etree.ElementTree import Element, ElementTree, SubElement
from PIL import Image
import openslide
-if TYPE_CHECKING:
- # Python 3.10+
- from typing import TypeGuard
-
class DeepZoomGenerator:
"""Generates Deep Zoom tiles and metadata."""
@@ -88,13 +84,15 @@ class DeepZoomGenerator:
# Slide level dimensions scale factor in each axis
size_scale = tuple(
int(osr.properties.get(prop, l0_lim)) / l0_lim
- for prop, l0_lim in zip(self.BOUNDS_SIZE_PROPS, osr.dimensions)
+ for prop, l0_lim in zip(
+ self.BOUNDS_SIZE_PROPS, osr.dimensions, strict=True
+ )
)
# Dimensions of active area
self._l_dimensions = tuple(
tuple(
- int(math.ceil(l_lim * scale))
- for l_lim, scale in zip(l_size, size_scale)
+ math.ceil(l_lim * scale)
+ for l_lim, scale in zip(l_size, size_scale, strict=True)
)
for l_size in osr.level_dimensions
)
@@ -106,14 +104,14 @@ class DeepZoomGenerator:
z_size = self._l0_dimensions
z_dimensions = [z_size]
while z_size[0] > 1 or z_size[1] > 1:
- z_size = tuple(max(1, int(math.ceil(z / 2))) for z in z_size)
+ z_size = tuple(max(1, math.ceil(z / 2)) for z in z_size)
z_dimensions.append(z_size)
# Narrow the type, for self.level_dimensions
self._z_dimensions = self._pairs_from_n_tuples(tuple(reversed(z_dimensions)))
# Tile
def tiles(z_lim: int) -> int:
- return int(math.ceil(z_lim / self._z_t_downsample))
+ return math.ceil(z_lim / self._z_t_downsample)
self._t_dimensions = tuple(
(tiles(z_w), tiles(z_h)) for z_w, z_h in self._z_dimensions
@@ -147,13 +145,7 @@ class DeepZoomGenerator:
)
def __repr__(self) -> str:
- return '{}({!r}, tile_size={!r}, overlap={!r}, limit_bounds={!r})'.format(
- self.__class__.__name__,
- self._osr,
- self._z_t_downsample,
- self._z_overlap,
- self._limit_bounds,
- )
+ return f'{self.__class__.__name__}({self._osr!r}, tile_size={self._z_t_downsample!r}, overlap={self._z_overlap!r}, limit_bounds={self._limit_bounds!r})'
@property
def level_count(self) -> int:
@@ -208,10 +200,10 @@ class DeepZoomGenerator:
) -> tuple[tuple[tuple[int, int], int, tuple[int, int]], tuple[int, int]]:
# Check parameters
if dz_level < 0 or dz_level >= self._dz_levels:
- raise ValueError("Invalid level")
- for t, t_lim in zip(t_location, self._t_dimensions[dz_level]):
+ raise ValueError('Invalid level')
+ for t, t_lim in zip(t_location, self._t_dimensions[dz_level], strict=True):
if t < 0 or t >= t_lim:
- raise ValueError("Invalid address")
+ raise ValueError('Invalid address')
# Get preferred slide level
slide_level = self._slide_from_dz_level[dz_level]
@@ -220,14 +212,18 @@ class DeepZoomGenerator:
z_overlap_tl = tuple(self._z_overlap * int(t != 0) for t in t_location)
z_overlap_br = tuple(
self._z_overlap * int(t != t_lim - 1)
- for t, t_lim in zip(t_location, self.level_tiles[dz_level])
+ for t, t_lim in zip(t_location, self.level_tiles[dz_level], strict=True)
)
# Get final size of the tile
z_size = tuple(
min(self._z_t_downsample, z_lim - self._z_t_downsample * t) + z_tl + z_br
for t, z_lim, z_tl, z_br in zip(
- t_location, self._z_dimensions[dz_level], z_overlap_tl, z_overlap_br
+ t_location,
+ self._z_dimensions[dz_level],
+ z_overlap_tl,
+ z_overlap_br,
+ strict=True,
)
)
@@ -235,16 +231,18 @@ class DeepZoomGenerator:
z_location = [self._z_from_t(t) for t in t_location]
l_location = [
self._l_from_z(dz_level, z - z_tl)
- for z, z_tl in zip(z_location, z_overlap_tl)
+ for z, z_tl in zip(z_location, z_overlap_tl, strict=True)
]
# Round location down and size up, and add offset of active area
l0_location = tuple(
int(self._l0_from_l(slide_level, l) + l0_off)
- for l, l0_off in zip(l_location, self._l0_offset)
+ for l, l0_off in zip(l_location, self._l0_offset, strict=True)
)
l_size = tuple(
int(min(math.ceil(self._l_from_z(dz_level, dz)), l_lim - math.ceil(l)))
- for l, dz, l_lim in zip(l_location, z_size, self._l_dimensions[slide_level])
+ for l, dz, l_lim in zip(
+ l_location, z_size, self._l_dimensions[slide_level], strict=True
+ )
)
# Return read_region() parameters plus tile size for final scaling
=====================================
openslide/lowlevel.py
=====================================
@@ -50,16 +50,13 @@ from ctypes import (
from itertools import count
import os
import platform
-from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast
+from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, TypeAlias, TypeVar, cast
from PIL import Image
from . import _convert
if TYPE_CHECKING:
- # Python 3.10+
- from typing import ParamSpec, TypeAlias
-
from _convert import _Buffer
@@ -89,10 +86,10 @@ def _load_library() -> CDLL:
except FileNotFoundError as exc:
raise ModuleNotFoundError(
"Couldn't locate OpenSlide DLL. "
- "Try `pip install openslide-bin`, "
+ 'Try `pip install openslide-bin`, '
"or if you're using an OpenSlide binary package, "
"ensure you've called os.add_dll_directory(). "
- "https://openslide.org/api/python/#installing"
+ 'https://openslide.org/api/python/#installing'
) from exc
elif platform.system() == 'Darwin':
try:
@@ -107,8 +104,8 @@ def _load_library() -> CDLL:
if lib is None:
raise ModuleNotFoundError(
"Couldn't locate OpenSlide dylib. "
- "Try `pip install openslide-bin`. "
- "https://openslide.org/api/python/#installing"
+ 'Try `pip install openslide-bin`. '
+ 'https://openslide.org/api/python/#installing'
) from exc
return cdll.LoadLibrary(lib)
else:
@@ -117,8 +114,8 @@ def _load_library() -> CDLL:
except OSError as exc:
raise ModuleNotFoundError(
"Couldn't locate OpenSlide shared library. "
- "Try `pip install openslide-bin`. "
- "https://openslide.org/api/python/#installing"
+ 'Try `pip install openslide-bin`. '
+ 'https://openslide.org/api/python/#installing'
) from exc
@@ -170,11 +167,11 @@ class _OpenSlide:
@classmethod
def from_param(cls, obj: _OpenSlide) -> _OpenSlide:
if obj.__class__ != cls:
- raise ValueError("Not an OpenSlide reference")
+ raise ValueError('Not an OpenSlide reference')
if not obj._as_parameter_:
- raise ValueError("Passing undefined slide object")
+ raise ValueError('Passing undefined slide object')
if not obj._valid:
- raise ValueError("Passing closed slide object")
+ raise ValueError('Passing closed slide object')
return obj
@@ -193,15 +190,13 @@ class _OpenSlideCache:
@classmethod
def from_param(cls, obj: _OpenSlideCache) -> _OpenSlideCache:
if obj.__class__ != cls:
- raise ValueError("Not an OpenSlide cache reference")
+ raise ValueError('Not an OpenSlide cache reference')
if not obj._as_parameter_:
- raise ValueError("Passing undefined cache object")
+ raise ValueError('Passing undefined cache object')
return obj
-if TYPE_CHECKING:
- # Python 3.10+
- Filename: TypeAlias = str | bytes | os.PathLike[Any]
+Filename: TypeAlias = str | bytes | os.PathLike[Any]
class _filename_p:
@@ -247,7 +242,7 @@ class _size_t:
def _load_image(buf: _Buffer, size: tuple[int, int]) -> Image.Image:
- '''buf must be a mutable buffer.'''
+ """buf must be a mutable buffer."""
_convert.argb2rgba(buf)
return Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
@@ -255,7 +250,7 @@ def _load_image(buf: _Buffer, size: tuple[int, int]) -> Image.Image:
# check for errors opening an image file and wrap the resulting handle
def _check_open(result: int | None, _func: Any, _args: Any) -> _OpenSlide:
if result is None:
- raise OpenSlideUnsupportedFormatError("Unsupported or missing image file")
+ raise OpenSlideUnsupportedFormatError('Unsupported or missing image file')
slide = _OpenSlide(c_void_p(result))
err = get_error(slide)
if err is not None:
@@ -303,7 +298,7 @@ def _check_name_list(result: _Pointer[c_char_p], func: Any, args: Any) -> list[s
class _FunctionUnavailable:
- '''Standin for a missing optional function. Fails when called.'''
+ """Standin for a missing optional function. Fails when called."""
def __init__(self, minimum_version: str):
self._minimum_version = minimum_version
@@ -314,24 +309,25 @@ class _FunctionUnavailable:
raise OpenSlideVersionError(self._minimum_version)
-# gate runtime code that requires ParamSpec, Python 3.10+
-if TYPE_CHECKING:
- _P = ParamSpec('_P')
- _T = TypeVar('_T', covariant=True)
+_P = ParamSpec('_P')
+_T = TypeVar('_T', covariant=True)
- class _Func(Protocol[_P, _T]):
- available: bool
- def __call__(self, *args: _P.args) -> _T: ... # type: ignore[valid-type]
+class _Func(Protocol[_P, _T]):
+ available: bool
- class _CTypesFunc(_Func[_P, _T]):
- restype: type | None
- argtypes: list[type]
- errcheck: _ErrCheck
+ def __call__(self, *args: _P.args) -> _T: ... # type: ignore[valid-type]
- _ErrCheck: TypeAlias = (
- Callable[[Any, _CTypesFunc[..., Any], tuple[Any, ...]], Any] | None
- )
+
+class _CTypesFunc(_Func[_P, _T]):
+ restype: type | None
+ argtypes: list[type]
+ errcheck: _ErrCheck
+
+
+_ErrCheck: TypeAlias = (
+ Callable[[Any, _CTypesFunc[..., Any], tuple[Any, ...]], Any] | None
+)
# resolve and return an OpenSlide function with the specified properties
@@ -361,11 +357,7 @@ def _wraps_funcs(
wrapped: list[_Func[..., Any]],
) -> Callable[[Callable[_P, _T]], _Func[_P, _T]]:
def decorator(fn: Callable[_P, _T]) -> _Func[_P, _T]:
- if TYPE_CHECKING:
- # requires ParamSpec, Python 3.10+
- f = cast(_Func[_P, _T], fn)
- else:
- f = fn
+ f = cast('_Func[_P, _T]', fn)
f.available = True
for w in wrapped:
f.available = f.available and w.available
@@ -379,7 +371,7 @@ try:
'openslide_detect_vendor', c_char_p, [_filename_p], _check_string
)
except AttributeError:
- raise OpenSlideVersionError('3.4.0')
+ raise OpenSlideVersionError('3.4.0') from None
open: _Func[[Filename], _OpenSlide] = _func(
'openslide_open', c_void_p, [_filename_p], _check_open
@@ -434,7 +426,7 @@ def read_region(
# OpenSlide would catch this, but not before we tried to allocate
# a negative-size buffer
raise OpenSlideError(
- "negative width (%d) or negative height (%d) not allowed" % (w, h)
+ f'negative width ({w}) or negative height ({h}) not allowed'
)
if w == 0 or h == 0:
# Image.frombuffer() would raise an exception
=====================================
openslide_python.egg-info/PKG-INFO
=====================================
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: openslide-python
-Version: 1.4.3
+Version: 1.4.6
Summary: Python interface to OpenSlide
Maintainer-email: OpenSlide project <openslide-users at lists.andrew.cmu.edu>
License-Expression: LGPL-2.1-only AND BSD-3-Clause AND MIT AND LicenseRef-Public-Domain
@@ -51,8 +51,10 @@ closest to a desired zoom level.
OpenSlide can read virtual slides in several formats:
* [Aperio][] (`.svs`, `.tif`)
+* [ARGOS][] (`.avs`)
* [DICOM][] (`.dcm`)
* [Hamamatsu][] (`.ndpi`, `.vms`, `.vmu`)
+* [Huron][] (`.tif`)
* [Leica][] (`.scn`)
* [MIRAX][] (`.mrxs`)
* [Philips][] (`.tiff`)
@@ -64,8 +66,10 @@ OpenSlide can read virtual slides in several formats:
[OpenSlide]: https://openslide.org/
[Aperio]: https://openslide.org/formats/aperio/
+[ARGOS]: https://openslide.org/formats/argos/
[DICOM]: https://openslide.org/formats/dicom/
[Hamamatsu]: https://openslide.org/formats/hamamatsu/
+[Huron]: https://openslide.org/formats/huron/
[Leica]: https://openslide.org/formats/leica/
[MIRAX]: https://openslide.org/formats/mirax/
[Philips]: https://openslide.org/formats/philips/
=====================================
pyproject.toml
=====================================
@@ -50,28 +50,12 @@ version = {attr = "openslide._version.__version__"}
[tool.setuptools.package-data]
openslide = ["py.typed", "*.pyi"]
-[tool.black]
-skip-string-normalization = true
-target-version = ["py310", "py311", "py312", "py313", "py314"]
-
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
[tool.codespell]
check-hidden = true
# ignore-regex = ""
# ignore-words-list = ""
-# https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8
-# also ignore:
-# - E741 ambiguous variable name
-# requires Flake8-pyproject
-[tool.flake8]
-max-line-length = 88
-extend-ignore = ["E203", "E741"]
-
-[tool.isort]
-profile = "black"
-force_sort_within_sections = true
-
[tool.mypy]
python_version = "3.10"
strict = true
@@ -87,6 +71,17 @@ pythonpath = "tests"
[tool.rstcheck]
ignore_messages = "(Hyperlink target \".*\" is not referenced\\.$)"
+[tool.ruff.format]
+quote-style = "single"
+
+[tool.ruff.lint]
+extend-select = ["B", "C4", "FLY", "I", "RUF", "UP"]
+# ambiguous-variable-name
+ignore = ["E741"]
+
+[tool.ruff.lint.isort]
+force-sort-within-sections = true
+
[build-system]
requires = ["setuptools >= 77.0.0"]
build-backend = "setuptools.build_meta"
=====================================
tests/common.py
=====================================
@@ -29,7 +29,7 @@ if os.name == 'nt':
# environment.
_dll_path = os.getenv('OPENSLIDE_PATH')
if _dll_path is not None:
- with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore] # noqa: E501
+ with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore]
import openslide # noqa: F401 module-imported-but-unused
=====================================
tests/test_base.py
=====================================
@@ -34,7 +34,7 @@ class TestLibrary(unittest.TestCase):
self.assertTrue(isinstance(osr, ImageSlide))
def test_lowlevel_available(self) -> None:
- '''Ensure all exported functions have an 'available' attribute.'''
+ """Ensure all exported functions have an 'available' attribute."""
for name in dir(lowlevel):
attr = getattr(lowlevel, name)
# ignore classes and unexported functions
=====================================
tests/test_deepzoom.py
=====================================
@@ -44,8 +44,7 @@ class _Abstract:
def test_repr(self) -> None:
self.assertEqual(
repr(self.dz),
- 'DeepZoomGenerator(%r, tile_size=254, overlap=1, limit_bounds=False)'
- % self.osr,
+ f'DeepZoomGenerator({self.osr!r}, tile_size=254, overlap=1, limit_bounds=False)',
)
def test_metadata(self) -> None:
@@ -87,7 +86,7 @@ class _Abstract:
def test_tile_color_profile(self) -> None:
if self.CLASS is OpenSlide and not lowlevel.read_icc_profile.available:
- self.skipTest("requires OpenSlide 4.0.0")
+ self.skipTest('requires OpenSlide 4.0.0')
self.assertEqual(len(self.dz.get_tile(9, (1, 0)).info['icc_profile']), 588)
def test_get_tile_bad_level(self) -> None:
=====================================
tests/test_imageslide.py
=====================================
@@ -21,8 +21,8 @@ from __future__ import annotations
import sys
import unittest
-from PIL import Image
from common import file_path
+from PIL import Image
from openslide import ImageSlide, OpenSlideCache, OpenSlideError, lowlevel
@@ -42,7 +42,7 @@ class TestImageWithoutOpening(unittest.TestCase):
with Image.open(file_path('boxes.png')) as img:
with ImageSlide(img) as osr:
self.assertEqual(osr.dimensions, (300, 250))
- self.assertEqual(repr(osr), 'ImageSlide(%r)' % img)
+ self.assertEqual(repr(osr), f'ImageSlide({img!r})')
@unittest.skipUnless(
sys.getfilesystemencoding() == 'utf-8',
@@ -96,7 +96,7 @@ class TestImage(_Abstract.SlideTest):
FILENAME = 'boxes.png'
def test_repr(self) -> None:
- self.assertEqual(repr(self.osr), 'ImageSlide(%r)' % file_path('boxes.png'))
+ self.assertEqual(repr(self.osr), f'ImageSlide({file_path("boxes.png")!r})')
def test_metadata(self) -> None:
self.assertEqual(self.osr.level_count, 1)
@@ -141,7 +141,7 @@ class TestImage(_Abstract.SlideTest):
def test_thumbnail(self) -> None:
self.assertEqual(self.osr.get_thumbnail((100, 100)).size, (100, 83))
- @unittest.skipUnless(lowlevel.cache_create.available, "requires OpenSlide 4.0.0")
+ @unittest.skipUnless(lowlevel.cache_create.available, 'requires OpenSlide 4.0.0')
def test_set_cache(self) -> None:
self.osr.set_cache(OpenSlideCache(64 << 10))
self.assertEqual(self.osr.read_region((0, 0), 0, (400, 400)).size, (400, 400))
=====================================
tests/test_openslide.py
=====================================
@@ -36,14 +36,15 @@ from openslide import (
class TestCache(unittest.TestCase):
- @unittest.skipUnless(lowlevel.cache_create.available, "requires OpenSlide 4.0.0")
+ @unittest.skipUnless(lowlevel.cache_create.available, 'requires OpenSlide 4.0.0')
def test_create_cache(self) -> None:
OpenSlideCache(0)
OpenSlideCache(1)
OpenSlideCache(4 << 20)
self.assertRaises(ArgumentError, lambda: OpenSlideCache(-1))
self.assertRaises(
- ArgumentError, lambda: OpenSlideCache(1.3) # type: ignore[arg-type]
+ ArgumentError,
+ lambda: OpenSlideCache(1.3), # type: ignore[arg-type]
)
@@ -123,7 +124,7 @@ class TestSlide(_Abstract.SlideTest):
FILENAME = 'boxes.tiff'
def test_repr(self) -> None:
- self.assertEqual(repr(self.osr), 'OpenSlide(%r)' % file_path('boxes.tiff'))
+ self.assertEqual(repr(self.osr), f'OpenSlide({file_path("boxes.tiff")!r})')
def test_basic_metadata(self) -> None:
self.assertEqual(self.osr.level_count, 4)
@@ -145,15 +146,13 @@ class TestSlide(_Abstract.SlideTest):
self.assertEqual(self.osr.properties['openslide.vendor'], 'generic-tiff')
self.assertRaises(KeyError, lambda: self.osr.properties['__does_not_exist'])
# test __len__ and __iter__
+ self.assertEqual(len(list(self.osr.properties)), len(self.osr.properties))
self.assertEqual(
- len([v for v in self.osr.properties]), len(self.osr.properties)
- )
- self.assertEqual(
- repr(self.osr.properties), '<_PropertyMap %r>' % dict(self.osr.properties)
+ repr(self.osr.properties), f'<_PropertyMap {dict(self.osr.properties)!r}>'
)
@unittest.skipUnless(
- lowlevel.read_icc_profile.available, "requires OpenSlide 4.0.0"
+ lowlevel.read_icc_profile.available, 'requires OpenSlide 4.0.0'
)
def test_color_profile(self) -> None:
assert self.osr.color_profile is not None # for type inference
@@ -192,15 +191,17 @@ class TestSlide(_Abstract.SlideTest):
def test_thumbnail(self) -> None:
self.assertEqual(self.osr.get_thumbnail((100, 100)).size, (100, 83))
- @unittest.skipUnless(lowlevel.cache_create.available, "requires OpenSlide 4.0.0")
+ @unittest.skipUnless(lowlevel.cache_create.available, 'requires OpenSlide 4.0.0')
def test_set_cache(self) -> None:
self.osr.set_cache(OpenSlideCache(64 << 10))
self.assertEqual(self.osr.read_region((0, 0), 0, (400, 400)).size, (400, 400))
self.assertRaises(
- TypeError, lambda: self.osr.set_cache(None) # type: ignore[arg-type]
+ TypeError,
+ lambda: self.osr.set_cache(None), # type: ignore[arg-type]
)
self.assertRaises(
- TypeError, lambda: self.osr.set_cache(3) # type: ignore[arg-type]
+ TypeError,
+ lambda: self.osr.set_cache(3), # type: ignore[arg-type]
)
@@ -212,7 +213,7 @@ class TestAperioSlide(_Abstract.SlideTest):
self.assertRaises(KeyError, lambda: self.osr.associated_images['__missing'])
# test __len__ and __iter__
self.assertEqual(
- len([v for v in self.osr.associated_images]),
+ len(list(self.osr.associated_images)),
len(self.osr.associated_images),
)
@@ -221,7 +222,7 @@ class TestAperioSlide(_Abstract.SlideTest):
self.assertEqual(
mangle_repr(self.osr.associated_images),
- '<_AssociatedImageMap %s>' % mangle_repr(dict(self.osr.associated_images)),
+ f'<_AssociatedImageMap {mangle_repr(dict(self.osr.associated_images))}>',
)
def test_color_profile(self) -> None:
@@ -236,7 +237,7 @@ class TestAperioSlide(_Abstract.SlideTest):
# Requires DICOM support in OpenSlide. Use associated image ICC support as
# a proxy.
@unittest.skipUnless(
- lowlevel.read_associated_image_icc_profile.available, "requires OpenSlide 4.0.0"
+ lowlevel.read_associated_image_icc_profile.available, 'requires OpenSlide 4.0.0'
)
class TestDicomSlide(_Abstract.SlideTest):
FILENAME = 'boxes_0.dcm'
View it on GitLab: https://salsa.debian.org/med-team/openslide-python/-/commit/bd248edaceedaba4f505393c6408622a3bbaf229
--
View it on GitLab: https://salsa.debian.org/med-team/openslide-python/-/commit/bd248edaceedaba4f505393c6408622a3bbaf229
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20260627/b5106fff/attachment-0001.htm>
More information about the debian-med-commit
mailing list