From 996a116a6260ab0ff660f2ad1f2fb234b3c0d409 Mon Sep 17 00:00:00 2001 From: Serene-Arc Date: Sun, 22 Jan 2023 11:48:52 +1000 Subject: [PATCH] artresizer: type module --- beets/util/artresizer.py | 123 ++++++++++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 34 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index ffbc2edba..18a5ef82d 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -22,6 +22,8 @@ import platform import re import subprocess from itertools import chain +from typing import AnyStr, Tuple, Optional, Mapping, Union +from PIL import Image from urllib.parse import urlencode from beets import logging, util @@ -32,7 +34,7 @@ PROXY_URL = "https://images.weserv.nl/" log = logging.getLogger("beets") -def resize_url(url, maxwidth, quality=0): +def resize_url(url: str, maxwidth: int, quality: int = 0) -> str: """Return a proxied image URL that resizes the original image to maxwidth (preserving aspect ratio). """ @@ -124,8 +126,13 @@ class IMBackend(LocalBackend): self.compare_cmd = ["magick", "compare"] def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 - ): + self, + maxwidth: int, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + quality: int = 0, + max_filesize: int = 0, + ) -> AnyStr: """Resize using ImageMagick. Use the ``magick`` program or ``convert`` on older versions. Return @@ -174,7 +181,7 @@ class IMBackend(LocalBackend): return path_out - def get_size(self, path_in): + def get_size(self, path_in: str) -> Optional[Tuple[int, ...]]: cmd = self.identify_cmd + [ "-format", "%w %h", @@ -199,7 +206,7 @@ class IMBackend(LocalBackend): log.warning("Could not understand IM output: {0!r}", out) return None - def deinterlace(self, path_in, path_out=None): + def deinterlace(self, path_in: AnyStr, path_out: Optional[AnyStr] = None) -> AnyStr: if not path_out: path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in) @@ -217,7 +224,7 @@ class IMBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, filepath): + def get_format(self, filepath: AnyStr) -> Optional[bytes]: cmd = self.identify_cmd + ["-format", "%[magick]", syspath(filepath)] try: @@ -226,7 +233,12 @@ class IMBackend(LocalBackend): # FIXME: Should probably issue a warning? return None - def convert_format(self, source, target, deinterlaced): + def convert_format( + self, + source: AnyStr, + target: AnyStr, + deinterlaced: bool, + ) -> AnyStr: cmd = self.convert_cmd + [ syspath(source), *(["-interlace", "none"] if deinterlaced else []), @@ -243,10 +255,15 @@ class IMBackend(LocalBackend): return source @property - def can_compare(self): + def can_compare(self) -> bool: return self.version() > (6, 8, 7) - def compare(self, im1, im2, compare_threshold): + def compare( + self, + im1: Image, + im2: Image, + compare_threshold: float, + ) -> Optional[bool]: is_windows = platform.system() == "Windows" # Converting images to grayscale tends to minimize the weight @@ -329,10 +346,10 @@ class IMBackend(LocalBackend): return phash_diff <= compare_threshold @property - def can_write_metadata(self): + def can_write_metadata(self) -> bool: return True - def write_metadata(self, file, metadata): + def write_metadata(self, file: AnyStr, metadata: Mapping): assignments = list( chain.from_iterable(("-set", k, v) for k, v in metadata.items()) ) @@ -359,8 +376,13 @@ class PILBackend(LocalBackend): self.version() def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 - ): + self, + maxwidth: int, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + quality: int = 0, + max_filesize: int = 0, + ) -> AnyStr: """Resize using Python Imaging Library (PIL). Return the output path of resized image. """ @@ -429,7 +451,7 @@ class PILBackend(LocalBackend): ) return path_in - def get_size(self, path_in): + def get_size(self, path_in: AnyStr) -> Optional[Tuple[int, int]]: from PIL import Image try: @@ -441,7 +463,11 @@ class PILBackend(LocalBackend): ) return None - def deinterlace(self, path_in, path_out=None): + def deinterlace( + self, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + ) -> AnyStr: if not path_out: path_out = get_temp_filename(__name__, "deinterlace_PIL_", path_in) @@ -455,7 +481,7 @@ class PILBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, filepath): + def get_format(self, filepath: AnyStr) -> Optional[str]: from PIL import Image, UnidentifiedImageError try: @@ -470,7 +496,12 @@ class PILBackend(LocalBackend): log.exception("failed to detect image format for {}", filepath) return None - def convert_format(self, source, target, deinterlaced): + def convert_format( + self, + source: AnyStr, + target: AnyStr, + deinterlaced: bool, + ) -> str: from PIL import Image, UnidentifiedImageError try: @@ -488,18 +519,23 @@ class PILBackend(LocalBackend): return source @property - def can_compare(self): + def can_compare(self) -> bool: return False - def compare(self, im1, im2, compare_threshold): + def compare( + self, + im1: Image, + im2: Image, + compare_threshold: float, + ): # It is an error to call this when ArtResizer.can_compare is not True. raise NotImplementedError() @property - def can_write_metadata(self): + def can_write_metadata(self) -> bool: return True - def write_metadata(self, file, metadata): + def write_metadata(self, file: AnyStr, metadata: Mapping): from PIL import Image, PngImagePlugin # FIXME: Detect and handle other file types (currently, the only user @@ -523,7 +559,7 @@ class Shareable(type): cls._instance = None @property - def shared(cls): + def shared(cls) -> 'Shareable': if cls._instance is None: cls._instance = cls() return cls._instance @@ -554,14 +590,19 @@ class ArtResizer(metaclass=Shareable): self.local_method = None @property - def method(self): + def method(self) -> str: if self.local: return self.local_method.NAME else: return "WEBPROXY" def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 + self, + maxwidth: int, + path_in: AnyStr, + path_out: Optional[AnyStr]=None, + quality: int = 0, + max_filesize: int = 0, ): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a @@ -580,7 +621,11 @@ class ArtResizer(metaclass=Shareable): # Handled by `proxy_url` already. return path_in - def deinterlace(self, path_in, path_out=None): + def deinterlace( + self, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + ) -> AnyStr: """Deinterlace an image. Only available locally. @@ -591,7 +636,7 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return path_in - def proxy_url(self, maxwidth, url, quality=0): + def proxy_url(self, maxwidth: int, url: str, quality: int = 0): """Modifies an image URL according the method, returning a new URL. For WEBPROXY, a URL on the proxy server is returned. Otherwise, the URL is returned unmodified. @@ -603,13 +648,13 @@ class ArtResizer(metaclass=Shareable): return resize_url(url, maxwidth, quality) @property - def local(self): + def local(self) -> bool: """A boolean indicating whether the resizing method is performed locally (i.e., PIL or ImageMagick). """ return self.local_method is not None - def get_size(self, path_in): + def get_size(self, path_in: AnyStr) -> Union[Tuple[int, int], AnyStr]: """Return the size of an image file as an int couple (width, height) in pixels. @@ -621,7 +666,7 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return path_in - def get_format(self, path_in): + def get_format(self, path_in: AnyStr) -> Optional[str]: """Returns the format of the image as a string. Only available locally. @@ -632,7 +677,12 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return None - def reformat(self, path_in, new_format, deinterlaced=True): + def reformat( + self, + path_in: AnyStr, + new_format: str, + deinterlaced: bool = True, + ) -> AnyStr: """Converts image to desired format, updating its extension, but keeping the same filename. @@ -664,7 +714,7 @@ class ArtResizer(metaclass=Shareable): return result_path @property - def can_compare(self): + def can_compare(self) -> bool: """A boolean indicating whether image comparison is available""" if self.local: @@ -672,7 +722,12 @@ class ArtResizer(metaclass=Shareable): else: return False - def compare(self, im1, im2, compare_threshold): + def compare( + self, + im1: Image, + im2: Image, + compare_threshold: float, + ) -> Optional[bool]: """Return a boolean indicating whether two images are similar. Only available locally. @@ -684,7 +739,7 @@ class ArtResizer(metaclass=Shareable): return None @property - def can_write_metadata(self): + def can_write_metadata(self) -> bool: """A boolean indicating whether writing image metadata is supported.""" if self.local: @@ -692,7 +747,7 @@ class ArtResizer(metaclass=Shareable): else: return False - def write_metadata(self, file, metadata): + def write_metadata(self, file: AnyStr, metadata: Mapping): """Write key-value metadata to the image file. Only available locally. Currently, expects the image to be a PNG file.