Skip to content

Commit 71c19b4

Browse files
authored
Merge pull request #286 from bgilbert/hints
Add type hints to ancillary code; add py.typed marker
2 parents aa2a01f + 8acd4d9 commit 71c19b4

14 files changed

+370
-317
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ repos:
5858
hooks:
5959
- id: mypy
6060
name: Check Python types
61-
additional_dependencies: [openslide-bin, pillow, types-setuptools]
62-
exclude: "^(doc/.*|tests/.*|examples/deepzoom/.*)$"
61+
additional_dependencies: [flask, openslide-bin, pillow, types-setuptools]
6362

6463
- repo: https://github.com/rstcheck/rstcheck
6564
rev: v6.2.4

doc/conf.py

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -174,98 +174,6 @@
174174
# This is the file name suffix for HTML files (e.g. ".xhtml").
175175
# html_file_suffix = None
176176

177-
# Output file base name for HTML help builder.
178-
htmlhelp_basename = 'OpenSlidePythondoc'
179-
180-
181-
# -- Options for LaTeX output --------------------------------------------------
182-
183-
latex_elements = {
184-
# The paper size ('letterpaper' or 'a4paper').
185-
# 'papersize': 'letterpaper',
186-
# The font size ('10pt', '11pt' or '12pt').
187-
# 'pointsize': '10pt',
188-
# Additional stuff for the LaTeX preamble.
189-
# 'preamble': '',
190-
}
191-
192-
# Grouping the document tree into LaTeX files. List of tuples
193-
# (source start file, target name, title, author, documentclass [howto/manual]).
194-
latex_documents = [
195-
(
196-
'index',
197-
'OpenSlidePython.tex',
198-
'OpenSlide Python Documentation',
199-
'OpenSlide project',
200-
'manual',
201-
),
202-
]
203-
204-
# The name of an image file (relative to this directory) to place at the top of
205-
# the title page.
206-
# latex_logo = None
207-
208-
# For "manual" documents, if this is true, then toplevel headings are parts,
209-
# not chapters.
210-
# latex_use_parts = False
211-
212-
# If true, show page references after internal links.
213-
# latex_show_pagerefs = False
214-
215-
# If true, show URL addresses after external links.
216-
# latex_show_urls = False
217-
218-
# Documents to append as an appendix to all manuals.
219-
# latex_appendices = []
220-
221-
# If false, no module index is generated.
222-
# latex_domain_indices = True
223-
224-
225-
# -- Options for manual page output --------------------------------------------
226-
227-
# One entry per manual page. List of tuples
228-
# (source start file, name, description, authors, manual section).
229-
man_pages = [
230-
(
231-
'index',
232-
'openslidepython',
233-
'OpenSlide Python Documentation',
234-
['OpenSlide project'],
235-
1,
236-
)
237-
]
238-
239-
# If true, show URL addresses after external links.
240-
# man_show_urls = False
241-
242-
243-
# -- Options for Texinfo output ------------------------------------------------
244-
245-
# Grouping the document tree into Texinfo files. List of tuples
246-
# (source start file, target name, title, author,
247-
# dir menu entry, description, category)
248-
texinfo_documents = [
249-
(
250-
'index',
251-
'OpenSlidePython',
252-
'OpenSlide Python Documentation',
253-
'OpenSlide project',
254-
'OpenSlidePython',
255-
'One line description of project.',
256-
'Miscellaneous',
257-
),
258-
]
259-
260-
# Documents to append as an appendix to all manuals.
261-
# texinfo_appendices = []
262-
263-
# If false, no module index is generated.
264-
# texinfo_domain_indices = True
265-
266-
# How to display URL addresses: 'footnote', 'no', or 'inline'.
267-
# texinfo_show_urls = 'footnote'
268-
269177

