This commit is contained in:
Kuri Schlarb 2022-04-29 08:23:30 +00:00 committed by GitHub
commit c45b029717
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 25 deletions

View File

@ -1,5 +1,6 @@
import io import io
import os import os
import pathlib
import shutil import shutil
import sys import sys
import tempfile import tempfile
@ -21,6 +22,22 @@ from .helper import (
) )
class MyStrPathLike:
def __init__(self, path_str):
self.path_str = path_str
def __fspath__(self):
return self.path_str
class MyBytesPathLike:
def __init__(self, path_str):
self.path_bytes = os.fsencode(path_str)
def __fspath__(self):
return self.path_bytes
class TestImage: class TestImage:
def test_image_modes_success(self): def test_image_modes_success(self):
for mode in [ for mode in [
@ -149,22 +166,30 @@ class TestImage:
with Image.open(io.StringIO()): with Image.open(io.StringIO()):
pass pass
def test_pathlib(self, tmp_path): @pytest.mark.parametrize(
from PIL.Image import Path "PathCls",
(
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: pathlib.Path,
MyStrPathLike, # Returns `str` on `os.fspath`
MyBytesPathLike, # Returns `bytes` on `os.fspath`
str,
os.fsencode, # Converts path to `bytes`
),
)
def test_path_like(self, tmp_path, PathCls):
with Image.open(PathCls("Tests/images/multipage-mmap.tiff")) as im:
assert im.mode == "P" assert im.mode == "P"
assert im.size == (10, 10) assert im.size == (10, 10)
with Image.open(Path("Tests/images/hopper.jpg")) as im: with Image.open(PathCls("Tests/images/hopper.jpg")) as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) assert im.size == (128, 128)
for ext in (".jpg", ".jp2"): for ext in (".jpg", ".jp2"):
temp_file = str(tmp_path / ("temp." + ext)) temp_file = str((tmp_path / "temp").with_suffix(ext))
if os.path.exists(temp_file): if os.path.exists(temp_file):
os.remove(temp_file) os.remove(temp_file)
im.save(Path(temp_file)) im.save(PathCls(temp_file))
def test_fp_name(self, tmp_path): def test_fp_name(self, tmp_path):
temp_file = str(tmp_path / "temp.jpg") temp_file = str(tmp_path / "temp.jpg")

View File

@ -4,6 +4,7 @@ import re
import shutil import shutil
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -87,6 +88,7 @@ class TestImageFont:
pytest.skip("Non-ASCII path could not be created") pytest.skip("Non-ASCII path could not be created")
ImageFont.truetype(tempfile, FONT_SIZE) ImageFont.truetype(tempfile, FONT_SIZE)
ImageFont.truetype(Path(tempfile), FONT_SIZE)
def _render(self, font): def _render(self, font):
txt = "Hello World!" txt = "Hello World!"

View File

@ -27,6 +27,21 @@ def test_path_obj_is_path():
assert it_is assert it_is
def test_path_like_is_path():
# Arrange
class PathThingy:
def __fspath__(self):
return "UNIMPORTANT" # pragma: no cover
test_path = PathThingy()
# Act
it_is = _util.is_path(test_path)
# Assert
assert it_is
def test_is_not_path(tmp_path): def test_is_not_path(tmp_path):
# Arrange # Arrange
with (tmp_path / "temp.ext").open("w") as fp: with (tmp_path / "temp.ext").open("w") as fp:
@ -39,6 +54,22 @@ def test_is_not_path(tmp_path):
assert not it_is_not assert not it_is_not
def test_path_like_file_is_not_path(tmp_path):
# Arrange
import io
class PathAndFileThingy(io.TextIOWrapper):
def __fspath__(self):
return "UNIMPORTANT" # pragma: no cover
with PathAndFileThingy(open(tmp_path / "temp.ext", "wb")) as test_obj:
# Act
it_is_not = _util.is_path(test_obj)
# Assert
assert not it_is_not
def test_is_directory(): def test_is_directory():
# Arrange # Arrange
directory = "Tests" directory = "Tests"

View File

@ -2182,9 +2182,9 @@ class Image:
def save(self, fp, format=None, **params): def save(self, fp, format=None, **params):
""" """
Saves this image under the given filename. If no format is Saves this image under the given file path. If no format is
specified, the format to use is determined from the filename specified, the format to use is determined from the path's
extension, if possible. file extension, if possible.
Keyword options can be used to provide additional instructions Keyword options can be used to provide additional instructions
to the writer. If a writer doesn't recognise an option, it is to the writer. If a writer doesn't recognise an option, it is
@ -2192,12 +2192,12 @@ class Image:
:doc:`image format documentation :doc:`image format documentation
<../handbook/image-file-formats>` for each writer. <../handbook/image-file-formats>` for each writer.
You can use a file object instead of a filename. In this case, You can use a file object instead of a file path. In this case,
you must always specify the format. The file object must you must always specify the format. The file object must
implement the ``seek``, ``tell``, and ``write`` implement the ``seek``, ``tell``, and ``write``
methods, and be opened in binary mode. methods, and be opened in binary mode.
:param fp: A filename (string), pathlib.Path object or file object. :param fp: A file path (str or bytes), path-like object or file object.
:param format: Optional format override. If omitted, the :param format: Optional format override. If omitted, the
format to use is determined from the filename extension. format to use is determined from the filename extension.
If a file object was used instead of a filename, this If a file object was used instead of a filename, this
@ -2216,7 +2216,7 @@ class Image:
filename = str(fp) filename = str(fp)
open_fp = True open_fp = True
elif is_path(fp): elif is_path(fp):
filename = fp filename = os.fsdecode(fp)
open_fp = True open_fp = True
elif fp == sys.stdout: elif fp == sys.stdout:
try: try:
@ -2992,7 +2992,7 @@ def open(fp, mode="r", formats=None):
:py:meth:`~PIL.Image.Image.load` method). See :py:meth:`~PIL.Image.Image.load` method). See
:py:func:`~PIL.Image.new`. See :ref:`file-handling`. :py:func:`~PIL.Image.new`. See :ref:`file-handling`.
:param fp: A filename (string), pathlib.Path object or a file object. :param fp: A file path (str or bytes), path-like object or a file object.
The file object must implement ``file.read``, The file object must implement ``file.read``,
``file.seek``, and ``file.tell`` methods, ``file.seek``, and ``file.tell`` methods,
and be opened in binary mode. and be opened in binary mode.

