Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/maintainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
## Maintainer issue templates

- [Release checklist](https://github.com/openslide/openslide-python/issues/new?title=Release+X.Y.Z&body=%23+OpenSlide+Python+release+process%0A%0A-+%5B+%5D+Update+%60CHANGELOG.md%60+and+version+in+%60openslide%2F_version.py%60%0A-+%5B+%5D+Create+and+push+signed+tag%0A-+%5B+%5D+Find+the+%5Bworkflow+run%5D%28https%3A%2F%2Fgithub.com%2Fopenslide%2Fopenslide-python%2Factions%2Fworkflows%2Fpython.yml%29+for+the+tag%0A++-+%5B+%5D+Once+the+build+finishes%2C+approve+deployment+to+PyPI%0A++-+%5B+%5D+Download+the+docs+artifact%0A-+%5B+%5D+Verify+that+the+workflow+created+a+%5BPyPI+release%5D%28https%3A%2F%2Fpypi.org%2Fp%2Fopenslide-python%29+with+a+description%2C+source+tarball%2C+and+wheels%0A-+%5B+%5D+Verify+that+the+workflow+created+a+%5BGitHub+release%5D%28https%3A%2F%2Fgithub.com%2Fopenslide%2Fopenslide-python%2Freleases%29+with+release+notes%2C+source+tarballs%2C+and+wheels%0A-+%5B+%5D+%60cd%60+into+website+checkout%3B+%60rm+-r+api%2Fpython+%26%26+unzip+%2Fpath%2Fto%2Fdownloaded%2Fopenslide-python-docs.zip+%26%26+mv+openslide-python-docs-%2A+api%2Fpython%60%0A-+%5B+%5D+Update+website%3A+%60_data%2Freleases.yaml%60%2C+%60_includes%2Fnews.md%60%0A-+%5B+%5D+Start+a+%5BCI+build%5D%28https%3A%2F%2Fgithub.com%2Fopenslide%2Fopenslide.github.io%2Factions%2Fworkflows%2Fretile.yml%29+of+the+demo+site%0A-+%5B+%5D+Update+Ubuntu+PPA%0A-+%5B+%5D+Update+Fedora+and+possibly+EPEL+packages%0A-+%5B+%5D+Check+that+%5BCopr+package%5D%28https%3A%2F%2Fcopr.fedorainfracloud.org%2Fcoprs%2Fg%2Fopenslide%2Fopenslide%2Fbuilds%2F%29+built+successfully%0A-+%5B+%5D+Send+mail+to+-announce+and+-users%0A-+%5B+%5D+Post+to+%5Bforum.image.sc%5D%28https%3A%2F%2Fforum.image.sc%2Fc%2Fannouncements%2F10%29%0A-+%5B+%5D+Update+MacPorts+package&labels=release)
- [Update checklist for a Python minor release](https://github.com/openslide/openslide-python/issues/new?title=Add+Python+X.Y&body=%23+Adding+a+new+Python+release%0A%0A-+Update+Git+main%0A++-+%5B+%5D+%60git+checkout+main%60%0A++-+%5B+%5D+In+%60pyproject.toml%60%2C+add+classifier+for+new+Python+version+and+update+%60tool.black.target-version%60%0A++-+%5B+%5D+In+%60.github%2Fworkflows%2Fpython.yml%60%2C+update+hardcoded+Python+versions+and+add+new+version+to+lists%0A++-+%5B+%5D+Commit+and+open+a+PR%0A++-+%5B+%5D+Merge+the+PR+when+CI+passes%0A++-+%5B+%5D+Add+new+Python+jobs+to+%5Bbranch+protection+required+checks%5D%28https%3A%2F%2Fgithub.com%2Fopenslide%2Fopenslide-python%2Fsettings%2Fbranches%29%0A-+%5B+%5D+Update+MacPorts+package%0A-+%5B+%5D+Update+website%3A+Python+3+versions+in+%60download%2Findex.md%60&labels=release)
- [Update checklist for a Python minor release](https://github.com/openslide/openslide-python/issues/new?title=Add+Python+X.Y&body=%23+Adding+a+new+Python+release%0A%0A-+Update+Git+main%0A++-+%5B+%5D+%60git+checkout+main%60%0A++-+%5B+%5D+In+%60pyproject.toml%60%2C+add+classifier+for+new+Python+version%0A++-+%5B+%5D+In+%60.github%2Fworkflows%2Fpython.yml%60%2C+update+hardcoded+Python+versions+and+add+new+version+to+lists%0A++-+%5B+%5D+Commit+and+open+a+PR%0A++-+%5B+%5D+Merge+the+PR+when+CI+passes%0A++-+%5B+%5D+Add+new+Python+jobs+to+%5Bbranch+protection+required+checks%5D%28https%3A%2F%2Fgithub.com%2Fopenslide%2Fopenslide-python%2Fsettings%2Fbranches%29%0A-+%5B+%5D+Update+MacPorts+package%0A-+%5B+%5D+Update+website%3A+Python+3+versions+in+%60download%2Findex.md%60&labels=release)
4 changes: 2 additions & 2 deletions .github/maintainer/mkmaintainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
'labels': ','.join(info.get('labels', [])),
}
)
url = f"https://github.com/{info['repo']}/issues/new?{args}"
fh.write(f"- [{info['link-text']}]({url})\n")
url = f'https://github.com/{info["repo"]}/issues/new?{args}'
fh.write(f'- [{info["link-text"]}]({url})\n')
2 changes: 1 addition & 1 deletion .github/maintainer/update-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ labels: [release]

- Update Git main
- [ ] `git checkout main`
- [ ] In `pyproject.toml`, add classifier for new Python version and update `tool.black.target-version`
- [ ] In `pyproject.toml`, add classifier for new Python version
- [ ] In `.github/workflows/python.yml`, update hardcoded Python versions and add new version to lists
- [ ] Commit and open a PR
- [ ] Merge the PR when CI passes
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ jobs:
with:
python-version: '3.14'
- name: Install dependencies
run: python -m pip install pre-commit
run: python -m pip install prek uv
- name: Cache pre-commit environments
uses: actions/cache@v5
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/prek
key: prek|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit hooks
run: pre-commit run -a --show-diff-on-failure --color=always
run: prek run -a --show-diff-on-failure --color=always
- name: Define artifact paths
id: paths
run: |
Expand Down
59 changes: 23 additions & 36 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# priorities:
# 0 - read-only
# 1000 - mutually non-conflicting fixers
# none - other fixers

# exclude vendored files
exclude: '^(COPYING\.LESSER|examples/deepzoom/static/.*\.js)$'

Expand All @@ -6,58 +11,34 @@ repos:
rev: v6.0.0
hooks:
- id: check-added-large-files
priority: 0
- id: check-merge-conflict
priority: 0
- id: check-toml
priority: 0
- id: check-vcs-permalinks
priority: 0
- id: check-yaml
priority: 0
- id: end-of-file-fixer
- id: fix-byte-order-marker
- id: mixed-line-ending
- id: trailing-whitespace

- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
name: Modernize python code
args: ["--py310-plus"]

- repo: https://github.com/PyCQA/isort
rev: 8.0.1
hooks:
- id: isort
name: Reorder python imports with isort

- repo: https://github.com/psf/black
rev: 26.3.1
hooks:
- id: black
name: Format python code with black

- repo: https://github.com/asottile/blacken-docs
rev: 1.20.0
hooks:
- id: blacken-docs
name: Format python code in documentation

- repo: https://github.com/asottile/yesqa
rev: v1.5.0
hooks:
- id: yesqa
additional_dependencies: [flake8-bugbear, Flake8-pyproject]

- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.10
hooks:
- id: flake8
name: Lint python code with flake8
additional_dependencies: [flake8-bugbear, Flake8-pyproject]
- id: ruff-check
types_or: [python, pyi, pyproject]
args: [--fix]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.20.0
hooks:
- id: mypy
name: Check Python types
priority: 0
# Pillow is ignored in dependabot.yml
additional_dependencies: [flask, openslide-bin, pillow >= 12.1.0, types-PyYAML, types-setuptools]

Expand All @@ -66,32 +47,38 @@ repos:
hooks:
- id: rstcheck
name: Validate reStructuredText syntax
priority: 0
additional_dependencies: [sphinx, toml]

- repo: https://github.com/codespell-project/codespell
rev: v2.4.2
hooks:
- id: codespell
name: Check spelling with codespell
priority: 0
additional_dependencies:
- tomli # Python < 3.11

- repo: meta
hooks:
- id: check-hooks-apply
priority: 0
- id: check-useless-excludes
priority: 0

- repo: local
hooks:
- id: mkmaintainer
name: Sync maintainer issue templates
priority: 1000
entry: .github/maintainer/mkmaintainer.py
files: .github/maintainer/
language: python
additional_dependencies: [PyYAML]

- id: annotations
name: Require "from __future__ import annotations"
priority: 0
language: pygrep
types: [python]
# exclude config-like files
Expand Down
2 changes: 1 addition & 1 deletion doc/jekyll_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
}
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'}

