Skip to content

Commit 86369e5

Browse files
authored
Merge pull request #355 from bgilbert/prek
pre-commit: switch to Ruff and prek
2 parents ff9ef51 + b7fd964 commit 86369e5

18 files changed

Lines changed: 139 additions & 153 deletions

.github/maintainer/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
## Maintainer issue templates
44

55
- [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)
6-
- [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)
6+
- [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)

.github/maintainer/mkmaintainer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@
2323
'labels': ','.join(info.get('labels', [])),
2424
}
2525
)
26-
url = f"https://github.com/{info['repo']}/issues/new?{args}"
27-
fh.write(f"- [{info['link-text']}]({url})\n")
26+
url = f'https://github.com/{info["repo"]}/issues/new?{args}'
27+
fh.write(f'- [{info["link-text"]}]({url})\n')

.github/maintainer/update-python.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ labels: [release]
99

1010
- Update Git main
1111
- [ ] `git checkout main`
12-
- [ ] In `pyproject.toml`, add classifier for new Python version and update `tool.black.target-version`
12+
- [ ] In `pyproject.toml`, add classifier for new Python version
1313
- [ ] In `.github/workflows/python.yml`, update hardcoded Python versions and add new version to lists
1414
- [ ] Commit and open a PR
1515
- [ ] Merge the PR when CI passes

.github/workflows/python.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ jobs:
2828
with:
2929
python-version: '3.14'
3030
- name: Install dependencies
31-
run: python -m pip install pre-commit
31+
run: python -m pip install prek uv
3232
- name: Cache pre-commit environments
3333
uses: actions/cache@v5
3434
with:
35-
path: ~/.cache/pre-commit
36-
key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
35+
path: ~/.cache/prek
36+
key: prek|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
3737
- name: Run pre-commit hooks
38-
run: pre-commit run -a --show-diff-on-failure --color=always
38+
run: prek run -a --show-diff-on-failure --color=always
3939
- name: Define artifact paths
4040
id: paths
4141
run: |

.pre-commit-config.yaml

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# priorities:
2+
# 0 - read-only
3+
# 1000 - mutually non-conflicting fixers
4+
# none - other fixers
5+
16
# exclude vendored files
27
exclude: '^(COPYING\.LESSER|examples/deepzoom/static/.*\.js)$'
38

@@ -6,58 +11,34 @@ repos:
611
rev: v6.0.0
712
hooks:
813
- id: check-added-large-files
14+
priority: 0
915
- id: check-merge-conflict
16+
priority: 0
1017
- id: check-toml
18+
priority: 0
1119
- id: check-vcs-permalinks
20+
priority: 0
1221
- id: check-yaml
22+
priority: 0
1323
- id: end-of-file-fixer
1424
- id: fix-byte-order-marker
1525
- id: mixed-line-ending
1626
- id: trailing-whitespace
1727

18-
- repo: https://github.com/asottile/pyupgrade
19-
rev: v3.21.2
20-
hooks:
21-
- id: pyupgrade
22-
name: Modernize python code
23-
args: ["--py310-plus"]
24-
25-
- repo: https://github.com/PyCQA/isort
26-
rev: 8.0.1
27-
hooks:
28-
- id: isort
29-
name: Reorder python imports with isort
30-
31-
- repo: https://github.com/psf/black
32-
rev: 26.3.1
33-
hooks:
34-
- id: black
35-
name: Format python code with black
36-
37-
- repo: https://github.com/asottile/blacken-docs
38-
rev: 1.20.0
39-
hooks:
40-
- id: blacken-docs
41-
name: Format python code in documentation
42-
43-
- repo: https://github.com/asottile/yesqa
44-
rev: v1.5.0
45-
hooks:
46-
- id: yesqa
47-
additional_dependencies: [flake8-bugbear, Flake8-pyproject]
48-
49-
- repo: https://github.com/PyCQA/flake8
50-
rev: 7.3.0
28+
- repo: https://github.com/astral-sh/ruff-pre-commit
29+
rev: v0.15.10
5130
hooks:
52-
- id: flake8
53-
name: Lint python code with flake8
54-
additional_dependencies: [flake8-bugbear, Flake8-pyproject]
31+
- id: ruff-check
32+
types_or: [python, pyi, pyproject]
33+
args: [--fix]
34+
- id: ruff-format
5535

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

@@ -66,32 +47,38 @@ repos:
6647
hooks:
6748
- id: rstcheck
6849
name: Validate reStructuredText syntax
50+
priority: 0
6951
additional_dependencies: [sphinx, toml]
7052

7153
- repo: https://github.com/codespell-project/codespell
7254
rev: v2.4.2
7355
hooks:
7456
- id: codespell
7557
name: Check spelling with codespell
58+
priority: 0
7659
additional_dependencies:
7760
- tomli # Python < 3.11
7861