270178
# intersphinx
271179
intersphinx_mapping = {

doc/jekyll_fix.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import os
2929

30+
from sphinx.application import Sphinx
3031
from sphinx.util import logging
3132
from sphinx.util.console import bold
3233

@@ -41,7 +42,7 @@
4142
REWRITE_EXTENSIONS = {'.html', '.js'}
4243

4344

44-
def remove_path_underscores(app, exception):
45+
def remove_path_underscores(app: Sphinx, exception: Exception | None) -> None:
4546
if exception:
4647
return
4748
# Get logger
@@ -82,5 +83,5 @@ def remove_path_underscores(app, exception):
8283
logger.info('done')
8384

8485

85-
def setup(app):
86+
def setup(app: Sphinx) -> None:
8687
app.connect('build-finished', remove_path_underscores)

examples/deepzoom/deepzoom_multiserver.py

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,24 @@
2424
from argparse import ArgumentParser
2525
import base64
2626
from collections import OrderedDict
27+
from collections.abc import Callable
2728
from io import BytesIO
2829
import os
2930
from threading import Lock
31+
from typing import TYPE_CHECKING, Any, Literal
3032
import zlib
3133

32-
from PIL import ImageCms
33-
from flask import Flask, abort, make_response, render_template, url_for
34+
from PIL import Image, ImageCms
35+
from flask import Flask, Response, abort, make_response, render_template, url_for
36+
37+
if TYPE_CHECKING:
38+
# Python 3.10+
39+
from typing import TypeAlias
3440

3541
if os.name == 'nt':
3642
_dll_path = os.getenv('OPENSLIDE_PATH')
3743
if _dll_path is not None:
38-
with os.add_dll_directory(_dll_path):
44+
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined]
3945
import openslide
4046
else:
4147
import openslide
@@ -62,10 +68,36 @@
6268
)
6369
SRGB_PROFILE = ImageCms.getOpenProfile(BytesIO(SRGB_PROFILE_BYTES))
6470

71+
if TYPE_CHECKING:
72+
ColorMode: TypeAlias = Literal[
73+
'default',
74+
'absolute-colorimetric',
75+
'perceptual',
76+
'relative-colorimetric',
77+
'saturation',
78+
'embed',
79+
'ignore',
80+
]
81+
Transform: TypeAlias = Callable[[Image.Image], None]
82+
83+
84+
class DeepZoomMultiServer(Flask):
85+
basedir: str
86+
cache: _SlideCache
87+
88+
89+
class AnnotatedDeepZoomGenerator(DeepZoomGenerator):
90+
filename: str
91+
mpp: float
92+
transform: Transform
93+
6594

66-
def create_app(config=None, config_file=None):
95+
def create_app(
96+
config: dict[str, Any] | None = None,
97+
config_file: str | None = None,
98+
) -> Flask:
6799
# Create and configure app
68-
app = Flask(__name__)
100+
app = DeepZoomMultiServer(__name__)
69101
app.config.from_mapping(
70102
SLIDE_DIR='.',
71103
SLIDE_CACHE_SIZE=10,
@@ -99,7 +131,7 @@ def create_app(config=None, config_file=None):
99131
)
100132

101133
# Helper functions
102-
def get_slide(path):
134+
def get_slide(path: str) -> AnnotatedDeepZoomGenerator:
103135
path = os.path.abspath(os.path.join(app.basedir, path))
104136
if not path.startswith(app.basedir + os.path.sep):
105137
# Directory traversal
@@ -115,11 +147,11 @@ def get_slide(path):
115147

116148
# Set up routes
117149
@app.route('/')
118-
def index():
150+
def index() -> str:
119151
return render_template('files.html', root_dir=_Directory(app.basedir))
120152

121153
@app.route('/<path:path>')
122-
def slide(path):
154+
def slide(path: str) -> str:
123155
slide = get_slide(path)
124156
slide_url = url_for('dzi', path=path)
125157
return render_template(
@@ -130,15 +162,15 @@ def slide(path):
130162
)
131163

132164
@app.route('/<path:path>.dzi')
133-
def dzi(path):
165+
def dzi(path: str) -> Response:
134166
slide = get_slide(path)
135167
format = app.config['DEEPZOOM_FORMAT']
136168
resp = make_response(slide.get_dzi(format))
137169
resp.mimetype = 'application/xml'
138170
return resp
139171

140172
@app.route('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>')
141-
def tile(path, level, col, row, format):
173+
def tile(path: str, level: int, col: int, row: int, format: str) -> Response:
142174
slide = get_slide(path)
143175
format = format.lower()
144176
if format != 'jpeg' and format != 'png':
@@ -165,19 +197,27 @@ def tile(path, level, col, row, format):
165197

