Skip to content

Commit 012fbb1

Browse files
committed
examples/deepzoom: switch from os.path to pathlib
Signed-off-by: Benjamin Gilbert <[email protected]>
1 parent d63d015 commit 012fbb1

File tree

3 files changed

+65
-60
lines changed

3 files changed

+65
-60
lines changed

examples/deepzoom/deepzoom_multiserver.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# deepzoom_multiserver - Example web application for viewing multiple slides
44
#
55
# Copyright (c) 2010-2015 Carnegie Mellon University
6-
# Copyright (c) 2021-2023 Benjamin Gilbert
6+
# Copyright (c) 2021-2024 Benjamin Gilbert
77
#
88
# This library is free software; you can redistribute it and/or modify it
99
# under the terms of version 2.1 of the GNU Lesser General Public License
@@ -27,6 +27,7 @@
2727
from collections.abc import Callable
2828
from io import BytesIO
2929
import os
30+
from pathlib import Path, PurePath
3031
from threading import Lock
3132
from typing import TYPE_CHECKING, Any, Literal
3233
import zlib
@@ -82,7 +83,7 @@
8283

8384

8485
class DeepZoomMultiServer(Flask):
85-
basedir: str
86+
basedir: Path
8687
cache: _SlideCache
8788

8889

@@ -94,7 +95,7 @@ class AnnotatedDeepZoomGenerator(DeepZoomGenerator):
9495