7962
- repo: meta
8063
hooks:
8164
- id: check-hooks-apply
65+
priority: 0
8266
- id: check-useless-excludes
67+
priority: 0
8368

8469
- repo: local
8570
hooks:
8671
- id: mkmaintainer
8772
name: Sync maintainer issue templates
73+
priority: 1000
8874
entry: .github/maintainer/mkmaintainer.py
8975
files: .github/maintainer/
9076
language: python
9177
additional_dependencies: [PyYAML]
9278

9379
- id: annotations
9480
name: Require "from __future__ import annotations"
81+
priority: 0
9582
language: pygrep
9683
types: [python]
9784
# exclude config-like files

doc/jekyll_fix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
}
3939
FILES = {
4040
# Added in Sphinx 5.0.0, scheduled to be removed in Sphinx 6
41-
'static/_sphinx_javascript_frameworks_compat.js': 'static/sphinx_javascript_frameworks_compat.js', # noqa: E501
41+
'static/_sphinx_javascript_frameworks_compat.js': 'static/sphinx_javascript_frameworks_compat.js',
4242
}
4343
REWRITE_EXTENSIONS = {'.html', '.js'}
4444

examples/deepzoom/deepzoom_multiserver.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from typing import TYPE_CHECKING, Any, Literal
3232
import zlib
3333

34-
from PIL import Image, ImageCms
3534
from flask import Flask, Response, abort, make_response, render_template, url_for
35+
from PIL import Image, ImageCms
3636

3737
if TYPE_CHECKING:
3838
# Python 3.10+
@@ -41,7 +41,7 @@
4141
if os.name == 'nt':
4242
_dll_path = os.getenv('OPENSLIDE_PATH')
4343
if _dll_path is not None:
44-
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore] # noqa: E501
44+
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore]
4545
import openslide
4646
else:
4747
import openslide
@@ -192,7 +192,7 @@ def tile(path: str, level: int, col: int, row: int, format: str) -> Response:
192192
icc_profile=tile.info.get('icc_profile'),
193193
)
194194
resp = make_response(buf.getvalue())
195-
resp.mimetype = 'image/%s' % format
195+
resp.mimetype = f'image/{format}'
196196
return resp
197197

198198
return app

examples/deepzoom/deepzoom_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from unicodedata import normalize
3232
import zlib
3333

34-
from PIL import Image, ImageCms
3534
from flask import Flask, Response, abort, make_response, render_template, url_for
35+
from PIL import Image, ImageCms
3636

3737
if TYPE_CHECKING:
3838
# Python 3.10+
@@ -41,7 +41,7 @@
4141
if os.name == 'nt':
4242
_dll_path = os.getenv('OPENSLIDE_PATH')
4343
if _dll_path is not None:
44-
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore] # noqa: E501
44+
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore]
4545
import openslide
4646
else:
4747
import openslide
@@ -193,7 +193,7 @@ def tile(slug: str, level: int, col: int, row: int, format: str) -> Response:
193193
icc_profile=tile.info.get('icc_profile'),
194194
)
195195
resp = make_response(buf.getvalue())
196-
resp.mimetype = 'image/%s' % format
196+
resp.mimetype = f'image/{format}'
197197
return resp
198198

199199
return app

examples/deepzoom/deepzoom_tile.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
if os.name == 'nt':
4848
_dll_path = os.getenv('OPENSLIDE_PATH')
4949
if _dll_path is not None:
50-
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore] # noqa: E501
50+
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined,unused-ignore]
5151
import openslide
5252
else:
5353
import openslide
@@ -231,8 +231,7 @@ def _tile_done(self) -> None:
231231
count, total = self._processed, self._dz.tile_count
232232
if count % 100 == 0 or count == total:
233233
print(
234-
"Tiling %s: wrote %d/%d tiles"
235-
% (self._associated or 'slide', count, total),
234+
f'Tiling {self._associated or "slide"}: wrote {count}/{total} tiles',
236235
end='\r',
237236
file=sys.stderr,
238237
)
@@ -320,7 +319,7 @@ def _url_for(self, associated: str | None) -> str:
320319
base = VIEWER_SLIDE_NAME
321320
else:
322321
base = self._slugify(associated)
323-
return '%s.dzi' % base
322+
return f'{base}.dzi'
324323

325324
def _write_html(self) -> None:
326325
import jinja2

openslide/__init__.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,12 @@
3535
from openslide import lowlevel
3636