View File

@ -202,6 +202,7 @@ class FreeTypeFont:
) )
if is_path(font): if is_path(font):
font = os.fspath(font)
if sys.platform == "win32": if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode() font_bytes_path = font if isinstance(font, bytes) else font.encode()
try: try:
@ -818,10 +819,10 @@ def load(filename):
def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
""" r"""
Load a TrueType or OpenType font from a file or file-like object, Load a TrueType or OpenType font from a file path or file-like object,
and create a font object. and create a font object.
This function loads a font object from the given file or file-like This function loads a font object from the given file path or file-like
object, and creates a font object for a font of the given size. object, and creates a font object for a font of the given size.
Pillow uses FreeType to open font files. If you are opening many fonts Pillow uses FreeType to open font files. If you are opening many fonts
@ -831,12 +832,12 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
This function requires the _imagingft service. This function requires the _imagingft service.
:param font: A filename or file-like object containing a TrueType font. :param font: A file path or file-like object containing a TrueType font.
If the file is not found in this filename, the loader may also If the file is not found at this path, the loader may also
search in other directories, such as the :file:`fonts/` search in other directories, such as the :file:`%WINDIR%\fonts\`
directory on Windows or :file:`/Library/Fonts/`, directory on Windows or :file:`/Library/Fonts/`,
:file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/`
macOS. on macOS.
:param size: The requested size, in pixels. :param size: The requested size, in pixels.
:param index: Which font face to load (default is first available face). :param index: Which font face to load (default is first available face).
@ -878,7 +879,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
except OSError: except OSError:
if not is_path(font): if not is_path(font):
raise raise
ttf_filename = os.path.basename(font) ttf_filename = os.fsdecode(os.path.basename(font))
dirs = [] dirs = []
if sys.platform == "win32": if sys.platform == "win32":

View File

@ -1,9 +1,12 @@
import os import os
from pathlib import Path
def is_path(f): def is_path(f):
return isinstance(f, (bytes, str, Path)) """
Checks whether the given object is a string or path-like object that
isn't also a file-like object
"""
return isinstance(f, (bytes, str, os.PathLike)) and not hasattr(f, "read")
def is_directory(f): def is_directory(f):