Expand Down
6 changes: 3 additions & 3 deletions examples/deepzoom/deepzoom_multiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
from typing import TYPE_CHECKING, Any, Literal
import zlib

from PIL import Image, ImageCms
from flask import Flask, Response, abort, make_response, render_template, url_for
from PIL import Image, ImageCms

if TYPE_CHECKING:
# Python 3.10+
Expand All @@ -41,7 +41,7 @@
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
Expand Down Expand Up @@ -192,7 +192,7 @@ def tile(path: str, level: int, col: int, row: int, format: str) -> Response:
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
Expand Down
6 changes: 3 additions & 3 deletions examples/deepzoom/deepzoom_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
from unicodedata import normalize
import zlib

from PIL import Image, ImageCms
from flask import Flask, Response, abort, make_response, render_template, url_for
from PIL import Image, ImageCms

if TYPE_CHECKING:
# Python 3.10+
Expand All @@ -41,7 +41,7 @@
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
Expand Down Expand Up @@ -193,7 +193,7 @@ def tile(slug: str, level: int, col: int, row: int, format: str) -> Response:
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
Expand Down
7 changes: 3 additions & 4 deletions examples/deepzoom/deepzoom_tile.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
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
Expand Down Expand Up @@ -231,8 +231,7 @@ def _tile_done(self) -> None:
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,
)
Expand Down Expand Up @@ -320,7 +319,7 @@ def _url_for(self, associated: str | None) -> str:
base = VIEWER_SLIDE_NAME
else:
base = self._slugify(associated)
return '%s.dzi' % base
return f'{base}.dzi'

def _write_html(self) -> None:
import jinja2
Expand Down
49 changes: 26 additions & 23 deletions openslide/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,12 @@
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()

Expand Down Expand Up @@ -173,7 +169,9 @@ def get_thumbnail(self, size: tuple[int, int]) -> Image.Image:
"""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
Expand Down Expand Up @@ -457,27 +455,32 @@ def read_region(
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)
Expand All @@ -500,12 +503,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)
Loading