diff --git a/.coveragerc b/.coveragerc index ca5f114c6..018cc1cbf 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 67a7d7eca..4aa1ff7b3 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -162,8 +162,6 @@ class TestImage: 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 b47ca8827..73e4acd55 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 2e2d3322f..99a18e9ea 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 f2932c322..899e4966f 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 f31941c9a..730c8da5b 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 7bb4736af..88b87a22c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,11 +27,12 @@ """ 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 @@ class GdImageFile(ImageFile.ImageFile): ] -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 d9d708d5d..a770488b7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -40,7 +40,6 @@ import tempfile 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 @@ class Image: 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 @@ class Image: 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 1ec8a9f4d..256c581df 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,10 +33,10 @@ import sys 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 @@ class FreeTypeFont: ) 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 b9e9243e5..1565612f8 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 ddea0b414..7075e8672 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 @@ else: 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 13f369cca..6bc762816 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)