9596
def create_app(
9697
config: dict[str, Any] | None = None,
97-
config_file: str | None = None,
98+
config_file: Path | None = None,
9899
) -> Flask:
99100
# Create and configure app
100101
app = DeepZoomMultiServer(__name__)
@@ -116,7 +117,7 @@ def create_app(
116117
app.config.from_mapping(config)
117118

118119
# Set up cache
119-
app.basedir = os.path.abspath(app.config['SLIDE_DIR'])
120+
app.basedir = Path(app.config['SLIDE_DIR']).resolve(strict=True)
120121
config_map = {
121122
'DEEPZOOM_TILE_SIZE': 'tile_size',
122123
'DEEPZOOM_OVERLAP': 'overlap',
@@ -131,16 +132,18 @@ def create_app(
131132
)
132133

133134
# Helper functions
134-
def get_slide(path: str) -> AnnotatedDeepZoomGenerator:
135-
path = os.path.abspath(os.path.join(app.basedir, path))
136-
if not path.startswith(app.basedir + os.path.sep):
137-
# Directory traversal
135+
def get_slide(user_path: PurePath) -> AnnotatedDeepZoomGenerator:
136+
try:
137+
path = (app.basedir / user_path).resolve(strict=True)
138+
except OSError:
139+
# Does not exist
138140
abort(404)
139-
if not os.path.exists(path):
141+
if path.parts[: len(app.basedir.parts)] != app.basedir.parts:
142+
# Directory traversal
140143
abort(404)
141144
try:
142145
slide = app.cache.get(path)
143-
slide.filename = os.path.basename(path)
146+
slide.filename = path.name
144147
return slide
145148
except OpenSlideError:
146149
abort(404)
@@ -152,7 +155,7 @@ def index() -> str:
152155

153156
@app.route('/<path:path>')
154157
def slide(path: str) -> str:
155-
slide = get_slide(path)
158+
slide = get_slide(PurePath(path))
156159
slide_url = url_for('dzi', path=path)
157160
return render_template(
158161
'slide-fullpage.html',
@@ -163,15 +166,15 @@ def slide(path: str) -> str:
163166

164167
@app.route('/<path:path>.dzi')
165168
def dzi(path: str) -> Response:
166-
slide = get_slide(path)
169+
slide = get_slide(PurePath(path))
167170
format = app.config['DEEPZOOM_FORMAT']
168171
resp = make_response(slide.get_dzi(format))
169172
resp.mimetype = 'application/xml'
170173
return resp
171174

172175
@app.route('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>')
173176
def tile(path: str, level: int, col: int, row: int, format: str) -> Response:
174-
slide = get_slide(path)
177+
slide = get_slide(PurePath(path))
175178
format = format.lower()
176179
if format != 'jpeg' and format != 'png':
177180
# Not supported by Deep Zoom
@@ -208,7 +211,7 @@ def __init__(
208211
self.dz_opts = dz_opts
209212
self.color_mode = color_mode
210213
self._lock = Lock()
211-
self._cache: OrderedDict[str, AnnotatedDeepZoomGenerator] = OrderedDict()
214+
self._cache: OrderedDict[Path, AnnotatedDeepZoomGenerator] = OrderedDict()
212215
# Share a single tile cache among all slide handles, if supported
213216
try:
214217
self._tile_cache: OpenSlideCache | None = OpenSlideCache(
@@ -217,7 +220,7 @@ def __init__(
217220
except OpenSlideVersionError:
218221
self._tile_cache = None
219222

220-
def get(self, path: str) -> AnnotatedDeepZoomGenerator:
223+
def get(self, path: Path) -> AnnotatedDeepZoomGenerator:
221224
with self._lock:
222225
if path in self._cache:
223226
# Move to end of LRU
@@ -286,13 +289,14 @@ def xfrm(img: Image.Image) -> None:
286289

287290

288291
class _Directory:
289-
def __init__(self, basedir: str, relpath: str = ''):
290-
self.name = os.path.basename(relpath)
292+
_DEFAULT_RELPATH = PurePath('.')
293+
294+
def __init__(self, basedir: Path, relpath: PurePath = _DEFAULT_RELPATH):
295+
self.name = relpath.name
291296
self.children: list[_Directory | _SlideFile] = []
292-
for name in sorted(os.listdir(os.path.join(basedir, relpath))):
293-
cur_relpath = os.path.join(relpath, name)
294-
cur_path = os.path.join(basedir, cur_relpath)
295-
if os.path.isdir(cur_path):
297+
for cur_path in sorted((basedir / relpath).iterdir()):
298+
cur_relpath = relpath / cur_path.name
299+
if cur_path.is_dir():
296300
cur_dir = _Directory(basedir, cur_relpath)
297301
if cur_dir.children:
298302
self.children.append(cur_dir)
@@ -301,9 +305,9 @@ def __init__(self, basedir: str, relpath: str = ''):
301305

302306

303307
class _SlideFile:
304-
def __init__(self, relpath: str):
305-
self.name = os.path.basename(relpath)
306-
self.url_path = relpath
308+
def __init__(self, relpath: PurePath):
309+
self.name = relpath.name
310+
self.url_path = relpath.as_posix()
307311

308312

309313
if __name__ == '__main__':
@@ -336,7 +340,7 @@ def __init__(self, relpath: str):
336340
),
337341
)
338342
parser.add_argument(
339-
'-c', '--config', metavar='FILE', dest='config', help='config file'
343+
'-c', '--config', metavar='FILE', type=Path, dest='config', help='config file'
340344
)
341345
parser.add_argument(
342346
'-d',
@@ -396,6 +400,7 @@ def __init__(self, relpath: str):
396400
parser.add_argument(
397401
'SLIDE_DIR',
398402
metavar='SLIDE-DIRECTORY',
403+
type=Path,
399404
nargs='?',
400405
help='slide directory',
401406
)

examples/deepzoom/deepzoom_server.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from collections.abc import Callable
2727
from io import BytesIO
2828
import os
29+
from pathlib import Path
2930
import re
3031
from typing import TYPE_CHECKING, Any, Literal, Mapping
3132
from unicodedata import normalize
@@ -93,7 +94,7 @@ class DeepZoomServer(Flask):
9394

9495
def create_app(
9596
config: dict[str, Any] | None = None,
96-
config_file: str | None = None,
97+
config_file: Path | None = None,
9798
) -> Flask:
9899
# Create and configure app
99100
app = DeepZoomServer(__name__)
@@ -113,9 +114,9 @@ def create_app(
113114
app.config.from_mapping(config)
114115

115116
# Open slide
116-
slidefile = app.config['DEEPZOOM_SLIDE']
117-
if slidefile is None:
117+
if app.config['DEEPZOOM_SLIDE'] is None:
118118
raise ValueError('No slide file specified')
119+
slidefile = Path(app.config['DEEPZOOM_SLIDE'])
119120
config_map = {
120121
'DEEPZOOM_TILE_SIZE': 'tile_size',
121122
'DEEPZOOM_OVERLAP': 'overlap',
@@ -273,7 +274,7 @@ def xfrm(img: Image.Image) -> None:
273274
),
274275
)
275276
parser.add_argument(
276-
'-c', '--config', metavar='FILE', dest='config', help='config file'
277+
'-c', '--config', metavar='FILE', type=Path, dest='config', help='config file'
277278
)
278279
parser.add_argument(
279280
'-d',
@@ -333,6 +334,7 @@ def xfrm(img: Image.Image) -> None:
333334
parser.add_argument(
334335
'DEEPZOOM_SLIDE',
335336
metavar='SLIDE',
337+
type=Path,
336338
nargs='?',
337339
help='slide file',
338340
)

examples/deepzoom/deepzoom_tile.py

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# deepzoom_tile - Convert whole-slide images to Deep Zoom format
44
#
55
# Copyright (c) 2010-2015 Carnegie Mellon University
6-
# Copyright (c) 2022-2023 Benjamin Gilbert
6+
# Copyright (c) 2022-2024 Benjamin Gilbert
77
#
88
# This library is free software; you can redistribute it and/or modify it
99
# under the terms of version 2.1 of the GNU Lesser General Public License
@@ -31,6 +31,7 @@
3131
from multiprocessing import JoinableQueue, Process
3232
import multiprocessing.queues
3333
import os
34+
from pathlib import Path
3435
import re
3536
import shutil
3637
import sys
@@ -87,7 +88,7 @@
8788
'ignore',
8889
]
8990
TileQueue: TypeAlias = multiprocessing.queues.JoinableQueue[
90-
tuple[str | None, int, tuple[int, int], str] | None
91+
tuple[str | None, int, tuple[int, int], Path] | None
9192
]
9293
Transform: TypeAlias = Callable[[Image.Image], None]
9394

@@ -98,7 +99,7 @@ class TileWorker(Process):
9899
def __init__(
99100
self,
100101
queue: TileQueue,
101-
slidepath: str,
102+
slidepath: Path,
102103
tile_size: int,
103104
overlap: int,
104105
limit_bounds: bool,
@@ -196,7 +197,7 @@ class DeepZoomImageTiler:
196197
def __init__(
197198
self,
198199
dz: DeepZoomGenerator,
199-
basename: str,
200+
basename: Path,
200201
format: str,
201202
associated: str | None,
202203
queue: TileQueue,
@@ -214,16 +215,15 @@ def run(self) -> None:
214215

215216
def _write_tiles(self) -> None:
216217
for level in range(self._dz.level_count):
217-
tiledir = os.path.join("%s_files" % self._basename, str(level))
218-
if not os.path.exists(tiledir):
219-
os.makedirs(tiledir)
218+
tiledir = self._basename.with_name(self._basename.name + '_files') / str(
219+
level
220+
)
221+
tiledir.mkdir(parents=True, exist_ok=True)
220222
cols, rows = self._dz.level_tiles[level]
221223
for row in range(rows):
222224
for col in range(cols):
223-
tilename = os.path.join(
224-
tiledir, '%d_%d.%s' % (col, row, self._format)
225-
)
226-
if not os.path.exists(tilename):
225+
tilename = tiledir / f'{col}_{row}.{self._format}'
226+
if not tilename.exists():
227227
self._queue.put((self._associated, level, (col, row), tilename))
228228
self._tile_done()
229229

@@ -241,7 +241,7 @@ def _tile_done(self) -> None:
241241
print(file=sys.stderr)
242242

243243
def _write_dzi(self) -> None:
244-
with open('%s.dzi' % self._basename, 'w') as fh:
244+
with self._basename.with_name(self._basename.name + '.dzi').open('w') as fh:
245245
fh.write(self.get_dzi())
246246

247247
def get_dzi(self) -> str:
@@ -253,8 +253,8 @@ class DeepZoomStaticTiler:
253253

254254
def __init__(
255255
self,
256-
slidepath: str,
257-
basename: str,
256+
slidepath: Path,
257+
basename: Path,
258258
format: str,
259259
tile_size: int,
260260
overlap: int,
@@ -303,12 +303,12 @@ def _run_image(self, associated: str | None = None) -> None:
303303
if associated is None:
304304
image = self._slide
305305
if self._with_viewer:
306-
basename = os.path.join(self._basename, VIEWER_SLIDE_NAME)
306+
basename = self._basename / VIEWER_SLIDE_NAME
307307
else:
308308
basename = self._basename
309309
else:
310310
image = ImageSlide(self._slide.associated_images[associated])
311-
basename = os.path.join(self._basename, self._slugify(associated))
311+
basename = self._basename / self._slugify(associated)
312312
dz = DeepZoomGenerator(
313313
image, self._tile_size, self._overlap, limit_bounds=self._limit_bounds
314314
)
@@ -335,9 +335,7 @@ def _write_html(self) -> None:
335335
# We're not running from a module (e.g. "python deepzoom_tile.py")
336336
# so PackageLoader('__main__') doesn't work in jinja2 3.x.
337337
# Load templates directly from the filesystem.
338-
loader = jinja2.FileSystemLoader(
339-
os.path.join(os.path.dirname(__file__), 'templates')
340-
)
338+
loader = jinja2.FileSystemLoader(Path(__file__).parent / 'templates')
341339
env = jinja2.Environment(loader=loader, autoescape=True)
342340
template = env.get_template('slide-multipane.html')
343341
associated_urls = {n: self._url_for(n) for n in self._slide.associated_images}
@@ -357,22 +355,20 @@ def _write_html(self) -> None:
357355
properties=self._slide.properties,
358356
dzi_data=json.dumps(self._dzi_data),
359357
)
360-
with open(os.path.join(self._basename, 'index.html'), 'w') as fh:
358+
with open(self._basename / 'index.html', 'w') as fh:
361359
fh.write(data)
362360

363361
def _write_static(self) -> None:
364-
basesrc = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
365-
basedst = os.path.join(self._basename, 'static')
362+
basesrc = Path(__file__).absolute().parent / 'static'
363+
basedst = self._basename / 'static'
366364
self._copydir(basesrc, basedst)
367-
self._copydir(os.path.join(basesrc, 'images'), os.path.join(basedst, 'images'))
365+
self._copydir(basesrc / 'images', basedst / 'images')
368366

369-
def _copydir(self, src: str, dest: str) -> None:
370-
if not os.path.exists(dest):
371-
os.makedirs(dest)
372-
for name in os.listdir(src):
373-
srcpath = os.path.join(src, name)
374-
if os.path.isfile(srcpath):
375-
shutil.copy(srcpath, os.path.join(dest, name))
367+
def _copydir(self, src: Path, dest: Path) -> None:
368+
dest.mkdir(parents=True, exist_ok=True)
369+
for srcpath in src.iterdir():
370+
if srcpath.is_file():
371+
shutil.copy(srcpath, dest / srcpath.name)
376372

377373
@classmethod
378374
def _slugify(cls, text: str) -> str:
@@ -444,6 +440,7 @@ def _shutdown(self) -> None:
444440
'-o',
445441
'--output',
446442
metavar='NAME',
443+
type=Path,
447444
dest='basename',
448445
help='base name of output file',
449446
)
@@ -475,12 +472,13 @@ def _shutdown(self) -> None:
475472
parser.add_argument(
476473
'slidepath',
477474
metavar='SLIDE',
475+
type=Path,
478476
help='slide file',
479477
)
480478

481479
args = parser.parse_args()
482480
if args.basename is None:
483-
args.basename = os.path.splitext(os.path.basename(args.slidepath))[0]
481+
args.basename = Path(args.slidepath.stem)
484482

485483
DeepZoomStaticTiler(
486484
args.slidepath,

0 commit comments

Comments
 (0)