diff --git a/doc/jekyll_fix.py b/doc/jekyll_fix.py index 798b8897..ef6555c5 100644 --- a/doc/jekyll_fix.py +++ b/doc/jekyll_fix.py @@ -2,6 +2,7 @@ # openslide-python - Python bindings for the OpenSlide library # # Copyright (c) 2014 Carnegie Mellon University +# Copyright (c) 2024 Benjamin Gilbert # # This library is free software; you can redistribute it and/or modify it # under the terms of version 2.1 of the GNU Lesser General Public License @@ -26,6 +27,7 @@ from __future__ import annotations import os +from pathlib import Path from sphinx.application import Sphinx from sphinx.util import logging @@ -49,37 +51,33 @@ def remove_path_underscores(app: Sphinx, exception: Exception | None) -> None: logger = logging.getLogger(__name__) logger.info(bold('fixing pathnames... '), nonl=True) # Rewrite references in HTML/JS files - for dirpath, _, filenames in os.walk(app.outdir): + outdir = Path(app.outdir) + for dirpath, _, filenames in os.walk(outdir): for filename in filenames: - _, ext = os.path.splitext(filename) - if ext in REWRITE_EXTENSIONS: - path = os.path.join(dirpath, filename) - with open(path, encoding='utf-8') as fh: + path = Path(dirpath) / filename + if path.suffix in REWRITE_EXTENSIONS: + with path.open(encoding='utf-8') as fh: contents = fh.read() for old, new in DIRS.items(): contents = contents.replace(old + '/', new + '/') for old, new in FILES.items(): contents = contents.replace(old, new) - with open(path, 'w', encoding='utf-8') as fh: + with path.open('w', encoding='utf-8') as fh: fh.write(contents) # Move directory contents for old, new in DIRS.items(): - olddir = os.path.join(app.outdir, old) - newdir = os.path.join(app.outdir, new) - if not os.path.exists(newdir): - os.mkdir(newdir) - if os.path.isdir(olddir): - for filename in os.listdir(olddir): - oldfile = os.path.join(olddir, filename) - newfile = os.path.join(newdir, filename) - os.rename(oldfile, newfile) - os.rmdir(olddir) + olddir = outdir / old + newdir = outdir / new + newdir.mkdir(exist_ok=True) + if olddir.is_dir(): + for oldfile in olddir.iterdir(): + oldfile.rename(newdir / oldfile.name) + olddir.rmdir() # Move files for old, new in FILES.items(): - oldfile = os.path.join(app.outdir, old) - newfile = os.path.join(app.outdir, new) - if os.path.isfile(oldfile): - os.rename(oldfile, newfile) + oldfile = outdir / old + if oldfile.is_file(): + oldfile.rename(outdir / new) logger.info('done') diff --git a/examples/deepzoom/deepzoom_multiserver.py b/examples/deepzoom/deepzoom_multiserver.py index 3869f3bb..46f51743 100755 --- a/examples/deepzoom/deepzoom_multiserver.py +++ b/examples/deepzoom/deepzoom_multiserver.py @@ -3,7 +3,7 @@ # deepzoom_multiserver - Example web application for viewing multiple slides # # Copyright (c) 2010-2015 Carnegie Mellon University -# Copyright (c) 2021-2023 Benjamin Gilbert +# Copyright (c) 2021-2024 Benjamin Gilbert # # This library is free software; you can redistribute it and/or modify it # under the terms of version 2.1 of the GNU Lesser General Public License @@ -27,6 +27,7 @@ from collections.abc import Callable from io import BytesIO import os +from pathlib import Path, PurePath from threading import Lock from typing import TYPE_CHECKING, Any, Literal import zlib @@ -82,7 +83,7 @@ class DeepZoomMultiServer(Flask): - basedir: str + basedir: Path cache: _SlideCache @@ -94,7 +95,7 @@ class AnnotatedDeepZoomGenerator(DeepZoomGenerator): def create_app( config: dict[str, Any] | None = None, - config_file: str | None = None, + config_file: Path | None = None, ) -> Flask: # Create and configure app app = DeepZoomMultiServer(__name__) @@ -116,7 +117,7 @@ def create_app( app.config.from_mapping(config) # Set up cache - app.basedir = os.path.abspath(app.config['SLIDE_DIR']) + app.basedir = Path(app.config['SLIDE_DIR']).resolve(strict=True) config_map = { 'DEEPZOOM_TILE_SIZE': 'tile_size', 'DEEPZOOM_OVERLAP': 'overlap', @@ -131,16 +132,18 @@ def create_app( ) # Helper functions - 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 + def get_slide(user_path: PurePath) -> AnnotatedDeepZoomGenerator: + try: + path = (app.basedir / user_path).resolve(strict=True) + except OSError: + # Does not exist abort(404) - if not os.path.exists(path): + if path.parts[: len(app.basedir.parts)] != app.basedir.parts: + # Directory traversal abort(404) try: slide = app.cache.get(path) - slide.filename = os.path.basename(path) + slide.filename = path.name return slide except OpenSlideError: abort(404) @@ -152,7 +155,7 @@ def index() -> str: @app.route('/') def slide(path: str) -> str: - slide = get_slide(path) + slide = get_slide(PurePath(path)) slide_url = url_for('dzi', path=path) return render_template( 'slide-fullpage.html', @@ -163,7 +166,7 @@ def slide(path: str) -> str: @app.route('/.dzi') def dzi(path: str) -> Response: - slide = get_slide(path) + slide = get_slide(PurePath(path)) format = app.config['DEEPZOOM_FORMAT'] resp = make_response(slide.get_dzi(format)) resp.mimetype = 'application/xml' @@ -171,7 +174,7 @@ def dzi(path: str) -> Response: @app.route('/_files//_.') def tile(path: str, level: int, col: int, row: int, format: str) -> Response: - slide = get_slide(path) + slide = get_slide(PurePath(path)) format = format.lower() if format != 'jpeg' and format != 'png': # Not supported by Deep Zoom @@ -208,7 +211,7 @@ def __init__( self.dz_opts = dz_opts self.color_mode = color_mode self._lock = Lock() - self._cache: OrderedDict[str, AnnotatedDeepZoomGenerator] = OrderedDict() + self._cache: OrderedDict[Path, AnnotatedDeepZoomGenerator] = OrderedDict() # Share a single tile cache among all slide handles, if supported try: self._tile_cache: OpenSlideCache | None = OpenSlideCache( @@ -217,7 +220,7 @@ def __init__( except OpenSlideVersionError: self._tile_cache = None - def get(self, path: str) -> AnnotatedDeepZoomGenerator: + def get(self, path: Path) -> AnnotatedDeepZoomGenerator: with self._lock: if path in self._cache: # Move to end of LRU @@ -286,13 +289,14 @@ def xfrm(img: Image.Image) -> None: class _Directory: - def __init__(self, basedir: str, relpath: str = ''): - self.name = os.path.basename(relpath) + _DEFAULT_RELPATH = PurePath('.') + + def __init__(self, basedir: Path, relpath: PurePath = _DEFAULT_RELPATH): + self.name = relpath.name 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) - if os.path.isdir(cur_path): + for cur_path in sorted((basedir / relpath).iterdir()): + cur_relpath = relpath / cur_path.name + if cur_path.is_dir(): cur_dir = _Directory(basedir, cur_relpath) if cur_dir.children: self.children.append(cur_dir) @@ -301,9 +305,9 @@ def __init__(self, basedir: str, relpath: str = ''): class _SlideFile: - def __init__(self, relpath: str): - self.name = os.path.basename(relpath) - self.url_path = relpath + def __init__(self, relpath: PurePath): + self.name = relpath.name + self.url_path = relpath.as_posix() if __name__ == '__main__': @@ -336,7 +340,7 @@ def __init__(self, relpath: str): ), ) parser.add_argument( - '-c', '--config', metavar='FILE', dest='config', help='config file' + '-c', '--config', metavar='FILE', type=Path, dest='config', help='config file' ) parser.add_argument( '-d', @@ -396,6 +400,7 @@ def __init__(self, relpath: str): parser.add_argument( 'SLIDE_DIR', metavar='SLIDE-DIRECTORY', + type=Path, nargs='?', help='slide directory', ) diff --git a/examples/deepzoom/deepzoom_server.py b/examples/deepzoom/deepzoom_server.py index 555997db..f57579e9 100755 --- a/examples/deepzoom/deepzoom_server.py +++ b/examples/deepzoom/deepzoom_server.py @@ -26,6 +26,7 @@ from collections.abc import Callable from io import BytesIO import os +from pathlib import Path import re from typing import TYPE_CHECKING, Any, Literal, Mapping from unicodedata import normalize @@ -93,7 +94,7 @@ class DeepZoomServer(Flask): def create_app( config: dict[str, Any] | None = None, - config_file: str | None = None, + config_file: Path | None = None, ) -> Flask: # Create and configure app app = DeepZoomServer(__name__) @@ -113,9 +114,9 @@ def create_app( app.config.from_mapping(config) # Open slide - slidefile = app.config['DEEPZOOM_SLIDE'] - if slidefile is None: + if app.config['DEEPZOOM_SLIDE'] is None: raise ValueError('No slide file specified') + slidefile = Path(app.config['DEEPZOOM_SLIDE']) config_map = { 'DEEPZOOM_TILE_SIZE': 'tile_size', 'DEEPZOOM_OVERLAP': 'overlap', @@ -273,7 +274,7 @@ def xfrm(img: Image.Image) -> None: ), ) parser.add_argument( - '-c', '--config', metavar='FILE', dest='config', help='config file' + '-c', '--config', metavar='FILE', type=Path, dest='config', help='config file' ) parser.add_argument( '-d', @@ -333,6 +334,7 @@ def xfrm(img: Image.Image) -> None: parser.add_argument( 'DEEPZOOM_SLIDE', metavar='SLIDE', + type=Path, nargs='?', help='slide file', ) diff --git a/examples/deepzoom/deepzoom_tile.py b/examples/deepzoom/deepzoom_tile.py index 46d3b542..05e4d9a7 100755 --- a/examples/deepzoom/deepzoom_tile.py +++ b/examples/deepzoom/deepzoom_tile.py @@ -3,7 +3,7 @@ # deepzoom_tile - Convert whole-slide images to Deep Zoom format # # Copyright (c) 2010-2015 Carnegie Mellon University -# Copyright (c) 2022-2023 Benjamin Gilbert +# Copyright (c) 2022-2024 Benjamin Gilbert # # This library is free software; you can redistribute it and/or modify it # under the terms of version 2.1 of the GNU Lesser General Public License @@ -31,6 +31,7 @@ from multiprocessing import JoinableQueue, Process import multiprocessing.queues import os +from pathlib import Path import re import shutil import sys @@ -87,7 +88,7 @@ 'ignore', ] TileQueue: TypeAlias = multiprocessing.queues.JoinableQueue[ - tuple[str | None, int, tuple[int, int], str] | None + tuple[str | None, int, tuple[int, int], Path] | None ] Transform: TypeAlias = Callable[[Image.Image], None] @@ -98,7 +99,7 @@ class TileWorker(Process): def __init__( self, queue: TileQueue, - slidepath: str, + slidepath: Path, tile_size: int, overlap: int, limit_bounds: bool, @@ -196,7 +197,7 @@ class DeepZoomImageTiler: def __init__( self, dz: DeepZoomGenerator, - basename: str, + basename: Path, format: str, associated: str | None, queue: TileQueue, @@ -214,16 +215,15 @@ def run(self) -> None: def _write_tiles(self) -> None: for level in range(self._dz.level_count): - tiledir = os.path.join("%s_files" % self._basename, str(level)) - if not os.path.exists(tiledir): - os.makedirs(tiledir) + tiledir = self._basename.with_name(self._basename.name + '_files') / str( + level + ) + tiledir.mkdir(parents=True, exist_ok=True) cols, rows = self._dz.level_tiles[level] for row in range(rows): for col in range(cols): - tilename = os.path.join( - tiledir, '%d_%d.%s' % (col, row, self._format) - ) - if not os.path.exists(tilename): + tilename = tiledir / f'{col}_{row}.{self._format}' + if not tilename.exists(): self._queue.put((self._associated, level, (col, row), tilename)) self._tile_done() @@ -241,7 +241,7 @@ def _tile_done(self) -> None: print(file=sys.stderr) def _write_dzi(self) -> None: - with open('%s.dzi' % self._basename, 'w') as fh: + with self._basename.with_name(self._basename.name + '.dzi').open('w') as fh: fh.write(self.get_dzi()) def get_dzi(self) -> str: @@ -253,8 +253,8 @@ class DeepZoomStaticTiler: def __init__( self, - slidepath: str, - basename: str, + slidepath: Path, + basename: Path, format: str, tile_size: int, overlap: int, @@ -303,12 +303,12 @@ def _run_image(self, associated: str | None = None) -> None: if associated is None: image = self._slide if self._with_viewer: - basename = os.path.join(self._basename, VIEWER_SLIDE_NAME) + basename = self._basename / VIEWER_SLIDE_NAME else: basename = self._basename else: image = ImageSlide(self._slide.associated_images[associated]) - basename = os.path.join(self._basename, self._slugify(associated)) + basename = self._basename / self._slugify(associated) dz = DeepZoomGenerator( image, self._tile_size, self._overlap, limit_bounds=self._limit_bounds ) @@ -335,9 +335,7 @@ def _write_html(self) -> None: # We're not running from a module (e.g. "python deepzoom_tile.py") # so PackageLoader('__main__') doesn't work in jinja2 3.x. # Load templates directly from the filesystem. - loader = jinja2.FileSystemLoader( - os.path.join(os.path.dirname(__file__), 'templates') - ) + loader = jinja2.FileSystemLoader(Path(__file__).parent / 'templates') env = jinja2.Environment(loader=loader, autoescape=True) template = env.get_template('slide-multipane.html') associated_urls = {n: self._url_for(n) for n in self._slide.associated_images} @@ -357,22 +355,20 @@ def _write_html(self) -> None: properties=self._slide.properties, dzi_data=json.dumps(self._dzi_data), ) - with open(os.path.join(self._basename, 'index.html'), 'w') as fh: + with open(self._basename / 'index.html', 'w') as fh: fh.write(data) def _write_static(self) -> None: - basesrc = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') - basedst = os.path.join(self._basename, 'static') + basesrc = Path(__file__).absolute().parent / 'static' + basedst = self._basename / 'static' self._copydir(basesrc, basedst) - self._copydir(os.path.join(basesrc, 'images'), os.path.join(basedst, 'images')) + self._copydir(basesrc / 'images', basedst / 'images') - def _copydir(self, src: str, dest: str) -> None: - if not os.path.exists(dest): - os.makedirs(dest) - for name in os.listdir(src): - srcpath = os.path.join(src, name) - if os.path.isfile(srcpath): - shutil.copy(srcpath, os.path.join(dest, name)) + def _copydir(self, src: Path, dest: Path) -> None: + dest.mkdir(parents=True, exist_ok=True) + for srcpath in src.iterdir(): + if srcpath.is_file(): + shutil.copy(srcpath, dest / srcpath.name) @classmethod def _slugify(cls, text: str) -> str: @@ -444,6 +440,7 @@ def _shutdown(self) -> None: '-o', '--output', metavar='NAME', + type=Path, dest='basename', help='base name of output file', ) @@ -475,12 +472,13 @@ def _shutdown(self) -> None: parser.add_argument( 'slidepath', metavar='SLIDE', + type=Path, help='slide file', ) args = parser.parse_args() if args.basename is None: - args.basename = os.path.splitext(os.path.basename(args.slidepath))[0] + args.basename = Path(args.slidepath.stem) DeepZoomStaticTiler( args.slidepath,