166198

167199
class _SlideCache:
168-
def __init__(self, cache_size, tile_cache_mb, dz_opts, color_mode):
200+
def __init__(
201+
self,
202+
cache_size: int,
203+
tile_cache_mb: int,
204+
dz_opts: dict[str, Any],
205+
color_mode: ColorMode,
206+
):
169207
self.cache_size = cache_size
170208
self.dz_opts = dz_opts
171209
self.color_mode = color_mode
172210
self._lock = Lock()
173-
self._cache = OrderedDict()
211+
self._cache: OrderedDict[str, AnnotatedDeepZoomGenerator] = OrderedDict()
174212
# Share a single tile cache among all slide handles, if supported
175213
try:
176-
self._tile_cache = OpenSlideCache(tile_cache_mb * 1024 * 1024)
214+
self._tile_cache: OpenSlideCache | None = OpenSlideCache(
215+
tile_cache_mb * 1024 * 1024
216+
)
177217
except OpenSlideVersionError:
178218
self._tile_cache = None
179219

180-
def get(self, path):
220+
def get(self, path: str) -> AnnotatedDeepZoomGenerator:
181221
with self._lock:
182222
if path in self._cache:
183223
# Move to end of LRU
@@ -188,7 +228,7 @@ def get(self, path):
188228
osr = OpenSlide(path)
189229
if self._tile_cache is not None:
190230
osr.set_cache(self._tile_cache)
191-
slide = DeepZoomGenerator(osr, **self.dz_opts)
231+
slide = AnnotatedDeepZoomGenerator(osr, **self.dz_opts)
192232
try:
193233
mpp_x = osr.properties[openslide.PROPERTY_NAME_MPP_X]
194234
mpp_y = osr.properties[openslide.PROPERTY_NAME_MPP_Y]
@@ -204,7 +244,7 @@ def get(self, path):
204244
self._cache[path] = slide
205245
return slide
206246

207-
def _get_transform(self, image):
247+
def _get_transform(self, image: OpenSlide) -> Transform:
208248
if image.color_profile is None:
209249
return lambda img: None
210250
mode = self.color_mode
@@ -215,7 +255,7 @@ def _get_transform(self, image):
215255
# embed ICC profile in tiles
216256
return lambda img: None
217257
elif mode == 'default':
218-
intent = ImageCms.getDefaultIntent(image.color_profile)
258+
intent = ImageCms.Intent(ImageCms.getDefaultIntent(image.color_profile))
219259
elif mode == 'absolute-colorimetric':
220260
intent = ImageCms.Intent.ABSOLUTE_COLORIMETRIC
221261
elif mode == 'relative-colorimetric':
@@ -232,10 +272,10 @@ def _get_transform(self, image):
232272
'RGB',
233273
'RGB',
234274
intent,
235-
0,
275+
ImageCms.Flags(0),
236276
)
237277

238-
def xfrm(img):
278+
def xfrm(img: Image.Image) -> None:
239279
ImageCms.applyTransform(img, transform, True)
240280
# Some browsers assume we intend the display's color space if we
241281
# don't embed the profile. Pillow's serialization is larger, so
@@ -246,9 +286,9 @@ def xfrm(img):
246286

247287

248288
class _Directory:
249-
def __init__(self, basedir, relpath=''):
289+
def __init__(self, basedir: str, relpath: str = ''):
250290
self.name = os.path.basename(relpath)
251-
self.children = []
291+
self.children: list[_Directory | _SlideFile] = []
252292
for name in sorted(os.listdir(os.path.join(basedir, relpath))):
253293
cur_relpath = os.path.join(relpath, name)
254294
cur_path = os.path.join(basedir, cur_relpath)
@@ -261,7 +301,7 @@ def __init__(self, basedir, relpath=''):
261301

262302

263303
class _SlideFile:
264-
def __init__(self, relpath):
304+
def __init__(self, relpath: str):
265305
self.name = os.path.basename(relpath)
266306
self.url_path = relpath
267307

0 commit comments

Comments
 (0)