Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints to ancillary code; add py.typed marker #286

Merged
merged 6 commits into from
Oct 20, 2024
Merged
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
3 changes: 1 addition & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ repos:
hooks:
- id: mypy
name: Check Python types
additional_dependencies: [openslide-bin, pillow, types-setuptools]
exclude: "^(doc/.*|tests/.*|examples/deepzoom/.*)$"
additional_dependencies: [flask, openslide-bin, pillow, types-setuptools]

- repo: https://github.com/rstcheck/rstcheck
rev: v6.2.4
Expand Down
92 changes: 0 additions & 92 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,98 +174,6 @@
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None

# Output file base name for HTML help builder.
htmlhelp_basename = 'OpenSlidePythondoc'


# -- Options for LaTeX output --------------------------------------------------

latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
(
'index',
'OpenSlidePython.tex',
'OpenSlide Python Documentation',
'OpenSlide project',
'manual',
),
]

# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None

# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False

# If true, show page references after internal links.
# latex_show_pagerefs = False

# If true, show URL addresses after external links.
# latex_show_urls = False

# Documents to append as an appendix to all manuals.
# latex_appendices = []

# If false, no module index is generated.
# latex_domain_indices = True


# -- Options for manual page output --------------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(
'index',
'openslidepython',
'OpenSlide Python Documentation',
['OpenSlide project'],
1,
)
]

# If true, show URL addresses after external links.
# man_show_urls = False


# -- Options for Texinfo output ------------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
'index',
'OpenSlidePython',
'OpenSlide Python Documentation',
'OpenSlide project',
'OpenSlidePython',
'One line description of project.',
'Miscellaneous',
),
]

# Documents to append as an appendix to all manuals.
# texinfo_appendices = []

# If false, no module index is generated.
# texinfo_domain_indices = True

# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'


# intersphinx
intersphinx_mapping = {
Expand Down
5 changes: 3 additions & 2 deletions doc/jekyll_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import os

from sphinx.application import Sphinx
from sphinx.util import logging
from sphinx.util.console import bold

Expand All @@ -41,7 +42,7 @@
REWRITE_EXTENSIONS = {'.html', '.js'}


def remove_path_underscores(app, exception):
def remove_path_underscores(app: Sphinx, exception: Exception | None) -> None:
if exception:
return
# Get logger
Expand Down Expand Up @@ -82,5 +83,5 @@ def remove_path_underscores(app, exception):
logger.info('done')


def setup(app):
def setup(app: Sphinx) -> None:
app.connect('build-finished', remove_path_underscores)
84 changes: 62 additions & 22 deletions examples/deepzoom/deepzoom_multiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@
from argparse import ArgumentParser
import base64
from collections import OrderedDict
from collections.abc import Callable
from io import BytesIO
import os
from threading import Lock
from typing import TYPE_CHECKING, Any, Literal
import zlib

from PIL import ImageCms
from flask import Flask, abort, make_response, render_template, url_for
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

if os.name == 'nt':
_dll_path = os.getenv('OPENSLIDE_PATH')
if _dll_path is not None:
with os.add_dll_directory(_dll_path):
with os.add_dll_directory(_dll_path): # type: ignore[attr-defined]
import openslide
else:
import openslide
Expand All @@ -62,10 +68,36 @@
)
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]


class DeepZoomMultiServer(Flask):
basedir: str
cache: _SlideCache


class AnnotatedDeepZoomGenerator(DeepZoomGenerator):
filename: str
mpp: float
transform: Transform


def create_app(config=None, config_file=None):
def create_app(
config: dict[str, Any] | None = None,
config_file: str | None = None,
) -> Flask:
# Create and configure app
app = Flask(__name__)
app = DeepZoomMultiServer(__name__)
app.config.from_mapping(
SLIDE_DIR='.',
SLIDE_CACHE_SIZE=10,
Expand Down Expand Up @@ -99,7 +131,7 @@ def create_app(config=None, config_file=None):
)