3737
# Re-exports for the benefit of library users
38-
from openslide._version import ( # noqa: F401 module-imported-but-unused
39-
__version__ as __version__,
40-
)
38+
from openslide._version import __version__ as __version__
39+
from openslide.lowlevel import OpenSlideError as OpenSlideError
4140
from openslide.lowlevel import (
4241
OpenSlideUnsupportedFormatError as OpenSlideUnsupportedFormatError,
4342
)
44-
from openslide.lowlevel import ( # noqa: F401 module-imported-but-unused
45-
OpenSlideVersionError as OpenSlideVersionError,
46-
)
47-
from openslide.lowlevel import OpenSlideError as OpenSlideError
43+
from openslide.lowlevel import OpenSlideVersionError as OpenSlideVersionError
4844

4945
__library_version__ = lowlevel.get_version()
5046

@@ -173,7 +169,9 @@ def get_thumbnail(self, size: tuple[int, int]) -> Image.Image:
173169
"""Return a PIL.Image containing an RGB thumbnail of the image.
174170
175171
size: the maximum size of the thumbnail."""
176-
downsample = max(dim / thumb for dim, thumb in zip(self.dimensions, size))
172+
downsample = max(
173+
dim / thumb for dim, thumb in zip(self.dimensions, size, strict=True)
174+
)
177175
level = self.get_best_level_for_downsample(downsample)
178176
tile = self.read_region((0, 0), level, self.level_dimensions[level])
179177
# Apply on solid background
@@ -457,27 +455,32 @@ def read_region(
457455
if self._image is None:
458456
raise ValueError('Cannot read from a closed slide')
459457
if level != 0:
460-
raise OpenSlideError("Invalid level")
458+
raise OpenSlideError('Invalid level')
461459
if ['fail' for s in size if s < 0]:
462-
raise OpenSlideError(f"Size {size} must be non-negative")
460+
raise OpenSlideError(f'Size {size} must be non-negative')
463461
# Any corner of the requested region may be outside the bounds of
464462
# the image. Create a transparent tile of the correct size and
465463
# paste the valid part of the region into the correct location.
466464
image_topleft = [
467-
max(0, min(l, limit - 1)) for l, limit in zip(location, self._image.size)
465+
max(0, min(l, limit - 1))
466+
for l, limit in zip(location, self._image.size, strict=True)
468467
]
469468
image_bottomright = [
470469
max(0, min(l + s - 1, limit - 1))
471-
for l, s, limit in zip(location, size, self._image.size)
470+
for l, s, limit in zip(location, size, self._image.size, strict=True)
472471
]
473-
tile = Image.new("RGBA", size, (0,) * 4)
472+
tile = Image.new('RGBA', size, (0,) * 4)
474473
if not [
475-
'fail' for tl, br in zip(image_topleft, image_bottomright) if br - tl < 0
474+
'fail'
475+
for tl, br in zip(image_topleft, image_bottomright, strict=True)
476+
if br - tl < 0
476477
]: # "< 0" not a typo
477478
# Crop size is greater than zero in both dimensions.
478479
# PIL thinks the bottom right is the first *excluded* pixel
479480
crop_box = tuple(image_topleft + [d + 1 for d in image_bottomright])
480-
tile_offset = tuple(il - l for il, l in zip(image_topleft, location))
481+
tile_offset = tuple(
482+
il - l for il, l in zip(image_topleft, location, strict=True)
483+
)
481484
assert len(crop_box) == 4 and len(tile_offset) == 2
482485
crop = self._image.crop(crop_box)
483486
tile.paste(crop, tile_offset)
@@ -500,12 +503,12 @@ def open_slide(filename: lowlevel.Filename) -> OpenSlide | ImageSlide:
500503
if __name__ == '__main__':
501504
import sys
502505

503-
print("OpenSlide vendor:", OpenSlide.detect_format(sys.argv[1]))
504-
print("PIL format:", ImageSlide.detect_format(sys.argv[1]))
506+
print('OpenSlide vendor:', OpenSlide.detect_format(sys.argv[1]))
507+
print('PIL format:', ImageSlide.detect_format(sys.argv[1]))
505508
with open_slide(sys.argv[1]) as _slide:
506-
print("Dimensions:", _slide.dimensions)
507-
print("Levels:", _slide.level_count)
508-
print("Level dimensions:", _slide.level_dimensions)
509-
print("Level downsamples:", _slide.level_downsamples)
510-
print("Properties:", _slide.properties)
511-
print("Associated images:", _slide.associated_images)
509+
print('Dimensions:', _slide.dimensions)
510+
print('Levels:', _slide.level_count)
511+
print('Level dimensions:', _slide.level_dimensions)
512+
print('Level downsamples:', _slide.level_downsamples)
513+
print('Properties:', _slide.properties)
514+
print('Associated images:', _slide.associated_images)

0 commit comments

Comments
 (0)