diff --git a/.coveragerc b/.coveragerc index ca5f114c645..018cc1cbfb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,6 +12,9 @@ exclude_also = except ImportError if TYPE_CHECKING: @abc.abstractmethod + # Empty bodies in protocols or abstract methods + ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ + ^\s*\.\.\.(\s*#.*)?$ [run] omit = diff --git a/Tests/test_image.py b/Tests/test_image.py index 67a7d7eca54..4aa1ff7b344 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -162,8 +162,6 @@ def test_stringio(self) -> None: pass def test_pathlib(self, tmp_path: Path) -> None: - from PIL.Image import Path - with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" assert im.size == (10, 10) diff --git a/Tests/test_util.py b/Tests/test_util.py index b47ca88271c..73e4acd5555 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,29 +1,16 @@ from __future__ import annotations -from pathlib import Path +from pathlib import Path, PurePath import pytest from PIL import _util -def test_is_path() -> None: - # Arrange - fp = "filename.ext" - - # Act - it_is = _util.is_path(fp) - - # Assert - assert it_is - - -def test_path_obj_is_path() -> None: - # Arrange - from pathlib import Path - - test_path = Path("filename.ext") - +@pytest.mark.parametrize( + "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] +) +def test_is_path(test_path) -> None: # Act it_is = _util.is_path(test_path) diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 2e2d3322f75..99a18e9ea99 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,5 +1,5 @@ -Internal Reference Docs -======================= +Internal Reference +================== .. toctree:: :maxdepth: 2 diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index f2932c32200..899e4966ff2 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,14 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: StrOrBytesPath + + Typing alias. + +.. py:class:: SupportsRead + + An object that supports the read method. + .. py:data:: TypeGuard :value: typing.TypeGuard diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index f31941c9abb..730c8da5b80 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -3,7 +3,7 @@ File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +When opening a file as an image, Pillow requires a filename, ``os.PathLike`` object, or a file-like object. Pillow uses the filename or ``Path`` to open a file, so for the rest of this article, they will all be treated as a file-like object. diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7bb4736af13..88b87a22cd6 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,11 +27,12 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a """ from __future__ import annotations -from io import BytesIO +from typing import IO from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._typing import StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -80,7 +81,7 @@ def _open(self) -> None: ] -def open(fp: BytesIO, mode: str = "r") -> GdImageFile: +def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d9d708d5da4..a770488b7ec 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -40,7 +40,6 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum -from pathlib import Path from types import ModuleType from typing import IO, TYPE_CHECKING, Any @@ -2383,7 +2382,7 @@ def save(self, fp, format=None, **params) -> None: implement the ``seek``, ``tell``, and ``write`` methods, and be opened in binary mode. - :param fp: A filename (string), pathlib.Path object or file object. + :param fp: A filename (string), os.PathLike object or file object. :param format: Optional format override. If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this @@ -2398,11 +2397,8 @@ def save(self, fp, format=None, **params) -> None: filename: str | bytes = "" open_fp = False - if isinstance(fp, Path): - filename = str(fp) - open_fp = True - elif isinstance(fp, (str, bytes)): - filename = fp + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) open_fp = True elif fp == sys.stdout: try: @@ -3225,7 +3221,7 @@ def open(fp, mode="r", formats=None) -> Image: :py:meth:`~PIL.Image.Image.load` method). See :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - :param fp: A filename (string), pathlib.Path object or a file object. + :param fp: A filename (string), os.PathLike object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, and be opened in binary mode. The file object will also seek to zero diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 1ec8a9f4d1f..256c581df0c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,10 +33,10 @@ import warnings from enum import IntEnum from io import BytesIO -from pathlib import Path from typing import BinaryIO from . import Image +from ._typing import StrOrBytesPath from ._util import is_directory, is_path @@ -193,7 +193,7 @@ class FreeTypeFont: def __init__( self, - font: bytes | str | Path | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", @@ -230,8 +230,7 @@ def load_from_bytes(f): ) if is_path(font): - if isinstance(font, Path): - font = str(font) + font = os.path.realpath(os.fspath(font)) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index b9e9243e59f..1565612f869 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -14,17 +14,16 @@ # from __future__ import annotations -from io import BytesIO - from . import Image, ImageFile from ._binary import i8 +from ._typing import SupportsRead # # Bitstream parser class BitStream: - def __init__(self, fp: BytesIO) -> None: + def __init__(self, fp: SupportsRead[bytes]) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index ddea0b41467..7075e86726a 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,7 +1,8 @@ from __future__ import annotations +import os import sys -from typing import Sequence, Union +from typing import Protocol, Sequence, TypeVar, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -19,4 +20,14 @@ def __class_getitem__(cls, item: Any) -> type[bool]: Coords = Union[Sequence[float], Sequence[Sequence[float]]] -__all__ = ["TypeGuard"] +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, __length: int = ...) -> _T_co: ... + + +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + + +__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 13f369cca1d..6bc76281616 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,17 +1,16 @@ from __future__ import annotations import os -from pathlib import Path from typing import Any, NoReturn -from ._typing import TypeGuard +from ._typing import StrOrBytesPath, TypeGuard -def is_path(f: Any) -> TypeGuard[bytes | str | Path]: - return isinstance(f, (bytes, str, Path)) +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: + return isinstance(f, (bytes, str, os.PathLike)) -def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: +def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: """Checks if an object is a string, and that it points to a directory.""" return is_path(f) and os.path.isdir(f)