# Helper functions
def get_slide(path):
def get_slide(path: str) -> AnnotatedDeepZoomGenerator:
path = os.path.abspath(os.path.join(app.basedir, path))
if not path.startswith(app.basedir + os.path.sep):
# Directory traversal
Expand All @@ -115,11 +147,11 @@ def get_slide(path):

# Set up routes
@app.route('/')
def index():
def index() -> str:
return render_template('files.html', root_dir=_Directory(app.basedir))

@app.route('/<path:path>')
def slide(path):
def slide(path: str) -> str:
slide = get_slide(path)
slide_url = url_for('dzi', path=path)
return render_template(
Expand All @@ -130,15 +162,15 @@ def slide(path):
)

@app.route('/<path:path>.dzi')
def dzi(path):
def dzi(path: str) -> Response:
slide = get_slide(path)
format = app.config['DEEPZOOM_FORMAT']
resp = make_response(slide.get_dzi(format))
resp.mimetype = 'application/xml'
return resp

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


class _SlideCache:
def __init__(self, cache_size, tile_cache_mb, dz_opts, color_mode):
def __init__(
self,
cache_size: int,
tile_cache_mb: int,
dz_opts: dict[str, Any],
color_mode: ColorMode,
):
self.cache_size = cache_size
self.dz_opts = dz_opts
self.color_mode = color_mode
self._lock = Lock()
self._cache = OrderedDict()
self._cache: OrderedDict[str, AnnotatedDeepZoomGenerator] = OrderedDict()
# Share a single tile cache among all slide handles, if supported
try:
self._tile_cache = OpenSlideCache(tile_cache_mb * 1024 * 1024)
self._tile_cache: OpenSlideCache | None = OpenSlideCache(
tile_cache_mb * 1024 * 1024
)
except OpenSlideVersionError:
self._tile_cache = None

def get(self, path):
def get(self, path: str) -> AnnotatedDeepZoomGenerator:
with self._lock:
if path in self._cache:
# Move to end of LRU
Expand All @@ -188,7 +228,7 @@ def get(self, path):
osr = OpenSlide(path)
if self._tile_cache is not None:
osr.set_cache(self._tile_cache)
slide = DeepZoomGenerator(osr, **self.dz_opts)
slide = AnnotatedDeepZoomGenerator(osr, **self.dz_opts)
try:
mpp_x = osr.properties[openslide.PROPERTY_NAME_MPP_X]
mpp_y = osr.properties[openslide.PROPERTY_NAME_MPP_Y]
Expand All @@ -204,7 +244,7 @@ def get(self, path):
self._cache[path] = slide
return slide

def _get_transform(self, image):
def _get_transform(self, image: OpenSlide) -> Transform:
if image.color_profile is None:
return lambda img: None
mode = self.color_mode
Expand All @@ -215,7 +255,7 @@ def _get_transform(self, image):
# embed ICC profile in tiles
return lambda img: None
elif mode == 'default':
intent = ImageCms.getDefaultIntent(image.color_profile)
intent = ImageCms.Intent(ImageCms.getDefaultIntent(image.color_profile))
elif mode == 'absolute-colorimetric':
intent = ImageCms.Intent.ABSOLUTE_COLORIMETRIC
elif mode == 'relative-colorimetric':
Expand All @@ -232,10 +272,10 @@ def _get_transform(self, image):
'RGB',
'RGB',
intent,
0,
ImageCms.Flags(0),
)

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


class _Directory:
def __init__(self, basedir, relpath=''):
def __init__(self, basedir: str, relpath: str = ''):
self.name = os.path.basename(relpath)
self.children = []
self.children: list[_Directory | _SlideFile] = []
for name in sorted(os.listdir(os.path.join(basedir, relpath))):
cur_relpath = os.path.join(relpath, name)
cur_path = os.path.join(basedir, cur_relpath)
Expand All @@ -261,7 +301,7 @@ def __init__(self, basedir, relpath=''):


class _SlideFile:
def __init__(self, relpath):
def __init__(self, relpath: str):
self.name = os.path.basename(relpath)
self.url_path = relpath

Expand Down
Loading
Loading