From f6f3dab6724d72a60ebf0244843a82418f7d7740 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 17:42:17 +1100 Subject: [PATCH 0001/1178] Import UnidentifiedImageError directly --- Tests/oss-fuzz/test_fuzzers.py | 4 ++-- Tests/test_file_eps.py | 4 ++-- Tests/test_file_psd.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a52..186a0efd3fe 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -6,7 +6,7 @@ import packaging import pytest -from PIL import Image, features +from PIL import Image, UnidentifiedImageError, features from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): @@ -42,7 +42,7 @@ def test_fuzz_images(path): except ( Image.DecompressionBombError, Image.DecompressionBombWarning, - Image.UnidentifiedImageError, + UnidentifiedImageError, ): # Known Image.* exceptions assert True diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a76..360ae11b515 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -3,7 +3,7 @@ import pytest -from PIL import EpsImagePlugin, Image, features +from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features from .helper import ( assert_image_similar, @@ -417,7 +417,7 @@ def test_emptyline(): ) def test_timeout(test_file): with open(test_file, "rb") as f: - with pytest.raises(Image.UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): with Image.open(f): pass diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b163..d98f2335648 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -3,7 +3,7 @@ import pytest -from PIL import Image, PsdImagePlugin +from PIL import Image, PsdImagePlugin, UnidentifiedImageError from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy @@ -146,11 +146,11 @@ def test_combined_larger_than_size(): [ ( "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", - Image.UnidentifiedImageError, + UnidentifiedImageError, ), ( "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", - Image.UnidentifiedImageError, + UnidentifiedImageError, ), ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), From 30015f6236ee3165e06b78ff7dced956e21e24d5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 16:16:25 +0100 Subject: [PATCH 0002/1178] simplify decompression bomb check in FreeTypeFont.render --- src/PIL/ImageFont.py | 17 +++-------------- src/_imagingft.c | 14 ++++---------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4eccb..c8d834f91d4 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -582,22 +582,13 @@ def getmask2( _string_length_check(text) if start is None: start = (0, 0) - im = None - size = None def fill(width, height): - nonlocal im, size - size = (width, height) - if Image.MAX_IMAGE_PIXELS is not None: - pixels = max(1, width) * max(1, height) - if pixels > 2 * Image.MAX_IMAGE_PIXELS: - return - - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - return im + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - offset = self.font.render( + return self.font.render( text, fill, mode, @@ -610,8 +601,6 @@ def fill(width, height): start[0], start[1], ) - Image._decompression_bomb_check(size) - return im, offset def font_variant( self, font=None, size=None, index=None, encoding=None, layout_engine=None diff --git a/src/_imagingft.c b/src/_imagingft.c index 68c66ac2c60..6e24fcf95ed 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { image = PyObject_CallFunction(fill, "ii", width, height); if (image == Py_None) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", 0, 0); + return Py_BuildValue("N(ii)", image, 0, 0); } else if (image == NULL) { PyMem_Del(glyph_info); return NULL; @@ -894,7 +894,7 @@ font_render(FontObject *self, PyObject *args) { y_offset -= stroke_width; if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); } if (stroke_width) { @@ -1130,18 +1130,12 @@ font_render(FontObject *self, PyObject *args) { if (bitmap_converted_ready) { FT_Bitmap_Done(library, &bitmap_converted); } - Py_DECREF(image); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); glyph_error: - if (im->destroy) { - im->destroy(im); - } - if (im->image) { - free(im->image); - } + Py_DECREF(image); if (stroker != NULL) { FT_Done_Glyph(glyph); } From 90991428fa4ca39076efa6329792f30b962d7d49 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:05:16 +0100 Subject: [PATCH 0003/1178] add LCMS2 flags to ImageCms --- Tests/test_imagecms.py | 10 +++++++ src/PIL/ImageCms.py | 63 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0dde82bd748..fec482f4393 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -90,6 +90,16 @@ def test_sanity(): hopper().point(t) +def test_flags(): + assert ImageCms.Flags.NONE == 0 + assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE + assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE + + assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16) + assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255) + assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) + + def test_name(): skip_missing() # get profile information for file diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 643fce830ba..eafafd58333 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -16,8 +16,10 @@ # below for the original description. from __future__ import annotations +import operator import sys -from enum import IntEnum +from enum import IntEnum, IntFlag +from functools import reduce from . import Image @@ -119,6 +121,48 @@ class Direction(IntEnum): # # flags + +class Flags(IntFlag): + # These are taken from lcms2.h (including comments) + NONE = 0 + NOCACHE = 0x0040 # Inhibit 1-pixel cache + NOOPTIMIZE = 0x0100 # Inhibit optimizations + NULLTRANSFORM = 0x0200 # Don't transform anyway + GAMUTCHECK = 0x1000 # Out of Gamut alarm + SOFTPROOFING = 0x4000 # Do softproofing + BLACKPOINTCOMPENSATION = 0x2000 + NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot + HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy + LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: + USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks + GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) + KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation + FORCE_CLUT = 0x0002 # Force CLUT optimization + CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible + CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible + NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms + COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + NODEFAULTRESOURCEDEF = 0x01000000 + + _GRIDPOINTS_1 = 1 << 16 + _GRIDPOINTS_2 = 2 << 16 + _GRIDPOINTS_4 = 4 << 16 + _GRIDPOINTS_8 = 8 << 16 + _GRIDPOINTS_16 = 16 << 16 + _GRIDPOINTS_32 = 32 << 16 + _GRIDPOINTS_64 = 64 << 16 + _GRIDPOINTS_128 = 128 << 16 + + @staticmethod + def GRIDPOINTS(n: int) -> Flags: + # Fine-tune control over number of gridpoints + return Flags.NONE | ((n & 0xFF) << 16) + + +_MAX_FLAG = reduce(operator.or_, Flags) + + FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, @@ -142,11 +186,6 @@ class Direction(IntEnum): "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints } -_MAX_FLAG = 0 -for flag in FLAGS.values(): - if isinstance(flag, int): - _MAX_FLAG = _MAX_FLAG | flag - # --------------------------------------------------------------------. # Experimental PIL-level API @@ -218,7 +257,7 @@ def __init__( intent=Intent.PERCEPTUAL, proof=None, proof_intent=Intent.ABSOLUTE_COLORIMETRIC, - flags=0, + flags=Flags.NONE, ): if proof is None: self.transform = core.buildTransform( @@ -303,7 +342,7 @@ def profileToProfile( renderingIntent=Intent.PERCEPTUAL, outputMode=None, inPlace=False, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Applies an ICC transformation to a given image, mapping from @@ -420,7 +459,7 @@ def buildTransform( inMode, outMode, renderingIntent=Intent.PERCEPTUAL, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -482,7 +521,7 @@ def buildTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: @@ -505,7 +544,7 @@ def buildProofTransform( outMode, renderingIntent=Intent.PERCEPTUAL, proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, - flags=FLAGS["SOFTPROOFING"], + flags=Flags.SOFTPROOFING, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -586,7 +625,7 @@ def buildProofTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: From 26b2aa5165c62ee38664e291206f8ff28a30bb39 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 02:07:59 +0100 Subject: [PATCH 0004/1178] document ImageCms.{ImageCmsProfile,Intent,Direction}; fix ImageCms.core.CmsProfile references (cherry picked from commit f2b1bbcf65b327c14646d4113302e3df59555110) --- docs/deprecations.rst | 4 ++-- docs/reference/ImageCms.rst | 21 ++++++++++++++++++++- docs/releasenotes/8.0.0.rst | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a42dc555fea..0f9c75756ee 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -338,8 +338,8 @@ ImageCms.CmsProfile attributes .. deprecated:: 3.2.0 .. versionremoved:: 8.0.0 -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, -they issued a :py:exc:`DeprecationWarning`: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed. +From 6.0.0, they issued a :py:exc:`DeprecationWarning`: ======================== =================================================== Removed Use instead diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b9b5e7b29e..9b6be40b106 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -8,9 +8,26 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management support using the LittleCMS2 color management engine, based on Kevin Cazabon's PyCMS library. +.. autoclass:: ImageCmsProfile + :members: + :special-members: __init__ .. autoclass:: ImageCmsTransform + :members: + :undoc-members: .. autoexception:: PyCMSError +Constants +--------- + +.. autoclass:: Intent + :members: + :member-order: bysource + :undoc-members: +.. autoclass:: Direction + :members: + :member-order: bysource + :undoc-members: + Functions --------- @@ -37,13 +54,15 @@ CmsProfile ---------- The ICC color profiles are wrapped in an instance of the class -:py:class:`CmsProfile`. The specification ICC.1:2010 contains more +:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more information about the meaning of the values in ICC profiles. For convenience, all XYZ-values are also given as xyY-values (so they can be easily displayed in a chromaticity diagram, for example). +.. py:currentmodule:: PIL.ImageCms.core .. py:class:: CmsProfile + :canonical: PIL._imagingcms.CmsProfile .. py:attribute:: creation_date :type: Optional[datetime.datetime] diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 2bf299dd3d8..1fc245c9a3c 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring ImageCms.CmsProfile attributes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed: ======================== =================================================== Removed Use instead From 0b2e2b224fe430c74995664c0d8eec9df789727e Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 21:38:29 +0100 Subject: [PATCH 0005/1178] document ImageCms.Flags --- docs/reference/ImageCms.rst | 4 +++ src/PIL/ImageCms.py | 57 +++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b6be40b106..4ef5ac77456 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -27,6 +27,10 @@ Constants :members: :member-order: bysource :undoc-members: +.. autoclass:: Flags + :members: + :member-order: bysource + :undoc-members: Functions --------- diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index eafafd58333..9a7afe81f2b 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -123,26 +123,43 @@ class Direction(IntEnum): class Flags(IntFlag): - # These are taken from lcms2.h (including comments) + """Flags and documentation are taken from ``lcms2.h``.""" + NONE = 0 - NOCACHE = 0x0040 # Inhibit 1-pixel cache - NOOPTIMIZE = 0x0100 # Inhibit optimizations - NULLTRANSFORM = 0x0200 # Don't transform anyway - GAMUTCHECK = 0x1000 # Out of Gamut alarm - SOFTPROOFING = 0x4000 # Do softproofing + NOCACHE = 0x0040 + """Inhibit 1-pixel cache""" + NOOPTIMIZE = 0x0100 + """Inhibit optimizations""" + NULLTRANSFORM = 0x0200 + """Don't transform anyway""" + GAMUTCHECK = 0x1000 + """Out of Gamut alarm""" + SOFTPROOFING = 0x4000 + """Do softproofing""" BLACKPOINTCOMPENSATION = 0x2000 - NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot - HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy - LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + NOWHITEONWHITEFIXUP = 0x0004 + """Don't fix scum dot""" + HIGHRESPRECALC = 0x0400 + """Use more memory to give better accuracy""" + LOWRESPRECALC = 0x0800 + """Use less memory to minimize resources""" # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: - USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks - GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) - KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation - FORCE_CLUT = 0x0002 # Force CLUT optimization - CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible - CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible - NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms - COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + USE_8BITS_DEVICELINK = 0x0008 + """Create 8 bits devicelinks""" + GUESSDEVICECLASS = 0x0020 + """Guess device class (for transform2devicelink)""" + KEEP_SEQUENCE = 0x0080 + """Keep profile sequence for devicelink creation""" + FORCE_CLUT = 0x0002 + """Force CLUT optimization""" + CLUT_POST_LINEARIZATION = 0x0001 + """create postlinearization tables if possible""" + CLUT_PRE_LINEARIZATION = 0x0010 + """create prelinearization tables if possible""" + NONEGATIVES = 0x8000 + """Prevent negative numbers in floating point transforms""" + COPY_ALPHA = 0x04000000 + """Alpha channels are copied on cmsDoTransform()""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 @@ -156,7 +173,11 @@ class Flags(IntFlag): @staticmethod def GRIDPOINTS(n: int) -> Flags: - # Fine-tune control over number of gridpoints + """ + Fine-tune control over number of gridpoints + + :param n: :py:class:`int` in range ``0 <= n <= 255`` + """ return Flags.NONE | ((n & 0xFF) << 16) From fd148246499bfd5c7d55f871e3f1f6e608a9bc3f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 19:15:13 +1100 Subject: [PATCH 0006/1178] bbox on macOS is not 2x on retina screens --- docs/reference/ImageGrab.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 0b94032d5f8..5c365132f5e 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,7 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. + On macOS, this is not increased to 2x for retina screens, so the full + width of a retina screen would be 1440, not 2880. + On Windows OS, the top-left point may be negative if + ``all_screens=True`` is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From dacd92853094414ab6a4326e7f5805666b92996c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:37:59 +1100 Subject: [PATCH 0007/1178] 10.3.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 1018b96b525..0568943b52b 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.2.0" +__version__ = "10.3.0.dev0" From ec6a57f69d45c0444d7e38df57af6761524463fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 21:27:12 +1100 Subject: [PATCH 0008/1178] Updated description Co-authored-by: Hugo van Kemenade --- docs/reference/ImageGrab.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 5c365132f5e..e0e8d5a2f1b 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,10 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - On macOS, this is not increased to 2x for retina screens, so the full - width of a retina screen would be 1440, not 2880. - On Windows OS, the top-left point may be negative if - ``all_screens=True`` is used. + On macOS, this is not increased to 2x for Retina screens, so the full + width of a Retina screen would be 1440, not 2880. + On Windows, the top-left point may be negative if ``all_screens=True`` + is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From 75015e9859183e426012aa59309473c479b59a59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 22:43:56 +1100 Subject: [PATCH 0009/1178] Skip PyPy3.8 Windows wheel --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 060fc497ea7..5adff7ec1c4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -143,6 +143,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" + CIBW_SKIP: pp38-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From 5ddcf4d11493524627dbf4b65ad0ed76b7625168 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:33:48 +1100 Subject: [PATCH 0010/1178] Package name is now lowercase in wheel filenames --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index b3fd72a520e..97f4f8dcd18 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -87,7 +87,7 @@ Released as needed privately to individual vendors for critical security-related and copy into `dist`. Check and upload them e.g.: ```bash python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.0* + python3 -m twine upload dist/pillow-5.2.0* ``` ## Publicize Release From 81ea98e4941af0940b4725a7cc187c689a3726e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Tue, 2 Jan 2024 14:10:07 +0100 Subject: [PATCH 0011/1178] document ImageCmsTransform's base class Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/reference/ImageCms.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 4ef5ac77456..22ed516ce58 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -14,6 +14,7 @@ Cazabon's PyCMS library. .. autoclass:: ImageCmsTransform :members: :undoc-members: + :show-inheritance: .. autoexception:: PyCMSError Constants From fc7088a561554aec8b6a4cf6517fdcc11554cb6b Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 14:52:12 +0100 Subject: [PATCH 0012/1178] improve ImageTransform documentation --- docs/PIL.rst | 8 ------- docs/reference/ImageTransform.rst | 35 +++++++++++++++++++++++++++++++ docs/reference/index.rst | 1 + src/PIL/Image.py | 4 ++++ src/PIL/ImageTransform.py | 13 +++++++----- 5 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 docs/reference/ImageTransform.rst diff --git a/docs/PIL.rst b/docs/PIL.rst index b6944e234a5..bdbf1373d88 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -77,14 +77,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageTransform` Module ---------------------------------- - -.. automodule:: PIL.ImageTransform - :members: - :undoc-members: - :show-inheritance: - :mod:`~PIL.PaletteFile` Module ------------------------------ diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst new file mode 100644 index 00000000000..1278801821d --- /dev/null +++ b/docs/reference/ImageTransform.rst @@ -0,0 +1,35 @@ + +.. py:module:: PIL.ImageTransform +.. py:currentmodule:: PIL.ImageTransform + +:py:mod:`~PIL.ImageTransform` Module +==================================== + +The :py:mod:`~PIL.ImageTransform` module contains implementations of +:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin +:py:class:`.Image.Transform` methods. + +.. autoclass:: Transform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: AffineTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: ExtentTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: QuadTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MeshTransform + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 5d6affa94ad..82c75e373ad 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -25,6 +25,7 @@ Reference ImageShow ImageStat ImageTk + ImageTransform ImageWin ExifTags TiffTags diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1bba9aad2c1..c56da545803 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2666,6 +2666,10 @@ class Example(Image.ImageTransformHandler): def transform(self, size, data, resample, fill=1): # Return result + Implementations of :py:class:`~PIL.Image.ImageTransformHandler` + for some of the :py:class:`Transform` methods are provided + in :py:mod:`~PIL.ImageTransform`. + It may also be an object with a ``method.getdata`` method that returns a tuple supplying new ``method`` and ``data`` values:: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 84c81f1844f..4f79500e64e 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,12 +20,14 @@ class Transform(Image.ImageTransformHandler): + """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" + method: Image.Transform def __init__(self, data: Sequence[int]) -> None: self.data = data - def getdata(self) -> tuple[int, Sequence[int]]: + def getdata(self) -> tuple[Image.Transform, Sequence[int]]: return self.method, self.data def transform( @@ -34,6 +36,7 @@ def transform( image: Image.Image, **options: dict[str, str | int | tuple[int, ...] | list[int]], ) -> Image.Image: + """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) @@ -51,7 +54,7 @@ class AffineTransform(Transform): This function can be used to scale, translate, rotate, and shear the original image. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows from an affine transform matrix. @@ -73,7 +76,7 @@ class ExtentTransform(Transform): rectangle in the current image. It is slightly slower than crop, but about as fast as a corresponding resize operation. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the input image's coordinate system. See :ref:`coordinate-system`. @@ -89,7 +92,7 @@ class QuadTransform(Transform): Maps a quadrilateral (a region defined by four corners) from the image to a rectangle of the given size. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the upper left, lower left, lower right, and upper right corner of the @@ -104,7 +107,7 @@ class MeshTransform(Transform): Define a mesh image transform. A mesh transform consists of one or more individual quad transforms. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param data: A list of (bbox, quad) tuples. """ From 8070fe10f1ccc346539f87c88fb65ea55d001b9a Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 23 Dec 2023 14:41:50 +0100 Subject: [PATCH 0013/1178] pass build config before setuptools command; add build_editable to custom build backend --- .github/workflows/wheels.yml | 2 ++ _custom_build/backend.py | 57 +++++++++++++----------------------- pyproject.toml | 2 +- setup.py | 34 ++++++++++++++++++--- 4 files changed, 54 insertions(+), 41 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5adff7ec1c4..0c8a941de30 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -5,6 +5,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" - "winbuild/fribidi.cmake" @@ -14,6 +15,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" - "winbuild/fribidi.cmake" diff --git a/_custom_build/backend.py b/_custom_build/backend.py index d1537b80987..0b183a5870b 100644 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -11,41 +11,16 @@ class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: - - def config_has(key, value): - settings = self.config_settings.get(key) - if settings: - if not isinstance(settings, list): - settings = [settings] - return value in settings - - flags = [] - for dependency in ( - "zlib", - "jpeg", - "tiff", - "freetype", - "raqm", - "lcms", - "webp", - "webpmux", - "jpeg2000", - "imagequant", - "xcb", - ): - if config_has(dependency, "enable"): - flags.append("--enable-" + dependency) - elif config_has(dependency, "disable"): - flags.append("--disable-" + dependency) - for dependency in ("raqm", "fribidi"): - if config_has(dependency, "vendor"): - flags.append("--vendor-" + dependency) - if self.config_settings.get("platform-guessing") == "disable": - flags.append("--disable-platform-guessing") - if self.config_settings.get("debug") == "true": - flags.append("--debug") - if flags: - sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:] + params = [] + for k, v in self.config_settings.items(): + if isinstance(v, list): + msg = "Conflicting options: " + ", ".join( + f"'--config-setting {k}={v_}'" for v_ in v + ) + raise ValueError(msg) + params.append(f"--pillow-configuration={k}={v}") + + sys.argv = sys.argv[:1] + params + sys.argv[1:] return super().run_setup(setup_script) def build_wheel( @@ -54,5 +29,15 @@ def build_wheel( self.config_settings = config_settings return super().build_wheel(wheel_directory, config_settings, metadata_directory) + def build_editable( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_editable( + wheel_directory, config_settings, metadata_directory + ) + -build_wheel = _CustomBuildMetaBackend().build_wheel +_backend = _CustomBuildMetaBackend() +build_wheel = _backend.build_wheel +build_editable = _backend.build_editable diff --git a/pyproject.toml b/pyproject.toml index da2537b2137..d63e401af1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ version = {attr = "PIL.__version__"} [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 -config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +config-settings = "raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index 1bf0bcff558..c74165fb73a 100755 --- a/setup.py +++ b/setup.py @@ -28,6 +28,9 @@ def get_version(): return locals()["__version__"] +configuration = {} + + PILLOW_VERSION = get_version() FREETYPE_ROOT = None HARFBUZZ_ROOT = None @@ -334,15 +337,24 @@ def __iter__(self): + [("add-imaging-libs=", None, "Add libs to _imaging build")] ) + @staticmethod + def check_configuration(option, value): + return True if configuration.get(option) == value else None + def initialize_options(self): - self.disable_platform_guessing = None + self.disable_platform_guessing = self.check_configuration( + "platform-guessing", "disable" + ) self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: - setattr(self, f"disable_{x}", None) - setattr(self, f"enable_{x}", None) + setattr(self, f"disable_{x}", self.check_configuration(x, "disable")) + setattr(self, f"enable_{x}", self.check_configuration(x, "enable")) for x in ("raqm", "fribidi"): - setattr(self, f"vendor_{x}", None) + setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) + if self.check_configuration("debug", "true"): + self.debug = True + self.parallel = configuration.get("parallel") def finalize_options(self): build_ext.finalize_options(self) @@ -390,6 +402,9 @@ def finalize_options(self): raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) + if x == "raqm": + _dbg("--vendor-raqm implies --enable-raqm") + self.feature.required.add(x) def _update_extension(self, name, libraries, define_macros=None, sources=None): for extension in self.extensions: @@ -985,6 +1000,17 @@ def debug_build(): Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] + +# parse configuration from _custom_build/backend.py +while len(sys.argv[1]) >= 2 and sys.argv[1].startswith("--pillow-configuration="): + _, key, value = sys.argv[1].split("=", 2) + old = configuration.get(key) + if old is not None: + msg = f"Conflicting options: '-C {key}={old}' and '-C {key}={value}'" + raise ValueError(msg) + configuration[key] = value + del sys.argv[1] + try: setup( cmdclass={"build_ext": pil_build_ext}, From b4e690049d81dc2569d6f02c5b0c1b9eb29a07b2 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 02:22:05 +0100 Subject: [PATCH 0014/1178] document config setting "-C parallel=n" for number of CPUs to use for compilation --- docs/installation.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index fbcfbb90724..03011619ff7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -390,9 +390,10 @@ After navigating to the Pillow directory, run:: Build Options """"""""""""" -* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` - sets the number of CPUs to use, or can disable parallel building by +* Config setting: ``-C parallel=n``. Can also be given + with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``-C parallel=n`` + sets the number of CPUs to use to ``n``, or can disable parallel building by using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. @@ -417,14 +418,13 @@ Build Options used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``-C platform-guessing=disable``. Skips all of the +* Config setting: ``-C platform-guessing=disable``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). -* Build flag: ``-C debug=true``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to - stdout. +* Config setting: ``-C debug=true``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to stdout. Sample usage:: From f27b838a451d0b20befea03da7b2d58dd1da7821 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 15:47:47 +0100 Subject: [PATCH 0015/1178] support multiple --config-settings --- _custom_build/backend.py | 12 +++++------- pyproject.toml | 2 +- setup.py | 13 +++---------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 0b183a5870b..2c670ff0ac3 100644 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -12,13 +12,11 @@ class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: params = [] - for k, v in self.config_settings.items(): - if isinstance(v, list): - msg = "Conflicting options: " + ", ".join( - f"'--config-setting {k}={v_}'" for v_ in v - ) - raise ValueError(msg) - params.append(f"--pillow-configuration={k}={v}") + for key, values in self.config_settings.items(): + if not isinstance(values, list): + values = [values] + for value in values: + params.append(f"--pillow-configuration={key}={value}") sys.argv = sys.argv[:1] + params + sys.argv[1:] return super().run_setup(setup_script) diff --git a/pyproject.toml b/pyproject.toml index d63e401af1e..da2537b2137 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ version = {attr = "PIL.__version__"} [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 -config-settings = "raqm=vendor fribidi=vendor imagequant=disable" +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index c74165fb73a..686e8889fd9 100755 --- a/setup.py +++ b/setup.py @@ -339,7 +339,7 @@ def __iter__(self): @staticmethod def check_configuration(option, value): - return True if configuration.get(option) == value else None + return True if value in configuration.get(option, []) else None def initialize_options(self): self.disable_platform_guessing = self.check_configuration( @@ -354,7 +354,7 @@ def initialize_options(self): setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) if self.check_configuration("debug", "true"): self.debug = True - self.parallel = configuration.get("parallel") + self.parallel = configuration.get("parallel", [None])[-1] def finalize_options(self): build_ext.finalize_options(self) @@ -402,9 +402,6 @@ def finalize_options(self): raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) - if x == "raqm": - _dbg("--vendor-raqm implies --enable-raqm") - self.feature.required.add(x) def _update_extension(self, name, libraries, define_macros=None, sources=None): for extension in self.extensions: @@ -1004,11 +1001,7 @@ def debug_build(): # parse configuration from _custom_build/backend.py while len(sys.argv[1]) >= 2 and sys.argv[1].startswith("--pillow-configuration="): _, key, value = sys.argv[1].split("=", 2) - old = configuration.get(key) - if old is not None: - msg = f"Conflicting options: '-C {key}={old}' and '-C {key}={value}'" - raise ValueError(msg) - configuration[key] = value + configuration.setdefault(key, []).append(value) del sys.argv[1] try: From 01e5f06da055490f70b1294f39a2718297ee8067 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 16:12:37 +0100 Subject: [PATCH 0016/1178] document editable mode installation in winbuild/build.rst --- winbuild/build.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6cc..26d0da0a35a 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -87,11 +87,18 @@ are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip: winbuild\build\build_env.cmd python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . +You can also install Pillow in `editable mode`_:: + + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . + To build a wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . +.. _editable mode: https://setuptools.pypa.io/en/latest/userguide/development_mode.html + Testing Pillow -------------- From b4a82712887e14b7ff1fc6302fdd9b1a48ac2280 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 17:26:11 +0100 Subject: [PATCH 0017/1178] update Windows 11 tested versions --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index fbcfbb90724..4c58b4ebb27 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -581,9 +581,9 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm64 | +| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.1.0 |x86-64 | +| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ | Windows 10 | 3.7 | 7.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ From d134110ace31194f5fab4a9c3a62563a5d2a91bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 09:01:35 +1100 Subject: [PATCH 0018/1178] If bbox is omitted, screenshot is taken at 2x on Retina screens --- docs/reference/ImageGrab.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index e0e8d5a2f1b..db2987eb081 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,9 +11,9 @@ or the clipboard to a PIL image memory. .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) - Take a snapshot of the screen. The pixels inside the bounding box are - returned as an "RGBA" on macOS, or an "RGB" image otherwise. - If the bounding box is omitted, the entire screen is copied. + Take a snapshot of the screen. The pixels inside the bounding box are returned as + an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, + the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is From 424737ef4906b5005f53b1642e5b538cadf391ed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 11:18:16 +1100 Subject: [PATCH 0019/1178] Updated macOS tested Pillow versions --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4c58b4ebb27..922720b9d05 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -510,7 +510,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm | +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | +----------------------------+------------------+ | From 05e73702f2651897efbdc0ad39551f1a99b371d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 10:51:10 +1100 Subject: [PATCH 0020/1178] Updated matrix variable name on Linux and macOS to match Windows --- .github/workflows/wheels.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5adff7ec1c4..85d9eba1c3e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -39,18 +39,18 @@ jobs: include: - name: "macOS x86_64" os: macos-latest - archs: x86_64 + cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" os: macos-latest - archs: arm64 + cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 - name: "manylinux_2_28 x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" steps: @@ -67,7 +67,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt python3 -m cibuildwheel --output-dir wheelhouse env: - CIBW_ARCHS: ${{ matrix.archs }} + CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} @@ -77,7 +77,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: From 85c552934af5ed378b034560ace23107bb4ec497 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 3 Jan 2024 17:45:41 +0200 Subject: [PATCH 0021/1178] Goodbye Travis CI --- .github/workflows/test-cygwin.yml | 2 -- .github/workflows/test-docker.yml | 2 -- .github/workflows/test-mingw.yml | 2 -- .github/workflows/test-windows.yml | 2 -- .github/workflows/test.yml | 2 -- .travis.yml | 52 ------------------------------ README.md | 3 -- RELEASING.md | 8 +---- docs/about.rst | 3 +- docs/index.rst | 4 --- 10 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 32ac6f65e76..7244315ac15 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eb27b4bf75b..3bb6856f6e8 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 115c2e9bebc..cdd51e2bb3f 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa1e..a0ef1c3f1ec 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -6,7 +6,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -14,7 +13,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7e112f4364..05f78704bcd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8f8250809d7..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -if: tag IS present OR type = api - -env: - global: - - CIBW_ARCHS=aarch64 - - CIBW_SKIP=pp38-* - -language: python -# Default Python version is usually 3.6 -python: "3.12" -dist: jammy -services: docker - -jobs: - include: - - name: "manylinux2014 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014 - - name: "manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28 - - name: "musllinux aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*musllinux*" - -install: - - python3 -m pip install -r .ci/requirements-cibw.txt - -script: - - python3 -m cibuildwheel --output-dir wheelhouse - - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/" - -# Upload wheels to GitHub Releases -deploy: - provider: releases - api_key: $GITHUB_RELEASE_TOKEN - file_glob: true - file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl" - on: - repo: python-pillow/Pillow - tags: true - skip_cleanup: true diff --git a/README.md b/README.md index e11bd2faa1d..6982676f518 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,6 @@ As of 2019, Pillow development is GitHub Actions build status (Wheels) - Travis CI wheels build status (aarch64) Code coverage diff --git a/RELEASING.md b/RELEASING.md index 97f4f8dcd18..62f3627de19 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Develop and prepare release in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. -* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them. +* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. @@ -83,12 +83,6 @@ Released as needed privately to individual vendors for critical security-related * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. -* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) - and copy into `dist`. Check and upload them e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/pillow-5.2.0* - ``` ## Publicize Release diff --git a/docs/about.rst b/docs/about.rst index 872ac0ea690..da351ce2c56 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,13 +6,12 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_ +- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow -.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/Pillow/ diff --git a/docs/index.rst b/docs/index.rst index 4f577fe9c22..053d55c3c39 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Tue, 2 Jan 2024 18:11:52 +0200 Subject: [PATCH 0022/1178] Build QEMU-emulated Linux aarch64 wheels on GitHub Actions --- .github/workflows/wheels.yml | 77 +++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c3e..6ebf9a4c029 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -30,7 +30,80 @@ env: FORCE_COLOR: 1 jobs: - build: + build-1-QEMU-emulated-wheels: + name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - pp39 + - pp310 + - cp38 + - cp39 + - cp310 + - cp311 + - cp312 + spec: + - manylinux2014 + - manylinux_2_28 + - musllinux + exclude: + - { python-version: pp39, spec: musllinux } + - { python-version: pp310, spec: musllinux } + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install cibuildwheel + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels (manylinux) + if: matrix.spec != 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + # Extra options for manylinux. + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} + + - name: Build wheels (musllinux) + if: matrix.spec == 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" + + - uses: actions/upload-artifact@v4 + with: + name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} + path: ./wheelhouse/*.whl + + build-2-native-wheels: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: @@ -187,7 +260,7 @@ jobs: pypi-publish: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build, windows, sdist] + needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] runs-on: ubuntu-latest name: Upload release to PyPI environment: From 55944860a5dd4a38d786df035346b9d5ddf3aa1e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:50:06 +0200 Subject: [PATCH 0023/1178] Remove unused docker/setup-buildx-action --- .github/workflows/wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6ebf9a4c029..4de599b810b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -65,10 +65,6 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt From 32ae1bd08a6e3d8b9d266a2e7c8b265ec3d5ad18 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:52:22 +0200 Subject: [PATCH 0024/1178] Use aarch64 instead of QEMU in job name --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4de599b810b..d7a93c70ce6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -31,7 +31,7 @@ env: jobs: build-1-QEMU-emulated-wheels: - name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} runs-on: ubuntu-latest strategy: fail-fast: false From fd37d86accff55d366950b3f9d286c7174ebe9b4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:55:04 +0200 Subject: [PATCH 0025/1178] Skip non-wheel CI runs for tags --- .github/workflows/test-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa1e..94c2d4d70b8 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -2,6 +2,8 @@ name: Test Windows on: push: + branches: + - "**" paths-ignore: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" From bc3cf97649d76bc231ec99d41dfd6eb2be72b175 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:00:11 +1100 Subject: [PATCH 0026/1178] Use general arch setting instead of platform-specific setting --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d7a93c70ce6..8a9f81dfd09 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -76,7 +76,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" # Extra options for manylinux. @@ -90,7 +90,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" From e84b0a401509325c7e5e553a9f1a4591256b4b45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:38:41 +1100 Subject: [PATCH 0027/1178] Combine build steps --- .github/workflows/wheels.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8a9f81dfd09..e2bc78b983c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -69,8 +69,7 @@ jobs: run: | python3 -m pip install -r .ci/requirements-cibw.txt - - name: Build wheels (manylinux) - if: matrix.spec != 'musllinux' + - name: Build wheels run: | python3 -m cibuildwheel --output-dir wheelhouse env: @@ -78,22 +77,11 @@ jobs: # parallelise for speed). CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} - - name: Build wheels (musllinux) - if: matrix.spec == 'musllinux' - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - # Build only the currently selected Linux architecture (so we can - # parallelise for speed). - CIBW_ARCHS: "aarch64" - # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" - - uses: actions/upload-artifact@v4 with: name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} From 60e82e5a3f21b25e1b54ebc1104e972290201a85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 17:24:09 +1100 Subject: [PATCH 0028/1178] Separate cibuildwheel install --- .github/workflows/wheels.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e2bc78b983c..50e47f19853 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -119,9 +119,11 @@ jobs: with: python-version: "3.x" - - name: Build wheels + - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} @@ -163,6 +165,10 @@ jobs: with: python-version: "3.x" + - name: Install cibuildwheel + run: | + & python.exe -m pip install -r .ci/requirements-cibw.txt + - name: Prepare for build run: | choco install nasm --no-progress @@ -171,8 +177,6 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} shell: pwsh From 46db79abe1b8ada6f980e01cee633f9c8f6fbbdf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 00:30:46 -0700 Subject: [PATCH 0029/1178] Fix syntax --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 50e47f19853..77c1489dfcd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -124,6 +124,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt - name: Build wheels + run: | python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} From f184775cd3a2410ad326572d2b1d9a75ac481790 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:32:54 +1100 Subject: [PATCH 0030/1178] Removed leading ampersand Co-authored-by: Hugo van Kemenade --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 77c1489dfcd..be8f652447c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -168,7 +168,7 @@ jobs: - name: Install cibuildwheel run: | - & python.exe -m pip install -r .ci/requirements-cibw.txt + python.exe -m pip install -r .ci/requirements-cibw.txt - name: Prepare for build run: | From 2dd00de1f36d0e5dfd15f28048c2e2b7550bb232 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 20:26:14 +0100 Subject: [PATCH 0031/1178] rename x64 to AMD64 in winbuild/build_prepare.py --- .appveyor.yml | 2 +- .github/workflows/wheels.yml | 17 +++++++---------- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 4 ++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 0f5dea9c515..4c5a7f9ee47 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,7 +14,7 @@ environment: ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 - ARCHITECTURE: x64 + ARCHITECTURE: AMD64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c3e..36e98aa554a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,18 +81,15 @@ jobs: path: ./wheelhouse/*.whl windows: - name: Windows ${{ matrix.arch }} + name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest strategy: fail-fast: false matrix: include: - - arch: x86 - cibw_arch: x86 - - arch: x64 - cibw_arch: AMD64 - - arch: ARM64 - cibw_arch: ARM64 + - cibw_arch: x86 + - cibw_arch: AMD64 + - cibw_arch: ARM64 steps: - uses: actions/checkout@v4 @@ -116,7 +113,7 @@ jobs: & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh - name: Build wheels @@ -157,13 +154,13 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: dist-windows-${{ matrix.arch }} + name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll uses: actions/upload-artifact@v4 with: - name: fribidi-windows-${{ matrix.arch }} + name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* sdist: diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6cc..cd3b559e7f0 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x86/x64: `Netwide Assembler (NASM) `_ +* x86/AMD64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] [--nmake] + [--architecture {x86,AMD64,ARM64}] [--nmake] [--no-imagequant] [--no-fribidi] Download and generate build scripts for Pillow dependencies. @@ -55,7 +55,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x86,x64,ARM64} + --architecture {x86,AMD64,ARM64} build architecture (default: same as host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8e3757ca89e..440e64d9851 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,7 +105,7 @@ def cmd_msbuild( ARCHITECTURES = { "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, - "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, + "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -651,7 +651,7 @@ def build_dep_all() -> None: ( "ARM64" if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64") + else ("x86" if struct.calcsize("P") == 4 else "AMD64") ), ), help="build architecture (default: same as host Python)", From 5e2ebaface37ff71e1b8e4e9b5708ff3eec3a909 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:00:06 +0100 Subject: [PATCH 0032/1178] winbuild: build libwebp using cmake --- winbuild/build_prepare.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 440e64d9851..1615abbdb82 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -175,22 +175,15 @@ def cmd_msbuild( "dir": "libwebp-1.3.2", "license": "COPYING", "build": [ - cmd_rmdir(r"output\release-static"), # clean - cmd_nmake( - "Makefile.vc", - "all", - [ - "CFG=release-static", - "RTLIBCFG=dynamic", - "OBJDIR=output", - "ARCH={architecture}", - "LIBWEBP_BASENAME=webp", - ], + *cmds_cmake( + "webp webpdemux webpmux", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWEBP_LINK_STATIC:BOOL=OFF", ), cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"output\release-static\{architecture}\lib\*.lib"], + "libs": [r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", @@ -204,7 +197,7 @@ def cmd_msbuild( }, r"libtiff\tif_webp.c": { # link against webp.lib - "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 + "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { "add_executable(test_write_read_tags ../placeholder.h)": "", @@ -217,6 +210,7 @@ def cmd_msbuild( *cmds_cmake( "tiff", "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', ) ], From 4094edd12f32a432309d279d7c7b90d068f3bdb1 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:26:47 +0100 Subject: [PATCH 0033/1178] winbuild: fix libwebp linking libsharpyuv --- winbuild/build_prepare.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1615abbdb82..84284ae10f4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -174,6 +174,11 @@ def cmd_msbuild( "filename": "libwebp-1.3.2.tar.gz", "dir": "libwebp-1.3.2", "license": "COPYING", + "patch": { + r"src\enc\picture_csp_enc.c": { + '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 + } + }, "build": [ *cmds_cmake( "webp webpdemux webpmux", @@ -183,7 +188,7 @@ def cmd_msbuild( cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"libwebp*.lib"], + "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", From eff9f06f0dac6521d89938aa5a93ab03a77fd50d Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 22:10:11 +0100 Subject: [PATCH 0034/1178] fix comments --- winbuild/build_prepare.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 84284ae10f4..df33ea493e6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,6 +176,7 @@ def cmd_msbuild( "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { + # link against libsharpyuv.lib '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 } }, @@ -201,7 +202,7 @@ def cmd_msbuild( "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 }, r"libtiff\tif_webp.c": { - # link against webp.lib + # link against libwebp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { From d329207e62125ee3dcda0b2a82112757e9ef6fbd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 07:03:40 +1100 Subject: [PATCH 0035/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 85036f6425b..ac961a680a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.3.0 (unreleased) +------------------- + +- Rename x64 to AMD64 in winbuild #7693 + [nulano] + 10.2.0 (2024-01-02) ------------------- From 2d6ad5868dac031a8b4eeda41116b3da3fd1be58 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 12:07:55 +1100 Subject: [PATCH 0036/1178] Use "non-zero" consistently --- Tests/test_file_eps.py | 4 ++-- Tests/test_image_resample.py | 2 +- src/libImaging/GifEncode.c | 2 +- src/libImaging/Jpeg.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a76..8b48e83ad2c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -270,7 +270,7 @@ def test_render_scale1(): image1_scale1_compare.load() assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() with Image.open(FILE2_COMPARE) as image2_scale1_compare: @@ -292,7 +292,7 @@ def test_render_scale2(): image1_scale2_compare.load() assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale2: image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index b4bf6c8df3a..5a578dba5c4 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -403,7 +403,7 @@ def test_reduce(self): if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_nonzero_coefficients(self): + def test_non_zero_coefficients(self): # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index f232454052a..e37301df765 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -105,7 +105,7 @@ static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr, st->head = st->codes[st->probe] >> 20; goto encode_loop; } else { - /* Reprobe decrement must be nonzero and relatively prime to table + /* Reprobe decrement must be non-zero and relatively prime to table * size. So, any odd positive number for power-of-2 size. */ if ((st->probe -= ((st->tail << 2) | 1)) < 0) { st->probe += TABLE_SIZE; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 98eaac28dd6..7cdba902281 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,7 +74,7 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; - /* Disable automatic conversion of RGB images to YCbCr if nonzero */ + /* Disable automatic conversion of RGB images to YCbCr if non-zero */ int keep_rgb; /* Stream type (0=full, 1=tables only, 2=image only) */ From 0d841aab9a51a0b9433bc5932dcd913c028301cf Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 6 Jan 2024 13:37:43 +0100 Subject: [PATCH 0037/1178] add support for grayscale pfm image format --- Tests/images/hopper.pfm | Bin 0 -> 65552 bytes Tests/images/hopper_be.pfm | Bin 0 -> 65551 bytes Tests/test_file_ppm.py | 45 ++++++++++++++++++++++++++- docs/handbook/image-file-formats.rst | 18 +++++++++++ src/PIL/PpmImagePlugin.py | 26 +++++++++++++--- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 Tests/images/hopper.pfm create mode 100644 Tests/images/hopper_be.pfm diff --git a/Tests/images/hopper.pfm b/Tests/images/hopper.pfm new file mode 100644 index 0000000000000000000000000000000000000000..b576615640136f3229ab5a3d29ab7956fd194dd0 GIT binary patch literal 65552 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 L`NZ}X?0@thg2ah6 literal 0 HcmV?d00001 diff --git a/Tests/images/hopper_be.pfm b/Tests/images/hopper_be.pfm new file mode 100644 index 0000000000000000000000000000000000000000..93c75e26fda2cd69362a58f61373b0d88075e51c GIT binary patch literal 65551 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 Date: Sat, 6 Jan 2024 14:33:11 +0100 Subject: [PATCH 0038/1178] Add code formatting to ImageCms.Flags docstrings Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageCms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 9a7afe81f2b..62b010f4512 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -147,7 +147,7 @@ class Flags(IntFlag): USE_8BITS_DEVICELINK = 0x0008 """Create 8 bits devicelinks""" GUESSDEVICECLASS = 0x0020 - """Guess device class (for transform2devicelink)""" + """Guess device class (for ``transform2devicelink``)""" KEEP_SEQUENCE = 0x0080 """Keep profile sequence for devicelink creation""" FORCE_CLUT = 0x0002 @@ -159,7 +159,7 @@ class Flags(IntFlag): NONEGATIVES = 0x8000 """Prevent negative numbers in floating point transforms""" COPY_ALPHA = 0x04000000 - """Alpha channels are copied on cmsDoTransform()""" + """Alpha channels are copied on ``cmsDoTransform()``""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 From a786a0551b75ab3e85da1bfad54226faa2336022 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 16:17:57 +1100 Subject: [PATCH 0039/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ac961a680a3..ebf731c5659 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add LCMS2 flags to ImageCms #7676 + [nulano, radarhere, hugovk] + - Rename x64 to AMD64 in winbuild #7693 [nulano] From bb5527484536f6c8e90ffffd95906c352fecca18 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 18:49:01 +1100 Subject: [PATCH 0040/1178] Removed PPM loop to read header tokens --- src/PIL/PpmImagePlugin.py | 53 ++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 25dbfa5b0bc..82314214a7f 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -105,40 +105,31 @@ def _open(self): elif magic_number in (b"P3", b"P6"): self.custom_mimetype = "image/x-portable-pixmap" - maxval = None + self._size = int(self._read_token()), int(self._read_token()) + decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" - for ix in range(3): - token = int(self._read_token()) - if ix == 0: # token is the x size - xsize = token - elif ix == 1: # token is the y size - ysize = token - if mode == "1": - self._mode = "1" - rawmode = "1;I" - break - else: - self._mode = rawmode = mode - elif ix == 2: # token is maxval - maxval = token - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" - - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" - - args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) - self._size = xsize, ysize - self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] + if mode == "1": + self._mode = "1" + args = "1;I" + else: + maxval = int(self._read_token()) + if not 0 < maxval < 65536: + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) + self._mode = "I" if maxval > 255 and mode == "L" else mode + + rawmode = mode + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" + + args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) + self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] # From ba6399cad14d816cafc44c25782d6a3153082ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 19:34:27 +1100 Subject: [PATCH 0041/1178] Added PerspectiveTransform --- Tests/test_image_transform.py | 2 ++ docs/reference/ImageTransform.rst | 5 +++++ src/PIL/ImageTransform.py | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 15939ef647c..578a0a2967a 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -16,6 +16,8 @@ def test_sanity(self): transform = ImageTransform.AffineTransform(seq[:6]) im.transform((100, 100), transform) + transform = ImageTransform.PerspectiveTransform(seq[:8]) + im.transform((100, 100), transform) transform = ImageTransform.ExtentTransform(seq[:4]) im.transform((100, 100), transform) transform = ImageTransform.QuadTransform(seq[:8]) diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 1278801821d..5b0a5ce49dc 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -19,6 +19,11 @@ The :py:mod:`~PIL.ImageTransform` module contains implementations of :undoc-members: :show-inheritance: +.. autoclass:: PerspectiveTransform + :members: + :undoc-members: + :show-inheritance: + .. autoclass:: ExtentTransform :members: :undoc-members: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 4f79500e64e..6aa82dadd9c 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -63,6 +63,26 @@ class AffineTransform(Transform): method = Image.Transform.AFFINE +class PerspectiveTransform(Transform): + """ + Define a perspective image transform. + + This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel + (x, y) in the output image, the new value is taken from a position + ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in + the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). + """ + + method = Image.Transform.PERSPECTIVE + + class ExtentTransform(Transform): """ Define a transform to extract a subregion from an image. From 6d99f9193f0abe211e611f2d9909a4eb5881d270 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Sun, 7 Jan 2024 16:00:58 -0500 Subject: [PATCH 0042/1178] Fix info for first frame of apng images getting clobbered when seeking to the first frame multiple times. --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index e4ed9388011..823f1249285 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -378,7 +378,7 @@ def save_rewind(self): } def rewind(self): - self.im_info = self.rewind_state["info"] + self.im_info = self.rewind_state["info"].copy() self.im_tile = self.rewind_state["tile"] self._seq_num = self.rewind_state["seq_num"] From 08f11c57a131b402f405db76dfd411b441df1ab5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:52:47 +0100 Subject: [PATCH 0043/1178] deprecate ImageCms members: DESCRIPTION, VERSION, FLAGS, versions() --- Tests/test_imagecms.py | 13 +++++++++++-- src/PIL/ImageCms.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index fec482f4393..9575b026db2 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -49,8 +49,8 @@ def skip_missing(): def test_sanity(): # basic smoke test. # this mostly follows the cms_test outline. - - v = ImageCms.versions() # should return four strings + with pytest.warns(DeprecationWarning): + v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -637,3 +637,12 @@ def test_rgb_lab(mode): im = Image.new("LAB", (1, 1), (255, 0, 0)) converted_im = im.convert(mode) assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + + +def test_deprecation(): + with pytest.warns(DeprecationWarning): + assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") + with pytest.warns(DeprecationWarning): + assert ImageCms.VERSION == "1.0.0 pil" + with pytest.warns(DeprecationWarning): + assert isinstance(ImageCms.FLAGS, dict) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 62b010f4512..827755bf672 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,10 @@ import sys from enum import IntEnum, IntFlag from functools import reduce +from typing import Any from . import Image +from ._deprecate import deprecate try: from . import _imagingcms @@ -32,7 +34,7 @@ _imagingcms = DeferredError.new(ex) -DESCRIPTION = """ +_DESCRIPTION = """ pyCMS a Python / PIL interface to the littleCMS ICC Color Management System @@ -95,7 +97,22 @@ """ -VERSION = "1.0.0 pil" +_VERSION = "1.0.0 pil" + + +def __getattr__(name: str) -> Any: + if name == "DESCRIPTION": + deprecate("PIL.ImageCms.DESCRIPTION", 12) + return _DESCRIPTION + elif name == "VERSION": + deprecate("PIL.ImageCms.VERSION", 12) + return _VERSION + elif name == "FLAGS": + deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") + return _FLAGS + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + # --------------------------------------------------------------------. @@ -184,7 +201,7 @@ def GRIDPOINTS(n: int) -> Flags: _MAX_FLAG = reduce(operator.or_, Flags) -FLAGS = { +_FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, "MATRIXONLY": (1 | 2), @@ -1064,4 +1081,9 @@ def versions(): (pyCMS) Fetches versions. """ - return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ + deprecate( + "PIL.ImageCms.versions()", + 12, + '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', + ) + return _VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ From ccdea48cf379fac585918001f304a7ab58448b96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Jan 2024 10:36:30 +1100 Subject: [PATCH 0044/1178] Added identity tests for Transform classes --- Tests/test_image_transform.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 578a0a2967a..f5d5ab70408 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -10,20 +10,25 @@ class TestImageTransform: def test_sanity(self): - im = Image.new("L", (100, 100)) - - seq = tuple(range(10)) - - transform = ImageTransform.AffineTransform(seq[:6]) - im.transform((100, 100), transform) - transform = ImageTransform.PerspectiveTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.ExtentTransform(seq[:4]) - im.transform((100, 100), transform) - transform = ImageTransform.QuadTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) - im.transform((100, 100), transform) + im = hopper() + + for transform in ( + ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)), + ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)), + ImageTransform.ExtentTransform((0, 0) + im.size), + ImageTransform.QuadTransform( + (0, 0, 0, im.height, im.width, im.height, im.width, 0) + ), + ImageTransform.MeshTransform( + [ + ( + (0, 0) + im.size, + (0, 0, 0, im.height, im.width, im.height, im.width, 0), + ) + ] + ), + ): + assert_image_equal(im, im.transform(im.size, transform)) def test_info(self): comment = b"File written by Adobe Photoshop\xa8 4.0" From edc46e223b1275f9b197c33011f305af2975afe3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:27:41 +0100 Subject: [PATCH 0045/1178] document ImageCms deprecations --- docs/deprecations.rst | 39 ++++++++++++++++- docs/reference/ImageCms.rst | 3 ++ docs/releasenotes/10.3.0.rst | 83 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/9.1.0.rst | 2 +- docs/releasenotes/index.rst | 1 + 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0f9c75756ee..7602f8b4e80 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -55,6 +55,43 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. +ImageCms constants and versions() function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.3.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been +replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + Removed features ---------------- @@ -118,7 +155,7 @@ Constants .. versionremoved:: 10.0.0 A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 22ed516ce58..c4484cbe22a 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -24,14 +24,17 @@ Constants :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Direction :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Flags :members: :member-order: bysource :undoc-members: + :show-inheritance: Functions --------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 00000000000..ddcb38aa161 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,83 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been replaced +with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 02da702a799..6400218f467 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -51,7 +51,7 @@ Constants ^^^^^^^^^ A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853cc2..e86f8082b48 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 From bb855583ea3320b637e7f6511721a9e2655f2b99 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:55:32 +0100 Subject: [PATCH 0046/1178] Update PyPI links to use pillow (lowercase) --- README.md | 4 ++-- docs/about.rst | 2 +- docs/index.rst | 4 ++-- docs/installation.rst | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6982676f518..6ca870166a1 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ As of 2019, Pillow development is Tidelift - Newest PyPI version - Number of PyPI downloads `_ and by direct URL access -eg. https://pypi.org/project/Pillow/1.0/. +`_ and by direct URL access +eg. https://pypi.org/project/pillow/1.0/. From 3515f997ce06664d8ec42cc21e39eef9897dbaeb Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Sun, 7 Jan 2024 20:42:52 -0500 Subject: [PATCH 0047/1178] Add test against info of apng images getting clobbered when seeking to the first frame multiple times. --- Tests/images/apng/issue_7700.png | Bin 0 -> 233 bytes Tests/test_file_apng.py | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 Tests/images/apng/issue_7700.png diff --git a/Tests/images/apng/issue_7700.png b/Tests/images/apng/issue_7700.png new file mode 100644 index 0000000000000000000000000000000000000000..984254b8e5632f9f1bf148485a77bb6a141dcb4b GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9Er&xK0ulYh#5ZjPA&jaQfUw| zkPuh{hyel)U@p%GyA42&x~Gd{NX4Aw6SvYo%ojStp!80BRioiiwh7HElNnUodZY}p z4j33RFfd-v>sQUc#0b;^GZ@51GuQzr6w`P67?6@pOK}VV(o8_Z4dze!4m2ES)C$JM dY&_!34DTjb$p7CmBOj=M!PC{xWt~$(698zJHUj_v literal 0 HcmV?d00001 diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 60d951636e8..8069d4b08bb 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -689,3 +689,13 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat ) with Image.open(test_file) as reloaded: assert reloaded.mode == mode + + +def test_apng_issue_7700(): + # https://github.com/python-pillow/Pillow/issues/7700 + with Image.open("Tests/images/apng/issue_7700.png") as im: + for i in range(5): + im.seek(0) + assert im.info["duration"] == 4000.0 + im.seek(1) + assert im.info["duration"] == 1000.0 From bddfebc3315d85bf822192c57a33646d79e738c3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 12:57:23 +0100 Subject: [PATCH 0048/1178] add license comment to ImageCms; explicitly say "no replacement" for deprecations without a replacement --- docs/deprecations.rst | 4 ++-- docs/releasenotes/10.3.0.rst | 4 ++-- src/PIL/ImageCms.py | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 7602f8b4e80..4c9abe19503 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -67,11 +67,11 @@ replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index ddcb38aa161..8dfe34d95fa 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -20,11 +20,11 @@ with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 827755bf672..3e40105e46a 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -4,6 +4,9 @@ # Optional color management support, based on Kevin Cazabon's PyCMS # library. +# Originally released under LGPL. Graciously donated to PIL in +# March 2009, for distribution under the standard PIL license + # History: # 2009-03-08 fl Added to PIL. From f044d53fd181446366ab78f570076a665933d4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Mon, 8 Jan 2024 17:17:17 +0100 Subject: [PATCH 0049/1178] swap conditions Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 6be1278eb2e..d43e21e14db 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ def _open(self): for ix in range(3): if mode == "F" and ix == 2: scale = float(self._read_token()) - if not math.isfinite(scale) or scale == 0.0: + if scale == 0.0 or not math.isfinite(scale): msg = "scale must be finite and non-zero" raise ValueError(msg) rawmode = "F;32F" if scale < 0 else "F;32BF" From 5dd1652f2775bb916faa648fe48c7c6350d2c8a4 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:16:23 +0100 Subject: [PATCH 0050/1178] use filename instead of f --- Tests/test_file_ppm.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index efdb880de07..d8e259b1cf8 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -89,20 +89,20 @@ def test_16bit_pgm(): def test_16bit_pgm_write(tmp_path): with Image.open("Tests/images/16_bit_binary.pgm") as im: - f = str(tmp_path / "temp.pgm") - im.save(f, "PPM") + filename = str(tmp_path / "temp.pgm") + im.save(filename, "PPM") - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pnm(tmp_path): with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - f = str(tmp_path / "temp.pnm") - im.save(f) + filename = str(tmp_path / "temp.pnm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm(tmp_path): @@ -110,10 +110,10 @@ def test_pfm(tmp_path): assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm_big_endian(tmp_path): @@ -121,10 +121,10 @@ def test_pfm_big_endian(tmp_path): assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) @pytest.mark.parametrize( From 586e7740947933454589f3c60b22387e01fa5701 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:35:01 +0100 Subject: [PATCH 0051/1178] add PFM support to release notes --- docs/handbook/image-file-formats.rst | 3 +- docs/releasenotes/10.3.0.rst | 49 ++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 02a6a3af729..569ccb7691b 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -701,7 +701,8 @@ PFM .. versionadded:: 10.3.0 -Pillow reads and writes grayscale (Pf format) PFM files containing ``F`` data. +Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files +containing ``F`` data. Color (PF format) PFM files are not supported. diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 00000000000..34afbe4b844 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,49 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Portable FloatMap (PFM) images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading and writing grayscale (Pf format) +Portable FloatMap (PFM) files containing ``F`` data. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853cc2..e86f8082b48 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 From a844871c5eec479275af20ae0f06e4204174e9a6 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Mon, 8 Jan 2024 15:18:49 -0500 Subject: [PATCH 0052/1178] Give apng repeated seeks test and image a more descriptive name. --- ...700.png => repeated_seeks_give_correct_info.png} | Bin Tests/test_file_apng.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename Tests/images/apng/{issue_7700.png => repeated_seeks_give_correct_info.png} (100%) diff --git a/Tests/images/apng/issue_7700.png b/Tests/images/apng/repeated_seeks_give_correct_info.png similarity index 100% rename from Tests/images/apng/issue_7700.png rename to Tests/images/apng/repeated_seeks_give_correct_info.png diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 8069d4b08bb..47d425f8b74 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -691,9 +691,9 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat assert reloaded.mode == mode -def test_apng_issue_7700(): +def test_apng_repeated_seeks_give_correct_info(): # https://github.com/python-pillow/Pillow/issues/7700 - with Image.open("Tests/images/apng/issue_7700.png") as im: + with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: for i in range(5): im.seek(0) assert im.info["duration"] == 4000.0 From a6051a4045354203d9fccafbe2ac620c505d9e00 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Mon, 8 Jan 2024 15:20:24 -0500 Subject: [PATCH 0053/1178] Add type hints and fix some formatting for the apng repeated seeks test. --- Tests/test_file_apng.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 47d425f8b74..ea5ab41ec60 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -691,11 +691,11 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat assert reloaded.mode == mode -def test_apng_repeated_seeks_give_correct_info(): +def test_apng_repeated_seeks_give_correct_info() -> None: # https://github.com/python-pillow/Pillow/issues/7700 with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: for i in range(5): im.seek(0) - assert im.info["duration"] == 4000.0 + assert im.info["duration"] == 4000 im.seek(1) - assert im.info["duration"] == 1000.0 + assert im.info["duration"] == 1000 From 931821688c0f9ef331097ac8b1358780eb82b21e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:22:25 +1100 Subject: [PATCH 0054/1178] Added release notes --- docs/releasenotes/10.3.0.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 34afbe4b844..391068769fb 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -26,10 +26,12 @@ TODO API Additions ============= -TODO -^^^^ +Added PerspectiveTransform +^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning +that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding +subclass of :py:class:`~PIL.ImageTransform.Transform`. Security ======== From 6c320323b44df48a5162a2113ae2c34e7c9bf564 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:47:27 +1100 Subject: [PATCH 0055/1178] Only set row order when needed --- src/PIL/PpmImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 83e028718eb..9d37dcde099 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -311,7 +311,6 @@ def decode(self, buffer): def _save(im, fp, filename): - row_order = 1 if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": @@ -322,7 +321,6 @@ def _save(im, fp, filename): rawmode, head = "RGB", b"P6" elif im.mode == "F": rawmode, head = "F;32F", b"Pf" - row_order = -1 else: msg = f"cannot write mode {im.mode} as PPM" raise OSError(msg) @@ -336,6 +334,7 @@ def _save(im, fp, filename): fp.write(b"65535\n") elif head == b"Pf": fp.write(b"-1.0\n") + row_order = -1 if im.mode == "F" else 1 ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) From ab262dbfd5b8076fd8d530e27c8e4896f03025e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:56:33 +1100 Subject: [PATCH 0056/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ebf731c5659..30bbaec3a64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add support for reading and writing grayscale PFM images #7696 + [nulano, hugovk] + - Add LCMS2 flags to ImageCms #7676 [nulano, radarhere, hugovk] From 1e8a03cd2d938d3773dfdbe9d477276e96527494 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:08:40 +1100 Subject: [PATCH 0057/1178] Link to Python enum documentation --- docs/reference/ExifTags.rst | 5 +++-- docs/releasenotes/10.0.0.rst | 2 +- docs/releasenotes/9.3.0.rst | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 464ab77ea35..06965ead3f0 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,9 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes -which provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` +classes which provide constants and clear-text names for various well-known +EXIF tags. .. py:data:: Base diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index a3f238119f0..705ca04152f 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -43,7 +43,7 @@ Constants ^^^^^^^^^ A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. ===================================================== ============================================================ Removed Use instead diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index fde2faae3a7..16075ce95ec 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -33,8 +33,9 @@ Added ExifTags enums ^^^^^^^^^^^^^^^^^^^^ The data from :py:data:`~PIL.ExifTags.TAGS` and -:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum`` -classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. +:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as +:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and +:py:data:`~PIL.ExifTags.GPS`. Security From 71ba20bb19899f55761dae1ccd0bc662766cdc65 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:22:10 +1100 Subject: [PATCH 0058/1178] Shortened table description --- docs/deprecations.rst | 54 ++++++++++++++++++------------------ docs/releasenotes/10.3.0.rst | 54 ++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4c9abe19503..205fcb9abcd 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -64,33 +64,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== Removed features ---------------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 8ce6f4b9c9f..548f95df95c 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -17,33 +17,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== API Changes =========== From dc6d7611e9196447aaa18305c79ee83f005f4ec3 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Tue, 9 Jan 2024 08:55:49 -0500 Subject: [PATCH 0059/1178] Test apng repeated seeks 3 times instead of 5. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_apng.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ea5ab41ec60..3403322585e 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -692,9 +692,8 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat def test_apng_repeated_seeks_give_correct_info() -> None: - # https://github.com/python-pillow/Pillow/issues/7700 with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: - for i in range(5): + for i in range(3): im.seek(0) assert im.info["duration"] == 4000 im.seek(1) From d7874e8a03dbf57b01c0fe41290603ddfe2875c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 09:07:10 +1100 Subject: [PATCH 0060/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 30bbaec3a64..c267ca472ce 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Added PerspectiveTransform #7699 + [radarhere] + - Add support for reading and writing grayscale PFM images #7696 [nulano, hugovk] From df99d48a0cb98cad2f6ee5ae65b0be5df8d84ef3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 09:26:56 +1100 Subject: [PATCH 0061/1178] Simplified code --- src/PIL/PsdImagePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 5cff564137d..9f7bf78290b 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -185,14 +185,13 @@ def read(size): # image info mode = [] ct_types = i16(read(2)) - types = list(range(ct_types)) - if len(types) > 4: - fp.seek(len(types) * 6 + 12, io.SEEK_CUR) + if ct_types > 4: + fp.seek(ct_types * 6 + 12, io.SEEK_CUR) size = i32(read(4)) fp.seek(size, io.SEEK_CUR) continue - for _ in types: + for _ in range(ct_types): type = i16(read(2)) if type == 65535: From 659098c6acc46ff353ba2a136805b463fb5cc97b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 22:05:26 +1100 Subject: [PATCH 0062/1178] Added type hints --- pyproject.toml | 1 - src/PIL/Image.py | 4 +- src/PIL/ImageMath.py | 136 +++++++++++++++++++++------------------ src/PIL/_imagingmath.pyi | 5 ++ 4 files changed, 80 insertions(+), 66 deletions(-) create mode 100644 src/PIL/_imagingmath.pyi diff --git a/pyproject.toml b/pyproject.toml index da2537b2137..54a4bcaec0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageMath.py$', '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', '^src/PIL/ImageShow.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c56da545803..0fbbe5861a0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -873,7 +873,7 @@ def verify(self): def convert( self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 - ): + ) -> Image: """ Returns a converted copy of this image. For the "P" mode, this method translates pixels through the palette. If mode is @@ -1305,7 +1305,7 @@ def getbands(self): """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only=True): + def getbbox(self, *, alpha_only=True) -> tuple[int, int, int, int]: """ Calculates the bounding box of the non-zero regions in the image. diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index b77f4bce567..949fa45bb45 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,6 +17,7 @@ from __future__ import annotations import builtins +from typing import Any from . import Image, _imagingmath @@ -24,10 +25,10 @@ class _Operand: """Wraps an image operand, providing standard operators""" - def __init__(self, im): + def __init__(self, im: Image.Image): self.im = im - def __fixup(self, im1): + def __fixup(self, im1: _Operand | float) -> Image.Image: # convert image to suitable mode if isinstance(im1, _Operand): # argument was an image. @@ -45,122 +46,131 @@ def __fixup(self, im1): else: return Image.new("F", self.im.size, im1) - def apply(self, op, im1, im2=None, mode=None): - im1 = self.__fixup(im1) + def apply( + self, + op: str, + im1: _Operand | float, + im2: _Operand | float | None = None, + mode: str | None = None, + ) -> _Operand: + im_1 = self.__fixup(im1) if im2 is None: # unary operation - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im1.im.id) + _imagingmath.unop(op, out.im.id, im_1.im.id) else: # binary operation - im2 = self.__fixup(im2) - if im1.mode != im2.mode: + im_2 = self.__fixup(im2) + if im_1.mode != im_2.mode: # convert both arguments to floating point - if im1.mode != "F": - im1 = im1.convert("F") - if im2.mode != "F": - im2 = im2.convert("F") - if im1.size != im2.size: + if im_1.mode != "F": + im_1 = im_1.convert("F") + if im_2.mode != "F": + im_2 = im_2.convert("F") + if im_1.size != im_2.size: # crop both arguments to a common size - size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) - if im1.size != size: - im1 = im1.crop((0, 0) + size) - if im2.size != size: - im2 = im2.crop((0, 0) + size) - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() - im2.load() + size = ( + min(im_1.size[0], im_2.size[0]), + min(im_1.size[1], im_2.size[1]), + ) + if im_1.size != size: + im_1 = im_1.crop((0, 0) + size) + if im_2.size != size: + im_2 = im_2.crop((0, 0) + size) + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() + im_2.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) + _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) return _Operand(out) # unary operators - def __bool__(self): + def __bool__(self) -> bool: # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - def __abs__(self): + def __abs__(self) -> _Operand: return self.apply("abs", self) - def __pos__(self): + def __pos__(self) -> _Operand: return self - def __neg__(self): + def __neg__(self) -> _Operand: return self.apply("neg", self) # binary operators - def __add__(self, other): + def __add__(self, other: _Operand | float) -> _Operand: return self.apply("add", self, other) - def __radd__(self, other): + def __radd__(self, other: _Operand | float) -> _Operand: return self.apply("add", other, self) - def __sub__(self, other): + def __sub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", self, other) - def __rsub__(self, other): + def __rsub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", other, self) - def __mul__(self, other): + def __mul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", self, other) - def __rmul__(self, other): + def __rmul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", other, self) - def __truediv__(self, other): + def __truediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", self, other) - def __rtruediv__(self, other): + def __rtruediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", other, self) - def __mod__(self, other): + def __mod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", self, other) - def __rmod__(self, other): + def __rmod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", other, self) - def __pow__(self, other): + def __pow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", self, other) - def __rpow__(self, other): + def __rpow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", other, self) # bitwise - def __invert__(self): + def __invert__(self) -> _Operand: return self.apply("invert", self) - def __and__(self, other): + def __and__(self, other: _Operand | float) -> _Operand: return self.apply("and", self, other) - def __rand__(self, other): + def __rand__(self, other: _Operand | float) -> _Operand: return self.apply("and", other, self) - def __or__(self, other): + def __or__(self, other: _Operand | float) -> _Operand: return self.apply("or", self, other) - def __ror__(self, other): + def __ror__(self, other: _Operand | float) -> _Operand: return self.apply("or", other, self) - def __xor__(self, other): + def __xor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", self, other) - def __rxor__(self, other): + def __rxor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", other, self) - def __lshift__(self, other): + def __lshift__(self, other: _Operand | float) -> _Operand: return self.apply("lshift", self, other) - def __rshift__(self, other): + def __rshift__(self, other: _Operand | float) -> _Operand: return self.apply("rshift", self, other) # logical @@ -170,46 +180,46 @@ def __eq__(self, other): def __ne__(self, other): return self.apply("ne", self, other) - def __lt__(self, other): + def __lt__(self, other: _Operand | float) -> _Operand: return self.apply("lt", self, other) - def __le__(self, other): + def __le__(self, other: _Operand | float) -> _Operand: return self.apply("le", self, other) - def __gt__(self, other): + def __gt__(self, other: _Operand | float) -> _Operand: return self.apply("gt", self, other) - def __ge__(self, other): + def __ge__(self, other: _Operand | float) -> _Operand: return self.apply("ge", self, other) # conversions -def imagemath_int(self): +def imagemath_int(self: _Operand) -> _Operand: return _Operand(self.im.convert("I")) -def imagemath_float(self): +def imagemath_float(self: _Operand) -> _Operand: return _Operand(self.im.convert("F")) # logical -def imagemath_equal(self, other): +def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("eq", self, other, mode="I") -def imagemath_notequal(self, other): +def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("ne", self, other, mode="I") -def imagemath_min(self, other): +def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("min", self, other) -def imagemath_max(self, other): +def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("max", self, other) -def imagemath_convert(self, mode): +def imagemath_convert(self: _Operand, mode: str) -> _Operand: return _Operand(self.im.convert(mode)) @@ -219,7 +229,7 @@ def imagemath_convert(self, mode): ops[k[10:]] = v -def eval(expression, _dict={}, **kw): +def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: """ Evaluates an image expression. @@ -247,7 +257,7 @@ def eval(expression, _dict={}, **kw): compiled_code = compile(expression, "", "eval") - def scan(code): + def scan(code) -> None: for const in code.co_consts: if type(const) is type(compiled_code): scan(const) diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imagingmath.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From 38bfe3cddf618bf5aaaf0d5c88011031fd0eaf45 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:36:26 +1100 Subject: [PATCH 0063/1178] Added type hint Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0fbbe5861a0..ac13c6c0c25 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1305,7 +1305,7 @@ def getbands(self): """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only=True) -> tuple[int, int, int, int]: + def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: """ Calculates the bounding box of the non-zero regions in the image. From 993bc6c2027926633321593d5c39a29cbe926e3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 23:41:09 +1100 Subject: [PATCH 0064/1178] Added type hint --- src/PIL/ImageMath.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 949fa45bb45..bc3318c04f4 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,6 +17,7 @@ from __future__ import annotations import builtins +from types import CodeType from typing import Any from . import Image, _imagingmath @@ -257,7 +258,7 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: compiled_code = compile(expression, "", "eval") - def scan(code) -> None: + def scan(code: CodeType) -> None: for const in code.co_consts: if type(const) is type(compiled_code): scan(const) From c2907dc04967109391a77eea00f7d583a0a0395f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 23:53:45 +1100 Subject: [PATCH 0065/1178] Layer co-ordinates may be negative --- Tests/images/negative_top_left_layer.psd | Bin 0 -> 8220 bytes Tests/test_file_psd.py | 5 +++++ src/PIL/PsdImagePlugin.py | 9 +++++---- src/PIL/_binary.py | 10 ++++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 Tests/images/negative_top_left_layer.psd diff --git a/Tests/images/negative_top_left_layer.psd b/Tests/images/negative_top_left_layer.psd new file mode 100644 index 0000000000000000000000000000000000000000..be9d9d6d03e357d4e3c31aecf0d3f90f9fc49974 GIT binary patch literal 8220 zcmeHLOKclO82+84w2B(Fg$kj{J{Py>O@s5)Tnk;DCfU!vP66^uVR05)uyyaj1IWz(WGT1y$ysS-%1)G!l)3h{_mtCT}}nG*az3o1HEcM}Oi^6FbX3af3SXscWnN8yQRS zkv;f{D6LjB3(OcF9sQ9%?~YCK@mQzlGb7Uxh;VkX$C1md)eI*XI|AUip+A3OS~z{? zDPjBfh(8Y7;a->|Mo>RmD$TJO8<*d|jC}e+WLO*9+)_JX(!fA>1hhC})cp|UXHZTz z!a9>*MR~8WNRX-6`zTNPxIvV^X8KAb|G?xbm(f9#{qp<_`nDHkq1q=``eb9#chE*d zn**lfBL2wfNH&@L9Fv{e67G!`7PXz!eS87?1K1vb6HtI6lt6$AEP)L*2tmZ&MNI(n zI6i%g`TO|%63(jNEE^1*F$*3zK=^!MF+dwEJ#GUlsd80p=QO~!?^>;29|YL`62Q%i zt=1oJwOTjd1{nPo;8V9Z-@A+v{|)sQd%6Q}0_=Jo;DakYUF8*k4=(`R_YLM7;p?!R z9I-5bU)}|nI0JCcHGuJ_Bi?X~jl(-%0hE!exvK!Ly$8HWg)-I`3h#FGOp7j4~@bc%4L8hTQ2-6pAH5O(=3Ana26Ds_C+#E3%+u zbv3EWGFrzj_jbgPxNj_zV8WU@+^iGh91VpEt=eur{_{tl#0A^T$Dd3U!Izr0>dN~Zz;^j<2WvK_UF`##V))XB=~LtY<8-MGLd} zINPsodWLQ4TuiEFI+IbdVkVukM9oU2#9W1@#foYeNjXI`ik0qzD!4&NT!S(w1W(wA zqgh&p=2TfsB~4A#vW6y-6g5RtlXGb$XXbKQtq`YT)&9jXQTZ1fa8)@**71c9Fmofwe|0a>Pzdx#Yc;WwsRrT7AAXKY7r zZQA_1aF)L?NBC*oxY&s>YDH<`}MGrhHY~b&bqgCV)xxx(PuRq~n z6vT%)30?EXB4rUm#za{W75T4>No|iM_TK2YJSF{&lqqh|_oBghkq%#A_&$`nFT^%! zxPv=c+QGP!O9;YnkxlVCq-#jmrWn|i@i4oFbZv@(O&Jff>p!DwymzIdF8&N^;Drg` zuFtStX|eJ-Yk2u$cWM>J*ncUpOYAm=pX_#b3GiANyTop3F8-Em|J8DYS+KHgj;v5W zy2aI&TpLxpnHuKs4!;5vI%7r+C4O-`rTwfiCIzSP* zx;hXQBQmxHbt}H9hJM^W1H*XSHUn!n)>?zmw&GXT+I8e=0NR#tbrrc9fVL%EwblUH L`ro%zxbyW->#0jd literal 0 HcmV?d00001 diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b163..e3c1f447af2 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -112,6 +112,11 @@ def test_rgba(): assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") +def test_negative_top_left_layer() -> None: + with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert im.layers[0][2] == (-50, -50, 50, 50) + + def test_layer_skip(): with Image.open("Tests/images/five_channels.psd") as im: assert im.n_frames == 1 diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 9f7bf78290b..d29bcf9970c 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -24,6 +24,7 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import si16be as si16 +from ._binary import si32be as si32 MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -177,10 +178,10 @@ def read(size): for _ in range(abs(ct)): # bounding box - y0 = i32(read(4)) - x0 = i32(read(4)) - y1 = i32(read(4)) - x1 = i32(read(4)) + y0 = si32(read(4)) + x0 = si32(read(4)) + y1 = si32(read(4)) + x1 = si32(read(4)) # image info mode = [] diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 0a07e8d0e12..4594ccce361 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -77,6 +77,16 @@ def si32le(c: bytes, o: int = 0) -> int: return unpack_from(" int: + """ + Converts a 4-bytes (32 bits) string to a signed integer, big endian. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(">i", c, o)[0] + + def i16be(c: bytes, o: int = 0) -> int: return unpack_from(">H", c, o)[0] From 6f144d45b98d6c331da9fcd437542e224793b893 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Wed, 10 Jan 2024 16:03:42 -0500 Subject: [PATCH 0066/1178] Rename repeated seeks apng to reflect what it is rather than how it is used. --- ...ive_correct_info.png => different_durations.png} | Bin Tests/test_file_apng.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Tests/images/apng/{repeated_seeks_give_correct_info.png => different_durations.png} (100%) diff --git a/Tests/images/apng/repeated_seeks_give_correct_info.png b/Tests/images/apng/different_durations.png similarity index 100% rename from Tests/images/apng/repeated_seeks_give_correct_info.png rename to Tests/images/apng/different_durations.png diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 3403322585e..e2c4569cee5 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -692,7 +692,7 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat def test_apng_repeated_seeks_give_correct_info() -> None: - with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: + with Image.open("Tests/images/apng/different_durations.png") as im: for i in range(3): im.seek(0) assert im.info["duration"] == 4000 From 5347b471c69c400c6199c8cd200b5844169f7988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 11 Jan 2024 02:08:46 +0100 Subject: [PATCH 0067/1178] Update Tests/test_imagecms.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_imagecms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 9575b026db2..810394e6f5f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -639,7 +639,7 @@ def test_rgb_lab(mode): assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) -def test_deprecation(): +def test_deprecation() -> None: with pytest.warns(DeprecationWarning): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") with pytest.warns(DeprecationWarning): From 08992cf6b18cc3ad1dc9e75088b505450c21388b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 20:01:25 +1100 Subject: [PATCH 0068/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c267ca472ce..887319dabf8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Deprecate ImageCms constants and versions() function #7702 + [nulano, radarhere] + - Added PerspectiveTransform #7699 [radarhere] From bc192557b8de105da5942108d33d643f83513976 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 23:11:12 +1100 Subject: [PATCH 0069/1178] Added type hints --- pyproject.toml | 1 - src/PIL/ImageMorph.py | 50 +++++++++++++++++++++++---------------- src/PIL/_imagingmorph.pyi | 5 ++++ 3 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 src/PIL/_imagingmorph.pyi diff --git a/pyproject.toml b/pyproject.toml index 54a4bcaec0c..8acfc04204e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', '^src/PIL/ImageShow.py$', '^src/PIL/ImImagePlugin.py$', diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 282e7d2a54e..534c6291a02 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -62,12 +62,14 @@ class LutBuilder: """ - def __init__(self, patterns=None, op_name=None): + def __init__( + self, patterns: list[str] | None = None, op_name: str | None = None + ) -> None: if patterns is not None: self.patterns = patterns else: self.patterns = [] - self.lut = None + self.lut: bytearray | None = None if op_name is not None: known_patterns = { "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], @@ -87,25 +89,27 @@ def __init__(self, patterns=None, op_name=None): self.patterns = known_patterns[op_name] - def add_patterns(self, patterns): + def add_patterns(self, patterns: list[str]) -> None: self.patterns += patterns - def build_default_lut(self): + def build_default_lut(self) -> None: symbols = [0, 1] m = 1 << 4 # pos of current pixel self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) - def get_lut(self): + def get_lut(self) -> bytearray | None: return self.lut - def _string_permute(self, pattern, permutation): + def _string_permute(self, pattern: str, permutation: list[int]) -> str: """string_permute takes a pattern and a permutation and returns the string permuted according to the permutation list. """ assert len(permutation) == 9 return "".join(pattern[p] for p in permutation) - def _pattern_permute(self, basic_pattern, options, basic_result): + def _pattern_permute( + self, basic_pattern: str, options: str, basic_result: int + ) -> list[tuple[str, int]]: """pattern_permute takes a basic pattern and its result and clones the pattern according to the modifications described in the $options parameter. It returns a list of all cloned patterns.""" @@ -135,12 +139,13 @@ def _pattern_permute(self, basic_pattern, options, basic_result): return patterns - def build_lut(self): + def build_lut(self) -> bytearray: """Compile all patterns into a morphology lut. TBD :Build based on (file) morphlut:modify_lut """ self.build_default_lut() + assert self.lut is not None patterns = [] # Parse and create symmetries of the patterns strings @@ -159,10 +164,10 @@ def build_lut(self): patterns += self._pattern_permute(pattern, options, result) # compile the patterns into regular expressions for speed - for i, pattern in enumerate(patterns): + compiled_patterns = [] + for pattern in patterns: p = pattern[0].replace(".", "X").replace("X", "[01]") - p = re.compile(p) - patterns[i] = (p, pattern[1]) + compiled_patterns.append((re.compile(p), pattern[1])) # Step through table and find patterns that match. # Note that all the patterns are searched. The last one @@ -172,8 +177,8 @@ def build_lut(self): bitpattern = bin(i)[2:] bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] - for p, r in patterns: - if p.match(bitpattern): + for pattern, r in compiled_patterns: + if pattern.match(bitpattern): self.lut[i] = [0, 1][r] return self.lut @@ -182,7 +187,12 @@ def build_lut(self): class MorphOp: """A class for binary morphological operators""" - def __init__(self, lut=None, op_name=None, patterns=None): + def __init__( + self, + lut: bytearray | None = None, + op_name: str | None = None, + patterns: list[str] | None = None, + ) -> None: """Create a binary morphological operator""" self.lut = lut if op_name is not None: @@ -190,7 +200,7 @@ def __init__(self, lut=None, op_name=None, patterns=None): elif patterns is not None: self.lut = LutBuilder(patterns=patterns).build_lut() - def apply(self, image): + def apply(self, image: Image.Image): """Run a single morphological operation on an image Returns a tuple of the number of changed pixels and the @@ -206,7 +216,7 @@ def apply(self, image): count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage - def match(self, image): + def match(self, image: Image.Image): """Get a list of coordinates matching the morphological operation on an image. @@ -221,7 +231,7 @@ def match(self, image): raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) - def get_on_pixels(self, image): + def get_on_pixels(self, image: Image.Image): """Get a list of all turned on pixels in a binary image Returns a list of tuples of (x,y) coordinates @@ -232,7 +242,7 @@ def get_on_pixels(self, image): raise ValueError(msg) return _imagingmorph.get_on_pixels(image.im.id) - def load_lut(self, filename): + def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" with open(filename, "rb") as f: self.lut = bytearray(f.read()) @@ -242,7 +252,7 @@ def load_lut(self, filename): msg = "Wrong size operator file!" raise Exception(msg) - def save_lut(self, filename): + def save_lut(self, filename: str) -> None: """Save an operator to an mrl file""" if self.lut is None: msg = "No operator loaded" @@ -250,6 +260,6 @@ def save_lut(self, filename): with open(filename, "wb") as f: f.write(self.lut) - def set_lut(self, lut): + def set_lut(self, lut: bytearray | None) -> None: """Set the lut from an external source""" self.lut = lut diff --git a/src/PIL/_imagingmorph.pyi b/src/PIL/_imagingmorph.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imagingmorph.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From 067c5f4123c7cab7507fca02605bfd9762861d1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 23:13:29 +1100 Subject: [PATCH 0070/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 887319dabf8..62ae2a68bbc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Fix APNG info after seeking backwards more than twice #7701 + [esoma, radarhere] + - Deprecate ImageCms constants and versions() function #7702 [nulano, radarhere] From 10cf2f2651eee87c127bbb6d090138506c86fbc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 14:40:59 +1100 Subject: [PATCH 0071/1178] Added type hints --- pyproject.toml | 1 - src/PIL/Image.py | 6 +++-- src/PIL/ImageShow.py | 60 ++++++++++++++++++++++++++------------------ tox.ini | 1 + 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8acfc04204e..789df6f5e67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,6 @@ exclude = [ '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', '^src/PIL/ImageQt.py$', - '^src/PIL/ImageShow.py$', '^src/PIL/ImImagePlugin.py$', '^src/PIL/MicImagePlugin.py$', '^src/PIL/PdfParser.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ac13c6c0c25..5ab27c35954 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -242,7 +242,7 @@ def _conv_type_shape(im): _MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") -def getmodebase(mode): +def getmodebase(mode: str) -> str: """ Gets the "base" mode for given mode. This function returns "L" for images that contain grayscale data, and "RGB" for images that @@ -583,7 +583,9 @@ def _ensure_mutable(self): else: self.load() - def _dump(self, file=None, format=None, **options): + def _dump( + self, file: str | None = None, format: str | None = None, **options + ) -> str: suffix = "" if format: suffix = "." + format diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index fad3e098003..d90545e92ec 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -13,18 +13,20 @@ # from __future__ import annotations +import abc import os import shutil import subprocess import sys from shlex import quote +from typing import Any from . import Image _viewers = [] -def register(viewer, order=1): +def register(viewer, order: int = 1) -> None: """ The :py:func:`register` function is used to register additional viewers:: @@ -49,7 +51,7 @@ def register(viewer, order=1): _viewers.insert(0, viewer) -def show(image, title=None, **options): +def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: r""" Display a given image. @@ -69,7 +71,7 @@ class Viewer: # main api - def show(self, image, **options): + def show(self, image: Image.Image, **options: Any) -> int: """ The main function for displaying an image. Converts the given image to the target format and displays it. @@ -87,16 +89,16 @@ def show(self, image, **options): # hook methods - format = None + format: str | None = None """The format to convert the image into.""" - options = {} + options: dict[str, Any] = {} """Additional options used to convert the image.""" - def get_format(self, image): + def get_format(self, image: Image.Image) -> str | None: """Return format name, or ``None`` to save as PGM/PPM.""" return self.format - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: """ Returns the command used to display the file. Not implemented in the base class. @@ -104,15 +106,15 @@ def get_command(self, file, **options): msg = "unavailable in base viewer" raise NotImplementedError(msg) - def save_image(self, image): + def save_image(self, image: Image.Image) -> str: """Save to temporary file and return filename.""" return image._dump(format=self.get_format(image), **self.options) - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -129,7 +131,7 @@ class WindowsViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: return ( f'start "Pillow" /WAIT "{file}" ' "&& ping -n 4 127.0.0.1 >NUL " @@ -147,14 +149,14 @@ class MacViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: # on darwin open returns immediately resulting in the temp # file removal while app is opening command = "open -a Preview.app" command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -180,7 +182,11 @@ class UnixViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + @abc.abstractmethod + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + pass + + def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] return f"({command} {quote(file)}" @@ -190,11 +196,11 @@ class XDGViewer(UnixViewer): The freedesktop.org ``xdg-open`` command. """ - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: command = executable = "xdg-open" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -208,13 +214,15 @@ class DisplayViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: command = executable = "display" if title: command += f" -title {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -231,12 +239,12 @@ def show_file(self, path, **options): class GmDisplayViewer(UnixViewer): """The GraphicsMagick ``gm display`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "gm" command = "gm display" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -247,12 +255,12 @@ def show_file(self, path, **options): class EogViewer(UnixViewer): """The GNOME Image Viewer ``eog`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "eog" command = "eog -n" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -266,7 +274,9 @@ class XVViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: # note: xv is pretty outdated. most modern systems have # imagemagick's display command instead. command = executable = "xv" @@ -274,7 +284,7 @@ def get_command_ex(self, file, title=None, **options): command += f" -name {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -304,7 +314,7 @@ def show_file(self, path, **options): class IPythonViewer(Viewer): """The viewer for IPython frontends.""" - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: ipython_display(image) return 1 diff --git a/tox.ini b/tox.ini index d89d017e45c..fb6746ce7bf 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ commands = [testenv:mypy] skip_install = true deps = + ipython mypy==1.7.1 numpy extras = From ffd0363b65ca05870f9306e8a6e999d58d9725ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 16:26:15 +1100 Subject: [PATCH 0072/1178] Added type hints --- src/PIL/FitsImagePlugin.py | 8 +++++--- src/PIL/Image.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 7dce2d60f76..e69890babcc 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -15,7 +15,7 @@ from . import Image, ImageFile -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == b"SIMPLE" @@ -23,8 +23,10 @@ class FitsImageFile(ImageFile.ImageFile): format = "FITS" format_description = "FITS" - def _open(self): - headers = {} + def _open(self) -> None: + assert self.fp is not None + + headers: dict[bytes, bytes] = {} while True: header = self.fp.read(80) if not header: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ac13c6c0c25..b2520d57c5a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3484,7 +3484,7 @@ def register_extension(id, extension) -> None: EXTENSION[extension.lower()] = id.upper() -def register_extensions(id, extensions): +def register_extensions(id, extensions) -> None: """ Registers image extensions. This function should not be used in application code. From 2fbd7dda839fb1cb4d9ba0a1e9abecf34e4786ec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 21:10:02 +1100 Subject: [PATCH 0073/1178] Use consistent arguments for load_seek --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index d2e60aa0759..690fb3586a7 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -408,7 +408,7 @@ def load(self, scale=1, transparency=False): self.tile = [] return Image.Image.load(self) - def load_seek(self, *args, **kwargs): + def load_seek(self, pos): # we can't incrementally load, so force ImageFile.parser to # use our custom load method by defining this method. pass diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 1b22f8645dc..2b21d957fd2 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -339,7 +339,7 @@ def load(self): self.size = im.size - def load_seek(self): + def load_seek(self, pos): # Flag the ImageFile.Parser so that it # just does all the decode at the end. pass From 543b5a674160c86d8b128e0af6c706a2e96c16d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 21:11:16 +1100 Subject: [PATCH 0074/1178] Use consistent arguments for load_read --- src/PIL/ImageFile.py | 2 +- src/PIL/XpmImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0923979af8b..17b1b3203e6 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -328,7 +328,7 @@ def load_end(self): # pass # may be defined for blocked formats (e.g. PNG) - # def load_read(self, bytes): + # def load_read(self, read_bytes): # pass def _seek_check(self, frame): diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index bf73c9bef06..3125f8d52c6 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -103,7 +103,7 @@ def _open(self): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] - def load_read(self, bytes): + def load_read(self, read_bytes): # # load all image data in one chunk From c97b5c6f7a221ed534d93e943992e44c2a7ec5fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Jan 2024 22:29:56 +1100 Subject: [PATCH 0075/1178] Exclude abstract method code from coverage --- src/PIL/ImageShow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index d90545e92ec..c03122c11aa 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -184,7 +184,7 @@ class UnixViewer(Viewer): @abc.abstractmethod def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass + pass # pragma: no cover def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] From c75a93b9a35e0835cc44465d5a73a9a05be0d166 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jan 2024 16:11:48 +1100 Subject: [PATCH 0076/1178] Added type hints --- src/PIL/Image.py | 4 ++-- src/PIL/ImageFile.py | 2 +- src/PIL/ImagePalette.py | 2 +- src/PIL/McIdasImagePlugin.py | 8 +++++--- src/PIL/PcdImagePlugin.py | 8 ++++++-- src/PIL/PcxImagePlugin.py | 10 +++++++--- src/PIL/PixarImagePlugin.py | 6 ++++-- src/PIL/SunImagePlugin.py | 6 ++++-- src/PIL/XVThumbImagePlugin.py | 6 ++++-- src/PIL/XbmImagePlugin.py | 9 ++++++--- 10 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c1f89af4616..e32f254de8e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3437,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None: OPEN[id] = factory, accept -def register_mime(id, mimetype): +def register_mime(id, mimetype) -> None: """ Registers an image MIME type by populating ``Image.MIME``. This function should not be used in application code. @@ -3452,7 +3452,7 @@ def register_mime(id, mimetype): MIME[id.upper()] = mimetype -def register_save(id, driver): +def register_save(id, driver) -> None: """ Registers an image save function. This function should not be used in application code. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0923979af8b..72c3c03c51a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -514,7 +514,7 @@ def close(self): # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize=0): +def _save(im, fp, tile, bufsize=0) -> None: """Helper to save image based on tile list :param im: Image object. diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fbcfa309d29..2b6cecc6105 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -192,7 +192,7 @@ def save(self, fp): # Internal -def raw(rawmode, data): +def raw(rawmode, data) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 9a85c0d15b0..27972236c0a 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -22,8 +22,8 @@ from . import Image, ImageFile -def _accept(s): - return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" +def _accept(prefix: bytes) -> bool: + return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" ## @@ -34,8 +34,10 @@ class McIdasImageFile(ImageFile.ImageFile): format = "MCIDAS" format_description = "McIdas area file" - def _open(self): + def _open(self) -> None: # parse area file directory + assert self.fp is not None + s = self.fp.read(256) if not _accept(s) or len(s) != 256: msg = "not an McIdas area file" diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index a0515b302eb..1cd5c4a9dbe 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -27,8 +27,10 @@ class PcdImageFile(ImageFile.ImageFile): format = "PCD" format_description = "Kodak PhotoCD" - def _open(self): + def _open(self) -> None: # rough + assert self.fp is not None + self.fp.seek(2048) s = self.fp.read(2048) @@ -47,9 +49,11 @@ def _open(self): self._size = 768, 512 # FIXME: not correct for rotated images! self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] - def load_end(self): + def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs + assert self.im is not None + self.im = self.im.rotate(self.tile_post_rotate) self._size = self.im.size diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 98ecefd0514..3e0968a8386 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -37,7 +37,7 @@ logger = logging.getLogger(__name__) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] @@ -49,8 +49,10 @@ class PcxImageFile(ImageFile.ImageFile): format = "PCX" format_description = "Paintbrush" - def _open(self): + def _open(self) -> None: # header + assert self.fp is not None + s = self.fp.read(128) if not _accept(s): msg = "not a PCX file" @@ -141,7 +143,7 @@ def _open(self): } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: @@ -199,6 +201,8 @@ def _save(im, fp, filename): if im.mode == "P": # colour palette + assert im.im is not None + fp.write(o8(12)) palette = im.im.getpalette("RGB", "RGB") palette += b"\x00" * (768 - len(palette)) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index af866feb362..887b6568bf7 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -27,7 +27,7 @@ # helpers -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"\200\350\000\000" @@ -39,8 +39,10 @@ class PixarImageFile(ImageFile.ImageFile): format = "PIXAR" format_description = "PIXAR raster image" - def _open(self): + def _open(self) -> None: # assuming a 4-byte magic label + assert self.fp is not None + s = self.fp.read(4) if not _accept(s): msg = "not a PIXAR file" diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 11ce3dfefd0..4e098474ab9 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -21,7 +21,7 @@ from ._binary import i32be as i32 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 @@ -33,7 +33,7 @@ class SunImageFile(ImageFile.ImageFile): format = "SUN" format_description = "Sun Raster File" - def _open(self): + def _open(self) -> None: # The Sun Raster file header is 32 bytes in length # and has the following format: @@ -49,6 +49,8 @@ def _open(self): # DWORD ColorMapLength; /* Size of the color map in bytes */ # } SUNRASTER; + assert self.fp is not None + # HEAD s = self.fp.read(32) if not _accept(s): diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 47ba1c54803..c84adaca215 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -33,7 +33,7 @@ ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == _MAGIC @@ -45,8 +45,10 @@ class XVThumbImageFile(ImageFile.ImageFile): format = "XVThumb" format_description = "XV thumbnail image" - def _open(self): + def _open(self) -> None: # check magic + assert self.fp is not None + if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" raise SyntaxError(msg) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 566acbfe5af..0291e2858ac 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,6 +21,7 @@ from __future__ import annotations import re +from io import BytesIO from . import Image, ImageFile @@ -36,7 +37,7 @@ ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix.lstrip()[:7] == b"#define" @@ -48,7 +49,9 @@ class XbmImageFile(ImageFile.ImageFile): format = "XBM" format_description = "X11 Bitmap" - def _open(self): + def _open(self) -> None: + assert self.fp is not None + m = xbm_head.match(self.fp.read(512)) if not m: @@ -67,7 +70,7 @@ def _open(self): self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) From 575edbefe49641107b7315b3113e35c6b74f9bca Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:25:31 +1100 Subject: [PATCH 0077/1178] Added type hints Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e32f254de8e..3f35bf50e1e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3437,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None: OPEN[id] = factory, accept -def register_mime(id, mimetype) -> None: +def register_mime(id: str, mimetype: str) -> None: """ Registers an image MIME type by populating ``Image.MIME``. This function should not be used in application code. @@ -3452,7 +3452,7 @@ def register_mime(id, mimetype) -> None: MIME[id.upper()] = mimetype -def register_save(id, driver) -> None: +def register_save(id: str, driver) -> None: """ Registers an image save function. This function should not be used in application code. From 4a6cb0f8447869ce886db1cde088723b55df32a2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jan 2024 20:04:51 +1100 Subject: [PATCH 0078/1178] Added type hints --- src/PIL/ImtImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 7469c592dd2..abb3fb762e7 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -33,10 +33,12 @@ class ImtImageFile(ImageFile.ImageFile): format = "IMT" format_description = "IM Tools" - def _open(self): + def _open(self) -> None: # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. + assert self.fp is not None + buffer = self.fp.read(100) if b"\n" not in buffer: msg = "not an IM file" From 6a2bdb6feb921fae4160c26e63e2ff541c4a7738 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 09:00:40 +1100 Subject: [PATCH 0079/1178] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3f35bf50e1e..b7bb514acc5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3507,7 +3507,7 @@ def registered_extensions(): return EXTENSION -def register_decoder(name, decoder): +def register_decoder(name: str, decoder) -> None: """ Registers an image decoder. This function should not be used in application code. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 72c3c03c51a..b79f2707b5a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ def decode(self, buffer): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data, rawmode=None): + def set_as_raw(self, data: bytes, rawmode = None) -> None: """ Convenience method to set the internal image from a stream of raw data diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 77dac65b6b3..bb7e466a790 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -35,7 +35,7 @@ # read MSP files -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] in [b"DanM", b"LinS"] @@ -48,8 +48,10 @@ class MspImageFile(ImageFile.ImageFile): format = "MSP" format_description = "Windows Paint" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(32) if not _accept(s): msg = "not an MSP file" @@ -109,7 +111,9 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -159,7 +163,7 @@ def decode(self, buffer): # write MSP files (uncompressed only) -def _save(im, fp, filename): +def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) From edaf7acdb3581e20cc7e77b631db736782cd8a15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:01:17 +0000 Subject: [PATCH 0080/1178] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b79f2707b5a..40353da677b 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ def decode(self, buffer): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data: bytes, rawmode = None) -> None: + def set_as_raw(self, data: bytes, rawmode=None) -> None: """ Convenience method to set the internal image from a stream of raw data From 5a587193c7ed360cc0705ae81c4628e990420384 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 12:22:59 +1100 Subject: [PATCH 0081/1178] Added type hints --- src/PIL/Image.py | 8 ++++---- src/PIL/SgiImagePlugin.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7bb514acc5..ec1cff89636 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -710,7 +710,7 @@ def __setstate__(self, state): self.putpalette(palette) self.frombytes(data) - def tobytes(self, encoder_name="raw", *args): + def tobytes(self, encoder_name: str = "raw", *args) -> bytes: """ Return image as a bytes object. @@ -788,7 +788,7 @@ def tobitmap(self, name="image"): ] ) - def frombytes(self, data, decoder_name="raw", *args): + def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: """ Loads this image with pixel data from a bytes object. @@ -1297,7 +1297,7 @@ def filter(self, filter): ] return merge(self.mode, ims) - def getbands(self): + def getbands(self) -> tuple[str, ...]: """ Returns a tuple containing the name of each band in this image. For example, ``getbands`` on an RGB image returns ("R", "G", "B"). @@ -2495,7 +2495,7 @@ def show(self, title=None): _show(self, title=title) - def split(self): + def split(self) -> tuple[Image, ...]: """ Split this image into individual bands. This method returns a tuple of individual image bands from an image. For example, diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index f9a10f6109c..ccf661ff1f3 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,13 +24,14 @@ import os import struct +from io import BytesIO from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import o8 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 2 and i16(prefix) == 474 @@ -52,8 +53,10 @@ class SgiImageFile(ImageFile.ImageFile): format = "SGI" format_description = "SGI Image File Format" - def _open(self): + def _open(self) -> None: # HEAD + assert self.fp is not None + headlen = 512 s = self.fp.read(headlen) @@ -122,7 +125,7 @@ def _open(self): ] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -168,8 +171,8 @@ def _save(im, fp, filename): # Maximum Byte value (255 = 8bits per pixel) pinmax = 255 # Image name (79 characters max, truncated below in write) - img_name = os.path.splitext(os.path.basename(filename))[0] - img_name = img_name.encode("ascii", "ignore") + filename = os.path.basename(filename) + img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") # Standard representation of pixel in the file colormap = 0 fp.write(struct.pack(">h", magic_number)) @@ -201,7 +204,10 @@ def _save(im, fp, filename): class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + assert self.im is not None + rawmode, stride, orientation = self.args pagesize = self.state.xsize * self.state.ysize zsize = len(self.mode) From e2aa0fd4996b85334df3831c2df9bf2d5f68dadd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 12:55:48 +1100 Subject: [PATCH 0082/1178] Changed ops to be static --- src/PIL/ImageMath.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index bc3318c04f4..a7652f237ed 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -224,10 +224,15 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: return _Operand(self.im.convert(mode)) -ops = {} -for k, v in list(globals().items()): - if k[:10] == "imagemath_": - ops[k[10:]] = v +ops = { + "int": imagemath_int, + "float": imagemath_float, + "equal": imagemath_equal, + "notequal": imagemath_notequal, + "min": imagemath_min, + "max": imagemath_max, + "convert": imagemath_convert, +} def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: @@ -244,7 +249,7 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: """ # build execution namespace - args = ops.copy() + args: dict[str, Any] = ops.copy() for k in list(_dict.keys()) + list(kw.keys()): if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" From 44e77a22b572a3ffe158a8b4b63c218ae549b8ac Mon Sep 17 00:00:00 2001 From: FangFuxin <38530078+lajiyuan@users.noreply.github.com> Date: Fri, 12 Jan 2024 15:46:49 +0800 Subject: [PATCH 0083/1178] Fix png image plugin load_end func handle truncated file. --- Tests/images/end_trunc_file.png | Bin 0 -> 30339 bytes Tests/test_file_png.py | 9 +++++++++ src/PIL/PngImagePlugin.py | 8 +++++++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Tests/images/end_trunc_file.png diff --git a/Tests/images/end_trunc_file.png b/Tests/images/end_trunc_file.png new file mode 100644 index 0000000000000000000000000000000000000000..5e88c5e4fa2a6ac010219a2469b83efa21102f0e GIT binary patch literal 30339 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 mOz9jWq7NYmRv5v_Ot0CrAEL_t(ca^tlC literal 0 HcmV?d00001 diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ff386211061..9b6a4c5e6ba 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -777,6 +777,15 @@ class MyStdOut: mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + + def test_end_truncated_file(self): + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + with Image.open("Tests/images/end_trunc_file.png") as im: + assert_image_equal_tofile(im, "Tests/images/end_trunc_file.png") + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 823f1249285..1248fb78529 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -981,7 +981,13 @@ def load_end(self): except EOFError: if cid == b"fdAT": length -= 4 - ImageFile._safe_read(self.fp, length) + try: + ImageFile._safe_read(self.fp, length) + except OSError as e: + if ImageFile.LOAD_TRUNCATED_IMAGES: + break + else: + raise e except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) From b2711c3e8b019d0d069d58069912abd4ecb99e61 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jan 2024 07:36:57 +0000 Subject: [PATCH 0084/1178] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_png.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9b6a4c5e6ba..3c285b0779e 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -777,7 +777,7 @@ class MyStdOut: mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - + def test_end_truncated_file(self): ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -787,7 +787,6 @@ def test_end_truncated_file(self): ImageFile.LOAD_TRUNCATED_IMAGES = False - @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") class TestTruncatedPngPLeaks(PillowLeakTestCase): From fe7b6d9e80ae8ee65d2b4e615971a00690b876fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 18:39:33 +1100 Subject: [PATCH 0085/1178] Corrected expected image path --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 3c285b0779e..aa2aac906f7 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -782,7 +782,7 @@ def test_end_truncated_file(self): ImageFile.LOAD_TRUNCATED_IMAGES = True try: with Image.open("Tests/images/end_trunc_file.png") as im: - assert_image_equal_tofile(im, "Tests/images/end_trunc_file.png") + assert_image_equal_tofile(im, "Tests/images/hopper.png") finally: ImageFile.LOAD_TRUNCATED_IMAGES = False From 62e6d62518f21333fbde364d7b7a57e25d39061b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 18:49:25 +1100 Subject: [PATCH 0086/1178] Test error is raised without LOAD_TRUNCATED_IMAGES --- .../{end_trunc_file.png => truncated_end_chunk.png} | Bin Tests/test_file_png.py | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) rename Tests/images/{end_trunc_file.png => truncated_end_chunk.png} (100%) diff --git a/Tests/images/end_trunc_file.png b/Tests/images/truncated_end_chunk.png similarity index 100% rename from Tests/images/end_trunc_file.png rename to Tests/images/truncated_end_chunk.png diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index aa2aac906f7..0884ddcc35d 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -778,10 +778,14 @@ class MyStdOut: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_end_truncated_file(self): + def test_truncated_end_chunk(self): + with Image.open("Tests/images/truncated_end_chunk.png") as im: + with pytest.raises(OSError): + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = True try: - with Image.open("Tests/images/end_trunc_file.png") as im: + with Image.open("Tests/images/truncated_end_chunk.png") as im: assert_image_equal_tofile(im, "Tests/images/hopper.png") finally: ImageFile.LOAD_TRUNCATED_IMAGES = False From 54c96df9d6e370d16960ee8a2aee932b41f213f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 08:03:09 +1100 Subject: [PATCH 0087/1178] Added type hints --- src/PIL/TgaImagePlugin.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 65c7484f756..584932d2c7d 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import warnings +from io import BytesIO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -49,8 +50,10 @@ class TgaImageFile(ImageFile.ImageFile): format = "TGA" format_description = "Targa" - def _open(self): + def _open(self) -> None: # process header + assert self.fp is not None + s = self.fp.read(18) id_len = s[0] @@ -151,8 +154,9 @@ def _open(self): except KeyError: pass # cannot decode - def load_end(self): + def load_end(self) -> None: if self._flip_horizontally: + assert self.im is not None self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) @@ -171,7 +175,7 @@ def load_end(self): } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: @@ -194,6 +198,7 @@ def _save(im, fp, filename): warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: + assert im.im is not None palette = im.im.getpalette("RGB", "BGR") colormaplength, colormapentry = len(palette) // 3, 24 else: From 7972332bc59800aa64c23664645fe9f8277cdbf4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 19:22:45 +1100 Subject: [PATCH 0088/1178] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageFile.py | 2 ++ src/PIL/PpmImagePlugin.py | 46 +++++++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ec1cff89636..553f36703b3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -282,7 +282,7 @@ def getmodebandnames(mode): return ImageMode.getmode(mode).bands -def getmodebands(mode): +def getmodebands(mode: str) -> int: """ Gets the number of individual bands for this mode. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 40353da677b..5ba5a6f8277 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -616,6 +616,8 @@ def extents(self): class PyCodec: + fd: io.BytesIO | None + def __init__(self, mode, *args): self.im = None self.state = PyCodecState() diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 9d37dcde099..3e45ba95c84 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -16,6 +16,7 @@ from __future__ import annotations import math +from io import BytesIO from . import Image, ImageFile from ._binary import i16be as i16 @@ -45,7 +46,7 @@ } -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" @@ -57,7 +58,9 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self): + def _read_magic(self) -> bytes: + assert self.fp is not None + magic = b"" # read until whitespace or longest available magic number for _ in range(6): @@ -67,7 +70,9 @@ def _read_magic(self): magic += c return magic - def _read_token(self): + def _read_token(self) -> bytes: + assert self.fp is not None + token = b"" while len(token) <= 10: # read until next whitespace or limit of 10 characters c = self.fp.read(1) @@ -93,7 +98,9 @@ def _read_token(self): raise ValueError(msg) return token - def _open(self): + def _open(self) -> None: + assert self.fp is not None + magic_number = self._read_magic() try: mode = MODES[magic_number] @@ -114,6 +121,8 @@ def _open(self): decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" + + args: str | tuple[str | int, ...] if mode == "1": args = "1;I" elif mode == "F": @@ -151,16 +160,19 @@ def _open(self): class PpmPlainDecoder(ImageFile.PyDecoder): _pulls_fd = True + _comment_spans: bool + + def _read_block(self) -> bytes: + assert self.fd is not None - def _read_block(self): return self.fd.read(ImageFile.SAFEBLOCK) - def _find_comment_end(self, block, start=0): + def _find_comment_end(self, block: bytes, start: int = 0) -> int: a = block.find(b"\n", start) b = block.find(b"\r", start) return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) - def _ignore_comments(self, block): + def _ignore_comments(self, block: bytes) -> bytes: if self._comment_spans: # Finish current comment while block: @@ -194,7 +206,7 @@ def _ignore_comments(self, block): break return block - def _decode_bitonal(self): + def _decode_bitonal(self) -> bytearray: """ This is a separate method because in the plain PBM format, all data tokens are exactly one byte, so the inter-token whitespace is optional. @@ -219,7 +231,7 @@ def _decode_bitonal(self): invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) - def _decode_blocks(self, maxval): + def _decode_blocks(self, maxval: int) -> bytearray: data = bytearray() max_len = 10 out_byte_count = 4 if self.mode == "I" else 1 @@ -227,7 +239,7 @@ def _decode_blocks(self, maxval): bands = Image.getmodebands(self.mode) total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count - half_token = False + half_token = b"" while len(data) != total_bytes: block = self._read_block() # read next block if not block: @@ -241,7 +253,7 @@ def _decode_blocks(self, maxval): if half_token: block = half_token + block # stitch half_token to new block - half_token = False + half_token = b"" tokens = block.split() @@ -259,15 +271,15 @@ def _decode_blocks(self, maxval): raise ValueError(msg) value = int(token) if value > maxval: - msg = f"Channel value too large for this mode: {value}" - raise ValueError(msg) + msg_str = f"Channel value too large for this mode: {value}" + raise ValueError(msg_str) value = round(value / maxval * out_max) data += o32(value) if self.mode == "I" else o8(value) if len(data) == total_bytes: # finished! break return data - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: self._comment_spans = False if self.mode == "1": data = self._decode_bitonal() @@ -283,7 +295,9 @@ def decode(self, buffer): class PpmDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + data = bytearray() maxval = self.args[-1] in_byte_count = 1 if maxval < 256 else 2 @@ -310,7 +324,7 @@ def decode(self, buffer): # -------------------------------------------------------------------- -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": From d8c7af0157269f82106216788e9073b4379f786d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 23:10:37 +1100 Subject: [PATCH 0089/1178] Added type hints to GdImageFile --- src/PIL/GdImageFile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index d84876eb6b6..7bb4736af13 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,6 +27,8 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a """ from __future__ import annotations +from io import BytesIO + from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 @@ -43,8 +45,10 @@ class GdImageFile(ImageFile.ImageFile): format = "GD" format_description = "GD uncompressed images" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(1037) if i16(s) not in [65534, 65535]: @@ -76,7 +80,7 @@ def _open(self): ] -def open(fp, mode="r"): +def open(fp: BytesIO, mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. From 6a85653cc373fd78fdf04e53d3fc38b6d83a7e1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Jan 2024 12:05:54 +1100 Subject: [PATCH 0090/1178] Added type hints --- src/PIL/MpegImagePlugin.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index f4e598ca3a0..b9e9243e59f 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -14,6 +14,8 @@ # from __future__ import annotations +from io import BytesIO + from . import Image, ImageFile from ._binary import i8 @@ -22,15 +24,15 @@ class BitStream: - def __init__(self, fp): + def __init__(self, fp: BytesIO) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 - def next(self): + def next(self) -> int: return i8(self.fp.read(1)) - def peek(self, bits): + def peek(self, bits: int) -> int: while self.bits < bits: c = self.next() if c < 0: @@ -40,13 +42,13 @@ def peek(self, bits): self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 - def skip(self, bits): + def skip(self, bits: int) -> None: while self.bits < bits: self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) self.bits += 8 self.bits = self.bits - bits - def read(self, bits): + def read(self, bits: int) -> int: v = self.peek(bits) self.bits = self.bits - bits return v @@ -61,9 +63,10 @@ class MpegImageFile(ImageFile.ImageFile): format = "MPEG" format_description = "MPEG" - def _open(self): - s = BitStream(self.fp) + def _open(self) -> None: + assert self.fp is not None + s = BitStream(self.fp) if s.read(32) != 0x1B3: msg = "not an MPEG file" raise SyntaxError(msg) From 81b5c5dc68be449478442364a86e391906a61228 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jan 2024 08:37:58 +1100 Subject: [PATCH 0091/1178] Added type hints --- Tests/helper.py | 2 +- Tests/oss-fuzz/fuzz_font.py | 4 ++-- Tests/oss-fuzz/fuzz_pillow.py | 4 ++-- Tests/oss-fuzz/fuzzers.py | 8 ++++---- Tests/oss-fuzz/test_fuzzers.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index b333c2fd4ef..88c1f02a803 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -158,7 +158,7 @@ def assert_tuple_approx_equal(actuals, targets, threshold, msg): assert value, msg + ": " + repr(actuals) + " != " + repr(targets) -def skip_unless_feature(feature): +def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index bc2ba9a7e27..8788d7021d3 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -23,7 +23,7 @@ import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_font(data) except Exception: @@ -32,7 +32,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 545daccb680..e6e99d415a6 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -23,7 +23,7 @@ import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_image(data) except Exception: @@ -32,7 +32,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 3f3c1e38833..3afa952157b 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -5,18 +5,18 @@ from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont -def enable_decompressionbomb_error(): +def enable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True warnings.filterwarnings("ignore") warnings.simplefilter("error", Image.DecompressionBombWarning) -def disable_decompressionbomb_error(): +def disable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = False warnings.resetwarnings() -def fuzz_image(data): +def fuzz_image(data: bytes) -> None: # This will fail on some images in the corpus, as we have many # invalid images in the test suite. with Image.open(io.BytesIO(data)) as im: @@ -25,7 +25,7 @@ def fuzz_image(data): im.save(io.BytesIO(), "BMP") -def fuzz_font(data): +def fuzz_font(data: bytes) -> None: wrapper = io.BytesIO(data) try: font = ImageFont.truetype(wrapper) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a52..028ee71eec8 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -23,7 +23,7 @@ "path", subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), ) -def test_fuzz_images(path): +def test_fuzz_images(path: str) -> None: fuzzers.enable_decompressionbomb_error() try: with open(path, "rb") as f: @@ -54,7 +54,7 @@ def test_fuzz_images(path): @pytest.mark.parametrize( "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") ) -def test_fuzz_fonts(path): +def test_fuzz_fonts(path: str) -> None: if not path: return with open(path, "rb") as f: From 1d63cffdadbf649442804dae288992cdd854a849 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jan 2024 21:50:27 +1100 Subject: [PATCH 0092/1178] Added type hints --- Tests/check_fli_overflow.py | 2 +- Tests/check_imaging_leaks.py | 15 ++++++++++----- Tests/check_j2k_leaks.py | 4 ++-- Tests/check_j2k_overflow.py | 3 ++- Tests/check_jpeg_leaks.py | 6 +++--- Tests/check_large_memory.py | 12 ++++++++---- Tests/check_large_memory_numpy.py | 7 ++++--- Tests/check_libtiff_segfault.py | 2 +- Tests/check_png_dos.py | 8 ++++---- Tests/check_wheel.py | 6 +++--- Tests/helper.py | 4 ++-- 11 files changed, 40 insertions(+), 29 deletions(-) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 0fabcb5d35f..109f8fb5498 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,7 @@ TEST_FILE = "Tests/images/fli_overflow.fli" -def test_fli_overflow(): +def test_fli_overflow() -> None: # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 8c17c051d4b..b0c4f620b93 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -2,6 +2,8 @@ from __future__ import annotations import pytest +from typing import Any, Callable + from PIL import Image from .helper import is_win32 @@ -12,31 +14,34 @@ pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def _get_mem_usage(): +def _get_mem_usage() -> float: from resource import RUSAGE_SELF, getpagesize, getrusage mem = getrusage(RUSAGE_SELF).ru_maxrss return mem * getpagesize() / 1024 / 1024 -def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs): +def _test_leak( + min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any +) -> None: mem_limit = None for i in range(max_iterations): - fn(*args, **kwargs) + fn(*args) mem = _get_mem_usage() if i < min_iterations: mem_limit = mem + 1 continue msg = f"memory usage limit exceeded after {i + 1} iterations" + assert mem_limit is not None assert mem <= mem_limit, msg -def test_leak_putdata(): +def test_leak_putdata() -> None: im = Image.new("RGB", (25, 25)) _test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) -def test_leak_getlist(): +def test_leak_getlist() -> None: im = Image.new("P", (25, 25)) _test_leak( min_iterations, diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 83a12e2c29f..0d0d3a57c2d 100644 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -19,7 +19,7 @@ ] -def test_leak_load(): +def test_leak_load() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) @@ -29,7 +29,7 @@ def test_leak_load(): im.load() -def test_leak_save(): +def test_leak_save() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 982f6ea74d2..ba14964b55f 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,10 +1,11 @@ from __future__ import annotations +from pathlib import PosixPath import pytest from PIL import Image -def test_j2k_overflow(tmp_path): +def test_j2k_overflow(tmp_path: PosixPath) -> None: im = Image.new("RGBA", (1024, 131584)) target = str(tmp_path / "temp.jpc") with pytest.raises(OSError): diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 3cd37c7af7b..e91709a9604 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -110,14 +110,14 @@ [standard_l_qtable, standard_chrominance_qtable], ), ) -def test_qtables_leak(qtables): +def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: im = hopper("RGB") for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) -def test_exif_leak(): +def test_exif_leak() -> None: """ pre patch: @@ -180,7 +180,7 @@ def test_exif_leak(): im.save(test_output, "JPEG", exif=exif) -def test_base_save(): +def test_base_save() -> None: """ base case: MB diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 9b83798d5e7..4d67270765c 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import PosixPath import sys +from types import ModuleType import pytest @@ -15,6 +17,7 @@ # 2.7 and 3.2. +numpy: ModuleType | None try: import numpy except ImportError: @@ -27,23 +30,24 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path, xdim, ydim): +def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) im.save(f) -def test_large(tmp_path): +def test_large(tmp_path: PosixPath) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path): +def test_2gpx(tmp_path: PosixPath) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) @pytest.mark.skipif(numpy is None, reason="Numpy is not installed") -def test_size_greater_than_int(): +def test_size_greater_than_int() -> None: + assert numpy is not None arr = numpy.ndarray(shape=(16394, 16394)) Image.fromarray(arr) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 0ff3de8dcc7..d1cbad887aa 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations +from pathlib import PosixPath import sys import pytest @@ -23,7 +24,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path, xdim, ydim): +def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) f = str(tmp_path / "temp.png") @@ -31,11 +32,11 @@ def _write_png(tmp_path, xdim, ydim): im.save(f) -def test_large(tmp_path): +def test_large(tmp_path: PosixPath) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path): +def test_2gpx(tmp_path: PosixPath) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index ee1d7d11f0c..f1c77efc16b 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -6,7 +6,7 @@ TEST_FILE = "Tests/images/libtiff_segfault.tif" -def test_libtiff_segfault(): +def test_libtiff_segfault() -> None: """This test should not segfault. It will on Pillow <= 3.1.0 and libtiff >= 4.0.0 """ diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 292fe4b7f09..a3d50fa57e5 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -7,7 +7,7 @@ TEST_FILE = "Tests/images/png_decompression_dos.png" -def test_ignore_dos_text(): +def test_ignore_dos_text() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -23,7 +23,7 @@ def test_ignore_dos_text(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" -def test_dos_text(): +def test_dos_text() -> None: try: im = Image.open(TEST_FILE) im.load() @@ -35,7 +35,7 @@ def test_dos_text(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" -def test_dos_total_memory(): +def test_dos_total_memory() -> None: im = Image.new("L", (1, 1)) compressed_data = zlib.compress(b"a" * 1024 * 1023) @@ -52,7 +52,7 @@ def test_dos_total_memory(): try: im2 = Image.open(b) except ValueError as msg: - assert "Too much memory" in msg + assert "Too much memory" in str(msg) return total_len = 0 diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index afe4cc3eeaa..969e596b45a 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -4,7 +4,7 @@ from PIL import features -def test_wheel_modules(): +def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} # tkinter is not available in cibuildwheel installed CPython on Windows @@ -18,13 +18,13 @@ def test_wheel_modules(): assert set(features.get_supported_modules()) == expected_modules -def test_wheel_codecs(): +def test_wheel_codecs() -> None: expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} assert set(features.get_supported_codecs()) == expected_codecs -def test_wheel_features(): +def test_wheel_features() -> None: expected_features = { "webp_anim", "webp_mux", diff --git a/Tests/helper.py b/Tests/helper.py index 88c1f02a803..34839422e2f 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -238,7 +238,7 @@ def tostring(im, string_format, **options): return out.getvalue() -def hopper(mode=None, cache={}): +def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image: if mode is None: # Always return fresh not-yet-loaded version of image. # Operations on not-yet-loaded images is separate class of errors @@ -323,7 +323,7 @@ def is_ppc64le(): return platform.machine() == "ppc64le" -def is_win32(): +def is_win32() -> bool: return sys.platform.startswith("win32") From 9b6c1e3763000329e80173b99507e10aa0569b43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jan 2024 11:13:06 +1100 Subject: [PATCH 0093/1178] Added type hints --- Tests/helper.py | 157 +++++++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 68 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 88c1f02a803..12a4fed3811 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -14,47 +14,46 @@ import pytest from packaging.version import parse as parse_version +from typing import Any, Callable, Sequence from PIL import Image, ImageMath, features logger = logging.getLogger(__name__) - -HAS_UPLOADER = False - +uploader = None if os.environ.get("SHOW_ERRORS"): - # local img.show for errors. - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - a.show() - b.show() - + uploader = "show" elif "GITHUB_ACTIONS" in os.environ: - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - dir_errors = os.path.join(os.path.dirname(__file__), "errors") - os.makedirs(dir_errors, exist_ok=True) - tmpdir = tempfile.mkdtemp(dir=dir_errors) - a.save(os.path.join(tmpdir, "a.png")) - b.save(os.path.join(tmpdir, "b.png")) - return tmpdir - + uploader = "github_actions" else: try: import test_image_results - HAS_UPLOADER = True + uploader = "aws" except ImportError: pass -def convert_to_comparable(a, b): +def upload(a: Image.Image, b: Image.Image) -> str | None: + if uploader == "show": + # local img.show for errors. + a.show() + b.show() + elif uploader == "github_actions": + dir_errors = os.path.join(os.path.dirname(__file__), "errors") + os.makedirs(dir_errors, exist_ok=True) + tmpdir = tempfile.mkdtemp(dir=dir_errors) + a.save(os.path.join(tmpdir, "a.png")) + b.save(os.path.join(tmpdir, "b.png")) + return tmpdir + elif uploader == "aws": + return test_image_results.upload(a, b) + return None + + +def convert_to_comparable( + a: Image.Image, b: Image.Image +) -> tuple[Image.Image, Image.Image]: new_a, new_b = a, b if a.mode == "P": new_a = Image.new("L", a.size) @@ -67,14 +66,16 @@ def convert_to_comparable(a, b): return new_a, new_b -def assert_deep_equal(a, b, msg=None): +def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: assert a == b, msg -def assert_image(im, mode, size, msg=None): +def assert_image( + im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None +) -> None: if mode is not None: assert im.mode == mode, ( msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" @@ -86,13 +87,13 @@ def assert_image(im, mode, size, msg=None): ) -def assert_image_equal(a, b, msg=None): +def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" if a.tobytes() != b.tobytes(): - if HAS_UPLOADER: + if uploader: try: - url = test_image_results.upload(a, b) + url = upload(a, b) logger.error("URL for test images: %s", url) except Exception: pass @@ -100,14 +101,18 @@ def assert_image_equal(a, b, msg=None): pytest.fail(msg or "got different content") -def assert_image_equal_tofile(a, filename, msg=None, mode=None): +def assert_image_equal_tofile( + a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_equal(a, img, msg) -def assert_image_similar(a, b, epsilon, msg=None): +def assert_image_similar( + a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None +) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -125,37 +130,43 @@ def assert_image_similar(a, b, epsilon, msg=None): + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" ) except Exception as e: - if HAS_UPLOADER: + if uploader: try: - url = test_image_results.upload(a, b) + url = upload(a, b) logger.exception("URL for test images: %s", url) except Exception: pass raise e -def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None): +def assert_image_similar_tofile( + a: Image.Image, + filename: str, + epsilon: float, + msg: str | None = None, + mode: str | None = None, +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_similar(a, img, epsilon, msg) -def assert_all_same(items, msg=None): +def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) == len(items), msg -def assert_not_all_same(items, msg=None): +def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg -def assert_tuple_approx_equal(actuals, targets, threshold, msg): +def assert_tuple_approx_equal( + actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str +) -> None: """Tests if actuals has values within threshold from targets""" - value = True for i, target in enumerate(targets): - value *= target - threshold <= actuals[i] <= target + threshold - - assert value, msg + ": " + repr(actuals) + " != " + repr(targets) + if not (target - threshold <= actuals[i] <= target + threshold): + pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) def skip_unless_feature(feature: str) -> pytest.MarkDecorator: @@ -163,17 +174,24 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator: return pytest.mark.skipif(not features.check(feature), reason=reason) -def skip_unless_feature_version(feature, version_required, reason=None): +def skip_unless_feature_version( + feature: str, required: str, reason: str | None = None +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.skip(f"{feature} not available") if reason is None: - reason = f"{feature} is older than {version_required}" - version_required = parse_version(version_required) + reason = f"{feature} is older than {required}" + version_required = parse_version(required) version_available = parse_version(features.version(feature)) return pytest.mark.skipif(version_available < version_required, reason=reason) -def mark_if_feature_version(mark, feature, version_blacklist, reason=None): +def mark_if_feature_version( + mark: pytest.MarkDecorator, + feature: str, + version_blacklist: str, + reason: str | None = None, +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.pil_noop_mark() if reason is None: @@ -194,7 +212,7 @@ class PillowLeakTestCase: iterations = 100 # count mem_limit = 512 # k - def _get_mem_usage(self): + def _get_mem_usage(self) -> float: """ Gets the RUSAGE memory usage, returns in K. Encapsulates the difference between macOS and Linux rss reporting @@ -216,7 +234,7 @@ def _get_mem_usage(self): # This is the maximum resident set size used (in kilobytes). return mem # Kb - def _test_leak(self, core): + def _test_leak(self, core: Callable[[], None]) -> None: start_mem = self._get_mem_usage() for cycle in range(self.iterations): core() @@ -228,17 +246,17 @@ def _test_leak(self, core): # helpers -def fromstring(data): +def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im, string_format, **options): +def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() -def hopper(mode=None, cache={}): +def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image: if mode is None: # Always return fresh not-yet-loaded version of image. # Operations on not-yet-loaded images is separate class of errors @@ -259,29 +277,31 @@ def hopper(mode=None, cache={}): return im.copy() -def djpeg_available(): +def djpeg_available() -> bool: if shutil.which("djpeg"): try: subprocess.check_call(["djpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - return False + pass + return False -def cjpeg_available(): +def cjpeg_available() -> bool: if shutil.which("cjpeg"): try: subprocess.check_call(["cjpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - return False + pass + return False -def netpbm_available(): +def netpbm_available() -> bool: return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) -def magick_command(): +def magick_command() -> list[str] | None: if sys.platform == "win32": magickhome = os.environ.get("MAGICK_HOME") if magickhome: @@ -298,47 +318,48 @@ def magick_command(): return imagemagick if graphicsmagick and shutil.which(graphicsmagick[0]): return graphicsmagick + return None -def on_appveyor(): +def on_appveyor() -> bool: return "APPVEYOR" in os.environ -def on_github_actions(): +def on_github_actions() -> bool: return "GITHUB_ACTIONS" in os.environ -def on_ci(): +def on_ci() -> bool: # GitHub Actions and AppVeyor have "CI" return "CI" in os.environ -def is_big_endian(): +def is_big_endian() -> bool: return sys.byteorder == "big" -def is_ppc64le(): +def is_ppc64le() -> bool: import platform return platform.machine() == "ppc64le" -def is_win32(): +def is_win32() -> bool: return sys.platform.startswith("win32") -def is_pypy(): +def is_pypy() -> bool: return hasattr(sys, "pypy_translation_info") -def is_mingw(): +def is_mingw() -> bool: return sysconfig.get_platform() == "mingw" class CachedProperty: - def __init__(self, func): + def __init__(self, func: Callable[[Any], None]) -> None: self.func = func - def __get__(self, instance, cls=None): + def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: result = instance.__dict__[self.func.__name__] = self.func(instance) return result From 5dc3de7974833bc8a0e2ecb389c6c183dd1a1d21 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 06:07:48 +0000 Subject: [PATCH 0094/1178] Update actions/cache action to v4 --- .github/workflows/lint.yml | 2 +- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9069fc615f3..cc4760288e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: pre-commit cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7244315ac15..9c3eb092417 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -95,7 +95,7 @@ jobs: python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'C:\cygwin\home\runneradmin\.cache\pip' key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 372f97fd667..8cad7a8b281 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -89,7 +89,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: winbuild\build key: From 74af933a9f0441e1285c398675dc3eca1de5406c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 20 Jan 2024 10:08:14 +0100 Subject: [PATCH 0095/1178] Link to stable setuptools documentation Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index 26d0da0a35a..f40982cd546 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -97,7 +97,7 @@ To build a wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . -.. _editable mode: https://setuptools.pypa.io/en/latest/userguide/development_mode.html +.. _editable mode: https://setuptools.pypa.io/en/stable/userguide/development_mode.html Testing Pillow -------------- From f7701e6596c9a2e7c9ed886621f01b3861d70990 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jan 2024 21:04:57 +1100 Subject: [PATCH 0096/1178] Do not log URL of test images if there is no URL --- Tests/helper.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 12a4fed3811..670c93633d7 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -91,12 +91,12 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" if a.tobytes() != b.tobytes(): - if uploader: - try: - url = upload(a, b) + try: + url = upload(a, b) + if url: logger.error("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass pytest.fail(msg or "got different content") @@ -130,12 +130,12 @@ def assert_image_similar( + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" ) except Exception as e: - if uploader: - try: - url = upload(a, b) + try: + url = upload(a, b) + if url: logger.exception("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass raise e From 99d851957fec5394bbc9d61ceff57f534bd1b873 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:23:08 +1100 Subject: [PATCH 0097/1178] Return early Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 670c93633d7..e4ed9c551ae 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -283,7 +283,7 @@ def djpeg_available() -> bool: subprocess.check_call(["djpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - pass + return False return False @@ -293,7 +293,7 @@ def cjpeg_available() -> bool: subprocess.check_call(["cjpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - pass + return False return False From 970bd102ba0304adeb5351cc09c616ab23535d2c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:24:34 +1100 Subject: [PATCH 0098/1178] Updated type hint Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index e4ed9c551ae..4d2d27226ae 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -66,7 +66,7 @@ def convert_to_comparable( return new_a, new_b -def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: +def assert_deep_equal(a: Sequence[Any], b: Sequence[Any], msg: str | None = None) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: From 9454c28f0f8363d1316b1dfec0f23404f9e36082 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:24:54 +0000 Subject: [PATCH 0099/1178] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index 4d2d27226ae..203d20053c3 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -66,7 +66,9 @@ def convert_to_comparable( return new_a, new_b -def assert_deep_equal(a: Sequence[Any], b: Sequence[Any], msg: str | None = None) -> None: +def assert_deep_equal( + a: Sequence[Any], b: Sequence[Any], msg: str | None = None +) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: From a18cee35ff4362c834d0fa43277ee5b02ab9c693 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:26:31 +1100 Subject: [PATCH 0100/1178] Updated import order Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index 203d20053c3..99170c765dd 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -11,10 +11,10 @@ import sysconfig import tempfile from io import BytesIO +from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version -from typing import Any, Callable, Sequence from PIL import Image, ImageMath, features From eba0be98ecea7150c691e05bf6faa3c73c121689 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:22:04 +0200 Subject: [PATCH 0101/1178] isort Tests --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 789df6f5e67..b1ce9cf1d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,7 +119,6 @@ extend-ignore = [ ] [tool.ruff.per-file-ignores] -"Tests/*.py" = ["I001"] "Tests/oss-fuzz/fuzz_font.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] From 53c3cd9f8e91afc038b64b14cd63d1eb592f717a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:23:03 +0200 Subject: [PATCH 0102/1178] isort Tests --- Tests/bench_cffi_access.py | 1 + Tests/check_fli_overflow.py | 1 + Tests/check_imaging_leaks.py | 1 + Tests/check_j2k_leaks.py | 1 + Tests/check_j2k_overflow.py | 1 + Tests/check_jp2_overflow.py | 1 - Tests/check_jpeg_leaks.py | 1 + Tests/check_large_memory.py | 1 + Tests/check_large_memory_numpy.py | 1 + Tests/check_libtiff_segfault.py | 1 + Tests/check_png_dos.py | 1 + Tests/check_release_notes.py | 1 + Tests/check_wheel.py | 1 + Tests/conftest.py | 1 + Tests/createfontdatachunk.py | 1 + Tests/oss-fuzz/fuzzers.py | 1 + Tests/oss-fuzz/test_fuzzers.py | 1 + Tests/test_000_sanity.py | 1 + Tests/test_binary.py | 1 + Tests/test_bmp_reference.py | 1 + Tests/test_box_blur.py | 1 + Tests/test_color_lut.py | 1 + Tests/test_core_resources.py | 1 + Tests/test_decompression_bomb.py | 1 + Tests/test_deprecate.py | 1 + Tests/test_features.py | 1 + Tests/test_file_apng.py | 1 + Tests/test_file_blp.py | 1 + Tests/test_file_bmp.py | 1 + Tests/test_file_bufrstub.py | 1 + Tests/test_file_container.py | 1 + Tests/test_file_cur.py | 1 + Tests/test_file_dcx.py | 1 + Tests/test_file_dds.py | 1 + Tests/test_file_eps.py | 1 + Tests/test_file_fits.py | 1 + Tests/test_file_fli.py | 1 + Tests/test_file_fpx.py | 1 + Tests/test_file_ftex.py | 1 + Tests/test_file_gbr.py | 1 + Tests/test_file_gd.py | 1 + Tests/test_file_gif.py | 1 + Tests/test_file_gimpgradient.py | 1 + Tests/test_file_gimppalette.py | 1 + Tests/test_file_gribstub.py | 1 + Tests/test_file_hdf5stub.py | 1 + Tests/test_file_icns.py | 1 + Tests/test_file_ico.py | 1 + Tests/test_file_im.py | 1 + Tests/test_file_imt.py | 1 + Tests/test_file_iptc.py | 1 + Tests/test_file_jpeg.py | 1 + Tests/test_file_jpeg2k.py | 1 + Tests/test_file_libtiff.py | 1 + Tests/test_file_libtiff_small.py | 1 + Tests/test_file_mcidas.py | 1 + Tests/test_file_mic.py | 1 + Tests/test_file_mpo.py | 1 + Tests/test_file_msp.py | 1 + Tests/test_file_palm.py | 1 + Tests/test_file_pcd.py | 1 + Tests/test_file_pcx.py | 1 + Tests/test_file_pdf.py | 1 + Tests/test_file_pixar.py | 1 + Tests/test_file_png.py | 1 + Tests/test_file_ppm.py | 1 + Tests/test_file_psd.py | 1 + Tests/test_file_qoi.py | 1 + Tests/test_file_sgi.py | 1 + Tests/test_file_spider.py | 1 + Tests/test_file_sun.py | 1 + Tests/test_file_tar.py | 1 + Tests/test_file_tga.py | 1 + Tests/test_file_tiff.py | 1 + Tests/test_file_tiff_metadata.py | 1 + Tests/test_file_wal.py | 1 + Tests/test_file_webp.py | 1 + Tests/test_file_webp_alpha.py | 1 + Tests/test_file_webp_animated.py | 1 + Tests/test_file_webp_lossless.py | 1 + Tests/test_file_webp_metadata.py | 1 + Tests/test_file_wmf.py | 1 + Tests/test_file_xbm.py | 1 + Tests/test_file_xpm.py | 1 + Tests/test_file_xvthumb.py | 1 + Tests/test_font_bdf.py | 1 + Tests/test_font_crash.py | 1 + Tests/test_font_leaks.py | 1 + Tests/test_font_pcf.py | 1 + Tests/test_font_pcf_charsets.py | 1 + Tests/test_fontfile.py | 1 + Tests/test_format_hsv.py | 1 + Tests/test_format_lab.py | 1 + Tests/test_image.py | 1 + Tests/test_image_access.py | 1 + Tests/test_image_array.py | 1 + Tests/test_image_convert.py | 1 + Tests/test_image_copy.py | 1 + Tests/test_image_crop.py | 1 + Tests/test_image_draft.py | 1 + Tests/test_image_entropy.py | 1 + Tests/test_image_filter.py | 1 + Tests/test_image_frombytes.py | 1 + Tests/test_image_fromqimage.py | 1 + Tests/test_image_getbands.py | 1 + Tests/test_image_getbbox.py | 1 + Tests/test_image_getcolors.py | 1 + Tests/test_image_getdata.py | 1 + Tests/test_image_getextrema.py | 1 + Tests/test_image_getim.py | 1 + Tests/test_image_getpalette.py | 1 + Tests/test_image_getprojection.py | 1 + Tests/test_image_histogram.py | 1 + Tests/test_image_load.py | 1 + Tests/test_image_mode.py | 1 + Tests/test_image_paste.py | 1 + Tests/test_image_point.py | 1 + Tests/test_image_putalpha.py | 1 + Tests/test_image_putdata.py | 1 + Tests/test_image_putpalette.py | 1 + Tests/test_image_quantize.py | 1 + Tests/test_image_reduce.py | 1 + Tests/test_image_resample.py | 1 + Tests/test_image_resize.py | 1 + Tests/test_image_rotate.py | 1 + Tests/test_image_split.py | 1 + Tests/test_image_thumbnail.py | 1 + Tests/test_image_tobitmap.py | 1 + Tests/test_image_tobytes.py | 1 + Tests/test_image_transform.py | 1 + Tests/test_image_transpose.py | 1 + Tests/test_imagechops.py | 1 + Tests/test_imagecms.py | 1 + Tests/test_imagecolor.py | 1 + Tests/test_imagedraw.py | 1 + Tests/test_imagedraw2.py | 1 + Tests/test_imageenhance.py | 1 + Tests/test_imagefile.py | 1 + Tests/test_imagefont.py | 1 + Tests/test_imagefontctl.py | 1 + Tests/test_imagefontpil.py | 6 ++++-- Tests/test_imagegrab.py | 1 + Tests/test_imagemath.py | 1 + Tests/test_imagemorph.py | 1 + Tests/test_imageops.py | 1 + Tests/test_imageops_usm.py | 1 + Tests/test_imagepalette.py | 1 + Tests/test_imagepath.py | 1 + Tests/test_imageqt.py | 1 + Tests/test_imagesequence.py | 1 + Tests/test_imageshow.py | 1 + Tests/test_imagestat.py | 1 + Tests/test_imagetk.py | 1 + Tests/test_imagewin.py | 1 + Tests/test_imagewin_pointers.py | 1 + Tests/test_lib_image.py | 1 + Tests/test_lib_pack.py | 1 + Tests/test_locale.py | 1 + Tests/test_main.py | 1 + Tests/test_map.py | 1 + Tests/test_mode_i16.py | 1 + Tests/test_numpy.py | 1 + Tests/test_pdfparser.py | 1 + Tests/test_pickle.py | 1 + Tests/test_psdraw.py | 1 + Tests/test_pyroma.py | 1 + Tests/test_qt_image_qapplication.py | 1 + Tests/test_qt_image_toqimage.py | 1 + Tests/test_sgi_crash.py | 1 + Tests/test_shell_injection.py | 1 + Tests/test_tiff_ifdrational.py | 1 + Tests/test_uploader.py | 1 + Tests/test_util.py | 1 + Tests/test_webp_leaks.py | 1 + 174 files changed, 176 insertions(+), 3 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 8a37c7d5157..ad15a9739b2 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,4 +1,5 @@ from __future__ import annotations + import time from PIL import PyAccess diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 0fabcb5d35f..8cb6ac0a6a8 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image TEST_FILE = "Tests/images/fli_overflow.fli" diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 8c17c051d4b..eed326a4ca8 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 83a12e2c29f..249c92cef86 100644 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 982f6ea74d2..6f3aa437ea1 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index 9afbff112b0..5adbb84b69c 100755 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -14,7 +14,6 @@ # version. from __future__ import annotations - from PIL import Image repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 3cd37c7af7b..147deb28593 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 9b83798d5e7..fb336f6e142 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 0ff3de8dcc7..d91d615f8f8 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index ee1d7d11f0c..17b5336286f 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 292fe4b7f09..7b0ed242435 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,4 +1,5 @@ from __future__ import annotations + import zlib from io import BytesIO diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py index ebfaffa47e1..cf414d7fff0 100644 --- a/Tests/check_release_notes.py +++ b/Tests/check_release_notes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from pathlib import Path diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index afe4cc3eeaa..daf9e621148 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from PIL import features diff --git a/Tests/conftest.py b/Tests/conftest.py index cd64bd755f0..ac618c5b945 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 2e990b70916..41c76f87eac 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations + import base64 import os diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 3f3c1e38833..5c00e9716cb 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import warnings diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a52..bf4fb45ff98 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -1,4 +1,5 @@ from __future__ import annotations + import subprocess import sys diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index c582dfad3e4..f64216bca8e 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 62da2663668..41fb93fcf48 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import _binary diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index bed8dc3a899..0da41e85845 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import warnings diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index e798cba3d4f..461e6aaacb3 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 448ba2fac80..fcd1169ef81 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,4 +1,5 @@ from __future__ import annotations + from array import array import pytest diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 5275652f66a..d3f76fdb1b9 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 391948d40c7..d3049eff124 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index d45a6603c54..6c7f509a795 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import _deprecate diff --git a/Tests/test_features.py b/Tests/test_features.py index 8f0e4b4184a..b90c1d25f2b 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import re diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index e2c4569cee5..23263b5d459 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageSequence, PngImagePlugin diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 4c1e38d1ddf..27ff7ab6640 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 4cc92c5f684..225fb28ba51 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 5780232a2d6..45081832e68 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import BufrStubImagePlugin, Image diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 0da5d3824b0..95a5b2337d8 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ContainerIO, Image diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 08c3257f9d1..27b2bc91489 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import CurImagePlugin, Image diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 25e4badbc92..cba7c10bf76 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index a605c8399d8..7064b74c07b 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,5 +1,6 @@ """Test DdsImagePlugin""" from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8b48e83ad2c..8def9a43511 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 1383f9c5ca3..7444eb673cb 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 10bf36cc290..00377e0c92d 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index af3b7981561..d710070c01b 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index a494c8029c9..0f9154e3d09 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import FtexImagePlugin, Image diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 7dfe0539673..d84004e1483 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GbrImagePlugin, Image diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index ec80c54a122..e7db54fb44c 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GdImageFile, UnidentifiedImageError diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 78b77e9743a..3e19940aab9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings from io import BytesIO diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index d5be46dc39e..ceea1edd34c 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import GimpGradientFile, ImagePalette diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 775d3b7cdae..28855c28acc 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL.GimpPaletteFile import GimpPaletteFile diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index d962e85a436..a4ce6dde674 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GribStubImagePlugin, Image diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 9c776b712ee..72764461730 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Hdf5StubImagePlugin, Image diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index c62fffc5be8..314fa800868 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os import warnings diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index de9fa353adb..99b3048d1e0 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 0cb26d06a51..a031b3e887c 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,4 +1,5 @@ from __future__ import annotations + import filecmp import warnings diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index 3db4885586a..aa13d4407bc 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c44a08f526a..a2c50ecefdf 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from io import BytesIO, StringIO diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 979c7e33d00..232e51f9126 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import re import warnings diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index aaa4104e57c..94b02c9ff52 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import re from io import BytesIO diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 65adf449dcc..494253c87c1 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,4 +1,5 @@ from __future__ import annotations + import base64 import io import itertools diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 9501c55a6b8..171e4a3f866 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 4b31aaa7857..73eba5cc861 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, McIdasImagePlugin diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index e7ea39ea918..8c43f7d7ab7 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index da62bc6d429..c7121ea28c9 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings from io import BytesIO diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f4e357ae0fd..9037ea33b54 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 735840de4cb..eba69415395 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os.path import subprocess diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 596a3414f79..1a37c6ab31d 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index f42ec4a6894..2565e0b6ddc 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFile, PcxImagePlugin diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9e07d9ed014..30c54c963cb 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os import os.path diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index 63779f202cb..c6ddc54e714 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, PixarImagePlugin diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ff386211061..ae2a4772b60 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,5 @@ from __future__ import annotations + import re import sys import warnings diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d8e259b1cf8..32de42ed45d 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from io import BytesIO diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b163..16f049602bc 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index b7c9457294d..6dc468754ef 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, QoiImagePlugin diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 13698276b8c..bc45bbfd34d 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, SgiImagePlugin diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index f2109875478..42d833fb25c 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,5 @@ from __future__ import annotations + import tempfile import warnings from io import BytesIO diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 874b37b5285..41f3b7d98f3 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 4470823cdbd..58226c33062 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index d0f228573bb..eafb61d30af 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os from glob import glob from itertools import product diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a50f50e5e96..f0995679b1f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import warnings from io import BytesIO diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index ee69681854e..06689bc9092 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import struct diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 0b84d0320aa..7acec975942 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import WalImageFile from .helper import assert_image_equal_tofile diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index c91818ef64e..c49418ce3c5 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import re import sys diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 79d01a4446a..cfda35a0962 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 22acb4be68f..426fe7a0293 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 6acf58ac3f5..08c80973a74 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index a7b7bbcf6ee..deaf5e380db 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 596dc8ba181..6e1d4c1361f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, WmfImagePlugin diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index b086ffd683f..69a0a1b38d8 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 265feab4294..529a4558073 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XpmImagePlugin diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index 5848995c1aa..b87494eba18 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XVThumbImagePlugin diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 1e5eff2f15d..e5e85618651 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import BdfFontFile, FontFile diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 388ee711861..e3c72c1aebd 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 6a038bb4038..4e29a856bc9 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image, ImageDraw, ImageFont from .helper import PillowLeakTestCase, skip_unless_feature diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 4365b931066..e6abede0787 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 950e5029ff5..4c2d7185e3c 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index ce1e02f63e0..eda8fb81283 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import FontFile diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index fd47fae39ba..6395ae4aad8 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,4 +1,5 @@ from __future__ import annotations + import colorsys import itertools diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index c7610ce8a6c..a55620e09ab 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image.py b/Tests/test_image.py index 80f6583d8d9..dd989ad99b5 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import logging import os diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4a794371d67..4ae56fae0cc 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import subprocess import sys diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index b3e5d9e3e08..0dacb3157a2 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 7c17040d30b..d4ddc2a31b2 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index abf5f846f89..3a26ef96e74 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import pytest diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 0bb54e5d811..5e02a3b0dfc 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 774272dd1f7..08c40af1f16 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import fromstring, skip_unless_feature, tostring diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 031fceda3fa..fce16122409 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 5bd7ee0d273..3fa5dd2423a 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 017da499d2c..5d5e9f2dfc9 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index b3ca43bde6d..76b576da56b 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index e7701dbc460..64339e2cd85 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 9e792cfdf7c..b18a7202e38 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index dea3a60a112..17460fa93f5 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 873cc65bf91..ace64279b8c 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index b17c8a786dd..6bbc4da9a01 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index e969c8164a2..bc8a7485eb0 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index a5be972d3ed..4340f46f619 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index aa47be3b2e0..e90f5f5056f 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 7ba2f10b765..3ac6649e074 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 17847c4fd9c..36f8ba575d8 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,4 +1,5 @@ from __future__ import annotations + import logging import os diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index ad90d1250dc..3c1d494fab2 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMode diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 0b87f607286..fd117f9dbc9 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index fce45ec4f0e..2232b94429d 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, hopper diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 0ba7e5919a5..c44b048d523 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index d3cb13e2e97..2648af8fa2e 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from array import array diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index de2d9024213..43b65be2b17 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 54c567aae6c..1475b027bd8 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index a4d0f510761..ba9100415a2 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath, ImageMode diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 5a578dba5c4..af730dce13d 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,4 +1,5 @@ from __future__ import annotations + from contextlib import contextmanager import pytest diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 0d3b43ee29f..aedcf4a09b5 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -2,6 +2,7 @@ Tests for resize functionality. """ from __future__ import annotations + from itertools import permutations import pytest diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 0931aa32d98..e63fef2c152 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 707508250f6..c39a100e777 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, features diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 9e6796ca299..7fa5692aa7d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 156b9919d27..89a41cf8ec2 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, fromstring, hopper diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index f6042bca527..8f15adac065 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index f5d5ab70408..0fe9fd1d5f5 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,4 +1,5 @@ from __future__ import annotations + import math import pytest diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 66a2d9e2955..01bf5a83918 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL.Image import Transpose diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 8e3a738d708..2f0614385aa 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 810394e6f5f..03332699a1d 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,4 +1,5 @@ from __future__ import annotations + import datetime import os import re diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index c0ffd2ebf0b..b602172b642 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageColor diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 379fe78cd8a..69aab48912b 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,4 +1,5 @@ from __future__ import annotations + import contextlib import os.path diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index d729af14d3a..004c2d768fd 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os.path import pytest diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index f4e4d59be32..e3d8a7ab251 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageEnhance diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 4804a554f83..99731f35208 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 807d581edf0..d2c87d42aaa 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import os import re diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index bea532b051e..09e68ea488a 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 9e085510149..be4be1c546e 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,9 +1,11 @@ from __future__ import annotations + import struct -import pytest from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, features, _util +import pytest + +from PIL import Image, ImageDraw, ImageFont, _util, features from .helper import assert_image_equal_tofile diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index b7683ec1817..9d3d40398f6 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import shutil import subprocess diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 9281de6f66a..622ad27eacb 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 64a1785ea97..0708ee63905 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,5 +1,6 @@ # Test the ImageMorphology functionality from __future__ import annotations + import pytest from PIL import Image, ImageMorph, _imagingmorph diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 7980bead0a4..636b99dbe8f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageOps, ImageStat, features diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 84d3a69507a..8ffb9bff7a2 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e5b59b74a84..be21464b4ae 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index ac3ea3281f5..5c6393e237f 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,4 +1,5 @@ from __future__ import annotations + import array import math import struct diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 41d247f429f..d55d980d9be 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6d71e4d87af..66d553bcbc7 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageSequence, TiffImagePlugin diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 761d28d3019..0996ad41d4d 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageShow diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 7b56b89cc4b..01687db353e 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageStat diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index bb20fbb6f92..c06fc58235b 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 6927eedcf86..f93eabcb4ff 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageWin diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index bd154335af7..63d6b903ca6 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image, ImageWin diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 92cad4ac1b1..1c642e4c981 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 1293f7628be..e2024abbf2e 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 49b052fa485..db9557d7ba2 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,4 +1,5 @@ from __future__ import annotations + import locale import pytest diff --git a/Tests/test_main.py b/Tests/test_main.py index a84e61a7b7d..9f61a0c8169 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import subprocess import sys diff --git a/Tests/test_map.py b/Tests/test_map.py index 76444f33d1c..9c79fe35906 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 3e17d8dccde..d3ee511b7c2 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6f0e99b3f93..24dff36a610 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index aeeafb6f1df..a89d75b59d3 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -1,4 +1,5 @@ from __future__ import annotations + import time import pytest diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index eb687b57b62..c445e349447 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pickle import pytest diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 77c7952e912..7f618d0f53b 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import sys from io import BytesIO diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 08133b6c30c..c2cea08ca61 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import __version__ diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 49ca016771f..ad2b5ad9bf5 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageQt diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 396bd9080ca..b26787ce668 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageQt diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index 37d72d451de..dee6258ecb5 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index d93b0390416..9f3e86a32a8 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,4 +1,5 @@ from __future__ import annotations + import shutil import pytest diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index e7b41fb4738..c07e7f7d395 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,4 +1,5 @@ from __future__ import annotations + from fractions import Fraction from PIL import Image, TiffImagePlugin, features diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 6b693f7cd5d..75326288f97 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import assert_image_equal, assert_image_similar, hopper diff --git a/Tests/test_util.py b/Tests/test_util.py index 4a312beb440..3395ef753d7 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import _util diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 28ebc7d79f1..0f51abc9574 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image From 420150f0e251c9519a5aea1c24546a44d489828c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 20 Jan 2024 14:56:20 +0100 Subject: [PATCH 0103/1178] Update winbuild/build.rst --- winbuild/build.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index f40982cd546..c980d9c7567 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -92,7 +92,7 @@ You can also install Pillow in `editable mode`_:: winbuild\build\build_env.cmd python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . -To build a wheel instead, run:: +To build a binary wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . From 46741953215a764d18fb18fe4a16cadb82d40f9c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jan 2024 15:01:12 +1100 Subject: [PATCH 0104/1178] Removed support for test-image-results --- Tests/helper.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 99170c765dd..b2e7d43dd67 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -25,13 +25,6 @@ uploader = "show" elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -else: - try: - import test_image_results - - uploader = "aws" - except ImportError: - pass def upload(a: Image.Image, b: Image.Image) -> str | None: @@ -46,8 +39,6 @@ def upload(a: Image.Image, b: Image.Image) -> str | None: a.save(os.path.join(tmpdir, "a.png")) b.save(os.path.join(tmpdir, "b.png")) return tmpdir - elif uploader == "aws": - return test_image_results.upload(a, b) return None From d331eb9c528920bf6c10d56b5a5a8149b8a92801 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jan 2024 16:47:38 +1100 Subject: [PATCH 0105/1178] Added type hints --- Tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/conftest.py b/Tests/conftest.py index ac618c5b945..e00d1f019f5 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -2,8 +2,10 @@ import io +import pytest -def pytest_report_header(config): + +def pytest_report_header(config: pytest.Config) -> str: try: from PIL import features @@ -14,7 +16,7 @@ def pytest_report_header(config): return f"pytest_report_header failed: {e}" -def pytest_configure(config): +def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "markers", "pil_noop_mark: A conditional mark where nothing special happens", From 16ea9bd102757faa7cd02ebfc74b5c9e3d1dac1f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:08:51 +0200 Subject: [PATCH 0106/1178] Include pyproject.toml in pip cache key --- .github/workflows/docs.yml | 4 +++- .github/workflows/test.yml | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9fe345c8a3d..6853462255f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,7 +37,9 @@ jobs: with: python-version: "3.x" cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05f78704bcd..2044620aa8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: @@ -65,7 +68,9 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py From 0b6c7ba49e80dbba02705e40274854b8b4b5b09b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:09:41 +0200 Subject: [PATCH 0107/1178] Disable wget progress bar but not all output --- depends/download-and-extract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index a318bfafd9f..04bfbc7556b 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,7 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget -O $archive.tar.gz $url + wget --no-verbose -O $archive.tar.gz $url fi rmdir $archive From 97d24f14a539115c82ecdd0a9801e37dc83636b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 21 Jan 2024 01:15:59 +0200 Subject: [PATCH 0108/1178] Cache libimagequant --- .github/workflows/docs.yml | 8 ++++++++ .github/workflows/test.yml | 9 +++++++++ depends/install_imagequant.sh | 38 +++++++++++++++++++++++++++-------- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6853462255f..4319cc8ff28 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -44,11 +44,19 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2044620aa8a..4e23f5c5b12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,12 +75,21 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index b7cebbdbf60..0fe8cbdba8e 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,15 +1,37 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.2.2 +archive_name=libimagequant +archive_version=4.2.2 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +archive=$archive_name-$archive_version -pushd $archive/imagequant-sys +if [[ "$GHA_LIBIMAGEQUANT_CACHE_HIT" == "true" ]]; then -cargo install cargo-c -cargo cinstall --prefix=/usr --destdir=. -sudo cp usr/lib/libimagequant.so* /usr/lib/ -sudo cp usr/include/libimagequant.h /usr/include/ + # Copy cached files into place + sudo cp ~/cache-$archive_name/libimagequant.so* /usr/lib/ + sudo cp ~/cache-$archive_name/libimagequant.h /usr/include/ -popd +else + + # Build from source + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive/imagequant-sys + + time cargo install cargo-c + time cargo cinstall --prefix=/usr --destdir=. + + # Copy into place + sudo cp usr/lib/libimagequant.so* /usr/lib/ + sudo cp usr/include/libimagequant.h /usr/include/ + + # Copy to cache + rm -rf ~/cache-$archive_name + mkdir ~/cache-$archive_name + cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + cp usr/include/libimagequant.h ~/cache-$archive_name/ + + popd + +fi From a09e056a4ca9ab5283b14bfd9ccec9aeb0757643 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Jan 2024 18:42:43 +1100 Subject: [PATCH 0109/1178] Added type hints --- Tests/test_font_bdf.py | 4 +-- Tests/test_font_crash.py | 4 +-- Tests/test_font_leaks.py | 6 ++-- Tests/test_font_pcf.py | 21 +++++++----- Tests/test_font_pcf_charsets.py | 59 +++++++++++++++++++-------------- 5 files changed, 53 insertions(+), 41 deletions(-) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index e5e85618651..136070f9e7d 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -7,7 +7,7 @@ filename = "Tests/images/courB08.bdf" -def test_sanity(): +def test_sanity() -> None: with open(filename, "rb") as test_file: font = BdfFontFile.BdfFontFile(test_file) @@ -15,7 +15,7 @@ def test_sanity(): assert len([_f for _f in font.glyph if _f]) == 190 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BdfFontFile.BdfFontFile(fp) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index e3c72c1aebd..b82340ef70b 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -8,7 +8,7 @@ class TestFontCrash: - def _fuzz_font(self, font): + def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") @@ -18,7 +18,7 @@ def _fuzz_font(self, font): draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") - def test_segfault(self): + def test_segfault(self) -> None: with pytest.raises(OSError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 4e29a856bc9..241f455b813 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -10,7 +10,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): iterations = 10 mem_limit = 4096 # k - def _test_font(self, font): + def _test_font(self, font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) self._test_leak( @@ -20,7 +20,7 @@ def _test_font(self, font): ) @skip_unless_feature("freetype2") - def test_leak(self): + def test_leak(self) -> None: ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) @@ -30,6 +30,6 @@ class TestDefaultFontLeak(TestTTypeFontLeak): iterations = 100 mem_limit = 1024 # k - def test_leak(self): + def test_leak(self) -> None: default_font = ImageFont.load_default() self._test_font(default_font) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index e6abede0787..0f1eabdce39 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import PosixPath import pytest @@ -20,7 +21,7 @@ pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path): +def save_font(request: pytest.FixtureRequest, tmp_path: PosixPath) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -29,7 +30,7 @@ def save_font(request, tmp_path): tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -47,11 +48,11 @@ def delete_tempfile(): return tempname -def test_sanity(request, tmp_path): +def test_sanity(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: save_font(request, tmp_path) -def test_less_than_256_characters(): +def test_less_than_256_characters() -> None: with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -59,13 +60,13 @@ def test_less_than_256_characters(): assert len([_f for _f in font.glyph if _f]) == 127 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): PcfFontFile.PcfFontFile(fp) -def test_draw(request, tmp_path): +def test_draw(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (130, 30), "white") @@ -74,7 +75,7 @@ def test_draw(request, tmp_path): assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) -def test_textsize(request, tmp_path): +def test_textsize(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): @@ -90,7 +91,9 @@ def test_textsize(request, tmp_path): assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) -def _test_high_characters(request, tmp_path, message): +def _test_high_characters( + request: pytest.FixtureRequest, tmp_path: PosixPath, message: str | bytes +) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (750, 30), "white") @@ -99,7 +102,7 @@ def _test_high_characters(request, tmp_path, message): assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) -def test_high_characters(request, tmp_path): +def test_high_characters(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: message = "".join(chr(i + 1) for i in range(140, 232)) _test_high_characters(request, tmp_path, message) # accept bytes instances. diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 4c2d7185e3c..9dfaa404e28 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import PosixPath import pytest @@ -14,38 +15,40 @@ fontname = "Tests/fonts/ter-x20b.pcf" -charsets = { - "iso8859-1": { - "glyph_count": 223, - "message": "hello, world", - "image1": "Tests/images/test_draw_pbm_ter_en_target.png", - }, - "iso8859-2": { - "glyph_count": 223, - "message": "witaj świecie", - "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", - }, - "cp1250": { - "glyph_count": 250, - "message": "witaj świecie", - "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", - }, +charsets: dict[str, tuple[int, str, str]] = { + "iso8859-1": ( + 223, + "hello, world", + "Tests/images/test_draw_pbm_ter_en_target.png", + ), + "iso8859-2": ( + 223, + "witaj świecie", + "Tests/images/test_draw_pbm_ter_pl_target.png", + ), + "cp1250": ( + 250, + "witaj świecie", + "Tests/images/test_draw_pbm_ter_pl_target.png", + ), } pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path, encoding): +def save_font( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) # check the number of characters in the font - assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] + assert len([_f for _f in font.glyph if _f]) == charsets[encoding][0] tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -64,23 +67,29 @@ def delete_tempfile(): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_sanity(request, tmp_path, encoding): +def test_sanity( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> None: save_font(request, tmp_path, encoding) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_draw(request, tmp_path, encoding): +def test_draw( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") draw = ImageDraw.Draw(im) - message = charsets[encoding]["message"].encode(encoding) + message = charsets[encoding][1].encode(encoding) draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) + assert_image_similar_tofile(im, charsets[encoding][2], 0) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_textsize(request, tmp_path, encoding): +def test_textsize( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): @@ -90,7 +99,7 @@ def test_textsize(request, tmp_path, encoding): assert dy == 20 assert dx in (0, 10) assert font.getlength(bytearray([i])) == dx - message = charsets[encoding]["message"].encode(encoding) + message = charsets[encoding][1].encode(encoding) for i in range(len(message)): msg = message[: i + 1] assert font.getlength(msg) == len(msg) * 10 From d96c196c48a27fc327eb1fcb7296ee47b7f25e0f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:42:03 +0200 Subject: [PATCH 0110/1178] Only cache on GHA, remove debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- depends/install_imagequant.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 0fe8cbdba8e..3adae91a525 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -19,18 +19,20 @@ else pushd $archive/imagequant-sys - time cargo install cargo-c - time cargo cinstall --prefix=/usr --destdir=. + cargo install cargo-c + cargo cinstall --prefix=/usr --destdir=. # Copy into place sudo cp usr/lib/libimagequant.so* /usr/lib/ sudo cp usr/include/libimagequant.h /usr/include/ - # Copy to cache - rm -rf ~/cache-$archive_name - mkdir ~/cache-$archive_name - cp usr/lib/libimagequant.so* ~/cache-$archive_name/ - cp usr/include/libimagequant.h ~/cache-$archive_name/ + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-$archive_name + mkdir ~/cache-$archive_name + cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + cp usr/include/libimagequant.h ~/cache-$archive_name/ + fi popd From 2521ec4732311ffd85444253d755e3acee89c10f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Jan 2024 22:08:45 +1100 Subject: [PATCH 0111/1178] Restored charsets dictionary --- Tests/test_font_pcf_charsets.py | 53 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 9dfaa404e28..cb77128eff7 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -15,22 +15,22 @@ fontname = "Tests/fonts/ter-x20b.pcf" -charsets: dict[str, tuple[int, str, str]] = { - "iso8859-1": ( - 223, - "hello, world", - "Tests/images/test_draw_pbm_ter_en_target.png", - ), - "iso8859-2": ( - 223, - "witaj świecie", - "Tests/images/test_draw_pbm_ter_pl_target.png", - ), - "cp1250": ( - 250, - "witaj świecie", - "Tests/images/test_draw_pbm_ter_pl_target.png", - ), +charsets: dict[str, dict[str, int | str]] = { + "iso8859-1": { + "glyph_count": 223, + "message": "hello, world", + "image1": "Tests/images/test_draw_pbm_ter_en_target.png", + }, + "iso8859-2": { + "glyph_count": 223, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, + "cp1250": { + "glyph_count": 250, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, } @@ -44,7 +44,7 @@ def save_font( font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) # check the number of characters in the font - assert len([_f for _f in font.glyph if _f]) == charsets[encoding][0] + assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] tempname = str(tmp_path / "temp.pil") @@ -81,9 +81,14 @@ def test_draw( font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") draw = ImageDraw.Draw(im) - message = charsets[encoding][1].encode(encoding) - draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, charsets[encoding][2], 0) + + message = charsets[encoding]["message"] + assert isinstance(message, str) + draw.text((0, 0), message.encode(encoding), "black", font=font) + + expected_path = charsets[encoding]["image1"] + assert isinstance(expected_path, str) + assert_image_similar_tofile(im, expected_path, 0) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @@ -99,8 +104,10 @@ def test_textsize( assert dy == 20 assert dx in (0, 10) assert font.getlength(bytearray([i])) == dx - message = charsets[encoding][1].encode(encoding) - for i in range(len(message)): - msg = message[: i + 1] + message = charsets[encoding]["message"] + assert isinstance(message, str) + message_bytes = message.encode(encoding) + for i in range(len(message_bytes)): + msg = message_bytes[: i + 1] assert font.getlength(msg) == len(msg) * 10 assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) From 231d54b9df90dcce1cdbb9928f8d21899eed8153 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:37:37 +0200 Subject: [PATCH 0112/1178] Replace io.BytesIO in type hints --- docs/reference/internal_design.rst | 4 ++-- docs/reference/internal_modules.rst | 12 ++++++++++++ src/PIL/GdImageFile.py | 7 +++++-- src/PIL/MpegImagePlugin.py | 5 ++--- src/PIL/_typing.py | 16 +++++++++++++++- src/PIL/_util.py | 4 ++-- 6 files changed, 38 insertions(+), 10 deletions(-) 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..c3cc700607f 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,18 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: FileDescriptor + + Typing alias. + +.. py:class:: StrOrBytesPath + + Typing alias. + +.. py:class:: SupportsRead + + An object that supports the read method. + .. py:data:: TypeGuard :value: typing.TypeGuard diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7bb4736af13..315ac6d6c1a 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 FileDescriptor, StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -80,7 +81,9 @@ def _open(self) -> None: ] -def open(fp: BytesIO, mode: str = "r") -> GdImageFile: +def open( + fp: StrOrBytesPath | FileDescriptor | IO[bytes], mode: str = "r" +) -> GdImageFile: """ Load texture from a GD image file. 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 608b2b41fa8..6eb25c1c171 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,6 +1,8 @@ from __future__ import annotations +import os import sys +from typing import Protocol, TypeVar, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -15,4 +17,16 @@ def __class_getitem__(cls, item: Any) -> type[bool]: return bool -__all__ = ["TypeGuard"] +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, __length: int = ...) -> _T_co: + ... + + +FileDescriptor = int +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + + +__all__ = ["FileDescriptor", "TypeGuard", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 13f369cca1d..4ecdc4bd307 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -4,10 +4,10 @@ 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]: +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: return isinstance(f, (bytes, str, Path)) From 474411b52a3619f01bb626ce8e56100094886d1a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 08:52:14 +1100 Subject: [PATCH 0113/1178] Updated zlib to 1.3.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec3148734f..a30d0468ccb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -29,7 +29,7 @@ else GIFLIB_VERSION=5.2.1 fi if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then - ZLIB_VERSION=1.3 + ZLIB_VERSION=1.3.1 else ZLIB_VERSION=1.2.8 fi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df33ea493e6..3117065e8f7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -143,9 +143,9 @@ def cmd_msbuild( "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib13.zip", - "filename": "zlib13.zip", - "dir": "zlib-1.3", + "url": "https://zlib.net/zlib131.zip", + "filename": "zlib131.zip", + "dir": "zlib-1.3.1", "license": "README", "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ From 16fd934b007d7090fc32ff4a1ad13182a32bf612 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 09:55:25 +1100 Subject: [PATCH 0114/1178] Use TypedDict --- Tests/test_font_pcf_charsets.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index cb77128eff7..894d4eb56bf 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -2,6 +2,7 @@ import os from pathlib import PosixPath +from typing import TypedDict import pytest @@ -15,7 +16,14 @@ fontname = "Tests/fonts/ter-x20b.pcf" -charsets: dict[str, dict[str, int | str]] = { + +class Charset(TypedDict): + glyph_count: int + message: str + image1: str + + +charsets: dict[str, Charset] = { "iso8859-1": { "glyph_count": 223, "message": "hello, world", @@ -81,14 +89,9 @@ def test_draw( font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") draw = ImageDraw.Draw(im) - - message = charsets[encoding]["message"] - assert isinstance(message, str) - draw.text((0, 0), message.encode(encoding), "black", font=font) - - expected_path = charsets[encoding]["image1"] - assert isinstance(expected_path, str) - assert_image_similar_tofile(im, expected_path, 0) + message = charsets[encoding]["message"].encode(encoding) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @@ -104,10 +107,8 @@ def test_textsize( assert dy == 20 assert dx in (0, 10) assert font.getlength(bytearray([i])) == dx - message = charsets[encoding]["message"] - assert isinstance(message, str) - message_bytes = message.encode(encoding) - for i in range(len(message_bytes)): - msg = message_bytes[: i + 1] + message = charsets[encoding]["message"].encode(encoding) + for i in range(len(message)): + msg = message[: i + 1] assert font.getlength(msg) == len(msg) * 10 assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) From 8caae8739f16cfddade0848ba686b1a40a0c10b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 18:51:41 +1100 Subject: [PATCH 0115/1178] Restored testing of non-TrueType default font --- Tests/test_font_leaks.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 4e29a856bc9..d29e9bcfc9a 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,8 +1,10 @@ from __future__ import annotations -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, _util -from .helper import PillowLeakTestCase, skip_unless_feature +from .helper import PillowLeakTestCase, features, skip_unless_feature + +original_core = ImageFont.core class TestTTypeFontLeak(PillowLeakTestCase): @@ -31,5 +33,9 @@ class TestDefaultFontLeak(TestTTypeFontLeak): mem_limit = 1024 # k def test_leak(self): + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) default_font = ImageFont.load_default() + ImageFont.core = original_core + self._test_font(default_font) From 4814bee6c0b99b4e00fa08a5d15663f8238f063a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 21:42:36 +1100 Subject: [PATCH 0116/1178] Use Path instead of PosixPath --- Tests/check_j2k_overflow.py | 4 ++-- Tests/check_large_memory.py | 8 ++++---- Tests/check_large_memory_numpy.py | 8 ++++---- Tests/test_font_pcf.py | 14 +++++++------- Tests/test_font_pcf_charsets.py | 16 +++++----------- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 8a85783fccb..dbdd5a4f557 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,13 +1,13 @@ from __future__ import annotations -from pathlib import PosixPath +from pathlib import Path import pytest from PIL import Image -def test_j2k_overflow(tmp_path: PosixPath) -> None: +def test_j2k_overflow(tmp_path: Path) -> None: im = Image.new("RGBA", (1024, 131584)) target = str(tmp_path / "temp.jpc") with pytest.raises(OSError): diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 2c8c77800bb..a9ce79e57e6 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from pathlib import PosixPath +from pathlib import Path from types import ModuleType import pytest @@ -31,18 +31,18 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) im.save(f) -def test_large(tmp_path: PosixPath) -> None: +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path: PosixPath) -> None: +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 8609fe6d07b..f4ca8d0aa68 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from pathlib import PosixPath +from pathlib import Path import pytest @@ -25,7 +25,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) f = str(tmp_path / "temp.png") @@ -33,11 +33,11 @@ def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: im.save(f) -def test_large(tmp_path: PosixPath) -> None: +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path: PosixPath) -> None: +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 0f1eabdce39..997809e463b 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from pathlib import PosixPath +from pathlib import Path import pytest @@ -21,7 +21,7 @@ pytestmark = skip_unless_feature("zlib") -def save_font(request: pytest.FixtureRequest, tmp_path: PosixPath) -> str: +def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -48,7 +48,7 @@ def delete_tempfile() -> None: return tempname -def test_sanity(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None: save_font(request, tmp_path) @@ -66,7 +66,7 @@ def test_invalid_file() -> None: PcfFontFile.PcfFontFile(fp) -def test_draw(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (130, 30), "white") @@ -75,7 +75,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) -def test_textsize(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): @@ -92,7 +92,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: def _test_high_characters( - request: pytest.FixtureRequest, tmp_path: PosixPath, message: str | bytes + request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes ) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) @@ -102,7 +102,7 @@ def _test_high_characters( assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) -def test_high_characters(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: message = "".join(chr(i + 1) for i in range(140, 232)) _test_high_characters(request, tmp_path, message) # accept bytes instances. diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 894d4eb56bf..895458d9d82 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from pathlib import PosixPath +from pathlib import Path from typing import TypedDict import pytest @@ -45,9 +45,7 @@ class Charset(TypedDict): pytestmark = skip_unless_feature("zlib") -def save_font( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str -) -> str: +def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) @@ -75,16 +73,12 @@ def delete_tempfile() -> None: @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_sanity( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str -) -> None: +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: save_font(request, tmp_path, encoding) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_draw( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str -) -> None: +def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") @@ -96,7 +90,7 @@ def test_draw( @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) def test_textsize( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str + request: pytest.FixtureRequest, tmp_path: Path, encoding: str ) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) From e2d1b2663d2b392089bfe59c7a17d2afbf74ce7f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:12:06 +1100 Subject: [PATCH 0117/1178] Restored original state using finally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- Tests/test_font_leaks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index d29e9bcfc9a..5eea0c34d5f 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -35,7 +35,9 @@ class TestDefaultFontLeak(TestTTypeFontLeak): def test_leak(self): if features.check_module("freetype2"): ImageFont.core = _util.DeferredError(ImportError) - default_font = ImageFont.load_default() - ImageFont.core = original_core + try: + default_font = ImageFont.load_default() + finally: + ImageFont.core = original_core self._test_font(default_font) From e3932b7dbaf6aff8ef4b7a24007f4de07477ec91 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:58:41 +0200 Subject: [PATCH 0118/1178] Exclude from coverage: empty bodies in protocols or abstract methods --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 46df3f90d27..5678e45661a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,9 @@ exclude_also = if DEBUG: # Don't complain about compatibility code for missing optional dependencies except ImportError + # Empty bodies in protocols or abstract methods + ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ + ^\s*\.\.\.(\s*#.*)?$ [run] omit = From b3a7ae065c4f34b345ecaa3b019bbd1d24e7922c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 06:40:03 +1100 Subject: [PATCH 0119/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 62ae2a68bbc..7d80eec0345 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Do not support using test-image-results to upload images after test failures #7739 + [radarhere] + +- Changed ImageMath.ops to be static #7721 + [radarhere] + - Fix APNG info after seeking backwards more than twice #7701 [esoma, radarhere] From cf9e6ff2563be6d0458856e2aa35e54973709752 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 08:10:07 +1100 Subject: [PATCH 0120/1178] Updated libjpeg-turbo to 3.0.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec3148734f..9013e8ae2c8 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -18,7 +18,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=8.3.0 LIBPNG_VERSION=1.6.40 -JPEGTURBO_VERSION=3.0.1 +JPEGTURBO_VERSION=3.0.2 OPENJPEG_VERSION=2.5.0 XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df33ea493e6..92cbcdf7a99 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,9 +113,9 @@ def cmd_msbuild( DEPS = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/3.0.1/libjpeg-turbo-3.0.1.tar.gz/download", - "filename": "libjpeg-turbo-3.0.1.tar.gz", - "dir": "libjpeg-turbo-3.0.1", + + "/libjpeg-turbo/files/3.0.2/libjpeg-turbo-3.0.2.tar.gz/download", + "filename": "libjpeg-turbo-3.0.2.tar.gz", + "dir": "libjpeg-turbo-3.0.2", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 5721577e4e6f5d64a5c50ab8732eea9e5a437120 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 20:20:53 +1100 Subject: [PATCH 0121/1178] Stop reading EPS at EOF marker --- Tests/test_file_eps.py | 8 ++++++++ src/PIL/EpsImagePlugin.py | 8 ++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8def9a43511..5ba3a0c1449 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -436,3 +436,11 @@ def test_eof_before_bounding_box(): with pytest.raises(OSError): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): pass + + +def test_invalid_data_after_eof() -> None: + with open("Tests/images/illuCS6_preview.eps", "rb") as f: + img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) + + with Image.open(img_bytes) as img: + assert img.mode == "RGB" diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index d2e60aa0759..94b163bc4d6 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -356,14 +356,10 @@ def _read_comment(s): self._size = columns, rows return + elif bytes_mv[:5] == b"%%EOF": + break elif trailer_reached and reading_trailer_comments: # Load EPS trailer - - # if this line starts with "%%EOF", - # then we've reached the end of the file - if bytes_mv[:5] == b"%%EOF": - break - s = str(bytes_mv[:bytes_read], "latin-1") _read_comment(s) elif bytes_mv[:9] == b"%%Trailer": From ddb7df0ec6b5852e509dbf00675a3866ca00bd66 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 22:18:46 +1100 Subject: [PATCH 0122/1178] Added type hints --- Tests/test_image_convert.py | 50 +++++++++++----------- Tests/test_image_copy.py | 6 +-- Tests/test_image_crop.py | 14 +++---- Tests/test_image_frombytes.py | 2 +- Tests/test_image_fromqimage.py | 15 +++---- Tests/test_image_getbands.py | 2 +- Tests/test_image_getbbox.py | 12 +++--- Tests/test_image_getcolors.py | 6 +-- Tests/test_image_getdata.py | 6 +-- Tests/test_image_getim.py | 2 +- Tests/test_image_getprojection.py | 2 +- Tests/test_image_histogram.py | 4 +- Tests/test_image_point.py | 8 ++-- Tests/test_image_putalpha.py | 6 +-- Tests/test_image_quantize.py | 24 +++++------ Tests/test_image_resize.py | 70 +++++++++++++++++++++---------- Tests/test_image_split.py | 12 +++--- Tests/test_image_tobitmap.py | 2 +- Tests/test_image_tobytes.py | 2 +- Tests/test_image_transpose.py | 19 +++++---- 20 files changed, 149 insertions(+), 115 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index d4ddc2a31b2..f154de123bb 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -7,8 +9,8 @@ from .helper import assert_image, assert_image_equal, assert_image_similar, hopper -def test_sanity(): - def convert(im, mode): +def test_sanity() -> None: + def convert(im: Image.Image, mode: str) -> None: out = im.convert(mode) assert out.mode == mode assert out.size == im.size @@ -40,13 +42,13 @@ def convert(im, mode): convert(im, output_mode) -def test_unsupported_conversion(): +def test_unsupported_conversion() -> None: im = hopper() with pytest.raises(ValueError): im.convert("INVALID") -def test_default(): +def test_default() -> None: im = hopper("P") assert im.mode == "P" converted_im = im.convert() @@ -62,18 +64,18 @@ def test_default(): # ref https://github.com/python-pillow/Pillow/issues/274 -def _test_float_conversion(im): +def _test_float_conversion(im: Image.Image) -> None: orig = im.getpixel((5, 5)) converted = im.convert("F").getpixel((5, 5)) assert orig == converted -def test_8bit(): +def test_8bit() -> None: with Image.open("Tests/images/hopper.jpg") as im: _test_float_conversion(im.convert("L")) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im) @@ -83,19 +85,19 @@ def test_16bit(): assert im_i16.getpixel((0, 0)) == 65535 -def test_16bit_workaround(): +def test_16bit_workaround() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im.convert("I")) -def test_opaque(): +def test_opaque() -> None: alpha = hopper("P").convert("PA").getchannel("A") solid = Image.new("L", (128, 128), 255) assert_image_equal(alpha, solid) -def test_rgba_p(): +def test_rgba_p() -> None: im = hopper("RGBA") im.putalpha(hopper("L")) @@ -105,14 +107,14 @@ def test_rgba_p(): assert_image_similar(im, comparable, 20) -def test_rgba(): +def test_rgba() -> None: with Image.open("Tests/images/transparent.png") as im: assert im.mode == "RGBA" assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) -def test_trns_p(tmp_path): +def test_trns_p(tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 @@ -131,7 +133,7 @@ def test_trns_p(tmp_path): @pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) -def test_trns_p_transparency(mode): +def test_trns_p_transparency(mode: str) -> None: # Arrange im = hopper("P") im.info["transparency"] = 128 @@ -148,7 +150,7 @@ def test_trns_p_transparency(mode): assert converted_im.palette is None -def test_trns_l(tmp_path): +def test_trns_l(tmp_path: Path) -> None: im = hopper("L") im.info["transparency"] = 128 @@ -171,7 +173,7 @@ def test_trns_l(tmp_path): im_p.save(f) -def test_trns_RGB(tmp_path): +def test_trns_RGB(tmp_path: Path) -> None: im = hopper("RGB") im.info["transparency"] = im.getpixel((0, 0)) @@ -201,7 +203,7 @@ def test_trns_RGB(tmp_path): @pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) -def test_l_macro_rounding(convert_mode): +def test_l_macro_rounding(convert_mode: str) -> None: for mode in ("P", "PA"): im = Image.new(mode, (1, 1)) im.palette.getcolor((0, 1, 2)) @@ -214,7 +216,7 @@ def test_l_macro_rounding(convert_mode): assert converted_color == 1 -def test_gif_with_rgba_palette_to_p(): +def test_gif_with_rgba_palette_to_p() -> None: # See https://github.com/python-pillow/Pillow/issues/2433 with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 @@ -226,7 +228,7 @@ def test_gif_with_rgba_palette_to_p(): im_p.load() -def test_p_la(): +def test_p_la() -> None: im = hopper("RGBA") alpha = hopper("L") im.putalpha(alpha) @@ -236,7 +238,7 @@ def test_p_la(): assert_image_similar(alpha, comparable, 5) -def test_p2pa_alpha(): +def test_p2pa_alpha() -> None: with Image.open("Tests/images/tiny.png") as im: assert im.mode == "P" @@ -250,13 +252,13 @@ def test_p2pa_alpha(): assert im_a.getpixel((x, y)) == alpha -def test_p2pa_palette(): +def test_p2pa_palette() -> None: with Image.open("Tests/images/tiny.png") as im: im_pa = im.convert("PA") assert im_pa.getpalette() == im.getpalette() -def test_matrix_illegal_conversion(): +def test_matrix_illegal_conversion() -> None: # Arrange im = hopper("CMYK") # fmt: off @@ -272,7 +274,7 @@ def test_matrix_illegal_conversion(): im.convert(mode="CMYK", matrix=matrix) -def test_matrix_wrong_mode(): +def test_matrix_wrong_mode() -> None: # Arrange im = hopper("L") # fmt: off @@ -289,7 +291,7 @@ def test_matrix_wrong_mode(): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_matrix_xyz(mode): +def test_matrix_xyz(mode: str) -> None: # Arrange im = hopper("RGB") im.info["transparency"] = (255, 0, 0) @@ -317,7 +319,7 @@ def test_matrix_xyz(mode): assert converted_im.info["transparency"] == 105 -def test_matrix_identity(): +def test_matrix_identity() -> None: # Arrange im = hopper("RGB") # fmt: off diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 3a26ef96e74..027e5338b7a 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_copy(mode): +def test_copy(mode: str) -> None: cropped_coordinates = (10, 10, 20, 20) cropped_size = (10, 10) @@ -39,7 +39,7 @@ def test_copy(mode): assert out.size == cropped_size -def test_copy_zero(): +def test_copy_zero() -> None: im = Image.new("RGB", (0, 0)) out = im.copy() assert out.mode == im.mode @@ -47,7 +47,7 @@ def test_copy_zero(): @skip_unless_feature("libtiff") -def test_deepcopy(): +def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 5e02a3b0dfc..d095364ba75 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_crop(mode): +def test_crop(mode: str) -> None: im = hopper(mode) assert_image_equal(im.crop(), im) @@ -17,8 +17,8 @@ def test_crop(mode): assert cropped.size == (50, 50) -def test_wide_crop(): - def crop(*bbox): +def test_wide_crop() -> None: + def crop(*bbox: int) -> tuple[int, ...]: i = im.crop(bbox) h = i.histogram() while h and not h[-1]: @@ -47,14 +47,14 @@ def crop(*bbox): @pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) -def test_negative_crop(box): +def test_negative_crop(box: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (10, 10)) with pytest.raises(ValueError): im.crop(box) -def test_crop_float(): +def test_crop_float() -> None: # Check cropping floats are rounded to nearest integer # https://github.com/python-pillow/Pillow/issues/1744 @@ -69,7 +69,7 @@ def test_crop_float(): assert cropped.size == (3, 5) -def test_crop_crash(): +def test_crop_crash() -> None: # Image.crop crashes prepatch with an access violation # apparently a use after free on Windows, see # https://github.com/python-pillow/Pillow/issues/1077 @@ -87,7 +87,7 @@ def test_crop_crash(): img.load() -def test_crop_zero(): +def test_crop_zero() -> None: im = Image.new("RGB", (0, 0), "white") cropped = im.crop((0, 0, 0, 0)) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 5d5e9f2dfc9..6474daba108 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type): +def test_sanity(data_type) -> None: im1 = hopper() data = im1.tobytes() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 76b576da56b..ea31a9de913 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from typing import Generator import pytest @@ -18,7 +19,7 @@ @pytest.fixture -def test_images(): +def test_images() -> Generator[Image.Image, None, None]: ims = [ hopper(), Image.open("Tests/images/transparent.png"), @@ -31,7 +32,7 @@ def test_images(): im.close() -def roundtrip(expected): +def roundtrip(expected: Image.Image) -> None: # PIL -> Qt intermediate = expected.toqimage() # Qt -> PIL @@ -43,26 +44,26 @@ def roundtrip(expected): assert_image_equal(result, expected.convert("RGB")) -def test_sanity_1(test_images): +def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("1")) -def test_sanity_rgb(test_images): +def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("RGB")) -def test_sanity_rgba(test_images): +def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("RGBA")) -def test_sanity_l(test_images): +def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("L")) -def test_sanity_p(test_images): +def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("P")) diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 64339e2cd85..887553fc042 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -3,7 +3,7 @@ from PIL import Image -def test_getbands(): +def test_getbands() -> None: assert Image.new("1", (1, 1)).getbands() == ("1",) assert Image.new("L", (1, 1)).getbands() == ("L",) assert Image.new("I", (1, 1)).getbands() == ("I",) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index b18a7202e38..18c6f657925 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -7,13 +7,13 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: bbox = hopper().getbbox() assert isinstance(bbox, tuple) -def test_bbox(): - def check(im, fill_color): +def test_bbox() -> None: + def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: assert im.getbbox() is None im.paste(fill_color, (10, 25, 90, 75)) @@ -34,8 +34,8 @@ def check(im, fill_color): check(im, 255) for mode in ("RGBA", "RGBa"): - for color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): - im = Image.new(mode, (100, 100), color) + for rgba_color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): + im = Image.new(mode, (100, 100), rgba_color) check(im, (255, 255, 255, 255)) for mode in ("La", "LA", "PA"): @@ -45,7 +45,7 @@ def check(im, fill_color): @pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) -def test_bbox_alpha_only_false(mode): +def test_bbox_alpha_only_false(mode: str) -> None: im = Image.new(mode, (100, 100)) assert im.getbbox(alpha_only=False) is None diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index 17460fa93f5..8f8870f4fec 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -3,8 +3,8 @@ from .helper import hopper -def test_getcolors(): - def getcolors(mode, limit=None): +def test_getcolors() -> None: + def getcolors(mode: str, limit: int | None = None) -> int | None: im = hopper(mode) if limit: colors = im.getcolors(limit) @@ -39,7 +39,7 @@ def getcolors(mode, limit=None): # -------------------------------------------------------------------- -def test_pack(): +def test_pack() -> None: # Pack problems for small tables (@PIL209) im = hopper().quantize(3).convert("RGB") diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index ace64279b8c..ac27400be94 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().getdata() len(data) @@ -14,8 +14,8 @@ def test_sanity(): assert data[0] == (20, 20, 70) -def test_roundtrip(): - def getdata(mode): +def test_roundtrip() -> None: + def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index bc8a7485eb0..9afa02b0a8b 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -3,7 +3,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() type_repr = repr(type(im.getim())) diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index e90f5f5056f..2b5a758ed29 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() projection = im.getprojection() diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 3ac6649e074..dbd55d4c2d3 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -3,8 +3,8 @@ from .helper import hopper -def test_histogram(): - def histogram(mode): +def test_histogram() -> None: + def histogram(mode: str) -> tuple[int, int, int]: h = hopper(mode).histogram() return len(h), min(h), max(h) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 2232b94429d..05f209351d2 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -5,7 +5,7 @@ from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() with pytest.raises(ValueError): @@ -39,7 +39,7 @@ def test_sanity(): im.point(lambda x: x // 2) -def test_16bit_lut(): +def test_16bit_lut() -> None: """Tests for 16 bit -> 8 bit lut for converting I->L images see https://github.com/python-pillow/Pillow/issues/440 """ @@ -47,7 +47,7 @@ def test_16bit_lut(): im.point(list(range(256)) * 256, "L") -def test_f_lut(): +def test_f_lut() -> None: """Tests for floating point lut of 8bit gray image""" im = hopper("L") lut = [0.5 * float(x) for x in range(256)] @@ -58,7 +58,7 @@ def test_f_lut(): assert_image_equal(out.convert("L"), im.point(int_lut, "L")) -def test_f_mode(): +def test_f_mode() -> None: im = hopper("F") with pytest.raises(ValueError): im.point(None) diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index c44b048d523..2c92911d1fb 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -3,7 +3,7 @@ from PIL import Image -def test_interface(): +def test_interface() -> None: im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) assert im.getpixel((0, 0)) == (1, 2, 3, 0) @@ -17,7 +17,7 @@ def test_interface(): assert im.getpixel((0, 0)) == (1, 2, 3, 5) -def test_promote(): +def test_promote() -> None: im = Image.new("L", (1, 1), 1) assert im.getpixel((0, 0)) == 1 @@ -40,7 +40,7 @@ def test_promote(): assert im.getpixel((0, 0)) == (1, 2, 3, 4) -def test_readonly(): +def test_readonly() -> None: im = Image.new("RGB", (1, 1), (1, 2, 3)) im.readonly = 1 diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 1475b027bd8..873a9bb5dcb 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -8,7 +8,7 @@ from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature -def test_sanity(): +def test_sanity() -> None: image = hopper() converted = image.quantize() assert converted.mode == "P" @@ -21,7 +21,7 @@ def test_sanity(): @skip_unless_feature("libimagequant") -def test_libimagequant_quantize(): +def test_libimagequant_quantize() -> None: image = hopper() if is_ppc64le(): libimagequant = parse_version(features.version_feature("libimagequant")) @@ -33,7 +33,7 @@ def test_libimagequant_quantize(): assert len(converted.getcolors()) == 100 -def test_octree_quantize(): +def test_octree_quantize() -> None: image = hopper() converted = image.quantize(100, Image.Quantize.FASTOCTREE) assert converted.mode == "P" @@ -41,7 +41,7 @@ def test_octree_quantize(): assert len(converted.getcolors()) == 100 -def test_rgba_quantize(): +def test_rgba_quantize() -> None: image = hopper("RGBA") with pytest.raises(ValueError): image.quantize(method=0) @@ -49,7 +49,7 @@ def test_rgba_quantize(): assert image.quantize().convert().mode == "RGBA" -def test_quantize(): +def test_quantize() -> None: with Image.open("Tests/images/caption_6_33_22.png") as image: image = image.convert("RGB") converted = image.quantize() @@ -57,7 +57,7 @@ def test_quantize(): assert_image_similar(converted.convert("RGB"), image, 1) -def test_quantize_no_dither(): +def test_quantize_no_dither() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -67,7 +67,7 @@ def test_quantize_no_dither(): assert converted.palette.palette == palette.palette.palette -def test_quantize_no_dither2(): +def test_quantize_no_dither2() -> None: im = Image.new("RGB", (9, 1)) im.putdata([(p,) * 3 for p in range(0, 36, 4)]) @@ -83,7 +83,7 @@ def test_quantize_no_dither2(): assert px[x, 0] == (0 if x < 5 else 1) -def test_quantize_dither_diff(): +def test_quantize_dither_diff() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -94,14 +94,14 @@ def test_quantize_dither_diff(): assert dither.tobytes() != nodither.tobytes() -def test_colors(): +def test_colors() -> None: im = hopper() colors = 2 converted = im.quantize(colors) assert len(converted.palette.palette) == colors * len("RGB") -def test_transparent_colors_equal(): +def test_transparent_colors_equal() -> None: im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) px = im.load() px[0, 1] = (255, 255, 255, 0) @@ -120,7 +120,7 @@ def test_transparent_colors_equal(): (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), ), ) -def test_palette(method, color): +def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) converted = im.quantize(method=method) @@ -128,7 +128,7 @@ def test_palette(method, color): assert converted_px[0, 0] == converted.palette.colors[color] -def test_small_palette(): +def test_small_palette() -> None: # Arrange im = hopper() diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index aedcf4a09b5..bd45ee893ad 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -4,6 +4,8 @@ from __future__ import annotations from itertools import permutations +from pathlib import Path +from typing import Generator import pytest @@ -19,7 +21,9 @@ class TestImagingCoreResize: - def resize(self, im, size, f): + def resize( + self, im: Image.Image, size: tuple[int, int], f: Image.Resampling + ) -> Image.Image: # Image class independent version of resize. im.load() return im._new(im.im.resize(size, f)) @@ -27,14 +31,14 @@ def resize(self, im, size, f): @pytest.mark.parametrize( "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") ) - def test_nearest_mode(self, mode): + def test_nearest_mode(self, mode: str) -> None: im = hopper(mode) r = self.resize(im, (15, 12), Image.Resampling.NEAREST) assert r.mode == mode assert r.size == (15, 12) assert r.im.bands == im.im.bands - def test_convolution_modes(self): + def test_convolution_modes(self) -> None: with pytest.raises(ValueError): self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): @@ -59,7 +63,7 @@ def test_convolution_modes(self): Image.Resampling.LANCZOS, ), ) - def test_reduce_filters(self, resample): + def test_reduce_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (15, 12), resample) assert r.mode == "RGB" assert r.size == (15, 12) @@ -75,7 +79,7 @@ def test_reduce_filters(self, resample): Image.Resampling.LANCZOS, ), ) - def test_enlarge_filters(self, resample): + def test_enlarge_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) @@ -99,7 +103,9 @@ def test_enlarge_filters(self, resample): ("LA", ("filled", "dirty")), ), ) - def test_endianness(self, resample, mode, channels_set): + def test_endianness( + self, resample: Image.Resampling, mode: str, channels_set: tuple[str, ...] + ) -> None: # Make an image with one colored pixel, in one channel. # When resized, that channel should be the same as a GS image. # Other channels should be unaffected. @@ -139,17 +145,17 @@ def test_endianness(self, resample, mode, channels_set): Image.Resampling.LANCZOS, ), ) - def test_enlarge_zero(self, resample): + def test_enlarge_zero(self, resample: Image.Resampling) -> None: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) assert r.getdata()[0] == (0, 0, 0) - def test_unknown_filter(self): + def test_unknown_filter(self) -> None: with pytest.raises(ValueError): self.resize(hopper(), (10, 10), 9) - def test_cross_platform(self, tmp_path): + def test_cross_platform(self, tmp_path: Path) -> None: # This test is intended for only check for consistent behaviour across # platforms. So if a future Pillow change requires that the test file # be updated, that is okay. @@ -162,7 +168,7 @@ def test_cross_platform(self, tmp_path): @pytest.fixture -def gradients_image(): +def gradients_image() -> Generator[Image.Image, None, None]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: @@ -172,7 +178,7 @@ def gradients_image(): class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image): + def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: ref = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, reducing_gap=None ) @@ -191,7 +197,12 @@ def test_reducing_gap_values(self, gradients_image): "box, epsilon", ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), ) - def test_reducing_gap_1(self, gradients_image, box, epsilon): + def test_reducing_gap_1( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 @@ -206,7 +217,12 @@ def test_reducing_gap_1(self, gradients_image, box, epsilon): "box, epsilon", ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), ) - def test_reducing_gap_2(self, gradients_image, box, epsilon): + def test_reducing_gap_2( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 @@ -221,7 +237,12 @@ def test_reducing_gap_2(self, gradients_image, box, epsilon): "box, epsilon", ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), ) - def test_reducing_gap_3(self, gradients_image, box, epsilon): + def test_reducing_gap_3( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 @@ -233,7 +254,9 @@ def test_reducing_gap_3(self, gradients_image, box, epsilon): assert_image_similar(ref, im, epsilon) @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) - def test_reducing_gap_8(self, gradients_image, box): + def test_reducing_gap_8( + self, gradients_image: Image.Image, box: tuple[float, float, float, float] + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 @@ -245,7 +268,12 @@ def test_reducing_gap_8(self, gradients_image, box): "box, epsilon", (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), ) - def test_box_filter(self, gradients_image, box, epsilon): + def test_box_filter( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 @@ -255,8 +283,8 @@ def test_box_filter(self, gradients_image, box, epsilon): class TestImageResize: - def test_resize(self): - def resize(mode, size): + def test_resize(self) -> None: + def resize(mode: str, size: tuple[int, int]) -> None: out = hopper(mode).resize(size) assert out.mode == mode assert out.size == size @@ -271,7 +299,7 @@ def resize(mode, size): im.resize((10, 10), "unknown") @skip_unless_feature("libtiff") - def test_load_first(self): + def test_load_first(self) -> None: # load() may change the size of the image # Test that resize() is calling it before getting the size with Image.open("Tests/images/g4_orientation_5.tif") as im: @@ -279,13 +307,13 @@ def test_load_first(self): assert im.size == (64, 64) @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) - def test_default_filter_bicubic(self, mode): + def test_default_filter_bicubic(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) @pytest.mark.parametrize( "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") ) - def test_default_filter_nearest(self, mode): + def test_default_filter_nearest(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index c39a100e777..3385f81f527 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, features @@ -7,8 +9,8 @@ from .helper import assert_image_equal, hopper -def test_split(): - def split(mode): +def test_split() -> None: + def split(mode: str) -> list[tuple[str, int, int]]: layers = hopper(mode).split() return [(i.mode, i.size[0], i.size[1]) for i in layers] @@ -36,18 +38,18 @@ def split(mode): @pytest.mark.parametrize( "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") ) -def test_split_merge(mode): +def test_split_merge(mode: str) -> None: expected = Image.merge(mode, hopper(mode).split()) assert_image_equal(hopper(mode), expected) -def test_split_open(tmp_path): +def test_split_open(tmp_path: Path) -> None: if features.check("zlib"): test_file = str(tmp_path / "temp.png") else: test_file = str(tmp_path / "temp.pcx") - def split_open(mode): + def split_open(mode: str) -> int: hopper(mode).save(test_file) with Image.open(test_file) as im: return len(im.split()) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 89a41cf8ec2..f7a3cc41d90 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -5,7 +5,7 @@ from .helper import assert_image_equal, fromstring, hopper -def test_sanity(): +def test_sanity() -> None: with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 8f15adac065..d32b6c09ba0 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -3,6 +3,6 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().tobytes() assert isinstance(data, bytes) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 01bf5a83918..d384d81414a 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -2,6 +2,7 @@ import pytest +from PIL import Image from PIL.Image import Transpose from . import helper @@ -14,7 +15,7 @@ @pytest.mark.parametrize("mode", HOPPER) -def test_flip_left_right(mode): +def test_flip_left_right(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_LEFT_RIGHT) assert out.mode == mode @@ -28,7 +29,7 @@ def test_flip_left_right(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_flip_top_bottom(mode): +def test_flip_top_bottom(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_TOP_BOTTOM) assert out.mode == mode @@ -42,7 +43,7 @@ def test_flip_top_bottom(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_90(mode): +def test_rotate_90(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_90) assert out.mode == mode @@ -56,7 +57,7 @@ def test_rotate_90(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_180(mode): +def test_rotate_180(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_180) assert out.mode == mode @@ -70,7 +71,7 @@ def test_rotate_180(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_270(mode): +def test_rotate_270(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_270) assert out.mode == mode @@ -84,7 +85,7 @@ def test_rotate_270(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_transpose(mode): +def test_transpose(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSPOSE) assert out.mode == mode @@ -98,7 +99,7 @@ def test_transpose(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_tranverse(mode): +def test_tranverse(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSVERSE) assert out.mode == mode @@ -112,10 +113,10 @@ def test_tranverse(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_roundtrip(mode): +def test_roundtrip(mode: str) -> None: im = HOPPER[mode] - def transpose(first, second): + def transpose(first: Transpose, second: Transpose) -> Image.Image: return im.transpose(first).transpose(second) assert_image_equal( From 945253672a74415807b5f685f54ebb1533ff468e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:11:18 +0200 Subject: [PATCH 0123/1178] Handle os.PathLike in is_path --- src/PIL/ImageFont.py | 2 +- src/PIL/_util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a63b73b33f5..9eecad1ca3a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,7 +230,7 @@ def load_from_bytes(f): ) if is_path(font): - if isinstance(font, Path): + if isinstance(font, os.PathLike): font = str(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 4ecdc4bd307..b649500abb5 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -8,7 +8,7 @@ def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: - return isinstance(f, (bytes, str, Path)) + return isinstance(f, (bytes, str, os.PathLike)) def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: From f613a9213f4edc7b58ac84a4793223c8e4fd9191 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:15:19 +0200 Subject: [PATCH 0124/1178] Parameterise test --- Tests/test_util.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 3395ef753d7..71a862569b7 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,27 +1,14 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import _util -def test_is_path(): - # Arrange - fp = "filename.ext" - - # Act - it_is = _util.is_path(fp) - - # Assert - assert it_is - - -def test_path_obj_is_path(): - # Arrange - from pathlib import Path - - test_path = Path("filename.ext") - +@pytest.mark.parametrize("test_path", ["filename.ext", Path("filename.ext")]) +def test_is_path(test_path): # Act it_is = _util.is_path(test_path) From 16d4068b42f0a6069e14b2327302df713dabbfed Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:17:13 +0200 Subject: [PATCH 0125/1178] Test os.PathLike that's not pathlib.Path --- Tests/test_util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 71a862569b7..617e5f7c6da 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,13 +1,15 @@ from __future__ import annotations -from pathlib import Path +from pathlib import Path, PurePath import pytest from PIL import _util -@pytest.mark.parametrize("test_path", ["filename.ext", Path("filename.ext")]) +@pytest.mark.parametrize( + "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] +) def test_is_path(test_path): # Act it_is = _util.is_path(test_path) From d631afc266c3c1214e12373d3ad0d16978867a7f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 20:46:58 +0200 Subject: [PATCH 0126/1178] Use os.fspath instead of isinstance and str --- src/PIL/ImageFont.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9eecad1ca3a..1feaf447a17 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,8 +230,7 @@ def load_from_bytes(f): ) if is_path(font): - if isinstance(font, os.PathLike): - font = str(font) + font = os.fspath(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From 737314923fd1abe8cea2b1986626302215436481 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 15:19:43 +1100 Subject: [PATCH 0127/1178] Added type hints --- Tests/test_000_sanity.py | 2 +- Tests/test_binary.py | 6 +++--- Tests/test_file_cur.py | 4 ++-- Tests/test_file_ftex.py | 6 +++--- Tests/test_file_gbr.py | 8 ++++---- Tests/test_file_gd.py | 6 +++--- Tests/test_file_gimppalette.py | 4 ++-- Tests/test_file_imt.py | 4 ++-- Tests/test_file_mcidas.py | 4 ++-- Tests/test_file_pcd.py | 2 +- Tests/test_file_pixar.py | 4 ++-- Tests/test_file_qoi.py | 4 ++-- Tests/test_file_wal.py | 4 ++-- Tests/test_file_webp_lossless.py | 4 +++- Tests/test_file_xpm.py | 6 +++--- Tests/test_file_xvthumb.py | 6 +++--- Tests/test_fontfile.py | 4 +++- Tests/test_format_lab.py | 6 +++--- Tests/test_lib_image.py | 2 +- Tests/test_locale.py | 2 +- Tests/test_main.py | 2 +- Tests/test_pyroma.py | 2 +- Tests/test_uploader.py | 4 ++-- Tests/test_util.py | 14 ++++++++------ Tests/test_webp_leaks.py | 4 ++-- 25 files changed, 60 insertions(+), 54 deletions(-) diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index f64216bca8e..c3926250f7b 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -3,7 +3,7 @@ from PIL import Image -def test_sanity(): +def test_sanity() -> None: # Make sure we have the binary extension Image.core.new("L", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 41fb93fcf48..d19799a095c 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -3,12 +3,12 @@ from PIL import _binary -def test_standard(): +def test_standard() -> None: assert _binary.i8(b"*") == 42 assert _binary.o8(42) == b"*" -def test_little_endian(): +def test_little_endian() -> None: assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 @@ -16,7 +16,7 @@ def test_little_endian(): assert _binary.o32le(65535) == b"\xff\xff\x00\x00" -def test_big_endian(): +def test_big_endian() -> None: assert _binary.i16be(b"\x00\x00\xff\xff") == 0 assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 27b2bc91489..dbf1b866d7f 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -7,7 +7,7 @@ TEST_FILE = "Tests/images/deerstalker.cur" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.size == (32, 32) assert isinstance(im, CurImagePlugin.CurImageFile) @@ -17,7 +17,7 @@ def test_sanity(): assert im.getpixel((16, 16)) == (84, 87, 86, 255) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 0f9154e3d09..0c544245a9f 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -7,18 +7,18 @@ from .helper import assert_image_equal_tofile, assert_image_similar -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/ftex_uncompressed.ftu") as im: assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") -def test_load_dxt1(): +def test_load_dxt1() -> None: with Image.open("Tests/images/ftex_dxt1.ftc") as im: with Image.open("Tests/images/ftex_dxt1.png") as target: assert_image_similar(im, target.convert("RGBA"), 15) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index d84004e1483..be98b08f2ad 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -7,12 +7,12 @@ from .helper import assert_image_equal_tofile -def test_gbr_file(): +def test_gbr_file() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_load(): +def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -20,14 +20,14 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_multiple_load_operations(): +def test_multiple_load_operations() -> None: with Image.open("Tests/images/gbr.gbr") as im: im.load() im.load() assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index e7db54fb44c..d512df284e1 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -7,18 +7,18 @@ TEST_GD_FILE = "Tests/images/hopper.gd" -def test_sanity(): +def test_sanity() -> None: with GdImageFile.open(TEST_GD_FILE) as im: assert im.size == (128, 128) assert im.format == "GD" -def test_bad_mode(): +def test_bad_mode() -> None: with pytest.raises(ValueError): GdImageFile.open(TEST_GD_FILE, "bad mode") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(UnidentifiedImageError): diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 28855c28acc..e8d5f170506 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -5,7 +5,7 @@ from PIL.GimpPaletteFile import GimpPaletteFile -def test_sanity(): +def test_sanity() -> None: with open("Tests/images/test.gpl", "rb") as fp: GimpPaletteFile(fp) @@ -22,7 +22,7 @@ def test_sanity(): GimpPaletteFile(fp) -def test_get_palette(): +def test_get_palette() -> None: # Arrange with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: palette_file = GimpPaletteFile(fp) diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index aa13d4407bc..6957dfa0ac5 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -9,13 +9,13 @@ from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/bw_gradient.imt") as im: assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") @pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) -def test_invalid_file(data): +def test_invalid_file(data: bytes) -> None: with io.BytesIO(data) as fp: with pytest.raises(SyntaxError): ImtImagePlugin.ImtImageFile(fp) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 73eba5cc861..2c94fdc3911 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -7,14 +7,14 @@ from .helper import assert_image_equal_tofile -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): McIdasImagePlugin.McIdasImageFile(invalid_file) -def test_valid_file(): +def test_valid_file() -> None: # Arrange # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 1a37c6ab31d..81a316fc14a 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -3,7 +3,7 @@ from PIL import Image -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: im.load() # should not segfault. diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index c6ddc54e714..8f208cfbf07 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -9,7 +9,7 @@ TEST_FILE = "Tests/images/hopper.pxr" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGB" @@ -21,7 +21,7 @@ def test_sanity(): assert_image_similar(im, im2, 4.8) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index 6dc468754ef..fd4b981ce93 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -7,7 +7,7 @@ from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/hopper.qoi") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -23,7 +23,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/pil123rgba.png") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 7acec975942..b34975e8380 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -7,7 +7,7 @@ TEST_FILE = "Tests/images/hopper.wal" -def test_open(): +def test_open() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.format == "WAL" assert im.format_description == "Quake2 Texture" @@ -19,7 +19,7 @@ def test_open(): assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") -def test_load(): +def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.load()[0, 0] == 122 diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 08c80973a74..32e29de56ad 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -10,7 +12,7 @@ RGB_MODE = "RGB" -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: if _webp.WebPDecoderVersion() < 0x0200: pytest.skip("lossless not included") diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 529a4558073..26afe93f450 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -9,7 +9,7 @@ TEST_FILE = "Tests/images/hopper.xpm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "P" @@ -20,14 +20,14 @@ def test_sanity(): assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XpmImagePlugin.XpmImageFile(invalid_file) -def test_load_read(): +def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_bytes = 1 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index b87494eba18..6b81159303e 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -9,7 +9,7 @@ TEST_FILE = "Tests/images/hopper.p7" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +20,7 @@ def test_open(): assert_image_similar(im, im_hopper, 9) -def test_unexpected_eof(): +def test_unexpected_eof() -> None: # Test unexpected EOF reading XV thumbnail file # Arrange bad_file = "Tests/images/hopper_bad.p7" @@ -30,7 +30,7 @@ def test_unexpected_eof(): XVThumbImagePlugin.XVThumbImageFile(bad_file) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index eda8fb81283..206499a047f 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,11 +1,13 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import FontFile -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: tempname = str(tmp_path / "temp.pil") font = FontFile.FontFile() diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index a55620e09ab..4fcc37e88cc 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -3,7 +3,7 @@ from PIL import Image -def test_white(): +def test_white() -> None: with Image.open("Tests/images/lab.tif") as i: i.load() @@ -24,7 +24,7 @@ def test_white(): assert list(b) == [128] * 100 -def test_green(): +def test_green() -> None: # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS # == RGB: 0, 152, 117 with Image.open("Tests/images/lab-green.tif") as i: @@ -32,7 +32,7 @@ def test_green(): assert k == (128, 28, 128) -def test_red(): +def test_red() -> None: # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS # == RGB: 255, 0, 124 with Image.open("Tests/images/lab-red.tif") as i: diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 1c642e4c981..31548bbc91f 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -5,7 +5,7 @@ from PIL import Image -def test_setmode(): +def test_setmode() -> None: im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_locale.py b/Tests/test_locale.py index db9557d7ba2..1c8b84a2b41 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -24,7 +24,7 @@ path = "Tests/images/hopper.jpg" -def test_sanity(): +def test_sanity() -> None: with Image.open(path): pass try: diff --git a/Tests/test_main.py b/Tests/test_main.py index 9f61a0c8169..46259f1dc52 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -5,7 +5,7 @@ import sys -def test_main(): +def test_main() -> None: out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") lines = out.splitlines() assert lines[0] == "-" * 68 diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2cea08ca61..c2f7fe22ecb 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -7,7 +7,7 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -def test_pyroma(): +def test_pyroma() -> None: # Arrange data = pyroma.projectdata.get_data(".") diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 75326288f97..d55ceb4be17 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -3,13 +3,13 @@ from .helper import assert_image_equal, assert_image_similar, hopper -def check_upload_equal(): +def check_upload_equal() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_equal(result, target) -def check_upload_similar(): +def check_upload_similar() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_similar(result, target, 0) diff --git a/Tests/test_util.py b/Tests/test_util.py index 3395ef753d7..b47ca88271c 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,11 +1,13 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import _util -def test_is_path(): +def test_is_path() -> None: # Arrange fp = "filename.ext" @@ -16,7 +18,7 @@ def test_is_path(): assert it_is -def test_path_obj_is_path(): +def test_path_obj_is_path() -> None: # Arrange from pathlib import Path @@ -29,7 +31,7 @@ def test_path_obj_is_path(): assert it_is -def test_is_not_path(tmp_path): +def test_is_not_path(tmp_path: Path) -> None: # Arrange with (tmp_path / "temp.ext").open("w") as fp: pass @@ -41,7 +43,7 @@ def test_is_not_path(tmp_path): assert not it_is_not -def test_is_directory(): +def test_is_directory() -> None: # Arrange directory = "Tests" @@ -52,7 +54,7 @@ def test_is_directory(): assert it_is -def test_is_not_directory(): +def test_is_not_directory() -> None: # Arrange text = "abc" @@ -63,7 +65,7 @@ def test_is_not_directory(): assert not it_is_not -def test_deferred_error(): +def test_deferred_error() -> None: # Arrange # Act diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 0f51abc9574..626fe427cab 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -14,11 +14,11 @@ class TestWebPLeaks(PillowLeakTestCase): mem_limit = 3 * 1024 # kb iterations = 100 - def test_leak_load(self): + def test_leak_load(self) -> None: with open(test_file, "rb") as f: im_data = f.read() - def core(): + def core() -> None: with Image.open(BytesIO(im_data)) as im: im.load() From cd640e5df27c6c3c58d8cc63c16cba71c237b9a4 Mon Sep 17 00:00:00 2001 From: Nicola Guerrera Date: Mon, 22 Jan 2024 15:19:59 +0100 Subject: [PATCH 0128/1178] Refactor grabclipboard() for x11 and wayland MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simpified logic and made it more robust against edge cases ( see the `allowed_errors` list ). Doing error checking this way, makes the behaviour of this function for x11 and wayland platforms more silimar to darwin and windows systems. fix typo src/PIL/ImageGrab.py Co-authored-by: Ondrej Baranovič fix typo src/PIL/ImageGrab.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> ImageGrab: \added debian edge case to comment --- src/PIL/ImageGrab.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index a4993d3d4b8..1cb02f5f93c 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -149,18 +149,7 @@ def grabclipboard(): session_type = None if shutil.which("wl-paste") and session_type in ("wayland", None): - output = subprocess.check_output(["wl-paste", "-l"]).decode() - mimetypes = output.splitlines() - if "image/png" in mimetypes: - mimetype = "image/png" - elif mimetypes: - mimetype = mimetypes[0] - else: - mimetype = None - - args = ["wl-paste"] - if mimetype: - args.extend(["-t", mimetype]) + args = ["wl-paste", "-t", "image"] elif shutil.which("xclip") and session_type in ("x11", None): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: @@ -168,10 +157,19 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr - if err: - msg = f"{args[0]} error: {err.strip().decode()}" + err = p.stderr.decode() + if p.returncode != 0: + allowed_errors = [ + "Nothing is copied", # wl-paste, when the clipboard is empty + "not available", # wl-paste/debian xclip, when an image isn't available + "cannot convert", # xclip, when an image isn't available + "There is no owner", # xclip, when the clipboard isn't initialized + ] + if any(e in err for e in allowed_errors): + return None + msg = f"{args[0]} error: {err.strip() if err else 'Unknown error'}" raise ChildProcessError(msg) + data = io.BytesIO(p.stdout) im = Image.open(data) im.load() From b81341ae7e62a246adabc40982d2b81ed3b7542d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 20:15:10 +1100 Subject: [PATCH 0129/1178] Only decode stderr when necessary --- src/PIL/ImageGrab.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 1cb02f5f93c..730351c0d68 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -157,17 +157,21 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr.decode() + err = p.stderr if p.returncode != 0: allowed_errors = [ - "Nothing is copied", # wl-paste, when the clipboard is empty - "not available", # wl-paste/debian xclip, when an image isn't available - "cannot convert", # xclip, when an image isn't available - "There is no owner", # xclip, when the clipboard isn't initialized + # wl-paste, when the clipboard is empty + b"Nothing is copied", + # wl-paste/debian xclip, when an image isn't available + b"not available", + # xclip, when an image isn't available + b"cannot convert", + # xclip, when the clipboard isn't initialized + b"There is no owner", ] if any(e in err for e in allowed_errors): return None - msg = f"{args[0]} error: {err.strip() if err else 'Unknown error'}" + msg = f"{args[0]} error: {err.strip().decode() if err else 'Unknown error'}" raise ChildProcessError(msg) data = io.BytesIO(p.stdout) From d2d9240de4cafee650f11c085a7ec321240a8e3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 19:26:55 +1100 Subject: [PATCH 0130/1178] Do not declare variable until necessary --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 730351c0d68..ca27b520caa 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -157,7 +157,6 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr if p.returncode != 0: allowed_errors = [ # wl-paste, when the clipboard is empty @@ -169,6 +168,7 @@ def grabclipboard(): # xclip, when the clipboard isn't initialized b"There is no owner", ] + err = p.stderr if any(e in err for e in allowed_errors): return None msg = f"{args[0]} error: {err.strip().decode() if err else 'Unknown error'}" From 6998f3476843e2f8da00eb23545aab55dc280006 Mon Sep 17 00:00:00 2001 From: Nicola Guerrera Date: Sat, 27 Jan 2024 12:08:16 +0100 Subject: [PATCH 0131/1178] Rearrange error handling Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index ca27b520caa..a2c7a935103 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -171,7 +171,9 @@ def grabclipboard(): err = p.stderr if any(e in err for e in allowed_errors): return None - msg = f"{args[0]} error: {err.strip().decode() if err else 'Unknown error'}" + msg = f"{args[0]} error" + if err: + msg += f": {err.strip().decode()}" raise ChildProcessError(msg) data = io.BytesIO(p.stdout) From d3205fae192ec10497326aacb7325f5880d07b04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 22:54:01 +1100 Subject: [PATCH 0132/1178] Simplified code --- src/PIL/ImageGrab.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index a2c7a935103..c04be521f91 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -158,7 +158,8 @@ def grabclipboard(): p = subprocess.run(args, capture_output=True) if p.returncode != 0: - allowed_errors = [ + err = p.stderr + for silent_error in [ # wl-paste, when the clipboard is empty b"Nothing is copied", # wl-paste/debian xclip, when an image isn't available @@ -167,10 +168,9 @@ def grabclipboard(): b"cannot convert", # xclip, when the clipboard isn't initialized b"There is no owner", - ] - err = p.stderr - if any(e in err for e in allowed_errors): - return None + ]: + if err in silent_error: + return None msg = f"{args[0]} error" if err: msg += f": {err.strip().decode()}" From 61d47c3dfa200b186ecacd7b9a5090cedb5523b6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:06:06 +0200 Subject: [PATCH 0133/1178] More support for arbitrary os.PathLike --- Tests/test_image.py | 3 +-- docs/reference/open_files.rst | 2 +- src/PIL/Image.py | 16 ++++++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index dd989ad99b5..84189df5477 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,6 +7,7 @@ import sys import tempfile import warnings +from pathlib import Path import pytest @@ -161,8 +162,6 @@ def test_stringio(self): pass def test_pathlib(self, tmp_path): - 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/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/Image.py b/src/PIL/Image.py index 553f36703b3..48125b3173b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -39,7 +39,6 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum -from pathlib import Path try: from defusedxml import ElementTree @@ -2370,7 +2369,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 @@ -2385,11 +2384,8 @@ def save(self, fp, format=None, **params) -> None: filename = "" open_fp = False - if isinstance(fp, Path): - filename = str(fp) - open_fp = True - elif is_path(fp): - filename = fp + if is_path(fp): + filename = os.fspath(fp) open_fp = True elif fp == sys.stdout: try: @@ -3206,7 +3202,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 @@ -3244,8 +3240,8 @@ def open(fp, mode="r", formats=None) -> Image: exclusive_fp = False filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) + if isinstance(fp, os.PathLike): + filename = os.path.realpath(os.fspath(fp)) elif is_path(fp): filename = fp From 52e51e12b950aac7a5bd5593ea9fc2981490c2d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:20:28 +0000 Subject: [PATCH 0134/1178] Update dependency cibuildwheel to v2.16.4 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index dd61634cd31..867543ebd84 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.16.2 +cibuildwheel==2.16.4 From 529487c244c64ee93ed7601eac9a9bbc3194827f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:48:39 +0200 Subject: [PATCH 0135/1178] Remove execute bit from setup.py --- setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 setup.py diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 From 866c26957d521ec02c6dc86c686800fe0a18a4d6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:37:24 +0200 Subject: [PATCH 0136/1178] Add check-shebang-scripts-are-executable to pre-commit --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6adc75b4902..5ce0c9a1792 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,6 +32,7 @@ repos: rev: v4.5.0 hooks: - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-json - id: check-toml From 0669532898c9cbb45ceebbeefb317dfcc97eaac1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:43:03 +0200 Subject: [PATCH 0137/1178] Remove shebangs --- Tests/check_fli_oob.py | 1 - Tests/images/create_eps.gnuplot | 2 -- Tests/oss-fuzz/fuzz_pillow.py | 2 -- setup.py | 1 - 4 files changed, 6 deletions(-) diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index ac46ff1ebc0..e0057a2c2ad 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations from PIL import Image diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/create_eps.gnuplot index 4d7e2987769..57a3c8c9725 100644 --- a/Tests/images/create_eps.gnuplot +++ b/Tests/images/create_eps.gnuplot @@ -1,5 +1,3 @@ -#!/usr/bin/gnuplot - #This is the script that was used to create our sample EPS files #We used the following version of the gnuplot program #G N U P L O T diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index e6e99d415a6..9137391b656 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/setup.py b/setup.py index 1bf0bcff558..1bbd2c05cc9 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . From 76955bbaf7ee718c743da8ba1866e5c98b69f272 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:43:51 +0200 Subject: [PATCH 0138/1178] Remove shebang and execute bit --- Tests/check_jp2_overflow.py | 2 -- 1 file changed, 2 deletions(-) mode change 100755 => 100644 Tests/check_jp2_overflow.py diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py old mode 100755 new mode 100644 index 5adbb84b69c..954d68bf7e3 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Reproductions/tests for OOB read errors in FliDecode.c # When run in python, all of these images should fail for From 139320be3a121dbc38d51baefda8d0b97441314d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jan 2024 19:44:42 +1100 Subject: [PATCH 0139/1178] Pin to Python 3.9.16-1 --- .github/workflows/test-cygwin.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9c3eb092417..b5c8c39aaef 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@v4 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 + uses: egor-tensin/setup-cygwin@v4 with: platform: x86_64 packages: > @@ -69,6 +69,7 @@ jobs: make netpbm perl + python39=3.9.16-1 python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel @@ -86,7 +87,7 @@ jobs: - name: Select Python version run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3 - name: Get latest NumPy version id: latest-numpy From 40fceedfba5d79cde4891ec70e69aee961cd3165 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jan 2024 22:22:13 +1100 Subject: [PATCH 0140/1178] brew remove libxau --- .github/workflows/wheels-dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec3148734f..50ac2e18e8b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -131,13 +131,13 @@ untar pillow-depends-main.zip if [[ -n "$IS_MACOS" ]]; then # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb - # libxdmcp causes an issue on macOS < 11 + # libxau and libxdmcp cause an issue on macOS < 11 # if php is installed, brew tries to reinstall these after installing openblas # remove cairo to fix building harfbuzz on arm64 # remove lcms2 and libpng to fix building openjpeg on arm64 # remove zstd to avoid inclusion on x86_64 # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd + brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd brew install pkg-config fi From b374f2679c4a0b102a1bc59b177a1a6b5cd0e1be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jan 2024 22:57:17 +1100 Subject: [PATCH 0141/1178] Build libxcb dependencies on macOS x86-64 --- .github/workflows/wheels-dependencies.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 50ac2e18e8b..26bf2f6d655 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -72,13 +72,11 @@ function build { build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then - cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc - fi + build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto + build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist + if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then + cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc fi else sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc From 7c6d8066452621f1adc54ca21305563e5fef99d5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:26:20 +0200 Subject: [PATCH 0142/1178] Test on macOS M1 where available --- .github/workflows/test.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e23f5c5b12..ae84a4d8fd5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: os: [ - "macos-latest", + "macos-14", "ubuntu-latest", ] python-version: [ @@ -50,11 +50,21 @@ jobs: "3.8", ] include: - - python-version: "3.9" + - python-version: "3.11" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - - python-version: "3.8" + - python-version: "3.10" PYTHONOPTIMIZE: 2 + # M1 only available for 3.10+ + - os: "macos-latest" + python-version: "3.9" + - os: "macos-latest" + python-version: "3.8" + exclude: + - os: "macos-14" + python-version: "3.9" + - os: "macos-14" + python-version: "3.8" runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} @@ -141,7 +151,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} + flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} gcov: true From d65f7b5ef7ef32905078e10af4471ce8bfa54c9f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 30 Jan 2024 22:10:22 +0200 Subject: [PATCH 0143/1178] brew install ghostscript --- .github/workflows/macos-install.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f41324c4ba6..28124d7f759 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,16 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm +brew install \ + freetype \ + ghostscript \ + libimagequant \ + libjpeg \ + libraqm \ + libtiff \ + little-cms2 \ + openjpeg \ + webp export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" # TODO Update condition when cffi supports 3.13 From 1dad1b87ed77e283d8fb10eee1e61e66c5173eba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:17:32 +0000 Subject: [PATCH 0144/1178] Update dependency cibuildwheel to v2.16.5 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 867543ebd84..ccd6d87edc6 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.16.4 +cibuildwheel==2.16.5 From 39cbd4f0f1bf4f40229f50aa5480b4b25eaae1a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jan 2024 16:31:03 +1100 Subject: [PATCH 0145/1178] Expanded error message strings --- src/PIL/ImageGrab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c04be521f91..b888e66f1c1 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -163,11 +163,11 @@ def grabclipboard(): # wl-paste, when the clipboard is empty b"Nothing is copied", # wl-paste/debian xclip, when an image isn't available - b"not available", + b" not available", # xclip, when an image isn't available - b"cannot convert", + b"cannot convert ", # xclip, when the clipboard isn't initialized - b"There is no owner", + b"xclip: Error: There is no owner for the ", ]: if err in silent_error: return None From 5efa2ade222785979c1b085be09eff5ee738c42c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jan 2024 16:53:27 +1100 Subject: [PATCH 0146/1178] Added test --- Tests/test_imagegrab.py | 12 ++++++++++++ src/PIL/ImageGrab.py | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 9d3d40398f6..efef4d90807 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -119,3 +119,15 @@ def test_grabclipboard_wl_clipboard(self, ext): subprocess.call(["wl-copy"], stdin=fp) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, image_path) + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("arg", ("text", "--clear")) + def test_grabclipboard_wl_clipboard_errors(self, arg): + subprocess.call(["wl-copy", arg]) + assert ImageGrab.grabclipboard() is None diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index b888e66f1c1..17f5750b1f3 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -162,7 +162,11 @@ def grabclipboard(): for silent_error in [ # wl-paste, when the clipboard is empty b"Nothing is copied", - # wl-paste/debian xclip, when an image isn't available + # Ubuntu/Debian wl-paste, when the clipboard is empty + b"No selection", + # Ubuntu/Debian wl-paste, when an image isn't available + b"No suitable type of content copied", + # wl-paste or Ubuntu/Debian xclip, when an image isn't available b" not available", # xclip, when an image isn't available b"cannot convert ", From d57b5e827cfd0e9850a074a4ba27e9f5ad0c9910 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jan 2024 16:49:44 +1100 Subject: [PATCH 0147/1178] Corrected check --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 17f5750b1f3..3f3be706d96 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -173,7 +173,7 @@ def grabclipboard(): # xclip, when the clipboard isn't initialized b"xclip: Error: There is no owner for the ", ]: - if err in silent_error: + if silent_error in err: return None msg = f"{args[0]} error" if err: From 4a4b90c3652d5036ab8d7d140763fa1eeef62985 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:12:58 +0200 Subject: [PATCH 0148/1178] Autotype tests (#7756) * autotyping: --none-return * autotyping: --scalar-return * autotyping: --int-param * autotyping: --float-param * autotyping: --str-param * autotyping: --annotate-named-param tmp_path:pathlib.Path --- Tests/bench_cffi_access.py | 8 +- Tests/test_bmp_reference.py | 6 +- Tests/test_box_blur.py | 34 +++--- Tests/test_color_lut.py | 52 ++++----- Tests/test_core_resources.py | 32 ++--- Tests/test_decompression_bomb.py | 28 ++--- Tests/test_deprecate.py | 14 +-- Tests/test_features.py | 30 ++--- Tests/test_file_apng.py | 52 +++++---- Tests/test_file_blp.py | 14 ++- Tests/test_file_bmp.py | 35 +++--- Tests/test_file_bufrstub.py | 16 +-- Tests/test_file_container.py | 20 ++-- Tests/test_file_dcx.py | 20 ++-- Tests/test_file_dds.py | 53 ++++----- Tests/test_file_eps.py | 71 ++++++------ Tests/test_file_fits.py | 10 +- Tests/test_file_fli.py | 30 ++--- Tests/test_file_fpx.py | 8 +- Tests/test_file_gif.py | 173 +++++++++++++-------------- Tests/test_file_gimpgradient.py | 20 ++-- Tests/test_file_gribstub.py | 16 +-- Tests/test_file_hdf5stub.py | 16 +-- Tests/test_file_icns.py | 23 ++-- Tests/test_file_ico.py | 33 +++--- Tests/test_file_im.py | 29 ++--- Tests/test_file_iptc.py | 18 +-- Tests/test_file_jpeg.py | 155 +++++++++++++------------ Tests/test_file_jpeg2k.py | 77 ++++++------ Tests/test_file_libtiff.py | 167 +++++++++++++------------- Tests/test_file_libtiff_small.py | 7 +- Tests/test_file_mic.py | 14 +-- Tests/test_file_mpo.py | 44 +++---- Tests/test_file_msp.py | 17 +-- Tests/test_file_palm.py | 13 ++- Tests/test_file_pcx.py | 32 ++--- Tests/test_file_pdf.py | 35 +++--- Tests/test_file_png.py | 109 ++++++++--------- Tests/test_file_ppm.py | 51 ++++---- Tests/test_file_psd.py | 34 +++--- Tests/test_file_sgi.py | 24 ++-- Tests/test_file_spider.py | 35 +++--- Tests/test_file_sun.py | 6 +- Tests/test_file_tar.py | 8 +- Tests/test_file_tga.py | 31 ++--- Tests/test_file_tiff.py | 151 ++++++++++++------------ Tests/test_file_tiff_metadata.py | 49 ++++---- Tests/test_file_webp.py | 41 +++---- Tests/test_file_webp_alpha.py | 14 ++- Tests/test_file_webp_animated.py | 18 +-- Tests/test_file_webp_metadata.py | 17 +-- Tests/test_file_wmf.py | 16 +-- Tests/test_file_xbm.py | 13 ++- Tests/test_format_hsv.py | 8 +- Tests/test_image.py | 155 +++++++++++++------------ Tests/test_image_access.py | 44 +++---- Tests/test_image_array.py | 10 +- Tests/test_image_draft.py | 6 +- Tests/test_image_entropy.py | 2 +- Tests/test_image_filter.py | 24 ++-- Tests/test_image_getextrema.py | 4 +- Tests/test_image_getpalette.py | 4 +- Tests/test_image_load.py | 10 +- Tests/test_image_mode.py | 4 +- Tests/test_image_paste.py | 28 ++--- Tests/test_image_putdata.py | 20 ++-- Tests/test_image_putpalette.py | 12 +- Tests/test_image_reduce.py | 40 ++++--- Tests/test_image_resample.py | 80 ++++++------- Tests/test_image_rotate.py | 30 ++--- Tests/test_image_thumbnail.py | 20 ++-- Tests/test_image_transform.py | 38 +++--- Tests/test_imagechops.py | 60 +++++----- Tests/test_imagecms.py | 71 ++++++------ Tests/test_imagecolor.py | 12 +- Tests/test_imagedraw.py | 174 ++++++++++++++-------------- Tests/test_imagedraw2.py | 24 ++-- Tests/test_imageenhance.py | 8 +- Tests/test_imagefile.py | 58 +++++----- Tests/test_imagefont.py | 122 +++++++++---------- Tests/test_imagefontctl.py | 40 +++---- Tests/test_imagefontpil.py | 16 +-- Tests/test_imagegrab.py | 16 +-- Tests/test_imagemath.py | 50 ++++---- Tests/test_imagemorph.py | 40 ++++--- Tests/test_imageops.py | 50 ++++---- Tests/test_imageops_usm.py | 10 +- Tests/test_imagepalette.py | 30 ++--- Tests/test_imagepath.py | 24 ++-- Tests/test_imageqt.py | 8 +- Tests/test_imagesequence.py | 20 ++-- Tests/test_imageshow.py | 18 +-- Tests/test_imagestat.py | 6 +- Tests/test_imagetk.py | 12 +- Tests/test_imagewin.py | 16 +-- Tests/test_imagewin_pointers.py | 3 +- Tests/test_lib_pack.py | 80 ++++++------- Tests/test_map.py | 6 +- Tests/test_mode_i16.py | 10 +- Tests/test_numpy.py | 32 ++--- Tests/test_pdfparser.py | 10 +- Tests/test_pickle.py | 17 +-- Tests/test_psdraw.py | 7 +- Tests/test_qt_image_qapplication.py | 8 +- Tests/test_qt_image_toqimage.py | 4 +- Tests/test_sgi_crash.py | 2 +- Tests/test_shell_injection.py | 11 +- Tests/test_tiff_ifdrational.py | 11 +- 108 files changed, 1866 insertions(+), 1798 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index ad15a9739b2..c4ab3bdccb3 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -9,21 +9,21 @@ # Not running this test by default. No DOS against CI. -def iterate_get(size, access): +def iterate_get(size, access) -> None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] -def iterate_set(size, access): +def iterate_set(size, access) -> None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] = (x % 256, y % 256, 0) -def timer(func, label, *args): +def timer(func, label, *args) -> None: iterations = 5000 starttime = time.time() for x in range(iterations): @@ -38,7 +38,7 @@ def timer(func, label, *args): ) -def test_direct(): +def test_direct() -> None: im = hopper() im.load() # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 0da41e85845..22ac9443e86 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -10,13 +10,13 @@ base = os.path.join("Tests", "images", "bmp") -def get_files(d, ext=".bmp"): +def get_files(d, ext: str = ".bmp"): return [ os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f ] -def test_bad(): +def test_bad() -> None: """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): @@ -56,7 +56,7 @@ def test_questionable(): raise -def test_good(): +def test_good() -> None: """These should all work. There's a set of target files in the html directory that we can compare against.""" diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 461e6aaacb3..dfedb48d911 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -16,18 +16,18 @@ # fmt: on -def test_imageops_box_blur(): +def test_imageops_box_blur() -> None: i = sample.filter(ImageFilter.BoxBlur(1)) assert i.mode == sample.mode assert i.size == sample.size assert isinstance(i, Image.Image) -def box_blur(image, radius=1, n=1): +def box_blur(image, radius: int = 1, n: int = 1): return image._new(image.im.box_blur((radius, radius), n)) -def assert_image(im, data, delta=0): +def assert_image(im, data, delta: int = 0) -> None: it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -37,7 +37,7 @@ def assert_image(im, data, delta=0): next(it) -def assert_blur(im, radius, data, passes=1, delta=0): +def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None: # check grayscale image assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) @@ -45,7 +45,7 @@ def assert_blur(im, radius, data, passes=1, delta=0): assert_image(band, data, delta) -def test_color_modes(): +def test_color_modes() -> None: with pytest.raises(ValueError): box_blur(sample.convert("1")) with pytest.raises(ValueError): @@ -65,7 +65,7 @@ def test_color_modes(): box_blur(sample.convert("YCbCr")) -def test_radius_0(): +def test_radius_0() -> None: assert_blur( sample, 0, @@ -81,7 +81,7 @@ def test_radius_0(): ) -def test_radius_0_02(): +def test_radius_0_02() -> None: assert_blur( sample, 0.02, @@ -98,7 +98,7 @@ def test_radius_0_02(): ) -def test_radius_0_05(): +def test_radius_0_05() -> None: assert_blur( sample, 0.05, @@ -115,7 +115,7 @@ def test_radius_0_05(): ) -def test_radius_0_1(): +def test_radius_0_1() -> None: assert_blur( sample, 0.1, @@ -132,7 +132,7 @@ def test_radius_0_1(): ) -def test_radius_0_5(): +def test_radius_0_5() -> None: assert_blur( sample, 0.5, @@ -149,7 +149,7 @@ def test_radius_0_5(): ) -def test_radius_1(): +def test_radius_1() -> None: assert_blur( sample, 1, @@ -166,7 +166,7 @@ def test_radius_1(): ) -def test_radius_1_5(): +def test_radius_1_5() -> None: assert_blur( sample, 1.5, @@ -183,7 +183,7 @@ def test_radius_1_5(): ) -def test_radius_bigger_then_half(): +def test_radius_bigger_then_half() -> None: assert_blur( sample, 3, @@ -200,7 +200,7 @@ def test_radius_bigger_then_half(): ) -def test_radius_bigger_then_width(): +def test_radius_bigger_then_width() -> None: assert_blur( sample, 10, @@ -215,7 +215,7 @@ def test_radius_bigger_then_width(): ) -def test_extreme_large_radius(): +def test_extreme_large_radius() -> None: assert_blur( sample, 600, @@ -230,7 +230,7 @@ def test_extreme_large_radius(): ) -def test_two_passes(): +def test_two_passes() -> None: assert_blur( sample, 1, @@ -248,7 +248,7 @@ def test_two_passes(): ) -def test_three_passes(): +def test_three_passes() -> None: assert_blur( sample, 1, diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index fcd1169ef81..e6c8d7819a6 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -41,7 +41,7 @@ def generate_identity_table(self, channels, size): [item for sublist in table for item in sublist], ) - def test_wrong_args(self): + def test_wrong_args(self) -> None: im = Image.new("RGB", (10, 10), 0) with pytest.raises(ValueError, match="filter"): @@ -101,7 +101,7 @@ def test_wrong_args(self): with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) - def test_correct_args(self): + def test_correct_args(self) -> None: im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d( @@ -136,7 +136,7 @@ def test_correct_args(self): *self.generate_identity_table(3, (3, 3, 65)), ) - def test_wrong_mode(self): + def test_wrong_mode(self) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) im.im.color_lut_3d( @@ -167,7 +167,7 @@ def test_wrong_mode(self): "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_correct_mode(self): + def test_correct_mode(self) -> None: im = Image.new("RGBA", (10, 10), 0) im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) @@ -188,7 +188,7 @@ def test_correct_mode(self): "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_identities(self): + def test_identities(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -224,7 +224,7 @@ def test_identities(self): ), ) - def test_identities_4_channels(self): + def test_identities_4_channels(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -247,7 +247,7 @@ def test_identities_4_channels(self): ), ) - def test_copy_alpha_channel(self): + def test_copy_alpha_channel(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGBA", @@ -270,7 +270,7 @@ def test_copy_alpha_channel(self): ), ) - def test_channels_order(self): + def test_channels_order(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -295,7 +295,7 @@ def test_channels_order(self): ]))) # fmt: on - def test_overflow(self): + def test_overflow(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -348,7 +348,7 @@ def test_overflow(self): class TestColorLut3DFilter: - def test_wrong_args(self): + def test_wrong_args(self) -> None: with pytest.raises(ValueError, match="should be either an integer"): ImageFilter.Color3DLUT("small", [1]) @@ -376,7 +376,7 @@ def test_wrong_args(self): with pytest.raises(ValueError, match="Only 3 or 4 output"): ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) - def test_convert_table(self): + def test_convert_table(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert tuple(lut.size) == (2, 2, 2) assert lut.name == "Color 3D LUT" @@ -394,7 +394,7 @@ def test_convert_table(self): assert lut.table == list(range(4)) * 8 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_sources(self): + def test_numpy_sources(self) -> None: table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) with pytest.raises(ValueError, match="should have either channels"): lut = ImageFilter.Color3DLUT((5, 6, 7), table) @@ -427,7 +427,7 @@ def test_numpy_sources(self): assert lut.table[0] == 33 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_formats(self): + def test_numpy_formats(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -466,7 +466,7 @@ def test_numpy_formats(self): lut.table = numpy.array(lut.table, dtype=numpy.int8) im.filter(lut) - def test_repr(self): + def test_repr(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert repr(lut) == "" @@ -484,7 +484,7 @@ def test_repr(self): class TestGenerateColorLut3D: - def test_wrong_channels_count(self): + def test_wrong_channels_count(self) -> None: with pytest.raises(ValueError, match="3 or 4 output channels"): ImageFilter.Color3DLUT.generate( 5, channels=2, callback=lambda r, g, b: (r, g, b) @@ -498,7 +498,7 @@ def test_wrong_channels_count(self): 5, channels=4, callback=lambda r, g, b: (r, g, b) ) - def test_3_channels(self): + def test_3_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) assert tuple(lut.size) == (5, 5, 5) assert lut.name == "Color 3D LUT" @@ -508,7 +508,7 @@ def test_3_channels(self): 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0] # fmt: on - def test_4_channels(self): + def test_4_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate( 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) ) @@ -521,7 +521,7 @@ def test_4_channels(self): ] # fmt: on - def test_apply(self): + def test_apply(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) g = Image.linear_gradient("L") @@ -537,7 +537,7 @@ def test_apply(self): class TestTransformColorLut3D: - def test_wrong_args(self): + def test_wrong_args(self) -> None: source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) with pytest.raises(ValueError, match="Only 3 or 4 output"): @@ -552,7 +552,7 @@ def test_wrong_args(self): with pytest.raises(TypeError): source.transform(lambda r, g, b, a: (r, g, b)) - def test_target_mode(self): + def test_target_mode(self) -> None: source = ImageFilter.Color3DLUT.generate( 2, lambda r, g, b: (r, g, b), target_mode="HSV" ) @@ -563,7 +563,7 @@ def test_target_mode(self): lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") assert lut.mode == "RGB" - def test_3_to_3_channels(self): + def test_3_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) assert tuple(lut.size) == tuple(source.size) @@ -571,7 +571,7 @@ def test_3_to_3_channels(self): assert lut.table != source.table assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] - def test_3_to_4_channels(self): + def test_3_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) assert tuple(lut.size) == tuple(source.size) @@ -583,7 +583,7 @@ def test_3_to_4_channels(self): 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] # fmt: on - def test_4_to_3_channels(self): + def test_4_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -599,7 +599,7 @@ def test_4_to_3_channels(self): 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] # fmt: on - def test_4_to_4_channels(self): + def test_4_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -613,7 +613,7 @@ def test_4_to_4_channels(self): 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] # fmt: on - def test_with_normals_3_channels(self): + def test_with_normals_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) ) @@ -629,7 +629,7 @@ def test_with_normals_3_channels(self): 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] # fmt: on - def test_with_normals_4_channels(self): + def test_with_normals_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 ) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index d3f76fdb1b9..5eabe8f11ba 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -9,7 +9,7 @@ from .helper import is_pypy -def test_get_stats(): +def test_get_stats() -> None: # Create at least one image Image.new("RGB", (10, 10)) @@ -22,7 +22,7 @@ def test_get_stats(): assert "blocks_cached" in stats -def test_reset_stats(): +def test_reset_stats() -> None: Image.core.reset_stats() stats = Image.core.get_stats() @@ -35,19 +35,19 @@ def test_reset_stats(): class TestCoreMemory: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_get_alignment(self): + def test_get_alignment(self) -> None: alignment = Image.core.get_alignment() assert alignment > 0 - def test_set_alignment(self): + def test_set_alignment(self) -> None: for i in [1, 2, 4, 8, 16, 32]: Image.core.set_alignment(i) alignment = Image.core.get_alignment() @@ -63,12 +63,12 @@ def test_set_alignment(self): with pytest.raises(ValueError): Image.core.set_alignment(3) - def test_get_block_size(self): + def test_get_block_size(self) -> None: block_size = Image.core.get_block_size() assert block_size >= 4096 - def test_set_block_size(self): + def test_set_block_size(self) -> None: for i in [4096, 2 * 4096, 3 * 4096]: Image.core.set_block_size(i) block_size = Image.core.get_block_size() @@ -84,7 +84,7 @@ def test_set_block_size(self): with pytest.raises(ValueError): Image.core.set_block_size(4000) - def test_set_block_size_stats(self): + def test_set_block_size_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -96,12 +96,12 @@ def test_set_block_size_stats(self): if not is_pypy(): assert stats["freed_blocks"] >= 64 - def test_get_blocks_max(self): + def test_get_blocks_max(self) -> None: blocks_max = Image.core.get_blocks_max() assert blocks_max >= 0 - def test_set_blocks_max(self): + def test_set_blocks_max(self) -> None: for i in [0, 1, 10]: Image.core.set_blocks_max(i) blocks_max = Image.core.get_blocks_max() @@ -117,7 +117,7 @@ def test_set_blocks_max(self): Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_set_blocks_max_stats(self): + def test_set_blocks_max_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(128) Image.core.set_block_size(4096) @@ -132,7 +132,7 @@ def test_set_blocks_max_stats(self): assert stats["blocks_cached"] == 64 @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_clear_cache_stats(self): + def test_clear_cache_stats(self) -> None: Image.core.reset_stats() Image.core.clear_cache() Image.core.set_blocks_max(128) @@ -149,7 +149,7 @@ def test_clear_cache_stats(self): assert stats["freed_blocks"] >= 48 assert stats["blocks_cached"] == 16 - def test_large_images(self): + def test_large_images(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -166,14 +166,14 @@ def test_large_images(self): class TestEnvVars: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_units(self): + def test_units(self) -> None: Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) assert Image.core.get_blocks_max() == 2 * 1024 Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) @@ -187,6 +187,6 @@ def test_units(self): {"PILLOW_BLOCKS_MAX": "wat"}, ), ) - def test_warnings(self, var): + def test_warnings(self, var) -> None: with pytest.warns(UserWarning): Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index d3049eff124..9c21efa45f7 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -12,16 +12,16 @@ class TestDecompressionBomb: - def teardown_method(self, method): + def teardown_method(self, method) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_no_warning_small_file(self): + def test_no_warning_small_file(self) -> None: # Implicit assert: no warning. # A warning would cause a failure. with Image.open(TEST_FILE): pass - def test_no_warning_no_limit(self): + def test_no_warning_no_limit(self) -> None: # Arrange # Turn limit off Image.MAX_IMAGE_PIXELS = None @@ -33,7 +33,7 @@ def test_no_warning_no_limit(self): with Image.open(TEST_FILE): pass - def test_warning(self): + def test_warning(self) -> None: # Set limit to trigger warning on the test file Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 @@ -42,7 +42,7 @@ def test_warning(self): with Image.open(TEST_FILE): pass - def test_exception(self): + def test_exception(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 @@ -51,22 +51,22 @@ def test_exception(self): with Image.open(TEST_FILE): pass - def test_exception_ico(self): + def test_exception_ico(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.ico"): pass - def test_exception_gif(self): + def test_exception_gif(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.gif"): pass - def test_exception_gif_extents(self): + def test_exception_gif_extents(self) -> None: with Image.open("Tests/images/decompression_bomb_extents.gif") as im: with pytest.raises(Image.DecompressionBombError): im.seek(1) - def test_exception_gif_zero_width(self): + def test_exception_gif_zero_width(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 @@ -75,7 +75,7 @@ def test_exception_gif_zero_width(self): with Image.open("Tests/images/zero_width.gif"): pass - def test_exception_bmp(self): + def test_exception_bmp(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): pass @@ -83,15 +83,15 @@ def test_exception_bmp(self): class TestDecompressionCrop: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: width, height = 128, 128 Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_enlarge_crop(self): + def test_enlarge_crop(self) -> None: # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. with hopper() as src: @@ -99,7 +99,7 @@ def test_enlarge_crop(self): with pytest.warns(Image.DecompressionBombWarning): src.crop(box) - def test_crop_decompression_checks(self): + def test_crop_decompression_checks(self) -> None: im = Image.new("RGB", (100, 100)) for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 6c7f509a795..6ffc8f6f589 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -20,12 +20,12 @@ ), ], ) -def test_version(version, expected): +def test_version(version, expected) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") -def test_unknown_version(): +def test_unknown_version() -> None: expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") @@ -46,13 +46,13 @@ def test_unknown_version(): ), ], ) -def test_old_version(deprecated, plural, expected): +def test_old_version(deprecated, plural, expected) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) -def test_plural(): +def test_plural() -> None: expected = ( r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Use new thing instead\." @@ -61,7 +61,7 @@ def test_plural(): _deprecate.deprecate("Old things", 11, "new thing", plural=True) -def test_replacement_and_action(): +def test_replacement_and_action() -> None: expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( @@ -76,7 +76,7 @@ def test_replacement_and_action(): "Upgrade to new thing.", ], ) -def test_action(action): +def test_action(action) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." @@ -85,7 +85,7 @@ def test_action(action): _deprecate.deprecate("Old thing", 11, action=action) -def test_no_replacement_or_action(): +def test_no_replacement_or_action() -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" ) diff --git a/Tests/test_features.py b/Tests/test_features.py index b90c1d25f2b..de74e9c1829 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -15,7 +15,7 @@ pass -def test_check(): +def test_check() -> None: # Check the correctness of the convenience function for module in features.modules: assert features.check_module(module) == features.check(module) @@ -25,11 +25,11 @@ def test_check(): assert features.check_feature(feature) == features.check(feature) -def test_version(): +def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function): + def test(name, function) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -47,56 +47,56 @@ def test(name, function): @skip_unless_feature("webp") -def test_webp_transparency(): +def test_webp_transparency() -> None: assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY @skip_unless_feature("webp") -def test_webp_mux(): +def test_webp_mux() -> None: assert features.check("webp_mux") == _webp.HAVE_WEBPMUX @skip_unless_feature("webp") -def test_webp_anim(): +def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM @skip_unless_feature("libjpeg_turbo") -def test_libjpeg_turbo_version(): +def test_libjpeg_turbo_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) @skip_unless_feature("libimagequant") -def test_libimagequant_version(): +def test_libimagequant_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature): +def test_check_modules(feature) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature): +def test_check_codecs(feature) -> None: assert features.check_codec(feature) in [True, False] -def test_check_warns_on_nonexistent(): +def test_check_warns_on_nonexistent() -> None: with pytest.warns(UserWarning) as cm: has_feature = features.check("typo") assert has_feature is False assert str(cm[-1].message) == "Unknown feature 'typo'." -def test_supported_modules(): +def test_supported_modules() -> None: assert isinstance(features.get_supported_modules(), list) assert isinstance(features.get_supported_codecs(), list) assert isinstance(features.get_supported_features(), list) assert isinstance(features.get_supported(), list) -def test_unsupported_codec(): +def test_unsupported_codec() -> None: # Arrange codec = "unsupported_codec" # Act / Assert @@ -106,7 +106,7 @@ def test_unsupported_codec(): features.version_codec(codec) -def test_unsupported_module(): +def test_unsupported_module() -> None: # Arrange module = "unsupported_module" # Act / Assert @@ -116,7 +116,7 @@ def test_unsupported_module(): features.version_module(module) -def test_pilinfo(): +def test_pilinfo() -> None: buf = io.StringIO() features.pilinfo(buf) out = buf.getvalue() diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 23263b5d459..f9edf6e9877 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageSequence, PngImagePlugin @@ -8,7 +10,7 @@ # APNG browser support tests and fixtures via: # https://philip.html5.org/tests/apng/tests.html # (referenced from https://wiki.mozilla.org/APNG_Specification) -def test_apng_basic(): +def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: assert not im.is_animated assert im.n_frames == 1 @@ -45,14 +47,14 @@ def test_apng_basic(): "filename", ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ) -def test_apng_fdat(filename): +def test_apng_fdat(filename) -> None: with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose(): +def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -84,7 +86,7 @@ def test_apng_dispose(): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_dispose_region(): +def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -106,7 +108,7 @@ def test_apng_dispose_region(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose_op_previous_frame(): +def test_apng_dispose_op_previous_frame() -> None: # Test that the dispose settings being used are from the previous frame # # Image created with: @@ -131,14 +133,14 @@ def test_apng_dispose_op_previous_frame(): assert im.getpixel((0, 0)) == (255, 0, 0, 255) -def test_apng_dispose_op_background_p_mode(): +def test_apng_dispose_op_background_p_mode() -> None: with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: im.seek(1) im.load() assert im.size == (128, 64) -def test_apng_blend(): +def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -165,20 +167,20 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_blend_transparency(): +def test_apng_blend_transparency() -> None: with Image.open("Tests/images/blend_transparency.png") as im: im.seek(1) assert im.getpixel((0, 0)) == (255, 0, 0) -def test_apng_chunk_order(): +def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_delay(): +def test_apng_delay() -> None: with Image.open("Tests/images/apng/delay.png") as im: im.seek(1) assert im.info.get("duration") == 500.0 @@ -218,7 +220,7 @@ def test_apng_delay(): assert im.info.get("duration") == 1000.0 -def test_apng_num_plays(): +def test_apng_num_plays() -> None: with Image.open("Tests/images/apng/num_plays.png") as im: assert im.info.get("loop") == 0 @@ -226,7 +228,7 @@ def test_apng_num_plays(): assert im.info.get("loop") == 1 -def test_apng_mode(): +def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: assert im.mode == "RGBA" im.seek(im.n_frames - 1) @@ -267,7 +269,7 @@ def test_apng_mode(): assert im.getpixel((64, 32)) == (0, 0, 255, 128) -def test_apng_chunk_errors(): +def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated @@ -292,7 +294,7 @@ def test_apng_chunk_errors(): im.seek(im.n_frames - 1) -def test_apng_syntax_errors(): +def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated @@ -336,14 +338,14 @@ def test_apng_syntax_errors(): "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(test_file): +def test_apng_sequence_errors(test_file) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) im.load() -def test_apng_save(tmp_path): +def test_apng_save(tmp_path: Path) -> None: with Image.open("Tests/images/apng/single_frame.png") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, save_all=True) @@ -374,7 +376,7 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_alpha(tmp_path): +def test_apng_save_alpha(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) @@ -388,7 +390,7 @@ def test_apng_save_alpha(tmp_path): assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) -def test_apng_save_split_fdat(tmp_path): +def test_apng_save_split_fdat(tmp_path: Path) -> None: # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case # both the default image and first animation frame will span multiple @@ -412,7 +414,7 @@ def test_apng_save_split_fdat(tmp_path): assert exception is None -def test_apng_save_duration_loop(tmp_path): +def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/apng/delay.png") as im: frames = [] @@ -475,7 +477,7 @@ def test_apng_save_duration_loop(tmp_path): assert im.info["duration"] == 600 -def test_apng_save_disposal(tmp_path): +def test_apng_save_disposal(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -576,7 +578,7 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_save_disposal_previous(tmp_path): +def test_apng_save_disposal_previous(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) blue = Image.new("RGBA", size, (0, 0, 255, 255)) @@ -598,7 +600,7 @@ def test_apng_save_disposal_previous(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_blend(tmp_path): +def test_apng_save_blend(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -666,7 +668,7 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/apng/delay.png") im.seek(1) im.close() @@ -678,7 +680,9 @@ def test_seek_after_close(): @pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) @pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("duplicate", (True, False)) -def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path): +def test_different_modes_in_later_frames( + mode, default_image, duplicate, tmp_path: Path +) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("L", (1, 1)) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 27ff7ab6640..3904d3bc5b5 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -12,7 +14,7 @@ ) -def test_load_blp1(): +def test_load_blp1() -> None: with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") @@ -20,22 +22,22 @@ def test_load_blp1(): im.load() -def test_load_blp2_raw(): +def test_load_blp2_raw() -> None: with Image.open("Tests/images/blp/blp2_raw.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") -def test_load_blp2_dxt1(): +def test_load_blp2_dxt1() -> None: with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") -def test_load_blp2_dxt1a(): +def test_load_blp2_dxt1a() -> None: with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: f = str(tmp_path / "temp.blp") for version in ("BLP1", "BLP2"): @@ -69,7 +71,7 @@ def test_save(tmp_path): "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file): +def test_crashes(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 225fb28ba51..c36466e0269 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +from pathlib import Path import pytest @@ -14,8 +15,8 @@ ) -def test_sanity(tmp_path): - def roundtrip(im): +def test_sanity(tmp_path: Path) -> None: + def roundtrip(im) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -35,20 +36,20 @@ def roundtrip(im): roundtrip(hopper("RGB")) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BmpImagePlugin.BmpImageFile(fp) -def test_fallback_if_mmap_errors(): +def test_fallback_if_mmap_errors() -> None: # This image has been truncated, # so that the buffer is not large enough when using mmap with Image.open("Tests/images/mmap_error.bmp") as im: assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "BMP") @@ -60,7 +61,7 @@ def test_save_to_bytes(): assert reloaded.format == "BMP" -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] im.putpalette(colors) @@ -72,7 +73,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_too_large(tmp_path): +def test_save_too_large(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.new("RGB", (1, 1)) as im: im._size = (37838, 37838) @@ -80,7 +81,7 @@ def test_save_too_large(tmp_path): im.save(outfile) -def test_dpi(): +def test_dpi() -> None: dpi = (72, 72) output = io.BytesIO() @@ -92,7 +93,7 @@ def test_dpi(): assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) -def test_save_bmp_with_dpi(tmp_path): +def test_save_bmp_with_dpi(tmp_path: Path) -> None: # Test for #1301 # Arrange outfile = str(tmp_path / "temp.jpg") @@ -110,7 +111,7 @@ def test_save_bmp_with_dpi(tmp_path): assert reloaded.format == "JPEG" -def test_save_float_dpi(tmp_path): +def test_save_float_dpi(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.open("Tests/images/hopper.bmp") as im: im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) @@ -118,7 +119,7 @@ def test_save_float_dpi(tmp_path): assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) -def test_load_dib(): +def test_load_dib() -> None: # test for #1293, Imagegrab returning Unsupported Bitfields Format with Image.open("Tests/images/clipboard.dib") as im: assert im.format == "DIB" @@ -127,7 +128,7 @@ def test_load_dib(): assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") -def test_save_dib(tmp_path): +def test_save_dib(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.dib") with Image.open("Tests/images/clipboard.dib") as im: @@ -139,7 +140,7 @@ def test_save_dib(tmp_path): assert_image_equal(im, reloaded) -def test_rgba_bitfields(): +def test_rgba_bitfields() -> None: # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: @@ -157,7 +158,7 @@ def test_rgba_bitfields(): ) -def test_rle8(): +def test_rle8() -> None: with Image.open("Tests/images/hopper_rle8.bmp") as im: assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) @@ -177,7 +178,7 @@ def test_rle8(): im.load() -def test_rle4(): +def test_rle4() -> None: with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) @@ -193,7 +194,7 @@ def test_rle4(): ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length): +def test_rle8_eof(file_name, length) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: @@ -201,7 +202,7 @@ def test_rle8_eof(file_name, length): im.load() -def test_offset(): +def test_offset() -> None: # This image has been hexedited # to exclude the palette size from the pixel data offset with Image.open("Tests/images/pal8_offset.bmp") as im: diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 45081832e68..3dd24533aae 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import BufrStubImagePlugin, Image @@ -9,7 +11,7 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +22,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -29,7 +31,7 @@ def test_invalid_file(): BufrStubImagePlugin.BufrStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -37,7 +39,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.bufr") @@ -47,13 +49,13 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -61,7 +63,7 @@ def load(self, im): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 95a5b2337d8..4dba4be5d5c 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -9,19 +9,19 @@ TEST_FILE = "Tests/images/dummy.container" -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ContainerIO) -def test_isatty(): +def test_isatty() -> None: with hopper() as im: container = ContainerIO.ContainerIO(im, 0, 0) assert container.isatty() is False -def test_seek_mode_0(): +def test_seek_mode_0() -> None: # Arrange mode = 0 with open(TEST_FILE, "rb") as fh: @@ -35,7 +35,7 @@ def test_seek_mode_0(): assert container.tell() == 33 -def test_seek_mode_1(): +def test_seek_mode_1() -> None: # Arrange mode = 1 with open(TEST_FILE, "rb") as fh: @@ -49,7 +49,7 @@ def test_seek_mode_1(): assert container.tell() == 66 -def test_seek_mode_2(): +def test_seek_mode_2() -> None: # Arrange mode = 2 with open(TEST_FILE, "rb") as fh: @@ -64,7 +64,7 @@ def test_seek_mode_2(): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode): +def test_read_n0(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -80,7 +80,7 @@ def test_read_n0(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode): +def test_read_n(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -96,7 +96,7 @@ def test_read_n(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode): +def test_read_eof(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -112,7 +112,7 @@ def test_read_eof(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode): +def test_readline(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) @@ -127,7 +127,7 @@ def test_readline(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode): +def test_readlines(bytesmode) -> None: # Arrange expected = [ "This is line 1\n", diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index cba7c10bf76..65337cad9b1 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -12,7 +12,7 @@ TEST_FILE = "Tests/images/hopper.dcx" -def test_sanity(): +def test_sanity() -> None: # Arrange # Act @@ -25,8 +25,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -34,26 +34,26 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): DcxImagePlugin.DcxImageFile(fp) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -63,13 +63,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_FILE) as im: n_frames = im.n_frames @@ -82,7 +82,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_too_far(): +def test_seek_too_far() -> None: # Arrange with Image.open(TEST_FILE) as im: frame = 999 # too big on purpose diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7064b74c07b..09ee8986aca 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -2,6 +2,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -46,7 +47,7 @@ TEST_FILE_DX10_BC1_TYPELESS, ), ) -def test_sanity_dxt1_bc1(image_path): +def test_sanity_dxt1_bc1(image_path) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") @@ -60,7 +61,7 @@ def test_sanity_dxt1_bc1(image_path): assert_image_equal(im, target) -def test_sanity_dxt3(): +def test_sanity_dxt3() -> None: """Check DXT3 images can be opened""" with Image.open(TEST_FILE_DXT3) as im: @@ -73,7 +74,7 @@ def test_sanity_dxt3(): assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) -def test_sanity_dxt5(): +def test_sanity_dxt5() -> None: """Check DXT5 images can be opened""" with Image.open(TEST_FILE_DXT5) as im: @@ -94,7 +95,7 @@ def test_sanity_dxt5(): TEST_FILE_BC4U, ), ) -def test_sanity_ati1_bc4u(image_path): +def test_sanity_ati1_bc4u(image_path) -> None: """Check ATI1 and BC4U images can be opened""" with Image.open(image_path) as im: @@ -115,7 +116,7 @@ def test_sanity_ati1_bc4u(image_path): TEST_FILE_DX10_BC4_TYPELESS, ), ) -def test_dx10_bc4(image_path): +def test_dx10_bc4(image_path) -> None: """Check DX10 BC4 images can be opened""" with Image.open(image_path) as im: @@ -136,7 +137,7 @@ def test_dx10_bc4(image_path): TEST_FILE_BC5U, ), ) -def test_sanity_ati2_bc5u(image_path): +def test_sanity_ati2_bc5u(image_path) -> None: """Check ATI2 and BC5U images can be opened""" with Image.open(image_path) as im: @@ -160,7 +161,7 @@ def test_sanity_ati2_bc5u(image_path): (TEST_FILE_BC5S, TEST_FILE_BC5S), ), ) -def test_dx10_bc5(image_path, expected_path): +def test_dx10_bc5(image_path, expected_path) -> None: """Check DX10 BC5 images can be opened""" with Image.open(image_path) as im: @@ -174,7 +175,7 @@ def test_dx10_bc5(image_path, expected_path): @pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) -def test_dx10_bc6h(image_path): +def test_dx10_bc6h(image_path) -> None: """Check DX10 BC6H/BC6HS images can be opened""" with Image.open(image_path) as im: @@ -187,7 +188,7 @@ def test_dx10_bc6h(image_path): assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) -def test_dx10_bc7(): +def test_dx10_bc7() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_BC7) as im: @@ -200,7 +201,7 @@ def test_dx10_bc7(): assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) -def test_dx10_bc7_unorm_srgb(): +def test_dx10_bc7_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: @@ -216,7 +217,7 @@ def test_dx10_bc7_unorm_srgb(): ) -def test_dx10_r8g8b8a8(): +def test_dx10_r8g8b8a8() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: @@ -229,7 +230,7 @@ def test_dx10_r8g8b8a8(): assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) -def test_dx10_r8g8b8a8_unorm_srgb(): +def test_dx10_r8g8b8a8_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: @@ -255,7 +256,7 @@ def test_dx10_r8g8b8a8_unorm_srgb(): ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ], ) -def test_uncompressed(mode, size, test_file): +def test_uncompressed(mode, size, test_file) -> None: """Check uncompressed images can be opened""" with Image.open(test_file) as im: @@ -266,7 +267,7 @@ def test_uncompressed(mode, size, test_file): assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) -def test__accept_true(): +def test__accept_true() -> None: """Check valid prefix""" # Arrange prefix = b"DDS etc" @@ -278,7 +279,7 @@ def test__accept_true(): assert output -def test__accept_false(): +def test__accept_false() -> None: """Check invalid prefix""" # Arrange prefix = b"something invalid" @@ -290,19 +291,19 @@ def test__accept_false(): assert not output -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): DdsImagePlugin.DdsImageFile(invalid_file) -def test_short_header(): +def test_short_header() -> None: """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_header(): + def short_header() -> None: with Image.open(BytesIO(img_file[:119])): pass # pragma: no cover @@ -310,13 +311,13 @@ def short_header(): short_header() -def test_short_file(): +def test_short_file() -> None: """Check that the appropriate error is thrown for a short file""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_file(): + def short_file() -> None: with Image.open(BytesIO(img_file[:-100])) as im: im.load() @@ -324,7 +325,7 @@ def short_file(): short_file() -def test_dxt5_colorblock_alpha_issue_4142(): +def test_dxt5_colorblock_alpha_issue_4142() -> None: """Check that colorblocks are decoded correctly in DXT5""" with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: @@ -339,12 +340,12 @@ def test_dxt5_colorblock_alpha_issue_4142(): assert px[2] != 0 -def test_palette(): +def test_palette() -> None: with Image.open("Tests/images/palette.dds") as im: assert_image_equal_tofile(im, "Tests/images/transparent.gif") -def test_unsupported_bitcount(): +def test_unsupported_bitcount() -> None: with pytest.raises(OSError): with Image.open("Tests/images/unsupported_bitcount.dds"): pass @@ -357,13 +358,13 @@ def test_unsupported_bitcount(): "Tests/images/unimplemented_pfflags.dds", ), ) -def test_not_implemented(test_file): +def test_not_implemented(test_file) -> None: with pytest.raises(NotImplementedError): with Image.open(test_file): pass -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") im = hopper("HSV") with pytest.raises(OSError): @@ -379,7 +380,7 @@ def test_save_unsupported_mode(tmp_path): ("RGBA", "Tests/images/pil123rgba.png"), ], ) -def test_save(mode, test_file, tmp_path): +def test_save(mode, test_file, tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(test_file) as im: assert im.mode == mode diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8def9a43511..06f927c7bb2 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +from pathlib import Path import pytest @@ -83,7 +84,7 @@ ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ) @pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename, size, scale): +def test_sanity(filename, size, scale) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: image.load(scale=scale) @@ -93,7 +94,7 @@ def test_sanity(filename, size, scale): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_load(): +def test_load() -> None: with Image.open(FILE1) as im: assert im.load()[0, 0] == (255, 255, 255) @@ -101,7 +102,7 @@ def test_load(): assert im.load()[0, 0] == (255, 255, 255) -def test_binary(): +def test_binary() -> None: if HAS_GHOSTSCRIPT: assert EpsImagePlugin.gs_binary is not None else: @@ -115,41 +116,41 @@ def test_binary(): assert EpsImagePlugin.gs_windows_binary is not None -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) -def test_binary_header_only(): +def test_binary_header_only() -> None: data = io.BytesIO(simple_binary_header) with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix): +def test_missing_version_comment(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix): +def test_missing_boundingbox_comment(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix): +def test_invalid_boundingbox_comment(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: data = io.BytesIO( prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) ) @@ -160,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix): +def test_ascii_comment_too_long(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) with pytest.raises(SyntaxError, match="not an EPS file"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix): +def test_long_binary_data(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) EpsImagePlugin.EpsImageFile(data) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix): +def test_load_long_binary_data(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() @@ -187,7 +188,7 @@ def test_load_long_binary_data(prefix): pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_cmyk(): +def test_cmyk() -> None: with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) @@ -203,7 +204,7 @@ def test_cmyk(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_showpage(): +def test_showpage() -> None: # See https://github.com/python-pillow/Pillow/issues/2615 with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/reqd_showpage.png") as target: @@ -214,7 +215,7 @@ def test_showpage(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_transparency(): +def test_transparency() -> None: with Image.open("Tests/images/reqd_showpage.eps") as plot_image: plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -225,7 +226,7 @@ def test_transparency(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_file_object(tmp_path): +def test_file_object(tmp_path: Path) -> None: # issue 479 with Image.open(FILE1) as image1: with open(str(tmp_path / "temp.eps"), "wb") as fh: @@ -233,7 +234,7 @@ def test_file_object(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_bytesio_object(): +def test_bytesio_object() -> None: with open(FILE1, "rb") as f: img_bytes = io.BytesIO(f.read()) @@ -246,12 +247,12 @@ def test_bytesio_object(): assert_image_similar(img, image1_scale1_compare, 5) -def test_1_mode(): +def test_1_mode() -> None: with Image.open("Tests/images/1.eps") as im: assert im.mode == "1" -def test_image_mode_not_supported(tmp_path): +def test_image_mode_not_supported(tmp_path: Path) -> None: im = hopper("RGBA") tmpfile = str(tmp_path / "temp.eps") with pytest.raises(ValueError): @@ -260,7 +261,7 @@ def test_image_mode_not_supported(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale1(): +def test_render_scale1() -> None: # We need png support for these render test # Zero bounding box @@ -282,7 +283,7 @@ def test_render_scale1(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale2(): +def test_render_scale2() -> None: # We need png support for these render test # Zero bounding box @@ -304,7 +305,7 @@ def test_render_scale2(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename): +def test_resize(filename) -> None: with Image.open(filename) as im: new_size = (100, 100) im = im.resize(new_size) @@ -313,7 +314,7 @@ def test_resize(filename): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename): +def test_thumbnail(filename) -> None: # Issue #619 with Image.open(filename) as im: new_size = (100, 100) @@ -321,20 +322,20 @@ def test_thumbnail(filename): assert max(im.size) == max(new_size) -def test_read_binary_preview(): +def test_read_binary_preview() -> None: # Issue 302 # open image with binary preview with Image.open(FILE3): pass -def test_readline_psfile(tmp_path): +def test_readline_psfile(tmp_path: Path) -> None: # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] strings = ["something", "else", "baz", "bif"] - def _test_readline(t, ending): + def _test_readline(t, ending) -> None: ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -343,13 +344,13 @@ def _test_readline(t, ending): assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(test_string, ending): + def _test_readline_io_psfile(test_string, ending) -> None: f = io.BytesIO(test_string.encode("latin-1")) with pytest.warns(DeprecationWarning): t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) - def _test_readline_file_psfile(test_string, ending): + def _test_readline_file_psfile(test_string, ending) -> None: f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) @@ -365,7 +366,7 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) -def test_psfile_deprecation(): +def test_psfile_deprecation() -> None: with pytest.warns(DeprecationWarning): EpsImagePlugin.PSFile(None) @@ -375,7 +376,7 @@ def test_psfile_deprecation(): "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(prefix, line_ending): +def test_readline(prefix, line_ending) -> None: simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) @@ -393,14 +394,14 @@ def test_readline(prefix, line_ending): "Tests/images/illuCS6_preview.eps", ), ) -def test_open_eps(filename): +def test_open_eps(filename) -> None: # https://github.com/python-pillow/Pillow/issues/1104 with Image.open(filename) as img: assert img.mode == "RGB" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_emptyline(): +def test_emptyline() -> None: # Test file includes an empty line in the header data emptyline_file = "Tests/images/zero_bb_emptyline.eps" @@ -416,14 +417,14 @@ def test_emptyline(): "test_file", ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) -def test_timeout(test_file): +def test_timeout(test_file) -> None: with open(test_file, "rb") as f: with pytest.raises(Image.UnidentifiedImageError): with Image.open(f): pass -def test_bounding_box_in_trailer(): +def test_bounding_box_in_trailer() -> None: # Check bounding boxes are parsed in the same way # when specified in the header and the trailer with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( @@ -432,7 +433,7 @@ def test_bounding_box_in_trailer(): assert trailer_image.size == header_image.size -def test_eof_before_bounding_box(): +def test_eof_before_bounding_box() -> None: with pytest.raises(OSError): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): pass diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 7444eb673cb..cce0b05cda8 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -11,7 +11,7 @@ TEST_FILE = "Tests/images/hopper.fits" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -22,7 +22,7 @@ def test_open(): assert_image_equal(im, hopper("L")) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -31,14 +31,14 @@ def test_invalid_file(): FitsImagePlugin.FitsImageFile(invalid_file) -def test_truncated_fits(): +def test_truncated_fits() -> None: # No END to headers image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) -def test_naxis_zero(): +def test_naxis_zero() -> None: # This test image has been manually hexedited # to set the number of data axes to zero with pytest.raises(ValueError): @@ -46,7 +46,7 @@ def test_naxis_zero(): pass -def test_comment(): +def test_comment() -> None: image_data = b"SIMPLE = T / comment string" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 00377e0c92d..a673d4af851 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -16,7 +16,7 @@ animated_test_file = "Tests/images/a.fli" -def test_sanity(): +def test_sanity() -> None: with Image.open(static_test_file) as im: im.load() assert im.mode == "P" @@ -33,8 +33,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(static_test_file) im.load() @@ -42,14 +42,14 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(static_test_file) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(animated_test_file) im.seek(1) im.close() @@ -58,13 +58,13 @@ def test_seek_after_close(): im.seek(0) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(static_test_file) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(static_test_file) as im: # Act @@ -74,20 +74,20 @@ def test_tell(): assert frame == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): FliImagePlugin.FliImageFile(invalid_file) -def test_palette_chunk_second(): +def test_palette_chunk_second() -> None: with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: with Image.open(static_test_file) as expected: assert_image_equal(im.convert("RGB"), expected.convert("RGB")) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(static_test_file) as im: assert im.n_frames == 1 assert not im.is_animated @@ -97,7 +97,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(animated_test_file) as im: n_frames = im.n_frames @@ -110,7 +110,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(animated_test_file) as im: layer_number = im.tell() assert layer_number == 0 @@ -132,7 +132,7 @@ def test_seek_tell(): assert layer_number == 1 -def test_seek(): +def test_seek() -> None: with Image.open(animated_test_file) as im: im.seek(50) @@ -147,7 +147,7 @@ def test_seek(): ], ) @pytest.mark.timeout(timeout=3) -def test_timeouts(test_file): +def test_timeouts(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): @@ -160,7 +160,7 @@ def test_timeouts(test_file): "Tests/images/crash-5762152299364352.fli", ], ) -def test_crash(test_file): +def test_crash(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index d710070c01b..e32f30a0123 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -11,7 +11,7 @@ ) -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: assert im.mode == "L" assert im.size == (70, 46) @@ -20,7 +20,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") -def test_close(): +def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: pass assert im.ole.fp.closed @@ -30,7 +30,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -42,7 +42,7 @@ def test_invalid_file(): FpxImagePlugin.FpxImageFile(ole_file) -def test_fpx_invalid_number_of_bands(): +def test_fpx_invalid_number_of_bands() -> None: with pytest.raises(OSError, match="Invalid number of bands"): with Image.open("Tests/images/input_bw_five_bands.fpx"): pass diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3e19940aab9..3f550fd1109 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -2,6 +2,7 @@ import warnings from io import BytesIO +from pathlib import Path import pytest @@ -23,7 +24,7 @@ data = f.read() -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_GIF) as im: im.load() assert im.mode == "P" @@ -33,8 +34,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_GIF) im.load() @@ -42,14 +43,14 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_GIF) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/iss634.gif") im.load() im.close() @@ -62,20 +63,20 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): GifImagePlugin.GifImageFile(invalid_file) -def test_l_mode_transparency(): +def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" assert im.load()[0, 0] == 128 @@ -86,7 +87,7 @@ def test_l_mode_transparency(): assert im.load()[0, 0] == 128 -def test_l_mode_after_rgb(): +def test_l_mode_after_rgb() -> None: with Image.open("Tests/images/no_palette_after_rgb.gif") as im: im.seek(1) assert im.mode == "RGB" @@ -95,13 +96,13 @@ def test_l_mode_after_rgb(): assert im.mode == "RGB" -def test_palette_not_needed_for_second_frame(): +def test_palette_not_needed_for_second_frame() -> None: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: im.seek(1) assert_image_similar(im, hopper("L").convert("RGB"), 8) -def test_strategy(): +def test_strategy() -> None: with Image.open("Tests/images/iss634.gif") as im: expected_rgb_always = im.convert("RGB") @@ -142,7 +143,7 @@ def test_strategy(): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_optimize(): +def test_optimize() -> None: def test_grayscale(optimize): im = Image.new("L", (1, 1), 0) filename = BytesIO() @@ -177,7 +178,7 @@ def test_bilevel(optimize): (4, 513, 256), ), ) -def test_optimize_correctness(colors, size, expected_palette_length): +def test_optimize_correctness(colors, size, expected_palette_length) -> None: # 256 color Palette image, posterize to > 128 and < 128 levels. # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. @@ -199,14 +200,14 @@ def test_optimize_correctness(colors, size, expected_palette_length): assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) -def test_optimize_full_l(): +def test_optimize_full_l() -> None: im = Image.frombytes("L", (16, 16), bytes(range(256))) test_file = BytesIO() im.save(test_file, "GIF", optimize=True) assert im.mode == "L" -def test_optimize_if_palette_can_be_reduced_by_half(): +def test_optimize_if_palette_can_be_reduced_by_half() -> None: im = Image.new("P", (8, 1)) im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150)) for i in range(8): @@ -219,7 +220,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): assert len(reloaded.palette.palette) // 3 == colors -def test_full_palette_second_frame(tmp_path): +def test_full_palette_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 256)) @@ -240,7 +241,7 @@ def test_full_palette_second_frame(tmp_path): reloaded.getpixel((0, i)) == i -def test_roundtrip(tmp_path): +def test_roundtrip(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper() im.save(out) @@ -248,7 +249,7 @@ def test_roundtrip(tmp_path): assert_image_similar(reread.convert("RGB"), im, 50) -def test_roundtrip2(tmp_path): +def test_roundtrip2(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/403 out = str(tmp_path / "temp.gif") with Image.open(TEST_GIF) as im: @@ -258,7 +259,7 @@ def test_roundtrip2(tmp_path): assert_image_similar(reread.convert("RGB"), hopper(), 50) -def test_roundtrip_save_all(tmp_path): +def test_roundtrip_save_all(tmp_path: Path) -> None: # Single frame image out = str(tmp_path / "temp.gif") im = hopper() @@ -275,7 +276,7 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 -def test_roundtrip_save_all_1(tmp_path): +def test_roundtrip_save_all_1(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("1", (1, 1)) im2 = Image.new("1", (1, 1), 1) @@ -296,7 +297,7 @@ def test_roundtrip_save_all_1(tmp_path): ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ), ) -def test_loading_multiple_palettes(path, mode): +def test_loading_multiple_palettes(path, mode) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() @@ -314,7 +315,7 @@ def test_loading_multiple_palettes(path, mode): assert im.load()[24, 24] not in first_frame_colors -def test_headers_saving_for_animated_gifs(tmp_path): +def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: @@ -327,7 +328,7 @@ def test_headers_saving_for_animated_gifs(tmp_path): assert info[header] == reread.info[header] -def test_palette_handling(tmp_path): +def test_palette_handling(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/513 with Image.open(TEST_GIF) as im: @@ -343,7 +344,7 @@ def test_palette_handling(tmp_path): assert_image_similar(im, reloaded.convert("RGB"), 10) -def test_palette_434(tmp_path): +def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 def roundtrip(im, *args, **kwargs): @@ -368,7 +369,7 @@ def roundtrip(im, *args, **kwargs): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_bmp_mode(tmp_path): +def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("RGB") @@ -379,7 +380,7 @@ def test_save_netpbm_bmp_mode(tmp_path): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_l_mode(tmp_path): +def test_save_netpbm_l_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("L") @@ -389,7 +390,7 @@ def test_save_netpbm_l_mode(tmp_path): assert_image_similar(img, reloaded.convert("L"), 0) -def test_seek(): +def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: frame_count = 0 try: @@ -400,7 +401,7 @@ def test_seek(): assert frame_count == 5 -def test_seek_info(): +def test_seek_info() -> None: with Image.open("Tests/images/iss634.gif") as im: info = im.info.copy() @@ -410,7 +411,7 @@ def test_seek_info(): assert im.info == info -def test_seek_rewind(): +def test_seek_rewind() -> None: with Image.open("Tests/images/iss634.gif") as im: im.seek(2) im.seek(1) @@ -428,7 +429,7 @@ def test_seek_rewind(): ("Tests/images/iss634.gif", 42), ), ) -def test_n_frames(path, n_frames): +def test_n_frames(path, n_frames) -> None: # Test is_animated before n_frames with Image.open(path) as im: assert im.is_animated == (n_frames != 1) @@ -439,7 +440,7 @@ def test_n_frames(path, n_frames): assert im.is_animated == (n_frames != 1) -def test_no_change(): +def test_no_change() -> None: # Test n_frames does not change the image with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) @@ -460,7 +461,7 @@ def test_no_change(): assert_image_equal(im, expected) -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_GIF) as im: n_frames = im.n_frames @@ -473,13 +474,13 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_first_frame_transparency(): +def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: px = im.load() assert px[0, 0] == im.info["transparency"] -def test_dispose_none(): +def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: try: while True: @@ -489,7 +490,7 @@ def test_dispose_none(): pass -def test_dispose_none_load_end(): +def test_dispose_none_load_end() -> None: # Test image created with: # # im = Image.open("transparent.gif") @@ -502,7 +503,7 @@ def test_dispose_none_load_end(): assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") -def test_dispose_background(): +def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: try: while True: @@ -512,7 +513,7 @@ def test_dispose_background(): pass -def test_dispose_background_transparency(): +def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() @@ -540,7 +541,7 @@ def test_dispose_background_transparency(): ), ), ) -def test_transparent_dispose(loading_strategy, expected_colors): +def test_transparent_dispose(loading_strategy, expected_colors) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/transparent_dispose.gif") as img: @@ -553,7 +554,7 @@ def test_transparent_dispose(loading_strategy, expected_colors): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_dispose_previous(): +def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: try: while True: @@ -563,7 +564,7 @@ def test_dispose_previous(): pass -def test_dispose_previous_first_frame(): +def test_dispose_previous_first_frame() -> None: with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: im.seek(1) assert_image_equal_tofile( @@ -571,7 +572,7 @@ def test_dispose_previous_first_frame(): ) -def test_previous_frame_loaded(): +def test_previous_frame_loaded() -> None: with Image.open("Tests/images/dispose_none.gif") as img: img.load() img.seek(1) @@ -582,7 +583,7 @@ def test_previous_frame_loaded(): assert_image_equal(img_skipped, img) -def test_save_dispose(tmp_path): +def test_save_dispose(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -610,7 +611,7 @@ def test_save_dispose(tmp_path): assert img.disposal_method == i + 1 -def test_dispose2_palette(tmp_path): +def test_dispose2_palette(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Four colors: white, gray, black, red @@ -641,7 +642,7 @@ def test_dispose2_palette(tmp_path): assert rgb_img.getpixel((50, 50)) == circle -def test_dispose2_diff(tmp_path): +def test_dispose2_diff(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # 4 frames: red/blue, red/red, blue/blue, red/blue @@ -683,7 +684,7 @@ def test_dispose2_diff(tmp_path): assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) -def test_dispose2_background(tmp_path): +def test_dispose2_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [] @@ -709,7 +710,7 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == (255, 0, 0) -def test_dispose2_background_frame(tmp_path): +def test_dispose2_background_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [Image.new("RGBA", (1, 20))] @@ -727,7 +728,7 @@ def test_dispose2_background_frame(tmp_path): assert im.n_frames == 3 -def test_transparency_in_second_frame(tmp_path): +def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: assert im.info["transparency"] == 0 @@ -747,7 +748,7 @@ def test_transparency_in_second_frame(tmp_path): ) -def test_no_transparency_in_second_frame(): +def test_no_transparency_in_second_frame() -> None: with Image.open("Tests/images/iss634.gif") as img: # Seek to the second frame img.seek(img.tell() + 1) @@ -757,7 +758,7 @@ def test_no_transparency_in_second_frame(): assert img.histogram()[255] == 0 -def test_remapped_transparency(tmp_path): +def test_remapped_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 2)) @@ -773,7 +774,7 @@ def test_remapped_transparency(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) -def test_duration(tmp_path): +def test_duration(tmp_path: Path) -> None: duration = 1000 out = str(tmp_path / "temp.gif") @@ -787,7 +788,7 @@ def test_duration(tmp_path): assert reread.info["duration"] == duration -def test_multiple_duration(tmp_path): +def test_multiple_duration(tmp_path: Path) -> None: duration_list = [1000, 2000, 3000] out = str(tmp_path / "temp.gif") @@ -822,7 +823,7 @@ def test_multiple_duration(tmp_path): pass -def test_roundtrip_info_duration(tmp_path): +def test_roundtrip_info_duration(tmp_path: Path) -> None: duration_list = [100, 500, 500] out = str(tmp_path / "temp.gif") @@ -839,7 +840,7 @@ def test_roundtrip_info_duration(tmp_path): ] == duration_list -def test_roundtrip_info_duration_combined(tmp_path): +def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/duplicate_frame.gif") as im: assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ @@ -855,7 +856,7 @@ def test_roundtrip_info_duration_combined(tmp_path): ] == [1000, 2000] -def test_identical_frames(tmp_path): +def test_identical_frames(tmp_path: Path) -> None: duration_list = [1000, 1500, 2000, 4000] out = str(tmp_path / "temp.gif") @@ -888,7 +889,7 @@ def test_identical_frames(tmp_path): 1500, ), ) -def test_identical_frames_to_single_frame(duration, tmp_path): +def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -905,7 +906,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path): assert reread.info["duration"] == 4500 -def test_loop_none(tmp_path): +def test_loop_none(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.save(out, loop=None) @@ -913,7 +914,7 @@ def test_loop_none(tmp_path): assert "loop" not in reread.info -def test_number_of_loops(tmp_path): +def test_number_of_loops(tmp_path: Path) -> None: number_of_loops = 2 out = str(tmp_path / "temp.gif") @@ -931,7 +932,7 @@ def test_number_of_loops(tmp_path): assert im.info["loop"] == 2 -def test_background(tmp_path): +def test_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.info["background"] = 1 @@ -940,7 +941,7 @@ def test_background(tmp_path): assert reread.info["background"] == im.info["background"] -def test_webp_background(tmp_path): +def test_webp_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test opaque WebP background @@ -955,7 +956,7 @@ def test_webp_background(tmp_path): im.save(out) -def test_comment(tmp_path): +def test_comment(tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" @@ -975,7 +976,7 @@ def test_comment(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_comment_over_255(tmp_path): +def test_comment_over_255(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" @@ -990,18 +991,18 @@ def test_comment_over_255(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_zero_comment_subblocks(): +def test_zero_comment_subblocks() -> None: with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: assert_image_equal_tofile(im, TEST_GIF) -def test_read_multiple_comment_blocks(): +def test_read_multiple_comment_blocks() -> None: with Image.open("Tests/images/multiple_comments.gif") as im: # Multiple comment blocks in a frame are separated not concatenated assert im.info["comment"] == b"Test comment 1\nTest comment 2" -def test_empty_string_comment(tmp_path): +def test_empty_string_comment(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/chi.gif") as im: assert "comment" in im.info @@ -1014,7 +1015,7 @@ def test_empty_string_comment(tmp_path): assert "comment" not in frame.info -def test_retain_comment_in_subsequent_frames(tmp_path): +def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: # Test that a comment block at the beginning is kept with Image.open("Tests/images/chi.gif") as im: for frame in ImageSequence.Iterator(im): @@ -1045,10 +1046,10 @@ def test_retain_comment_in_subsequent_frames(tmp_path): assert frame.info["comment"] == b"Test" -def test_version(tmp_path): +def test_version(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") - def assert_version_after_save(im, version): + def assert_version_after_save(im, version) -> None: im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version @@ -1075,7 +1076,7 @@ def assert_version_after_save(im, version): assert_version_after_save(im, b"GIF87a") -def test_append_images(tmp_path): +def test_append_images(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test appending single frame images @@ -1104,7 +1105,7 @@ def im_generator(ims): assert reread.n_frames == 10 -def test_transparent_optimize(tmp_path): +def test_transparent_optimize(tmp_path: Path) -> None: # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. # Need a palette that isn't using the 0 color, @@ -1124,7 +1125,7 @@ def test_transparent_optimize(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) -def test_removed_transparency(tmp_path): +def test_removed_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (256, 1)) @@ -1139,7 +1140,7 @@ def test_removed_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgb_transparency(tmp_path): +def test_rgb_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Single frame @@ -1161,7 +1162,7 @@ def test_rgb_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgba_transparency(tmp_path): +def test_rgba_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper("P") @@ -1172,13 +1173,13 @@ def test_rgba_transparency(tmp_path): assert_image_equal(hopper("P").convert("RGB"), reloaded) -def test_background_outside_palettte(tmp_path): +def test_background_outside_palettte(tmp_path: Path) -> None: with Image.open("Tests/images/background_outside_palette.gif") as im: im.seek(1) assert im.info["background"] == 255 -def test_bbox(tmp_path): +def test_bbox(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (100, 100), "#fff") @@ -1189,7 +1190,7 @@ def test_bbox(tmp_path): assert reread.n_frames == 2 -def test_bbox_alpha(tmp_path): +def test_bbox_alpha(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) @@ -1201,7 +1202,7 @@ def test_bbox_alpha(tmp_path): assert reread.n_frames == 2 -def test_palette_save_L(tmp_path): +def test_palette_save_L(tmp_path: Path) -> None: # Generate an L mode image with a separate palette im = hopper("P") @@ -1215,7 +1216,7 @@ def test_palette_save_L(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_palette_save_P(tmp_path): +def test_palette_save_P(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1229,7 +1230,7 @@ def test_palette_save_P(tmp_path): assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6) -def test_palette_save_duplicate_entries(tmp_path): +def test_palette_save_duplicate_entries(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1242,7 +1243,7 @@ def test_palette_save_duplicate_entries(tmp_path): assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) -def test_palette_save_all_P(tmp_path): +def test_palette_save_all_P(tmp_path: Path) -> None: frames = [] colors = ((255, 0, 0), (0, 255, 0)) for color in colors: @@ -1265,7 +1266,7 @@ def test_palette_save_all_P(tmp_path): assert im.palette.palette == im.global_palette.palette -def test_palette_save_ImagePalette(tmp_path): +def test_palette_save_ImagePalette(tmp_path: Path) -> None: # Pass in a different palette, as an ImagePalette.ImagePalette # effectively the same as test_palette_save_P @@ -1280,7 +1281,7 @@ def test_palette_save_ImagePalette(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_save_I(tmp_path): +def test_save_I(tmp_path: Path) -> None: # Test saving something that would trigger the auto-convert to 'L' im = hopper("I") @@ -1292,7 +1293,7 @@ def test_save_I(tmp_path): assert_image_equal(reloaded.convert("L"), im.convert("L")) -def test_getdata(): +def test_getdata() -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) @@ -1320,7 +1321,7 @@ def test_getdata(): GifImagePlugin._FORCE_OPTIMIZE = False -def test_lzw_bits(): +def test_lzw_bits() -> None: # see https://github.com/python-pillow/Pillow/issues/2811 with Image.open("Tests/images/issue_2811.gif") as im: assert im.tile[0][3][0] == 11 # LZW bits @@ -1328,7 +1329,7 @@ def test_lzw_bits(): im.load() -def test_extents(): +def test_extents() -> None: with Image.open("Tests/images/test_extents.gif") as im: assert im.size == (100, 100) @@ -1340,7 +1341,7 @@ def test_extents(): assert im.size == (150, 150) -def test_missing_background(): +def test_missing_background() -> None: # The Global Color Table Flag isn't set, so there is no background color index, # but the disposal method is "Restore to background color" with Image.open("Tests/images/missing_background.gif") as im: @@ -1348,7 +1349,7 @@ def test_missing_background(): assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") -def test_saving_rgba(tmp_path): +def test_saving_rgba(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/transparent.png") as im: im.save(out) diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index ceea1edd34c..006ee952d43 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -3,7 +3,7 @@ from PIL import GimpGradientFile, ImagePalette -def test_linear_pos_le_middle(): +def test_linear_pos_le_middle() -> None: # Arrange middle = 0.5 pos = 0.25 @@ -15,7 +15,7 @@ def test_linear_pos_le_middle(): assert ret == 0.25 -def test_linear_pos_le_small_middle(): +def test_linear_pos_le_small_middle() -> None: # Arrange middle = 1e-11 pos = 1e-12 @@ -27,7 +27,7 @@ def test_linear_pos_le_small_middle(): assert ret == 0.0 -def test_linear_pos_gt_middle(): +def test_linear_pos_gt_middle() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -39,7 +39,7 @@ def test_linear_pos_gt_middle(): assert ret == 0.75 -def test_linear_pos_gt_small_middle(): +def test_linear_pos_gt_small_middle() -> None: # Arrange middle = 1 - 1e-11 pos = 1 - 1e-12 @@ -51,7 +51,7 @@ def test_linear_pos_gt_small_middle(): assert ret == 1.0 -def test_curved(): +def test_curved() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -63,7 +63,7 @@ def test_curved(): assert ret == 0.75 -def test_sine(): +def test_sine() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -75,7 +75,7 @@ def test_sine(): assert ret == 0.8535533905932737 -def test_sphere_increasing(): +def test_sphere_increasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -87,7 +87,7 @@ def test_sphere_increasing(): assert round(abs(ret - 0.9682458365518543), 7) == 0 -def test_sphere_decreasing(): +def test_sphere_decreasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -99,7 +99,7 @@ def test_sphere_decreasing(): assert ret == 0.3385621722338523 -def test_load_via_imagepalette(): +def test_load_via_imagepalette() -> None: # Arrange test_file = "Tests/images/gimp_gradient.ggr" @@ -112,7 +112,7 @@ def test_load_via_imagepalette(): assert palette[1] == "RGBA" -def test_load_1_3_via_imagepalette(): +def test_load_1_3_via_imagepalette() -> None: # Arrange # GIMP 1.3 gradient files contain a name field test_file = "Tests/images/gimp_gradient_with_name.ggr" diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index a4ce6dde674..4945468be40 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import GribStubImagePlugin, Image @@ -9,7 +11,7 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +22,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -29,7 +31,7 @@ def test_invalid_file(): GribStubImagePlugin.GribStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -37,7 +39,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.grib") @@ -47,13 +49,13 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -61,7 +63,7 @@ def load(self, im): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 72764461730..ac3d40bf287 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Hdf5StubImagePlugin, Image @@ -7,7 +9,7 @@ TEST_FILE = "Tests/images/hdf5.h5" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -18,7 +20,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -27,7 +29,7 @@ def test_invalid_file(): Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -35,7 +37,7 @@ def test_load(): im.load() -def test_save(): +def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_fp = None @@ -48,13 +50,13 @@ def test_save(): Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -62,7 +64,7 @@ def load(self, im): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 314fa800868..488984aef70 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -3,6 +3,7 @@ import io import os import warnings +from pathlib import Path import pytest @@ -14,7 +15,7 @@ TEST_FILE = "Tests/images/pillow.icns" -def test_sanity(): +def test_sanity() -> None: # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: @@ -27,7 +28,7 @@ def test_sanity(): assert im.format == "ICNS" -def test_load(): +def test_load() -> None: with Image.open(TEST_FILE) as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -35,7 +36,7 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") with Image.open(TEST_FILE) as im: @@ -52,7 +53,7 @@ def test_save(tmp_path): assert _binary.i32be(fp.read(4)) == file_length -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) @@ -67,7 +68,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_save_fp(): +def test_save_fp() -> None: fp = io.BytesIO() with Image.open(TEST_FILE) as im: @@ -79,7 +80,7 @@ def test_save_fp(): assert reread.format == "ICNS" -def test_sizes(): +def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: @@ -96,7 +97,7 @@ def test_sizes(): im.size = (1, 1) -def test_older_icon(): +def test_older_icon() -> None: # This icon was made with Icon Composer rather than iconutil; it still # uses PNG rather than JP2, however (since it was made on 10.9). with Image.open("Tests/images/pillow2.icns") as im: @@ -111,7 +112,7 @@ def test_older_icon(): @skip_unless_feature("jpg_2000") -def test_jp2_icon(): +def test_jp2_icon() -> None: # This icon uses JPEG 2000 images instead of the PNG images. # The advantage of doing this is that OS X 10.5 supports JPEG 2000 # but not PNG; some commercial software therefore does just this. @@ -127,7 +128,7 @@ def test_jp2_icon(): assert im2.size == (wr, hr) -def test_getimage(): +def test_getimage() -> None: with open(TEST_FILE, "rb") as fp: icns_file = IcnsImagePlugin.IcnsFile(fp) @@ -140,14 +141,14 @@ def test_getimage(): assert im.size == (512, 512) -def test_not_an_icns_file(): +def test_not_an_icns_file() -> None: with io.BytesIO(b"invalid\n") as fp: with pytest.raises(SyntaxError): IcnsImagePlugin.IcnsFile(fp) @skip_unless_feature("jpg_2000") -def test_icns_decompression_bomb(): +def test_icns_decompression_bomb() -> None: with Image.open( "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" ) as im: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 99b3048d1e0..65f090931b4 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -2,6 +2,7 @@ import io import os +from pathlib import Path import pytest @@ -12,7 +13,7 @@ TEST_ICO_FILE = "Tests/images/hopper.ico" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_ICO_FILE) as im: im.load() assert im.mode == "RGBA" @@ -21,29 +22,29 @@ def test_sanity(): assert im.get_format_mimetype() == "image/x-icon" -def test_load(): +def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: assert im.load()[0, 0] == (1, 1, 9, 255) -def test_mask(): +def test_mask() -> None: with Image.open("Tests/images/hopper_mask.ico") as im: assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") -def test_black_and_white(): +def test_black_and_white() -> None: with Image.open("Tests/images/black_and_white.ico") as im: assert im.mode == "RGBA" assert im.size == (16, 16) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): IcoImagePlugin.IcoImageFile(fp) -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "ico", sizes=[(32, 32), (64, 64)]) @@ -73,7 +74,7 @@ def test_save_to_bytes(): ) -def test_getpixel(tmp_path): +def test_getpixel(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") im = hopper() @@ -86,7 +87,7 @@ def test_getpixel(tmp_path): assert reloaded.getpixel((0, 0)) == (18, 20, 62) -def test_no_duplicates(tmp_path): +def test_no_duplicates(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -100,7 +101,7 @@ def test_no_duplicates(tmp_path): assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) -def test_different_bit_depths(tmp_path): +def test_different_bit_depths(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -134,7 +135,7 @@ def test_different_bit_depths(tmp_path): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode): +def test_save_to_bytes_bmp(mode) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) @@ -162,13 +163,13 @@ def test_save_to_bytes_bmp(mode): assert_image_equal(reloaded, im) -def test_incorrect_size(): +def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: with pytest.raises(ValueError): im.size = (1, 1) -def test_save_256x256(tmp_path): +def test_save_256x256(tmp_path: Path) -> None: """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange with Image.open("Tests/images/hopper_256x256.ico") as im: @@ -181,7 +182,7 @@ def test_save_256x256(tmp_path): assert im_saved.size == (256, 256) -def test_only_save_relevant_sizes(tmp_path): +def test_only_save_relevant_sizes(tmp_path: Path) -> None: """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 Should save in 16x16, 24x24, 32x32, 48x48 sizes and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes @@ -197,7 +198,7 @@ def test_only_save_relevant_sizes(tmp_path): assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: # append_images should be used for scaled down versions of the image im = hopper("RGBA") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) @@ -211,7 +212,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_unexpected_size(): +def test_unexpected_size() -> None: # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 with pytest.warns(UserWarning): @@ -219,7 +220,7 @@ def test_unexpected_size(): assert im.size == (16, 16) -def test_draw_reloaded(tmp_path): +def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(TEST_ICO_FILE) as im: outfile = str(tmp_path / "temp_saved_hopper_draw.ico") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index a031b3e887c..f932069b9c3 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -2,6 +2,7 @@ import filecmp import warnings +from pathlib import Path import pytest @@ -13,7 +14,7 @@ TEST_IM = "Tests/images/hopper.im" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_IM) as im: im.load() assert im.mode == "RGB" @@ -21,7 +22,7 @@ def test_sanity(): assert im.format == "IM" -def test_name_limit(tmp_path): +def test_name_limit(tmp_path: Path) -> None: out = str(tmp_path / ("name_limit_test" * 7 + ".im")) with Image.open(TEST_IM) as im: im.save(out) @@ -29,8 +30,8 @@ def test_name_limit(tmp_path): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_IM) im.load() @@ -38,20 +39,20 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_IM) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_IM) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_IM) as im: # Act @@ -61,13 +62,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_IM) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_IM) as im: n_frames = im.n_frames @@ -81,14 +82,14 @@ def test_eoferror(): @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path): +def test_roundtrip(mode, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) assert_image_equal_tofile(im, out) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 1, 2] im.putpalette(colors) @@ -100,19 +101,19 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors + [0] * 765 -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper("HSV") with pytest.raises(ValueError): im.save(out) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): ImImagePlugin.ImImageFile(invalid_file) -def test_number(): +def test_number() -> None: assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index a2c50ecefdf..9c0969437ea 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -12,7 +12,7 @@ TEST_FILE = "Tests/images/iptc.jpg" -def test_open(): +def test_open() -> None: expected = Image.new("L", (1, 1)) f = BytesIO( @@ -24,7 +24,7 @@ def test_open(): assert_image_equal(im, expected) -def test_getiptcinfo_jpg_none(): +def test_getiptcinfo_jpg_none() -> None: # Arrange with hopper() as im: # Act @@ -34,7 +34,7 @@ def test_getiptcinfo_jpg_none(): assert iptc is None -def test_getiptcinfo_jpg_found(): +def test_getiptcinfo_jpg_found() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -46,7 +46,7 @@ def test_getiptcinfo_jpg_found(): assert iptc[(2, 101)] == b"Hungary" -def test_getiptcinfo_fotostation(): +def test_getiptcinfo_fotostation() -> None: # Arrange with open(TEST_FILE, "rb") as fp: data = bytearray(fp.read()) @@ -63,7 +63,7 @@ def test_getiptcinfo_fotostation(): pytest.fail("FotoStation tag not found") -def test_getiptcinfo_zero_padding(): +def test_getiptcinfo_zero_padding() -> None: # Arrange with Image.open(TEST_FILE) as im: im.info["photoshop"][0x0404] += b"\x00\x00\x00" @@ -76,7 +76,7 @@ def test_getiptcinfo_zero_padding(): assert len(iptc) == 3 -def test_getiptcinfo_tiff_none(): +def test_getiptcinfo_tiff_none() -> None: # Arrange with Image.open("Tests/images/hopper.tif") as im: # Act @@ -86,7 +86,7 @@ def test_getiptcinfo_tiff_none(): assert iptc is None -def test_i(): +def test_i() -> None: # Arrange c = b"a" @@ -98,7 +98,7 @@ def test_i(): assert ret == 97 -def test_dump(monkeypatch): +def test_dump(monkeypatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout @@ -113,6 +113,6 @@ def test_dump(monkeypatch): assert mystdout.getvalue() == "61 62 63 \n" -def test_pad_deprecation(): +def test_pad_deprecation() -> None: with pytest.warns(DeprecationWarning): assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 232e51f9126..ff278d4c1bf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -4,6 +4,7 @@ import re import warnings from io import BytesIO +from pathlib import Path import pytest @@ -50,7 +51,7 @@ def roundtrip(self, im, **options): im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode="RGB"): + def gen_random_image(self, size, mode: str = "RGB"): """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -58,7 +59,7 @@ def gen_random_image(self, size, mode="RGB"): """ return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) - def test_sanity(self): + def test_sanity(self) -> None: # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -70,13 +71,13 @@ def test_sanity(self): assert im.get_format_mimetype() == "image/jpeg" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size, tmp_path): + def test_zero(self, size, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = Image.new("RGB", size) with pytest.raises(ValueError): im.save(f) - def test_app(self): + def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") @@ -89,7 +90,7 @@ def test_app(self): assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" assert im.app["COM"] == im.info["comment"] - def test_comment_write(self): + def test_comment_write(self) -> None: with Image.open(TEST_FILE) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" @@ -115,7 +116,7 @@ def test_comment_write(self): comment = comment.encode() assert reloaded.info["comment"] == comment - def test_cmyk(self): + def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. f = "Tests/images/pil_sample_cmyk.jpg" @@ -143,7 +144,7 @@ def test_cmyk(self): ) assert k > 0.9 - def test_rgb(self): + def test_rgb(self) -> None: def getchannels(im): return tuple(v[0] for v in im.layer) @@ -160,7 +161,7 @@ def getchannels(im): "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) - def test_dpi(self, test_image_path): + def test_dpi(self, test_image_path) -> None: def test(xdpi, ydpi=None): with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) @@ -174,7 +175,7 @@ def test(xdpi, ydpi=None): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_icc(self, tmp_path): + def test_icc(self, tmp_path: Path) -> None: # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: icc_profile = im1.info["icc_profile"] @@ -206,7 +207,7 @@ def test_icc(self, tmp_path): ImageFile.MAXBLOCK * 4 + 3, # large block ), ) - def test_icc_big(self, n): + def test_icc_big(self, n) -> None: # Make sure that the "extra" support handles large blocks # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of @@ -219,7 +220,7 @@ def test_icc_big(self, n): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_large_icc_meta(self, tmp_path): + def test_large_icc_meta(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. @@ -243,7 +244,7 @@ def test_large_icc_meta(self, tmp_path): f = str(tmp_path / "temp3.jpg") im.save(f, progressive=True, quality=94, exif=b" " * 43668) - def test_optimize(self): + def test_optimize(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) im3 = self.roundtrip(hopper(), optimize=1) @@ -252,14 +253,14 @@ def test_optimize(self): assert im1.bytes >= im2.bytes assert im1.bytes >= im3.bytes - def test_optimize_large_buffer(self, tmp_path): + def test_optimize_large_buffer(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) - def test_progressive(self): + def test_progressive(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), progressive=False) im3 = self.roundtrip(hopper(), progressive=True) @@ -270,25 +271,25 @@ def test_progressive(self): assert_image_equal(im1, im3) assert im1.bytes >= im3.bytes - def test_progressive_large_buffer(self, tmp_path): + def test_progressive_large_buffer(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) - def test_progressive_large_buffer_highest_quality(self, tmp_path): + def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) - def test_progressive_cmyk_buffer(self): + def test_progressive_cmyk_buffer(self) -> None: # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. f = BytesIO() im = self.gen_random_image((256, 256), "CMYK") im.save(f, format="JPEG", progressive=True, quality=94) - def test_large_exif(self, tmp_path): + def test_large_exif(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() @@ -297,12 +298,12 @@ def test_large_exif(self, tmp_path): with pytest.raises(ValueError): im.save(f, "JPEG", quality=90, exif=b"1" * 65534) - def test_exif_typeerror(self): + def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_exif_gps(self, tmp_path): + def test_exif_gps(self, tmp_path: Path) -> None: expected_exif_gps = { 0: b"\x00\x00\x00\x01", 2: 4294967295, @@ -327,7 +328,7 @@ def test_exif_gps(self, tmp_path): exif = reloaded._getexif() assert exif[gps_index] == expected_exif_gps - def test_empty_exif_gps(self): + def test_empty_exif_gps(self) -> None: with Image.open("Tests/images/empty_gps_ifd.jpg") as im: exif = im.getexif() del exif[0x8769] @@ -345,7 +346,7 @@ def test_empty_exif_gps(self): # Assert that it was transposed assert 0x0112 not in exif - def test_exif_equality(self): + def test_exif_equality(self) -> None: # In 7.2.0, Exif rationals were changed to be read as # TiffImagePlugin.IFDRational. This class had a bug in __eq__, # breaking the self-equality of Exif data @@ -355,7 +356,7 @@ def test_exif_equality(self): exifs.append(im._getexif()) assert exifs[0] == exifs[1] - def test_exif_rollback(self): + def test_exif_rollback(self) -> None: # rolling back exif support in 3.1 to pre-3.0 formatting. # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility # this test passes on 2.9 and 3.1, but not 3.0 @@ -390,12 +391,12 @@ def test_exif_rollback(self): for tag, value in expected_exif.items(): assert value == exif[tag] - def test_exif_gps_typeerror(self): + def test_exif_gps_typeerror(self) -> None: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_progressive_compat(self): + def test_progressive_compat(self) -> None: im1 = self.roundtrip(hopper()) assert not im1.info.get("progressive") assert not im1.info.get("progression") @@ -416,7 +417,7 @@ def test_progressive_compat(self): assert im3.info.get("progressive") assert im3.info.get("progression") - def test_quality(self): + def test_quality(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), quality=50) assert_image(im1, im2.mode, im2.size) @@ -426,12 +427,12 @@ def test_quality(self): assert_image(im1, im3.mode, im3.size) assert im2.bytes > im3.bytes - def test_smooth(self): + def test_smooth(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), smooth=100) assert_image(im1, im2.mode, im2.size) - def test_subsampling(self): + def test_subsampling(self) -> None: def getsampling(im): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] @@ -463,23 +464,23 @@ def getsampling(im): with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") - def test_exif(self): + def test_exif(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() assert info[305] == "Adobe Photoshop CS Macintosh" - def test_get_child_images(self): + def test_get_child_images(self) -> None: with Image.open("Tests/images/flower.jpg") as im: ims = im.get_child_images() assert len(ims) == 1 assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) - def test_mp(self): + def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None - def test_quality_keep(self, tmp_path): + def test_quality_keep(self, tmp_path: Path) -> None: # RGB with Image.open("Tests/images/hopper.jpg") as im: f = str(tmp_path / "temp.jpg") @@ -493,13 +494,13 @@ def test_quality_keep(self, tmp_path): f = str(tmp_path / "temp.jpg") im.save(f, quality="keep") - def test_junk_jpeg_header(self): + def test_junk_jpeg_header(self) -> None: # https://github.com/python-pillow/Pillow/issues/630 filename = "Tests/images/junk_jpeg_header.jpg" with Image.open(filename): pass - def test_ff00_jpeg_header(self): + def test_ff00_jpeg_header(self) -> None: filename = "Tests/images/jpeg_ff00_header.jpg" with Image.open(filename): pass @@ -507,7 +508,7 @@ def test_ff00_jpeg_header(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_truncated_jpeg_should_read_all_the_data(self): + def test_truncated_jpeg_should_read_all_the_data(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True with Image.open(filename) as im: @@ -515,7 +516,7 @@ def test_truncated_jpeg_should_read_all_the_data(self): ImageFile.LOAD_TRUNCATED_IMAGES = False assert im.getbbox() is not None - def test_truncated_jpeg_throws_oserror(self): + def test_truncated_jpeg_throws_oserror(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" with Image.open(filename) as im: with pytest.raises(OSError): @@ -528,8 +529,8 @@ def test_truncated_jpeg_throws_oserror(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_qtables(self, tmp_path): - def _n_qtables_helper(n, test_file): + def test_qtables(self, tmp_path: Path) -> None: + def _n_qtables_helper(n, test_file) -> None: with Image.open(test_file) as im: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) @@ -637,24 +638,24 @@ def _n_qtables_helper(n, test_file): with pytest.raises(ValueError): self.roundtrip(im, qtables=[[1, 2, 3, 4]]) - def test_load_16bit_qtables(self): + def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 - def test_save_multiple_16bit_qtables(self): + def test_save_multiple_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables="keep") assert im.quantization == im2.quantization - def test_save_single_16bit_qtable(self): + def test_save_single_16bit_qtable(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] - def test_save_low_quality_baseline_qtables(self): + def test_save_low_quality_baseline_qtables(self) -> None: with Image.open(TEST_FILE) as im: im2 = self.roundtrip(im, quality=10) assert len(im2.quantization) == 2 @@ -665,7 +666,7 @@ def test_save_low_quality_baseline_qtables(self): "blocks, rows, markers", ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ) - def test_restart_markers(self, blocks, rows, markers): + def test_restart_markers(self, blocks, rows, markers) -> None: im = Image.new("RGB", (32, 32)) # 16 MCUs out = BytesIO() im.save( @@ -679,20 +680,20 @@ def test_restart_markers(self, blocks, rows, markers): assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg(self): + def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg(self, tmp_path): + def test_save_cjpeg(self, tmp_path: Path) -> None: with Image.open(TEST_FILE) as img: tempfile = str(tmp_path / "temp.jpg") JpegImagePlugin._save_cjpeg(img, 0, tempfile) # Default save quality is 75%, so a tiny bit of difference is alright assert_image_similar_tofile(img, tempfile, 17) - def test_no_duplicate_0x1001_tag(self): + def test_no_duplicate_0x1001_tag(self) -> None: # Arrange tag_ids = {v: k for k, v in ExifTags.TAGS.items()} @@ -700,7 +701,7 @@ def test_no_duplicate_0x1001_tag(self): assert tag_ids["RelatedImageWidth"] == 0x1001 assert tag_ids["RelatedImageLength"] == 0x1002 - def test_MAXBLOCK_scaling(self, tmp_path): + def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: im = self.gen_random_image((512, 512)) f = str(tmp_path / "temp.jpeg") im.save(f, quality=100, optimize=True) @@ -711,7 +712,7 @@ def test_MAXBLOCK_scaling(self, tmp_path): reloaded.save(f, quality="keep", progressive=True) reloaded.save(f, quality="keep", optimize=True) - def test_bad_mpo_header(self): + def test_bad_mpo_header(self) -> None: """Treat unknown MPO as JPEG""" # Arrange @@ -723,20 +724,20 @@ def test_bad_mpo_header(self): assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode): + def test_save_correct_modes(self, mode) -> None: out = BytesIO() img = Image.new(mode, (20, 20)) img.save(out, "JPEG") @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode): + def test_save_wrong_modes(self, mode) -> None: # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() img = Image.new(mode, (20, 20)) with pytest.raises(OSError): img.save(out, "JPEG") - def test_save_tiff_with_dpi(self, tmp_path): + def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: @@ -748,7 +749,7 @@ def test_save_tiff_with_dpi(self, tmp_path): reloaded.load() assert im.info["dpi"] == reloaded.info["dpi"] - def test_save_dpi_rounding(self, tmp_path): + def test_save_dpi_rounding(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -761,7 +762,7 @@ def test_save_dpi_rounding(self, tmp_path): with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == (73, 73) - def test_dpi_tuple_from_exif(self): + def test_dpi_tuple_from_exif(self) -> None: # Arrange # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) @@ -769,7 +770,7 @@ def test_dpi_tuple_from_exif(self): # Act / Assert assert im.info.get("dpi") == (200, 200) - def test_dpi_int_from_exif(self): + def test_dpi_int_from_exif(self) -> None: # Arrange # This image has DPI in EXIF not metadata # EXIF XResolution is 72 @@ -777,7 +778,7 @@ def test_dpi_int_from_exif(self): # Act / Assert assert im.info.get("dpi") == (72, 72) - def test_dpi_from_dpcm_exif(self): + def test_dpi_from_dpcm_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg @@ -785,7 +786,7 @@ def test_dpi_from_dpcm_exif(self): # Act / Assert assert im.info.get("dpi") == (508, 508) - def test_dpi_exif_zero_division(self): + def test_dpi_exif_zero_division(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg @@ -794,7 +795,7 @@ def test_dpi_exif_zero_division(self): # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_string(self): + def test_dpi_exif_string(self) -> None: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: @@ -802,14 +803,14 @@ def test_dpi_exif_string(self): # This should return the default assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_truncated(self): + def test_dpi_exif_truncated(self) -> None: # Arrange with Image.open("Tests/images/truncated_exif_dpi.jpg") as im: # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) - def test_no_dpi_in_exif(self): + def test_no_dpi_in_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg @@ -819,7 +820,7 @@ def test_no_dpi_in_exif(self): # https://exiv2.org/tags.html assert im.info.get("dpi") == (72, 72) - def test_invalid_exif(self): + def test_invalid_exif(self) -> None: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: @@ -830,7 +831,7 @@ def test_invalid_exif(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_x_resolution(self, tmp_path): + def test_exif_x_resolution(self, tmp_path: Path) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif[282] == 180 @@ -842,14 +843,14 @@ def test_exif_x_resolution(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.getexif()[282] == 180 - def test_invalid_exif_x_resolution(self): + def test_invalid_exif_x_resolution(self) -> None: # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) - def test_ifd_offset_exif(self): + def test_ifd_offset_exif(self) -> None: # Arrange # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 @@ -857,14 +858,14 @@ def test_ifd_offset_exif(self): # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" - def test_multiple_exif(self): + def test_multiple_exif(self) -> None: with Image.open("Tests/images/multiple_exif.jpg") as im: assert im.info["exif"] == b"Exif\x00\x00firstsecond" @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_photoshop(self): + def test_photoshop(self) -> None: with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { "XResolution": 200.0, @@ -881,14 +882,14 @@ def test_photoshop(self): with Image.open("Tests/images/app13.jpg") as im: assert "photoshop" not in im.info - def test_photoshop_malformed_and_multiple(self): + def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] assert [65504, 24] == apps_13_lengths - def test_adobe_transform(self): + def test_adobe_transform(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im.info["adobe_transform"] == 1 @@ -902,11 +903,11 @@ def test_adobe_transform(self): assert "adobe" in im.info assert "adobe_transform" not in im.info - def test_icc_after_SOF(self): + def test_icc_after_SOF(self) -> None: with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" - def test_jpeg_magic_number(self): + def test_jpeg_magic_number(self) -> None: size = 4097 buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer.max_pos = 0 @@ -925,7 +926,7 @@ def read(n=-1): # Assert the entire file has not been read assert 0 < buffer.max_pos < size - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/xmp_test.jpg") as im: if ElementTree is None: with pytest.warns( @@ -954,7 +955,7 @@ def test_getxmp(self): with Image.open("Tests/images/hopper.jpg") as im: assert im.getxmp() == {} - def test_getxmp_no_prefix(self): + def test_getxmp_no_prefix(self) -> None: with Image.open("Tests/images/xmp_no_prefix.jpg") as im: if ElementTree is None: with pytest.warns( @@ -965,7 +966,7 @@ def test_getxmp_no_prefix(self): else: assert im.getxmp() == {"xmpmeta": {"key": "value"}} - def test_getxmp_padded(self): + def test_getxmp_padded(self) -> None: with Image.open("Tests/images/xmp_padded.jpg") as im: if ElementTree is None: with pytest.warns( @@ -977,7 +978,7 @@ def test_getxmp_padded(self): assert im.getxmp() == {"xmpmeta": None} @pytest.mark.timeout(timeout=1) - def test_eof(self): + def test_eof(self) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): @@ -1000,7 +1001,7 @@ def closure(mode, *args): im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_separate_tables(self): + def test_separate_tables(self) -> None: im = hopper() data = [] # [interchange, tables-only, image-only] for streamtype in range(3): @@ -1022,14 +1023,14 @@ def test_separate_tables(self): with Image.open(BytesIO(data[1] + data[2])) as combined_im: assert_image_equal(interchange_im, combined_im) - def test_repr_jpeg(self): + def test_repr_jpeg(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: assert repr_jpeg.format == "JPEG" assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpeg_error_returns_none(self): + def test_repr_jpeg_error_returns_none(self) -> None: im = hopper("F") assert im._repr_jpeg_() is None @@ -1038,7 +1039,7 @@ def test_repr_jpeg_error_returns_none(self): @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") class TestFileCloseW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 94b02c9ff52..e3f1fa8fde1 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -3,6 +3,7 @@ import os import re from io import BytesIO +from pathlib import Path import pytest @@ -46,7 +47,7 @@ def roundtrip(im, **options): return im -def test_sanity(): +def test_sanity() -> None: # Internal version number assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) @@ -59,20 +60,20 @@ def test_sanity(): assert im.get_format_mimetype() == "image/jp2" -def test_jpf(): +def test_jpf() -> None: with Image.open("Tests/images/balloon.jpf") as im: assert im.format == "JPEG2000" assert im.get_format_mimetype() == "image/jpx" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio(): +def test_bytesio() -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) assert_image_similar_tofile(test_card, data, 1.0e-3) @@ -82,7 +83,7 @@ def test_bytesio(): # PIL (they were made using Adobe Photoshop) -def test_lossless(tmp_path): +def test_lossless(tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") @@ -90,54 +91,54 @@ def test_lossless(tmp_path): assert_image_similar(im, test_card, 1.0e-3) -def test_lossy_tiled(): +def test_lossy_tiled() -> None: assert_image_similar_tofile( test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 ) -def test_lossless_rt(): +def test_lossless_rt() -> None: im = roundtrip(test_card) assert_image_equal(im, test_card) -def test_lossy_rt(): +def test_lossy_rt() -> None: im = roundtrip(test_card, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_tiled_rt(): +def test_tiled_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128)) assert_image_equal(im, test_card) -def test_tiled_offset_rt(): +def test_tiled_offset_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) assert_image_equal(im, test_card) -def test_tiled_offset_too_small(): +def test_tiled_offset_too_small() -> None: with pytest.raises(ValueError): roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt(): +def test_irreversible_rt() -> None: im = roundtrip(test_card, irreversible=True, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_prog_qual_rt(): +def test_prog_qual_rt() -> None: im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt(): +def test_prog_res_rt() -> None: im = roundtrip(test_card, num_resolutions=8, progression="RLCP") assert_image_equal(im, test_card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions): +def test_default_num_resolutions(num_resolutions) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -146,7 +147,7 @@ def test_default_num_resolutions(num_resolutions): assert_image_equal(im, reloaded) -def test_reduce(): +def test_reduce() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) @@ -160,7 +161,7 @@ def test_reduce(): assert im.size == (40, 30) -def test_load_dpi(): +def test_load_dpi() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.info["dpi"] == (71.9836, 71.9836) @@ -168,7 +169,7 @@ def test_load_dpi(): assert "dpi" not in im.info -def test_restricted_icc_profile(): +def test_restricted_icc_profile() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: # JPEG2000 image with a restricted ICC profile and a known colorspace @@ -178,7 +179,7 @@ def test_restricted_icc_profile(): ImageFile.LOAD_TRUNCATED_IMAGES = False -def test_header_errors(): +def test_header_errors() -> None: for path in ( "Tests/images/invalid_header_length.jp2", "Tests/images/not_enough_data.jp2", @@ -192,7 +193,7 @@ def test_header_errors(): pass -def test_layers_type(tmp_path): +def test_layers_type(tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) @@ -202,7 +203,7 @@ def test_layers_type(tmp_path): test_card.save(outfile, quality_layers=quality_layers) -def test_layers(): +def test_layers() -> None: out = BytesIO() test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) @@ -232,7 +233,7 @@ def test_layers(): ("foo.jp2", {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name, args, offset, data): +def test_no_jp2(name, args, offset, data) -> None: out = BytesIO() if name: out.name = name @@ -241,7 +242,7 @@ def test_no_jp2(name, args, offset, data): assert out.read(2) == data -def test_mct(): +def test_mct() -> None: # Three component for val in (0, 1): out = BytesIO() @@ -262,7 +263,7 @@ def test_mct(): assert_image_similar(im, jp2, 1.0e-3) -def test_sgnd(tmp_path): +def test_sgnd(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jp2") im = Image.new("L", (1, 1)) @@ -277,7 +278,7 @@ def test_sgnd(tmp_path): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext): +def test_rgba(ext) -> None: # Arrange with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: # Act @@ -288,47 +289,47 @@ def test_rgba(ext): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_16bit_monochrome_has_correct_mode(ext): +def test_16bit_monochrome_has_correct_mode(ext) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: im.load() assert im.mode == "I;16" -def test_16bit_monochrome_jp2_like_tiff(): +def test_16bit_monochrome_jp2_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) -def test_16bit_monochrome_j2k_like_tiff(): +def test_16bit_monochrome_j2k_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) -def test_16bit_j2k_roundtrips(): +def test_16bit_j2k_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.j2k") as j2k: im = roundtrip(j2k) assert_image_equal(im, j2k) -def test_16bit_jp2_roundtrips(): +def test_16bit_jp2_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.jp2") as jp2: im = roundtrip(jp2) assert_image_equal(im, jp2) -def test_issue_6194(): +def test_issue_6194() -> None: with Image.open("Tests/images/issue_6194.j2k") as im: assert im.getpixel((5, 5)) == 31 -def test_unbound_local(): +def test_unbound_local() -> None: # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. with pytest.raises(OSError): with Image.open("Tests/images/unbound_variable.jp2"): pass -def test_parser_feed(): +def test_parser_feed() -> None: # Arrange with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = f.read() @@ -345,7 +346,7 @@ def test_parser_feed(): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name): +def test_subsampling_decode(name) -> None: test = f"{EXTRA_DIR}/{name}.jp2" reference = f"{EXTRA_DIR}/{name}.ppm" @@ -361,7 +362,7 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) -def test_comment(): +def test_comment() -> None: with Image.open("Tests/images/comment.jp2") as im: assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" @@ -372,7 +373,7 @@ def test_comment(): pass -def test_save_comment(): +def test_save_comment() -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) @@ -399,7 +400,7 @@ def test_save_comment(): "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", ], ) -def test_crashes(test_file): +def test_crashes(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: # Valgrind should not complain here @@ -410,7 +411,7 @@ def test_crashes(test_file): @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker(): +def test_plt_marker() -> None: # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 494253c87c1..1386034e5a3 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -7,6 +7,7 @@ import re import sys from collections import namedtuple +from pathlib import Path import pytest @@ -26,7 +27,7 @@ @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path, im): + def _assert_noerr(self, tmp_path: Path, im) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -50,10 +51,10 @@ def _assert_noerr(self, tmp_path, im): class TestFileLibTiff(LibTiffTestCase): - def test_version(self): + def test_version(self) -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) - def test_g4_tiff(self, tmp_path): + def test_g4_tiff(self, tmp_path: Path) -> None: """Test the ordinary file path load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -61,12 +62,12 @@ def test_g4_tiff(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_large(self, tmp_path): + def test_g4_large(self, tmp_path: Path) -> None: test_file = "Tests/images/pport_g4.tif" with Image.open(test_file) as im: self._assert_noerr(tmp_path, im) - def test_g4_tiff_file(self, tmp_path): + def test_g4_tiff_file(self, tmp_path: Path) -> None: """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -75,7 +76,7 @@ def test_g4_tiff_file(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_tiff_bytesio(self, tmp_path): + def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -86,7 +87,7 @@ def test_g4_tiff_bytesio(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_non_disk_file_object(self, tmp_path): + def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: """Testing loading from non-disk non-BytesIO file object""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -98,18 +99,18 @@ def test_g4_non_disk_file_object(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_eq_png(self): + def test_g4_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/hopper_bw_500.png") as png: assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") # see https://github.com/python-pillow/Pillow/issues/279 - def test_g4_fillorder_eq_png(self): + def test_g4_fillorder_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/g4-fillorder-test.tif") as g4: assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") - def test_g4_write(self, tmp_path): + def test_g4_write(self, tmp_path: Path) -> None: """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -128,7 +129,7 @@ def test_g4_write(self, tmp_path): assert orig.tobytes() != reread.tobytes() - def test_adobe_deflate_tiff(self): + def test_adobe_deflate_tiff(self) -> None: test_file = "Tests/images/tiff_adobe_deflate.tif" with Image.open(test_file) as im: assert im.mode == "RGB" @@ -139,7 +140,7 @@ def test_adobe_deflate_tiff(self): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api, tmp_path): + def test_write_metadata(self, legacy_api, tmp_path: Path) -> None: """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -184,7 +185,7 @@ def test_write_metadata(self, legacy_api, tmp_path): assert field in reloaded, f"{field} not in metadata" @pytest.mark.valgrind_known_error(reason="Known invalid metadata") - def test_additional_metadata(self, tmp_path): + def test_additional_metadata(self, tmp_path: Path) -> None: # these should not crash. Seriously dummy data, most of it doesn't make # any sense, so we're running up against limits where we're asking # libtiff to do stupid things. @@ -241,7 +242,7 @@ def test_additional_metadata(self, tmp_path): TiffImagePlugin.WRITE_LIBTIFF = False - def test_custom_metadata(self, tmp_path): + def test_custom_metadata(self, tmp_path: Path) -> None: tc = namedtuple("test_case", "value,type,supported_by_default") custom = { 37000 + k: v @@ -283,7 +284,7 @@ def test_custom_metadata(self, tmp_path): for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - def check_tags(tiffinfo): + def check_tags(tiffinfo) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -322,7 +323,7 @@ def check_tags(tiffinfo): ) TiffImagePlugin.WRITE_LIBTIFF = False - def test_subifd(self, tmp_path): + def test_subifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[SUBIFD] = 10000 @@ -330,7 +331,7 @@ def test_subifd(self, tmp_path): # Should not segfault im.save(outfile) - def test_xmlpacket_tag(self, tmp_path): + def test_xmlpacket_tag(self, tmp_path: Path) -> None: TiffImagePlugin.WRITE_LIBTIFF = True out = str(tmp_path / "temp.tif") @@ -341,7 +342,7 @@ def test_xmlpacket_tag(self, tmp_path): if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" - def test_int_dpi(self, tmp_path): + def test_int_dpi(self, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -351,7 +352,7 @@ def test_int_dpi(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.info["dpi"] == (72.0, 72.0) - def test_g3_compression(self, tmp_path): + def test_g3_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_g4_500.tif") as i: out = str(tmp_path / "temp.tif") i.save(out, compression="group3") @@ -360,7 +361,7 @@ def test_g3_compression(self, tmp_path): assert reread.info["compression"] == "group3" assert_image_equal(reread, i) - def test_little_endian(self, tmp_path): + def test_little_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -379,7 +380,7 @@ def test_little_endian(self, tmp_path): # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def test_big_endian(self, tmp_path): + def test_big_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.MM.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -396,7 +397,7 @@ def test_big_endian(self, tmp_path): assert reread.info["compression"] == im.info["compression"] assert reread.getpixel((0, 0)) == 480 - def test_g4_string_info(self, tmp_path): + def test_g4_string_info(self, tmp_path: Path) -> None: """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -409,7 +410,7 @@ def test_g4_string_info(self, tmp_path): assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" TiffImagePlugin.READ_LIBTIFF = True @@ -424,7 +425,7 @@ def test_12bit_rawmode(self): assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_blur(self, tmp_path): + def test_blur(self, tmp_path: Path) -> None: # test case from irc, how to do blur on b/w image # and save to compressed tif. out = str(tmp_path / "temp.tif") @@ -436,7 +437,7 @@ def test_blur(self, tmp_path): assert_image_equal_tofile(im, out) - def test_compressions(self, tmp_path): + def test_compressions(self, tmp_path: Path) -> None: # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") @@ -462,7 +463,7 @@ def test_compressions(self, tmp_path): assert size_compressed > size_jpeg assert size_jpeg > size_jpeg_30 - def test_tiff_jpeg_compression(self, tmp_path): + def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_jpeg") @@ -470,7 +471,7 @@ def test_tiff_jpeg_compression(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "jpeg" - def test_tiff_deflate_compression(self, tmp_path): + def test_tiff_deflate_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_deflate") @@ -478,7 +479,7 @@ def test_tiff_deflate_compression(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "tiff_adobe_deflate" - def test_quality(self, tmp_path): + def test_quality(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -493,7 +494,7 @@ def test_quality(self, tmp_path): im.save(out, compression="jpeg", quality=0) im.save(out, compression="jpeg", quality=100) - def test_cmyk_save(self, tmp_path): + def test_cmyk_save(self, tmp_path: Path) -> None: im = hopper("CMYK") out = str(tmp_path / "temp.tif") @@ -501,7 +502,7 @@ def test_cmyk_save(self, tmp_path): assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im, tmp_path): + def test_palette_save(self, im, tmp_path: Path) -> None: out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -513,14 +514,14 @@ def test_palette_save(self, im, tmp_path): assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def test_bw_compression_w_rgb(self, compression, tmp_path): + def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(out, compression=compression) - def test_fp_leak(self): + def test_fp_leak(self) -> None: im = Image.open("Tests/images/hopper_g4_500.tif") fn = im.fp.fileno() @@ -534,7 +535,7 @@ def test_fp_leak(self): with pytest.raises(OSError): os.close(fn) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -557,7 +558,7 @@ def test_multipage(self): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_nframes(self): + def test_multipage_nframes(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -570,7 +571,7 @@ def test_multipage_nframes(self): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_seek_backwards(self): + def test_multipage_seek_backwards(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: im.seek(1) @@ -581,14 +582,14 @@ def test_multipage_seek_backwards(self): TiffImagePlugin.READ_LIBTIFF = False - def test__next(self): + def test__next(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.tif") as im: assert not im.tag.next im.load() assert not im.tag.next - def test_4bit(self): + def test_4bit(self) -> None: # Arrange test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") @@ -603,7 +604,7 @@ def test_4bit(self): assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -636,7 +637,7 @@ def test_gray_semibyte_per_pixel(self): assert im2.mode == "L" assert_image_equal(im, im2) - def test_save_bytesio(self): + def test_save_bytesio(self) -> None: # PR 1011 # Test TIFF saving to io.BytesIO() object. @@ -646,7 +647,7 @@ def test_save_bytesio(self): # Generate test image pilim = hopper() - def save_bytesio(compression=None): + def save_bytesio(compression=None) -> None: buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -661,7 +662,7 @@ def save_bytesio(compression=None): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False - def test_save_ycbcr(self, tmp_path): + def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") outfile = str(tmp_path / "temp.tif") im.save(outfile, compression="jpeg") @@ -670,7 +671,7 @@ def test_save_ycbcr(self, tmp_path): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) - def test_exif_ifd(self, tmp_path): + def test_exif_ifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: assert im.tag_v2[34665] == 125456 @@ -680,7 +681,7 @@ def test_exif_ifd(self, tmp_path): if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 - def test_crashing_metadata(self, tmp_path): + def test_crashing_metadata(self, tmp_path: Path) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: out = str(tmp_path / "temp.tif") @@ -690,7 +691,7 @@ def test_crashing_metadata(self, tmp_path): im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False - def test_page_number_x_0(self, tmp_path): + def test_page_number_x_0(self, tmp_path: Path) -> None: # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. @@ -704,7 +705,7 @@ def test_page_number_x_0(self, tmp_path): # Should not divide by zero im.save(outfile) - def test_fd_duplication(self, tmp_path): + def test_fd_duplication(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1651 tmpfile = str(tmp_path / "temp.tif") @@ -718,7 +719,7 @@ def test_fd_duplication(self, tmp_path): # Should not raise PermissionError. os.remove(tmpfile) - def test_read_icc(self): + def test_read_icc(self) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc = img.info.get("icc_profile") assert icc is not None @@ -729,8 +730,8 @@ def test_read_icc(self): TiffImagePlugin.READ_LIBTIFF = False assert icc == icc_libtiff - def test_write_icc(self, tmp_path): - def check_write(libtiff): + def test_write_icc(self, tmp_path: Path) -> None: + def check_write(libtiff) -> None: TiffImagePlugin.WRITE_LIBTIFF = libtiff with Image.open("Tests/images/hopper.iccprofile.tif") as img: @@ -749,7 +750,7 @@ def check_write(libtiff): for libtiff in libtiffs: check_write(libtiff) - def test_multipage_compression(self): + def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: im.seek(0) assert im._compression == "tiff_ccitt" @@ -765,7 +766,7 @@ def test_multipage_compression(self): assert im.size == (10, 10) im.load() - def test_save_tiff_with_jpegtables(self, tmp_path): + def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") @@ -777,7 +778,7 @@ def test_save_tiff_with_jpegtables(self, tmp_path): # Should not raise UnicodeDecodeError or anything else im.save(outfile) - def test_16bit_RGB_tiff(self): + def test_16bit_RGB_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: assert im.mode == "RGB" assert im.size == (100, 40) @@ -793,7 +794,7 @@ def test_16bit_RGB_tiff(self): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_16bit_RGBa_tiff(self): + def test_16bit_RGBa_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: assert im.mode == "RGBA" assert im.size == (100, 40) @@ -805,7 +806,7 @@ def test_16bit_RGBa_tiff(self): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @skip_unless_feature("jpg") - def test_gimp_tiff(self): + def test_gimp_tiff(self) -> None: # Read TIFF JPEG images from GIMP [@PIL168] filename = "Tests/images/pil168.tif" with Image.open(filename) as im: @@ -818,14 +819,14 @@ def test_gimp_tiff(self): assert_image_equal_tofile(im, "Tests/images/pil168.png") - def test_sampleformat(self): + def test_sampleformat(self) -> None: # https://github.com/python-pillow/Pillow/issues/1466 with Image.open("Tests/images/copyleft.tiff") as im: assert im.mode == "RGB" assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") - def test_sampleformat_write(self, tmp_path): + def test_sampleformat_write(self, tmp_path: Path) -> None: im = Image.new("F", (1, 1)) out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -874,7 +875,7 @@ def test_webp(self, capfd): sys.stderr.write(captured.err) raise - def test_lzw(self): + def test_lzw(self) -> None: with Image.open("Tests/images/hopper_lzw.tif") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -882,12 +883,12 @@ def test_lzw(self): im2 = hopper() assert_image_similar(im, im2, 5) - def test_strip_cmyk_jpeg(self): + def test_strip_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - def test_strip_cmyk_16l_jpeg(self): + def test_strip_cmyk_16l_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -895,7 +896,7 @@ def test_strip_cmyk_16l_jpeg(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_2x2_sampling(self): + def test_strip_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) @@ -903,12 +904,12 @@ def test_strip_ycbcr_jpeg_2x2_sampling(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_1x1_sampling(self): + def test_strip_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) - def test_tiled_cmyk_jpeg(self): + def test_tiled_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -916,7 +917,7 @@ def test_tiled_cmyk_jpeg(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_1x1_sampling(self): + def test_tiled_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) @@ -924,45 +925,45 @@ def test_tiled_ycbcr_jpeg_1x1_sampling(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_2x2_sampling(self): + def test_tiled_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) - def test_strip_planar_rgb(self): + def test_strip_planar_rgb(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff infile = "Tests/images/tiff_strip_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_rgb(self): + def test_tiled_planar_rgb(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff infile = "Tests/images/tiff_tiled_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_16bit_RGB(self): + def test_tiled_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_strip_planar_16bit_RGB(self): + def test_strip_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_tiled_planar_16bit_RGBa(self): + def test_tiled_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=yes \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - def test_strip_planar_16bit_RGBa(self): + def test_strip_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=no \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff @@ -970,7 +971,7 @@ def test_strip_planar_16bit_RGBa(self): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @pytest.mark.parametrize("compression", (None, "jpeg")) - def test_block_tile_tags(self, compression, tmp_path): + def test_block_tile_tags(self, compression, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -986,11 +987,11 @@ def test_block_tile_tags(self, compression, tmp_path): for tag in tags: assert tag not in reloaded.getexif() - def test_old_style_jpeg(self): + def test_old_style_jpeg(self) -> None: with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") - def test_open_missing_samplesperpixel(self): + def test_open_missing_samplesperpixel(self) -> None: with Image.open( "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" ) as im: @@ -1019,21 +1020,21 @@ def test_open_missing_samplesperpixel(self): ), ], ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode assert im.size == size assert im.tile == tile im.load() - def test_no_rows_per_strip(self): + def test_no_rows_per_strip(self) -> None: # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" with Image.open(infile) as im: im.load() assert im.size == (950, 975) - def test_orientation(self): + def test_orientation(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1044,7 +1045,7 @@ def test_orientation(self): assert_image_similar(base_im, im, 0.7) - def test_exif_transpose(self): + def test_exif_transpose(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1053,7 +1054,7 @@ def test_exif_transpose(self): assert_image_similar(base_im, im, 0.7) @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") - def test_sampleformat_not_corrupted(self): + def test_sampleformat_not_corrupted(self) -> None: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted # when saving to a new file. # Pillow 6.0 fails with "OSError: cannot identify image file". @@ -1074,7 +1075,7 @@ def test_sampleformat_not_corrupted(self): with Image.open(out) as im: im.load() - def test_realloc_overflow(self): + def test_realloc_overflow(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with pytest.raises(OSError) as e: @@ -1085,7 +1086,7 @@ def test_realloc_overflow(self): TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) - def test_save_multistrip(self, compression, tmp_path): + def test_save_multistrip(self, compression, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") im.save(out, compression=compression) @@ -1095,7 +1096,7 @@ def test_save_multistrip(self, compression, tmp_path): assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument, tmp_path): + def test_save_single_strip(self, argument, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") @@ -1113,13 +1114,13 @@ def test_save_single_strip(self, argument, tmp_path): TiffImagePlugin.STRIP_SIZE = 65536 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression, tmp_path): + def test_save_zero(self, compression, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) - def test_save_many_compressed(self, tmp_path): + def test_save_many_compressed(self, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") for _ in range(10000): @@ -1133,7 +1134,7 @@ def test_save_many_compressed(self, tmp_path): ("Tests/images/child_ifd_jpeg.tiff", (20,)), ), ) - def test_get_child_images(self, path, sizes): + def test_get_child_images(self, path, sizes) -> None: with Image.open(path) as im: ims = im.get_child_images() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 171e4a3f866..ac5270eac30 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path from PIL import Image @@ -17,7 +18,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): file just before reading in libtiff. These tests remain to ensure that it stays fixed.""" - def test_g4_hopper_file(self, tmp_path): + def test_g4_hopper_file(self, tmp_path: Path) -> None: """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" @@ -26,7 +27,7 @@ def test_g4_hopper_file(self, tmp_path): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper_bytesio(self, tmp_path): + def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: """Testing the bytesio loading code path""" test_file = "Tests/images/hopper_g4.tif" s = BytesIO() @@ -37,7 +38,7 @@ def test_g4_hopper_bytesio(self, tmp_path): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper(self, tmp_path): + def test_g4_hopper(self, tmp_path: Path) -> None: """The 128x128 lena image failed for some reason.""" test_file = "Tests/images/hopper_g4.tif" diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 8c43f7d7ab7..9a6f13ea366 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -13,7 +13,7 @@ TEST_FILE = "Tests/images/hopper.mic" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGBA" @@ -28,22 +28,22 @@ def test_sanity(): assert_image_similar(im, im2, 10) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 -def test_is_animated(): +def test_is_animated() -> None: with Image.open(TEST_FILE) as im: assert not im.is_animated -def test_tell(): +def test_tell() -> None: with Image.open(TEST_FILE) as im: assert im.tell() == 0 -def test_seek(): +def test_seek() -> None: with Image.open(TEST_FILE) as im: im.seek(0) assert im.tell() == 0 @@ -53,7 +53,7 @@ def test_seek(): assert im.tell() == 0 -def test_close(): +def test_close() -> None: with Image.open(TEST_FILE) as im: pass assert im.ole.fp.closed @@ -63,7 +63,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index c7121ea28c9..55b04a1e076 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -30,7 +30,7 @@ def roundtrip(im, **options): @pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file): +def test_sanity(test_file) -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -39,8 +39,8 @@ def test_sanity(test_file): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_files[0]) im.load() @@ -48,14 +48,14 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_files[0]) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(test_files[0]) im.close() @@ -63,14 +63,14 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_files[0]) as im: im.load() @pytest.mark.parametrize("test_file", test_files) -def test_app(test_file): +def test_app(test_file) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" @@ -82,7 +82,7 @@ def test_app(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file): +def test_exif(test_file) -> None: with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) @@ -93,7 +93,7 @@ def test_exif(test_file): assert info[34665] == 188 -def test_frame_size(): +def test_frame_size() -> None: # This image has been hexedited to contain a different size # in the EXIF data of the second frame with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: @@ -106,7 +106,7 @@ def test_frame_size(): assert im.size == (640, 480) -def test_ignore_frame_size(): +def test_ignore_frame_size() -> None: # Ignore the different size of the second frame # since this is not a "Large Thumbnail" image with Image.open("Tests/images/ignore_frame_size.mpo") as im: @@ -120,7 +120,7 @@ def test_ignore_frame_size(): assert im.size == (64, 64) -def test_parallax(): +def test_parallax() -> None: # Nintendo with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() @@ -133,7 +133,7 @@ def test_parallax(): assert exif.get_ifd(0x927C)[0xB211] == -3.125 -def test_reload_exif_after_seek(): +def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() del exif[296] @@ -143,14 +143,14 @@ def test_reload_exif_after_seek(): @pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file): +def test_mp(test_file) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 -def test_mp_offset(): +def test_mp_offset() -> None: # This image has been manually hexedited to have an IFD offset of 10 # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: @@ -159,7 +159,7 @@ def test_mp_offset(): assert mpinfo[45057] == 2 -def test_mp_no_data(): +def test_mp_no_data() -> None: # This image has been manually hexedited to have the second frame # beyond the end of the file with Image.open("Tests/images/sugarshack_no_data.mpo") as im: @@ -168,7 +168,7 @@ def test_mp_no_data(): @pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file): +def test_mp_attribute(test_file) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() for frame_number, mpentry in enumerate(mpinfo[0xB002]): @@ -185,7 +185,7 @@ def test_mp_attribute(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file): +def test_seek(test_file) -> None: with Image.open(test_file) as im: assert im.tell() == 0 # prior to first image raises an error, both blatant and borderline @@ -209,13 +209,13 @@ def test_seek(test_file): assert im.tell() == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: assert im.n_frames == 2 assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: n_frames = im.n_frames @@ -229,7 +229,7 @@ def test_eoferror(): @pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file): +def test_image_grab(test_file) -> None: with Image.open(test_file) as im: assert im.tell() == 0 im0 = im.tobytes() @@ -244,7 +244,7 @@ def test_image_grab(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_save(test_file): +def test_save(test_file) -> None: with Image.open(test_file) as im: assert im.tell() == 0 jpg0 = roundtrip(im) @@ -255,7 +255,7 @@ def test_save(test_file): assert_image_similar(im, jpg1, 30) -def test_save_all(): +def test_save_all() -> None: for test_file in test_files: with Image.open(test_file) as im: im_reloaded = roundtrip(im, save_all=True) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 9037ea33b54..f9f81d11413 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import Path import pytest @@ -13,7 +14,7 @@ YA_EXTRA_DIR = "Tests/images/msp" -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.msp") hopper("1").save(test_file) @@ -25,14 +26,14 @@ def test_sanity(tmp_path): assert im.format == "MSP" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): MspImagePlugin.MspImageFile(invalid_file) -def test_bad_checksum(): +def test_bad_checksum() -> None: # Arrange # This was created by forcing Pillow to save with checksum=0 bad_checksum = "Tests/images/hopper_bad_checksum.msp" @@ -42,7 +43,7 @@ def test_bad_checksum(): MspImagePlugin.MspImageFile(bad_checksum) -def test_open_windows_v1(): +def test_open_windows_v1() -> None: # Arrange # Act with Image.open(TEST_FILE) as im: @@ -51,7 +52,7 @@ def test_open_windows_v1(): assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path): +def _assert_file_image_equal(source_path, target_path) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) @@ -59,7 +60,7 @@ def _assert_file_image_equal(source_path, target_path): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_open_windows_v2(): +def test_open_windows_v2() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) @@ -72,7 +73,7 @@ def test_open_windows_v2(): @pytest.mark.skipif( not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" ) -def test_msp_v2(): +def test_msp_v2() -> None: for f in os.listdir(YA_EXTRA_DIR): if ".MSP" not in f: continue @@ -80,7 +81,7 @@ def test_msp_v2(): _assert_file_image_equal(path, path.replace(".MSP", ".png")) -def test_cannot_save_wrong_mode(tmp_path): +def test_cannot_save_wrong_mode(tmp_path: Path) -> None: # Arrange im = hopper() filename = str(tmp_path / "temp.msp") diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index eba69415395..55041a4b202 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -2,6 +2,7 @@ import os.path import subprocess +from pathlib import Path import pytest @@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper, magick_command -def helper_save_as_palm(tmp_path, mode): +def helper_save_as_palm(tmp_path: Path, mode) -> None: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".palm")) @@ -23,7 +24,7 @@ def helper_save_as_palm(tmp_path, mode): assert os.path.getsize(outfile) > 0 -def open_with_magick(magick, tmp_path, f): +def open_with_magick(magick, tmp_path: Path, f): outfile = str(tmp_path / "temp.png") rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT @@ -32,7 +33,7 @@ def open_with_magick(magick, tmp_path, f): return Image.open(outfile) -def roundtrip(tmp_path, mode): +def roundtrip(tmp_path: Path, mode) -> None: magick = magick_command() if not magick: return @@ -45,7 +46,7 @@ def roundtrip(tmp_path, mode): assert_image_equal(converted, im) -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -55,7 +56,7 @@ def test_monochrome(tmp_path): @pytest.mark.xfail(reason="Palm P image is wrong") -def test_p_mode(tmp_path): +def test_p_mode(tmp_path: Path) -> None: # Arrange mode = "P" @@ -65,6 +66,6 @@ def test_p_mode(tmp_path): @pytest.mark.parametrize("mode", ("L", "RGB")) -def test_oserror(tmp_path, mode): +def test_oserror(tmp_path: Path, mode) -> None: with pytest.raises(OSError): helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 2565e0b6ddc..a2486be40c5 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageFile, PcxImagePlugin @@ -7,7 +9,7 @@ from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path, im): +def _roundtrip(tmp_path: Path, im) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -18,7 +20,7 @@ def _roundtrip(tmp_path, im): assert_image_equal(im2, im) -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: for mode in ("1", "L", "P", "RGB"): _roundtrip(tmp_path, hopper(mode)) @@ -34,7 +36,7 @@ def test_sanity(tmp_path): im.save(f) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -42,7 +44,7 @@ def test_invalid_file(): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path, mode): +def test_odd(tmp_path: Path, mode) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -51,7 +53,7 @@ def test_odd(tmp_path, mode): _roundtrip(tmp_path, hopper(mode).resize((511, 511))) -def test_odd_read(): +def test_odd_read() -> None: # Reading an image with an odd stride, making it malformed with Image.open("Tests/images/odd_stride.pcx") as im: im.load() @@ -59,7 +61,7 @@ def test_odd_read(): assert im.size == (371, 150) -def test_pil184(): +def test_pil184() -> None: # Check reading of files where xmin/xmax is not zero. test_file = "Tests/images/pil184.pcx" @@ -71,7 +73,7 @@ def test_pil184(): assert im.histogram()[0] + im.histogram()[255] == 447 * 144 -def test_1px_width(tmp_path): +def test_1px_width(tmp_path: Path) -> None: im = Image.new("L", (1, 256)) px = im.load() for y in range(256): @@ -79,7 +81,7 @@ def test_1px_width(tmp_path): _roundtrip(tmp_path, im) -def test_large_count(tmp_path): +def test_large_count(tmp_path: Path) -> None: im = Image.new("L", (256, 1)) px = im.load() for x in range(256): @@ -87,7 +89,7 @@ def test_large_count(tmp_path): _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path, im, size=1024): +def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: @@ -96,7 +98,7 @@ def _test_buffer_overflow(tmp_path, im, size=1024): ImageFile.MAXBLOCK = _last -def test_break_in_count_overflow(tmp_path): +def test_break_in_count_overflow(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -105,7 +107,7 @@ def test_break_in_count_overflow(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_in_loop(tmp_path): +def test_break_one_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -114,7 +116,7 @@ def test_break_one_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_in_loop(tmp_path): +def test_break_many_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -125,7 +127,7 @@ def test_break_many_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_at_end(tmp_path): +def test_break_one_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -135,7 +137,7 @@ def test_break_one_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_at_end(tmp_path): +def test_break_many_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -147,7 +149,7 @@ def test_break_many_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_padding(tmp_path): +def test_break_padding(tmp_path: Path) -> None: im = Image.new("L", (257, 5)) px = im.load() for y in range(5): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 30c54c963cb..65a93c138e9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -5,6 +5,7 @@ import os.path import tempfile import time +from pathlib import Path import pytest @@ -13,7 +14,7 @@ from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -40,17 +41,17 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path, mode): +def test_save(tmp_path: Path, mode) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path, mode): +def test_save_alpha(tmp_path: Path, mode) -> None: helper_save_as_pdf(tmp_path, mode) -def test_p_alpha(tmp_path): +def test_p_alpha(tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.pdf") with Image.open("Tests/images/pil123p.png") as im: @@ -66,7 +67,7 @@ def test_p_alpha(tmp_path): assert b"\n/SMask " in contents -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -75,7 +76,7 @@ def test_monochrome(tmp_path): assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("PA") outfile = str(tmp_path / "temp_PA.pdf") @@ -83,7 +84,7 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -def test_resolution(tmp_path): +def test_resolution(tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -111,7 +112,7 @@ def test_resolution(tmp_path): {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path): +def test_dpi(params, tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -135,7 +136,7 @@ def test_dpi(params, tmp_path): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_save_all(tmp_path): +def test_save_all(tmp_path: Path) -> None: # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) @@ -171,7 +172,7 @@ def im_generator(ims): assert os.path.getsize(outfile) > 0 -def test_multiframe_normal_save(tmp_path): +def test_multiframe_normal_save(tmp_path: Path) -> None: # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: outfile = str(tmp_path / "temp.pdf") @@ -181,7 +182,7 @@ def test_multiframe_normal_save(tmp_path): assert os.path.getsize(outfile) > 0 -def test_pdf_open(tmp_path): +def test_pdf_open(tmp_path: Path) -> None: # fail on a buffer full of null bytes with pytest.raises(PdfParser.PdfFormatError): PdfParser.PdfParser(buf=bytearray(65536)) @@ -218,14 +219,14 @@ def test_pdf_open(tmp_path): assert not hopper_pdf.should_close_file -def test_pdf_append_fails_on_nonexistent_file(): +def test_pdf_append_fails_on_nonexistent_file() -> None: im = hopper("RGB") with tempfile.TemporaryDirectory() as temp_dir: with pytest.raises(OSError): im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf): +def check_pdf_pages_consistency(pdf) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -243,7 +244,7 @@ def check_pdf_pages_consistency(pdf): assert kids_not_used == [] -def test_pdf_append(tmp_path): +def test_pdf_append(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") @@ -294,7 +295,7 @@ def test_pdf_append(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_info(tmp_path): +def test_pdf_info(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf( tmp_path, @@ -323,7 +324,7 @@ def test_pdf_info(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_append_to_bytesio(): +def test_pdf_append_to_bytesio() -> None: im = hopper("RGB") f = io.BytesIO() im.save(f, format="PDF") @@ -338,7 +339,7 @@ def test_pdf_append_to_bytesio(): @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline): +def test_redos(newline) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ae2a4772b60..0f1d96365ea 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -5,6 +5,7 @@ import warnings import zlib from io import BytesIO +from pathlib import Path import pytest @@ -79,7 +80,7 @@ def get_chunks(self, filename): png.crc(cid, s) return chunks - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: # internal version number assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) @@ -102,13 +103,13 @@ def test_sanity(self, tmp_path): reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PngImagePlugin.PngImageFile(invalid_file) - def test_broken(self): + def test_broken(self) -> None: # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. @@ -117,7 +118,7 @@ def test_broken(self): with Image.open(test_file): pass - def test_bad_text(self): + def test_bad_text(self) -> None: # Make sure PIL can read malformed tEXt chunks (@PIL152) im = load(HEAD + chunk(b"tEXt") + TAIL) @@ -135,7 +136,7 @@ def test_bad_text(self): im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) assert im.info == {"spam": "egg\x00"} - def test_bad_ztxt(self): + def test_bad_ztxt(self) -> None: # Test reading malformed zTXt chunks (python-pillow/Pillow#318) im = load(HEAD + chunk(b"zTXt") + TAIL) @@ -156,7 +157,7 @@ def test_bad_ztxt(self): im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) assert im.info == {"spam": "egg"} - def test_bad_itxt(self): + def test_bad_itxt(self) -> None: im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -200,7 +201,7 @@ def test_bad_itxt(self): assert im.info["spam"].lang == "en" assert im.info["spam"].tkey == "Spam" - def test_interlace(self): + def test_interlace(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -215,7 +216,7 @@ def test_interlace(self): im.load() - def test_load_transparent_p(self): + def test_load_transparent_p(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -225,7 +226,7 @@ def test_load_transparent_p(self): # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_load_transparent_rgb(self): + def test_load_transparent_rgb(self) -> None: test_file = "Tests/images/rgb_trns.png" with Image.open(test_file) as im: assert im.info["transparency"] == (0, 255, 52) @@ -237,7 +238,7 @@ def test_load_transparent_rgb(self): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_palette(self, tmp_path): + def test_save_p_transparent_palette(self, tmp_path: Path) -> None: in_file = "Tests/images/pil123p.png" with Image.open(in_file) as im: # 'transparency' contains a byte string with the opacity for @@ -258,7 +259,7 @@ def test_save_p_transparent_palette(self, tmp_path): # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_save_p_single_transparency(self, tmp_path): + def test_save_p_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/p_trns_single.png" with Image.open(in_file) as im: # pixel value 164 is full transparent @@ -281,7 +282,7 @@ def test_save_p_single_transparency(self, tmp_path): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_black(self, tmp_path): + def test_save_p_transparent_black(self, tmp_path: Path) -> None: # check if solid black image with full transparency # is supported (check for #1838) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) @@ -299,7 +300,7 @@ def test_save_p_transparent_black(self, tmp_path): assert_image(im, "RGBA", (10, 10)) assert im.getcolors() == [(100, (0, 0, 0, 0))] - def test_save_grayscale_transparency(self, tmp_path): + def test_save_grayscale_transparency(self, tmp_path: Path) -> None: for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): in_file = "Tests/images/" + mode.lower() + "_trns.png" with Image.open(in_file) as im: @@ -320,13 +321,13 @@ def test_save_grayscale_transparency(self, tmp_path): test_im_rgba = test_im.convert("RGBA") assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - def test_save_rgb_single_transparency(self, tmp_path): + def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: test_file = str(tmp_path / "temp.png") im.save(test_file) - def test_load_verify(self): + def test_load_verify(self) -> None: # Check open/load/verify exception (@PIL150) with Image.open(TEST_PNG_FILE) as im: @@ -339,7 +340,7 @@ def test_load_verify(self): with pytest.raises(RuntimeError): im.verify() - def test_verify_struct_error(self): + def test_verify_struct_error(self) -> None: # Check open/load/verify exception (#1755) # offsets to test, -10: breaks in i32() in read. (OSError) @@ -355,7 +356,7 @@ def test_verify_struct_error(self): with pytest.raises((OSError, SyntaxError)): im.verify() - def test_verify_ignores_crc_error(self): + def test_verify_ignores_crc_error(self) -> None: # check ignores crc errors in ancillary chunks chunk_data = chunk(b"tEXt", b"spam") @@ -372,7 +373,7 @@ def test_verify_ignores_crc_error(self): finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_verify_not_ignores_crc_error_in_required_chunk(self): + def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: # check does not ignore crc errors in required chunks image_data = MAGIC + IHDR[:-1] + b"q" + TAIL @@ -384,18 +385,18 @@ def test_verify_not_ignores_crc_error_in_required_chunk(self): finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_roundtrip_dpi(self): + def test_roundtrip_dpi(self) -> None: # Check dpi roundtripping with Image.open(TEST_PNG_FILE) as im: im = roundtrip(im, dpi=(100.33, 100.33)) assert im.info["dpi"] == (100.33, 100.33) - def test_load_float_dpi(self): + def test_load_float_dpi(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.info["dpi"] == (95.9866, 95.9866) - def test_roundtrip_text(self): + def test_roundtrip_text(self) -> None: # Check text roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -407,7 +408,7 @@ def test_roundtrip_text(self): assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_roundtrip_itxt(self): + def test_roundtrip_itxt(self) -> None: # Check iTXt roundtripping im = Image.new("RGB", (32, 32)) @@ -423,7 +424,7 @@ def test_roundtrip_itxt(self): assert im.text["eggs"].lang == "en" assert im.text["eggs"].tkey == "Eggs" - def test_nonunicode_text(self): + def test_nonunicode_text(self) -> None: # Check so that non-Unicode text is saved as a tEXt rather than iTXt im = Image.new("RGB", (32, 32)) @@ -432,10 +433,10 @@ def test_nonunicode_text(self): im = roundtrip(im, pnginfo=info) assert isinstance(im.info["Text"], str) - def test_unicode_text(self): + def test_unicode_text(self) -> None: # Check preservation of non-ASCII characters - def rt_text(value): + def rt_text(value) -> None: im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) @@ -448,7 +449,7 @@ def rt_text(value): rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined - def test_scary(self): + def test_scary(self) -> None: # Check reading of evil PNG file. For information, see: # http://scary.beasts.org/security/CESA-2004-001.txt # The first byte is removed from pngtest_bad.png @@ -462,7 +463,7 @@ def test_scary(self): with Image.open(pngfile): pass - def test_trns_rgb(self): + def test_trns_rgb(self) -> None: # Check writing and reading of tRNS chunks for RGB images. # Independent file sample provided by Sebastian Spaeth. @@ -477,7 +478,7 @@ def test_trns_rgb(self): im = roundtrip(im, transparency=(0, 1, 2)) assert im.info["transparency"] == (0, 1, 2) - def test_trns_p(self, tmp_path): + def test_trns_p(self, tmp_path: Path) -> None: # Check writing a transparency of 0, issue #528 im = hopper("P") im.info["transparency"] = 0 @@ -490,13 +491,13 @@ def test_trns_p(self, tmp_path): assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) - def test_trns_null(self): + def test_trns_null(self) -> None: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: assert im.info["transparency"] == 0 - def test_save_icc_profile(self): + def test_save_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None @@ -506,40 +507,40 @@ def test_save_icc_profile(self): im = roundtrip(im, icc_profile=expected_icc) assert im.info["icc_profile"] == expected_icc - def test_discard_icc_profile(self): + def test_discard_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: assert "icc_profile" in im.info im = roundtrip(im, icc_profile=None) assert "icc_profile" not in im.info - def test_roundtrip_icc_profile(self): + def test_roundtrip_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: expected_icc = im.info["icc_profile"] im = roundtrip(im) assert im.info["icc_profile"] == expected_icc - def test_roundtrip_no_icc_profile(self): + def test_roundtrip_no_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None im = roundtrip(im) assert "icc_profile" not in im.info - def test_repr_png(self): + def test_repr_png(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_png_())) as repr_png: assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_repr_png_error_returns_none(self): + def test_repr_png_error_returns_none(self) -> None: im = hopper("F") assert im._repr_png_() is None - def test_chunk_order(self, tmp_path): + def test_chunk_order(self, tmp_path: Path) -> None: with Image.open("Tests/images/icc_profile.png") as im: test_file = str(tmp_path / "temp.png") im.convert("P").save(test_file, dpi=(100, 100)) @@ -560,17 +561,17 @@ def test_chunk_order(self, tmp_path): # pHYs - before IDAT assert chunks.index(b"pHYs") < chunks.index(b"IDAT") - def test_getchunks(self): + def test_getchunks(self) -> None: im = hopper() chunks = PngImagePlugin.getchunks(im) assert len(chunks) == 3 - def test_read_private_chunks(self): + def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: assert im.private_chunks == [(b"orNT", b"\x01")] - def test_roundtrip_private_chunk(self): + def test_roundtrip_private_chunk(self) -> None: # Check private chunk roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -588,7 +589,7 @@ def test_roundtrip_private_chunk(self): (b"prIV", b"VALUE3", True), ] - def test_textual_chunks_after_idat(self): + def test_textual_chunks_after_idat(self) -> None: with Image.open("Tests/images/hopper.png") as im: assert "comment" in im.text for k, v in { @@ -615,7 +616,7 @@ def test_textual_chunks_after_idat(self): with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_padded_idat(self): + def test_padded_idat(self) -> None: # This image has been manually hexedited # so that the IDAT chunk has padding at the end # Set MAXBLOCK to the length of the actual data @@ -635,7 +636,7 @@ def test_padded_idat(self): @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid): + def test_truncated_chunks(self, cid) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): @@ -645,7 +646,7 @@ def test_truncated_chunks(self, cid): png.call(cid, 0, 0) ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_specify_bits(self, tmp_path): + def test_specify_bits(self, tmp_path: Path) -> None: im = hopper("P") out = str(tmp_path / "temp.png") @@ -654,7 +655,7 @@ def test_specify_bits(self, tmp_path): with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 48 - def test_plte_length(self, tmp_path): + def test_plte_length(self, tmp_path: Path) -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 1, 1)) @@ -664,7 +665,7 @@ def test_plte_length(self, tmp_path): with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/color_snakes.png") as im: if ElementTree is None: with pytest.warns( @@ -679,7 +680,7 @@ def test_getxmp(self): assert description["PixelXDimension"] == "10" assert description["subject"]["Seq"] is None - def test_exif(self): + def test_exif(self) -> None: # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: exif = im._getexif() @@ -705,7 +706,7 @@ def test_exif(self): exif = im.getexif() assert exif[274] == 3 - def test_exif_save(self, tmp_path): + def test_exif_save(self, tmp_path: Path) -> None: # Test exif is not saved from info test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/exif.png") as im: @@ -725,7 +726,7 @@ def test_exif_save(self, tmp_path): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_from_jpg(self, tmp_path): + def test_exif_from_jpg(self, tmp_path: Path) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=im.getexif()) @@ -734,7 +735,7 @@ def test_exif_from_jpg(self, tmp_path): exif = reloaded._getexif() assert exif[305] == "Adobe Photoshop CS Macintosh" - def test_exif_argument(self, tmp_path): + def test_exif_argument(self, tmp_path: Path) -> None: with Image.open(TEST_PNG_FILE) as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=b"exifstring") @@ -742,11 +743,11 @@ def test_exif_argument(self, tmp_path): with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - def test_tell(self): + def test_tell(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.tell() == 0 - def test_seek(self): + def test_seek(self) -> None: with Image.open(TEST_PNG_FILE) as im: im.seek(0) @@ -754,7 +755,7 @@ def test_seek(self): im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer): + def test_save_stdout(self, buffer) -> None: old_stdout = sys.stdout if buffer: @@ -786,7 +787,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - def test_leak_load(self): + def test_leak_load(self) -> None: with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) @@ -794,7 +795,7 @@ def test_leak_load(self): with Image.open(DATA) as im: im.load() - def core(): + def core() -> None: with Image.open(DATA) as im: im.load() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 32de42ed45d..94f66ee7d28 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -2,6 +2,7 @@ import sys from io import BytesIO +from pathlib import Path import pytest @@ -18,7 +19,7 @@ TEST_FILE = "Tests/images/hopper.ppm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -69,7 +70,7 @@ def test_sanity(): ), ), ) -def test_arbitrary_maxval(data, mode, pixels): +def test_arbitrary_maxval(data, mode, pixels) -> None: fp = BytesIO(data) with Image.open(fp) as im: assert im.size == (3, 1) @@ -79,7 +80,7 @@ def test_arbitrary_maxval(data, mode, pixels): assert tuple(px[x, 0] for x in range(3)) == pixels -def test_16bit_pgm(): +def test_16bit_pgm() -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: assert im.mode == "I" assert im.size == (20, 100) @@ -88,7 +89,7 @@ def test_16bit_pgm(): assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png") -def test_16bit_pgm_write(tmp_path): +def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: filename = str(tmp_path / "temp.pgm") im.save(filename, "PPM") @@ -96,7 +97,7 @@ def test_16bit_pgm_write(tmp_path): assert_image_equal_tofile(im, filename) -def test_pnm(tmp_path): +def test_pnm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) @@ -106,7 +107,7 @@ def test_pnm(tmp_path): assert_image_equal_tofile(im, filename) -def test_pfm(tmp_path): +def test_pfm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pfm") as im: assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) @@ -117,7 +118,7 @@ def test_pfm(tmp_path): assert_image_equal_tofile(im, filename) -def test_pfm_big_endian(tmp_path): +def test_pfm_big_endian(tmp_path: Path) -> None: with Image.open("Tests/images/hopper_be.pfm") as im: assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) @@ -138,7 +139,7 @@ def test_pfm_big_endian(tmp_path): b"Pf 1 1 -0.0 \0\0\0\0", ], ) -def test_pfm_invalid(data): +def test_pfm_invalid(data) -> None: with pytest.raises(ValueError): with Image.open(BytesIO(data)): pass @@ -161,12 +162,12 @@ def test_pfm_invalid(data): ), ), ) -def test_plain(plain_path, raw_path): +def test_plain(plain_path, raw_path) -> None: with Image.open(plain_path) as im: assert_image_equal_tofile(im, raw_path) -def test_16bit_plain_pgm(): +def test_16bit_plain_pgm() -> None: # P2 with maxval 2 ** 16 - 1 with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: assert im.mode == "I" @@ -185,7 +186,7 @@ def test_16bit_plain_pgm(): (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), ), ) -def test_plain_data_with_comment(tmp_path, header, data, comment_count): +def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None: path1 = str(tmp_path / "temp1.ppm") path2 = str(tmp_path / "temp2.ppm") comment = b"# comment" * comment_count @@ -198,7 +199,7 @@ def test_plain_data_with_comment(tmp_path, header, data, comment_count): @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path, data): +def test_plain_truncated_data(tmp_path: Path, data) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -209,7 +210,7 @@ def test_plain_truncated_data(tmp_path, data): @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path, data): +def test_plain_invalid_data(tmp_path: Path, data) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -226,7 +227,7 @@ def test_plain_invalid_data(tmp_path, data): b"P3\n128 128\n255\n012345678910 0", # token too long ), ) -def test_plain_ppm_token_too_long(tmp_path, data): +def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -236,7 +237,7 @@ def test_plain_ppm_token_too_long(tmp_path, data): im.load() -def test_plain_ppm_value_too_large(tmp_path): +def test_plain_ppm_value_too_large(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n256") @@ -246,12 +247,12 @@ def test_plain_ppm_value_too_large(tmp_path): im.load() -def test_magic(): +def test_magic() -> None: with pytest.raises(SyntaxError): PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) -def test_header_with_comments(tmp_path): +def test_header_with_comments(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") @@ -260,7 +261,7 @@ def test_header_with_comments(tmp_path): assert im.size == (128, 128) -def test_non_integer_token(tmp_path): +def test_non_integer_token(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\nTEST") @@ -270,7 +271,7 @@ def test_non_integer_token(tmp_path): pass -def test_header_token_too_long(tmp_path): +def test_header_token_too_long(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n 01234567890") @@ -282,7 +283,7 @@ def test_header_token_too_long(tmp_path): assert str(e.value) == "Token too long in file header: 01234567890" -def test_truncated_file(tmp_path): +def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -301,7 +302,7 @@ def test_truncated_file(tmp_path): im.load() -def test_not_enough_image_data(tmp_path): +def test_not_enough_image_data(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P2 1 2 255 255") @@ -312,7 +313,7 @@ def test_not_enough_image_data(tmp_path): @pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval, tmp_path): +def test_invalid_maxval(maxval, tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -324,7 +325,7 @@ def test_invalid_maxval(maxval, tmp_path): assert str(e.value) == "maxval must be greater than 0 and less than 65536" -def test_neg_ppm(): +def test_neg_ppm() -> None: # Storage.c accepted negative values for xsize, ysize. the # internal open_ppm function didn't check for sanity but it # has been removed. The default opener doesn't accept negative @@ -335,7 +336,7 @@ def test_neg_ppm(): pass -def test_mimetypes(tmp_path): +def test_mimetypes(tmp_path: Path) -> None: path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -350,7 +351,7 @@ def test_mimetypes(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer): +def test_save_stdout(buffer) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 16f049602bc..7eca8d9b151 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -11,7 +11,7 @@ test_file = "Tests/images/hopper.psd" -def test_sanity(): +def test_sanity() -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -24,8 +24,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_file) im.load() @@ -33,27 +33,27 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_file) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_file) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PsdImagePlugin.PsdImageFile(invalid_file) -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert im.n_frames == 1 assert not im.is_animated @@ -64,7 +64,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(test_file) as im: # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -78,7 +78,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(test_file) as im: layer_number = im.tell() assert layer_number == 1 @@ -95,30 +95,30 @@ def test_seek_tell(): assert layer_number == 2 -def test_seek_eoferror(): +def test_seek_eoferror() -> None: with Image.open(test_file) as im: with pytest.raises(EOFError): im.seek(-1) -def test_open_after_exclusive_load(): +def test_open_after_exclusive_load() -> None: with Image.open(test_file) as im: im.load() im.seek(im.tell() + 1) im.load() -def test_rgba(): +def test_rgba() -> None: with Image.open("Tests/images/rgba.psd") as im: assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") -def test_layer_skip(): +def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: assert im.n_frames == 1 -def test_icc_profile(): +def test_icc_profile() -> None: with Image.open(test_file) as im: assert "icc_profile" in im.info @@ -126,12 +126,12 @@ def test_icc_profile(): assert len(icc_profile) == 3144 -def test_no_icc_profile(): +def test_no_icc_profile() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert "icc_profile" not in im.info -def test_combined_larger_than_size(): +def test_combined_larger_than_size() -> None: # The combined size of the individual parts is larger than the # declared 'size' of the extra data field, resulting in a backwards seek. @@ -157,7 +157,7 @@ def test_combined_larger_than_size(): ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises): +def test_crashes(test_file, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index bc45bbfd34d..92aea07350e 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, SgiImagePlugin @@ -12,7 +14,7 @@ ) -def test_rgb(): +def test_rgb() -> None: # Created with ImageMagick then renamed: # convert hopper.ppm -compress None sgi:hopper.rgb test_file = "Tests/images/hopper.rgb" @@ -22,11 +24,11 @@ def test_rgb(): assert im.get_format_mimetype() == "image/rgb" -def test_rgb16(): +def test_rgb16() -> None: assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") -def test_l(): +def test_l() -> None: # Created with ImageMagick # convert hopper.ppm -monochrome -compress None sgi:hopper.bw test_file = "Tests/images/hopper.bw" @@ -36,7 +38,7 @@ def test_l(): assert im.get_format_mimetype() == "image/sgi" -def test_rgba(): +def test_rgba() -> None: # Created with ImageMagick: # convert transparent.png -compress None transparent.sgi test_file = "Tests/images/transparent.sgi" @@ -46,7 +48,7 @@ def test_rgba(): assert im.get_format_mimetype() == "image/sgi" -def test_rle(): +def test_rle() -> None: # Created with ImageMagick: # convert hopper.ppm hopper.sgi test_file = "Tests/images/hopper.sgi" @@ -55,22 +57,22 @@ def test_rle(): assert_image_equal_tofile(im, "Tests/images/hopper.rgb") -def test_rle16(): +def test_rle16() -> None: test_file = "Tests/images/tv16.sgi" with Image.open(test_file) as im: assert_image_equal_tofile(im, "Tests/images/tv.rgb") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(ValueError): SgiImagePlugin.SgiImageFile(invalid_file) -def test_write(tmp_path): - def roundtrip(img): +def test_write(tmp_path: Path) -> None: + def roundtrip(img) -> None: out = str(tmp_path / "temp.sgi") img.save(out, format="sgi") assert_image_equal_tofile(img, out) @@ -89,7 +91,7 @@ def roundtrip(img): roundtrip(Image.new("L", (10, 1))) -def test_write16(tmp_path): +def test_write16(tmp_path: Path) -> None: test_file = "Tests/images/hopper16.rgb" with Image.open(test_file) as im: @@ -99,7 +101,7 @@ def test_write16(tmp_path): assert_image_equal_tofile(im, out) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("LA") out = str(tmp_path / "temp.sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 42d833fb25c..75fef1dc6b6 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -3,6 +3,7 @@ import tempfile import warnings from io import BytesIO +from pathlib import Path import pytest @@ -13,7 +14,7 @@ TEST_FILE = "Tests/images/hopper.spider" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "F" @@ -22,8 +23,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -31,20 +32,20 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange temp = str(tmp_path / "temp.spider") im = hopper() @@ -59,7 +60,7 @@ def test_save(tmp_path): assert im2.format == "SPIDER" -def test_tempfile(): +def test_tempfile() -> None: # Arrange im = hopper() @@ -75,11 +76,11 @@ def test_tempfile(): assert reloaded.format == "SPIDER" -def test_is_spider_image(): +def test_is_spider_image() -> None: assert SpiderImagePlugin.isSpiderImage(TEST_FILE) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -89,13 +90,13 @@ def test_tell(): assert index == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_load_image_series(): +def test_load_image_series() -> None: # Arrange not_spider_file = "Tests/images/hopper.ppm" file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] @@ -109,7 +110,7 @@ def test_load_image_series(): assert img_list[0].size == (128, 128) -def test_load_image_series_no_input(): +def test_load_image_series_no_input() -> None: # Arrange file_list = None @@ -120,7 +121,7 @@ def test_load_image_series_no_input(): assert img_list is None -def test_is_int_not_a_number(): +def test_is_int_not_a_number() -> None: # Arrange not_a_number = "a" @@ -131,7 +132,7 @@ def test_is_int_not_a_number(): assert ret == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/invalid.spider" with pytest.raises(OSError): @@ -139,20 +140,20 @@ def test_invalid_file(): pass -def test_nonstack_file(): +def test_nonstack_file() -> None: with Image.open(TEST_FILE) as im: with pytest.raises(EOFError): im.seek(0) -def test_nonstack_dos(): +def test_nonstack_dos() -> None: with Image.open(TEST_FILE) as im: for i, frame in enumerate(ImageSequence.Iterator(im)): assert i <= 1, "Non-stack DOS file test failed" # for issue #4093 -def test_odd_size(): +def test_odd_size() -> None: data = BytesIO() width = 100 im = Image.new("F", (width, 64)) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 41f3b7d98f3..6cfff87309c 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -11,7 +11,7 @@ EXTRA_DIR = "Tests/images/sunraster" -def test_sanity(): +def test_sanity() -> None: # Arrange # Created with ImageMagick: convert hopper.jpg hopper.ras test_file = "Tests/images/hopper.ras" @@ -28,7 +28,7 @@ def test_sanity(): SunImagePlugin.SunImageFile(invalid_file) -def test_im1(): +def test_im1() -> None: with Image.open("Tests/images/sunraster.im1") as im: assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") @@ -36,7 +36,7 @@ def test_im1(): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_others(): +def test_others() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 58226c33062..44e78e972dc 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -19,7 +19,7 @@ ("jpg", "hopper.jpg", "JPEG"), ), ) -def test_sanity(codec, test_path, format): +def test_sanity(codec, test_path, format) -> None: if features.check(codec): with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: with Image.open(tar) as im: @@ -30,18 +30,18 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): +def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") -def test_close(): +def test_close() -> None: with warnings.catch_warnings(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() -def test_contextmanager(): +def test_contextmanager() -> None: with warnings.catch_warnings(): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index eafb61d30af..bd8e522c7b5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -3,6 +3,7 @@ import os from glob import glob from itertools import product +from pathlib import Path import pytest @@ -21,8 +22,8 @@ @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path): - def roundtrip(original_im): +def test_sanity(mode, tmp_path: Path) -> None: + def roundtrip(original_im) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) @@ -64,7 +65,7 @@ def roundtrip(original_im): roundtrip(original_im) -def test_palette_depth_16(tmp_path): +def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") @@ -74,7 +75,7 @@ def test_palette_depth_16(tmp_path): assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") -def test_id_field(): +def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -84,7 +85,7 @@ def test_id_field(): assert im.size == (100, 100) -def test_id_field_rle(): +def test_id_field_rle() -> None: # tga file with id field test_file = "Tests/images/rgb32rle.tga" @@ -94,7 +95,7 @@ def test_id_field_rle(): assert im.size == (199, 199) -def test_cross_scan_line(): +def test_cross_scan_line() -> None: with Image.open("Tests/images/cross_scan_line.tga") as im: assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") @@ -103,7 +104,7 @@ def test_cross_scan_line(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -120,7 +121,7 @@ def test_save(tmp_path): assert test_im.size == (100, 100) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0] im.putpalette(colors) @@ -132,7 +133,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") out = str(tmp_path / "temp.tga") @@ -140,7 +141,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_save_mapdepth(): +def test_save_mapdepth() -> None: # This image has been manually hexedited from 200x32_p_bl_raw.tga # to include an origin test_file = "Tests/images/200x32_p_bl_raw_origin.tga" @@ -148,7 +149,7 @@ def test_save_mapdepth(): assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") -def test_save_id_section(tmp_path): +def test_save_id_section(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -179,7 +180,7 @@ def test_save_id_section(tmp_path): assert "id_section" not in test_im.info -def test_save_orientation(tmp_path): +def test_save_orientation(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" out = str(tmp_path / "temp.tga") with Image.open(test_file) as im: @@ -190,7 +191,7 @@ def test_save_orientation(tmp_path): assert test_im.info["orientation"] == 1 -def test_horizontal_orientations(): +def test_horizontal_orientations() -> None: # These images have been manually hexedited to have the relevant orientations with Image.open("Tests/images/rgb32rle_top_right.tga") as im: assert im.load()[90, 90][:3] == (0, 0, 0) @@ -199,7 +200,7 @@ def test_horizontal_orientations(): assert im.load()[90, 90][:3] == (0, 255, 0) -def test_save_rle(tmp_path): +def test_save_rle(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: assert im.info["compression"] == "tga_rle" @@ -232,7 +233,7 @@ def test_save_rle(tmp_path): assert test_im.info["compression"] == "tga_rle" -def test_save_l_transparency(tmp_path): +def test_save_l_transparency(tmp_path: Path) -> None: # There are 559 transparent pixels in la.tga. num_transparent = 559 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index f0995679b1f..a16b76e19f7 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -3,6 +3,7 @@ import os import warnings from io import BytesIO +from pathlib import Path import pytest @@ -26,7 +27,7 @@ class TestFileTiff: - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -58,21 +59,21 @@ def test_sanity(self, tmp_path): pass @pytest.mark.skipif(is_pypy(), reason="Requires CPython") - def test_unclosed_file(self): - def open(): + def test_unclosed_file(self) -> None: + def open() -> None: im = Image.open("Tests/images/multipage.tiff") im.load() with pytest.warns(ResourceWarning): open() - def test_closed_file(self): + def test_closed_file(self) -> None: with warnings.catch_warnings(): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() - def test_seek_after_close(self): + def test_seek_after_close(self) -> None: im = Image.open("Tests/images/multipage.tiff") im.close() @@ -81,12 +82,12 @@ def test_seek_after_close(self): with pytest.raises(ValueError): im.seek(1) - def test_context_manager(self): + def test_context_manager(self) -> None: with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - def test_mac_tiff(self): + def test_mac_tiff(self) -> None: # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" @@ -98,7 +99,7 @@ def test_mac_tiff(self): assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self, tmp_path): + def test_bigtiff(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") @@ -109,13 +110,13 @@ def test_bigtiff(self, tmp_path): outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) - def test_set_legacy_api(self): + def test_set_legacy_api(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: ifd.legacy_api = None assert str(e.value) == "Not allowing setting of legacy api" - def test_xyres_tiff(self): + def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # legacy api @@ -128,7 +129,7 @@ def test_xyres_tiff(self): assert im.info["dpi"] == (72.0, 72.0) - def test_xyres_fallback_tiff(self): + def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: # v2 api @@ -142,7 +143,7 @@ def test_xyres_fallback_tiff(self): # Fallback "inch". assert im.info["dpi"] == (100.0, 100.0) - def test_int_resolution(self): + def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # Try to read a file where X,Y_RESOLUTION are ints @@ -155,14 +156,14 @@ def test_int_resolution(self): "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi): + def test_load_float_dpi(self, resolution_unit, dpi) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) - def test_save_float_dpi(self, tmp_path): + def test_save_float_dpi(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: dpi = (72.2, 72.2) @@ -171,7 +172,7 @@ def test_save_float_dpi(self, tmp_path): with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == dpi - def test_save_setting_missing_resolution(self): + def test_save_setting_missing_resolution(self) -> None: b = BytesIO() with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) @@ -179,7 +180,7 @@ def test_save_setting_missing_resolution(self): assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -190,30 +191,30 @@ def test_invalid_file(self): TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.pop() - def test_bad_exif(self): + def test_bad_exif(self) -> None: with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. with pytest.warns(UserWarning): i._getexif() - def test_save_rgba(self, tmp_path): + def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_save_unsupported_mode(self, tmp_path): + def test_save_unsupported_mode(self, tmp_path: Path) -> None: im = hopper("HSV") outfile = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(outfile) - def test_8bit_s(self): + def test_8bit_s(self) -> None: with Image.open("Tests/images/8bit.s.tif") as im: im.load() assert im.mode == "L" assert im.getpixel((50, 50)) == 184 - def test_little_endian(self): + def test_little_endian(self) -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -223,7 +224,7 @@ def test_little_endian(self): assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_big_endian(self): + def test_big_endian(self) -> None: with Image.open("Tests/images/16bit.MM.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -233,7 +234,7 @@ def test_big_endian(self): assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - def test_16bit_r(self): + def test_16bit_r(self) -> None: with Image.open("Tests/images/16bit.r.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -242,14 +243,14 @@ def test_16bit_r(self): assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_16bit_s(self): + def test_16bit_s(self) -> None: with Image.open("Tests/images/16bit.s.tif") as im: im.load() assert im.mode == "I" assert im.getpixel((0, 0)) == 32767 assert im.getpixel((0, 1)) == 0 - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" @@ -262,7 +263,7 @@ def test_12bit_rawmode(self): assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_32bit_float(self): + def test_32bit_float(self) -> None: # Issue 614, specific 32-bit float format path = "Tests/images/10ct_32bit_128.tiff" with Image.open(path) as im: @@ -271,7 +272,7 @@ def test_32bit_float(self): assert im.getpixel((0, 0)) == -0.4526388943195343 assert im.getextrema() == (-3.140936851501465, 3.140684127807617) - def test_unknown_pixel_mode(self): + def test_unknown_pixel_mode(self) -> None: with pytest.raises(OSError): with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): pass @@ -283,12 +284,12 @@ def test_unknown_pixel_mode(self): ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames): + def test_n_frames(self, path, n_frames) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) - def test_eoferror(self): + def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: n_frames = im.n_frames @@ -300,7 +301,7 @@ def test_eoferror(self): # Test that seeking to the last frame does not raise an error im.seek(n_frames - 1) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue @@ -324,13 +325,13 @@ def test_multipage(self): assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_multipage_last_frame(self): + def test_multipage_last_frame(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: im.load() assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_frame_order(self): + def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: assert im.n_frames == 1 @@ -343,7 +344,7 @@ def test_frame_order(self): with Image.open("Tests/images/multipage_out_of_order.tiff") as im: assert im.n_frames == 3 - def test___str__(self): + def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: # Act @@ -352,7 +353,7 @@ def test___str__(self): # Assert assert isinstance(ret, str) - def test_dict(self): + def test_dict(self) -> None: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -392,7 +393,7 @@ def test_dict(self): } assert dict(im.tag) == legacy_tags - def test__delitem__(self): + def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: len_before = len(dict(im.ifd)) @@ -401,36 +402,36 @@ def test__delitem__(self): assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api): + def test_load_byte(self, legacy_api) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) assert ret == b"abc" - def test_load_string(self): + def test_load_string(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data, False) assert ret == "abc" - def test_load_float(self): + def test_load_float(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data, False) assert ret == (1.6777999408082104e22, 1.6777999408082104e22) - def test_load_double(self): + def test_load_double(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data, False) assert ret == (8.540883223036124e194, 8.540883223036124e194) - def test_ifd_tag_type(self): + def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 - def test_exif(self, tmp_path): - def check_exif(exif): + def test_exif(self, tmp_path: Path) -> None: + def check_exif(exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -481,7 +482,7 @@ def check_exif(exif): exif = im.getexif() check_exif(exif) - def test_modify_exif(self, tmp_path): + def test_modify_exif(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() @@ -493,7 +494,7 @@ def test_modify_exif(self, tmp_path): exif = im.getexif() assert exif[264] == 100 - def test_reload_exif_after_seek(self): + def test_reload_exif_after_seek(self) -> None: with Image.open("Tests/images/multipage.tiff") as im: exif = im.getexif() del exif[256] @@ -501,7 +502,7 @@ def test_reload_exif_after_seek(self): assert 256 in exif - def test_exif_frames(self): + def test_exif_frames(self) -> None: # Test that EXIF data can change across frames with Image.open("Tests/images/g4-multi.tiff") as im: assert im.getexif()[273] == (328, 815) @@ -510,7 +511,7 @@ def test_exif_frames(self): assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path): + def test_photometric(self, mode, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -518,13 +519,13 @@ def test_photometric(self, mode, tmp_path): assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) - def test_seek(self): + def test_seek(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: im.seek(0) assert im.tell() == 0 - def test_seek_eof(self): + def test_seek_eof(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: assert im.tell() == 0 @@ -533,21 +534,21 @@ def test_seek_eof(self): with pytest.raises(EOFError): im.seek(1) - def test__limit_rational_int(self): + def test__limit_rational_int(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 34 ret = _limit_rational(value, 65536) assert ret == (34, 1) - def test__limit_rational_float(self): + def test__limit_rational_float(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 22.3 ret = _limit_rational(value, 65536) assert ret == (223, 10) - def test_4bit(self): + def test_4bit(self) -> None: test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") with Image.open(test_file) as im: @@ -555,7 +556,7 @@ def test_4bit(self): assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -588,7 +589,7 @@ def test_gray_semibyte_per_pixel(self): assert im2.mode == "L" assert_image_equal(im, im2) - def test_with_underscores(self, tmp_path): + def test_with_underscores(self, tmp_path: Path) -> None: kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) @@ -601,7 +602,7 @@ def test_with_underscores(self, tmp_path): assert im.tag_v2[X_RESOLUTION] == 72 assert im.tag_v2[Y_RESOLUTION] == 36 - def test_roundtrip_tiff_uint16(self, tmp_path): + def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None: # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" @@ -613,7 +614,7 @@ def test_roundtrip_tiff_uint16(self, tmp_path): assert_image_equal_tofile(im, tmpfile) - def test_rowsperstrip(self, tmp_path): + def test_rowsperstrip(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper() im.save(outfile, tiffinfo={278: 256}) @@ -621,25 +622,25 @@ def test_rowsperstrip(self, tmp_path): with Image.open(outfile) as im: assert im.tag_v2[278] == 256 - def test_strip_raw(self): + def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw(self): + def test_strip_planar_raw(self) -> None: # gdal_translate -of GTiff -co INTERLEAVE=BAND \ # tiff_strip_raw.tif tiff_strip_planar_raw.tiff infile = "Tests/images/tiff_strip_planar_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw_with_overviews(self): + def test_strip_planar_raw_with_overviews(self) -> None: # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_raw(self): + def test_tiled_planar_raw(self) -> None: # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff @@ -647,7 +648,7 @@ def test_tiled_planar_raw(self): with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_planar_configuration_save(self, tmp_path): + def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: assert im._planar_configuration == 2 @@ -659,7 +660,7 @@ def test_planar_configuration_save(self, tmp_path): assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path): + def test_palette(self, mode, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -668,7 +669,7 @@ def test_palette(self, mode, tmp_path): with Image.open(outfile) as reloaded: assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - def test_tiff_save_all(self): + def test_tiff_save_all(self) -> None: mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) @@ -698,7 +699,7 @@ def im_generator(ims): with Image.open(mp) as reread: assert reread.n_frames == 3 - def test_saving_icc_profile(self, tmp_path): + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs # as libtiff does not support embedded ICC profiles, @@ -712,7 +713,7 @@ def test_saving_icc_profile(self, tmp_path): with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] - def test_save_icc_profile(self, tmp_path): + def test_save_icc_profile(self, tmp_path: Path) -> None: im = hopper() assert "icc_profile" not in im.info @@ -723,14 +724,14 @@ def test_save_icc_profile(self, tmp_path): with Image.open(outfile) as reloaded: assert reloaded.info["icc_profile"] == icc_profile - def test_save_bmp_compression(self, tmp_path): + def test_save_bmp_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.bmp") as im: assert im.info["compression"] == 0 outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_discard_icc_profile(self, tmp_path): + def test_discard_icc_profile(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/icc_profile.png") as im: @@ -741,7 +742,7 @@ def test_discard_icc_profile(self, tmp_path): with Image.open(outfile) as reloaded: assert "icc_profile" not in reloaded.info - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/lab.tif") as im: if ElementTree is None: with pytest.warns( @@ -756,7 +757,7 @@ def test_getxmp(self): assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] - def test_get_photoshop_blocks(self): + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert list(im.get_photoshop_blocks().keys()) == [ 1061, @@ -782,7 +783,7 @@ def test_get_photoshop_blocks(self): 4001, ] - def test_tiff_chunks(self, tmp_path): + def test_tiff_chunks(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") im = hopper() @@ -803,7 +804,7 @@ def test_tiff_chunks(self, tmp_path): assert_image_equal_tofile(im, tmpfile) - def test_close_on_load_exclusive(self, tmp_path): + def test_close_on_load_exclusive(self, tmp_path: Path) -> None: # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") @@ -816,7 +817,7 @@ def test_close_on_load_exclusive(self, tmp_path): im.load() assert fp.closed - def test_close_on_load_nonexclusive(self, tmp_path): + def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: @@ -838,7 +839,7 @@ def test_close_on_load_nonexclusive(self, tmp_path): not os.path.exists("Tests/images/string_dimension.tiff"), reason="Extra image files not installed", ) - def test_string_dimension(self): + def test_string_dimension(self) -> None: # Assert that an error is raised if one of the dimensions is a string with Image.open("Tests/images/string_dimension.tiff") as im: with pytest.raises(OSError): @@ -846,7 +847,7 @@ def test_string_dimension(self): @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self): + def test_timeout(self) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True im.load() @@ -859,7 +860,7 @@ def test_timeout(self): ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file): + def test_oom(self, test_file) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): @@ -868,7 +869,7 @@ def test_oom(self, test_file): @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") # this is an mmaped file. diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 06689bc9092..bb6225d075b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -2,6 +2,7 @@ import io import struct +from pathlib import Path import pytest @@ -13,7 +14,7 @@ TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} -def test_rt_metadata(tmp_path): +def test_rt_metadata(tmp_path: Path) -> None: """Test writing arbitrary metadata into the tiff image directory Use case is ImageJ private tags, one numeric, one arbitrary data. https://github.com/python-pillow/Pillow/issues/291 @@ -79,7 +80,7 @@ def test_rt_metadata(tmp_path): assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) -def test_read_metadata(): +def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: assert { "YResolution": IFDRational(4294967295, 113653537), @@ -120,7 +121,7 @@ def test_read_metadata(): } == img.tag.named() -def test_write_metadata(tmp_path): +def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: f = str(tmp_path / "temp.tiff") @@ -157,7 +158,7 @@ def test_write_metadata(tmp_path): assert value == reloaded[tag], f"{tag} didn't roundtrip" -def test_change_stripbytecounts_tag_type(tmp_path): +def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 @@ -176,19 +177,19 @@ def test_change_stripbytecounts_tag_type(tmp_path): assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG -def test_no_duplicate_50741_tag(): +def test_no_duplicate_50741_tag() -> None: assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 -def test_iptc(tmp_path): +def test_iptc(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.Lab.tif") as im: im.save(out) @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path): +def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -205,7 +206,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path): @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path): +def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -221,7 +222,7 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" -def test_writing_other_types_to_undefined(tmp_path): +def test_writing_other_types_to_undefined(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -237,7 +238,7 @@ def test_writing_other_types_to_undefined(tmp_path): assert reloaded.tag_v2[33723] == b"1" -def test_undefined_zero(tmp_path): +def test_undefined_zero(tmp_path: Path) -> None: # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] assert tag.type == TiffTags.UNDEFINED @@ -252,7 +253,7 @@ def test_undefined_zero(tmp_path): assert info[45059] == original -def test_empty_metadata(): +def test_empty_metadata() -> None: f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) @@ -261,7 +262,7 @@ def test_empty_metadata(): info.load(f) -def test_iccprofile(tmp_path): +def test_iccprofile(tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1462 out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.iccprofile.tif") as im: @@ -272,7 +273,7 @@ def test_iccprofile(tmp_path): assert im.info["icc_profile"] == reloaded.info["icc_profile"] -def test_iccprofile_binary(): +def test_iccprofile_binary() -> None: # https://github.com/python-pillow/Pillow/issues/1526 # We should be able to load this, # but probably won't be able to save it. @@ -282,19 +283,19 @@ def test_iccprofile_binary(): assert im.info["icc_profile"] -def test_iccprofile_save_png(tmp_path): +def test_iccprofile_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_iccprofile_binary_save_png(tmp_path): +def test_iccprofile_binary_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_exif_div_zero(tmp_path): +def test_exif_div_zero(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) @@ -307,7 +308,7 @@ def test_exif_div_zero(tmp_path): assert 0 == reloaded.tag_v2[41988].denominator -def test_ifd_unsigned_rational(tmp_path): +def test_ifd_unsigned_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -338,7 +339,7 @@ def test_ifd_unsigned_rational(tmp_path): assert 1 == reloaded.tag_v2[41493].denominator -def test_ifd_signed_rational(tmp_path): +def test_ifd_signed_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -381,7 +382,7 @@ def test_ifd_signed_rational(tmp_path): assert -1 == reloaded.tag_v2[37380].denominator -def test_ifd_signed_long(tmp_path): +def test_ifd_signed_long(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -394,7 +395,7 @@ def test_ifd_signed_long(tmp_path): assert reloaded.tag_v2[37000] == -60000 -def test_empty_values(): +def test_empty_values() -> None: data = io.BytesIO( b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -409,7 +410,7 @@ def test_empty_values(): assert 33432 in info -def test_photoshop_info(tmp_path): +def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) @@ -420,7 +421,7 @@ def test_photoshop_info(tmp_path): assert isinstance(reloaded.tag_v2[34377], bytes) -def test_too_many_entries(): +def test_too_many_entries() -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), @@ -432,7 +433,7 @@ def test_too_many_entries(): assert ifd[277] == 4 -def test_tag_group_data(): +def test_tag_group_data() -> None: base_ifd = TiffImagePlugin.ImageFileDirectory_v2() interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) for ifd in (base_ifd, interop_ifd): @@ -446,7 +447,7 @@ def test_tag_group_data(): assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] -def test_empty_subifd(tmp_path): +def test_empty_subifd(tmp_path: Path) -> None: out = str(tmp_path / "temp.jpg") im = hopper() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index c49418ce3c5..249846da481 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -4,6 +4,7 @@ import re import sys import warnings +from pathlib import Path import pytest @@ -26,7 +27,7 @@ class TestUnsupportedWebp: - def test_unsupported(self): + def test_unsupported(self) -> None: if HAVE_WEBP: WebPImagePlugin.SUPPORTED = False @@ -42,15 +43,15 @@ def test_unsupported(self): @skip_unless_feature("webp") class TestFileWebp: - def setup_method(self): + def setup_method(self) -> None: self.rgb_mode = "RGB" - def test_version(self): + def test_version(self) -> None: _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) - def test_read_rgb(self): + def test_read_rgb(self) -> None: """ Can we read a RGB mode WebP file without error? Does it have the bits we expect? @@ -67,7 +68,7 @@ def test_read_rgb(self): # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - def _roundtrip(self, tmp_path, mode, epsilon, args={}): + def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None: temp_file = str(tmp_path / "temp.webp") hopper(mode).save(temp_file, **args) @@ -93,7 +94,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}): target = target.convert(self.rgb_mode) assert_image_similar(image, target, epsilon) - def test_write_rgb(self, tmp_path): + def test_write_rgb(self, tmp_path: Path) -> None: """ Can we write a RGB mode file to webp without error? Does it have the bits we expect? @@ -101,7 +102,7 @@ def test_write_rgb(self, tmp_path): self._roundtrip(tmp_path, self.rgb_mode, 12.5) - def test_write_method(self, tmp_path): + def test_write_method(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) buffer_no_args = io.BytesIO() @@ -112,7 +113,7 @@ def test_write_method(self, tmp_path): assert buffer_no_args.getbuffer() != buffer_method.getbuffer() @skip_unless_feature("webp_anim") - def test_save_all(self, tmp_path): + def test_save_all(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") @@ -124,14 +125,14 @@ def test_save_all(self, tmp_path): reloaded.seek(1) assert_image_similar(im2, reloaded, 1) - def test_icc_profile(self, tmp_path): + def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) if _webp.HAVE_WEBPANIM: self._roundtrip( tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} ) - def test_write_unsupported_mode_L(self, tmp_path): + def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ Saving a black-and-white file to WebP format should work, and be similar to the original file. @@ -139,7 +140,7 @@ def test_write_unsupported_mode_L(self, tmp_path): self._roundtrip(tmp_path, "L", 10.0) - def test_write_unsupported_mode_P(self, tmp_path): + def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: """ Saving a palette-based file to WebP format should work, and be similar to the original file. @@ -148,14 +149,14 @@ def test_write_unsupported_mode_P(self, tmp_path): self._roundtrip(tmp_path, "P", 50.0) @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") - def test_write_encoding_error_message(self, tmp_path): + def test_write_encoding_error_message(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) with pytest.raises(ValueError) as e: im.save(temp_file, method=0) assert str(e.value) == "encoding error 6" - def test_WebPEncode_with_invalid_args(self): + def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. """ @@ -166,7 +167,7 @@ def test_WebPEncode_with_invalid_args(self): with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self): + def test_WebPDecode_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ @@ -177,14 +178,14 @@ def test_WebPDecode_with_invalid_args(self): with pytest.raises(TypeError): _webp.WebPDecode() - def test_no_resource_warning(self, tmp_path): + def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: temp_file = str(tmp_path / "temp.webp") with warnings.catch_warnings(): image.save(temp_file) - def test_file_pointer_could_be_reused(self): + def test_file_pointer_could_be_reused(self) -> None: file_path = "Tests/images/hopper.webp" with open(file_path, "rb") as blob: Image.open(blob).load() @@ -195,14 +196,14 @@ def test_file_pointer_could_be_reused(self): (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) @skip_unless_feature("webp_anim") - def test_invalid_background(self, background, tmp_path): + def test_invalid_background(self, background, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = hopper() with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) @skip_unless_feature("webp_anim") - def test_background_from_gif(self, tmp_path): + def test_background_from_gif(self, tmp_path: Path) -> None: # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: out_webp = str(tmp_path / "temp.webp") @@ -227,7 +228,7 @@ def test_background_from_gif(self, tmp_path): assert difference < 5 @skip_unless_feature("webp_anim") - def test_duration(self, tmp_path): + def test_duration(self, tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 @@ -238,7 +239,7 @@ def test_duration(self, tmp_path): reloaded.load() assert reloaded.info["duration"] == 1000 - def test_roundtrip_rgba_palette(self, tmp_path): + def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index cfda35a0962..a95434624f5 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -14,12 +16,12 @@ _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -def setup_module(): +def setup_module() -> None: if _webp.WebPDecoderBuggyAlpha(): pytest.skip("Buggy early version of WebP installed, not testing transparency") -def test_read_rgba(): +def test_read_rgba() -> None: """ Can we read an RGBA mode file without error? Does it have the bits we expect? @@ -39,7 +41,7 @@ def test_read_rgba(): assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0) -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: """ Can we write an RGBA mode file with lossless compression without error? Does it have the bits we expect? @@ -68,7 +70,7 @@ def test_write_lossless_rgb(tmp_path): assert_image_equal(image, pil_image) -def test_write_rgba(tmp_path): +def test_write_rgba(tmp_path: Path) -> None: """ Can we write a RGBA mode file to WebP without error. Does it have the bits we expect? @@ -99,7 +101,7 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) -def test_keep_rgb_values_when_transparent(tmp_path): +def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: """ Saving transparent pixels should retain their original RGB values when using the "exact" parameter. @@ -128,7 +130,7 @@ def test_keep_rgb_values_when_transparent(tmp_path): assert_image_equal(reloaded.convert("RGB"), image) -def test_write_unsupported_mode_PA(tmp_path): +def test_write_unsupported_mode_PA(tmp_path: Path) -> None: """ Saving a palette-based file with transparency to WebP format should work, and be similar to the original file. diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 426fe7a0293..9a730f1f9bd 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from packaging.version import parse as parse_version @@ -18,7 +20,7 @@ ] -def test_n_frames(): +def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: @@ -30,7 +32,7 @@ def test_n_frames(): assert im.is_animated -def test_write_animation_L(tmp_path): +def test_write_animation_L(tmp_path: Path) -> None: """ Convert an animated GIF to animated WebP, then compare the frame count, and first and last frames to ensure they're visually similar. @@ -60,13 +62,13 @@ def test_write_animation_L(tmp_path): assert_image_similar(im, orig.convert("RGBA"), 32.9) -def test_write_animation_RGB(tmp_path): +def test_write_animation_RGB(tmp_path: Path) -> None: """ Write an animated WebP from RGB frames, and ensure the frames are visually similar to the originals. """ - def check(temp_file): + def check(temp_file) -> None: with Image.open(temp_file) as im: assert im.n_frames == 2 @@ -105,7 +107,7 @@ def im_generator(ims): check(temp_file2) -def test_timestamp_and_duration(tmp_path): +def test_timestamp_and_duration(tmp_path: Path) -> None: """ Try passing a list of durations, and make sure the encoded timestamps and durations are correct. @@ -136,7 +138,7 @@ def test_timestamp_and_duration(tmp_path): ts += durations[frame] -def test_float_duration(tmp_path): +def test_float_duration(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") with Image.open("Tests/images/iss634.apng") as im: assert im.info["duration"] == 70.0 @@ -148,7 +150,7 @@ def test_float_duration(tmp_path): assert reloaded.info["duration"] == 70 -def test_seeking(tmp_path): +def test_seeking(tmp_path: Path) -> None: """ Create an animated WebP file, and then try seeking through frames in reverse-order, verifying the timestamps and durations are correct. @@ -179,7 +181,7 @@ def test_seeking(tmp_path): ts -= dur -def test_seek_errors(): +def test_seek_errors() -> None: with Image.open("Tests/images/iss634.webp") as im: with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index deaf5e380db..fea19694109 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -19,7 +20,7 @@ ElementTree = None -def test_read_exif_metadata(): +def test_read_exif_metadata() -> None: file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -37,7 +38,7 @@ def test_read_exif_metadata(): assert exif_data == expected_exif -def test_read_exif_metadata_without_prefix(): +def test_read_exif_metadata_without_prefix() -> None: with Image.open("Tests/images/flower2.webp") as im: # Assert prefix is not present assert im.info["exif"][:6] != b"Exif\x00\x00" @@ -49,7 +50,7 @@ def test_read_exif_metadata_without_prefix(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_exif_metadata(): +def test_write_exif_metadata() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -63,7 +64,7 @@ def test_write_exif_metadata(): assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" -def test_read_icc_profile(): +def test_read_icc_profile() -> None: file_path = "Tests/images/flower2.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -80,7 +81,7 @@ def test_read_icc_profile(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_icc_metadata(): +def test_write_icc_metadata() -> None: file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -100,7 +101,7 @@ def test_write_icc_metadata(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_read_no_exif(): +def test_read_no_exif() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -113,7 +114,7 @@ def test_read_no_exif(): assert not webp_image._getexif() -def test_getxmp(): +def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info assert im.getxmp() == {} @@ -133,7 +134,7 @@ def test_getxmp(): @skip_unless_feature("webp_anim") -def test_write_animated_metadata(tmp_path): +def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" xmp_data = b"" diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 6e1d4c1361f..b43e3f2965f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, WmfImagePlugin @@ -7,7 +9,7 @@ from .helper import assert_image_similar_tofile, hopper -def test_load_raw(): +def test_load_raw() -> None: # Test basic EMF open and rendering with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): @@ -25,17 +27,17 @@ def test_load_raw(): assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) -def test_load(): +def test_load() -> None: with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): assert im.load()[0, 0] == (255, 255, 255) -def test_register_handler(tmp_path): +def test_register_handler(tmp_path: Path) -> None: class TestHandler: methodCalled = False - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.methodCalled = True handler = TestHandler() @@ -51,12 +53,12 @@ def save(self, im, fp, filename): WmfImagePlugin.register_handler(original_handler) -def test_load_float_dpi(): +def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 -def test_load_set_dpi(): +def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: assert im.size == (82, 82) @@ -68,7 +70,7 @@ def test_load_set_dpi(): @pytest.mark.parametrize("ext", (".wmf", ".emf")) -def test_save(ext, tmp_path): +def test_save(ext, tmp_path: Path) -> None: im = hopper() tmpfile = str(tmp_path / ("temp" + ext)) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 69a0a1b38d8..44dd2541fb5 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -32,14 +33,14 @@ """ -def test_pil151(): +def test_pil151() -> None: with Image.open(BytesIO(PIL151)) as im: im.load() assert im.mode == "1" assert im.size == (32, 32) -def test_open(): +def test_open() -> None: # Arrange # Created with `convert hopper.png hopper.xbm` filename = "Tests/images/hopper.xbm" @@ -51,7 +52,7 @@ def test_open(): assert im.size == (128, 128) -def test_open_filename_with_underscore(): +def test_open_filename_with_underscore() -> None: # Arrange # Created with `convert hopper.png hopper_underscore.xbm` filename = "Tests/images/hopper_underscore.xbm" @@ -63,14 +64,14 @@ def test_open_filename_with_underscore(): assert im.size == (128, 128) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XbmImagePlugin.XbmImageFile(invalid_file) -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.xbm") @@ -78,7 +79,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_hotspot(tmp_path): +def test_hotspot(tmp_path: Path) -> None: im = hopper("1") out = str(tmp_path / "temp.xbm") diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 6395ae4aad8..73aaae6e757 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -21,7 +21,7 @@ def tuple_to_ints(tp): return int(x * 255.0), int(y * 255.0), int(z * 255.0) -def test_sanity(): +def test_sanity() -> None: Image.new("HSV", (100, 100)) @@ -78,7 +78,7 @@ def to_rgb_colorsys(im): return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") -def test_wedge(): +def test_wedge() -> None: src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) @@ -110,7 +110,7 @@ def test_wedge(): ) -def test_convert(): +def test_convert() -> None: im = hopper("RGB").convert("HSV") comparable = to_hsv_colorsys(hopper("RGB")) @@ -128,7 +128,7 @@ def test_convert(): ) -def test_hsv_to_rgb(): +def test_hsv_to_rgb() -> None: comparable = to_hsv_colorsys(hopper("RGB")) converted = comparable.convert("RGB") comparable = to_rgb_colorsys(comparable) diff --git a/Tests/test_image.py b/Tests/test_image.py index dd989ad99b5..67a7d7eca54 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,6 +7,7 @@ import sys import tempfile import warnings +from pathlib import Path import pytest @@ -60,19 +61,19 @@ class TestImage: "HSV", ), ) - def test_image_modes_success(self, mode): + def test_image_modes_success(self, mode) -> None: Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) - def test_image_modes_fail(self, mode): + def test_image_modes_fail(self, mode) -> None: with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) assert str(e.value) == "unrecognized image mode" - def test_exception_inheritance(self): + def test_exception_inheritance(self) -> None: assert issubclass(UnidentifiedImageError, OSError) - def test_sanity(self): + def test_sanity(self) -> None: im = Image.new("L", (100, 100)) assert repr(im)[:45] == " None: class Pretty: - def text(self, text): + def text(self, text) -> None: self.pretty_output = text im = Image.new("L", (100, 100)) @@ -108,7 +109,7 @@ def text(self, text): im._repr_pretty_(p, None) assert p.pretty_output == "" - def test_open_formats(self): + def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" JPGFILE = "Tests/images/hopper.jpg" @@ -130,7 +131,7 @@ def test_open_formats(self): assert im.mode == "RGB" assert im.size == (128, 128) - def test_width_height(self): + def test_width_height(self) -> None: im = Image.new("RGB", (1, 2)) assert im.width == 1 assert im.height == 2 @@ -138,29 +139,29 @@ def test_width_height(self): with pytest.raises(AttributeError): im.size = (3, 4) - def test_set_mode(self): + def test_set_mode(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(AttributeError): im.mode = "P" - def test_invalid_image(self): + def test_invalid_image(self) -> None: im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): with Image.open(im): pass - def test_bad_mode(self): + def test_bad_mode(self) -> None: with pytest.raises(ValueError): with Image.open("filename", "bad mode"): pass - def test_stringio(self): + def test_stringio(self) -> None: with pytest.raises(ValueError): with Image.open(io.StringIO()): pass - def test_pathlib(self, tmp_path): + def test_pathlib(self, tmp_path: Path) -> None: from PIL.Image import Path with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: @@ -179,11 +180,11 @@ def test_pathlib(self, tmp_path): os.remove(temp_file) im.save(Path(temp_file)) - def test_fp_name(self, tmp_path): + def test_fp_name(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") class FP: - def write(self, b): + def write(self, b) -> None: pass fp = FP() @@ -192,7 +193,7 @@ def write(self, b): im = hopper() im.save(fp) - def test_tempfile(self): + def test_tempfile(self) -> None: # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 im = hopper() @@ -201,13 +202,13 @@ def test_tempfile(self): fp.seek(0) assert_image_similar_tofile(im, fp, 20) - def test_unknown_extension(self, tmp_path): + def test_unknown_extension(self, tmp_path: Path) -> None: im = hopper() temp_file = str(tmp_path / "temp.unknown") with pytest.raises(ValueError): im.save(temp_file) - def test_internals(self): + def test_internals(self) -> None: im = Image.new("L", (100, 100)) im.readonly = 1 im._copy() @@ -222,7 +223,7 @@ def test_internals(self): sys.platform == "cygwin", reason="Test requires opening an mmaped file for writing", ) - def test_readonly_save(self, tmp_path): + def test_readonly_save(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) @@ -230,7 +231,7 @@ def test_readonly_save(self, tmp_path): assert im.readonly im.save(temp_file) - def test_dump(self, tmp_path): + def test_dump(self, tmp_path: Path) -> None: im = Image.new("L", (10, 10)) im._dump(str(tmp_path / "temp_L.ppm")) @@ -241,7 +242,7 @@ def test_dump(self, tmp_path): with pytest.raises(ValueError): im._dump(str(tmp_path / "temp_HSV.ppm")) - def test_comparison_with_other_type(self): + def test_comparison_with_other_type(self) -> None: # Arrange item = Image.new("RGB", (25, 25), "#000") num = 12 @@ -251,7 +252,7 @@ def test_comparison_with_other_type(self): assert item is not None assert item != num - def test_expand_x(self): + def test_expand_x(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -264,7 +265,7 @@ def test_expand_x(self): assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * xmargin - def test_expand_xy(self): + def test_expand_xy(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -278,12 +279,12 @@ def test_expand_xy(self): assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * ymargin - def test_getbands(self): + def test_getbands(self) -> None: # Assert assert hopper("RGB").getbands() == ("R", "G", "B") assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr") - def test_getchannel_wrong_params(self): + def test_getchannel_wrong_params(self) -> None: im = hopper() with pytest.raises(ValueError): @@ -295,7 +296,7 @@ def test_getchannel_wrong_params(self): with pytest.raises(ValueError): im.getchannel("1") - def test_getchannel(self): + def test_getchannel(self) -> None: im = hopper("YCbCr") Y, Cb, Cr = im.split() @@ -306,7 +307,7 @@ def test_getchannel(self): assert_image_equal(Cr, im.getchannel(2)) assert_image_equal(Cr, im.getchannel("Cr")) - def test_getbbox(self): + def test_getbbox(self) -> None: # Arrange im = hopper() @@ -316,7 +317,7 @@ def test_getbbox(self): # Assert assert bbox == (0, 0, 128, 128) - def test_ne(self): + def test_ne(self) -> None: # Arrange im1 = Image.new("RGB", (25, 25), "black") im2 = Image.new("RGB", (25, 25), "white") @@ -324,7 +325,7 @@ def test_ne(self): # Act / Assert assert im1 != im2 - def test_alpha_composite(self): + def test_alpha_composite(self) -> None: # https://stackoverflow.com/questions/3374878 # Arrange expected_colors = sorted( @@ -355,7 +356,7 @@ def test_alpha_composite(self): img_colors = sorted(img.getcolors()) assert img_colors == expected_colors - def test_alpha_inplace(self): + def test_alpha_inplace(self) -> None: src = Image.new("RGBA", (128, 128), "blue") over = Image.new("RGBA", (128, 128), "red") @@ -407,7 +408,7 @@ def test_alpha_inplace(self): with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) - def test_register_open_duplicates(self): + def test_register_open_duplicates(self) -> None: # Arrange factory, accept = Image.OPEN["JPEG"] id_length = len(Image.ID) @@ -418,7 +419,7 @@ def test_register_open_duplicates(self): # Assert assert len(Image.ID) == id_length - def test_registered_extensions_uninitialized(self): + def test_registered_extensions_uninitialized(self) -> None: # Arrange Image._initialized = 0 @@ -428,7 +429,7 @@ def test_registered_extensions_uninitialized(self): # Assert assert Image._initialized == 2 - def test_registered_extensions(self): + def test_registered_extensions(self) -> None: # Arrange # Open an image to trigger plugin registration with Image.open("Tests/images/rgb.jpg"): @@ -442,7 +443,7 @@ def test_registered_extensions(self): for ext in [".cur", ".icns", ".tif", ".tiff"]: assert ext in extensions - def test_effect_mandelbrot(self): + def test_effect_mandelbrot(self) -> None: # Arrange size = (512, 512) extent = (-3, -2.5, 2, 2.5) @@ -455,7 +456,7 @@ def test_effect_mandelbrot(self): assert im.size == (512, 512) assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png") - def test_effect_mandelbrot_bad_arguments(self): + def test_effect_mandelbrot_bad_arguments(self) -> None: # Arrange size = (512, 512) # Get coordinates the wrong way round: @@ -467,7 +468,7 @@ def test_effect_mandelbrot_bad_arguments(self): with pytest.raises(ValueError): Image.effect_mandelbrot(size, extent, quality) - def test_effect_noise(self): + def test_effect_noise(self) -> None: # Arrange size = (100, 100) sigma = 128 @@ -485,7 +486,7 @@ def test_effect_noise(self): p4 = im.getpixel((0, 4)) assert_not_all_same([p0, p1, p2, p3, p4]) - def test_effect_spread(self): + def test_effect_spread(self) -> None: # Arrange im = hopper() distance = 10 @@ -497,7 +498,7 @@ def test_effect_spread(self): assert im.size == (128, 128) assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110) - def test_effect_spread_zero(self): + def test_effect_spread_zero(self) -> None: # Arrange im = hopper() distance = 0 @@ -508,7 +509,7 @@ def test_effect_spread_zero(self): # Assert assert_image_equal(im, im2) - def test_check_size(self): + def test_check_size(self) -> None: # Checking that the _check_size function throws value errors when we want it to with pytest.raises(ValueError): Image.new("RGB", 0) # not a tuple @@ -537,10 +538,10 @@ def test_check_size(self): "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - def test_empty_image(self, size): + def test_empty_image(self, size) -> None: Image.new("RGB", size) - def test_storage_neg(self): + def test_storage_neg(self) -> None: # Storage.c accepted negative values for xsize, ysize. Was # test_neg_ppm, but the core function for that has been # removed Calling directly into core to test the error in @@ -549,13 +550,13 @@ def test_storage_neg(self): with pytest.raises(ValueError): Image.core.fill("RGB", (2, -2), (0, 0, 0)) - def test_one_item_tuple(self): + def test_one_item_tuple(self) -> None: for mode in ("I", "F", "L"): im = Image.new(mode, (100, 100), (5,)) px = im.load() assert px[0, 0] == 5 - def test_linear_gradient_wrong_mode(self): + def test_linear_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -564,7 +565,7 @@ def test_linear_gradient_wrong_mode(self): Image.linear_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_linear_gradient(self, mode): + def test_linear_gradient(self, mode) -> None: # Arrange target_file = "Tests/images/linear_gradient.png" @@ -580,7 +581,7 @@ def test_linear_gradient(self, mode): target = target.convert(mode) assert_image_equal(im, target) - def test_radial_gradient_wrong_mode(self): + def test_radial_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -589,7 +590,7 @@ def test_radial_gradient_wrong_mode(self): Image.radial_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_radial_gradient(self, mode): + def test_radial_gradient(self, mode) -> None: # Arrange target_file = "Tests/images/radial_gradient.png" @@ -605,7 +606,7 @@ def test_radial_gradient(self, mode): target = target.convert(mode) assert_image_equal(im, target) - def test_register_extensions(self): + def test_register_extensions(self) -> None: test_format = "a" exts = ["b", "c"] for ext in exts: @@ -621,7 +622,7 @@ def test_register_extensions(self): assert ext_individual == ext_multiple - def test_remap_palette(self): + def test_remap_palette(self) -> None: # Test identity transform with Image.open("Tests/images/hopper.gif") as im: assert_image_equal(im, im.remap_palette(list(range(256)))) @@ -640,7 +641,7 @@ def test_remap_palette(self): with pytest.raises(ValueError): im.remap_palette(None) - def test_remap_palette_transparency(self): + def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) im.putpixel((0, 1), (255, 0, 0)) im.info["transparency"] = 0 @@ -655,7 +656,7 @@ def test_remap_palette_transparency(self): im_remapped = im.remap_palette([1, 0]) assert "transparency" not in im_remapped.info - def test__new(self): + def test__new(self) -> None: im = hopper("RGB") im_p = hopper("P") @@ -664,7 +665,7 @@ def test__new(self): blank_p.palette = None blank_pa.palette = None - def _make_new(base_image, image, palette_result=None): + def _make_new(base_image, image, palette_result=None) -> None: new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size @@ -679,7 +680,7 @@ def _make_new(base_image, image, palette_result=None): _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) - def test_p_from_rgb_rgba(self): + def test_p_from_rgb_rgba(self) -> None: for mode, color in [ ("RGB", "#DDEEFF"), ("RGB", (221, 238, 255)), @@ -689,7 +690,7 @@ def test_p_from_rgb_rgba(self): expected = Image.new(mode, (100, 100), color) assert_image_equal(im.convert(mode), expected) - def test_no_resource_warning_on_save(self, tmp_path): + def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" @@ -700,7 +701,7 @@ def test_no_resource_warning_on_save(self, tmp_path): with warnings.catch_warnings(): im.save(temp_file) - def test_no_new_file_on_error(self, tmp_path): + def test_no_new_file_on_error(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") im = Image.new("RGB", (0, 0)) @@ -709,10 +710,10 @@ def test_no_new_file_on_error(self, tmp_path): assert not os.path.exists(temp_file) - def test_load_on_nonexclusive_multiframe(self): + def test_load_on_nonexclusive_multiframe(self) -> None: with open("Tests/images/frozenpond.mpo", "rb") as fp: - def act(fp): + def act(fp) -> None: im = Image.open(fp) im.load() @@ -723,7 +724,7 @@ def act(fp): assert not fp.closed - def test_empty_exif(self): + def test_empty_exif(self) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert dict(exif) @@ -739,7 +740,7 @@ def test_empty_exif(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_jpeg(self, tmp_path): + def test_exif_jpeg(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() assert 258 not in exif @@ -785,7 +786,7 @@ def test_exif_jpeg(self, tmp_path): @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_exif_webp(self, tmp_path): + def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() assert exif == {} @@ -795,7 +796,7 @@ def test_exif_webp(self, tmp_path): exif[40963] = 455 exif[305] = "Pillow test" - def check_exif(): + def check_exif() -> None: with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() assert reloaded_exif[258] == 8 @@ -807,7 +808,7 @@ def check_exif(): im.save(out, exif=exif, save_all=True) check_exif() - def test_exif_png(self, tmp_path): + def test_exif_png(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert exif == {274: 1} @@ -823,7 +824,7 @@ def test_exif_png(self, tmp_path): reloaded_exif = reloaded.getexif() assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} - def test_exif_interop(self): + def test_exif_interop(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(0xA005) == { @@ -837,7 +838,7 @@ def test_exif_interop(self): reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) - def test_exif_ifd1(self): + def test_exif_ifd1(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(ExifTags.IFD.IFD1) == { @@ -849,7 +850,7 @@ def test_exif_ifd1(self): 283: 180.0, } - def test_exif_ifd(self): + def test_exif_ifd(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() del exif.get_ifd(0x8769)[0xA005] @@ -858,7 +859,7 @@ def test_exif_ifd(self): reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) - def test_exif_load_from_fp(self): + def test_exif_load_from_fp(self) -> None: with Image.open("Tests/images/flower.jpg") as im: data = im.info["exif"] if data.startswith(b"Exif\x00\x00"): @@ -879,7 +880,7 @@ def test_exif_load_from_fp(self): 34665: 196, } - def test_exif_hide_offsets(self): + def test_exif_hide_offsets(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -905,18 +906,18 @@ def test_exif_hide_offsets(self): assert exif.get_ifd(0xA005) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_tobytes(self, size): + def test_zero_tobytes(self, size) -> None: im = Image.new("RGB", size) assert im.tobytes() == b"" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_frombytes(self, size): + def test_zero_frombytes(self, size) -> None: Image.frombytes("RGB", size, b"") im = Image.new("RGB", size) im.frombytes(b"") - def test_has_transparency_data(self): + def test_has_transparency_data(self) -> None: for mode in ("1", "L", "P", "RGB"): im = Image.new(mode, (1, 1)) assert not im.has_transparency_data @@ -941,7 +942,7 @@ def test_has_transparency_data(self): assert im.palette.mode == "RGBA" assert im.has_transparency_data - def test_apply_transparency(self): + def test_apply_transparency(self) -> None: im = Image.new("P", (1, 1)) im.putpalette((0, 0, 0, 1, 1, 1)) assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} @@ -970,7 +971,7 @@ def test_apply_transparency(self): im.apply_transparency() assert im.palette.colors[(27, 35, 6, 214)] == 24 - def test_constants(self): + def test_constants(self) -> None: for enum in ( Image.Transpose, Image.Transform, @@ -995,7 +996,7 @@ def test_constants(self): "01r_00.pcx", ], ) - def test_overrun(self, path): + def test_overrun(self, path) -> None: """For overrun completeness, test as: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ @@ -1009,7 +1010,7 @@ def test_overrun(self, path): assert buffer_overrun or truncated - def test_fli_overrun2(self): + def test_fli_overrun2(self) -> None: with Image.open("Tests/images/fli_overrun2.bin") as im: try: im.seek(1) @@ -1017,12 +1018,12 @@ def test_fli_overrun2(self): except OSError as e: assert str(e) == "buffer overrun when reading image file" - def test_exit_fp(self): + def test_exit_fp(self) -> None: with Image.new("L", (1, 1)) as im: pass assert not hasattr(im, "fp") - def test_close_graceful(self, caplog): + def test_close_graceful(self, caplog) -> None: with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() with caplog.at_level(logging.DEBUG): @@ -1043,7 +1044,7 @@ def mock_encode(*args): class TestRegistry: - def test_encode_registry(self): + def test_encode_registry(self) -> None: Image.register_encoder("MOCK", mock_encode) assert "MOCK" in Image.ENCODERS @@ -1052,6 +1053,6 @@ def test_encode_registry(self): assert isinstance(enc, MockEncoder) assert enc.args == ("RGB", "args", "extra") - def test_encode_registry_fail(self): + def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4ae56fae0cc..00cd4e7a9a1 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -35,16 +35,16 @@ class AccessTest: _need_cffi_access = False @classmethod - def setup_class(cls): + def setup_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._need_cffi_access @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._init_cffi_access class TestImagePutPixel(AccessTest): - def test_sanity(self): + def test_sanity(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -81,7 +81,7 @@ def test_sanity(self): assert_image_equal(im1, im2) - def test_sanity_negative_index(self): + def test_sanity_negative_index(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -119,7 +119,7 @@ def test_sanity_negative_index(self): assert_image_equal(im1, im2) @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy(self): + def test_numpy(self) -> None: im = hopper() pix = im.load() @@ -138,7 +138,7 @@ def color(mode): return (16, 32, 49) return tuple(range(1, bands + 1)) - def check(self, mode, expected_color=None): + def check(self, mode, expected_color=None) -> None: if self._need_cffi_access and mode.startswith("BGR;"): pytest.skip("Support not added to deprecated module for BGR;* modes") @@ -222,10 +222,10 @@ def check(self, mode, expected_color=None): "YCbCr", ), ) - def test_basic(self, mode): + def test_basic(self, mode) -> None: self.check(mode) - def test_list(self): + def test_list(self) -> None: im = hopper() assert im.getpixel([0, 0]) == (20, 20, 70) @@ -233,14 +233,14 @@ def test_list(self): @pytest.mark.parametrize( "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) ) - def test_signedness(self, mode, expected_color): + def test_signedness(self, mode, expected_color) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode, color): + def test_p_putpixel_rgb_rgba(self, mode, color) -> None: im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) @@ -264,7 +264,7 @@ class TestCffiGetPixel(TestImageGetPixel): class TestCffi(AccessTest): _need_cffi_access = True - def _test_get_access(self, im): + def _test_get_access(self, im) -> None: """Do we get the same thing as the old pixel access Using private interfaces, forcing a capi access and @@ -282,7 +282,7 @@ def _test_get_access(self, im): with pytest.raises(ValueError): access[(access.xsize + 1, access.ysize + 1)] - def test_get_vs_c(self): + def test_get_vs_c(self) -> None: with pytest.warns(DeprecationWarning): rgb = hopper("RGB") rgb.load() @@ -301,7 +301,7 @@ def test_get_vs_c(self): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_get_access(im) - def _test_set_access(self, im, color): + def _test_set_access(self, im, color) -> None: """Are we writing the correct bits into the image? Using private interfaces, forcing a capi access and @@ -322,7 +322,7 @@ def _test_set_access(self, im, color): with pytest.raises(ValueError): access[(0, 0)] = color - def test_set_vs_c(self): + def test_set_vs_c(self) -> None: rgb = hopper("RGB") with pytest.warns(DeprecationWarning): rgb.load() @@ -345,11 +345,11 @@ def test_set_vs_c(self): # self._test_set_access(im, 2**13-1) @pytest.mark.filterwarnings("ignore::DeprecationWarning") - def test_not_implemented(self): + def test_not_implemented(self) -> None: assert PyAccess.new(hopper("BGR;15")) is None # ref https://github.com/python-pillow/Pillow/pull/2009 - def test_reference_counting(self): + def test_reference_counting(self) -> None: size = 10 for _ in range(10): @@ -361,7 +361,7 @@ def test_reference_counting(self): assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode): + def test_p_putpixel_rgb_rgba(self, mode) -> None: for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): @@ -379,7 +379,7 @@ class TestImagePutPixelError(AccessTest): INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode): + def test_putpixel_type_error1(self, mode) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): @@ -402,14 +402,14 @@ def test_putpixel_type_error1(self, mode): ), ), ) - def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match): + def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None: im = hopper(mode) for band_number in band_numbers: with pytest.raises(TypeError, match=match): im.putpixel((0, 0), (0,) * band_number) @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode): + def test_putpixel_type_error2(self, mode) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises( @@ -418,7 +418,7 @@ def test_putpixel_type_error2(self, mode): im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode): + def test_putpixel_overflow_error(self, mode) -> None: im = hopper(mode) with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) @@ -427,7 +427,7 @@ def test_putpixel_overflow_error(self, mode): class TestEmbeddable: @pytest.mark.xfail(reason="failing test") @pytest.mark.skipif(not is_win32(), reason="requires Windows") - def test_embeddable(self): + def test_embeddable(self) -> None: import ctypes from setuptools.command.build_ext import new_compiler diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 0dacb3157a2..0125ab56af9 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -12,12 +12,12 @@ im = hopper().resize((128, 100)) -def test_toarray(): +def test_toarray() -> None: def test(mode): ai = numpy.array(im.convert(mode)) return ai.shape, ai.dtype.str, ai.nbytes - def test_with_dtype(dtype): + def test_with_dtype(dtype) -> None: ai = numpy.array(im, dtype=dtype) assert ai.dtype == dtype @@ -46,11 +46,11 @@ def test_with_dtype(dtype): numpy.array(im_truncated) -def test_fromarray(): +def test_fromarray() -> None: class Wrapper: """Class with API matching Image.fromarray""" - def __init__(self, img, arr_params): + def __init__(self, img, arr_params) -> None: self.img = img self.__array_interface__ = arr_params @@ -89,7 +89,7 @@ def test(mode): Image.fromarray(wrapped) -def test_fromarray_palette(): +def test_fromarray_palette() -> None: # Arrange i = im.convert("L") a = numpy.array(i) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 08c40af1f16..54474311a09 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -19,7 +19,7 @@ def draft_roundtrip(in_mode, in_size, req_mode, req_size): return im -def test_size(): +def test_size() -> None: for in_size, req_size, out_size in [ ((435, 361), (2048, 2048), (435, 361)), # bigger ((435, 361), (435, 361), (435, 361)), # same @@ -48,7 +48,7 @@ def test_size(): assert im.size == out_size -def test_mode(): +def test_mode() -> None: for in_mode, req_mode, out_mode in [ ("RGB", "1", "RGB"), ("RGB", "L", "L"), @@ -68,7 +68,7 @@ def test_mode(): assert im.mode == out_mode -def test_several_drafts(): +def test_several_drafts() -> None: im = draft_roundtrip("L", (128, 128), None, (64, 64)) im.draft(None, (64, 64)) im.load() diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index fce16122409..01107ae6b88 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -3,7 +3,7 @@ from .helper import hopper -def test_entropy(): +def test_entropy() -> None: def entropy(mode): return hopper(mode).entropy() diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 3fa5dd2423a..2b6787933cd 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -36,7 +36,7 @@ ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply, mode): +def test_sanity(filter_to_apply, mode) -> None: im = hopper(mode) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): out = im.filter(filter_to_apply) @@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode): @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode): +def test_sanity_error(mode) -> None: with pytest.raises(TypeError): im = hopper(mode) im.filter("hello") @@ -53,7 +53,7 @@ def test_sanity_error(mode): # crashes on small images @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size): +def test_crash(size) -> None: im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) @@ -67,7 +67,7 @@ def test_crash(size): ("RGB", ((4, 0, 0), (0, 0, 0))), ), ) -def test_modefilter(mode, expected): +def test_modefilter(mode, expected) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -90,7 +90,7 @@ def test_modefilter(mode, expected): ("F", (0.0, 4.0, 8.0)), ), ) -def test_rankfilter(mode, expected): +def test_rankfilter(mode, expected) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -106,7 +106,7 @@ def test_rankfilter(mode, expected): @pytest.mark.parametrize( "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) ) -def test_rankfilter_error(filter): +def test_rankfilter_error(filter) -> None: with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -117,27 +117,27 @@ def test_rankfilter_error(filter): im.filter(filter).getpixel((1, 1)) -def test_rankfilter_properties(): +def test_rankfilter_properties() -> None: rankfilter = ImageFilter.RankFilter(1, 2) assert rankfilter.size == 1 assert rankfilter.rank == 2 -def test_builtinfilter_p(): +def test_builtinfilter_p() -> None: builtin_filter = ImageFilter.BuiltinFilter() with pytest.raises(ValueError): builtin_filter.filter(hopper("P")) -def test_kernel_not_enough_coefficients(): +def test_kernel_not_enough_coefficients() -> None: with pytest.raises(ValueError): ImageFilter.Kernel((3, 3), (0, 0)) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode): +def test_consistency_3x3(mode) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -163,7 +163,7 @@ def test_consistency_3x3(mode): @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode): +def test_consistency_5x5(mode) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss_more" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -199,7 +199,7 @@ def test_consistency_5x5(mode): (2, -2), ), ) -def test_invalid_box_blur_filter(radius): +def test_invalid_box_blur_filter(radius) -> None: with pytest.raises(ValueError): ImageFilter.BoxBlur(radius) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 6bbc4da9a01..0107fdcc426 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_extrema(): +def test_extrema() -> None: def extrema(mode): return hopper(mode).getextrema() @@ -20,7 +20,7 @@ def extrema(mode): assert extrema("I;16") == (1, 255) -def test_true_16(): +def test_true_16() -> None: with Image.open("Tests/images/16_bit_noise.tif") as im: assert im.mode == "I;16" extrema = im.getextrema() diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 4340f46f619..e7304c98f30 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_palette(): +def test_palette() -> None: def palette(mode): p = hopper(mode).getpalette() if p: @@ -23,7 +23,7 @@ def palette(mode): assert palette("YCbCr") is None -def test_palette_rawmode(): +def test_palette_rawmode() -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 2, 3)) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 36f8ba575d8..5b1a9ee2dda 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -10,14 +10,14 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() pix = im.load() assert pix[0, 0] == (20, 20, 70) -def test_close(): +def test_close() -> None: im = Image.open("Tests/images/hopper.gif") im.close() with pytest.raises(ValueError): @@ -26,7 +26,7 @@ def test_close(): im.getpixel((0, 0)) -def test_close_after_load(caplog): +def test_close_after_load(caplog) -> None: im = Image.open("Tests/images/hopper.gif") im.load() with caplog.at_level(logging.DEBUG): @@ -34,7 +34,7 @@ def test_close_after_load(caplog): assert len(caplog.records) == 0 -def test_contextmanager(): +def test_contextmanager() -> None: fn = None with Image.open("Tests/images/hopper.gif") as im: fn = im.fp.fileno() @@ -44,7 +44,7 @@ def test_contextmanager(): os.fstat(fn) -def test_contextmanager_non_exclusive_fp(): +def test_contextmanager_non_exclusive_fp() -> None: with open("Tests/images/hopper.gif", "rb") as fp: with Image.open(fp): pass diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 3c1d494fab2..8e94aafc598 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -7,7 +7,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: with hopper() as im: im.mode @@ -69,7 +69,7 @@ def test_sanity(): ) def test_properties( mode, expected_base, expected_type, expected_bands, expected_band_names -): +) -> None: assert Image.getmodebase(mode) == expected_base assert Image.getmodetype(mode) == expected_type assert Image.getmodebands(mode) == expected_bands diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index fd117f9dbc9..34a2f8f3db5 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -11,7 +11,7 @@ class TestImagingPaste: masks = {} size = 128 - def assert_9points_image(self, im, expected): + def assert_9points_image(self, im, expected) -> None: expected = [ point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected ] @@ -29,7 +29,7 @@ def assert_9points_image(self, im, expected): ] assert actual == expected - def assert_9points_paste(self, im, im2, mask, expected): + def assert_9points_paste(self, im, im2, mask, expected) -> None: im3 = im.copy() im3.paste(im2, (0, 0), mask) self.assert_9points_image(im3, expected) @@ -106,7 +106,7 @@ def gradient_RGBa(self): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode): + def test_image_solid(self, mode) -> None: im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -116,7 +116,7 @@ def test_image_solid(self, mode): assert_image_equal(im, im2) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode): + def test_image_mask_1(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -138,7 +138,7 @@ def test_image_mask_1(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode): + def test_image_mask_L(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -160,7 +160,7 @@ def test_image_mask_L(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode): + def test_image_mask_LA(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -182,7 +182,7 @@ def test_image_mask_LA(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode): + def test_image_mask_RGBA(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -204,7 +204,7 @@ def test_image_mask_RGBA(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode): + def test_image_mask_RGBa(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -226,7 +226,7 @@ def test_image_mask_RGBa(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode): + def test_color_solid(self, mode) -> None: im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) @@ -239,7 +239,7 @@ def test_color_solid(self, mode): assert sum(head[:255]) == 0 @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode): + def test_color_mask_1(self, mode) -> None: im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -261,7 +261,7 @@ def test_color_mask_1(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode): + def test_color_mask_L(self, mode) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -283,7 +283,7 @@ def test_color_mask_L(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode): + def test_color_mask_RGBA(self, mode) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -305,7 +305,7 @@ def test_color_mask_RGBA(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode): + def test_color_mask_RGBa(self, mode) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -326,7 +326,7 @@ def test_color_mask_RGBa(self, mode): ], ) - def test_different_sizes(self): + def test_different_sizes(self) -> None: im = Image.new("RGB", (100, 100)) im2 = Image.new("RGB", (50, 50)) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 2648af8fa2e..10301991624 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -10,7 +10,7 @@ from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im1 = hopper() data = list(im1.getdata()) @@ -29,7 +29,7 @@ def test_sanity(): assert_image_equal(im1, im2) -def test_long_integers(): +def test_long_integers() -> None: # see bug-200802-systemerror def put(value): im = Image.new("RGBA", (1, 1)) @@ -46,19 +46,19 @@ def put(value): assert put(sys.maxsize) == (255, 255, 255, 127) -def test_pypy_performance(): +def test_pypy_performance() -> None: im = Image.new("L", (256, 256)) im.putdata(list(range(256)) * 256) -def test_mode_with_L_with_float(): +def test_mode_with_L_with_float() -> None: im = Image.new("L", (1, 1), 0) im.putdata([2.0]) assert im.getpixel((0, 0)) == 2 @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode): +def test_mode_i(mode) -> None: src = hopper("L") data = list(src.getdata()) im = Image.new(mode, src.size, 0) @@ -68,7 +68,7 @@ def test_mode_i(mode): assert list(im.getdata()) == target -def test_mode_F(): +def test_mode_F() -> None: src = hopper("L") data = list(src.getdata()) im = Image.new("F", src.size, 0) @@ -79,7 +79,7 @@ def test_mode_F(): @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode): +def test_mode_BGR(mode) -> None: data = [(16, 32, 49), (32, 32, 98)] im = Image.new(mode, (1, 2)) im.putdata(data) @@ -87,7 +87,7 @@ def test_mode_BGR(mode): assert list(im.getdata()) == data -def test_array_B(): +def test_array_B() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -98,7 +98,7 @@ def test_array_B(): assert len(im.getdata()) == len(arr) -def test_array_F(): +def test_array_F() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -109,7 +109,7 @@ def test_array_F(): assert len(im.getdata()) == len(arr) -def test_not_flattened(): +def test_not_flattened() -> None: im = Image.new("L", (1, 1)) with pytest.raises(TypeError): im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 43b65be2b17..ffe2551d239 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -7,7 +7,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper -def test_putpalette(): +def test_putpalette() -> None: def palette(mode): im = hopper(mode).copy() im.putpalette(list(range(256)) * 3) @@ -43,7 +43,7 @@ def palette(mode): im.putpalette(list(range(256)) * 3) -def test_imagepalette(): +def test_imagepalette() -> None: im = hopper("P") im.putpalette(ImagePalette.negative()) assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") @@ -57,7 +57,7 @@ def test_imagepalette(): assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") -def test_putpalette_with_alpha_values(): +def test_putpalette_with_alpha_values() -> None: with Image.open("Tests/images/transparent.gif") as im: expected = im.convert("RGBA") @@ -81,19 +81,19 @@ def test_putpalette_with_alpha_values(): ("RGBAX", (1, 2, 3, 4, 0)), ), ) -def test_rgba_palette(mode, palette): +def test_rgba_palette(mode, palette) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] assert im.palette.colors == {(1, 2, 3, 4): 0} -def test_empty_palette(): +def test_empty_palette() -> None: im = Image.new("P", (1, 1)) assert im.getpalette() == [] -def test_undefined_palette_index(): +def test_undefined_palette_index() -> None: im = Image.new("P", (1, 1), 3) im.putpalette((1, 2, 3)) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0) diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index ba9100415a2..c29830a7e5f 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -48,7 +48,7 @@ ((1, 3), (10, 4)), ), ) -def test_args_factor(size, expected): +def test_args_factor(size, expected) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(size).size @@ -56,7 +56,7 @@ def test_args_factor(size, expected): @pytest.mark.parametrize( "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, expected_error): +def test_args_factor_error(size, expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(size) @@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error): ((5, 5, 6, 6), (1, 1)), ), ) -def test_args_box(size, expected): +def test_args_box(size, expected) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(2, size).size @@ -86,14 +86,14 @@ def test_args_box(size, expected): ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, expected_error): +def test_args_box_error(size, expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(2, size).size @pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode): +def test_unsupported_modes(mode) -> None: im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) @@ -119,14 +119,16 @@ def get_image(mode): return im.crop((0, 0, im.width, im.height - 5)) -def compare_reduce_with_box(im, factor): +def compare_reduce_with_box(im, factor) -> None: box = (11, 13, 146, 164) reduced = im.reduce(factor, box=box) reference = im.crop(box).reduce(factor) assert reduced == reference -def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): +def compare_reduce_with_reference( + im, factor, average_diff: float = 0.4, max_diff: int = 1 +) -> None: """Image.reduce() should look very similar to Image.resize(BOX). A reference image is compiled from a large source area @@ -171,7 +173,7 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): assert_compare_images(reduced, reference, average_diff, max_diff) -def assert_compare_images(a, b, max_average_diff, max_diff=255): +def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -199,20 +201,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor): +def test_mode_L(factor) -> None: im = get_image("L") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor): +def test_mode_LA(factor) -> None: im = get_image("LA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor): +def test_mode_LA_opaque(factor) -> None: im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -221,27 +223,27 @@ def test_mode_LA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor): +def test_mode_La(factor) -> None: im = get_image("La") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor): +def test_mode_RGB(factor) -> None: im = get_image("RGB") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor): +def test_mode_RGBA(factor) -> None: im = get_image("RGBA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor): +def test_mode_RGBA_opaque(factor) -> None: im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -250,27 +252,27 @@ def test_mode_RGBA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor): +def test_mode_RGBa(factor) -> None: im = get_image("RGBa") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor): +def test_mode_I(factor) -> None: im = get_image("I") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor): +def test_mode_F(factor) -> None: im = get_image("F") compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor) @skip_unless_feature("jpg_2000") -def test_jpeg2k(): +def test_jpeg2k() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.reduce(2).size == (320, 240) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index af730dce13d..f4c9eb0e6fa 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -16,7 +16,7 @@ class TestImagingResampleVulnerability: # see https://github.com/python-pillow/Pillow/issues/1710 - def test_overflow(self): + def test_overflow(self) -> None: im = hopper("L") size_too_large = 0x100000008 // 4 size_normal = 1000 # unimportant @@ -28,7 +28,7 @@ def test_overflow(self): # any resampling filter will do here im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) - def test_invalid_size(self): + def test_invalid_size(self) -> None: im = hopper() # Should not crash @@ -40,7 +40,7 @@ def test_invalid_size(self): with pytest.raises(ValueError): im.resize((100, -100)) - def test_modify_after_resizing(self): + def test_modify_after_resizing(self) -> None: im = hopper("RGB") # get copy with same size copy = im.resize(im.size) @@ -83,7 +83,7 @@ def make_sample(self, data, size): s_px[size[0] - x - 1, y] = 255 - val return sample - def check_case(self, case, sample): + def check_case(self, case, sample) -> None: s_px = sample.load() c_px = case.load() for y in range(case.size[1]): @@ -103,7 +103,7 @@ def serialize_image(self, image): ) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode): + def test_reduce_box(self, mode) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -114,7 +114,7 @@ def test_reduce_box(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode): + def test_reduce_bilinear(self, mode) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -125,7 +125,7 @@ def test_reduce_bilinear(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode): + def test_reduce_hamming(self, mode) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -136,7 +136,7 @@ def test_reduce_hamming(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode): + def test_reduce_bicubic(self, mode) -> None: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off @@ -148,7 +148,7 @@ def test_reduce_bicubic(self, mode): self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode): + def test_reduce_lanczos(self, mode) -> None: case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off @@ -161,7 +161,7 @@ def test_reduce_lanczos(self, mode): self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode): + def test_enlarge_box(self, mode) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -172,7 +172,7 @@ def test_enlarge_box(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode): + def test_enlarge_bilinear(self, mode) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -183,7 +183,7 @@ def test_enlarge_bilinear(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode): + def test_enlarge_hamming(self, mode) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -194,7 +194,7 @@ def test_enlarge_hamming(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode): + def test_enlarge_bicubic(self, mode) -> None: case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off @@ -207,7 +207,7 @@ def test_enlarge_bicubic(self, mode): self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode): + def test_enlarge_lanczos(self, mode) -> None: case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( @@ -221,7 +221,7 @@ def test_enlarge_lanczos(self, mode): for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) - def test_box_filter_correct_range(self): + def test_box_filter_correct_range(self) -> None: im = Image.new("RGB", (8, 8), "#1688ff").resize( (100, 100), Image.Resampling.BOX ) @@ -234,7 +234,7 @@ def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - def run_case(self, case): + def run_case(self, case) -> None: channel, color = case px = channel.load() for x in range(channel.size[0]): @@ -243,7 +243,7 @@ def run_case(self, case): message = f"{px[x, y]} != {color} for pixel {(x, y)}" assert px[x, y] == color, message - def test_8u(self): + def test_8u(self) -> None: im, color = self.make_case("RGB", (0, 64, 255)) r, g, b = im.split() self.run_case((r, color[0])) @@ -251,13 +251,13 @@ def test_8u(self): self.run_case((b, color[2])) self.run_case(self.make_case("L", 12)) - def test_32i(self): + def test_32i(self) -> None: self.run_case(self.make_case("I", 12)) self.run_case(self.make_case("I", 0x7FFFFFFF)) self.run_case(self.make_case("I", -12)) self.run_case(self.make_case("I", -1 << 31)) - def test_32f(self): + def test_32f(self) -> None: self.run_case(self.make_case("F", 1)) self.run_case(self.make_case("F", 3.40282306074e38)) self.run_case(self.make_case("F", 1.175494e-38)) @@ -275,7 +275,7 @@ def make_levels_case(self, mode): px[x, y] = tuple(pix) return i - def run_levels_case(self, i): + def run_levels_case(self, i) -> None: px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} @@ -285,7 +285,7 @@ def run_levels_case(self, i): ) @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_rgba(self): + def test_levels_rgba(self) -> None: case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -294,7 +294,7 @@ def test_levels_rgba(self): self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_la(self): + def test_levels_la(self) -> None: case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -312,7 +312,7 @@ def make_dirty_case(self, mode, clean_pixel, dirty_pixel): px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dirty_case(self, i, clean_pixel): + def run_dirty_case(self, i, clean_pixel) -> None: px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): @@ -323,7 +323,7 @@ def run_dirty_case(self, i, clean_pixel): ) assert px[x, y][:3] == clean_pixel, message - def test_dirty_pixels_rgba(self): + def test_dirty_pixels_rgba(self) -> None: case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) self.run_dirty_case( @@ -339,7 +339,7 @@ def test_dirty_pixels_rgba(self): case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) ) - def test_dirty_pixels_la(self): + def test_dirty_pixels_la(self) -> None: case = self.make_dirty_case("LA", (255, 128), (0, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) @@ -355,22 +355,22 @@ def count(self, diff): yield assert Image.core.get_stats()["new_count"] - count == diff - def test_horizontal(self): + def test_horizontal(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) - def test_vertical(self): + def test_vertical(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) - def test_both(self): + def test_both(self) -> None: im = hopper("L") with self.count(2): im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) - def test_box_horizontal(self): + def test_box_horizontal(self) -> None: im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): @@ -380,7 +380,7 @@ def test_box_horizontal(self): cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) - def test_box_vertical(self): + def test_box_vertical(self) -> None: im = hopper("L") box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): @@ -392,7 +392,7 @@ def test_box_vertical(self): class TestCoreResampleCoefficients: - def test_reduce(self): + def test_reduce(self) -> None: test_color = 254 for size in range(400000, 400010, 2): @@ -404,7 +404,7 @@ def test_reduce(self): if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_non_zero_coefficients(self): + def test_non_zero_coefficients(self) -> None: # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) @@ -432,7 +432,7 @@ class TestCoreResampleBox: Image.Resampling.LANCZOS, ), ) - def test_wrong_arguments(self, resample): + def test_wrong_arguments(self, resample) -> None: im = hopper() im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -478,7 +478,7 @@ def split_range(size, tiles): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiles(self): + def test_tiles(self) -> None: with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) dst_size = (251, 188) @@ -491,7 +491,7 @@ def test_tiles(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_subsample(self): + def test_subsample(self) -> None: # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). with Image.open("Tests/images/flower.jpg") as im: @@ -518,14 +518,14 @@ def test_subsample(self): @pytest.mark.parametrize( "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) ) - def test_formats(self, mode, resample): + def test_formats(self, mode, resample) -> None: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) cropped = im.crop(box).resize((32, 32), resample) assert_image_similar(cropped, with_box, 0.4) - def test_passthrough(self): + def test_passthrough(self) -> None: # When no resize is required im = hopper() @@ -539,7 +539,7 @@ def test_passthrough(self): assert res.size == size assert_image_equal(res, im.crop(box), f">>> {size} {box}") - def test_no_passthrough(self): + def test_no_passthrough(self) -> None: # When resize is required im = hopper() @@ -558,7 +558,7 @@ def test_no_passthrough(self): @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_horizontal(self, flt): + def test_skip_horizontal(self, flt) -> None: # Can skip resize for one dimension im = hopper() @@ -581,7 +581,7 @@ def test_skip_horizontal(self, flt): @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_vertical(self, flt): + def test_skip_vertical(self, flt) -> None: # Can skip resize for one dimension im = hopper() diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index e63fef2c152..51e0f585421 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -12,7 +12,7 @@ ) -def rotate(im, mode, angle, center=None, translate=None): +def rotate(im, mode, angle, center=None, translate=None) -> None: out = im.rotate(angle, center=center, translate=translate) assert out.mode == mode assert out.size == im.size # default rotate clips output @@ -27,13 +27,13 @@ def rotate(im, mode, angle, center=None, translate=None): @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode): +def test_mode(mode) -> None: im = hopper(mode) rotate(im, mode, 45) @pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle): +def test_angle(angle) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) @@ -42,12 +42,12 @@ def test_angle(angle): @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle): +def test_zero(angle) -> None: im = Image.new("RGB", (0, 0)) rotate(im, im.mode, angle) -def test_resample(): +def test_resample() -> None: # Target image creation, inspected by eye. # >>> im = Image.open('Tests/images/hopper.ppm') # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) @@ -64,7 +64,7 @@ def test_resample(): assert_image_similar(im, target, epsilon) -def test_center_0(): +def test_center_0() -> None: im = hopper() im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) @@ -75,7 +75,7 @@ def test_center_0(): assert_image_similar(im, target, 15) -def test_center_14(): +def test_center_14() -> None: im = hopper() im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) @@ -86,7 +86,7 @@ def test_center_14(): assert_image_similar(im, target, 10) -def test_translate(): +def test_translate() -> None: im = hopper() with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 @@ -99,7 +99,7 @@ def test_translate(): assert_image_similar(im, target, 1) -def test_fastpath_center(): +def test_fastpath_center() -> None: # if the center is -1,-1 and we rotate by 90<=x<=270 the # resulting image should be black for angle in (90, 180, 270): @@ -107,7 +107,7 @@ def test_fastpath_center(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_fastpath_translate(): +def test_fastpath_translate() -> None: # if we post-translate by -128 # resulting image should be black for angle in (0, 90, 180, 270): @@ -115,26 +115,26 @@ def test_fastpath_translate(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_center(): +def test_center() -> None: im = hopper() rotate(im, im.mode, 45, center=(0, 0)) rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) -def test_rotate_no_fill(): +def test_rotate_no_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45) assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png") -def test_rotate_with_fill(): +def test_rotate_with_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45, fillcolor="white") assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png") -def test_alpha_rotate_no_fill(): +def test_alpha_rotate_no_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1) @@ -142,7 +142,7 @@ def test_alpha_rotate_no_fill(): assert corner == (0, 0, 0, 0) -def test_alpha_rotate_with_fill(): +def test_alpha_rotate_with_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 7fa5692aa7d..6aeeea2ed0d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -14,14 +14,14 @@ ) -def test_sanity(): +def test_sanity() -> None: im = hopper() assert im.thumbnail((100, 100)) is None assert im.size == (100, 100) -def test_aspect(): +def test_aspect() -> None: im = Image.new("L", (128, 128)) im.thumbnail((100, 100)) assert im.size == (100, 100) @@ -67,19 +67,19 @@ def test_aspect(): assert im.size == (75, 23) # ratio is 3.260869565217 -def test_division_by_zero(): +def test_division_by_zero() -> None: im = Image.new("L", (200, 2)) im.thumbnail((75, 75)) assert im.size == (75, 1) -def test_float(): +def test_float() -> None: im = Image.new("L", (128, 128)) im.thumbnail((99.9, 99.9)) assert im.size == (99, 99) -def test_no_resize(): +def test_no_resize() -> None: # Check that draft() can resize the image to the destination size with Image.open("Tests/images/hopper.jpg") as im: im.draft(None, (64, 64)) @@ -92,7 +92,7 @@ def test_no_resize(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: # load() may change the size of the image # Test that thumbnail() is calling it before performing size calculations with Image.open("Tests/images/g4_orientation_5.tif") as im: @@ -106,7 +106,7 @@ def test_load_first(): assert im.size == (590, 88) -def test_load_first_unless_jpeg(): +def test_load_first_unless_jpeg() -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft @@ -124,7 +124,7 @@ def im_draft(mode, size): # valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") -def test_DCT_scaling_edges(): +def test_DCT_scaling_edges() -> None: # Make an image with red borders and size (N * 8) + 1 to cross DCT grid im = Image.new("RGB", (257, 257), "red") im.paste(Image.new("RGB", (235, 235)), (11, 11)) @@ -138,7 +138,7 @@ def test_DCT_scaling_edges(): assert_image_similar(thumb, ref, 1.5) -def test_reducing_gap_values(): +def test_reducing_gap_values() -> None: im = hopper() im.thumbnail((18, 18), Image.Resampling.BICUBIC) @@ -155,7 +155,7 @@ def test_reducing_gap_values(): assert_image_similar(ref, im, 3.5) -def test_reducing_gap_for_DCT_scaling(): +def test_reducing_gap_for_DCT_scaling() -> None: with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 0fe9fd1d5f5..1067dd563c1 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -10,7 +10,7 @@ class TestImageTransform: - def test_sanity(self): + def test_sanity(self) -> None: im = hopper() for transform in ( @@ -31,7 +31,7 @@ def test_sanity(self): ): assert_image_equal(im, im.transform(im.size, transform)) - def test_info(self): + def test_info(self) -> None: comment = b"File written by Adobe Photoshop\xa8 4.0" with Image.open("Tests/images/hopper.gif") as im: @@ -41,14 +41,14 @@ def test_info(self): new_im = im.transform((100, 100), transform) assert new_im.info["comment"] == comment - def test_palette(self): + def test_palette(self) -> None: with Image.open("Tests/images/hopper.gif") as im: transformed = im.transform( im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) assert im.palette.palette == transformed.palette.palette - def test_extent(self): + def test_extent(self) -> None: im = hopper("RGB") (w, h) = im.size transformed = im.transform( @@ -63,7 +63,7 @@ def test_extent(self): # undone -- precision? assert_image_similar(transformed, scaled, 23) - def test_quad(self): + def test_quad(self) -> None: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") (w, h) = im.size @@ -91,7 +91,7 @@ def test_quad(self): ("LA", (76, 0)), ), ) - def test_fill(self, mode, expected_pixel): + def test_fill(self, mode, expected_pixel) -> None: im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -103,7 +103,7 @@ def test_fill(self, mode, expected_pixel): ) assert transformed.getpixel((w - 1, h - 1)) == expected_pixel - def test_mesh(self): + def test_mesh(self) -> None: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") (w, h) = im.size @@ -142,7 +142,7 @@ def test_mesh(self): assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - def _test_alpha_premult(self, op): + def _test_alpha_premult(self, op) -> None: # create image with half white, half black, # with the black half transparent. # do op, @@ -158,13 +158,13 @@ def _test_alpha_premult(self, op): hist = im_background.histogram() assert 40 * 10 == hist[-1] - def test_alpha_premult_resize(self): + def test_alpha_premult_resize(self) -> None: def op(im, sz): return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) - def test_alpha_premult_transform(self): + def test_alpha_premult_transform(self) -> None: def op(im, sz): (w, h) = im.size return im.transform( @@ -173,7 +173,7 @@ def op(im, sz): self._test_alpha_premult(op) - def _test_nearest(self, op, mode): + def _test_nearest(self, op, mode) -> None: # create white image with half transparent, # do op, # the image should remain white with half transparent @@ -196,14 +196,14 @@ def _test_nearest(self, op, mode): ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode): + def test_nearest_resize(self, mode) -> None: def op(im, sz): return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode): + def test_nearest_transform(self, mode) -> None: def op(im, sz): (w, h) = im.size return im.transform( @@ -212,7 +212,7 @@ def op(im, sz): self._test_nearest(op, mode) - def test_blank_fill(self): + def test_blank_fill(self) -> None: # attempting to hit # https://github.com/python-pillow/Pillow/issues/254 reported # @@ -234,13 +234,13 @@ def test_blank_fill(self): self.test_mesh() - def test_missing_method_data(self): + def test_missing_method_data(self) -> None: with hopper() as im: with pytest.raises(ValueError): im.transform((100, 100), None) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample): + def test_unknown_resampling_filter(self, resample) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): @@ -263,7 +263,7 @@ def _test_image(self): (270, Image.Transpose.ROTATE_270), ), ) - def test_rotate(self, deg, transpose): + def test_rotate(self, deg, transpose) -> None: im = self._test_image() angle = -math.radians(deg) @@ -313,7 +313,7 @@ def test_rotate(self, deg, transpose): (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilon_scale, resample, epsilon): + def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -342,7 +342,7 @@ def test_resize(self, scale, epsilon_scale, resample, epsilon): (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilon_scale, resample, epsilon): + def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None: im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 2f0614385aa..94f57e06690 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -15,7 +15,7 @@ GRAY = 128 -def test_sanity(): +def test_sanity() -> None: im = hopper("L") ImageChops.constant(im, 128) @@ -48,7 +48,7 @@ def test_sanity(): ImageChops.offset(im, 10, 20) -def test_add(): +def test_add() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -60,7 +60,7 @@ def test_add(): assert new.getpixel((50, 50)) == ORANGE -def test_add_scale_offset(): +def test_add_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -72,7 +72,7 @@ def test_add_scale_offset(): assert new.getpixel((50, 50)) == (202, 151, 100) -def test_add_clip(): +def test_add_clip() -> None: # Arrange im = hopper() @@ -83,7 +83,7 @@ def test_add_clip(): assert new.getpixel((50, 50)) == (255, 255, 254) -def test_add_modulo(): +def test_add_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -95,7 +95,7 @@ def test_add_modulo(): assert new.getpixel((50, 50)) == ORANGE -def test_add_modulo_no_clip(): +def test_add_modulo_no_clip() -> None: # Arrange im = hopper() @@ -106,7 +106,7 @@ def test_add_modulo_no_clip(): assert new.getpixel((50, 50)) == (224, 76, 254) -def test_blend(): +def test_blend() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -118,7 +118,7 @@ def test_blend(): assert new.getpixel((50, 50)) == BROWN -def test_constant(): +def test_constant() -> None: # Arrange im = Image.new("RGB", (20, 10)) @@ -131,7 +131,7 @@ def test_constant(): assert new.getpixel((19, 9)) == GRAY -def test_darker_image(): +def test_darker_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -142,7 +142,7 @@ def test_darker_image(): assert_image_equal(new, im2) -def test_darker_pixel(): +def test_darker_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -153,7 +153,7 @@ def test_darker_pixel(): assert new.getpixel((50, 50)) == (240, 166, 0) -def test_difference(): +def test_difference() -> None: # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: @@ -164,7 +164,7 @@ def test_difference(): assert new.getbbox() == (25, 25, 76, 76) -def test_difference_pixel(): +def test_difference_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: @@ -175,7 +175,7 @@ def test_difference_pixel(): assert new.getpixel((50, 50)) == (240, 166, 128) -def test_duplicate(): +def test_duplicate() -> None: # Arrange im = hopper() @@ -186,7 +186,7 @@ def test_duplicate(): assert_image_equal(new, im) -def test_invert(): +def test_invert() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: # Act @@ -198,7 +198,7 @@ def test_invert(): assert new.getpixel((50, 50)) == CYAN -def test_lighter_image(): +def test_lighter_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -209,7 +209,7 @@ def test_lighter_image(): assert_image_equal(new, im1) -def test_lighter_pixel(): +def test_lighter_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -220,7 +220,7 @@ def test_lighter_pixel(): assert new.getpixel((50, 50)) == (255, 255, 127) -def test_multiply_black(): +def test_multiply_black() -> None: """If you multiply an image with a solid black image, the result is black.""" # Arrange @@ -234,7 +234,7 @@ def test_multiply_black(): assert_image_equal(new, black) -def test_multiply_green(): +def test_multiply_green() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: green = Image.new("RGB", im.size, "green") @@ -248,7 +248,7 @@ def test_multiply_green(): assert new.getpixel((50, 50)) == BLACK -def test_multiply_white(): +def test_multiply_white() -> None: """If you multiply with a solid white image, the image is unaffected.""" # Arrange im1 = hopper() @@ -261,7 +261,7 @@ def test_multiply_white(): assert_image_equal(new, im1) -def test_offset(): +def test_offset() -> None: # Arrange xoffset = 45 yoffset = 20 @@ -278,7 +278,7 @@ def test_offset(): assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) -def test_screen(): +def test_screen() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -290,7 +290,7 @@ def test_screen(): assert new.getpixel((50, 50)) == ORANGE -def test_subtract(): +def test_subtract() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -303,7 +303,7 @@ def test_subtract(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_scale_offset(): +def test_subtract_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -315,7 +315,7 @@ def test_subtract_scale_offset(): assert new.getpixel((50, 50)) == (100, 202, 100) -def test_subtract_clip(): +def test_subtract_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -326,7 +326,7 @@ def test_subtract_clip(): assert new.getpixel((50, 50)) == (0, 0, 127) -def test_subtract_modulo(): +def test_subtract_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -339,7 +339,7 @@ def test_subtract_modulo(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_modulo_no_clip(): +def test_subtract_modulo_no_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -350,7 +350,7 @@ def test_subtract_modulo_no_clip(): assert new.getpixel((50, 50)) == (241, 167, 127) -def test_soft_light(): +def test_soft_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -362,7 +362,7 @@ def test_soft_light(): assert new.getpixel((15, 100)) == (1, 1, 3) -def test_hard_light(): +def test_hard_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -374,7 +374,7 @@ def test_hard_light(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_overlay(): +def test_overlay() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -386,7 +386,7 @@ def test_overlay(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_logical(): +def test_logical() -> None: def table(op, a, b): out = [] for x in (a, b): diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 03332699a1d..7f652715566 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -5,6 +5,7 @@ import re import shutil from io import BytesIO +from pathlib import Path import pytest @@ -32,7 +33,7 @@ HAVE_PROFILE = os.path.exists(SRGB) -def setup_module(): +def setup_module() -> None: try: from PIL import ImageCms @@ -42,12 +43,12 @@ def setup_module(): pytest.skip(str(v)) -def skip_missing(): +def skip_missing() -> None: if not HAVE_PROFILE: pytest.skip("SRGB profile not available") -def test_sanity(): +def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. with pytest.warns(DeprecationWarning): @@ -91,7 +92,7 @@ def test_sanity(): hopper().point(t) -def test_flags(): +def test_flags() -> None: assert ImageCms.Flags.NONE == 0 assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE @@ -101,7 +102,7 @@ def test_flags(): assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) -def test_name(): +def test_name() -> None: skip_missing() # get profile information for file assert ( @@ -110,7 +111,7 @@ def test_name(): ) -def test_info(): +def test_info() -> None: skip_missing() assert ImageCms.getProfileInfo(SRGB).splitlines() == [ "sRGB IEC61966-2-1 black scaled", @@ -120,7 +121,7 @@ def test_info(): ] -def test_copyright(): +def test_copyright() -> None: skip_missing() assert ( ImageCms.getProfileCopyright(SRGB).strip() @@ -128,12 +129,12 @@ def test_copyright(): ) -def test_manufacturer(): +def test_manufacturer() -> None: skip_missing() assert ImageCms.getProfileManufacturer(SRGB).strip() == "" -def test_model(): +def test_model() -> None: skip_missing() assert ( ImageCms.getProfileModel(SRGB).strip() @@ -141,14 +142,14 @@ def test_model(): ) -def test_description(): +def test_description() -> None: skip_missing() assert ( ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" ) -def test_intent(): +def test_intent() -> None: skip_missing() assert ImageCms.getDefaultIntent(SRGB) == 0 support = ImageCms.isIntentSupported( @@ -157,7 +158,7 @@ def test_intent(): assert support == 1 -def test_profile_object(): +def test_profile_object() -> None: # same, using profile object p = ImageCms.createProfile("sRGB") # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" @@ -170,7 +171,7 @@ def test_profile_object(): assert support == 1 -def test_extensions(): +def test_extensions() -> None: # extensions with Image.open("Tests/images/rgb.jpg") as i: @@ -181,7 +182,7 @@ def test_extensions(): ) -def test_exceptions(): +def test_exceptions() -> None: # Test mode mismatch psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -207,17 +208,17 @@ def test_exceptions(): ImageCms.isIntentSupported(SRGB, None, None) -def test_display_profile(): +def test_display_profile() -> None: # try fetching the profile for the current display device ImageCms.get_display_profile() -def test_lab_color_profile(): +def test_lab_color_profile() -> None: ImageCms.createProfile("LAB", 5000) ImageCms.createProfile("LAB", 6500) -def test_unsupported_color_space(): +def test_unsupported_color_space() -> None: with pytest.raises( ImageCms.PyCMSError, match=re.escape( @@ -227,7 +228,7 @@ def test_unsupported_color_space(): ImageCms.createProfile("unsupported") -def test_invalid_color_temperature(): +def test_invalid_color_temperature() -> None: with pytest.raises( ImageCms.PyCMSError, match='Color temperature must be numeric, "invalid" not valid', @@ -236,7 +237,7 @@ def test_invalid_color_temperature(): @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag): +def test_invalid_flag(flag) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -244,7 +245,7 @@ def test_invalid_flag(flag): ImageCms.profileToProfile(im, "foo", "bar", flags=flag) -def test_simple_lab(): +def test_simple_lab() -> None: i = Image.new("RGB", (10, 10), (128, 128, 128)) psRGB = ImageCms.createProfile("sRGB") @@ -268,7 +269,7 @@ def test_simple_lab(): assert list(b_data) == [128] * 100 -def test_lab_color(): +def test_lab_color() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") @@ -283,7 +284,7 @@ def test_lab_color(): assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5) -def test_lab_srgb(): +def test_lab_srgb() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") @@ -300,7 +301,7 @@ def test_lab_srgb(): assert "sRGB" in ImageCms.getProfileDescription(profile) -def test_lab_roundtrip(): +def test_lab_roundtrip() -> None: # check to see if we're at least internally consistent. psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -317,7 +318,7 @@ def test_lab_roundtrip(): assert_image_similar(hopper(), out, 2) -def test_profile_tobytes(): +def test_profile_tobytes() -> None: with Image.open("Tests/images/rgb.jpg") as i: p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) @@ -329,12 +330,12 @@ def test_profile_tobytes(): assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) -def test_extended_information(): +def test_extended_information() -> None: skip_missing() o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits=10): + def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits @@ -476,7 +477,7 @@ def truncate_tuple(tuple_or_float): assert p.xcolor_space == "RGB " -def test_non_ascii_path(tmp_path): +def test_non_ascii_path(tmp_path: Path) -> None: skip_missing() tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) try: @@ -489,7 +490,7 @@ def test_non_ascii_path(tmp_path): assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" -def test_profile_typesafety(): +def test_profile_typesafety() -> None: """Profile init type safety prepatch, these would segfault, postpatch they should emit a typeerror @@ -501,7 +502,7 @@ def test_profile_typesafety(): ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): +def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: def create_test_image(): # set up test image with something interesting in the tested aux channel. # fmt: off @@ -556,31 +557,31 @@ def create_test_image(): assert_image_equal(source_image_aux, result_image_aux) -def test_preserve_auxiliary_channels_rgba(): +def test_preserve_auxiliary_channels_rgba() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=False, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgba_in_place(): +def test_preserve_auxiliary_channels_rgba_in_place() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=True, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgbx(): +def test_preserve_auxiliary_channels_rgbx() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=False, preserved_channel="X" ) -def test_preserve_auxiliary_channels_rgbx_in_place(): +def test_preserve_auxiliary_channels_rgbx_in_place() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=True, preserved_channel="X" ) -def test_auxiliary_channels_isolated(): +def test_auxiliary_channels_isolated() -> None: # test data in aux channels does not affect non-aux channels aux_channel_formats = [ # format, profile, color-only format, source test image @@ -630,7 +631,7 @@ def test_auxiliary_channels_isolated(): @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode): +def test_rgb_lab(mode) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index b602172b642..6eea7886dd8 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -5,7 +5,7 @@ from PIL import Image, ImageColor -def test_hash(): +def test_hash() -> None: # short 3 components assert (255, 0, 0) == ImageColor.getrgb("#f00") assert (0, 255, 0) == ImageColor.getrgb("#0f0") @@ -57,7 +57,7 @@ def test_hash(): ImageColor.getrgb("#f00000 ") -def test_colormap(): +def test_colormap() -> None: assert (0, 0, 0) == ImageColor.getrgb("black") assert (255, 255, 255) == ImageColor.getrgb("white") assert (255, 255, 255) == ImageColor.getrgb("WHITE") @@ -66,7 +66,7 @@ def test_colormap(): ImageColor.getrgb("black ") -def test_functions(): +def test_functions() -> None: # rgb numbers assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") @@ -160,7 +160,7 @@ def test_functions(): # look for rounding errors (based on code by Tim Hatch) -def test_rounding_errors(): +def test_rounding_errors() -> None: for color in ImageColor.colormap: expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) actual = ImageColor.getcolor(color, "L") @@ -195,11 +195,11 @@ def test_rounding_errors(): Image.new("LA", (1, 1), "white") -def test_color_hsv(): +def test_color_hsv() -> None: assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") -def test_color_too_long(): +def test_color_too_long() -> None: # Arrange color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 69aab48912b..86d25b1ebc7 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -47,7 +47,7 @@ ) -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw.ImageDraw(im) @@ -59,13 +59,13 @@ def test_sanity(): draw.rectangle(list(range(4))) -def test_valueerror(): +def test_valueerror() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) -def test_mode_mismatch(): +def test_mode_mismatch() -> None: im = hopper("RGB").copy() with pytest.raises(ValueError): @@ -74,7 +74,7 @@ def test_mode_mismatch(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox, start, end): +def test_arc(bbox, start, end) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -87,7 +87,7 @@ def test_arc(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox): +def test_arc_end_le_start(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -102,7 +102,7 @@ def test_arc_end_le_start(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox): +def test_arc_no_loops(bbox) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -118,7 +118,7 @@ def test_arc_no_loops(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox): +def test_arc_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -131,7 +131,7 @@ def test_arc_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox): +def test_arc_width_pieslice_large(bbox) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -145,7 +145,7 @@ def test_arc_width_pieslice_large(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox): +def test_arc_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -158,7 +158,7 @@ def test_arc_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox): +def test_arc_width_non_whole_angle(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -171,7 +171,7 @@ def test_arc_width_non_whole_angle(bbox): assert_image_similar_tofile(im, expected, 1) -def test_arc_high(): +def test_arc_high() -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -184,7 +184,7 @@ def test_arc_high(): assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png") -def test_bitmap(): +def test_bitmap() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -200,7 +200,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode, bbox): +def test_chord(mode, bbox) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -214,7 +214,7 @@ def test_chord(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox): +def test_chord_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -227,7 +227,7 @@ def test_chord_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox): +def test_chord_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -240,7 +240,7 @@ def test_chord_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox): +def test_chord_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -252,7 +252,7 @@ def test_chord_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") -def test_chord_too_fat(): +def test_chord_too_fat() -> None: # Arrange im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) @@ -266,7 +266,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode, bbox): +def test_ellipse(mode, bbox) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -280,7 +280,7 @@ def test_ellipse(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox): +def test_ellipse_translucent(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -293,7 +293,7 @@ def test_ellipse_translucent(bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -305,7 +305,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -def test_ellipse_symmetric(): +def test_ellipse_symmetric() -> None: for width, bbox in ( (100, (24, 24, 75, 75)), (101, (25, 25, 75, 75)), @@ -317,7 +317,7 @@ def test_ellipse_symmetric(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox): +def test_ellipse_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -329,7 +329,7 @@ def test_ellipse_width(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) -def test_ellipse_width_large(): +def test_ellipse_width_large() -> None: # Arrange im = Image.new("RGB", (500, 500)) draw = ImageDraw.Draw(im) @@ -342,7 +342,7 @@ def test_ellipse_width_large(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox): +def test_ellipse_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -355,7 +355,7 @@ def test_ellipse_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox): +def test_ellipse_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -394,13 +394,13 @@ def ellipse_various_sizes_helper(filled): return im -def test_ellipse_various_sizes(): +def test_ellipse_various_sizes() -> None: im = ellipse_various_sizes_helper(False) assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png") -def test_ellipse_various_sizes_filled(): +def test_ellipse_various_sizes_filled() -> None: im = ellipse_various_sizes_helper(True) assert_image_equal_tofile( @@ -409,7 +409,7 @@ def test_ellipse_various_sizes_filled(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -421,7 +421,7 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_shape1(): +def test_shape1() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -442,7 +442,7 @@ def test_shape1(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png") -def test_shape2(): +def test_shape2() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -463,7 +463,7 @@ def test_shape2(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") -def test_transform(): +def test_transform() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") expected = im.copy() @@ -482,7 +482,7 @@ def test_transform(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox, start, end): +def test_pieslice(bbox, start, end) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -495,7 +495,7 @@ def test_pieslice(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox): +def test_pieslice_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -508,7 +508,7 @@ def test_pieslice_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox): +def test_pieslice_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -522,7 +522,7 @@ def test_pieslice_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox): +def test_pieslice_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -534,7 +534,7 @@ def test_pieslice_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") -def test_pieslice_wide(): +def test_pieslice_wide() -> None: # Arrange im = Image.new("RGB", (200, 100)) draw = ImageDraw.Draw(im) @@ -546,7 +546,7 @@ def test_pieslice_wide(): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") -def test_pieslice_no_spikes(): +def test_pieslice_no_spikes() -> None: im = Image.new("RGB", (161, 161), "white") draw = ImageDraw.Draw(im) cxs = ( @@ -577,7 +577,7 @@ def test_pieslice_no_spikes(): @pytest.mark.parametrize("points", POINTS) -def test_point(points): +def test_point(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -589,7 +589,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -def test_point_I16(): +def test_point_I16() -> None: # Arrange im = Image.new("I;16", (1, 1)) draw = ImageDraw.Draw(im) @@ -602,7 +602,7 @@ def test_point_I16(): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -616,7 +616,7 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite(mode, kite_points): +def test_polygon_kite(mode, kite_points) -> None: # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -631,7 +631,7 @@ def test_polygon_kite(mode, kite_points): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high(): +def test_polygon_1px_high() -> None: # Test drawing a 1px high polygon # Arrange im = Image.new("RGB", (3, 3)) @@ -645,7 +645,7 @@ def test_polygon_1px_high(): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high_translucent(): +def test_polygon_1px_high_translucent() -> None: # Test drawing a translucent 1px high polygon # Arrange im = Image.new("RGB", (4, 3)) @@ -659,7 +659,7 @@ def test_polygon_1px_high_translucent(): assert_image_equal_tofile(im, expected) -def test_polygon_translucent(): +def test_polygon_translucent() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -673,7 +673,7 @@ def test_polygon_translucent(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -685,7 +685,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -700,7 +700,7 @@ def test_big_rectangle(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox): +def test_rectangle_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -714,7 +714,7 @@ def test_rectangle_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox): +def test_rectangle_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -728,7 +728,7 @@ def test_rectangle_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox): +def test_rectangle_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -741,7 +741,7 @@ def test_rectangle_zero_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox): +def test_rectangle_I16(bbox) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -754,7 +754,7 @@ def test_rectangle_I16(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox): +def test_rectangle_translucent_outline(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -772,7 +772,7 @@ def test_rectangle_translucent_outline(bbox): "xy", [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) -def test_rounded_rectangle(xy): +def test_rounded_rectangle(xy) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -788,7 +788,9 @@ def test_rounded_rectangle(xy): @pytest.mark.parametrize("top_right", (True, False)) @pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False)) -def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left): +def test_rounded_rectangle_corners( + top_left, top_right, bottom_right, bottom_left +) -> None: corners = (top_left, top_right, bottom_right, bottom_left) # Arrange @@ -822,7 +824,7 @@ def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_lef ((10, 20, 190, 181), 85, "height"), ], ) -def test_rounded_rectangle_non_integer_radius(xy, radius, type): +def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -838,7 +840,7 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox): +def test_rounded_rectangle_zero_radius(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -860,7 +862,7 @@ def test_rounded_rectangle_zero_radius(bbox): ((20, 20, 80, 80), "both"), ], ) -def test_rounded_rectangle_translucent(xy, suffix): +def test_rounded_rectangle_translucent(xy, suffix) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -877,7 +879,7 @@ def test_rounded_rectangle_translucent(xy, suffix): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox): +def test_floodfill(bbox) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -910,7 +912,7 @@ def test_floodfill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox): +def test_floodfill_border(bbox) -> None: # floodfill() is experimental # Arrange @@ -932,7 +934,7 @@ def test_floodfill_border(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox): +def test_floodfill_thresh(bbox) -> None: # floodfill() is experimental # Arrange @@ -948,7 +950,7 @@ def test_floodfill_thresh(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_not_negative(): +def test_floodfill_not_negative() -> None: # floodfill() is experimental # Test that floodfill does not extend into negative coordinates @@ -976,7 +978,7 @@ def create_base_image_draw( return img, ImageDraw.Draw(img) -def test_square(): +def test_square() -> None: expected = os.path.join(IMAGES_PATH, "square.png") img, draw = create_base_image_draw((10, 10)) draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) @@ -989,7 +991,7 @@ def test_square(): assert_image_equal_tofile(img, expected, "square as normal rectangle failed") -def test_triangle_right(): +def test_triangle_right() -> None: img, draw = create_base_image_draw((20, 20)) draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) assert_image_equal_tofile( @@ -1001,7 +1003,7 @@ def test_triangle_right(): "fill, suffix", ((BLACK, "width"), (None, "width_no_fill")), ) -def test_triangle_right_width(fill, suffix): +def test_triangle_right_width(fill, suffix) -> None: img, draw = create_base_image_draw((100, 100)) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) assert_image_equal_tofile( @@ -1009,7 +1011,7 @@ def test_triangle_right_width(fill, suffix): ) -def test_line_horizontal(): +def test_line_horizontal() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) assert_image_equal_tofile( @@ -1047,7 +1049,7 @@ def test_line_horizontal(): ) -def test_line_h_s1_w2(): +def test_line_h_s1_w2() -> None: pytest.skip("failing") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 6), BLACK, 2) @@ -1058,7 +1060,7 @@ def test_line_h_s1_w2(): ) -def test_line_vertical(): +def test_line_vertical() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 2) assert_image_equal_tofile( @@ -1104,7 +1106,7 @@ def test_line_vertical(): ) -def test_line_oblique_45(): +def test_line_oblique_45() -> None: expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 14), BLACK, 3) @@ -1126,7 +1128,7 @@ def test_line_oblique_45(): ) -def test_wide_line_dot(): +def test_wide_line_dot() -> None: # Test drawing a wide "line" from one point to another just draws a single point # Arrange im = Image.new("RGB", (W, H)) @@ -1139,7 +1141,7 @@ def test_wide_line_dot(): assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) -def test_wide_line_larger_than_int(): +def test_wide_line_larger_than_int() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1233,7 +1235,7 @@ def test_wide_line_larger_than_int(): ], ], ) -def test_line_joint(xy): +def test_line_joint(xy) -> None: im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) @@ -1244,7 +1246,7 @@ def test_line_joint(xy): assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # https://github.com/python-pillow/Pillow/issues/2783 # Arrange im = Image.new("RGB", (W, H)) @@ -1260,7 +1262,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_textbbox_stroke(): +def test_textbbox_stroke() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1274,7 +1276,7 @@ def test_textbbox_stroke(): @skip_unless_feature("freetype2") -def test_stroke(): +def test_stroke() -> None: for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): # Arrange im = Image.new("RGB", (120, 130)) @@ -1291,7 +1293,7 @@ def test_stroke(): @skip_unless_feature("freetype2") -def test_stroke_descender(): +def test_stroke_descender() -> None: # Arrange im = Image.new("RGB", (120, 130)) draw = ImageDraw.Draw(im) @@ -1305,7 +1307,7 @@ def test_stroke_descender(): @skip_unless_feature("freetype2") -def test_split_word(): +def test_split_word() -> None: # Arrange im = Image.new("RGB", (230, 55)) expected = im.copy() @@ -1326,7 +1328,7 @@ def test_split_word(): @skip_unless_feature("freetype2") -def test_stroke_multiline(): +def test_stroke_multiline() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1342,7 +1344,7 @@ def test_stroke_multiline(): @skip_unless_feature("freetype2") -def test_setting_default_font(): +def test_setting_default_font() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1359,7 +1361,7 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.load_default().__class__) -def test_default_font_size(): +def test_default_font_size() -> None: freetype_support = features.check_module("freetype2") text = "Default font at a specific size." @@ -1386,7 +1388,7 @@ def test_default_font_size(): @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox): +def test_same_color_outline(bbox) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1432,7 +1434,7 @@ def test_same_color_outline(bbox): (3, "triangle_width", {"width": 5, "outline": "yellow"}), ], ) -def test_draw_regular_polygon(n_sides, polygon_name, args): +def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) @@ -1469,7 +1471,7 @@ def test_draw_regular_polygon(n_sides, polygon_name, args): ), ], ) -def test_compute_regular_polygon_vertices(n_sides, expected_vertices): +def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None: bounding_circle = (W // 2, H // 2, 25) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @@ -1521,13 +1523,13 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices): ) def test_compute_regular_polygon_vertices_input_error_handling( n_sides, bounding_circle, rotation, expected_error, error_message -): +) -> None: with pytest.raises(expected_error) as e: ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) assert str(e.value) == error_message -def test_continuous_horizontal_edges_polygon(): +def test_continuous_horizontal_edges_polygon() -> None: xy = [ (2, 6), (6, 6), @@ -1546,7 +1548,7 @@ def test_continuous_horizontal_edges_polygon(): ) -def test_discontiguous_corners_polygon(): +def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) @@ -1558,7 +1560,7 @@ def test_discontiguous_corners_polygon(): assert_image_similar_tofile(img, expected, 1) -def test_polygon2(): +def test_polygon2() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") @@ -1567,7 +1569,7 @@ def test_polygon2(): @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy): +def test_incorrectly_ordered_coordinates(xy) -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 004c2d768fd..07a25b84b4e 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -43,7 +43,7 @@ FONT_PATH = "Tests/fonts/FreeMono.ttf" -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw2.Draw(im) @@ -56,7 +56,7 @@ def test_sanity(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox): +def test_ellipse(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -70,7 +70,7 @@ def test_ellipse(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -84,7 +84,7 @@ def test_ellipse_edge(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -98,7 +98,7 @@ def test_line(points): @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points): +def test_line_pen_as_brush(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -114,7 +114,7 @@ def test_line_pen_as_brush(points): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -129,7 +129,7 @@ def test_polygon(points): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -143,7 +143,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -160,7 +160,7 @@ def test_big_rectangle(): @skip_unless_feature("freetype2") -def test_text(): +def test_text() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -175,7 +175,7 @@ def test_text(): @skip_unless_feature("freetype2") -def test_textbbox(): +def test_textbbox() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -190,7 +190,7 @@ def test_textbbox(): @skip_unless_feature("freetype2") -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -206,7 +206,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_flush(): +def test_flush() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index e3d8a7ab251..9ce9cda8267 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -7,7 +7,7 @@ from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: # FIXME: assert_image # Implicit asserts no exception: ImageEnhance.Color(hopper()).enhance(0.5) @@ -16,7 +16,7 @@ def test_sanity(): ImageEnhance.Sharpness(hopper()).enhance(0.5) -def test_crash(): +def test_crash() -> None: # crashes on small images im = Image.new("RGB", (1, 1)) ImageEnhance.Sharpness(im).enhance(0.5) @@ -34,7 +34,7 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount): +def _check_alpha(im, original, op, amount) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -44,7 +44,7 @@ def _check_alpha(im, original, op, amount): @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op): +def test_alpha(op) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 99731f35208..49140978168 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,7 @@ class TestImageFile: - def test_parser(self): + def test_parser(self) -> None: def roundtrip(format): im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): @@ -84,7 +84,7 @@ def roundtrip(format): with pytest.raises(OSError): roundtrip("PDF") - def test_ico(self): + def test_ico(self) -> None: with open("Tests/images/python.ico", "rb") as f: data = f.read() with ImageFile.Parser() as p: @@ -93,7 +93,7 @@ def test_ico(self): @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_incremental_webp(self): + def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: p.feed(f.read(1024)) @@ -105,7 +105,7 @@ def test_incremental_webp(self): assert (128, 128) == p.image.size @skip_unless_feature("zlib") - def test_safeblock(self): + def test_safeblock(self) -> None: im1 = hopper() try: @@ -116,17 +116,17 @@ def test_safeblock(self): assert_image_equal(im1, im2) - def test_raise_oserror(self): + def test_raise_oserror(self) -> None: with pytest.warns(DeprecationWarning): with pytest.raises(OSError): ImageFile.raise_oserror(1) - def test_raise_typeerror(self): + def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() parser.feed(1) - def test_negative_stride(self): + def test_negative_stride(self) -> None: with open("Tests/images/raw_negative_stride.bin", "rb") as f: input = f.read() p = ImageFile.Parser() @@ -134,11 +134,11 @@ def test_negative_stride(self): with pytest.raises(OSError): p.close() - def test_no_format(self): + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) class DummyImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self._mode = "RGB" self._size = (1, 1) @@ -146,12 +146,12 @@ def _open(self): assert im.format is None assert im.get_format_mimetype() is None - def test_oserror(self): + def test_oserror(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(OSError): im.save(BytesIO(), "JPEG2000", num_resolutions=2) - def test_truncated(self): + def test_truncated(self) -> None: b = BytesIO( b"BM000000000000" # head_data + _binary.o32le( @@ -166,7 +166,7 @@ def test_truncated(self): assert str(e.value) == "Truncated File Read" @skip_unless_feature("zlib") - def test_truncated_with_errors(self): + def test_truncated_with_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: with pytest.raises(OSError): im.load() @@ -176,7 +176,7 @@ def test_truncated_with_errors(self): im.load() @skip_unless_feature("zlib") - def test_truncated_without_errors(self): + def test_truncated_without_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -185,13 +185,13 @@ def test_truncated_without_errors(self): ImageFile.LOAD_TRUNCATED_IMAGES = False @skip_unless_feature("zlib") - def test_broken_datastream_with_errors(self): + def test_broken_datastream_with_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: with pytest.raises(OSError): im.load() @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self): + def test_broken_datastream_without_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -210,7 +210,7 @@ class MockPyEncoder(ImageFile.PyEncoder): def encode(self, buffer): return 1, 1, b"" - def cleanup(self): + def cleanup(self) -> None: self.cleanup_called = True @@ -218,7 +218,7 @@ def cleanup(self): class MockImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self.rawmode = "RGBA" self._mode = "RGBA" self._size = (200, 200) @@ -227,7 +227,7 @@ def _open(self): class CodecsTest: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: cls.decoder = MockPyDecoder(None) cls.encoder = MockPyEncoder(None) @@ -244,7 +244,7 @@ def encoder_closure(mode, *args): class TestPyDecoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -259,7 +259,7 @@ def test_setimage(self): with pytest.raises(ValueError): self.decoder.set_as_raw(b"\x00") - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -272,7 +272,7 @@ def test_extents_none(self): assert self.decoder.state.xsize == 200 assert self.decoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -285,7 +285,7 @@ def test_negsize(self): with pytest.raises(ValueError): im.load() - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -298,14 +298,14 @@ def test_oversize(self): with pytest.raises(ValueError): im.load() - def test_decode(self): + def test_decode(self) -> None: decoder = ImageFile.PyDecoder(None) with pytest.raises(NotImplementedError): decoder.decode(None) class TestPyEncoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -320,7 +320,7 @@ def test_setimage(self): assert self.encoder.state.xsize == xsize assert self.encoder.state.ysize == ysize - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -334,7 +334,7 @@ def test_extents_none(self): assert self.encoder.state.xsize == 200 assert self.encoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -352,7 +352,7 @@ def test_negsize(self): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] ) - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -372,7 +372,7 @@ def test_oversize(self): [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], ) - def test_encode(self): + def test_encode(self) -> None: encoder = ImageFile.PyEncoder(None) with pytest.raises(NotImplementedError): encoder.encode(None) @@ -388,6 +388,6 @@ def test_encode(self): with pytest.raises(NotImplementedError): encoder.encode_to_file(None, None) - def test_zero_height(self): + def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/zero_height.j2k") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index d2c87d42aaa..909026dc8c6 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -31,7 +31,7 @@ pytestmark = skip_unless_feature("freetype2") -def test_sanity(): +def test_sanity() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) @@ -51,7 +51,7 @@ def font(layout_engine): return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font): +def test_font_properties(font) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -80,11 +80,11 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font): +def test_font_with_name(layout_engine, font) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine): +def test_font_with_filelike(layout_engine) -> None: def _font_as_bytes(): with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) @@ -102,12 +102,12 @@ def _font_as_bytes(): # _render(shared_bytes) -def test_font_with_open_file(layout_engine): +def test_font_with_open_file(layout_engine) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine): +def test_render_equal(layout_engine) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -116,7 +116,7 @@ def test_render_equal(layout_engine): assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path, layout_engine): +def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -126,7 +126,7 @@ def test_non_ascii_path(tmp_path, layout_engine): ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font): +def test_transparent_background(font) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -140,7 +140,7 @@ def test_transparent_background(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font): +def test_I16(font) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -153,7 +153,7 @@ def test_I16(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font): +def test_textbbox_equal(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -182,7 +182,7 @@ def test_textbbox_equal(font): ) def test_getlength( text, mode, fontname, size, layout_engine, length_basic, length_raqm -): +) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) im = Image.new(mode, (1, 1), 0) @@ -197,7 +197,7 @@ def test_getlength( assert length == length_raqm -def test_float_size(): +def test_float_size() -> None: lengths = [] for size in (48, 48.5, 49): f = ImageFont.truetype( @@ -207,7 +207,7 @@ def test_float_size(): assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font): +def test_render_multiline(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -223,7 +223,7 @@ def test_render_multiline(font): assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font): +def test_render_multiline_text(font) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -243,7 +243,7 @@ def test_render_multiline_text(font): @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext): +def test_render_multiline_text_align(font, align, ext) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -251,7 +251,7 @@ def test_render_multiline_text_align(font, align, ext): assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font): +def test_unknown_align(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -260,14 +260,14 @@ def test_unknown_align(font): draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font): +def test_draw_align(font) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font): +def test_multiline_bbox(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -285,7 +285,7 @@ def test_multiline_bbox(font): draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font): +def test_multiline_width(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -295,7 +295,7 @@ def test_multiline_width(font): ) -def test_multiline_spacing(font): +def test_multiline_spacing(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -306,7 +306,7 @@ def test_multiline_spacing(font): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation): +def test_rotated_transposed_font(font, orientation) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -347,7 +347,7 @@ def test_rotated_transposed_font(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation): +def test_unrotated_transposed_font(font, orientation) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -382,7 +382,7 @@ def test_unrotated_transposed_font(font, orientation): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation): +def test_rotated_transposed_font_get_mask(font, orientation) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -403,7 +403,7 @@ def test_rotated_transposed_font_get_mask(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation): +def test_unrotated_transposed_font_get_mask(font, orientation) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -415,11 +415,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation): assert mask.size == (108, 13) -def test_free_type_font_get_name(font): +def test_free_type_font_get_name(font) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font): +def test_free_type_font_get_metrics(font) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -427,7 +427,7 @@ def test_free_type_font_get_metrics(font): assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font): +def test_free_type_font_get_mask(font) -> None: # Arrange text = "mask this" @@ -438,7 +438,7 @@ def test_free_type_font_get_mask(font): assert mask.size == (108, 13) -def test_load_path_not_found(): +def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -449,13 +449,13 @@ def test_load_path_not_found(): ImageFont.truetype(filename) -def test_load_non_font_bytes(): +def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): ImageFont.truetype(f) -def test_default_font(): +def test_default_font() -> None: # Arrange txt = "This is a default font using FreeType support." im = Image.new(mode="RGB", size=(300, 100)) @@ -473,16 +473,16 @@ def test_default_font(): @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode): +def test_getbbox(font, mode) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font): +def test_getbbox_empty(font) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font): +def test_render_empty(font) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -492,7 +492,7 @@ def test_render_empty(font): assert_image_equal(im, target) -def test_unicode_extended(layout_engine): +def test_unicode_extended(layout_engine) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -515,8 +515,8 @@ def test_unicode_extended(layout_engine): (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory): - def _test_fake_loading_font(path_to_fake, fontname): +def test_find_font(monkeypatch, platform, font_directory) -> None: + def _test_fake_loading_font(path_to_fake, fontname) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: @@ -567,7 +567,7 @@ def fake_walker(path): _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font): +def test_imagefont_getters(font) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -588,7 +588,7 @@ def test_imagefont_getters(font): @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width): +def test_getsize_stroke(font, stroke_width) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -597,7 +597,7 @@ def test_getsize_stroke(font, stroke_width): ) -def test_complex_font_settings(): +def test_complex_font_settings() -> None: t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) with pytest.raises(KeyError): t.getmask("абвг", direction="rtl") @@ -607,7 +607,7 @@ def test_complex_font_settings(): t.getmask("абвг", language="sr") -def test_variation_get(font): +def test_variation_get(font) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -677,7 +677,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font): +def test_variation_set_by_name(font) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -702,7 +702,7 @@ def test_variation_set_by_name(font): _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font): +def test_variation_set_by_axes(font) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -737,7 +737,7 @@ def test_variation_set_by_axes(font): ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top): +def test_anchor(layout_engine, anchor, left, top) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -782,7 +782,7 @@ def test_anchor(layout_engine, anchor, left, top): ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align): +def test_anchor_multiline(layout_engine, anchor, align) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -800,7 +800,7 @@ def test_anchor_multiline(layout_engine, anchor, align): assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font): +def test_anchor_invalid(font) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -826,7 +826,7 @@ def test_anchor_invalid(font): @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp): +def test_bitmap_font(layout_engine, bpp) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -843,7 +843,7 @@ def test_bitmap_font(layout_engine, bpp): assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine): +def test_bitmap_font_stroke(layout_engine) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -861,7 +861,7 @@ def test_bitmap_font_stroke(layout_engine): @pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine, embedded_color): +def test_bitmap_blend(layout_engine, embedded_color) -> None: font = ImageFont.truetype( "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine ) @@ -873,7 +873,7 @@ def test_bitmap_blend(layout_engine, embedded_color): assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") -def test_standard_embedded_color(layout_engine): +def test_standard_embedded_color(layout_engine) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -908,7 +908,7 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine): +def test_cbdt(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -925,7 +925,7 @@ def test_cbdt(layout_engine): pytest.skip("freetype compiled without libpng or CBDT support") -def test_cbdt_mask(layout_engine): +def test_cbdt_mask(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -942,7 +942,7 @@ def test_cbdt_mask(layout_engine): pytest.skip("freetype compiled without libpng or CBDT support") -def test_sbix(layout_engine): +def test_sbix(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -959,7 +959,7 @@ def test_sbix(layout_engine): pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine): +def test_sbix_mask(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -977,7 +977,7 @@ def test_sbix_mask(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine): +def test_colr(layout_engine) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -993,7 +993,7 @@ def test_colr(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine): +def test_colr_mask(layout_engine) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -1008,7 +1008,7 @@ def test_colr_mask(layout_engine): assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine): +def test_woff2(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1027,7 +1027,7 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_render_mono_size(): +def test_render_mono_size() -> None: # issue 4177 im = Image.new("P", (100, 30), "white") @@ -1042,7 +1042,7 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font): +def test_too_many_characters(font) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1070,14 +1070,14 @@ def test_too_many_characters(font): "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file): +def test_oom(test_file) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch): +def test_raqm_missing_warning(monkeypatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( @@ -1091,6 +1091,6 @@ def test_raqm_missing_warning(monkeypatch): @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size): +def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 09e68ea488a..325e7ef216b 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -12,7 +12,7 @@ pytestmark = skip_unless_feature("raqm") -def test_english(): +def test_english() -> None: # smoke test, this should not fail ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -20,7 +20,7 @@ def test_english(): draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") -def test_complex_text(): +def test_complex_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -31,7 +31,7 @@ def test_complex_text(): assert_image_similar_tofile(im, target, 0.5) -def test_y_offset(): +def test_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -42,7 +42,7 @@ def test_y_offset(): assert_image_similar_tofile(im, target, 1.7) -def test_complex_unicode_text(): +def test_complex_unicode_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -62,7 +62,7 @@ def test_complex_unicode_text(): assert_image_similar_tofile(im, target, 2.33) -def test_text_direction_rtl(): +def test_text_direction_rtl() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -73,7 +73,7 @@ def test_text_direction_rtl(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ltr(): +def test_text_direction_ltr() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -84,7 +84,7 @@ def test_text_direction_ltr(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_rtl2(): +def test_text_direction_rtl2() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -95,7 +95,7 @@ def test_text_direction_rtl2(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ttb(): +def test_text_direction_ttb() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) im = Image.new(mode="RGB", size=(100, 300)) @@ -110,7 +110,7 @@ def test_text_direction_ttb(): assert_image_similar_tofile(im, target, 2.8) -def test_text_direction_ttb_stroke(): +def test_text_direction_ttb_stroke() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) im = Image.new(mode="RGB", size=(100, 300)) @@ -133,7 +133,7 @@ def test_text_direction_ttb_stroke(): assert_image_similar_tofile(im, target, 19.4) -def test_ligature_features(): +def test_ligature_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -146,7 +146,7 @@ def test_ligature_features(): assert liga_bbox == (0, 4, 13, 19) -def test_kerning_features(): +def test_kerning_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -157,7 +157,7 @@ def test_kerning_features(): assert_image_similar_tofile(im, target, 0.5) -def test_arabictext_features(): +def test_arabictext_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -174,7 +174,7 @@ def test_arabictext_features(): assert_image_similar_tofile(im, target, 0.5) -def test_x_max_and_y_offset(): +def test_x_max_and_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) im = Image.new(mode="RGB", size=(50, 100)) @@ -185,7 +185,7 @@ def test_x_max_and_y_offset(): assert_image_similar_tofile(im, target, 0.5) -def test_language(): +def test_language() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -208,7 +208,7 @@ def test_language(): ), ids=("None", "ltr", "rtl2", "rtl", "ttb"), ) -def test_getlength(mode, text, direction, expected): +def test_getlength(mode, text, direction, expected) -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) @@ -230,7 +230,7 @@ def test_getlength(mode, text, direction, expected): ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) -def test_getlength_combine(mode, direction, text): +def test_getlength_combine(mode, direction, text) -> None: if text == "i\u0305i" and direction == "ttb": pytest.skip("fails with this font") @@ -250,7 +250,7 @@ def test_getlength_combine(mode, direction, text): @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor): +def test_anchor_ttb(anchor) -> None: text = "f" path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) @@ -306,7 +306,7 @@ def test_anchor_ttb(anchor): @pytest.mark.parametrize( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) -def test_combine(name, text, dir, anchor, epsilon): +def test_combine(name, text, dir, anchor, epsilon) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -337,7 +337,7 @@ def test_combine(name, text, dir, anchor, epsilon): ("rm", "right"), # pass with getsize ), ) -def test_combine_multiline(anchor, align): +def test_combine_multiline(anchor, align) -> None: # test that multiline text uses getlength, not getsize or getbbox path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" @@ -355,7 +355,7 @@ def test_combine_multiline(anchor, align): assert_image_similar_tofile(im, path, 0.015) -def test_anchor_invalid_ttb(): +def test_anchor_invalid_ttb() -> None: font = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index be4be1c546e..3b1c14b4e18 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -12,16 +12,16 @@ original_core = ImageFont.core -def setup_module(): +def setup_module() -> None: if features.check_module("freetype2"): ImageFont.core = _util.DeferredError(ImportError) -def teardown_module(): +def teardown_module() -> None: ImageFont.core = original_core -def test_default_font(): +def test_default_font() -> None: # Arrange txt = 'This is a "better than nothing" default font.' im = Image.new(mode="RGB", size=(300, 100)) @@ -35,12 +35,12 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") -def test_size_without_freetype(): +def test_size_without_freetype() -> None: with pytest.raises(ImportError): ImageFont.load_default(size=14) -def test_unicode(): +def test_unicode() -> None: # should not segfault, should return UnicodeDecodeError # issue #2826 font = ImageFont.load_default() @@ -48,7 +48,7 @@ def test_unicode(): font.getbbox("’") -def test_textbbox(): +def test_textbbox() -> None: im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() @@ -56,7 +56,7 @@ def test_textbbox(): assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) -def test_decompression_bomb(): +def test_decompression_bomb() -> None: glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) @@ -67,7 +67,7 @@ def test_decompression_bomb(): @pytest.mark.timeout(4) -def test_oom(): +def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 ) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 9d3d40398f6..40c1d323e77 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -20,7 +20,7 @@ class TestImageGrab: @pytest.mark.skipif( sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) - def test_grab(self): + def test_grab(self) -> None: ImageGrab.grab() ImageGrab.grab(include_layered_windows=True) ImageGrab.grab(all_screens=True) @@ -29,7 +29,7 @@ def test_grab(self): assert im.size == (40, 60) @skip_unless_feature("xcb") - def test_grab_x11(self): + def test_grab_x11(self) -> None: try: if sys.platform not in ("win32", "darwin"): ImageGrab.grab() @@ -39,7 +39,7 @@ def test_grab_x11(self): pytest.skip(str(e)) @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") - def test_grab_no_xcb(self): + def test_grab_no_xcb(self) -> None: if sys.platform not in ("win32", "darwin") and not shutil.which( "gnome-screenshot" ): @@ -52,12 +52,12 @@ def test_grab_no_xcb(self): assert str(e.value).startswith("Pillow was built without XCB support") @skip_unless_feature("xcb") - def test_grab_invalid_xdisplay(self): + def test_grab_invalid_xdisplay(self) -> None: with pytest.raises(OSError) as e: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") - def test_grabclipboard(self): + def test_grabclipboard(self) -> None: if sys.platform == "darwin": subprocess.call(["screencapture", "-cx"]) elif sys.platform == "win32": @@ -82,7 +82,7 @@ def test_grabclipboard(self): ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_file(self): + def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -92,7 +92,7 @@ def test_grabclipboard_file(self): assert os.path.samefile(im[0], "Tests/images/hopper.gif") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_png(self): + def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") @@ -113,7 +113,7 @@ def test_grabclipboard_png(self): reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext): + def test_grabclipboard_wl_clipboard(self, ext) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 622ad27eacb..ea6e80f1ee7 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -24,7 +24,7 @@ def pixel(im): images = {"A": A, "B": B, "F": F, "I": I} -def test_sanity(): +def test_sanity() -> None: assert ImageMath.eval("1") == 1 assert ImageMath.eval("1+A", A=2) == 3 assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3" @@ -33,7 +33,7 @@ def test_sanity(): assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3" -def test_ops(): +def test_ops() -> None: assert pixel(ImageMath.eval("-A", images)) == "I -1" assert pixel(ImageMath.eval("+B", images)) == "L 2" @@ -60,51 +60,51 @@ def test_ops(): "(lambda: (lambda: exec('pass'))())()", ), ) -def test_prevent_exec(expression): +def test_prevent_exec(expression) -> None: with pytest.raises(ValueError): ImageMath.eval(expression) -def test_prevent_double_underscores(): +def test_prevent_double_underscores() -> None: with pytest.raises(ValueError): ImageMath.eval("1", {"__": None}) -def test_prevent_builtins(): +def test_prevent_builtins() -> None: with pytest.raises(ValueError): ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None}) -def test_logical(): +def test_logical() -> None: assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" assert pixel(ImageMath.eval("A or B", images)) == "L 1" -def test_convert(): +def test_convert() -> None: assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3" assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0" assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" -def test_compare(): +def test_compare() -> None: assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1" assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2" assert pixel(ImageMath.eval("A == 1", images)) == "I 1" assert pixel(ImageMath.eval("A == 2", images)) == "I 0" -def test_one_image_larger(): +def test_one_image_larger() -> None: assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3" assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3" -def test_abs(): +def test_abs() -> None: assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" -def test_binary_mod(): +def test_binary_mod() -> None: assert pixel(ImageMath.eval("A%A", A=A)) == "I 0" assert pixel(ImageMath.eval("B%B", B=B)) == "I 0" assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1" @@ -113,90 +113,90 @@ def test_binary_mod(): assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0" -def test_bitwise_invert(): +def test_bitwise_invert() -> None: assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1" assert pixel(ImageMath.eval("~A", A=A)) == "I -2" assert pixel(ImageMath.eval("~B", B=B)) == "I -3" -def test_bitwise_and(): +def test_bitwise_and() -> None: assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1" -def test_bitwise_or(): +def test_bitwise_or() -> None: assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1" -def test_bitwise_xor(): +def test_bitwise_xor() -> None: assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0" -def test_bitwise_leftshift(): +def test_bitwise_leftshift() -> None: assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1" assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2" -def test_bitwise_rightshift(): +def test_bitwise_rightshift() -> None: assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1" assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0" -def test_logical_eq(): +def test_logical_eq() -> None: assert pixel(ImageMath.eval("A==A", A=A)) == "I 1" assert pixel(ImageMath.eval("B==B", B=B)) == "I 1" assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0" -def test_logical_ne(): +def test_logical_ne() -> None: assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0" assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0" assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1" -def test_logical_lt(): +def test_logical_lt() -> None: assert pixel(ImageMath.eval("A None: assert pixel(ImageMath.eval("A<=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B<=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A<=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B<=A", A=A, B=B)) == "I 0" -def test_logical_gt(): +def test_logical_gt() -> None: assert pixel(ImageMath.eval("A>A", A=A)) == "I 0" assert pixel(ImageMath.eval("B>B", B=B)) == "I 0" assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1" -def test_logical_ge(): +def test_logical_ge() -> None: assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1" -def test_logical_equal(): +def test_logical_equal() -> None: assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1" assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1" assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1" @@ -205,7 +205,7 @@ def test_logical_equal(): assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0" -def test_logical_not_equal(): +def test_logical_not_equal() -> None: assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0" assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0" assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 0708ee63905..0b0c6d2d31a 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,6 +1,8 @@ # Test the ImageMorphology functionality from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageMorph, _imagingmorph @@ -50,18 +52,18 @@ def img_string_normalize(im): return img_to_string(string_to_img(im)) -def assert_img_equal_img_string(a, b_string): +def assert_img_equal_img_string(a, b_string) -> None: assert img_to_string(a) == img_string_normalize(b_string) -def test_str_to_img(): +def test_str_to_img() -> None: assert_image_equal_tofile(A, "Tests/images/morph_a.png") @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) -def test_lut(op): +def test_lut(op) -> None: lb = ImageMorph.LutBuilder(op_name=op) assert lb.get_lut() is None @@ -70,7 +72,7 @@ def test_lut(op): assert lut == bytearray(f.read()) -def test_no_operator_loaded(): +def test_no_operator_loaded() -> None: mop = ImageMorph.MorphOp() with pytest.raises(Exception) as e: mop.apply(None) @@ -84,7 +86,7 @@ def test_no_operator_loaded(): # Test the named patterns -def test_erosion8(): +def test_erosion8() -> None: # erosion8 mop = ImageMorph.MorphOp(op_name="erosion8") count, Aout = mop.apply(A) @@ -103,7 +105,7 @@ def test_erosion8(): ) -def test_dialation8(): +def test_dialation8() -> None: # dialation8 mop = ImageMorph.MorphOp(op_name="dilation8") count, Aout = mop.apply(A) @@ -122,7 +124,7 @@ def test_dialation8(): ) -def test_erosion4(): +def test_erosion4() -> None: # erosion4 mop = ImageMorph.MorphOp(op_name="dilation4") count, Aout = mop.apply(A) @@ -141,7 +143,7 @@ def test_erosion4(): ) -def test_edge(): +def test_edge() -> None: # edge mop = ImageMorph.MorphOp(op_name="edge") count, Aout = mop.apply(A) @@ -160,7 +162,7 @@ def test_edge(): ) -def test_corner(): +def test_corner() -> None: # Create a corner detector pattern mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -188,7 +190,7 @@ def test_corner(): assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) -def test_mirroring(): +def test_mirroring() -> None: # Test 'M' for mirroring mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -207,7 +209,7 @@ def test_mirroring(): ) -def test_negate(): +def test_negate() -> None: # Test 'N' for negate mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -226,7 +228,7 @@ def test_negate(): ) -def test_incorrect_mode(): +def test_incorrect_mode() -> None: im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") @@ -241,7 +243,7 @@ def test_incorrect_mode(): assert str(e.value) == "Image mode must be L" -def test_add_patterns(): +def test_add_patterns() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] @@ -259,12 +261,12 @@ def test_add_patterns(): ] -def test_unknown_pattern(): +def test_unknown_pattern() -> None: with pytest.raises(Exception): ImageMorph.LutBuilder(op_name="unknown") -def test_pattern_syntax_error(): +def test_pattern_syntax_error() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") new_patterns = ["a pattern with a syntax error"] @@ -276,7 +278,7 @@ def test_pattern_syntax_error(): assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' -def test_load_invalid_mrl(): +def test_load_invalid_mrl() -> None: # Arrange invalid_mrl = "Tests/images/hopper.png" mop = ImageMorph.MorphOp() @@ -287,7 +289,7 @@ def test_load_invalid_mrl(): assert str(e.value) == "Wrong size operator file!" -def test_roundtrip_mrl(tmp_path): +def test_roundtrip_mrl(tmp_path: Path) -> None: # Arrange tempfile = str(tmp_path / "temp.mrl") mop = ImageMorph.MorphOp(op_name="corner") @@ -301,7 +303,7 @@ def test_roundtrip_mrl(tmp_path): assert mop.lut == initial_lut -def test_set_lut(): +def test_set_lut() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") lut = lb.build_lut() @@ -314,7 +316,7 @@ def test_set_lut(): assert mop.lut == lut -def test_wrong_mode(): +def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() imrgb = Image.new("RGB", (10, 10)) iml = Image.new("L", (10, 10)) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 636b99dbe8f..50bf404aebe 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -22,7 +22,7 @@ def getmesh(self, im): deformer = Deformer() -def test_sanity(): +def test_sanity() -> None: ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -84,7 +84,7 @@ def test_sanity(): ImageOps.exif_transpose(hopper("RGB")) -def test_1pxfit(): +def test_1pxfit() -> None: # Division by zero in equalize if image is 1 pixel high newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) assert newimg.size == (35, 35) @@ -96,7 +96,7 @@ def test_1pxfit(): assert newimg.size == (35, 35) -def test_fit_same_ratio(): +def test_fit_same_ratio() -> None: # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 # If the ratios are not acknowledged to be the same, # and Pillow attempts to adjust the width to @@ -108,13 +108,13 @@ def test_fit_same_ratio(): @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size): +def test_contain(new_size) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) -def test_contain_round(): +def test_contain_round() -> None: im = Image.new("1", (43, 63), 1) new_im = ImageOps.contain(im, (5, 7)) assert new_im.width == 5 @@ -132,13 +132,13 @@ def test_contain_round(): ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size): +def test_cover(image_name, expected_size) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size -def test_pad(): +def test_pad() -> None: # Same ratio im = hopper() new_size = (im.width * 2, im.height * 2) @@ -158,7 +158,7 @@ def test_pad(): ) -def test_pad_round(): +def test_pad_round() -> None: im = Image.new("1", (1, 1), 1) new_im = ImageOps.pad(im, (4, 1)) assert new_im.load()[2, 0] == 1 @@ -168,7 +168,7 @@ def test_pad_round(): @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode): +def test_palette(mode) -> None: im = hopper(mode) # Expand @@ -182,7 +182,7 @@ def test_palette(mode): ) -def test_pil163(): +def test_pil163() -> None: # Division by zero in equalize if < 255 pixels in image (@PIL163) i = hopper("RGB").resize((15, 16)) @@ -192,7 +192,7 @@ def test_pil163(): ImageOps.equalize(i.convert("RGB")) -def test_scale(): +def test_scale() -> None: # Test the scaling function i = hopper("L").resize((50, 50)) @@ -210,7 +210,7 @@ def test_scale(): @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border): +def test_expand_palette(border) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -236,7 +236,7 @@ def test_expand_palette(border): assert_image_equal(im_cropped, im) -def test_colorize_2color(): +def test_colorize_2color() -> None: # Test the colorizing function with 2-color functionality # Open test image (256px by 10px, black to white) @@ -270,7 +270,7 @@ def test_colorize_2color(): ) -def test_colorize_2color_offset(): +def test_colorize_2color_offset() -> None: # Test the colorizing function with 2-color functionality and offset # Open test image (256px by 10px, black to white) @@ -306,7 +306,7 @@ def test_colorize_2color_offset(): ) -def test_colorize_3color_offset(): +def test_colorize_3color_offset() -> None: # Test the colorizing function with 3-color functionality and offset # Open test image (256px by 10px, black to white) @@ -359,14 +359,14 @@ def test_colorize_3color_offset(): ) -def test_exif_transpose(): +def test_exif_transpose() -> None: exts = [".jpg"] if features.check("webp") and features.check("webp_anim"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im): + def check(orientation_im) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -423,7 +423,7 @@ def check(orientation_im): assert 0x0112 not in transposed_im.getexif() -def test_exif_transpose_in_place(): +def test_exif_transpose_in_place() -> None: with Image.open("Tests/images/orientation_rectangle.jpg") as im: assert im.size == (2, 1) assert im.getexif()[0x0112] == 8 @@ -435,13 +435,13 @@ def test_exif_transpose_in_place(): assert_image_equal(im, expected) -def test_autocontrast_unsupported_mode(): +def test_autocontrast_unsupported_mode() -> None: im = Image.new("RGBA", (1, 1)) with pytest.raises(OSError): ImageOps.autocontrast(im) -def test_autocontrast_cutoff(): +def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: @@ -452,7 +452,7 @@ def autocontrast(cutoff): assert autocontrast(10) != autocontrast((1, 10)) -def test_autocontrast_mask_toy_input(): +def test_autocontrast_mask_toy_input() -> None: # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: rect_mask = Image.new("L", img.size, 0) @@ -471,7 +471,7 @@ def test_autocontrast_mask_toy_input(): assert ImageStat.Stat(result_nomask).median == [128] -def test_autocontrast_mask_real_input(): +def test_autocontrast_mask_real_input() -> None: # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: rect_mask = Image.new("L", img.size, 0) @@ -498,7 +498,7 @@ def test_autocontrast_mask_real_input(): ) -def test_autocontrast_preserve_tone(): +def test_autocontrast_preserve_tone() -> None: def autocontrast(mode, preserve_tone): im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -507,7 +507,7 @@ def autocontrast(mode, preserve_tone): assert autocontrast("L", True) == autocontrast("L", False) -def test_autocontrast_preserve_gradient(): +def test_autocontrast_preserve_gradient() -> None: gradient = Image.linear_gradient("L") # test with a grayscale gradient that extends to 0,255. @@ -533,7 +533,7 @@ def test_autocontrast_preserve_gradient(): @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color): +def test_autocontrast_preserve_one_color(color) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 8ffb9bff7a2..03302e20f2a 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -18,7 +18,7 @@ def test_images(): im.close() -def test_filter_api(test_images): +def test_filter_api(test_images) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -32,7 +32,7 @@ def test_filter_api(test_images): assert i.size == (128, 128) -def test_usm_formats(test_images): +def test_usm_formats(test_images) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -50,7 +50,7 @@ def test_usm_formats(test_images): im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images): +def test_blur_formats(test_images) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -68,7 +68,7 @@ def test_blur_formats(test_images): im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images): +def test_usm_accuracy(test_images) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -77,7 +77,7 @@ def test_usm_accuracy(test_images): assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images): +def test_blur_accuracy(test_images) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index be21464b4ae..545229500a6 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImagePalette @@ -7,19 +9,19 @@ from .helper import assert_image_equal, assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 -def test_reload(): +def test_reload() -> None: with Image.open("Tests/images/hopper.gif") as im: original = im.copy() im.palette.dirty = 1 assert_image_equal(im.convert("RGB"), original.convert("RGB")) -def test_getcolor(): +def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -46,7 +48,7 @@ def test_getcolor(): palette.getcolor("unknown") -def test_getcolor_rgba_color_rgb_palette(): +def test_getcolor_rgba_color_rgb_palette() -> None: palette = ImagePalette.ImagePalette("RGB") # Opaque RGBA colors are converted @@ -65,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette(): (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), ], ) -def test_getcolor_not_special(index, palette): +def test_getcolor_not_special(index, palette) -> None: im = Image.new("P", (1, 1)) # Do not use transparency index as a new color @@ -79,7 +81,7 @@ def test_getcolor_not_special(index, palette): assert index2 not in (index, index1) -def test_file(tmp_path): +def test_file(tmp_path: Path) -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") @@ -97,7 +99,7 @@ def test_file(tmp_path): assert p.palette == palette.tobytes() -def test_make_linear_lut(): +def test_make_linear_lut() -> None: # Arrange black = 0 white = 255 @@ -113,7 +115,7 @@ def test_make_linear_lut(): assert lut[i] == i -def test_make_linear_lut_not_yet_implemented(): +def test_make_linear_lut_not_yet_implemented() -> None: # Update after FIXME # Arrange black = 1 @@ -124,7 +126,7 @@ def test_make_linear_lut_not_yet_implemented(): ImagePalette.make_linear_lut(black, white) -def test_make_gamma_lut(): +def test_make_gamma_lut() -> None: # Arrange exp = 5 @@ -142,7 +144,7 @@ def test_make_gamma_lut(): assert lut[255] == 255 -def test_rawmode_valueerrors(tmp_path): +def test_rawmode_valueerrors(tmp_path: Path) -> None: # Arrange palette = ImagePalette.raw("RGB", list(range(256)) * 3) @@ -156,7 +158,7 @@ def test_rawmode_valueerrors(tmp_path): palette.save(f) -def test_getdata(): +def test_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.ImagePalette("RGB", data_in) @@ -168,7 +170,7 @@ def test_getdata(): assert mode == "RGB" -def test_rawmode_getdata(): +def test_rawmode_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.raw("RGB", data_in) @@ -181,7 +183,7 @@ def test_rawmode_getdata(): assert data_in == data_out -def test_2bit_palette(tmp_path): +def test_2bit_palette(tmp_path: Path) -> None: # issue #2258, 2 bit palettes are corrupted. outfile = str(tmp_path / "temp.png") @@ -193,6 +195,6 @@ def test_2bit_palette(tmp_path): assert_image_equal_tofile(img, outfile) -def test_invalid_palette(): +def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 5c6393e237f..8ba745f215d 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -9,7 +9,7 @@ from PIL import Image, ImagePath -def test_path(): +def test_path() -> None: p = ImagePath.Path(list(range(10))) # sequence interface @@ -57,7 +57,7 @@ def test_path(): ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords): +def test_path_constructors(coords) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -75,7 +75,7 @@ def test_path_constructors(coords): [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords): +def test_invalid_path_constructors(coords) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -93,7 +93,7 @@ def test_invalid_path_constructors(coords): [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords): +def test_path_odd_number_of_coordinates(coords) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -111,7 +111,7 @@ def test_path_odd_number_of_coordinates(coords): (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected): +def test_getbbox(coords, expected) -> None: # Arrange p = ImagePath.Path(coords) @@ -119,7 +119,7 @@ def test_getbbox(coords, expected): assert p.getbbox() == expected -def test_getbbox_no_args(): +def test_getbbox_no_args() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) @@ -135,7 +135,7 @@ def test_getbbox_no_args(): (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected): +def test_map(coords, expected) -> None: # Arrange p = ImagePath.Path(coords) @@ -147,7 +147,7 @@ def test_map(coords, expected): assert list(p) == expected -def test_transform(): +def test_transform() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -165,7 +165,7 @@ def test_transform(): ] -def test_transform_with_wrap(): +def test_transform_with_wrap() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -184,7 +184,7 @@ def test_transform_with_wrap(): ] -def test_overflow_segfault(): +def test_overflow_segfault() -> None: # Some Pythons fail getting the argument as an integer, and it falls # through to the sequence. Seeing this on 32-bit Windows. with pytest.raises((TypeError, MemoryError)): @@ -198,12 +198,12 @@ def test_overflow_segfault(): class Evil: - def __init__(self): + def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) def __getitem__(self, i): x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x): + def __setitem__(self, i, x) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index d55d980d9be..909f9716700 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -16,7 +16,7 @@ from PIL.ImageQt import qRgba -def test_rgb(): +def test_rgb() -> None: # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, @@ -28,7 +28,7 @@ def test_rgb(): assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b): + def checkrgb(r, g, b) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r @@ -41,7 +41,7 @@ def checkrgb(r, g, b): checkrgb(0, 0, 255) -def test_image(): +def test_image() -> None: modes = ["1", "RGB", "RGBA", "L", "P"] qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ @@ -55,6 +55,6 @@ def test_image(): assert_image_similar(roundtripped_im, im, 1) -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 66d553bcbc7..7280dded01f 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageSequence, TiffImagePlugin @@ -7,7 +9,7 @@ from .helper import assert_image_equal, hopper, skip_unless_feature -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.im") im = hopper("RGB") @@ -27,7 +29,7 @@ def test_sanity(tmp_path): ImageSequence.Iterator(0) -def test_iterator(): +def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: i = ImageSequence.Iterator(im) for index in range(0, im.n_frames): @@ -38,14 +40,14 @@ def test_iterator(): next(i) -def test_iterator_min_frame(): +def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) -def _test_multipage_tiff(): +def _test_multipage_tiff() -> None: with Image.open("Tests/images/multipage.tiff") as im: for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() @@ -53,18 +55,18 @@ def _test_multipage_tiff(): frame.convert("RGB") -def test_tiff(): +def test_tiff() -> None: _test_multipage_tiff() @skip_unless_feature("libtiff") -def test_libtiff(): +def test_libtiff() -> None: TiffImagePlugin.READ_LIBTIFF = True _test_multipage_tiff() TiffImagePlugin.READ_LIBTIFF = False -def test_consecutive(): +def test_consecutive() -> None: with Image.open("Tests/images/multipage.tiff") as im: first_frame = None for frame in ImageSequence.Iterator(im): @@ -75,7 +77,7 @@ def test_consecutive(): break -def test_palette_mmap(): +def test_palette_mmap() -> None: # Using mmap in ImageFile can require to reload the palette. with Image.open("Tests/images/multipage-mmap.tiff") as im: color1 = im.getpalette()[:3] @@ -84,7 +86,7 @@ def test_palette_mmap(): assert color1 == color2 -def test_all_frames(): +def test_all_frames() -> None: # Test a single image with Image.open("Tests/images/iss634.gif") as im: ims = ImageSequence.all_frames(im) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 0996ad41d4d..f7269d45b56 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -7,12 +7,12 @@ from .helper import hopper, is_win32, on_ci -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ImageShow) -def test_register(): +def test_register() -> None: # Test registering a viewer that is not a class ImageShow.register("not a class") @@ -24,9 +24,9 @@ def test_register(): "order", [-1, 0], ) -def test_viewer_show(order): +def test_viewer_show(order) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options): + def show_image(self, image, **options) -> bool: self.methodCalled = True return True @@ -48,12 +48,12 @@ def show_image(self, image, **options): reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode): +def test_show(mode) -> None: im = hopper(mode) assert ImageShow.show(im) -def test_show_without_viewers(): +def test_show_without_viewers() -> None: viewers = ImageShow._viewers ImageShow._viewers = [] @@ -63,7 +63,7 @@ def test_show_without_viewers(): ImageShow._viewers = viewers -def test_viewer(): +def test_viewer() -> None: viewer = ImageShow.Viewer() assert viewer.get_format(None) is None @@ -73,14 +73,14 @@ def test_viewer(): @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer): +def test_viewers(viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: pass -def test_ipythonviewer(): +def test_ipythonviewer() -> None: pytest.importorskip("IPython", reason="IPython not installed") for viewer in ImageShow._viewers: if isinstance(viewer, ImageShow.IPythonViewer): diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 01687db353e..b1c1306c1ec 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -7,7 +7,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() st = ImageStat.Stat(im) @@ -31,7 +31,7 @@ def test_sanity(): ImageStat.Stat(1) -def test_hopper(): +def test_hopper() -> None: im = hopper() st = ImageStat.Stat(im) @@ -44,7 +44,7 @@ def test_hopper(): assert st.sum[2] == 1563008 -def test_constant(): +def test_constant() -> None: im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index c06fc58235b..a216bd21de2 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -23,7 +23,7 @@ pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") -def setup_module(): +def setup_module() -> None: try: # setup tk tk.Frame() @@ -34,7 +34,7 @@ def setup_module(): pytest.skip(f"TCL Error: {v}") -def test_kw(): +def test_kw() -> None: TEST_JPG = "Tests/images/hopper.jpg" TEST_PNG = "Tests/images/hopper.png" with Image.open(TEST_JPG) as im1: @@ -57,7 +57,7 @@ def test_kw(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode): +def test_photoimage(mode) -> None: # test as image: im = hopper(mode) @@ -71,7 +71,7 @@ def test_photoimage(mode): assert_image_equal(reloaded, im.convert("RGBA")) -def test_photoimage_apply_transparency(): +def test_photoimage_apply_transparency() -> None: with Image.open("Tests/images/pil123p.png") as im: im_tk = ImageTk.PhotoImage(im) reloaded = ImageTk.getimage(im_tk) @@ -79,7 +79,7 @@ def test_photoimage_apply_transparency(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode): +def test_photoimage_blank(mode) -> None: # test a image using mode/size: im_tk = ImageTk.PhotoImage(mode, (100, 100)) @@ -91,7 +91,7 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_bitmapimage(): +def test_bitmapimage() -> None: im = hopper("1") # this should not crash diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index f93eabcb4ff..b43c31b521f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -8,10 +8,10 @@ class TestImageWin: - def test_sanity(self): + def test_sanity(self) -> None: dir(ImageWin) - def test_hdc(self): + def test_hdc(self) -> None: # Arrange dc = 50 @@ -22,7 +22,7 @@ def test_hdc(self): # Assert assert dc2 == 50 - def test_hwnd(self): + def test_hwnd(self) -> None: # Arrange wnd = 50 @@ -36,7 +36,7 @@ def test_hwnd(self): @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestImageWinDib: - def test_dib_image(self): + def test_dib_image(self) -> None: # Arrange im = hopper() @@ -46,7 +46,7 @@ def test_dib_image(self): # Assert assert dib.size == im.size - def test_dib_mode_string(self): + def test_dib_mode_string(self) -> None: # Arrange mode = "RGBA" size = (128, 128) @@ -57,7 +57,7 @@ def test_dib_mode_string(self): # Assert assert dib.size == (128, 128) - def test_dib_paste(self): + def test_dib_paste(self) -> None: # Arrange im = hopper() @@ -71,7 +71,7 @@ def test_dib_paste(self): # Assert assert dib.size == (128, 128) - def test_dib_paste_bbox(self): + def test_dib_paste_bbox(self) -> None: # Arrange im = hopper() bbox = (0, 0, 10, 10) @@ -86,7 +86,7 @@ def test_dib_paste_bbox(self): # Assert assert dib.size == (128, 128) - def test_dib_frombytes_tobytes_roundtrip(self): + def test_dib_frombytes_tobytes_roundtrip(self) -> None: # Arrange # Make two different DIB images im = hopper() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index 63d6b903ca6..c7f633e6290 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path from PIL import Image, ImageWin @@ -83,7 +84,7 @@ def serialize_dib(bi, pixels): memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) return bytearray(buf) - def test_pointer(tmp_path): + def test_pointer(tmp_path: Path) -> None: im = hopper() (width, height) = im.size opath = str(tmp_path / "temp.png") diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index e2024abbf2e..c8d6d33d223 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -10,7 +10,7 @@ class TestLibPack: - def assert_pack(self, mode, rawmode, data, *pixels): + def assert_pack(self, mode, rawmode, data, *pixels) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -24,7 +24,7 @@ def assert_pack(self, mode, rawmode, data, *pixels): assert data == im.tobytes("raw", rawmode) - def test_1(self): + def test_1(self) -> None: self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -37,29 +37,29 @@ def test_1(self): self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) - def test_L(self): + def test_L(self) -> None: self.assert_pack("L", "L", 1, 1, 2, 3, 4) self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) self.assert_pack("P", "P", 1, 1, 2, 3, 4) - def test_PA(self): + def test_PA(self) -> None: self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack( "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) @@ -79,7 +79,7 @@ def test_RGB(self): self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -101,12 +101,12 @@ def test_RGBA(self): self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -134,7 +134,7 @@ def test_RGBX(self): self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "CMYK", @@ -149,7 +149,7 @@ def test_CMYK(self): ) self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_pack( @@ -172,19 +172,19 @@ def test_YCbCr(self): self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) self.assert_pack( "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 @@ -209,10 +209,10 @@ def test_I(self): 0x01000083, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) if sys.byteorder == "little": @@ -228,7 +228,7 @@ def test_F_float(self): class TestLibUnpack: - def assert_unpack(self, mode, rawmode, data, *pixels): + def assert_unpack(self, mode, rawmode, data, *pixels) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -241,7 +241,7 @@ def assert_unpack(self, mode, rawmode, data, *pixels): for x, pixel in enumerate(pixels): assert pixel == im.getpixel((x, 0)) - def test_1(self): + def test_1(self) -> None: self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -254,7 +254,7 @@ def test_1(self): self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) - def test_L(self): + def test_L(self) -> None: self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) @@ -273,14 +273,14 @@ def test_L(self): self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) # erroneous? @@ -291,11 +291,11 @@ def test_P(self): self.assert_unpack("P", "P", 1, 1, 2, 3, 4) self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) - def test_PA(self): + def test_PA(self) -> None: self.assert_unpack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) @@ -346,14 +346,14 @@ def test_RGB(self): "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) ) - def test_BGR(self): + def test_BGR(self) -> None: self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) self.assert_unpack( "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) ) self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) @@ -522,7 +522,7 @@ def test_RGBA(self): "RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5) ) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_unpack( "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -536,7 +536,7 @@ def test_RGBa(self): "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) ) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) @@ -581,7 +581,7 @@ def test_RGBX(self): self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_unpack( "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -619,25 +619,25 @@ def test_CMYK(self): "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) ) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) @@ -678,7 +678,7 @@ def test_I(self): 0x01000083, ) - def test_F_int(self): + def test_F_int(self) -> None: self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) @@ -717,7 +717,7 @@ def test_F_int(self): 16777348, ) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_unpack( "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 ) @@ -768,7 +768,7 @@ def test_F_float(self): -1234.5, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) @@ -785,7 +785,7 @@ def test_I16(self): self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) - def test_CMYK16(self): + def test_CMYK16(self) -> None: self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) if sys.byteorder == "little": @@ -793,7 +793,7 @@ def test_CMYK16(self): else: self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - def test_value_error(self): + def test_value_error(self) -> None: with pytest.raises(ValueError): self.assert_unpack("L", "L", 0, 0) with pytest.raises(ValueError): diff --git a/Tests/test_map.py b/Tests/test_map.py index 9c79fe35906..93140f6e5a5 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -7,7 +7,7 @@ from PIL import Image -def test_overflow(): +def test_overflow() -> None: # There is the potential to overflow comparisons in map.c # if there are > SIZE_MAX bytes in the image or if # the file encodes an offset that makes @@ -25,7 +25,7 @@ def test_overflow(): Image.MAX_IMAGE_PIXELS = max_pixels -def test_tobytes(): +def test_tobytes() -> None: # Note that this image triggers the decompression bomb warning: max_pixels = Image.MAX_IMAGE_PIXELS Image.MAX_IMAGE_PIXELS = None @@ -39,7 +39,7 @@ def test_tobytes(): @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") -def test_ysize(): +def test_ysize() -> None: numpy = pytest.importorskip("numpy", reason="NumPy not installed") # Should not raise 'Integer overflow in ysize' diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index d3ee511b7c2..f2540bb465e 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -9,7 +11,7 @@ original = hopper().resize((32, 32)).convert("I") -def verify(im1): +def verify(im1) -> None: im2 = original.copy() assert im1.size == im2.size pix1 = im1.load() @@ -25,7 +27,7 @@ def verify(im1): @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path, mode): +def test_basic(tmp_path: Path, mode) -> None: # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -75,7 +77,7 @@ def test_basic(tmp_path, mode): assert im_in.getpixel((0, 0)) == min(512, maximum) -def test_tobytes(): +def test_tobytes() -> None: def tobytes(mode): return Image.new(mode, (1, 1), 1).tobytes() @@ -87,7 +89,7 @@ def tobytes(mode): assert tobytes("I") == b"\x01\x00\x00\x00"[::order] -def test_convert(): +def test_convert() -> None: im = original.copy() for mode in ("I;16", "I;16B", "I;16N"): diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 24dff36a610..6ba95c2d700 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -13,8 +13,8 @@ TEST_IMAGE_SIZE = (10, 10) -def test_numpy_to_image(): - def to_image(dtype, bands=1, boolean=0): +def test_numpy_to_image() -> None: + def to_image(dtype, bands: int = 1, boolean: int = 0): if bands == 1: if boolean: data = [0, 255] * 50 @@ -82,7 +82,7 @@ def to_image(dtype, bands=1, boolean=0): # Based on an erring example at # https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function -def test_3d_array(): +def test_3d_array() -> None: size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) a = numpy.ones(size, dtype=numpy.uint8) assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) @@ -94,12 +94,12 @@ def test_3d_array(): assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) -def test_1d_array(): +def test_1d_array() -> None: a = numpy.ones(5, dtype=numpy.uint8) assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np): +def _test_img_equals_nparray(img, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -109,14 +109,14 @@ def _test_img_equals_nparray(img, np): assert_deep_equal(px[x, y], np[y, x]) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as img: np_img = numpy.array(img) _test_img_equals_nparray(img, np_img) assert np_img.dtype == numpy.dtype(" None: # Test that 1-bit arrays convert to numpy and back # See: https://github.com/python-pillow/Pillow/issues/350 arr = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], "u1") @@ -126,7 +126,7 @@ def test_1bit(): numpy.testing.assert_array_equal(arr, arr_back) -def test_save_tiff_uint16(): +def test_save_tiff_uint16() -> None: # Tests that we're getting the pixel value in the right byte order. pixel_value = 0x1234 a = numpy.array( @@ -157,7 +157,7 @@ def test_save_tiff_uint16(): ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype): +def test_to_array(mode, dtype) -> None: img = hopper(mode) # Resize to non-square @@ -169,7 +169,7 @@ def test_to_array(mode, dtype): assert np_img.dtype == dtype -def test_point_lut(): +def test_point_lut() -> None: # See https://github.com/python-pillow/Pillow/issues/439 data = list(range(256)) * 3 @@ -180,7 +180,7 @@ def test_point_lut(): im.point(lut) -def test_putdata(): +def test_putdata() -> None: # Shouldn't segfault # See https://github.com/python-pillow/Pillow/issues/1008 @@ -207,12 +207,12 @@ def test_putdata(): numpy.float64, ), ) -def test_roundtrip_eye(dtype): +def test_roundtrip_eye(dtype) -> None: arr = numpy.eye(10, dtype=dtype) numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) -def test_zero_size(): +def test_zero_size() -> None: # Shouldn't cause floating point exception # See https://github.com/python-pillow/Pillow/issues/2259 @@ -222,13 +222,13 @@ def test_zero_size(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: a = numpy.array(im) assert a.shape == (88, 590) -def test_bool(): +def test_bool() -> None: # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) a[0][0] = True @@ -237,7 +237,7 @@ def test_bool(): assert im2.getdata()[0] == 255 -def test_no_resource_warning_for_numpy_array(): +def test_no_resource_warning_for_numpy_array() -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange from numpy import array diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index a89d75b59d3..f6b12cb2047 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -19,14 +19,14 @@ ) -def test_text_encode_decode(): +def test_text_encode_decode() -> None: assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" assert decode_text(b"abc") == "abc" assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" -def test_indirect_refs(): +def test_indirect_refs() -> None: assert IndirectReference(1, 2) == IndirectReference(1, 2) assert IndirectReference(1, 2) != IndirectReference(1, 3) assert IndirectReference(1, 2) != IndirectObjectDef(1, 2) @@ -37,7 +37,7 @@ def test_indirect_refs(): assert IndirectObjectDef(1, 2) != (1, 2) -def test_parsing(): +def test_parsing() -> None: assert PdfParser.interpret_name(b"Name#23Hash") == b"Name#Hash" assert PdfParser.interpret_name(b"Name#23Hash", as_text=True) == "Name#Hash" assert PdfParser.get_value(b"1 2 R ", 0) == (IndirectReference(1, 2), 5) @@ -95,7 +95,7 @@ def test_parsing(): assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value -def test_pdf_repr(): +def test_pdf_repr() -> None: assert bytes(IndirectReference(1, 2)) == b"1 2 R" assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash" @@ -121,7 +121,7 @@ def test_pdf_repr(): assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" -def test_duplicate_xref_entry(): +def test_duplicate_xref_entry() -> None: pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") assert pdf.xref_table.existing_entries[6][0] == 1197 pdf.close() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index c445e349447..560cdbd35f7 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,6 +1,7 @@ from __future__ import annotations import pickle +from pathlib import Path import pytest @@ -12,7 +13,7 @@ FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" -def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): +def helper_pickle_file(tmp_path: Path, pickle, protocol, test_file, mode) -> None: # Arrange with Image.open(test_file) as im: filename = str(tmp_path / "temp.pkl") @@ -29,7 +30,7 @@ def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): assert im == loaded_im -def helper_pickle_string(pickle, protocol, test_file, mode): +def helper_pickle_string(pickle, protocol, test_file, mode) -> None: with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -63,13 +64,13 @@ def helper_pickle_string(pickle, protocol, test_file, mode): ], ) @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) -def test_pickle_image(tmp_path, test_file, test_mode, protocol): +def test_pickle_image(tmp_path: Path, test_file, test_mode, protocol) -> None: # Act / Assert helper_pickle_string(pickle, protocol, test_file, test_mode) helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) -def test_pickle_la_mode_with_palette(tmp_path): +def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") with Image.open("Tests/images/hopper.jpg") as im: @@ -88,7 +89,7 @@ def test_pickle_la_mode_with_palette(tmp_path): @skip_unless_feature("webp") -def test_pickle_tell(): +def test_pickle_tell() -> None: # Arrange with Image.open("Tests/images/hopper.webp") as image: # Act: roundtrip @@ -98,7 +99,7 @@ def test_pickle_tell(): assert unpickled_image.tell() == 0 -def helper_assert_pickled_font_images(font1, font2): +def helper_assert_pickled_font_images(font1, font2) -> None: # Arrange im1 = Image.new(mode="RGBA", size=(300, 100)) im2 = Image.new(mode="RGBA", size=(300, 100)) @@ -116,7 +117,7 @@ def helper_assert_pickled_font_images(font1, font2): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_string(protocol): +def test_pickle_font_string(protocol) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -130,7 +131,7 @@ def test_pickle_font_string(protocol): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_file(tmp_path, protocol): +def test_pickle_font_file(tmp_path: Path, protocol) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) filename = str(tmp_path / "temp.pkl") diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 7f618d0f53b..797539f35ef 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -3,13 +3,14 @@ import os import sys from io import BytesIO +from pathlib import Path import pytest from PIL import Image, PSDraw -def _create_document(ps): +def _create_document(ps) -> None: title = "hopper" box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points @@ -31,7 +32,7 @@ def _create_document(ps): ps.end_document() -def test_draw_postscript(tmp_path): +def test_draw_postscript(tmp_path: Path) -> None: # Based on Pillow tutorial, but there is no textsize: # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript @@ -49,7 +50,7 @@ def test_draw_postscript(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer): +def test_stdout(buffer) -> None: # Temporarily redirect stdout old_stdout = sys.stdout diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index ad2b5ad9bf5..7d6c0a8cb7d 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import ImageQt @@ -19,7 +21,7 @@ from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() img = hopper().resize((1000, 1000)) @@ -35,14 +37,14 @@ def __init__(self): lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected): +def roundtrip(expected) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: # Segfault test app = QApplication([]) ex = Example() diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index b26787ce668..a222a7d71b9 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import ImageQt @@ -15,7 +17,7 @@ @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path): +def test_sanity(mode, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index dee6258ecb5..9442801d05c 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -21,7 +21,7 @@ "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", ], ) -def test_crashes(test_file): +def test_crashes(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 9f3e86a32a8..3db0660eab7 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,6 +1,7 @@ from __future__ import annotations import shutil +from pathlib import Path import pytest @@ -16,7 +17,7 @@ @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") class TestShellInjection: - def assert_save_filename_check(self, tmp_path, src_img, save_func): + def assert_save_filename_check(self, tmp_path: Path, src_img, save_func) -> None: for filename in test_filenames: dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) @@ -25,7 +26,7 @@ def assert_save_filename_check(self, tmp_path, src_img, save_func): im.load() @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg_filename(self, tmp_path): + def test_load_djpeg_filename(self, tmp_path: Path) -> None: for filename in test_filenames: src_file = str(tmp_path / filename) shutil.copy(TEST_JPG, src_file) @@ -34,18 +35,18 @@ def test_load_djpeg_filename(self, tmp_path): im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg_filename(self, tmp_path): + def test_save_cjpeg_filename(self, tmp_path: Path) -> None: with Image.open(TEST_JPG) as im: self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_bmp_mode(self, tmp_path): + def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("RGB") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_l_mode(self, tmp_path): + def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("L") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index c07e7f7d395..5368545232b 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,6 +1,7 @@ from __future__ import annotations from fractions import Fraction +from pathlib import Path from PIL import Image, TiffImagePlugin, features from PIL.TiffImagePlugin import IFDRational @@ -8,14 +9,14 @@ from .helper import hopper -def _test_equal(num, denom, target): +def _test_equal(num, denom, target) -> None: t = IFDRational(num, denom) assert target == t assert t == target -def test_sanity(): +def test_sanity() -> None: _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) @@ -31,13 +32,13 @@ def test_sanity(): _test_equal(7, 5, 1.4) -def test_ranges(): +def test_ranges() -> None: for num in range(1, 10): for denom in range(1, 10): assert IFDRational(num, denom) == IFDRational(num, denom) -def test_nonetype(): +def test_nonetype() -> None: # Fails if the _delegate function doesn't return a valid function xres = IFDRational(72) @@ -51,7 +52,7 @@ def test_nonetype(): assert xres and yres -def test_ifd_rational_save(tmp_path): +def test_ifd_rational_save(tmp_path: Path) -> None: methods = (True, False) if not features.check("libtiff"): methods = (False,) From bb1fece57a2c894773597c9a6fb10bd81e36123d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Jan 2024 21:55:32 +1100 Subject: [PATCH 0149/1178] Added type hints --- Tests/test_bmp_reference.py | 6 +- Tests/test_box_blur.py | 12 +- Tests/test_file_apng.py | 6 +- Tests/test_file_container.py | 10 +- Tests/test_file_gif.py | 30 ++-- Tests/test_file_mpo.py | 19 +-- Tests/test_file_ppm.py | 22 +-- Tests/test_file_sgi.py | 2 +- Tests/test_image_frombytes.py | 2 +- Tests/test_image_load.py | 2 +- Tests/test_imagedraw.py | 291 ++++++++++++++++++++++++++++------ Tests/test_imagetk.py | 4 +- Tests/test_lib_pack.py | 16 +- Tests/test_mode_i16.py | 6 +- 14 files changed, 327 insertions(+), 101 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 22ac9443e86..0ad49613553 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -10,7 +10,7 @@ base = os.path.join("Tests", "images", "bmp") -def get_files(d, ext: str = ".bmp"): +def get_files(d: str, ext: str = ".bmp") -> list[str]: return [ os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f ] @@ -29,7 +29,7 @@ def test_bad() -> None: pass -def test_questionable(): +def test_questionable() -> None: """These shouldn't crash/dos, but it's not well defined that these are in spec""" supported = [ @@ -80,7 +80,7 @@ def test_good() -> None: "rgb32bf.bmp": "rgb24.png", } - def get_compare(f): + def get_compare(f: str) -> str: name = os.path.split(f)[1] if name in file_map: return os.path.join(base, "html", file_map[name]) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index dfedb48d911..1f6ed61277a 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -23,11 +23,11 @@ def test_imageops_box_blur() -> None: assert isinstance(i, Image.Image) -def box_blur(image, radius: int = 1, n: int = 1): +def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: return image._new(image.im.box_blur((radius, radius), n)) -def assert_image(im, data, delta: int = 0) -> None: +def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -37,7 +37,13 @@ def assert_image(im, data, delta: int = 0) -> None: next(it) -def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None: +def assert_blur( + im: Image.Image, + radius: float, + data: list[list[int]], + passes: int = 1, + delta: int = 0, +) -> None: # check grayscale image assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f9edf6e9877..395165b3657 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -47,7 +47,7 @@ def test_apng_basic() -> None: "filename", ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ) -def test_apng_fdat(filename) -> None: +def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -338,7 +338,7 @@ def test_apng_syntax_errors() -> None: "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(test_file) -> None: +def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) @@ -681,7 +681,7 @@ def test_seek_after_close() -> None: @pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("duplicate", (True, False)) def test_different_modes_in_later_frames( - mode, default_image, duplicate, tmp_path: Path + mode: str, default_image: bool, duplicate: bool, tmp_path: Path ) -> None: test_file = str(tmp_path / "temp.png") diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 4dba4be5d5c..813b444dbcf 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -64,7 +64,7 @@ def test_seek_mode_2() -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode) -> None: +def test_read_n0(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -80,7 +80,7 @@ def test_read_n0(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode) -> None: +def test_read_n(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -96,7 +96,7 @@ def test_read_n(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode) -> None: +def test_read_eof(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -112,7 +112,7 @@ def test_read_eof(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode) -> None: +def test_readline(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) @@ -127,7 +127,7 @@ def test_readline(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode) -> None: +def test_readlines(bytesmode: bool) -> None: # Arrange expected = [ "This is line 1\n", diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3f550fd1109..db9d3586c4d 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -3,6 +3,7 @@ import warnings from io import BytesIO from pathlib import Path +from typing import Generator import pytest @@ -144,13 +145,13 @@ def test_strategy() -> None: def test_optimize() -> None: - def test_grayscale(optimize): + def test_grayscale(optimize: int) -> int: im = Image.new("L", (1, 1), 0) filename = BytesIO() im.save(filename, "GIF", optimize=optimize) return len(filename.getvalue()) - def test_bilevel(optimize): + def test_bilevel(optimize: int) -> int: im = Image.new("1", (1, 1), 0) test_file = BytesIO() im.save(test_file, "GIF", optimize=optimize) @@ -178,7 +179,9 @@ def test_bilevel(optimize): (4, 513, 256), ), ) -def test_optimize_correctness(colors, size, expected_palette_length) -> None: +def test_optimize_correctness( + colors: int, size: int, expected_palette_length: int +) -> None: # 256 color Palette image, posterize to > 128 and < 128 levels. # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. @@ -297,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ), ) -def test_loading_multiple_palettes(path, mode) -> None: +def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() @@ -347,9 +350,9 @@ def test_palette_handling(tmp_path: Path) -> None: def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): + def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: out = str(tmp_path / "temp.gif") - im.copy().save(out, *args, **kwargs) + im.copy().save(out, **kwargs) reloaded = Image.open(out) return reloaded @@ -429,7 +432,7 @@ def test_seek_rewind() -> None: ("Tests/images/iss634.gif", 42), ), ) -def test_n_frames(path, n_frames) -> None: +def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: assert im.is_animated == (n_frames != 1) @@ -541,7 +544,10 @@ def test_dispose_background_transparency() -> None: ), ), ) -def test_transparent_dispose(loading_strategy, expected_colors) -> None: +def test_transparent_dispose( + loading_strategy: GifImagePlugin.LoadingStrategy, + expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], +) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/transparent_dispose.gif") as img: @@ -889,7 +895,9 @@ def test_identical_frames(tmp_path: Path) -> None: 1500, ), ) -def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None: +def test_identical_frames_to_single_frame( + duration: int | list[int], tmp_path: Path +) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -1049,7 +1057,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") - def assert_version_after_save(im, version) -> None: + def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version @@ -1088,7 +1096,7 @@ def test_append_images(tmp_path: Path) -> None: assert reread.n_frames == 3 # Tests appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(out, save_all=True, append_images=im_generator(ims)) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 55b04a1e076..4fb00d6994a 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -2,6 +2,7 @@ import warnings from io import BytesIO +from typing import Any import pytest @@ -19,7 +20,7 @@ pytestmark = skip_unless_feature("jpg") -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "MPO", **options) test_bytes = out.tell() @@ -30,7 +31,7 @@ def roundtrip(im, **options): @pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file) -> None: +def test_sanity(test_file: str) -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -70,7 +71,7 @@ def test_context_manager() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_app(test_file) -> None: +def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" @@ -82,7 +83,7 @@ def test_app(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file) -> None: +def test_exif(test_file: str) -> None: with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) @@ -143,7 +144,7 @@ def test_reload_exif_after_seek() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file) -> None: +def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() assert mpinfo[45056] == b"0100" @@ -168,7 +169,7 @@ def test_mp_no_data() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file) -> None: +def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() for frame_number, mpentry in enumerate(mpinfo[0xB002]): @@ -185,7 +186,7 @@ def test_mp_attribute(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file) -> None: +def test_seek(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 # prior to first image raises an error, both blatant and borderline @@ -229,7 +230,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file) -> None: +def test_image_grab(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 im0 = im.tobytes() @@ -244,7 +245,7 @@ def test_image_grab(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_save(test_file) -> None: +def test_save(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 jpg0 = roundtrip(im) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 94f66ee7d28..6e0fa32e49f 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -70,7 +70,9 @@ def test_sanity() -> None: ), ), ) -def test_arbitrary_maxval(data, mode, pixels) -> None: +def test_arbitrary_maxval( + data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...] +) -> None: fp = BytesIO(data) with Image.open(fp) as im: assert im.size == (3, 1) @@ -139,7 +141,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None: b"Pf 1 1 -0.0 \0\0\0\0", ], ) -def test_pfm_invalid(data) -> None: +def test_pfm_invalid(data: bytes) -> None: with pytest.raises(ValueError): with Image.open(BytesIO(data)): pass @@ -162,7 +164,7 @@ def test_pfm_invalid(data) -> None: ), ), ) -def test_plain(plain_path, raw_path) -> None: +def test_plain(plain_path: str, raw_path: str) -> None: with Image.open(plain_path) as im: assert_image_equal_tofile(im, raw_path) @@ -186,7 +188,9 @@ def test_16bit_plain_pgm() -> None: (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), ), ) -def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None: +def test_plain_data_with_comment( + tmp_path: Path, header: bytes, data: bytes, comment_count: int +) -> None: path1 = str(tmp_path / "temp1.ppm") path2 = str(tmp_path / "temp2.ppm") comment = b"# comment" * comment_count @@ -199,7 +203,7 @@ def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path: Path, data) -> None: +def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -210,7 +214,7 @@ def test_plain_truncated_data(tmp_path: Path, data) -> None: @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path: Path, data) -> None: +def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -227,7 +231,7 @@ def test_plain_invalid_data(tmp_path: Path, data) -> None: b"P3\n128 128\n255\n012345678910 0", # token too long ), ) -def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None: +def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -313,7 +317,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None: @pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval, tmp_path: Path) -> None: +def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -351,7 +355,7 @@ def test_mimetypes(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer) -> None: +def test_save_stdout(buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 92aea07350e..e13a8019ed4 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -72,7 +72,7 @@ def test_invalid_file() -> None: def test_write(tmp_path: Path) -> None: - def roundtrip(img) -> None: + def roundtrip(img: Image.Image) -> None: out = str(tmp_path / "temp.sgi") img.save(out, format="sgi") assert_image_equal_tofile(img, out) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 6474daba108..98c0ea0b43c 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type) -> None: +def test_sanity(data_type: str) -> None: im1 = hopper() data = im1.tobytes() diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 5b1a9ee2dda..0605821e0ad 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -26,7 +26,7 @@ def test_close() -> None: im.getpixel((0, 0)) -def test_close_after_load(caplog) -> None: +def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: im = Image.open("Tests/images/hopper.gif") im.load() with caplog.at_level(logging.DEBUG): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 86d25b1ebc7..c02ac49ddc3 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -74,7 +74,14 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox, start, end) -> None: +def test_arc( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], + start: float, + end: float, +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -87,7 +94,12 @@ def test_arc(bbox, start, end) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox) -> None: +def test_arc_end_le_start( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -102,7 +114,12 @@ def test_arc_end_le_start(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox) -> None: +def test_arc_no_loops( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -118,7 +135,12 @@ def test_arc_no_loops(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox) -> None: +def test_arc_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -131,7 +153,12 @@ def test_arc_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox) -> None: +def test_arc_width_pieslice_large( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -145,7 +172,12 @@ def test_arc_width_pieslice_large(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox) -> None: +def test_arc_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -158,7 +190,12 @@ def test_arc_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox) -> None: +def test_arc_width_non_whole_angle( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -200,7 +237,13 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode, bbox) -> None: +def test_chord( + mode: str, + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], +) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -214,7 +257,12 @@ def test_chord(mode, bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox) -> None: +def test_chord_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -227,7 +275,12 @@ def test_chord_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox) -> None: +def test_chord_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -240,7 +293,12 @@ def test_chord_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox) -> None: +def test_chord_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -266,7 +324,13 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode, bbox) -> None: +def test_ellipse( + mode: str, + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], +) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -280,7 +344,12 @@ def test_ellipse(mode, bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox) -> None: +def test_ellipse_translucent( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -317,7 +386,12 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox) -> None: +def test_ellipse_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -342,7 +416,12 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox) -> None: +def test_ellipse_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -355,7 +434,12 @@ def test_ellipse_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox) -> None: +def test_ellipse_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -367,7 +451,7 @@ def test_ellipse_zero_width(bbox) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") -def ellipse_various_sizes_helper(filled): +def ellipse_various_sizes_helper(filled: bool) -> Image.Image: ellipse_sizes = range(32) image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 im = Image.new("RGB", (image_size, image_size)) @@ -409,7 +493,12 @@ def test_ellipse_various_sizes_filled() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line(points) -> None: +def test_line( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -482,7 +571,14 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox, start, end) -> None: +def test_pieslice( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], + start: float, + end: float, +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -495,7 +591,12 @@ def test_pieslice(bbox, start, end) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox) -> None: +def test_pieslice_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -508,7 +609,12 @@ def test_pieslice_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox) -> None: +def test_pieslice_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -522,7 +628,12 @@ def test_pieslice_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox) -> None: +def test_pieslice_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -577,7 +688,12 @@ def test_pieslice_no_spikes() -> None: @pytest.mark.parametrize("points", POINTS) -def test_point(points) -> None: +def test_point( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -602,7 +718,12 @@ def test_point_I16() -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon(points) -> None: +def test_polygon( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -616,7 +737,9 @@ def test_polygon(points) -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite(mode, kite_points) -> None: +def test_polygon_kite( + mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]] +) -> None: # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -673,7 +796,12 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox) -> None: +def test_rectangle( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -700,7 +828,12 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox) -> None: +def test_rectangle_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -714,7 +847,12 @@ def test_rectangle_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox) -> None: +def test_rectangle_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -728,7 +866,12 @@ def test_rectangle_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox) -> None: +def test_rectangle_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -741,7 +884,12 @@ def test_rectangle_zero_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox) -> None: +def test_rectangle_I16( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -754,7 +902,12 @@ def test_rectangle_I16(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox) -> None: +def test_rectangle_translucent_outline( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -772,7 +925,11 @@ def test_rectangle_translucent_outline(bbox) -> None: "xy", [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) -def test_rounded_rectangle(xy) -> None: +def test_rounded_rectangle( + xy: tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -789,7 +946,7 @@ def test_rounded_rectangle(xy) -> None: @pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False)) def test_rounded_rectangle_corners( - top_left, top_right, bottom_right, bottom_left + top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool ) -> None: corners = (top_left, top_right, bottom_right, bottom_left) @@ -824,7 +981,9 @@ def test_rounded_rectangle_corners( ((10, 20, 190, 181), 85, "height"), ], ) -def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: +def test_rounded_rectangle_non_integer_radius( + xy: tuple[int, int, int, int], radius: float, type: str +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -840,7 +999,12 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox) -> None: +def test_rounded_rectangle_zero_radius( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -862,7 +1026,9 @@ def test_rounded_rectangle_zero_radius(bbox) -> None: ((20, 20, 80, 80), "both"), ], ) -def test_rounded_rectangle_translucent(xy, suffix) -> None: +def test_rounded_rectangle_translucent( + xy: tuple[int, int, int, int], suffix: str +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -879,7 +1045,12 @@ def test_rounded_rectangle_translucent(xy, suffix) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox) -> None: +def test_floodfill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -912,7 +1083,12 @@ def test_floodfill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox) -> None: +def test_floodfill_border( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # floodfill() is experimental # Arrange @@ -934,7 +1110,12 @@ def test_floodfill_border(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox) -> None: +def test_floodfill_thresh( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # floodfill() is experimental # Arrange @@ -968,8 +1149,11 @@ def test_floodfill_not_negative() -> None: def create_base_image_draw( - size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY -): + size: tuple[int, int], + mode: str = DEFAULT_MODE, + background1: tuple[int, int, int] = WHITE, + background2: tuple[int, int, int] = GRAY, +) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) for x in range(0, size[0]): for y in range(0, size[1]): @@ -1003,7 +1187,7 @@ def test_triangle_right() -> None: "fill, suffix", ((BLACK, "width"), (None, "width_no_fill")), ) -def test_triangle_right_width(fill, suffix) -> None: +def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None: img, draw = create_base_image_draw((100, 100)) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) assert_image_equal_tofile( @@ -1235,7 +1419,7 @@ def test_wide_line_larger_than_int() -> None: ], ], ) -def test_line_joint(xy) -> None: +def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None: im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) @@ -1388,7 +1572,12 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox) -> None: +def test_same_color_outline( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1402,7 +1591,8 @@ def test_same_color_outline(bbox) -> None: # Begin for mode in ["RGB", "L"]: - for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: + fill = "red" + for outline in [None, "red", "#f00"]: for operation, args in { "chord": [bbox, 0, 180], "ellipse": [bbox], @@ -1417,6 +1607,7 @@ def test_same_color_outline(bbox) -> None: # Act draw_method = getattr(draw, operation) + assert isinstance(args, list) args += [fill, outline] draw_method(*args) @@ -1434,7 +1625,9 @@ def test_same_color_outline(bbox) -> None: (3, "triangle_width", {"width": 5, "outline": "yellow"}), ], ) -def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: +def test_draw_regular_polygon( + n_sides: int, polygon_name: str, args: dict[str, int | str] +) -> None: im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) @@ -1471,7 +1664,9 @@ def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: ), ], ) -def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None: +def test_compute_regular_polygon_vertices( + n_sides: int, expected_vertices: list[tuple[float, float]] +) -> None: bounding_circle = (W // 2, H // 2, 25) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @@ -1569,7 +1764,7 @@ def test_polygon2() -> None: @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy) -> None: +def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index a216bd21de2..b607b8c43aa 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -57,7 +57,7 @@ def test_kw() -> None: @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode) -> None: +def test_photoimage(mode: str) -> None: # test as image: im = hopper(mode) @@ -79,7 +79,7 @@ def test_photoimage_apply_transparency() -> None: @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode) -> None: +def test_photoimage_blank(mode: str) -> None: # test a image using mode/size: im_tk = ImageTk.PhotoImage(mode, (100, 100)) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index c8d6d33d223..629a6dc7a87 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -10,7 +10,13 @@ class TestLibPack: - def assert_pack(self, mode, rawmode, data, *pixels) -> None: + def assert_pack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -228,7 +234,13 @@ def test_F_float(self) -> None: class TestLibUnpack: - def assert_unpack(self, mode, rawmode, data, *pixels) -> None: + def assert_unpack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index f2540bb465e..903f7e0c60e 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -11,7 +11,7 @@ original = hopper().resize((32, 32)).convert("I") -def verify(im1) -> None: +def verify(im1: Image.Image) -> None: im2 = original.copy() assert im1.size == im2.size pix1 = im1.load() @@ -27,7 +27,7 @@ def verify(im1) -> None: @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path: Path, mode) -> None: +def test_basic(tmp_path: Path, mode: str) -> None: # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -78,7 +78,7 @@ def test_basic(tmp_path: Path, mode) -> None: def test_tobytes() -> None: - def tobytes(mode): + def tobytes(mode: str) -> Image.Image: return Image.new(mode, (1, 1), 1).tobytes() order = 1 if Image._ENDIAN == "<" else -1 From b8769d1cf5782f3db934b861ad764dc9b1466fb4 Mon Sep 17 00:00:00 2001 From: FangFuxin <38530078+lajiyuan@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:02:50 +0800 Subject: [PATCH 0150/1178] Update Tests/test_file_png.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0884ddcc35d..ec8794b3008 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -778,7 +778,7 @@ class MyStdOut: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_truncated_end_chunk(self): + def test_truncated_end_chunk(self) -> None: with Image.open("Tests/images/truncated_end_chunk.png") as im: with pytest.raises(OSError): im.load() From f2228e0a7c19d74c83b99f92edc113ba0cac7625 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:37:53 -0700 Subject: [PATCH 0151/1178] Replace bytes | str | Path with StrOrBytesPath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index b649500abb5..f7a69fae15d 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -11,7 +11,7 @@ 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) From 256f3f1966d6b56f178d0a9bb2bc4b0b334c77b1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:38:49 +0000 Subject: [PATCH 0152/1178] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index f7a69fae15d..6bc76281616 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -from pathlib import Path from typing import Any, NoReturn from ._typing import StrOrBytesPath, TypeGuard From 6dba9c988765084c104fd93c9fcc9ba3d18f6873 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:14:01 +0000 Subject: [PATCH 0153/1178] Update github-actions to v4 --- .github/workflows/test-cygwin.yml | 4 ++-- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index b5c8c39aaef..7bbe5a37f50 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -81,7 +81,7 @@ jobs: zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v3 + uses: egor-tensin/cleanup-path@v4 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' @@ -142,7 +142,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 3bb6856f6e8..75aab9bd489 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -101,7 +101,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index cdd51e2bb3f..acea78c37d9 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -82,7 +82,7 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 8cad7a8b281..b737615ca4b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -202,7 +202,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae84a4d8fd5..038bcfbc35a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -149,7 +149,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 2515938cdd321a5940a070f808c01ed48ad4e10e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Feb 2024 19:04:22 +1100 Subject: [PATCH 0154/1178] Simplified type hints --- Tests/test_imagedraw.py | 222 ++++++---------------------------------- 1 file changed, 32 insertions(+), 190 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index c02ac49ddc3..6e7dce420d2 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -2,6 +2,7 @@ import contextlib import os.path +from typing import Sequence import pytest @@ -74,14 +75,7 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], - start: float, - end: float, -) -> None: +def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -94,12 +88,7 @@ def test_arc( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -114,12 +103,7 @@ def test_arc_end_le_start( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -135,12 +119,7 @@ def test_arc_no_loops( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -153,12 +132,7 @@ def test_arc_width( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -172,12 +146,7 @@ def test_arc_width_pieslice_large( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -190,12 +159,7 @@ def test_arc_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_non_whole_angle(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -237,13 +201,7 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord( - mode: str, - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], -) -> None: +def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -257,12 +215,7 @@ def test_chord( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -275,12 +228,7 @@ def test_chord_width( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -293,12 +241,7 @@ def test_chord_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -324,13 +267,7 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse( - mode: str, - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], -) -> None: +def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -344,12 +281,7 @@ def test_ellipse( @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_translucent(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -386,12 +318,7 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -416,12 +343,7 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -434,12 +356,7 @@ def test_ellipse_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -572,12 +489,7 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) def test_pieslice( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], - start: float, - end: float, + bbox: Sequence[int | Sequence[int]], start: float, end: float ) -> None: # Arrange im = Image.new("RGB", (W, H)) @@ -591,12 +503,7 @@ def test_pieslice( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -609,12 +516,7 @@ def test_pieslice_width( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -628,12 +530,7 @@ def test_pieslice_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -796,12 +693,7 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -828,12 +720,7 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -847,12 +734,7 @@ def test_rectangle_width( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -866,12 +748,7 @@ def test_rectangle_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -884,12 +761,7 @@ def test_rectangle_zero_width( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -902,12 +774,7 @@ def test_rectangle_I16( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_translucent_outline(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -999,12 +866,7 @@ def test_rounded_rectangle_non_integer_radius( @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rounded_rectangle_zero_radius(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1045,12 +907,7 @@ def test_rounded_rectangle_translucent( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -1083,12 +940,7 @@ def test_floodfill( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: # floodfill() is experimental # Arrange @@ -1110,12 +962,7 @@ def test_floodfill_border( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill_thresh(bbox: Sequence[int | Sequence[int]]) -> None: # floodfill() is experimental # Arrange @@ -1572,12 +1419,7 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_same_color_outline(bbox: Sequence[int | Sequence[int]]) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 From 8d96e3bc590ec9c003efc47ad35295d7de4ed95c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Feb 2024 23:54:31 +1100 Subject: [PATCH 0155/1178] Changed name of first _Tile parameter --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 5ba5a6f8277..487f53efe1f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -91,7 +91,7 @@ def _tilesort(t): class _Tile(NamedTuple): - encoder_name: str + codec_name: str extents: tuple[int, int, int, int] offset: int args: tuple[Any, ...] | str | None From 6207ad419640475440de2f57c710e1a6235dfe90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 02:24:17 +0000 Subject: [PATCH 0156/1178] Update release-drafter/release-drafter action to v6 --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8fc7bd3799d..a8ddef22c86 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -23,6 +23,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From dba7dea3263dfa3252f7381307323477531646c8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 3 Feb 2024 14:43:04 +0200 Subject: [PATCH 0157/1178] Pin codecov/codecov-action to v3.1.5 --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7bbe5a37f50..a6b2935a921 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -142,7 +142,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 75aab9bd489..f40286fe434 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -101,7 +101,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index acea78c37d9..1c6d15b77d7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -82,7 +82,7 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b737615ca4b..75fccf7959a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -202,7 +202,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 038bcfbc35a..19f4a6daedc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -149,7 +149,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 435c884ebbee326daf55599c6248028684206cc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Feb 2024 23:44:33 +1100 Subject: [PATCH 0158/1178] Removed platform argument from setup-cygwin action --- .github/workflows/test-cygwin.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7bbe5a37f50..4b958e889bb 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -49,7 +49,6 @@ jobs: - name: Install Cygwin uses: egor-tensin/setup-cygwin@v4 with: - platform: x86_64 packages: > gcc-g++ ghostscript From 1b6723967440cf8474a9bd1e1c394c90c5c2f986 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 11:56:55 +1100 Subject: [PATCH 0159/1178] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7d80eec0345..a8404260ff3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 + [nik012003, radarhere] + +- Remove execute bit from ``setup.py`` #7760 + [hugovk] + - Do not support using test-image-results to upload images after test failures #7739 [radarhere] From dfb48ff297aa2b227a98f20ff0ae5a0009644ad3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 19:16:15 +1100 Subject: [PATCH 0160/1178] Match mask size to pasted image size --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3f550fd1109..263c897ef51 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1105,6 +1105,21 @@ def im_generator(ims): assert reread.n_frames == 10 +def test_append_different_size_image(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100)) + bigger_im = Image.new("RGB", (200, 200), "#f00") + + im.save(out, save_all=True, append_images=[bigger_im]) + + with Image.open(out) as reread: + assert reread.size == (100, 100) + + reread.seek(1) + assert reread.size == (100, 100) + + def test_transparent_optimize(tmp_path: Path) -> None: # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57d87078bca..935b95ca8c6 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -649,9 +649,7 @@ def _write_multiple_frames(im, fp, palette): if "transparency" in encoderinfo: # When the delta is zero, fill the image with transparency diff_frame = im_frame.copy() - fill = Image.new( - "P", diff_frame.size, encoderinfo["transparency"] - ) + fill = Image.new("P", delta.size, encoderinfo["transparency"]) if delta.mode == "RGBA": r, g, b, a = delta.split() mask = ImageMath.eval( From 5a8e7dda79e5ee4d0f8436179f61881a5d8bd286 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 20:36:34 +1100 Subject: [PATCH 0161/1178] Added type hints --- Tests/test_imagedraw.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/ImageDraw.py | 73 +++++++++++++++++++++-------------------- src/PIL/ImageFont.py | 2 +- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 6e7dce420d2..4503a929280 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1519,7 +1519,7 @@ def test_compute_regular_polygon_vertices( [ (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), - (3, 50, 0, TypeError, "bounding_circle should be a tuple"), + (3, 50, 0, TypeError, "bounding_circle should be a sequence"), ( 3, (50, 50, 100, 100), diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 553f36703b3..111d060129e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -571,7 +571,7 @@ def close(self): # object is gone. self.im = DeferredError(ValueError("Operation on closed image")) - def _copy(self): + def _copy(self) -> None: self.load() self.im = self.im.copy() self.pyaccess = None diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 84665f54fff..650e3085763 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -48,7 +48,7 @@ class ImageDraw: font = None - def __init__(self, im, mode=None): + def __init__(self, im: Image.Image, mode: str | None = None) -> None: """ Create a drawing instance. @@ -115,7 +115,7 @@ def getfont(self): self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size): + def _getfont(self, font_size: float | None): if font_size is not None: from . import ImageFont @@ -124,7 +124,7 @@ def _getfont(self, font_size): font = self.getfont() return font - def _getink(self, ink, fill=None): + def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: if ink is None and fill is None: if self.fill: fill = self.ink @@ -145,13 +145,13 @@ def _getink(self, ink, fill=None): fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1): + def arc(self, xy, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None): + def bitmap(self, xy, bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -160,7 +160,7 @@ def bitmap(self, xy, bitmap, fill=None): if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1): + def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -168,7 +168,7 @@ def chord(self, xy, start, end, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1): + def ellipse(self, xy, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -176,7 +176,7 @@ def ellipse(self, xy, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None): + def line(self, xy, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: @@ -236,7 +236,7 @@ def coord_at_angle(coord, angle): ] self.line(gap_coords, fill, width=3) - def shape(self, shape, fill=None, outline=None): + def shape(self, shape, fill=None, outline=None) -> None: """(Experimental) Draw a shape.""" shape.close() ink, fill = self._getink(outline, fill) @@ -245,7 +245,7 @@ def shape(self, shape, fill=None, outline=None): if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1): + def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -253,13 +253,13 @@ def pieslice(self, xy, start, end, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None): + def point(self, xy, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1): + def polygon(self, xy, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -267,7 +267,7 @@ def polygon(self, xy, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: if width == 1: self.draw.draw_polygon(xy, ink, 0, width) - else: + elif self.im is not None: # To avoid expanding the polygon outwards, # use the fill as a mask mask = Image.new("1", self.im.size) @@ -291,12 +291,12 @@ def polygon(self, xy, fill=None, outline=None, width=1): def regular_polygon( self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 - ): + ) -> None: """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1): + def rectangle(self, xy, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -306,7 +306,7 @@ def rectangle(self, xy, fill=None, outline=None, width=1): def rounded_rectangle( self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None - ): + ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): (x0, y0), (x1, y1) = xy @@ -346,7 +346,7 @@ def rounded_rectangle( r = d // 2 ink, fill = self._getink(outline, fill) - def draw_corners(pieslice): + def draw_corners(pieslice) -> None: if full_x: # Draw top and bottom halves parts = ( @@ -431,12 +431,12 @@ def draw_corners(pieslice): right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text): + def _multiline_check(self, text) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text): + def _multiline_split(self, text) -> list[str | bytes]: split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -465,7 +465,7 @@ def text( embedded_color=False, *args, **kwargs, - ): + ) -> None: """Draw text.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -497,7 +497,7 @@ def getink(fill): return fill return ink - def draw_text(ink, stroke_width=0, stroke_offset=None): + def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" @@ -547,7 +547,8 @@ def draw_text(ink, stroke_width=0, stroke_offset=None): ink_alpha = struct.pack("i", ink)[3] color.fillband(3, ink_alpha) x, y = coord - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + if self.im is not None: + self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) else: self.draw.draw_bitmap(coord, mask, ink) @@ -584,7 +585,7 @@ def multiline_text( embedded_color=False, *, font_size=None, - ): + ) -> None: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -693,7 +694,7 @@ def textbbox( embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -738,7 +739,7 @@ def multiline_textbbox( embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -777,7 +778,7 @@ def multiline_textbbox( elif anchor[1] == "d": top -= (len(lines) - 1) * line_spacing - bbox = None + bbox: tuple[int, int, int, int] | None = None for idx, line in enumerate(lines): left = xy[0] @@ -828,7 +829,7 @@ def multiline_textbbox( return bbox -def Draw(im, mode=None): +def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -876,7 +877,7 @@ def getdraw(im=None, hints=None): return im, handler -def floodfill(image, xy, value, border=None, thresh=0): +def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: """ (experimental) Fills a bounded region with a given color. @@ -932,7 +933,7 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): +def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -982,7 +983,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a tuple" + msg = "bounding_circle should be a sequence" raise TypeError(msg) if len(bounding_circle) == 3: @@ -1014,7 +1015,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): raise ValueError(msg) # 2. Define Helper Functions - def _apply_rotation(point, degrees, centroid): + def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: return ( round( point[0] * math.cos(math.radians(360 - degrees)) @@ -1030,11 +1031,11 @@ def _apply_rotation(point, degrees, centroid): ), ) - def _compute_polygon_vertex(centroid, polygon_radius, angle): + def _compute_polygon_vertex(angle: float) -> tuple[int, int]: start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle, centroid) + return _apply_rotation(start_point, angle) - def _get_angles(n_sides, rotation): + def _get_angles(n_sides: int, rotation: float) -> list[float]: angles = [] degrees = 360 / n_sides # Start with the bottom left polygon vertex @@ -1051,11 +1052,11 @@ def _get_angles(n_sides, rotation): # 4. Compute Vertices return [ - _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles + _compute_polygon_vertex(angle) for angle in angles ] -def _color_diff(color1, color2): +def _color_diff(color1, color2: float | tuple[int, ...]) -> float: """ Uses 1-norm distance to calculate difference between two values. """ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a63b73b33f5..1ec8a9f4d1f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -872,7 +872,7 @@ def load_path(filename): raise OSError(msg) -def load_default(size=None): +def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, https://dotcolon.net/font/aileron, with a more limited character set. From e0da2b71206c8c06ff4a9f67d6dade6973a7ae96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:18:25 +0000 Subject: [PATCH 0162/1178] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.2.0) - [github.com/psf/black-pre-commit-mirror: 23.12.1 → 24.1.1](https://github.com/psf/black-pre-commit-mirror/compare/23.12.1...24.1.1) - [github.com/PyCQA/bandit: 1.7.6 → 1.7.7](https://github.com/PyCQA/bandit/compare/1.7.6...1.7.7) - [github.com/tox-dev/pyproject-fmt: 1.5.3 → 1.7.0](https://github.com/tox-dev/pyproject-fmt/compare/1.5.3...1.7.0) - [github.com/abravalheri/validate-pyproject: v0.15 → v0.16](https://github.com/abravalheri/validate-pyproject/compare/v0.15...v0.16) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ce0c9a1792..c52fdcb552d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.2.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.6 + rev: 1.7.7 hooks: - id: bandit args: [--severity-level=high] @@ -48,12 +48,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.5.3 + rev: 1.7.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject From 27b0cf67e784c0c9e58e60afd8ffa1ba274681c6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:18:49 +0000 Subject: [PATCH 0163/1178] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/workflows/system-info.py | 1 + Tests/helper.py | 1 + Tests/test_file_dds.py | 1 + Tests/test_file_libtiff_small.py | 1 - Tests/test_image_access.py | 4 +--- Tests/test_image_resize.py | 1 + Tests/test_imagecms.py | 8 +++++--- docs/example/DdsImagePlugin.py | 1 + src/PIL/BlpImagePlugin.py | 1 + src/PIL/DdsImagePlugin.py | 1 + src/PIL/FontFile.py | 4 +--- src/PIL/FtexImagePlugin.py | 1 + src/PIL/GifImagePlugin.py | 6 +++--- src/PIL/ImageCms.py | 2 -- src/PIL/JpegPresets.py | 1 + src/PIL/PdfImagePlugin.py | 6 +++--- src/PIL/__init__.py | 1 + src/PIL/_tkinter_finder.py | 1 + 18 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py index 57f28c620b7..9e97b897104 100644 --- a/.github/workflows/system-info.py +++ b/.github/workflows/system-info.py @@ -6,6 +6,7 @@ Requested here: https://github.com/actions/virtual-environments/issues/79 """ + from __future__ import annotations import os diff --git a/Tests/helper.py b/Tests/helper.py index b2e7d43dd67..3e2a40e02da 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,6 +1,7 @@ """ Helper functions. """ + from __future__ import annotations import logging diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 09ee8986aca..b78a0dd8109 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,4 +1,5 @@ """Test DdsImagePlugin""" + from __future__ import annotations from io import BytesIO diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index ac5270eac30..617e1e89c72 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -9,7 +9,6 @@ class TestFileLibTiffSmall(LibTiffTestCase): - """The small lena image was failing on open in the libtiff decoder because the file pointer was set to the wrong place by a spurious seek. It wasn't failing with the byteio method. diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 00cd4e7a9a1..e4cb2dad102 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -230,9 +230,7 @@ def test_list(self) -> None: assert im.getpixel([0, 0]) == (20, 20, 70) @pytest.mark.parametrize("mode", ("I;16", "I;16B")) - @pytest.mark.parametrize( - "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) - ) + @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) def test_signedness(self, mode, expected_color) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index bd45ee893ad..a64e4a84621 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,6 +1,7 @@ """ Tests for resize functionality. """ + from __future__ import annotations from itertools import permutations diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 7f652715566..83fc38ed3fd 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -342,9 +342,11 @@ def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: def truncate_tuple(tuple_or_float): return tuple( - truncate_tuple(val) - if isinstance(val, tuple) - else int(val * power) / power + ( + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + ) for val in tuple_or_float ) diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index e98bb86806f..2a2a0ba2961 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -9,6 +9,7 @@ Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import struct diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index b8f38b78a2e..f0fbc8cc28a 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -28,6 +28,7 @@ - DXT3 compression is used if alpha_encoding == 1. - DXT5 compression is used if alpha_encoding == 7. """ + from __future__ import annotations import os diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index eb4c8f557af..3785174ef65 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -9,6 +9,7 @@ Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import io diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 3ec1ae819fc..1e0c1c166b5 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -50,9 +50,7 @@ def __init__(self) -> None: | None ] = [None] * 256 - def __getitem__( - self, ix: int - ) -> ( + def __getitem__(self, ix: int) -> ( tuple[ tuple[int, int], tuple[int, int, int, int], diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index d5513a56a11..b4488e6ee9c 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -50,6 +50,7 @@ Note: All data is stored in little-Endian (Intel) byte order. """ + from __future__ import annotations import struct diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57d87078bca..dc842d7a30b 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -641,9 +641,9 @@ def _write_multiple_frames(im, fp, palette): if encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: try: - encoderinfo[ - "transparency" - ] = im_frame.palette._new_color_index(im_frame) + encoderinfo["transparency"] = ( + im_frame.palette._new_color_index(im_frame) + ) except ValueError: pass if "transparency" in encoderinfo: diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 3e40105e46a..2b0ed6c9d2f 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -281,7 +281,6 @@ def tobytes(self): class ImageCmsTransform(Image.ImagePointHandler): - """ Transform. This can be used with the procedural API, or with the standard :py:func:`~PIL.Image.Image.point` method. @@ -369,7 +368,6 @@ def get_display_profile(handle=None): class PyCMSError(Exception): - """(pyCMS) Exception class. This is used for all errors in the pyCMS API.""" diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 9ecfdb2599a..d0e64a35ee1 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -62,6 +62,7 @@ https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ + from __future__ import annotations # fmt: off diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 3506aadce83..1777f1f20db 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -188,9 +188,9 @@ def _save(im, fp, filename, save_all=False): x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { - "title": None - if is_appending - else os.path.splitext(os.path.basename(filename))[0], + "title": ( + None if is_appending else os.path.splitext(os.path.basename(filename))[0] + ), "author": None, "subject": None, "keywords": None, diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 3fcac8643cb..63a45769ba6 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -12,6 +12,7 @@ ;-) """ + from __future__ import annotations from . import _version diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 03a6eba44ff..71c0ad4651b 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,5 +1,6 @@ """ Find compiled module linking to Tcl / Tk libraries """ + from __future__ import annotations import sys From 1acaf20f7215c567f0ea04bf20c396a237a2a542 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:37:45 +0200 Subject: [PATCH 0164/1178] Enable LOG rules for Ruff linter --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b1ce9cf1d07..48257b750a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,12 +104,12 @@ select = [ "F", # pyflakes errors "I", # isort "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade "W", # pycodestyle warnings "YTT", # flake8-2020 - # "LOG", # TODO: enable flake8-logging when it's not in preview anymore ] extend-ignore = [ "E203", # Whitespace before ':' From 3bcc7072d68d3d534f06938879cd6b2fa31e61b9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:39:08 +0200 Subject: [PATCH 0165/1178] Move linter config from deprecated top-level to own section --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48257b750a4..d7b60ef17d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" -[tool.ruff] +[tool.ruff.lint] select = [ "C4", # flake8-comprehensions "E", # pycodestyle errors @@ -118,11 +118,11 @@ extend-ignore = [ "E241", # Multiple spaces after ',' ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "Tests/oss-fuzz/fuzz_font.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["PIL"] required-imports = ["from __future__ import annotations"] From 65cb0b0487c29c91f0145226e1cd173511bc3586 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Feb 2024 07:49:43 +1100 Subject: [PATCH 0166/1178] Added _typing.Coords --- Tests/test_imagedraw.py | 87 +++++++++++++++++------------------------ src/PIL/ImageDraw.py | 87 ++++++++++++++++++++++++----------------- src/PIL/_typing.py | 4 ++ 3 files changed, 91 insertions(+), 87 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4503a929280..4e6cedcd15f 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -2,11 +2,11 @@ import contextlib import os.path -from typing import Sequence import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -75,7 +75,7 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> None: +def test_arc(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -88,7 +88,7 @@ def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> N @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_end_le_start(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -103,7 +103,7 @@ def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_no_loops(bbox: Coords) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -119,7 +119,7 @@ def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -132,7 +132,7 @@ def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_pieslice_large(bbox: Coords) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -146,7 +146,7 @@ def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -159,7 +159,7 @@ def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_non_whole_angle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -201,7 +201,7 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -215,7 +215,7 @@ def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -228,7 +228,7 @@ def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -241,7 +241,7 @@ def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -267,7 +267,7 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -281,7 +281,7 @@ def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_translucent(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -318,7 +318,7 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -343,7 +343,7 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -356,7 +356,7 @@ def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -410,12 +410,7 @@ def test_ellipse_various_sizes_filled() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -488,9 +483,7 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice( - bbox: Sequence[int | Sequence[int]], start: float, end: float -) -> None: +def test_pieslice(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -503,7 +496,7 @@ def test_pieslice( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -516,7 +509,7 @@ def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -530,7 +523,7 @@ def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -585,12 +578,7 @@ def test_pieslice_no_spikes() -> None: @pytest.mark.parametrize("points", POINTS) -def test_point( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_point(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -615,12 +603,7 @@ def test_point_I16() -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -693,7 +676,7 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -720,7 +703,7 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -734,7 +717,7 @@ def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -748,7 +731,7 @@ def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -761,7 +744,7 @@ def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_I16(bbox: Coords) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -774,7 +757,7 @@ def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_translucent_outline(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -866,7 +849,7 @@ def test_rounded_rectangle_non_integer_radius( @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rounded_rectangle_zero_radius(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -907,7 +890,7 @@ def test_rounded_rectangle_translucent( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill(bbox: Coords) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -940,7 +923,7 @@ def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill_border(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -962,7 +945,7 @@ def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill_thresh(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -1419,7 +1402,7 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox: Sequence[int | Sequence[int]]) -> None: +def test_same_color_outline(bbox: Coords) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 650e3085763..d4e000087c4 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,8 +34,10 @@ import math import numbers import struct +from typing import Sequence, cast from . import Image, ImageColor +from ._typing import Coords """ A simple 2D drawing interface for PIL images. @@ -145,13 +147,13 @@ def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1) -> None: + def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None) -> None: + def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -160,7 +162,7 @@ def bitmap(self, xy, bitmap, fill=None) -> None: if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: + def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -168,7 +170,7 @@ def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1) -> None: + def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -176,20 +178,29 @@ def ellipse(self, xy, fill=None, outline=None, width=1) -> None: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None) -> None: + def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: self.draw.draw_lines(xy, ink, width) if joint == "curve" and width > 4: - if not isinstance(xy[0], (list, tuple)): - xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] - for i in range(1, len(xy) - 1): - point = xy[i] + points: Sequence[Sequence[float]] + if isinstance(xy[0], (list, tuple)): + points = cast(Sequence[Sequence[float]], xy) + else: + points = [ + cast(Sequence[float], tuple(xy[i : i + 2])) + for i in range(0, len(xy), 2) + ] + for i in range(1, len(points) - 1): + point = points[i] angles = [ math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) % 360 - for start, end in ((xy[i - 1], point), (point, xy[i + 1])) + for start, end in ( + (points[i - 1], point), + (point, points[i + 1]), + ) ] if angles[0] == angles[1]: # This is a straight line, so no joint is required @@ -245,7 +256,9 @@ def shape(self, shape, fill=None, outline=None) -> None: if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: + def pieslice( + self, xy: Coords, start, end, fill=None, outline=None, width=1 + ) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -253,13 +266,13 @@ def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None) -> None: + def point(self, xy: Coords, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1) -> None: + def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -296,7 +309,7 @@ def regular_polygon( xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1) -> None: + def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -305,13 +318,13 @@ def rectangle(self, xy, fill=None, outline=None, width=1) -> None: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None + self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = xy + (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) else: - x0, y0, x1, y1 = xy + x0, y0, x1, y1 = cast(Sequence[float], xy) if x1 < x0: msg = "x1 must be greater than or equal to x0" raise ValueError(msg) @@ -347,6 +360,7 @@ def rounded_rectangle( ink, fill = self._getink(outline, fill) def draw_corners(pieslice) -> None: + parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] if full_x: # Draw top and bottom halves parts = ( @@ -361,17 +375,18 @@ def draw_corners(pieslice) -> None: ) else: # Draw four separate corners - parts = [] - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), + parts = tuple( + part + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) ) - ): - if corners[i]: - parts.append(part) + if corners[i] + ) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -520,7 +535,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: *args, **kwargs, ) - coord = coord[0] + offset[0], coord[1] + offset[1] + coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: mask = font.getmask( @@ -539,7 +554,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: except TypeError: mask = font.getmask(text) if stroke_offset: - coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] if mode == "RGBA": # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # extract mask and set text alpha @@ -548,7 +563,9 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: color.fillband(3, ink_alpha) x, y = coord if self.im is not None: - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) else: self.draw.draw_bitmap(coord, mask, ink) @@ -829,7 +846,7 @@ def multiline_textbbox( return bbox -def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: +def Draw(im, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -933,7 +950,9 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> list[tuple[float, float]]: +def _compute_regular_polygon_vertices( + bounding_circle, n_sides, rotation +) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -1051,9 +1070,7 @@ def _get_angles(n_sides: int, rotation: float) -> list[float]: angles = _get_angles(n_sides, rotation) # 4. Compute Vertices - return [ - _compute_polygon_vertex(angle) for angle in angles - ] + return [_compute_polygon_vertex(angle) for angle in angles] def _color_diff(color1, color2: float | tuple[int, ...]) -> float: diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 608b2b41fa8..ddea0b41467 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +from typing import Sequence, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -15,4 +16,7 @@ def __class_getitem__(cls, item: Any) -> type[bool]: return bool +Coords = Union[Sequence[float], Sequence[Sequence[float]]] + + __all__ = ["TypeGuard"] From 5f115df74f7aa26ec94b79ecad720a707be029e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Feb 2024 08:05:30 +1100 Subject: [PATCH 0167/1178] Replace deprecated "extend-ignore" with "ignore" --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7b60ef17d2..652ae36333c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,7 +111,7 @@ select = [ "W", # pycodestyle warnings "YTT", # flake8-2020 ] -extend-ignore = [ +ignore = [ "E203", # Whitespace before ':' "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator From 469db5114cf317ea128bd8c4b508eed537b7ce9e Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 6 Feb 2024 15:41:08 -0500 Subject: [PATCH 0168/1178] Release GIL while calling WebPAnimDecoderGetNext --- src/_webp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_webp.c b/src/_webp.c index a1b4dbc1a25..4e7d41f1121 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -450,12 +450,16 @@ _anim_decoder_get_next(PyObject *self) { int timestamp; PyObject *bytes; PyObject *ret; + ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + ImagingSectionEnter(&cookie); if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { + ImagingSectionLeave(&cookie); PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } + ImagingSectionLeave(&cookie); bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); From 91645f9efffc623cb83221a8d9c9a0b98d3ce548 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Feb 2024 10:19:00 +1100 Subject: [PATCH 0169/1178] Lint fix --- Tests/test_imagedraw.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4e6cedcd15f..f7aea30348d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -776,9 +776,11 @@ def test_rectangle_translucent_outline(bbox: Coords) -> None: [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) def test_rounded_rectangle( - xy: tuple[int, int, int, int] - | tuple[list[int]] - | tuple[tuple[int, int], tuple[int, int]] + xy: ( + tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] + ) ) -> None: # Arrange im = Image.new("RGB", (200, 200)) From cdc498e6f3b2060906ca14fe9b9187e0a93a1613 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Feb 2024 19:16:28 +1100 Subject: [PATCH 0170/1178] Added type hints --- Tests/test_color_lut.py | 4 ++- Tests/test_file_eps.py | 32 ++++++++++---------- Tests/test_file_jpeg.py | 35 +++++++++++----------- Tests/test_file_jpeg2k.py | 21 ++++++------- Tests/test_file_libtiff.py | 38 +++++++++++++----------- Tests/test_file_png.py | 15 +++++----- Tests/test_image_paste.py | 41 +++++++++++++++----------- Tests/test_image_reduce.py | 43 +++++++++++++++------------ Tests/test_image_resample.py | 57 ++++++++++++++++++++---------------- Tests/test_imagemath.py | 8 ++--- 10 files changed, 160 insertions(+), 134 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index e6c8d7819a6..2bb1b57d482 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -15,7 +15,9 @@ class TestColorLut3DCoreAPI: - def generate_identity_table(self, channels, size): + def generate_identity_table( + self, channels: int, size: int | tuple[int, int, int] + ) -> tuple[int, int, int, int, list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 06f927c7bb2..00f5f39e8c8 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -84,7 +84,7 @@ ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ) @pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename, size, scale) -> None: +def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: image.load(scale=scale) @@ -129,28 +129,28 @@ def test_binary_header_only() -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix) -> None: +def test_missing_version_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix) -> None: +def test_missing_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix) -> None: +def test_invalid_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: data = io.BytesIO( prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) ) @@ -161,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix) -> None: +def test_ascii_comment_too_long(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) with pytest.raises(SyntaxError, match="not an EPS file"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix) -> None: +def test_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) EpsImagePlugin.EpsImageFile(data) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix) -> None: +def test_load_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() @@ -305,7 +305,7 @@ def test_render_scale2() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename) -> None: +def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) im = im.resize(new_size) @@ -314,7 +314,7 @@ def test_resize(filename) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename) -> None: +def test_thumbnail(filename: str) -> None: # Issue #619 with Image.open(filename) as im: new_size = (100, 100) @@ -335,7 +335,7 @@ def test_readline_psfile(tmp_path: Path) -> None: line_endings = ["\r\n", "\n", "\n\r", "\r"] strings = ["something", "else", "baz", "bif"] - def _test_readline(t, ending) -> None: + def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -344,13 +344,13 @@ def _test_readline(t, ending) -> None: assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(test_string, ending) -> None: + def _test_readline_io_psfile(test_string: str, ending: str) -> None: f = io.BytesIO(test_string.encode("latin-1")) with pytest.warns(DeprecationWarning): t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) - def _test_readline_file_psfile(test_string, ending) -> None: + def _test_readline_file_psfile(test_string: str, ending: str) -> None: f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) @@ -376,7 +376,7 @@ def test_psfile_deprecation() -> None: "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(prefix, line_ending) -> None: +def test_readline(prefix: bytes, line_ending: bytes) -> None: simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) @@ -394,7 +394,7 @@ def test_readline(prefix, line_ending) -> None: "Tests/images/illuCS6_preview.eps", ), ) -def test_open_eps(filename) -> None: +def test_open_eps(filename: str) -> None: # https://github.com/python-pillow/Pillow/issues/1104 with Image.open(filename) as img: assert img.mode == "RGB" @@ -417,7 +417,7 @@ def test_emptyline() -> None: "test_file", ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) -def test_timeout(test_file) -> None: +def test_timeout(test_file: str) -> None: with open(test_file, "rb") as f: with pytest.raises(Image.UnidentifiedImageError): with Image.open(f): diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ff278d4c1bf..6b0662e0be3 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -5,6 +5,7 @@ import warnings from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -42,7 +43,7 @@ @skip_unless_feature("jpg") class TestFileJpeg: - def roundtrip(self, im, **options): + def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG", **options) test_bytes = out.tell() @@ -51,7 +52,7 @@ def roundtrip(self, im, **options): im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode: str = "RGB"): + def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -71,7 +72,7 @@ def test_sanity(self) -> None: assert im.get_format_mimetype() == "image/jpeg" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size, tmp_path: Path) -> None: + def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = Image.new("RGB", size) with pytest.raises(ValueError): @@ -108,13 +109,11 @@ def test_comment_write(self) -> None: assert "comment" not in reloaded.info # Test that a comment argument overrides the default comment - for comment in ("Test comment text", b"Text comment text"): + for comment in ("Test comment text", b"Test comment text"): out = BytesIO() im.save(out, format="JPEG", comment=comment) with Image.open(out) as reloaded: - if not isinstance(comment, bytes): - comment = comment.encode() - assert reloaded.info["comment"] == comment + assert reloaded.info["comment"] == b"Test comment text" def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, @@ -145,7 +144,7 @@ def test_cmyk(self) -> None: assert k > 0.9 def test_rgb(self) -> None: - def getchannels(im): + def getchannels(im: Image.Image) -> tuple[int, int, int]: return tuple(v[0] for v in im.layer) im = hopper() @@ -161,8 +160,8 @@ def getchannels(im): "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) - def test_dpi(self, test_image_path) -> None: - def test(xdpi, ydpi=None): + def test_dpi(self, test_image_path: str) -> None: + def test(xdpi: int, ydpi: int | None = None): with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") @@ -207,7 +206,7 @@ def test_icc(self, tmp_path: Path) -> None: ImageFile.MAXBLOCK * 4 + 3, # large block ), ) - def test_icc_big(self, n) -> None: + def test_icc_big(self, n: int) -> None: # Make sure that the "extra" support handles large blocks # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of @@ -433,7 +432,7 @@ def test_smooth(self) -> None: assert_image(im1, im2.mode, im2.size) def test_subsampling(self) -> None: - def getsampling(im): + def getsampling(im: Image.Image): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] @@ -530,7 +529,7 @@ def test_truncated_jpeg_throws_oserror(self) -> None: pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) def test_qtables(self, tmp_path: Path) -> None: - def _n_qtables_helper(n, test_file) -> None: + def _n_qtables_helper(n: int, test_file: str) -> None: with Image.open(test_file) as im: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) @@ -666,7 +665,7 @@ def test_save_low_quality_baseline_qtables(self) -> None: "blocks, rows, markers", ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ) - def test_restart_markers(self, blocks, rows, markers) -> None: + def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: im = Image.new("RGB", (32, 32)) # 16 MCUs out = BytesIO() im.save( @@ -724,13 +723,13 @@ def test_bad_mpo_header(self) -> None: assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode) -> None: + def test_save_correct_modes(self, mode: str) -> None: out = BytesIO() img = Image.new(mode, (20, 20)) img.save(out, "JPEG") @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode) -> None: + def test_save_wrong_modes(self, mode: str) -> None: # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() img = Image.new(mode, (20, 20)) @@ -982,12 +981,12 @@ def test_eof(self) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 decoder = InfiniteMockPyDecoder(None) - def closure(mode, *args): + def closure(mode: str, *args) -> InfiniteMockPyDecoder: decoder.__init__(mode, *args) return decoder diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index e3f1fa8fde1..fab19e2eab4 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -4,6 +4,7 @@ import re from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -36,7 +37,7 @@ # 'Not enough memory to handle tile data' -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG2000", **options) test_bytes = out.tell() @@ -138,7 +139,7 @@ def test_prog_res_rt() -> None: @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions) -> None: +def test_default_num_resolutions(num_resolutions: int) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -198,9 +199,9 @@ def test_layers_type(tmp_path: Path) -> None: for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in ["quality_layers", ("100", "50", "10")]: + for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers) + test_card.save(outfile, quality_layers=quality_layers_str) def test_layers() -> None: @@ -233,7 +234,7 @@ def test_layers() -> None: ("foo.jp2", {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name, args, offset, data) -> None: +def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: out = BytesIO() if name: out.name = name @@ -278,7 +279,7 @@ def test_sgnd(tmp_path: Path) -> None: @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext) -> None: +def test_rgba(ext: str) -> None: # Arrange with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: # Act @@ -289,7 +290,7 @@ def test_rgba(ext) -> None: @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_16bit_monochrome_has_correct_mode(ext) -> None: +def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: im.load() assert im.mode == "I;16" @@ -346,12 +347,12 @@ def test_parser_feed() -> None: not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name) -> None: +def test_subsampling_decode(name: str) -> None: test = f"{EXTRA_DIR}/{name}.jp2" reference = f"{EXTRA_DIR}/{name}.ppm" with Image.open(test) as im: - epsilon = 3 # for YCbCr images + epsilon = 3.0 # for YCbCr images with Image.open(reference) as im2: width, height = im2.size if name[-1] == "2": @@ -400,7 +401,7 @@ def test_save_comment() -> None: "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: # Valgrind should not complain here diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1386034e5a3..0994d99040e 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -27,7 +27,7 @@ @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path: Path, im) -> None: + def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -140,7 +140,7 @@ def test_adobe_deflate_tiff(self) -> None: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api, tmp_path: Path) -> None: + def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -243,7 +243,7 @@ def test_additional_metadata(self, tmp_path: Path) -> None: TiffImagePlugin.WRITE_LIBTIFF = False def test_custom_metadata(self, tmp_path: Path) -> None: - tc = namedtuple("test_case", "value,type,supported_by_default") + tc = namedtuple("tc", "value,type,supported_by_default") custom = { 37000 + k: v for k, v in enumerate( @@ -284,7 +284,9 @@ def test_custom_metadata(self, tmp_path: Path) -> None: for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - def check_tags(tiffinfo) -> None: + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -502,7 +504,7 @@ def test_cmyk_save(self, tmp_path: Path) -> None: assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im, tmp_path: Path) -> None: + def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None: out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -514,7 +516,7 @@ def test_palette_save(self, im, tmp_path: Path) -> None: assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None: + def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -647,7 +649,7 @@ def test_save_bytesio(self) -> None: # Generate test image pilim = hopper() - def save_bytesio(compression=None) -> None: + def save_bytesio(compression: str | None = None) -> None: buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -731,7 +733,7 @@ def test_read_icc(self) -> None: assert icc == icc_libtiff def test_write_icc(self, tmp_path: Path) -> None: - def check_write(libtiff) -> None: + def check_write(libtiff: bool) -> None: TiffImagePlugin.WRITE_LIBTIFF = libtiff with Image.open("Tests/images/hopper.iccprofile.tif") as img: @@ -837,7 +839,7 @@ def test_sampleformat_write(self, tmp_path: Path) -> None: assert reloaded.mode == "F" assert reloaded.getexif()[SAMPLEFORMAT] == 3 - def test_lzma(self, capfd): + def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_lzma.tif") as im: assert im.mode == "RGB" @@ -853,7 +855,7 @@ def test_lzma(self, capfd): sys.stderr.write(captured.err) raise - def test_webp(self, capfd): + def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_webp.tif") as im: assert im.mode == "RGB" @@ -971,7 +973,7 @@ def test_strip_planar_16bit_RGBa(self) -> None: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @pytest.mark.parametrize("compression", (None, "jpeg")) - def test_block_tile_tags(self, compression, tmp_path: Path) -> None: + def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -1020,7 +1022,9 @@ def test_open_missing_samplesperpixel(self) -> None: ), ], ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None: + def test_wrong_bits_per_sample( + self, file_name: str, mode: str, size: tuple[int, int], tile + ) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode assert im.size == size @@ -1086,7 +1090,7 @@ def test_realloc_overflow(self) -> None: TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) - def test_save_multistrip(self, compression, tmp_path: Path) -> None: + def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") im.save(out, compression=compression) @@ -1096,14 +1100,14 @@ def test_save_multistrip(self, compression, tmp_path: Path) -> None: assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument, tmp_path: Path) -> None: + def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") if not argument: TiffImagePlugin.STRIP_SIZE = 2**18 try: - arguments = {"compression": "tiff_adobe_deflate"} + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} if argument: arguments["strip_size"] = 2**18 im.save(out, **arguments) @@ -1114,7 +1118,7 @@ def test_save_single_strip(self, argument, tmp_path: Path) -> None: TiffImagePlugin.STRIP_SIZE = 65536 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression, tmp_path: Path) -> None: + def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): @@ -1134,7 +1138,7 @@ def test_save_many_compressed(self, tmp_path: Path) -> None: ("Tests/images/child_ifd_jpeg.tiff", (20,)), ), ) - def test_get_child_images(self, path, sizes) -> None: + def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None: with Image.open(path) as im: ims = im.get_child_images() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0f1d96365ea..d4a63431647 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -6,6 +6,7 @@ import zlib from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -36,7 +37,7 @@ MAGIC = PngImagePlugin._MAGIC -def chunk(cid, *data): +def chunk(cid: bytes, *data: bytes) -> bytes: test_file = BytesIO() PngImagePlugin.putchunk(*(test_file, cid) + data) return test_file.getvalue() @@ -52,11 +53,11 @@ def chunk(cid, *data): TAIL = IDAT + IEND -def load(data): +def load(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "PNG", **options) out.seek(0) @@ -65,7 +66,7 @@ def roundtrip(im, **options): @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename): + def get_chunks(self, filename: str) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -436,7 +437,7 @@ def test_nonunicode_text(self) -> None: def test_unicode_text(self) -> None: # Check preservation of non-ASCII characters - def rt_text(value) -> None: + def rt_text(value: str) -> None: im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) @@ -636,7 +637,7 @@ def test_padded_idat(self) -> None: @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid) -> None: + def test_truncated_chunks(self, cid: bytes) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): @@ -755,7 +756,7 @@ def test_seek(self) -> None: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer) -> None: + def test_save_stdout(self, buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 34a2f8f3db5..c4d7a5dd254 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -11,10 +11,9 @@ class TestImagingPaste: masks = {} size = 128 - def assert_9points_image(self, im, expected) -> None: - expected = [ - point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected - ] + def assert_9points_image( + self, im: Image.Image, expected: list[tuple[int, int, int, int]] + ) -> None: px = im.load() actual = [ px[0, 0], @@ -27,9 +26,17 @@ def assert_9points_image(self, im, expected) -> None: px[self.size // 2, self.size - 1], px[self.size - 1, self.size - 1], ] - assert actual == expected + assert actual == [ + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected + ] - def assert_9points_paste(self, im, im2, mask, expected) -> None: + def assert_9points_paste( + self, + im: Image.Image, + im2: Image.Image, + mask: Image.Image, + expected: list[tuple[int, int, int, int]], + ) -> None: im3 = im.copy() im3.paste(im2, (0, 0), mask) self.assert_9points_image(im3, expected) @@ -106,7 +113,7 @@ def gradient_RGBa(self): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode) -> None: + def test_image_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -116,7 +123,7 @@ def test_image_solid(self, mode) -> None: assert_image_equal(im, im2) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode) -> None: + def test_image_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -138,7 +145,7 @@ def test_image_mask_1(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode) -> None: + def test_image_mask_L(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -160,7 +167,7 @@ def test_image_mask_L(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode) -> None: + def test_image_mask_LA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -182,7 +189,7 @@ def test_image_mask_LA(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode) -> None: + def test_image_mask_RGBA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -204,7 +211,7 @@ def test_image_mask_RGBA(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode) -> None: + def test_image_mask_RGBa(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -226,7 +233,7 @@ def test_image_mask_RGBa(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode) -> None: + def test_color_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) @@ -239,7 +246,7 @@ def test_color_solid(self, mode) -> None: assert sum(head[:255]) == 0 @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode) -> None: + def test_color_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -261,7 +268,7 @@ def test_color_mask_1(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode) -> None: + def test_color_mask_L(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -283,7 +290,7 @@ def test_color_mask_L(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode) -> None: + def test_color_mask_RGBA(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -305,7 +312,7 @@ def test_color_mask_RGBA(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode) -> None: + def test_color_mask_RGBa(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index c29830a7e5f..33b33d6b7fc 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -48,7 +48,7 @@ ((1, 3), (10, 4)), ), ) -def test_args_factor(size, expected) -> None: +def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(size).size @@ -56,7 +56,7 @@ def test_args_factor(size, expected) -> None: @pytest.mark.parametrize( "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, expected_error) -> None: +def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(size) @@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error) -> None: ((5, 5, 6, 6), (1, 1)), ), ) -def test_args_box(size, expected) -> None: +def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(2, size).size @@ -86,20 +86,20 @@ def test_args_box(size, expected) -> None: ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, expected_error) -> None: +def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(2, size).size @pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode) -> None: +def test_unsupported_modes(mode: str) -> None: im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) -def get_image(mode): +def get_image(mode: str) -> Image.Image: mode_info = ImageMode.getmode(mode) if mode_info.basetype == "L": bands = [gradients_image] @@ -119,7 +119,7 @@ def get_image(mode): return im.crop((0, 0, im.width, im.height - 5)) -def compare_reduce_with_box(im, factor) -> None: +def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None: box = (11, 13, 146, 164) reduced = im.reduce(factor, box=box) reference = im.crop(box).reduce(factor) @@ -127,7 +127,10 @@ def compare_reduce_with_box(im, factor) -> None: def compare_reduce_with_reference( - im, factor, average_diff: float = 0.4, max_diff: int = 1 + im: Image.Image, + factor: int | tuple[int, int], + average_diff: float = 0.4, + max_diff: int = 1, ) -> None: """Image.reduce() should look very similar to Image.resize(BOX). @@ -173,7 +176,9 @@ def compare_reduce_with_reference( assert_compare_images(reduced, reference, average_diff, max_diff) -def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: +def assert_compare_images( + a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255 +) -> None: assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -201,20 +206,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor) -> None: +def test_mode_L(factor: int | tuple[int, int]) -> None: im = get_image("L") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor) -> None: +def test_mode_LA(factor: int | tuple[int, int]) -> None: im = get_image("LA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor) -> None: +def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -223,27 +228,27 @@ def test_mode_LA_opaque(factor) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor) -> None: +def test_mode_La(factor: int | tuple[int, int]) -> None: im = get_image("La") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor) -> None: +def test_mode_RGB(factor: int | tuple[int, int]) -> None: im = get_image("RGB") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor) -> None: +def test_mode_RGBA(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor) -> None: +def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -252,21 +257,21 @@ def test_mode_RGBA_opaque(factor) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor) -> None: +def test_mode_RGBa(factor: int | tuple[int, int]) -> None: im = get_image("RGBa") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor) -> None: +def test_mode_I(factor: int | tuple[int, int]) -> None: im = get_image("I") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor) -> None: +def test_mode_F(factor: int | tuple[int, int]) -> None: im = get_image("F") compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f4c9eb0e6fa..f3ec12c053b 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,6 +1,7 @@ from __future__ import annotations from contextlib import contextmanager +from typing import Generator import pytest @@ -51,7 +52,7 @@ def test_modify_after_resizing(self) -> None: class TestImagingCoreResampleAccuracy: - def make_case(self, mode, size, color): + def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image: """Makes a sample image with two dark and two bright squares. For example: e0 e0 1f 1f @@ -66,7 +67,7 @@ def make_case(self, mode, size, color): return Image.merge(mode, [case] * len(mode)) - def make_sample(self, data, size): + def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ @@ -83,7 +84,7 @@ def make_sample(self, data, size): s_px[size[0] - x - 1, y] = 255 - val return sample - def check_case(self, case, sample) -> None: + def check_case(self, case: Image.Image, sample: Image.Image) -> None: s_px = sample.load() c_px = case.load() for y in range(case.size[1]): @@ -95,7 +96,7 @@ def check_case(self, case, sample) -> None: ) assert s_px[x, y] == c_px[x, y], message - def serialize_image(self, image): + def serialize_image(self, image: Image.Image) -> str: s_px = image.load() return "\n".join( " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) @@ -103,7 +104,7 @@ def serialize_image(self, image): ) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode) -> None: + def test_reduce_box(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -114,7 +115,7 @@ def test_reduce_box(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode) -> None: + def test_reduce_bilinear(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -125,7 +126,7 @@ def test_reduce_bilinear(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode) -> None: + def test_reduce_hamming(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -136,7 +137,7 @@ def test_reduce_hamming(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode) -> None: + def test_reduce_bicubic(self, mode: str) -> None: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off @@ -148,7 +149,7 @@ def test_reduce_bicubic(self, mode) -> None: self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode) -> None: + def test_reduce_lanczos(self, mode: str) -> None: case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off @@ -161,7 +162,7 @@ def test_reduce_lanczos(self, mode) -> None: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode) -> None: + def test_enlarge_box(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -172,7 +173,7 @@ def test_enlarge_box(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode) -> None: + def test_enlarge_bilinear(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -183,7 +184,7 @@ def test_enlarge_bilinear(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode) -> None: + def test_enlarge_hamming(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -194,7 +195,7 @@ def test_enlarge_hamming(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode) -> None: + def test_enlarge_bicubic(self, mode: str) -> None: case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off @@ -207,7 +208,7 @@ def test_enlarge_bicubic(self, mode) -> None: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode) -> None: + def test_enlarge_lanczos(self, mode: str) -> None: case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( @@ -230,7 +231,7 @@ def test_box_filter_correct_range(self) -> None: class TestCoreResampleConsistency: - def make_case(self, mode, fill): + def make_case(self, mode: str, fill: tuple[int, int, int] | float): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] @@ -265,7 +266,7 @@ def test_32f(self) -> None: class TestCoreResampleAlphaCorrect: - def make_levels_case(self, mode): + def make_levels_case(self, mode: str) -> Image.Image: i = Image.new(mode, (256, 16)) px = i.load() for y in range(i.size[1]): @@ -275,7 +276,7 @@ def make_levels_case(self, mode): px[x, y] = tuple(pix) return i - def run_levels_case(self, i) -> None: + def run_levels_case(self, i: Image.Image) -> None: px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} @@ -302,7 +303,9 @@ def test_levels_la(self) -> None: self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) - def make_dirty_case(self, mode, clean_pixel, dirty_pixel): + def make_dirty_case( + self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] + ) -> Image.Image: i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() xdiv4 = i.size[0] // 4 @@ -312,7 +315,7 @@ def make_dirty_case(self, mode, clean_pixel, dirty_pixel): px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dirty_case(self, i, clean_pixel) -> None: + def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): @@ -432,7 +435,7 @@ class TestCoreResampleBox: Image.Resampling.LANCZOS, ), ) - def test_wrong_arguments(self, resample) -> None: + def test_wrong_arguments(self, resample: Image.Resampling) -> None: im = hopper() im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -459,8 +462,12 @@ def test_wrong_arguments(self, resample) -> None: with pytest.raises(ValueError, match="can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) - def resize_tiled(self, im, dst_size, xtiles, ytiles): - def split_range(size, tiles): + def resize_tiled( + self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int + ) -> Image.Image: + def split_range( + size: int, tiles: int + ) -> Generator[tuple[int, int], None, None]: scale = size / tiles for i in range(tiles): yield int(round(scale * i)), int(round(scale * (i + 1))) @@ -518,7 +525,7 @@ def test_subsample(self) -> None: @pytest.mark.parametrize( "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) ) - def test_formats(self, mode, resample) -> None: + def test_formats(self, mode: str, resample: Image.Resampling) -> None: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) @@ -558,7 +565,7 @@ def test_no_passthrough(self) -> None: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_horizontal(self, flt) -> None: + def test_skip_horizontal(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() @@ -581,7 +588,7 @@ def test_skip_horizontal(self, flt) -> None: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_vertical(self, flt) -> None: + def test_skip_vertical(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index ea6e80f1ee7..b65ea874063 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -5,11 +5,11 @@ from PIL import Image, ImageMath -def pixel(im): - if hasattr(im, "im"): - return f"{im.mode} {repr(im.getpixel((0, 0)))}" +def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans + else: + return f"{im.mode} {repr(im.getpixel((0, 0)))}" A = Image.new("L", (1, 1), 1) @@ -60,7 +60,7 @@ def test_ops() -> None: "(lambda: (lambda: exec('pass'))())()", ), ) -def test_prevent_exec(expression) -> None: +def test_prevent_exec(expression: str) -> None: with pytest.raises(ValueError): ImageMath.eval(expression) From 463c36821136652a05517a4db810a265d25c9b0c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 7 Feb 2024 21:02:34 +1100 Subject: [PATCH 0171/1178] Simplified code Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_imagemath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index b65ea874063..a21e2307d5f 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -8,8 +8,8 @@ def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans - else: - return f"{im.mode} {repr(im.getpixel((0, 0)))}" + + return f"{im.mode} {repr(im.getpixel((0, 0)))}" A = Image.new("L", (1, 1), 1) From c93b23239d4cbe8b8c6d4d6c04db35763a25db62 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Wed, 7 Feb 2024 20:20:27 -0500 Subject: [PATCH 0172/1178] Update src/_webp.c Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/_webp.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 4e7d41f1121..927d8dc3f4c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -448,18 +448,19 @@ PyObject * _anim_decoder_get_next(PyObject *self) { uint8_t *buf; int timestamp; + int ok; PyObject *bytes; PyObject *ret; ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; ImagingSectionEnter(&cookie); - if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { - ImagingSectionLeave(&cookie); + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp) + ImagingSectionLeave(&cookie); + if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } - ImagingSectionLeave(&cookie); bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); From cb39b1c89e71f67ce4dacd41cebf723ff86306dd Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:29:06 +1100 Subject: [PATCH 0173/1178] Corrected syntax --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 927d8dc3f4c..47592547cc2 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -455,7 +455,7 @@ _anim_decoder_get_next(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; ImagingSectionEnter(&cookie); - ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp) + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); ImagingSectionLeave(&cookie); if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); From a276cf2c9fadf39cc5e663e44bc160c566d7c050 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Feb 2024 18:48:38 +1100 Subject: [PATCH 0174/1178] Use _typing alias --- src/PIL/ImageFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9f8394d63b4..7be2fdf0445 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 = "", From a118a82c30acf6427653b129fab263fde3bdbbac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Feb 2024 18:35:37 +1100 Subject: [PATCH 0175/1178] Use os.path.realpath consistently when os.fspath is used --- src/PIL/Image.py | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d7d0a1ae7f0..adb63b07f79 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2385,7 +2385,7 @@ def save(self, fp, format=None, **params) -> None: filename = "" open_fp = False if is_path(fp): - filename = os.fspath(fp) + filename = os.path.realpath(os.fspath(fp)) open_fp = True elif fp == sys.stdout: try: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7be2fdf0445..256c581df0c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,7 +230,7 @@ def load_from_bytes(f): ) if is_path(font): - font = os.fspath(font) + font = os.path.realpath(os.fspath(font)) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From e6a521130e975f89529442597735a6a48cc3685f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Feb 2024 19:47:09 +1100 Subject: [PATCH 0176/1178] If previous disposal was 2, do not fill identical pixels --- Tests/test_file_gif.py | 3 +++ src/PIL/GifImagePlugin.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index db9d3586c4d..0399c6b6798 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -647,6 +647,9 @@ def test_dispose2_palette(tmp_path: Path) -> None: # Center remains red every frame assert rgb_img.getpixel((50, 50)) == circle + # Check that frame transparency wasn't added unnecessarily + assert img._frame_transparency is None + def test_dispose2_diff(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dc842d7a30b..73a5487d9a7 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -638,7 +638,11 @@ def _write_multiple_frames(im, fp, palette): background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) delta, bbox = _getbbox(background_im, im_frame) - if encoderinfo.get("optimize") and im_frame.mode != "1": + if ( + encoderinfo.get("optimize") + and im_frames[-1]["encoderinfo"].get("disposal") != 2 + and im_frame.mode != "1" + ): if "transparency" not in encoderinfo: try: encoderinfo["transparency"] = ( From 152a24e13abfe099d4cf75dc7982290feb200ad2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 16:48:02 +1100 Subject: [PATCH 0177/1178] Simplified code --- src/PIL/Image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index adb63b07f79..231674f5448 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3240,10 +3240,8 @@ def open(fp, mode="r", formats=None) -> Image: exclusive_fp = False filename = "" - if isinstance(fp, os.PathLike): + if is_path(fp): filename = os.path.realpath(os.fspath(fp)) - elif is_path(fp): - filename = fp if filename: fp = builtins.open(filename, "rb") From 373c62e5cbcd27a4e497e61b44cda89911f38807 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 19:47:50 +1100 Subject: [PATCH 0178/1178] Use subprocess with CREATE_NO_WINDOW flag in WindowsViewer --- src/PIL/ImageShow.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c03122c11aa..4a801e5b0fb 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -138,6 +138,17 @@ def get_command(self, file: str, **options: Any) -> str: f'&& del /f "{file}"' ) + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.Popen( + self.get_command(path, **options), + shell=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), + ) # nosec + return 1 + if sys.platform == "win32": register(WindowsViewer) From 19a6edeecce2f3605fcdb074c00ac0152c6bdf05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 19:50:45 +1100 Subject: [PATCH 0179/1178] Added type hints --- pyproject.toml | 10 ---- src/PIL/DdsImagePlugin.py | 16 ++++-- src/PIL/ImImagePlugin.py | 4 +- src/PIL/Image.py | 86 +++++++++++++++++++---------- src/PIL/ImageQt.py | 12 +++- src/PIL/PdfParser.py | 11 +++- src/PIL/PyAccess.py | 1 + src/PIL/TiffImagePlugin.py | 109 +++++++++++++++++++++---------------- src/PIL/TiffTags.py | 4 +- src/PIL/_imaging.pyi | 5 ++ src/PIL/_tkinter_finder.py | 3 +- src/PIL/_webp.pyi | 5 ++ tox.ini | 5 ++ 13 files changed, 171 insertions(+), 100 deletions(-) create mode 100644 src/PIL/_imaging.pyi create mode 100644 src/PIL/_webp.pyi diff --git a/pyproject.toml b/pyproject.toml index 652ae36333c..48c59f2a1b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,16 +141,6 @@ warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true exclude = [ - '^src/PIL/_tkinter_finder.py$', - '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/Image.py$', - '^src/PIL/ImageQt.py$', - '^src/PIL/ImImagePlugin.py$', '^src/PIL/MicImagePlugin.py$', - '^src/PIL/PdfParser.py$', - '^src/PIL/PyAccess.py$', - '^src/PIL/TiffImagePlugin.py$', - '^src/PIL/TiffTags.py$', - '^src/PIL/WebPImagePlugin.py$', ] diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 3785174ef65..be17f4223aa 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -270,13 +270,17 @@ class D3DFMT(IntEnum): # Backward compatibility layer module = sys.modules[__name__] for item in DDSD: + assert item.name is not None setattr(module, "DDSD_" + item.name, item.value) -for item in DDSCAPS: - setattr(module, "DDSCAPS_" + item.name, item.value) -for item in DDSCAPS2: - setattr(module, "DDSCAPS2_" + item.name, item.value) -for item in DDPF: - setattr(module, "DDPF_" + item.name, item.value) +for item1 in DDSCAPS: + assert item1.name is not None + setattr(module, "DDSCAPS_" + item1.name, item1.value) +for item2 in DDSCAPS2: + assert item2.name is not None + setattr(module, "DDSCAPS2_" + item2.name, item2.value) +for item3 in DDPF: + assert item3.name is not None + setattr(module, "DDPF_" + item3.name, item3.value) DDS_FOURCC = DDPF.FOURCC DDS_RGB = DDPF.RGB diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 97d726a8a65..4613e40b60f 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -93,8 +93,8 @@ for i in ["32S"]: OPEN[f"L {i} image"] = ("I", f"I;{i}") OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for i in range(2, 33): - OPEN[f"L*{i} image"] = ("F", f"F;{i}") +for j in range(2, 33): + OPEN[f"L*{j} image"] = ("F", f"F;{j}") # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 111d060129e..d32a0fc199e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -26,6 +26,7 @@ from __future__ import annotations +import abc import atexit import builtins import io @@ -40,11 +41,8 @@ from collections.abc import Callable, MutableMapping from enum import IntEnum from pathlib import Path - -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None +from types import ModuleType +from typing import IO, TYPE_CHECKING, Any # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -60,6 +58,12 @@ from ._binary import i32le, o32be, o32le from ._util import DeferredError, is_path +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + logger = logging.getLogger(__name__) @@ -110,6 +114,7 @@ class DecompressionBombError(Exception): USE_CFFI_ACCESS = False +cffi: ModuleType | None try: import cffi except ImportError: @@ -211,14 +216,22 @@ class Quantize(IntEnum): # -------------------------------------------------------------------- # Registries -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -SAVE_ALL = {} -EXTENSION = {} -DECODERS = {} -ENCODERS = {} +if TYPE_CHECKING: + from . import ImageFile # pragma: no cover +ID: list[str] = [] +OPEN: dict[ + str, + tuple[ + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + Callable[[bytes], bool] | None, + ], +] = {} +MIME: dict[str, str] = {} +SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +EXTENSION: dict[str, str] = {} +DECODERS: dict[str, object] = {} +ENCODERS: dict[str, object] = {} # -------------------------------------------------------------------- # Modes @@ -2383,12 +2396,12 @@ def save(self, fp, format=None, **params) -> None: may have been created, and may contain partial data. """ - filename = "" + filename: str | bytes = "" open_fp = False if isinstance(fp, Path): filename = str(fp) open_fp = True - elif is_path(fp): + elif isinstance(fp, (str, bytes)): filename = fp open_fp = True elif fp == sys.stdout: @@ -2398,7 +2411,7 @@ def save(self, fp, format=None, **params) -> None: pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = fp.name + filename = os.path.realpath(os.fspath(fp.name)) # may mutate self! self._ensure_mutable() @@ -2409,7 +2422,8 @@ def save(self, fp, format=None, **params) -> None: preinit() - ext = os.path.splitext(filename)[1].lower() + filename_ext = os.path.splitext(filename)[1].lower() + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext if not format: if ext not in EXTENSION: @@ -2451,7 +2465,7 @@ def save(self, fp, format=None, **params) -> None: if open_fp: fp.close() - def seek(self, frame) -> Image: + def seek(self, frame) -> None: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an @@ -2511,10 +2525,9 @@ def split(self) -> tuple[Image, ...]: self.load() if self.im.bands == 1: - ims = [self.copy()] + return (self.copy(),) else: - ims = map(self._new, self.im.split()) - return tuple(ims) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ @@ -2871,7 +2884,14 @@ class ImageTransformHandler: (for use with :py:meth:`~PIL.Image.Image.transform`) """ - pass + @abc.abstractmethod + def transform( + self, + size: tuple[int, int], + image: Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image: + pass # pragma: no cover # -------------------------------------------------------------------- @@ -3243,11 +3263,9 @@ def open(fp, mode="r", formats=None) -> Image: raise TypeError(msg) exclusive_fp = False - filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) - elif is_path(fp): - filename = fp + filename: str | bytes = "" + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) if filename: fp = builtins.open(filename, "rb") @@ -3421,7 +3439,11 @@ def merge(mode, bands): # Plugin registry -def register_open(id, factory, accept=None) -> None: +def register_open( + id, + factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + accept: Callable[[bytes], bool] | None = None, +) -> None: """ Register an image file plugin. This function should not be used in application code. @@ -3631,7 +3653,13 @@ def _apply_env_variables(env=None): atexit.register(core.clear_cache) -class Exif(MutableMapping): +if TYPE_CHECKING: + _ExifBase = MutableMapping[int, Any] # pragma: no cover +else: + _ExifBase = MutableMapping + + +class Exif(_ExifBase): """ This class provides read and write access to EXIF image data:: diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 6377c750105..293ba4941e3 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,19 +19,26 @@ import sys from io import BytesIO +from typing import Callable from . import Image from ._util import is_path +qt_version: str | None qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], ] # If a version has already been imported, attempt it first -qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) -for qt_version, qt_module in qt_versions: +qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) +for version, qt_module in qt_versions: try: + QBuffer: type + QIODevice: type + QImage: type + QPixmap: type + qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": from PyQt6.QtCore import QBuffer, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba @@ -41,6 +48,7 @@ except (ImportError, RuntimeError): continue qt_is_installed = True + qt_version = version break else: qt_is_installed = False diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 0144600066f..9aa8dde8327 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,6 +8,7 @@ import re import time import zlib +from typing import TYPE_CHECKING, Any, List, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -239,12 +240,18 @@ def __bytes__(self): return bytes(result) -class PdfArray(list): +class PdfArray(List[Any]): def __bytes__(self): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -class PdfDict(collections.UserDict): +if TYPE_CHECKING: + _DictBase = collections.UserDict[Union[str, bytes], Any] # pragma: no cover +else: + _DictBase = collections.UserDict + + +class PdfDict(_DictBase): def __setattr__(self, key, value): if key == "data": collections.UserDict.__setattr__(self, key, value) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 07bb712d83e..2c831913d69 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -25,6 +25,7 @@ from ._deprecate import deprecate +FFI: type try: from cffi import FFI diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index e20d4d5ea81..af22d76cbf5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,6 +50,7 @@ from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational +from typing import TYPE_CHECKING, Any, Callable from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -306,6 +307,13 @@ def _limit_signed_rational(val, max_val, min_val): _write_dispatch = {} +def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + + return delegate + + class IFDRational(Rational): """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. @@ -391,12 +399,6 @@ def __setstate__(self, state): self._numerator = _numerator self._denominator = _denominator - def _delegate(op): - def delegate(self, *args): - return getattr(self._val, op)(*args) - - return delegate - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod','rmod', 'pow','rpow', 'pos', 'neg', @@ -436,7 +438,50 @@ def delegate(self, *args): __int__ = _delegate("__int__") -class ImageFileDirectory_v2(MutableMapping): +def _register_loader(idx, size): + def decorator(func): + from .TiffTags import TYPES + + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func # noqa: F821 + return func + + return decorator + + +def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func # noqa: F821 + return func + + return decorator + + +def _register_basic(idx_fmt_name): + from .TiffTags import TYPES + + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack(f"{len(data) // size}{fmt}", data) + ), + ) + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + +if TYPE_CHECKING: + _IFDv2Base = MutableMapping[int, Any] # pragma: no cover +else: + _IFDv2Base = MutableMapping + + +class ImageFileDirectory_v2(_IFDv2Base): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping): """ + _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _write_dispatch: dict[int, Callable[..., Any]] = {} + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. @@ -531,7 +579,10 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) - legacy_api = property(lambda self: self._legacy_api) + + @property + def legacy_api(self): + return self._legacy_api @legacy_api.setter def legacy_api(self, value): @@ -674,40 +725,6 @@ def _unpack(self, fmt, data): def _pack(self, fmt, *values): return struct.pack(self._endian + fmt, *values) - def _register_loader(idx, size): - def decorator(func): - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - def _register_writer(idx): - def decorator(func): - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - def _register_basic(idx_fmt_name): - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - list( map( _register_basic, @@ -995,7 +1012,7 @@ def __init__(self, *args, **kwargs): tagdata = property(lambda self: self._tagdata) # defined in ImageFileDirectory_v2 - tagtype: dict + tagtype: dict[int, int] """Dictionary of tag types""" @classmethod @@ -1835,11 +1852,11 @@ def _save(im, fp, filename): tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) - e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - e.setimage(im.im, (0, 0) + im.size) + encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + encoder.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - errcode, data = e.encode(16 * 1024)[1:] + errcode, data = encoder.encode(16 * 1024)[1:] if not _fp: fp.write(data) if errcode: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 88ff2f4fcd5..b9419393119 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -22,7 +22,7 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): - __slots__ = [] + __slots__: list[str] = [] def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): return super().__new__(cls, value, name, type, length, enum or {}) @@ -437,7 +437,7 @@ def _populate(): ## # Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = {} +TYPES: dict[int, str] = {} # # These tags are handled by default in libtiff, without diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imaging.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 71c0ad4651b..beddfb0628a 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -5,7 +5,8 @@ import sys import tkinter -from tkinter import _tkinter as tk + +tk = getattr(tkinter, "_tkinter") try: if hasattr(sys, "pypy_find_executable"): diff --git a/src/PIL/_webp.pyi b/src/PIL/_webp.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_webp.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/tox.ini b/tox.ini index fb6746ce7bf..8c818df7a6a 100644 --- a/tox.ini +++ b/tox.ini @@ -33,9 +33,14 @@ commands = [testenv:mypy] skip_install = true deps = + IceSpringPySideStubs-PyQt6 + IceSpringPySideStubs-PySide6 ipython mypy==1.7.1 numpy + packaging + types-cffi + types-defusedxml extras = typing commands = From 517b797132a65a8a873d0c22008d760d2a706ff6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 20:47:32 +1100 Subject: [PATCH 0180/1178] Removed FileDescriptor --- src/PIL/GdImageFile.py | 6 ++---- src/PIL/_typing.py | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 315ac6d6c1a..88b87a22cd6 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -32,7 +32,7 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._typing import FileDescriptor, StrOrBytesPath +from ._typing import StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -81,9 +81,7 @@ def _open(self) -> None: ] -def open( - fp: StrOrBytesPath | FileDescriptor | IO[bytes], 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/_typing.py b/src/PIL/_typing.py index 346702037d2..7075e86726a 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -27,8 +27,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, __length: int = ...) -> _T_co: ... -FileDescriptor = int StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -__all__ = ["FileDescriptor", "TypeGuard", "StrOrBytesPath", "SupportsRead"] +__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] From 430f50606e2fc5620c6605cd43eb360be6bbd655 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 21:33:32 +1100 Subject: [PATCH 0181/1178] Current delta is determined by previous disposal --- Tests/test_file_gif.py | 19 +++++++++++++++++++ src/PIL/GifImagePlugin.py | 10 +++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0399c6b6798..6527d90de96 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -737,6 +737,25 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: assert im.n_frames == 3 +def test_dispose2_previous_frame(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (100, 100)) + im.info["transparency"] = 0 + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], 1) + im.putpalette((0, 0, 0, 255, 0, 0)) + + im2 = Image.new("P", (100, 100)) + im2.putpalette((0, 0, 0)) + + im.save(out, save_all=True, append_images=[im2], disposal=[0, 2]) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 0, 0, 255) + + def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 73a5487d9a7..9368dd7e7c4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -629,7 +629,7 @@ def _write_multiple_frames(im, fp, palette): "duration" ] continue - if encoderinfo.get("disposal") == 2: + if im_frames[-1]["encoderinfo"].get("disposal") == 2: if background_im is None: color = im.encoderinfo.get( "transparency", im.info.get("transparency", (0, 0, 0)) @@ -637,12 +637,8 @@ def _write_multiple_frames(im, fp, palette): background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) - delta, bbox = _getbbox(background_im, im_frame) - if ( - encoderinfo.get("optimize") - and im_frames[-1]["encoderinfo"].get("disposal") != 2 - and im_frame.mode != "1" - ): + bbox = _getbbox(background_im, im_frame)[1] + elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: try: encoderinfo["transparency"] = ( From 68db96981c0819efc51bea995915eaa389a292e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:50:48 +1100 Subject: [PATCH 0182/1178] Removed else Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d32a0fc199e..c3ab6217452 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2526,8 +2526,7 @@ def split(self) -> tuple[Image, ...]: self.load() if self.im.bands == 1: return (self.copy(),) - else: - return tuple(map(self._new, self.im.split())) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ From d02a778efd443db9f69233763f187e14eebde6db Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:57:59 +1100 Subject: [PATCH 0183/1178] Removed no cover pragmas Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 6 +++--- src/PIL/PdfParser.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c3ab6217452..d9d708d5da4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -217,7 +217,7 @@ class Quantize(IntEnum): # Registries if TYPE_CHECKING: - from . import ImageFile # pragma: no cover + from . import ImageFile ID: list[str] = [] OPEN: dict[ str, @@ -2890,7 +2890,7 @@ def transform( image: Image, **options: dict[str, str | int | tuple[int, ...] | list[int]], ) -> Image: - pass # pragma: no cover + pass # -------------------------------------------------------------------- @@ -3653,7 +3653,7 @@ def _apply_env_variables(env=None): if TYPE_CHECKING: - _ExifBase = MutableMapping[int, Any] # pragma: no cover + _ExifBase = MutableMapping[int, Any] else: _ExifBase = MutableMapping diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 9aa8dde8327..4c510173814 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -246,7 +246,7 @@ def __bytes__(self): if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] # pragma: no cover + _DictBase = collections.UserDict[Union[str, bytes], Any] else: _DictBase = collections.UserDict diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index af22d76cbf5..3ba4de9d12e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -476,7 +476,7 @@ def _register_basic(idx_fmt_name): if TYPE_CHECKING: - _IFDv2Base = MutableMapping[int, Any] # pragma: no cover + _IFDv2Base = MutableMapping[int, Any] else: _IFDv2Base = MutableMapping From 8ef0ffc2b849245bde6f96b58b4af48bf498bda7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:37:42 +1100 Subject: [PATCH 0184/1178] Removed no cover pragma --- src/PIL/ImageShow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c03122c11aa..d90545e92ec 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -184,7 +184,7 @@ class UnixViewer(Viewer): @abc.abstractmethod def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass # pragma: no cover + pass def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] From e614bbfe501811bcb4a080ba9f07d745ba3ad231 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:39:18 +1100 Subject: [PATCH 0185/1178] Exclude code only for type checking --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 46df3f90d27..115286b749c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,7 @@ exclude_also = if DEBUG: # Don't complain about compatibility code for missing optional dependencies except ImportError + if TYPE_CHECKING: [run] omit = From 112a5a4813f34235530c2ac382108a5cf722789a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:40:24 +1100 Subject: [PATCH 0186/1178] Exclude abstract methods --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 115286b749c..ca5f114c645 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,7 @@ exclude_also = # Don't complain about compatibility code for missing optional dependencies except ImportError if TYPE_CHECKING: + @abc.abstractmethod [run] omit = From 3977124908b934a9b037d1e8ba5549393bed9dda Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 10 Feb 2024 14:54:20 +0200 Subject: [PATCH 0187/1178] Update docs/reference/internal_modules.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/reference/internal_modules.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index c3cc700607f..899e4966ff2 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,10 +33,6 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. -.. py:class:: FileDescriptor - - Typing alias. - .. py:class:: StrOrBytesPath Typing alias. From 3f6422b512ff39cffaa5a37915c970d7683b6d62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Feb 2024 09:28:53 +1100 Subject: [PATCH 0188/1178] Added type hints --- Tests/helper.py | 2 +- Tests/test_image_access.py | 47 ++++++++++++++++++-------------- Tests/test_image_array.py | 10 ++++--- Tests/test_image_draft.py | 7 ++++- Tests/test_image_entropy.py | 2 +- Tests/test_image_filter.py | 27 ++++++++++++------ Tests/test_image_getextrema.py | 2 +- Tests/test_image_getpalette.py | 2 +- Tests/test_image_paste.py | 14 +++++----- Tests/test_image_putdata.py | 6 ++-- Tests/test_image_putpalette.py | 4 +-- Tests/test_image_resample.py | 8 ++++-- Tests/test_image_rotate.py | 14 +++++++--- Tests/test_image_thumbnail.py | 2 +- Tests/test_image_transform.py | 50 ++++++++++++++++++++++++---------- 15 files changed, 124 insertions(+), 73 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 3e2a40e02da..b9888394659 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -244,7 +244,7 @@ def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: +def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index e4cb2dad102..3bdaea75026 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -4,6 +4,7 @@ import subprocess import sys import sysconfig +from types import ModuleType import pytest @@ -23,6 +24,7 @@ except ImportError: cffi = None +numpy: ModuleType | None try: import numpy except ImportError: @@ -71,9 +73,10 @@ def test_sanity(self) -> None: pix1 = im1.load() pix2 = im2.load() - for x, y in ((0, "0"), ("0", 0)): - with pytest.raises(TypeError): - pix1[x, y] + with pytest.raises(TypeError): + pix1[0, "0"] + with pytest.raises(TypeError): + pix1["0", 0] for y in range(im1.size[1]): for x in range(im1.size[0]): @@ -123,12 +126,13 @@ def test_numpy(self) -> None: im = hopper() pix = im.load() + assert numpy is not None assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) class TestImageGetPixel(AccessTest): @staticmethod - def color(mode): + def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) if bands == 1: return 1 @@ -138,12 +142,13 @@ def color(mode): return (16, 32, 49) return tuple(range(1, bands + 1)) - def check(self, mode, expected_color=None) -> None: + def check(self, mode: str, expected_color_int: int | None = None) -> None: if self._need_cffi_access and mode.startswith("BGR;"): pytest.skip("Support not added to deprecated module for BGR;* modes") - if not expected_color: - expected_color = self.color(mode) + expected_color = ( + expected_color_int if expected_color_int is not None else self.color(mode) + ) # check putpixel im = Image.new(mode, (1, 1), None) @@ -222,7 +227,7 @@ def check(self, mode, expected_color=None) -> None: "YCbCr", ), ) - def test_basic(self, mode) -> None: + def test_basic(self, mode: str) -> None: self.check(mode) def test_list(self) -> None: @@ -231,14 +236,14 @@ def test_list(self) -> None: @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) - def test_signedness(self, mode, expected_color) -> None: + def test_signedness(self, mode: str, expected_color: int) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode, color) -> None: + def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) @@ -262,7 +267,7 @@ class TestCffiGetPixel(TestImageGetPixel): class TestCffi(AccessTest): _need_cffi_access = True - def _test_get_access(self, im) -> None: + def _test_get_access(self, im: Image.Image) -> None: """Do we get the same thing as the old pixel access Using private interfaces, forcing a capi access and @@ -299,7 +304,7 @@ def test_get_vs_c(self) -> None: # im = Image.new('I;32B', (10, 10), 2**10) # self._test_get_access(im) - def _test_set_access(self, im, color) -> None: + def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? Using private interfaces, forcing a capi access and @@ -359,7 +364,7 @@ def test_reference_counting(self) -> None: assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode) -> None: + def test_p_putpixel_rgb_rgba(self, mode: str) -> None: for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): @@ -377,7 +382,7 @@ class TestImagePutPixelError(AccessTest): INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode) -> None: + def test_putpixel_type_error1(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): @@ -400,14 +405,16 @@ def test_putpixel_type_error1(self, mode) -> None: ), ), ) - def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None: + def test_putpixel_invalid_number_of_bands( + self, mode: str, band_numbers: tuple[int, ...], match: str + ) -> None: im = hopper(mode) for band_number in band_numbers: with pytest.raises(TypeError, match=match): im.putpixel((0, 0), (0,) * band_number) @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode) -> None: + def test_putpixel_type_error2(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises( @@ -416,7 +423,7 @@ def test_putpixel_type_error2(self, mode) -> None: im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode) -> None: + def test_putpixel_overflow_error(self, mode: str) -> None: im = hopper(mode) with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) @@ -428,7 +435,7 @@ class TestEmbeddable: def test_embeddable(self) -> None: import ctypes - from setuptools.command.build_ext import new_compiler + from setuptools.command import build_ext with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( @@ -457,7 +464,7 @@ def test_embeddable(self) -> None: % sys.prefix.replace("\\", "\\\\") ) - compiler = new_compiler() + compiler = getattr(build_ext, "new_compiler")() compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( @@ -471,7 +478,7 @@ def test_embeddable(self) -> None: env["PATH"] = sys.prefix + ";" + env["PATH"] # do not display the Windows Error Reporting dialog - ctypes.windll.kernel32.SetErrorMode(0x0002) + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 0125ab56af9..cf85ee4fa1c 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from packaging.version import parse as parse_version @@ -13,7 +15,7 @@ def test_toarray() -> None: - def test(mode): + def test(mode: str) -> tuple[tuple[int, ...], str, int]: ai = numpy.array(im.convert(mode)) return ai.shape, ai.dtype.str, ai.nbytes @@ -50,14 +52,14 @@ def test_fromarray() -> None: class Wrapper: """Class with API matching Image.fromarray""" - def __init__(self, img, arr_params) -> None: + def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None: self.img = img self.__array_interface__ = arr_params - def tobytes(self): + def tobytes(self) -> bytes: return self.img.tobytes() - def test(mode): + def test(mode: str) -> tuple[str, tuple[int, int], bool]: i = im.convert(mode) a = numpy.array(i) # Make wrapper instance for image, new array interface diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 54474311a09..1ce1a7cd8f6 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -7,7 +7,12 @@ pytestmark = skip_unless_feature("jpg") -def draft_roundtrip(in_mode, in_size, req_mode, req_size): +def draft_roundtrip( + in_mode: str, + in_size: tuple[int, int], + req_mode: str | None, + req_size: tuple[int, int] | None, +) -> Image.Image: im = Image.new(in_mode, in_size) data = tostring(im, "JPEG") im = fromstring(data) diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 01107ae6b88..c1dbb879b0b 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -4,7 +4,7 @@ def test_entropy() -> None: - def entropy(mode): + def entropy(mode: str) -> float: return hopper(mode).entropy() assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 2b6787933cd..6a10ae4532b 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -36,7 +36,7 @@ ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply, mode) -> None: +def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: im = hopper(mode) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): out = im.filter(filter_to_apply) @@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode) -> None: @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode) -> None: +def test_sanity_error(mode: str) -> None: with pytest.raises(TypeError): im = hopper(mode) im.filter("hello") @@ -53,7 +53,7 @@ def test_sanity_error(mode) -> None: # crashes on small images @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size) -> None: +def test_crash(size: tuple[int, int]) -> None: im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) @@ -67,7 +67,10 @@ def test_crash(size) -> None: ("RGB", ((4, 0, 0), (0, 0, 0))), ), ) -def test_modefilter(mode, expected) -> None: +def test_modefilter( + mode: str, + expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]], +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -90,7 +93,13 @@ def test_modefilter(mode, expected) -> None: ("F", (0.0, 4.0, 8.0)), ), ) -def test_rankfilter(mode, expected) -> None: +def test_rankfilter( + mode: str, + expected: ( + tuple[float, float, float] + | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] + ), +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -106,7 +115,7 @@ def test_rankfilter(mode, expected) -> None: @pytest.mark.parametrize( "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) ) -def test_rankfilter_error(filter) -> None: +def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None: with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -137,7 +146,7 @@ def test_kernel_not_enough_coefficients() -> None: @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode) -> None: +def test_consistency_3x3(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -163,7 +172,7 @@ def test_consistency_3x3(mode) -> None: @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode) -> None: +def test_consistency_5x5(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss_more" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -199,7 +208,7 @@ def test_consistency_5x5(mode) -> None: (2, -2), ), ) -def test_invalid_box_blur_filter(radius) -> None: +def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None: with pytest.raises(ValueError): ImageFilter.BoxBlur(radius) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 0107fdcc426..a5b974459a6 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -6,7 +6,7 @@ def test_extrema() -> None: - def extrema(mode): + def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]: return hopper(mode).getextrema() assert extrema("1") == (0, 255) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index e7304c98f30..6a8f157fc3e 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -6,7 +6,7 @@ def test_palette() -> None: - def palette(mode): + def palette(mode: str) -> list[int] | None: p = hopper(mode).getpalette() if p: return p[:10] diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index c4d7a5dd254..ce73455729a 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -46,7 +46,7 @@ def assert_9points_paste( self.assert_9points_image(im, expected) @CachedProperty - def mask_1(self): + def mask_1(self) -> Image.Image: mask = Image.new("1", (self.size, self.size)) px = mask.load() for y in range(mask.height): @@ -55,11 +55,11 @@ def mask_1(self): return mask @CachedProperty - def mask_L(self): + def mask_L(self) -> Image.Image: return self.gradient_L.transpose(Image.Transpose.ROTATE_270) @CachedProperty - def gradient_L(self): + def gradient_L(self) -> Image.Image: gradient = Image.new("L", (self.size, self.size)) px = gradient.load() for y in range(gradient.height): @@ -68,7 +68,7 @@ def gradient_L(self): return gradient @CachedProperty - def gradient_RGB(self): + def gradient_RGB(self) -> Image.Image: return Image.merge( "RGB", [ @@ -79,7 +79,7 @@ def gradient_RGB(self): ) @CachedProperty - def gradient_LA(self): + def gradient_LA(self) -> Image.Image: return Image.merge( "LA", [ @@ -89,7 +89,7 @@ def gradient_LA(self): ) @CachedProperty - def gradient_RGBA(self): + def gradient_RGBA(self) -> Image.Image: return Image.merge( "RGBA", [ @@ -101,7 +101,7 @@ def gradient_RGBA(self): ) @CachedProperty - def gradient_RGBa(self): + def gradient_RGBa(self) -> Image.Image: return Image.merge( "RGBa", [ diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 10301991624..73145faac15 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -31,7 +31,7 @@ def test_sanity() -> None: def test_long_integers() -> None: # see bug-200802-systemerror - def put(value): + def put(value: int) -> tuple[int, int, int, int]: im = Image.new("RGBA", (1, 1)) im.putdata([value]) return im.getpixel((0, 0)) @@ -58,7 +58,7 @@ def test_mode_with_L_with_float() -> None: @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode) -> None: +def test_mode_i(mode: str) -> None: src = hopper("L") data = list(src.getdata()) im = Image.new(mode, src.size, 0) @@ -79,7 +79,7 @@ def test_mode_F() -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode) -> None: +def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] im = Image.new(mode, (1, 2)) im.putdata(data) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index ffe2551d239..cc7cf58f0fc 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -8,7 +8,7 @@ def test_putpalette() -> None: - def palette(mode): + def palette(mode: str) -> str | tuple[str, list[int]]: im = hopper(mode).copy() im.putpalette(list(range(256)) * 3) p = im.getpalette() @@ -81,7 +81,7 @@ def test_putpalette_with_alpha_values() -> None: ("RGBAX", (1, 2, 3, 4, 0)), ), ) -def test_rgba_palette(mode, palette) -> None: +def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f3ec12c053b..7090ff9cdb8 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -231,11 +231,13 @@ def test_box_filter_correct_range(self) -> None: class TestCoreResampleConsistency: - def make_case(self, mode: str, fill: tuple[int, int, int] | float): + def make_case( + self, mode: str, fill: tuple[int, int, int] | float + ) -> tuple[Image.Image, tuple[int, ...]]: im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - def run_case(self, case) -> None: + def run_case(self, case: tuple[Image.Image, Image.Image]) -> None: channel, color = case px = channel.load() for x in range(channel.size[0]): @@ -353,7 +355,7 @@ def test_dirty_pixels_la(self) -> None: class TestCoreResamplePasses: @contextmanager - def count(self, diff): + def count(self, diff: int) -> Generator[None, None, None]: count = Image.core.get_stats()["new_count"] yield assert Image.core.get_stats()["new_count"] - count == diff diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 51e0f585421..c10c96da6f9 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -12,7 +12,13 @@ ) -def rotate(im, mode, angle, center=None, translate=None) -> None: +def rotate( + im: Image.Image, + mode: str, + angle: int, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, +) -> None: out = im.rotate(angle, center=center, translate=translate) assert out.mode == mode assert out.size == im.size # default rotate clips output @@ -27,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None) -> None: @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode) -> None: +def test_mode(mode: str) -> None: im = hopper(mode) rotate(im, mode, 45) @pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle) -> None: +def test_angle(angle: int) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) @@ -42,7 +48,7 @@ def test_angle(angle) -> None: @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle) -> None: +def test_zero(angle: int) -> None: im = Image.new("RGB", (0, 0)) rotate(im, im.mode, angle) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6aeeea2ed0d..2ca1d2cfc03 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -111,7 +111,7 @@ def test_load_first_unless_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft - def im_draft(mode, size): + def im_draft(mode: str, size: tuple[int, int]): result = draft(mode, size) assert result is not None diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 1067dd563c1..638d1224710 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +from typing import Callable import pytest @@ -91,7 +92,7 @@ def test_quad(self) -> None: ("LA", (76, 0)), ), ) - def test_fill(self, mode, expected_pixel) -> None: + def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -142,7 +143,9 @@ def test_mesh(self) -> None: assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - def _test_alpha_premult(self, op) -> None: + def _test_alpha_premult( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image] + ) -> None: # create image with half white, half black, # with the black half transparent. # do op, @@ -159,13 +162,13 @@ def _test_alpha_premult(self, op) -> None: assert 40 * 10 == hist[-1] def test_alpha_premult_resize(self) -> None: - def op(im, sz): + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) def test_alpha_premult_transform(self) -> None: - def op(im, sz): + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR @@ -173,7 +176,9 @@ def op(im, sz): self._test_alpha_premult(op) - def _test_nearest(self, op, mode) -> None: + def _test_nearest( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str + ) -> None: # create white image with half transparent, # do op, # the image should remain white with half transparent @@ -196,15 +201,15 @@ def _test_nearest(self, op, mode) -> None: ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode) -> None: - def op(im, sz): + def test_nearest_resize(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode) -> None: - def op(im, sz): + def test_nearest_transform(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST @@ -227,7 +232,9 @@ def test_blank_fill(self) -> None: # Running by default, but I'd totally understand not doing it in # the future - pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] + pattern: list[Image.Image] | None = [ + Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65) + ] # Yeah. Watch some JIT optimize this out. pattern = None # noqa: F841 @@ -240,7 +247,7 @@ def test_missing_method_data(self) -> None: im.transform((100, 100), None) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample) -> None: + def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): @@ -250,7 +257,7 @@ def test_unknown_resampling_filter(self, resample) -> None: class TestImageTransformAffine: transform = Image.Transform.AFFINE - def _test_image(self): + def _test_image(self) -> Image.Image: im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) @@ -263,7 +270,7 @@ def _test_image(self): (270, Image.Transpose.ROTATE_270), ), ) - def test_rotate(self, deg, transpose) -> None: + def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None: im = self._test_image() angle = -math.radians(deg) @@ -313,7 +320,13 @@ def test_rotate(self, deg, transpose) -> None: (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: + def test_resize( + self, + scale: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: int, + ) -> None: im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -342,7 +355,14 @@ def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None: + def test_translate( + self, + x: float, + y: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: float, + ) -> None: im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) From ea0240bf2d414ff297c7959f904c252bae696ff3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:12:08 +1100 Subject: [PATCH 0189/1178] Use is None Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_image_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 3bdaea75026..380b89de86f 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -147,7 +147,7 @@ def check(self, mode: str, expected_color_int: int | None = None) -> None: pytest.skip("Support not added to deprecated module for BGR;* modes") expected_color = ( - expected_color_int if expected_color_int is not None else self.color(mode) + self.color(mode) if expected_color_int is None else expected_color_int ) # check putpixel From 4ce06aac3bc6aeb0425f78fc691210a839d6d875 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Feb 2024 21:06:17 +1100 Subject: [PATCH 0190/1178] Added type hints --- Tests/test_color_lut.py | 4 ++++ Tests/test_core_resources.py | 2 +- Tests/test_file_dds.py | 18 ++++++++-------- Tests/test_file_fli.py | 4 ++-- Tests/test_file_gribstub.py | 7 +++--- Tests/test_file_hdf5stub.py | 7 +++--- Tests/test_file_jpeg.py | 22 ++++++++++--------- Tests/test_file_palm.py | 8 +++---- Tests/test_file_tar.py | 2 +- Tests/test_file_webp_metadata.py | 2 ++ Tests/test_image.py | 37 +++++++++++++++++++------------- Tests/test_imagefontctl.py | 14 +++++++----- Tests/test_imagemorph.py | 10 ++++----- Tests/test_imagepalette.py | 2 +- Tests/test_pickle.py | 22 ++++++++++++------- Tests/test_psdraw.py | 4 ++-- Tests/test_sgi_crash.py | 2 +- Tests/test_shell_injection.py | 8 ++++++- Tests/test_tiff_crashes.py | 2 +- Tests/test_tiff_ifdrational.py | 6 +++--- 20 files changed, 108 insertions(+), 75 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 2bb1b57d482..c8886a7796d 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,6 +1,7 @@ from __future__ import annotations from array import array +from types import ModuleType import pytest @@ -8,6 +9,7 @@ from .helper import assert_image_equal +numpy: ModuleType | None try: import numpy except ImportError: @@ -397,6 +399,7 @@ def test_convert_table(self) -> None: @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_sources(self) -> None: + assert numpy is not None table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) with pytest.raises(ValueError, match="should have either channels"): lut = ImageFilter.Color3DLUT((5, 6, 7), table) @@ -430,6 +433,7 @@ def test_numpy_sources(self) -> None: @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_formats(self) -> None: + assert numpy is not None g = Image.linear_gradient("L") im = Image.merge( "RGB", diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 5eabe8f11ba..2c1de8bc3d2 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -187,6 +187,6 @@ def test_units(self) -> None: {"PILLOW_BLOCKS_MAX": "wat"}, ), ) - def test_warnings(self, var) -> None: + def test_warnings(self, var: dict[str, str]) -> None: with pytest.warns(UserWarning): Image._apply_env_variables(var) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index b78a0dd8109..ebc0e89a1c5 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -48,7 +48,7 @@ TEST_FILE_DX10_BC1_TYPELESS, ), ) -def test_sanity_dxt1_bc1(image_path) -> None: +def test_sanity_dxt1_bc1(image_path: str) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") @@ -96,7 +96,7 @@ def test_sanity_dxt5() -> None: TEST_FILE_BC4U, ), ) -def test_sanity_ati1_bc4u(image_path) -> None: +def test_sanity_ati1_bc4u(image_path: str) -> None: """Check ATI1 and BC4U images can be opened""" with Image.open(image_path) as im: @@ -117,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path) -> None: TEST_FILE_DX10_BC4_TYPELESS, ), ) -def test_dx10_bc4(image_path) -> None: +def test_dx10_bc4(image_path: str) -> None: """Check DX10 BC4 images can be opened""" with Image.open(image_path) as im: @@ -138,7 +138,7 @@ def test_dx10_bc4(image_path) -> None: TEST_FILE_BC5U, ), ) -def test_sanity_ati2_bc5u(image_path) -> None: +def test_sanity_ati2_bc5u(image_path: str) -> None: """Check ATI2 and BC5U images can be opened""" with Image.open(image_path) as im: @@ -162,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path) -> None: (TEST_FILE_BC5S, TEST_FILE_BC5S), ), ) -def test_dx10_bc5(image_path, expected_path) -> None: +def test_dx10_bc5(image_path: str, expected_path: str) -> None: """Check DX10 BC5 images can be opened""" with Image.open(image_path) as im: @@ -176,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path) -> None: @pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) -def test_dx10_bc6h(image_path) -> None: +def test_dx10_bc6h(image_path: str) -> None: """Check DX10 BC6H/BC6HS images can be opened""" with Image.open(image_path) as im: @@ -257,7 +257,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None: ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ], ) -def test_uncompressed(mode, size, test_file) -> None: +def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None: """Check uncompressed images can be opened""" with Image.open(test_file) as im: @@ -359,7 +359,7 @@ def test_unsupported_bitcount() -> None: "Tests/images/unimplemented_pfflags.dds", ), ) -def test_not_implemented(test_file) -> None: +def test_not_implemented(test_file: str) -> None: with pytest.raises(NotImplementedError): with Image.open(test_file): pass @@ -381,7 +381,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: ("RGBA", "Tests/images/pil123rgba.png"), ], ) -def test_save(mode, test_file, tmp_path: Path) -> None: +def test_save(mode: str, test_file: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(test_file) as im: assert im.mode == mode diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index a673d4af851..fc524721c2b 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -147,7 +147,7 @@ def test_seek() -> None: ], ) @pytest.mark.timeout(timeout=3) -def test_timeouts(test_file) -> None: +def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): @@ -160,7 +160,7 @@ def test_timeouts(test_file) -> None: "Tests/images/crash-5762152299364352.fli", ], ) -def test_crash(test_file) -> None: +def test_crash(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 4945468be40..096a5b88b21 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import IO import pytest @@ -55,15 +56,15 @@ class TestHandler: loaded = False saved = False - def open(self, im) -> None: + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename) -> None: + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index ac3d40bf287..f871e2eff16 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import IO import pytest @@ -56,15 +57,15 @@ class TestHandler: loaded = False saved = False - def open(self, im) -> None: + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename) -> None: + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 6b0662e0be3..65424214838 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -5,6 +5,7 @@ import warnings from io import BytesIO from pathlib import Path +from types import ModuleType from typing import Any import pytest @@ -33,6 +34,7 @@ skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -440,25 +442,25 @@ def getsampling(im: Image.Image): for subsampling in (-1, 3): # (default, invalid) im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - for subsampling in (0, "4:4:4"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (0, "4:4:4"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling in (1, "4:2:2"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (1, "4:2:2"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (2, 1, 1, 1, 1, 1) - for subsampling in (2, "4:2:0", "4:1:1"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (2, "4:2:0", "4:1:1"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (2, 2, 1, 1, 1, 1) # RGB colorspace - for subsampling in (-1, 0, "4:4:4"): + for subsampling1 in (-1, 0, "4:4:4"): # "4:4:4" doesn't really make sense for RGB, but the conversion # to an integer happens at a higher level - im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling in (1, "4:2:2", 2, "4:2:0", 3): + for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3): with pytest.raises(OSError): - self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 55041a4b202..194f39b30b8 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -11,7 +11,7 @@ from .helper import assert_image_equal, hopper, magick_command -def helper_save_as_palm(tmp_path: Path, mode) -> None: +def helper_save_as_palm(tmp_path: Path, mode: str) -> None: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".palm")) @@ -24,7 +24,7 @@ def helper_save_as_palm(tmp_path: Path, mode) -> None: assert os.path.getsize(outfile) > 0 -def open_with_magick(magick, tmp_path: Path, f): +def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: outfile = str(tmp_path / "temp.png") rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT @@ -33,7 +33,7 @@ def open_with_magick(magick, tmp_path: Path, f): return Image.open(outfile) -def roundtrip(tmp_path: Path, mode) -> None: +def roundtrip(tmp_path: Path, mode: str) -> None: magick = magick_command() if not magick: return @@ -66,6 +66,6 @@ def test_p_mode(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("L", "RGB")) -def test_oserror(tmp_path: Path, mode) -> None: +def test_oserror(tmp_path: Path, mode: str) -> None: with pytest.raises(OSError): helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 44e78e972dc..6217ebedd8a 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -19,7 +19,7 @@ ("jpg", "hopper.jpg", "JPEG"), ), ) -def test_sanity(codec, test_path, format) -> None: +def test_sanity(codec: str, test_path: str, format: str) -> None: if features.check(codec): with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: with Image.open(tar) as im: diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index fea19694109..8759412408c 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -2,6 +2,7 @@ from io import BytesIO from pathlib import Path +from types import ModuleType import pytest @@ -14,6 +15,7 @@ skip_unless_feature("webp_mux"), ] +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: diff --git a/Tests/test_image.py b/Tests/test_image.py index 67a7d7eca54..75b28c2dc07 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,6 +8,7 @@ import tempfile import warnings from pathlib import Path +from typing import IO import pytest @@ -61,11 +62,11 @@ class TestImage: "HSV", ), ) - def test_image_modes_success(self, mode) -> None: + def test_image_modes_success(self, mode: str) -> None: Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) - def test_image_modes_fail(self, mode) -> None: + def test_image_modes_fail(self, mode: str) -> None: with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) assert str(e.value) == "unrecognized image mode" @@ -100,7 +101,7 @@ def test_sanity(self) -> None: def test_repr_pretty(self) -> None: class Pretty: - def text(self, text) -> None: + def text(self, text: str) -> None: self.pretty_output = text im = Image.new("L", (100, 100)) @@ -184,7 +185,9 @@ def test_fp_name(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") class FP: - def write(self, b) -> None: + name: str + + def write(self, b: bytes) -> None: pass fp = FP() @@ -538,7 +541,7 @@ def test_check_size(self) -> None: "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - def test_empty_image(self, size) -> None: + def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) def test_storage_neg(self) -> None: @@ -565,7 +568,7 @@ def test_linear_gradient_wrong_mode(self) -> None: Image.linear_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_linear_gradient(self, mode) -> None: + def test_linear_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/linear_gradient.png" @@ -590,7 +593,7 @@ def test_radial_gradient_wrong_mode(self) -> None: Image.radial_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_radial_gradient(self, mode) -> None: + def test_radial_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/radial_gradient.png" @@ -665,7 +668,11 @@ def test__new(self) -> None: blank_p.palette = None blank_pa.palette = None - def _make_new(base_image, image, palette_result=None) -> None: + def _make_new( + base_image: Image.Image, + image: Image.Image, + palette_result: ImagePalette.ImagePalette | None = None, + ) -> None: new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size @@ -713,7 +720,7 @@ def test_no_new_file_on_error(self, tmp_path: Path) -> None: def test_load_on_nonexclusive_multiframe(self) -> None: with open("Tests/images/frozenpond.mpo", "rb") as fp: - def act(fp) -> None: + def act(fp: IO[bytes]) -> None: im = Image.open(fp) im.load() @@ -906,12 +913,12 @@ def test_exif_hide_offsets(self) -> None: assert exif.get_ifd(0xA005) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_tobytes(self, size) -> None: + def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) assert im.tobytes() == b"" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_frombytes(self, size) -> None: + def test_zero_frombytes(self, size: tuple[int, int]) -> None: Image.frombytes("RGB", size, b"") im = Image.new("RGB", size) @@ -996,7 +1003,7 @@ def test_constants(self) -> None: "01r_00.pcx", ], ) - def test_overrun(self, path) -> None: + def test_overrun(self, path: str) -> None: """For overrun completeness, test as: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ @@ -1023,7 +1030,7 @@ def test_exit_fp(self) -> None: pass assert not hasattr(im, "fp") - def test_close_graceful(self, caplog) -> None: + def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() with caplog.at_level(logging.DEBUG): @@ -1034,10 +1041,10 @@ def test_close_graceful(self, caplog) -> None: class MockEncoder: - pass + args: tuple[str, ...] -def mock_encode(*args): +def mock_encode(*args: str) -> MockEncoder: encoder = MockEncoder() encoder.args = args return encoder diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 325e7ef216b..24c7b871a68 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -208,7 +208,9 @@ def test_language() -> None: ), ids=("None", "ltr", "rtl2", "rtl", "ttb"), ) -def test_getlength(mode, text, direction, expected) -> None: +def test_getlength( + mode: str, text: str, direction: str | None, expected: float +) -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) @@ -230,7 +232,7 @@ def test_getlength(mode, text, direction, expected) -> None: ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) -def test_getlength_combine(mode, direction, text) -> None: +def test_getlength_combine(mode: str, direction: str, text: str) -> None: if text == "i\u0305i" and direction == "ttb": pytest.skip("fails with this font") @@ -250,7 +252,7 @@ def test_getlength_combine(mode, direction, text) -> None: @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor) -> None: +def test_anchor_ttb(anchor: str) -> None: text = "f" path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) @@ -306,7 +308,9 @@ def test_anchor_ttb(anchor) -> None: @pytest.mark.parametrize( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) -def test_combine(name, text, dir, anchor, epsilon) -> None: +def test_combine( + name: str, text: str, dir: str | None, anchor: str | None, epsilon: float +) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -337,7 +341,7 @@ def test_combine(name, text, dir, anchor, epsilon) -> None: ("rm", "right"), # pass with getsize ), ) -def test_combine_multiline(anchor, align) -> None: +def test_combine_multiline(anchor: str, align: str) -> None: # test that multiline text uses getlength, not getsize or getbbox path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 0b0c6d2d31a..46b473d7ac8 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -10,7 +10,7 @@ from .helper import assert_image_equal_tofile, hopper -def string_to_img(image_string): +def string_to_img(image_string: str) -> Image.Image: """Turn a string image representation into a binary image""" rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] height = len(rows) @@ -38,7 +38,7 @@ def string_to_img(image_string): ) -def img_to_string(im): +def img_to_string(im: Image.Image) -> str: """Turn a (small) binary image into a string representation""" chars = ".1" width, height = im.size @@ -48,11 +48,11 @@ def img_to_string(im): ) -def img_string_normalize(im): +def img_string_normalize(im: str) -> str: return img_to_string(string_to_img(im)) -def assert_img_equal_img_string(a, b_string) -> None: +def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None: assert img_to_string(a) == img_string_normalize(b_string) @@ -63,7 +63,7 @@ def test_str_to_img() -> None: @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) -def test_lut(op) -> None: +def test_lut(op: str) -> None: lb = ImageMorph.LutBuilder(op_name=op) assert lb.get_lut() is None diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 545229500a6..8e2db15aab7 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -67,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette() -> None: (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), ], ) -def test_getcolor_not_special(index, palette) -> None: +def test_getcolor_not_special(index: int, palette: ImagePalette.ImagePalette) -> None: im = Image.new("P", (1, 1)) # Do not use transparency index as a new color diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 560cdbd35f7..ed415953f06 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -13,7 +13,9 @@ FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" -def helper_pickle_file(tmp_path: Path, pickle, protocol, test_file, mode) -> None: +def helper_pickle_file( + tmp_path: Path, protocol: int, test_file: str, mode: str | None +) -> None: # Arrange with Image.open(test_file) as im: filename = str(tmp_path / "temp.pkl") @@ -30,7 +32,7 @@ def helper_pickle_file(tmp_path: Path, pickle, protocol, test_file, mode) -> Non assert im == loaded_im -def helper_pickle_string(pickle, protocol, test_file, mode) -> None: +def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -64,10 +66,12 @@ def helper_pickle_string(pickle, protocol, test_file, mode) -> None: ], ) @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) -def test_pickle_image(tmp_path: Path, test_file, test_mode, protocol) -> None: +def test_pickle_image( + tmp_path: Path, test_file: str, test_mode: str | None, protocol: int +) -> None: # Act / Assert - helper_pickle_string(pickle, protocol, test_file, test_mode) - helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) + helper_pickle_string(protocol, test_file, test_mode) + helper_pickle_file(tmp_path, protocol, test_file, test_mode) def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: @@ -99,7 +103,9 @@ def test_pickle_tell() -> None: assert unpickled_image.tell() == 0 -def helper_assert_pickled_font_images(font1, font2) -> None: +def helper_assert_pickled_font_images( + font1: ImageFont.FreeTypeFont, font2: ImageFont.FreeTypeFont +) -> None: # Arrange im1 = Image.new(mode="RGBA", size=(300, 100)) im2 = Image.new(mode="RGBA", size=(300, 100)) @@ -117,7 +123,7 @@ def helper_assert_pickled_font_images(font1, font2) -> None: @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_string(protocol) -> None: +def test_pickle_font_string(protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -131,7 +137,7 @@ def test_pickle_font_string(protocol) -> None: @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_file(tmp_path: Path, protocol) -> None: +def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) filename = str(tmp_path / "temp.pkl") diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 797539f35ef..64dfb2c95fb 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -10,7 +10,7 @@ from PIL import Image, PSDraw -def _create_document(ps) -> None: +def _create_document(ps: PSDraw.PSDraw) -> None: title = "hopper" box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points @@ -50,7 +50,7 @@ def test_draw_postscript(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer) -> None: +def test_stdout(buffer: bool) -> None: # Temporarily redirect stdout old_stdout = sys.stdout diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index 9442801d05c..3ce31cd2d1e 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -21,7 +21,7 @@ "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 3db0660eab7..2a072fd44c5 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -2,6 +2,7 @@ import shutil from pathlib import Path +from typing import Callable import pytest @@ -17,7 +18,12 @@ @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") class TestShellInjection: - def assert_save_filename_check(self, tmp_path: Path, src_img, save_func) -> None: + def assert_save_filename_check( + self, + tmp_path: Path, + src_img: Image.Image, + save_func: Callable[[Image.Image, int, str], None], + ) -> None: for filename in test_filenames: dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 64e781cbada..f51e8b3a8bd 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -42,7 +42,7 @@ @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") @pytest.mark.filterwarnings("ignore:Metadata warning") @pytest.mark.filterwarnings("ignore:Truncated File Read") -def test_tiff_crashes(test_file): +def test_tiff_crashes(test_file: str) -> None: try: with Image.open(test_file) as im: im.load() diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 5368545232b..f6adae3e6e7 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -53,9 +53,9 @@ def test_nonetype() -> None: def test_ifd_rational_save(tmp_path: Path) -> None: - methods = (True, False) - if not features.check("libtiff"): - methods = (False,) + methods = [True] + if features.check("libtiff"): + methods.append(False) for libtiff in methods: TiffImagePlugin.WRITE_LIBTIFF = libtiff From 47eaf0937f8e4f10fce8473007aba449c0c280f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Feb 2024 22:26:23 +1100 Subject: [PATCH 0191/1178] Use IO[bytes] in type hints --- src/PIL/ImageFile.py | 4 ++-- src/PIL/MspImagePlugin.py | 3 ++- src/PIL/PcxImagePlugin.py | 3 ++- src/PIL/PpmImagePlugin.py | 4 ++-- src/PIL/SgiImagePlugin.py | 4 ++-- src/PIL/TgaImagePlugin.py | 4 ++-- src/PIL/XbmImagePlugin.py | 4 ++-- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 487f53efe1f..e929b665e06 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import itertools import struct import sys -from typing import Any, NamedTuple +from typing import IO, Any, NamedTuple from . import Image from ._deprecate import deprecate @@ -616,7 +616,7 @@ def extents(self): class PyCodec: - fd: io.BytesIO | None + fd: IO[bytes] | None def __init__(self, mode, *args): self.im = None diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index bb7e466a790..65cc70624b7 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -26,6 +26,7 @@ import io import struct +from typing import IO from . import Image, ImageFile from ._binary import i16le as i16 @@ -163,7 +164,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: # write MSP files (uncompressed only) -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3e0968a8386..026bfd9a01b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -28,6 +28,7 @@ import io import logging +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -143,7 +144,7 @@ def _open(self) -> None: } -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 3e45ba95c84..6ac7a9bbc79 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -16,7 +16,7 @@ from __future__ import annotations import math -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -324,7 +324,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index ccf661ff1f3..7bd84ebd491 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,7 +24,7 @@ import os import struct -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -125,7 +125,7 @@ def _open(self) -> None: ] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 584932d2c7d..828701342b7 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,7 +18,7 @@ from __future__ import annotations import warnings -from io import BytesIO +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -175,7 +175,7 @@ def load_end(self) -> None: } -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 0291e2858ac..eee7274361c 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,7 +21,7 @@ from __future__ import annotations import re -from io import BytesIO +from typing import IO from . import Image, ImageFile @@ -70,7 +70,7 @@ def _open(self) -> None: self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) From 63987b7abaf6907a8985b867be154debfef0ec1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 06:55:13 +1100 Subject: [PATCH 0192/1178] Set mode to L if palette is missing --- Tests/test_file_tga.py | 5 +++++ src/PIL/TgaImagePlugin.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd8e522c7b5..87a59ff3d7a 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -133,6 +133,11 @@ def test_small_palette(tmp_path: Path) -> None: assert reloaded.getpalette() == colors +def test_missing_palette() -> None: + with Image.open("Tests/images/dilation4.lut") as im: + assert im.mode == "L" + + def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") out = str(tmp_path / "temp.tga") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 584932d2c7d..5d48275251e 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -85,7 +85,7 @@ def _open(self) -> None: elif depth == 16: self._mode = "LA" elif imagetype in (1, 9): - self._mode = "P" + self._mode = "P" if colormaptype else "L" elif imagetype in (2, 10): self._mode = "RGB" if depth == 32: From 818500b329555969cdb852c81d667cc70faaed94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 07:10:44 +1100 Subject: [PATCH 0193/1178] Raise an error if map depth is unknown --- Tests/images/p_8.tga | Bin 0 -> 18 bytes Tests/test_file_tga.py | 7 ++++++- src/PIL/TgaImagePlugin.py | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Tests/images/p_8.tga diff --git a/Tests/images/p_8.tga b/Tests/images/p_8.tga new file mode 100644 index 0000000000000000000000000000000000000000..73759a2822419fd38134be80acf7918c0a437e43 GIT binary patch literal 18 Wcmb1RWMp7qVB`Q23=KfY!2kdU!vQh? literal 0 HcmV?d00001 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 87a59ff3d7a..75c592da26b 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -7,7 +7,7 @@ import pytest -from PIL import Image +from PIL import Image, UnidentifiedImageError from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -65,6 +65,11 @@ def roundtrip(original_im) -> None: roundtrip(original_im) +def test_palette_depth_8(tmp_path: Path) -> None: + with pytest.raises(UnidentifiedImageError): + Image.open("Tests/images/p_8.tga") + + def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 5d48275251e..b6748c25ed2 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -128,6 +128,9 @@ def _open(self) -> None: self.palette = ImagePalette.raw( "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) ) + else: + msg = "unknown TGA map depth" + raise SyntaxError(msg) # setup tile descriptor try: From 21e5d5d082dfe47fda2779e10d446e6593c04dd8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 09:17:22 +1100 Subject: [PATCH 0194/1178] Use palette when loading --- Tests/test_file_ico.py | 11 +++++++++++ src/PIL/IcoImagePlugin.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 65f090931b4..e75561f6978 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -38,6 +38,17 @@ def test_black_and_white() -> None: assert im.size == (16, 16) +def test_palette(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.ico") + + im = Image.new("P", (16, 16)) + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "P" + assert reloaded.palette is not None + + def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 1b22f8645dc..b558fdf3476 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -329,6 +329,8 @@ def load(self): self.im = im.im self.pyaccess = None self._mode = im.mode + if im.palette: + self.palette = im.palette if im.size != self.size: warnings.warn("Image was not the expected size") From 3199c0ea40c041d41fe2499c86893a7e795f0929 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Feb 2024 20:20:42 +1100 Subject: [PATCH 0195/1178] Decoder and encoders subclass PyDecoder and PyEncoder --- Tests/test_file_jpeg.py | 8 +----- Tests/test_image.py | 16 ++++------- Tests/test_imagefile.py | 64 ++++++++++++++++++++--------------------- src/PIL/Image.py | 14 ++++----- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 65424214838..4858d92e6ea 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -986,13 +986,7 @@ class InfiniteMockPyDecoder(ImageFile.PyDecoder): def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 - decoder = InfiniteMockPyDecoder(None) - - def closure(mode: str, *args) -> InfiniteMockPyDecoder: - decoder.__init__(mode, *args) - return decoder - - Image.register_decoder("INFINITE", closure) + Image.register_decoder("INFINITE", InfiniteMockPyDecoder) with Image.open(TEST_FILE) as im: im.tile = [ diff --git a/Tests/test_image.py b/Tests/test_image.py index 4c04e0da48e..aae51eaa42a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -16,6 +16,7 @@ ExifTags, Image, ImageDraw, + ImageFile, ImagePalette, UnidentifiedImageError, features, @@ -1038,25 +1039,20 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: assert im.fp is None -class MockEncoder: - args: tuple[str, ...] - - -def mock_encode(*args: str) -> MockEncoder: - encoder = MockEncoder() - encoder.args = args - return encoder +class MockEncoder(ImageFile.PyEncoder): + pass class TestRegistry: def test_encode_registry(self) -> None: - Image.register_encoder("MOCK", mock_encode) + Image.register_encoder("MOCK", MockEncoder) assert "MOCK" in Image.ENCODERS enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) assert isinstance(enc, MockEncoder) - assert enc.args == ("RGB", "args", "extra") + assert enc.mode == "RGB" + assert enc.args == ("args", "extra") def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 49140978168..cf251c9cec6 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from typing import Any import pytest @@ -201,12 +202,22 @@ def test_broken_datastream_without_errors(self) -> None: class MockPyDecoder(ImageFile.PyDecoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyDecoder.last = self + + super().__init__(mode, *args) + def decode(self, buffer): # eof return -1, 0 class MockPyEncoder(ImageFile.PyEncoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyEncoder.last = self + + super().__init__(mode, *args) + def encode(self, buffer): return 1, 1, b"" @@ -228,19 +239,8 @@ def _open(self) -> None: class CodecsTest: @classmethod def setup_class(cls) -> None: - cls.decoder = MockPyDecoder(None) - cls.encoder = MockPyEncoder(None) - - def decoder_closure(mode, *args): - cls.decoder.__init__(mode, *args) - return cls.decoder - - def encoder_closure(mode, *args): - cls.encoder.__init__(mode, *args) - return cls.encoder - - Image.register_decoder("MOCK", decoder_closure) - Image.register_encoder("MOCK", encoder_closure) + Image.register_decoder("MOCK", MockPyDecoder) + Image.register_encoder("MOCK", MockPyEncoder) class TestPyDecoder(CodecsTest): @@ -251,13 +251,13 @@ def test_setimage(self) -> None: im.load() - assert self.decoder.state.xoff == xoff - assert self.decoder.state.yoff == yoff - assert self.decoder.state.xsize == xsize - assert self.decoder.state.ysize == ysize + assert MockPyDecoder.last.state.xoff == xoff + assert MockPyDecoder.last.state.yoff == yoff + assert MockPyDecoder.last.state.xsize == xsize + assert MockPyDecoder.last.state.ysize == ysize with pytest.raises(ValueError): - self.decoder.set_as_raw(b"\x00") + MockPyDecoder.last.set_as_raw(b"\x00") def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -267,10 +267,10 @@ def test_extents_none(self) -> None: im.load() - assert self.decoder.state.xoff == 0 - assert self.decoder.state.yoff == 0 - assert self.decoder.state.xsize == 200 - assert self.decoder.state.ysize == 200 + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -315,10 +315,10 @@ def test_setimage(self) -> None: im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] ) - assert self.encoder.state.xoff == xoff - assert self.encoder.state.yoff == yoff - assert self.encoder.state.xsize == xsize - assert self.encoder.state.ysize == ysize + assert MockPyEncoder.last.state.xoff == xoff + assert MockPyEncoder.last.state.yoff == yoff + assert MockPyEncoder.last.state.xsize == xsize + assert MockPyEncoder.last.state.ysize == ysize def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -329,10 +329,10 @@ def test_extents_none(self) -> None: fp = BytesIO() ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) - assert self.encoder.state.xoff == 0 - assert self.encoder.state.yoff == 0 - assert self.encoder.state.xsize == 200 - assert self.encoder.state.ysize == 200 + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -340,12 +340,12 @@ def test_negsize(self) -> None: im = MockImageFile(buf) fp = BytesIO() - self.encoder.cleanup_called = False + MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] ) - assert self.encoder.cleanup_called + assert MockPyEncoder.last.cleanup_called with pytest.raises(ValueError): ImageFile._save( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7ec..eba30537f13 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -229,8 +229,8 @@ class Quantize(IntEnum): SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} EXTENSION: dict[str, str] = {} -DECODERS: dict[str, object] = {} -ENCODERS: dict[str, object] = {} +DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} +ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} # -------------------------------------------------------------------- # Modes @@ -3524,28 +3524,26 @@ def registered_extensions(): return EXTENSION -def register_decoder(name: str, decoder) -> None: +def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: """ Registers an image decoder. This function should not be used in application code. :param name: The name of the decoder - :param decoder: A callable(mode, args) that returns an - ImageFile.PyDecoder object + :param decoder: An ImageFile.PyDecoder object .. versionadded:: 4.1.0 """ DECODERS[name] = decoder -def register_encoder(name, encoder): +def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: """ Registers an image encoder. This function should not be used in application code. :param name: The name of the encoder - :param encoder: A callable(mode, args) that returns an - ImageFile.PyEncoder object + :param encoder: An ImageFile.PyEncoder object .. versionadded:: 4.1.0 """ From 26e0f6df56c1289d52b156642c6ee1197d2bf69b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 10:18:54 +1100 Subject: [PATCH 0196/1178] Pin Python 3.13 on Windows to a3 --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 75fccf7959a..79a2e60b251 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"] timeout-minutes: 30 From 5c858d75e4a58e895bed56c2ff6c0bae245c88cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 10:45:52 +1100 Subject: [PATCH 0197/1178] Added type hints --- src/PIL/Image.py | 33 ++++---- src/PIL/ImageColor.py | 2 +- src/PIL/ImageOps.py | 171 +++++++++++++++++++++++++++------------- src/PIL/ImagePalette.py | 7 +- 4 files changed, 139 insertions(+), 74 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7ec..ba81a22c7f0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1430,7 +1430,7 @@ def get_value(element): root = ElementTree.fromstring(xmp_tags) return {get_name(root.tag): get_value(root)} - def getexif(self): + def getexif(self) -> Exif: """ Gets EXIF data from the image. @@ -1438,7 +1438,6 @@ def getexif(self): """ if self._exif is None: self._exif = Exif() - self._exif._loaded = False elif self._exif._loaded: return self._exif self._exif._loaded = True @@ -1525,7 +1524,7 @@ def getim(self): self.load() return self.im.ptr - def getpalette(self, rawmode="RGB"): + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: """ Returns the image palette as a list. @@ -1615,7 +1614,7 @@ def getprojection(self): x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask=None, extrema=None): + def histogram(self, mask=None, extrema=None) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1804,7 +1803,7 @@ def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): result = alpha_composite(background, overlay) self.paste(result, box) - def point(self, lut, mode=None): + def point(self, lut, mode: str | None = None) -> Image: """ Maps this image through a lookup table or function. @@ -1928,7 +1927,7 @@ def putdata(self, data, scale=1.0, offset=0.0): self.im.putdata(data, scale, offset) - def putpalette(self, data, rawmode="RGB"): + def putpalette(self, data, rawmode="RGB") -> None: """ Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. @@ -2108,7 +2107,7 @@ def _get_safe_box(self, size, resample, box): min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: """ Returns a resized copy of this image. @@ -2200,10 +2199,11 @@ def resize(self, size, resample=None, box=None, reducing_gap=None): if factor_x > 1 or factor_y > 1: reduce_box = self._get_safe_box(size, resample, box) factor = (factor_x, factor_y) - if callable(self.reduce): - self = self.reduce(factor, box=reduce_box) - else: - self = Image.reduce(self, factor, box=reduce_box) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) box = ( (box[0] - reduce_box[0]) / factor_x, (box[1] - reduce_box[1]) / factor_y, @@ -2818,7 +2818,7 @@ def __transformer( self.im.transform2(box, image.im, method, data, resample, fill) - def transpose(self, method): + def transpose(self, method: Transpose) -> Image: """ Transpose image (flip or rotate in 90 degree steps) @@ -2870,7 +2870,9 @@ class ImagePointHandler: (for use with :py:meth:`~PIL.Image.Image.point`) """ - pass + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass class ImageTransformHandler: @@ -3690,6 +3692,7 @@ class Exif(_ExifBase): endian = None bigtiff = False + _loaded = False def __init__(self): self._data = {} @@ -3805,7 +3808,7 @@ def _get_merged_dict(self): return merged_dict - def tobytes(self, offset=8): + def tobytes(self, offset: int = 8) -> bytes: from . import TiffImagePlugin head = self._get_head() @@ -3960,7 +3963,7 @@ def __setitem__(self, tag, value): del self._info[tag] self._data[tag] = value - def __delitem__(self, tag): + def __delitem__(self, tag: int) -> None: if self._info is not None and tag in self._info: del self._info[tag] else: diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index ad59b066779..5fb80b75310 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -124,7 +124,7 @@ def getrgb(color): @lru_cache -def getcolor(color, mode): +def getcolor(color, mode: str) -> tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a9e626b2b2a..6218c723f85 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,6 +21,7 @@ import functools import operator import re +from typing import Protocol, Sequence, cast from . import ExifTags, Image, ImagePalette @@ -28,7 +29,7 @@ # helpers -def _border(border): +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: if isinstance(border, tuple): if len(border) == 2: left, top = right, bottom = border @@ -39,7 +40,7 @@ def _border(border): return left, top, right, bottom -def _color(color, mode): +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: if isinstance(color, str): from . import ImageColor @@ -47,7 +48,7 @@ def _color(color, mode): return color -def _lut(image, lut): +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: if image.mode == "P": # FIXME: apply to lookup table, not image data msg = "mode P support coming soon" @@ -65,7 +66,13 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers - try: + if isinstance(ignore, int): h[ignore] = 0 - except TypeError: - # assume sequence + else: for ix in ignore: h[ix] = 0 if cutoff: @@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff[0] // 100 + cut = int(n * cutoff[0] // 100) for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): if cut <= 0: break # remove cutoff% samples from the high end - cut = n * cutoff[1] // 100 + cut = int(n * cutoff[1] // 100) for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): return _lut(image, lut) -def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: """ Colorize grayscale image. This function calculates a color wedge which maps all black pixels in @@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 # Define colors from arguments - black = _color(black, "RGB") - white = _color(white, "RGB") - if mid is not None: - mid = _color(mid, "RGB") + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None # Empty lists for the mapping red = [] @@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the low-end values for i in range(0, blackpoint): - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) + red.append(rgb_black[0]) + green.append(rgb_black[1]) + blue.append(rgb_black[2]) # Create the mapping (2-color) - if mid is None: + if rgb_mid is None: range_map = range(0, whitepoint - blackpoint) for i in range_map: - red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) - green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) - blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) # Create the mapping (3-color) else: @@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi range_map2 = range(0, whitepoint - midpoint) for i in range_map1: - red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) - green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) - blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) for i in range_map2: - red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) - green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) - blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values for i in range(0, 256 - whitepoint): - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) -def contain(image, size, method=Image.Resampling.BICUBIC): +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, set to the maximum width and height within the requested size, while maintaining the original aspect ratio. @@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def cover(image, size, method=Image.Resampling.BICUBIC): +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, so that the requested size is covered, while maintaining the original aspect ratio. @@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and padded version of the image, expanded to fill the requested aspect ratio and size. @@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 return out -def crop(image, border=0): +def crop(image: Image.Image, border: int = 0) -> Image.Image: """ Remove border from image. The same amount of pixels are removed from all four sides. This function works on all image modes. @@ -349,7 +386,9 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.Resampling.BICUBIC): +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -372,7 +411,19 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): return image.resize(size, resample) -def deform(image, deformer, resample=Image.Resampling.BILINEAR): +class _SupportsGetMesh(Protocol): + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: _SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: """ Deform the image. @@ -388,7 +439,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR): ) -def equalize(image, mask=None): +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: """ Equalize the image histogram. This function applies a non-linear mapping to the input image, in order to create a uniform @@ -419,7 +470,11 @@ def equalize(image, mask=None): return _lut(image, lut) -def expand(image, border=0, fill=0): +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: """ Add border to the image @@ -445,7 +500,13 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. @@ -479,13 +540,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, # kevin@cazabon.com # https://www.cazabon.com - # ensure centering is mutable - centering = list(centering) + centering_x, centering_y = centering - if not 0.0 <= centering[0] <= 1.0: - centering[0] = 0.5 - if not 0.0 <= centering[1] <= 1.0: - centering[1] = 0.5 + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 if not 0.0 <= bleed < 0.5: bleed = 0.0 @@ -522,8 +582,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, crop_height = live_size[0] / output_ratio # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) @@ -531,7 +591,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, return image.resize(size, method, box=crop) -def flip(image): +def flip(image: Image.Image) -> Image.Image: """ Flip the image vertically (top to bottom). @@ -541,7 +601,7 @@ def flip(image): return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) -def grayscale(image): +def grayscale(image: Image.Image) -> Image.Image: """ Convert the image to grayscale. @@ -551,7 +611,7 @@ def grayscale(image): return image.convert("L") -def invert(image): +def invert(image: Image.Image) -> Image.Image: """ Invert (negate) the image. @@ -562,7 +622,7 @@ def invert(image): return image.point(lut) if image.mode == "1" else _lut(image, lut) -def mirror(image): +def mirror(image: Image.Image) -> Image.Image: """ Flip image horizontally (left to right). @@ -572,7 +632,7 @@ def mirror(image): return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) -def posterize(image, bits): +def posterize(image: Image.Image, bits: int) -> Image.Image: """ Reduce the number of bits for each color channel. @@ -585,7 +645,7 @@ def posterize(image, bits): return _lut(image, lut) -def solarize(image, threshold=128): +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: """ Invert all pixel values above a threshold. @@ -602,7 +662,7 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, in_place=False): +def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. @@ -616,7 +676,7 @@ def exif_transpose(image, *, in_place=False): """ image.load() image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation) + orientation = image_exif.get(ExifTags.Base.Orientation, 1) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -653,3 +713,4 @@ def exif_transpose(image, *, in_place=False): return transposed_image elif not in_place: return image.copy() + return None diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 2b6cecc6105..770d10025c8 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,6 +18,7 @@ from __future__ import annotations import array +from typing import Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -34,11 +35,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None): + def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() - self.dirty = None + self.dirty: int | None = None @property def palette(self): @@ -127,7 +128,7 @@ def _new_color_index(self, image=None, e=None): raise ValueError(msg) from e return index - def getcolor(self, color, image=None): + def getcolor(self, color, image=None) -> int: """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. From d3b974b78607d36fbfa392b80c81adbd08d277ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 11:19:32 +1100 Subject: [PATCH 0198/1178] Use font in ImageDraw examples --- docs/deprecations.rst | 8 ++++---- docs/releasenotes/9.2.0.rst | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 205fcb9abcd..a58ce9bcba6 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -232,10 +232,10 @@ Previous code:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world") + width, height = draw.textsize("Hello world", font) width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) Use instead:: @@ -247,9 +247,9 @@ Use instead:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world") + width = draw.textlength("Hello world", font) - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top FreeTypeFont.getmask2 fill parameter diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index b875edf8e5c..3b8d2535f74 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -69,10 +69,10 @@ Previous code:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world") + width, height = draw.textsize("Hello world", font) width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) Use instead:: @@ -84,9 +84,9 @@ Use instead:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world") + width = draw.textlength("Hello world", font) - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top API Additions From 617b9cbc00759f042d2c38ea63a14adca2bc902b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 11:50:48 +1100 Subject: [PATCH 0199/1178] Describe difference between size and bbox --- docs/deprecations.rst | 4 ++++ docs/releasenotes/9.2.0.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a58ce9bcba6..c90ad481adb 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -252,6 +252,10 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods explicitly distinguish this as a +``top`` offset. + FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 3b8d2535f74..495926ca749 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -89,6 +89,10 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods explicitly distinguish this as a +``top`` offset. + API Additions ============= From 1a108281b9b6d894574ec63534a043385549b3be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 14:03:56 +1100 Subject: [PATCH 0200/1178] Removed unused code --- Tests/test_format_hsv.py | 4 ---- Tests/test_image_paste.py | 1 - 2 files changed, 5 deletions(-) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 73aaae6e757..da909c06c80 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -12,10 +12,6 @@ def int_to_float(i): return i / 255 -def str_to_float(i): - return ord(i) / 255 - - def tuple_to_ints(tp): x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index ce73455729a..2966f724f4a 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -8,7 +8,6 @@ class TestImagingPaste: - masks = {} size = 128 def assert_9points_image( From 5ff7d926fd24acc2d6d575959635d59123b308a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 15:00:38 +1100 Subject: [PATCH 0201/1178] Added type hints --- Tests/test_features.py | 7 +- Tests/test_file_blp.py | 2 +- Tests/test_file_bmp.py | 4 +- Tests/test_file_im.py | 2 +- Tests/test_file_pcx.py | 6 +- Tests/test_file_pdf.py | 15 ++-- Tests/test_file_tiff.py | 19 ++--- Tests/test_format_hsv.py | 19 +++-- Tests/test_imagechops.py | 6 +- Tests/test_imagedraw2.py | 11 +-- Tests/test_imageenhance.py | 8 ++- Tests/test_imagefile.py | 2 +- Tests/test_imagefont.py | 140 ++++++++++++++++++++++--------------- Tests/test_imageops.py | 22 +++--- Tests/test_imageops_usm.py | 14 ++-- Tests/test_imagepath.py | 13 ++-- 16 files changed, 170 insertions(+), 120 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index de74e9c1829..8d2d198ffcd 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,6 +2,7 @@ import io import re +from typing import Callable import pytest @@ -29,7 +30,7 @@ def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function) -> None: + def test(name: str, function: Callable[[str], bool]) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -73,12 +74,12 @@ def test_libimagequant_version() -> None: @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature) -> None: +def test_check_modules(feature: str) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature) -> None: +def test_check_codecs(feature: str) -> None: assert features.check_codec(feature) in [True, False] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 3904d3bc5b5..1e2f20c407b 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None: "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index c36466e0269..1eaff0c7df0 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -16,7 +16,7 @@ def test_sanity(tmp_path: Path) -> None: - def roundtrip(im) -> None: + def roundtrip(im: Image.Image) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -194,7 +194,7 @@ def test_rle4() -> None: ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length) -> None: +def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index f932069b9c3..036965bf5d8 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -82,7 +82,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path: Path) -> None: +def test_roundtrip(mode: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index a2486be40c5..ab9f9663e1b 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -9,7 +9,7 @@ from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path: Path, im) -> None: +def _roundtrip(tmp_path: Path, im: Image.Image) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -44,7 +44,7 @@ def test_invalid_file() -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path: Path, mode) -> None: +def test_odd(tmp_path: Path, mode: str) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None: _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None: +def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 65a93c138e9..d39a86565ce 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -6,6 +6,7 @@ import tempfile import time from pathlib import Path +from typing import Any, Generator import pytest @@ -14,7 +15,7 @@ from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path: Path, mode) -> None: +def test_save(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path: Path, mode) -> None: +def test_save_alpha(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None: {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path: Path) -> None: +def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None: assert os.path.getsize(outfile) > 0 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(outfile, save_all=True, append_images=im_generator(ims)) @@ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None: im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf) -> None: +def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None: @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline) -> None: +def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a16b76e19f7..0110948aeac 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -4,6 +4,8 @@ import warnings from io import BytesIO from pathlib import Path +from types import ModuleType +from typing import Generator import pytest @@ -20,6 +22,7 @@ is_win32, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -156,7 +159,7 @@ def test_int_resolution(self) -> None: "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi) -> None: + def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: @@ -284,7 +287,7 @@ def test_unknown_pixel_mode(self) -> None: ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames) -> None: + def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -402,7 +405,7 @@ def test__delitem__(self) -> None: assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api) -> None: + def test_load_byte(self, legacy_api: bool) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) @@ -431,7 +434,7 @@ def test_ifd_tag_type(self) -> None: assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: - def check_exif(exif) -> None: + def check_exif(exif: Image.Exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -511,7 +514,7 @@ def test_exif_frames(self) -> None: assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path: Path) -> None: + def test_photometric(self, mode: str, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -660,7 +663,7 @@ def test_planar_configuration_save(self, tmp_path: Path) -> None: assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path: Path) -> None: + def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -689,7 +692,7 @@ def test_tiff_save_all(self) -> None: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() @@ -860,7 +863,7 @@ def test_timeout(self) -> None: ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file) -> None: + def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 73aaae6e757..fe055bf4b15 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,21 +2,22 @@ import colorsys import itertools +from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper -def int_to_float(i): +def int_to_float(i: int) -> float: return i / 255 -def str_to_float(i): +def str_to_float(i: str) -> float: return ord(i) / 255 -def tuple_to_ints(tp): +def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) @@ -25,7 +26,7 @@ def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge(): +def wedge() -> Image.Image: w = Image._wedge() w90 = w.rotate(90) @@ -49,7 +50,11 @@ def wedge(): return img -def to_xxx_colorsys(im, func, mode): +def to_xxx_colorsys( + im: Image.Image, + func: Callable[[float, float, float], tuple[float, float, float]], + mode: str, +) -> Image.Image: # convert the hard way using the library colorsys routines. (r, g, b) = im.split() @@ -70,11 +75,11 @@ def to_xxx_colorsys(im, func, mode): return hsv -def to_hsv_colorsys(im): +def to_hsv_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") -def to_rgb_colorsys(im): +def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 94f57e06690..7e2290c156e 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper @@ -387,7 +389,9 @@ def test_overlay() -> None: def test_logical() -> None: - def table(op, a, b): + def table( + op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int + ) -> tuple[int, int, int, int]: out = [] for x in (a, b): imx = Image.new("1", (1, 1), x) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 07a25b84b4e..3171eb9aec1 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -5,6 +5,7 @@ import pytest from PIL import Image, ImageDraw, ImageDraw2, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -56,7 +57,7 @@ def test_sanity() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox) -> None: +def test_ellipse(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -84,7 +85,7 @@ def test_ellipse_edge() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line(points) -> None: +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -98,7 +99,7 @@ def test_line(points) -> None: @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points) -> None: +def test_line_pen_as_brush(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon(points) -> None: +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -129,7 +130,7 @@ def test_polygon(points) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox) -> None: +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 9ce9cda8267..6ebc61e1bc4 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -22,7 +22,7 @@ def test_crash() -> None: ImageEnhance.Sharpness(im).enhance(0.5) -def _half_transparent_image(): +def _half_transparent_image() -> Image.Image: # returns an image, half transparent, half solid im = hopper("RGB") @@ -34,7 +34,9 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount) -> None: +def _check_alpha( + im: Image.Image, original: Image.Image, op: str, amount: float +) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None: @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op) -> None: +def test_alpha(op: str) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 49140978168..44521a8b3dd 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -31,7 +31,7 @@ class TestImageFile: def test_parser(self) -> None: - def roundtrip(format): + def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 909026dc8c6..c79b36ca432 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,11 +7,13 @@ import sys from io import BytesIO from pathlib import Path +from typing import BinaryIO import pytest from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features +from PIL._typing import StrOrBytesPath from .helper import ( assert_image_equal, @@ -47,11 +49,11 @@ def layout_engine(request): @pytest.fixture(scope="module") -def font(layout_engine): +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font) -> None: +def test_font_properties(font: ImageFont.FreeTypeFont) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -67,7 +69,9 @@ def test_font_properties(font) -> None: assert font_copy.path == second_font_path -def _render(font, layout_engine): +def _render( + font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout +) -> Image.Image: txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) ttf.getbbox(txt) @@ -80,12 +84,12 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font) -> None: +def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine) -> None: - def _font_as_bytes(): +def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: + def _font_as_bytes() -> BytesIO: with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes @@ -102,12 +106,12 @@ def _font_as_bytes(): # _render(shared_bytes) -def test_font_with_open_file(layout_engine) -> None: +def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine) -> None: +def test_render_equal(layout_engine: ImageFont.Layout) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None: assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: +def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font) -> None: +def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -140,7 +144,7 @@ def test_transparent_background(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font) -> None: +def test_I16(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -153,7 +157,7 @@ def test_I16(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font) -> None: +def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None: ), ) def test_getlength( - text, mode, fontname, size, layout_engine, length_basic, length_raqm + text: str, + mode: str, + fontname: str, + size: int, + layout_engine: ImageFont.Layout, + length_basic: int, + length_raqm: float, ) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) @@ -207,7 +217,7 @@ def test_float_size() -> None: assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font) -> None: +def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -223,7 +233,7 @@ def test_render_multiline(font) -> None: assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font) -> None: +def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None: @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext) -> None: +def test_render_multiline_text_align( + font: ImageFont.FreeTypeFont, align: str, ext: str +) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None: assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font) -> None: +def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -260,14 +272,14 @@ def test_unknown_align(font) -> None: draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font) -> None: +def test_draw_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font) -> None: +def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None: draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font) -> None: +def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -295,7 +307,7 @@ def test_multiline_width(font) -> None: ) -def test_multiline_spacing(font) -> None: +def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation) -> None: +def test_rotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation) -> None: +def test_unrotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation) -> None: +def test_rotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation) -> None: +def test_unrotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None: assert mask.size == (108, 13) -def test_free_type_font_get_name(font) -> None: +def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font) -> None: +def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None: assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font) -> None: +def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: # Arrange text = "mask this" @@ -473,16 +493,16 @@ def test_default_font() -> None: @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode) -> None: +def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font) -> None: +def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font) -> None: +def test_render_empty(font: ImageFont.FreeTypeFont) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -492,7 +512,7 @@ def test_render_empty(font) -> None: assert_image_equal(im, target) -def test_unicode_extended(layout_engine) -> None: +def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -516,7 +536,7 @@ def test_unicode_extended(layout_engine) -> None: ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") def test_find_font(monkeypatch, platform, font_directory) -> None: - def _test_fake_loading_font(path_to_fake, fontname) -> None: + def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: @@ -567,7 +587,7 @@ def fake_walker(path): _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font) -> None: +def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -588,7 +608,7 @@ def test_imagefont_getters(font) -> None: @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width) -> None: +def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -607,7 +627,7 @@ def test_complex_font_settings() -> None: t.getmask("абвг", language="sr") -def test_variation_get(font) -> None: +def test_variation_get(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -662,7 +682,7 @@ def test_variation_get(font) -> None: ] -def _check_text(font, path, epsilon): +def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) d.text((10, 10), "Text", font=font, fill="black") @@ -677,7 +697,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font) -> None: +def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -702,7 +722,7 @@ def test_variation_set_by_name(font) -> None: _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font) -> None: +def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -737,7 +757,9 @@ def test_variation_set_by_axes(font) -> None: ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top) -> None: +def test_anchor( + layout_engine: ImageFont.Layout, anchor: str, left: int, top: int +) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -782,7 +804,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None: ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align) -> None: +def test_anchor_multiline( + layout_engine: ImageFont.Layout, anchor: str, align: str +) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -800,7 +824,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None: assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font) -> None: +def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -826,7 +850,7 @@ def test_anchor_invalid(font) -> None: @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp) -> None: +def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -843,7 +867,7 @@ def test_bitmap_font(layout_engine, bpp) -> None: assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine) -> None: +def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -861,7 +885,7 @@ def test_bitmap_font_stroke(layout_engine) -> None: @pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine, embedded_color) -> None: +def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: font = ImageFont.truetype( "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine ) @@ -873,7 +897,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None: assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") -def test_standard_embedded_color(layout_engine) -> None: +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -886,7 +910,7 @@ def test_standard_embedded_color(layout_engine) -> None: @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) -def test_float_coord(layout_engine, fontmode): +def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) @@ -908,7 +932,7 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine) -> None: +def test_cbdt(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -925,7 +949,7 @@ def test_cbdt(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_cbdt_mask(layout_engine) -> None: +def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -942,7 +966,7 @@ def test_cbdt_mask(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_sbix(layout_engine) -> None: +def test_sbix(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -959,7 +983,7 @@ def test_sbix(layout_engine) -> None: pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine) -> None: +def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -977,7 +1001,7 @@ def test_sbix_mask(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine) -> None: +def test_colr(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -993,7 +1017,7 @@ def test_colr(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine) -> None: +def test_colr_mask(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -1008,7 +1032,7 @@ def test_colr_mask(layout_engine) -> None: assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine) -> None: +def test_woff2(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1042,7 +1066,7 @@ def test_render_mono_size() -> None: assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font) -> None: +def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1070,7 +1094,7 @@ def test_too_many_characters(font) -> None: "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file) -> None: +def test_oom(test_file: str) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): @@ -1091,6 +1115,8 @@ def test_raqm_missing_warning(monkeypatch) -> None: @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None: +def test_invalid_truetype_sizes_raise_valueerror( + layout_engine: ImageFont.Layout, size: int +) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 50bf404aebe..b320e79c10e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -14,7 +14,7 @@ class Deformer: - def getmesh(self, im): + def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]: x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -108,7 +108,7 @@ def test_fit_same_ratio() -> None: @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size) -> None: +def test_contain(new_size: tuple[int, int]) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) @@ -132,7 +132,7 @@ def test_contain_round() -> None: ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size) -> None: +def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size @@ -168,7 +168,7 @@ def test_pad_round() -> None: @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode) -> None: +def test_palette(mode: str) -> None: im = hopper(mode) # Expand @@ -210,7 +210,7 @@ def test_scale() -> None: @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border) -> None: +def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -366,7 +366,7 @@ def test_exif_transpose() -> None: for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im) -> None: + def check(orientation_im: Image.Image) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -445,7 +445,7 @@ def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - def autocontrast(cutoff): + def autocontrast(cutoff: int | tuple[int, int]): return ImageOps.autocontrast(img, cutoff).histogram() assert autocontrast(10) == autocontrast((10, 10)) @@ -486,20 +486,20 @@ def test_autocontrast_mask_real_input() -> None: assert result_nomask != result assert_tuple_approx_equal( ImageStat.Stat(result, mask=rect_mask).median, - [195, 202, 184], + (195, 202, 184), threshold=2, msg="autocontrast with mask pixel incorrect", ) assert_tuple_approx_equal( ImageStat.Stat(result_nomask).median, - [119, 106, 79], + (119, 106, 79), threshold=2, msg="autocontrast without mask pixel incorrect", ) def test_autocontrast_preserve_tone() -> None: - def autocontrast(mode, preserve_tone): + def autocontrast(mode: str, preserve_tone: bool) -> Image.Image: im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -533,7 +533,7 @@ def test_autocontrast_preserve_gradient() -> None: @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color) -> None: +def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 03302e20f2a..519d791050d 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,12 +1,14 @@ from __future__ import annotations +from typing import Generator + import pytest from PIL import Image, ImageFilter @pytest.fixture -def test_images(): +def test_images() -> Generator[dict[str, Image.Image], None, None]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), @@ -18,7 +20,7 @@ def test_images(): im.close() -def test_filter_api(test_images) -> None: +def test_filter_api(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -32,7 +34,7 @@ def test_filter_api(test_images) -> None: assert i.size == (128, 128) -def test_usm_formats(test_images) -> None: +def test_usm_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None: im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images) -> None: +def test_blur_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None: im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images) -> None: +def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None: assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images) -> None: +def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8ba745f215d..bd600b17744 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -3,6 +3,7 @@ import array import math import struct +from typing import Sequence import pytest @@ -75,7 +76,9 @@ def test_path_constructors(coords) -> None: [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords) -> None: +def test_invalid_path_constructors( + coords: tuple[str, str] | Sequence[Sequence[int]] +) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -93,7 +96,7 @@ def test_invalid_path_constructors(coords) -> None: [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords) -> None: +def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -111,7 +114,9 @@ def test_path_odd_number_of_coordinates(coords) -> None: (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected) -> None: +def test_getbbox( + coords: int | list[int], expected: tuple[float, float, float, float] +) -> None: # Arrange p = ImagePath.Path(coords) @@ -135,7 +140,7 @@ def test_getbbox_no_args() -> None: (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected) -> None: +def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: # Arrange p = ImagePath.Path(coords) From 96fc60d5d2aa0ad13be0951efb1fe990a64f190a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 20:21:25 +1100 Subject: [PATCH 0202/1178] Removed mypy excludes --- pyproject.toml | 4 ---- tox.ini | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48c59f2a1b4..e687f4bcf76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/MicImagePlugin.py$', -] diff --git a/tox.ini b/tox.ini index 8c818df7a6a..3ef011c9e94 100644 --- a/tox.ini +++ b/tox.ini @@ -41,6 +41,7 @@ deps = packaging types-cffi types-defusedxml + types-olefile extras = typing commands = From b6fdf2e9e7a65bf23cac224b2ab96c6b1d2c8449 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Feb 2024 07:54:58 +1100 Subject: [PATCH 0203/1178] Updated package name for Tidelift --- .github/FUNDING.yml | 2 +- README.md | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e0e6804bfe4..8fc6bd0ad54 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: "pypi/Pillow" +tidelift: "pypi/pillow" diff --git a/README.md b/README.md index 6ca870166a1..9776c40e2f7 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ As of 2019, Pillow development is src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"> Tidelift + src="https://tidelift.com/badges/package/pypi/pillow?style=flat"> Newest PyPI version diff --git a/docs/conf.py b/docs/conf.py index 9ae7ae605fd..97289c91d0c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -326,7 +326,7 @@ r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", - r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", + r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", } diff --git a/docs/index.rst b/docs/index.rst index 5583699193c..bf2feea9a53 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Mon, 19 Feb 2024 17:08:21 +1100 Subject: [PATCH 0204/1178] Added image to illustrate size vs bbox --- docs/deprecations.rst | 8 ++++++-- docs/example/size_vs_bbox.png | Bin 0 -> 12934 bytes docs/releasenotes/9.2.0.rst | 8 ++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 docs/example/size_vs_bbox.png diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c90ad481adb..74021a218ef 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -253,8 +253,12 @@ Use instead:: width, height = right - left, bottom - top Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods explicitly distinguish this as a -``top`` offset. +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ./example/size_vs_bbox.png + :alt: Demonstration of size height vs bbox top and bottom + :align: center FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/example/size_vs_bbox.png b/docs/example/size_vs_bbox.png new file mode 100644 index 0000000000000000000000000000000000000000..11a05d2a8146a612741c7e4ec4d3cf0ce05a35a0 GIT binary patch literal 12934 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 literal 0 HcmV?d00001 diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 495926ca749..db514068f77 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -90,8 +90,12 @@ Use instead:: width, height = right - left, bottom - top Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods explicitly distinguish this as a -``top`` offset. +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ./example/size_vs_bbox.png + :alt: Demonstration of size height vs bbox top and bottom + :align: center API Additions ============= From 0692ad8cdd4fe979478d910b705dcc12f8e82f61 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Feb 2024 23:29:58 +1100 Subject: [PATCH 0205/1178] Updated giflib on macOS to 5.2.2 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 26bf2f6d655..1ec2811f6d8 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -24,7 +24,7 @@ XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.1.4 + GIFLIB_VERSION=5.2.2 else GIFLIB_VERSION=5.2.1 fi From fe1edb1e0f8608e3924f67b53776f653c418ddfe Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:47:07 +0200 Subject: [PATCH 0206/1178] Install mypy from requirements file So Renovate can update it on a schedule --- .ci/requirements-mypy.txt | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .ci/requirements-mypy.txt diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt new file mode 100644 index 00000000000..ed32694607d --- /dev/null +++ b/.ci/requirements-mypy.txt @@ -0,0 +1 @@ +mypy==1.7.1 diff --git a/tox.ini b/tox.ini index 8c818df7a6a..85800ff8d7a 100644 --- a/tox.ini +++ b/tox.ini @@ -33,10 +33,10 @@ commands = [testenv:mypy] skip_install = true deps = + -r .ci/requirements-mypy.txt IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython - mypy==1.7.1 numpy packaging types-cffi From 7490aee8362c589a475035bf86c05b29c894593c Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 19 Feb 2024 20:18:18 +0100 Subject: [PATCH 0207/1178] Show how to use anchors to align text in imagefont deprecations --- docs/deprecations.rst | 24 ++++++++++++++++++++++++ docs/releasenotes/9.2.0.rst | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 74021a218ef..9efb1316d5c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -260,6 +260,30 @@ offset. :alt: Demonstration of size height vs bbox top and bottom :align: center +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index db514068f77..e8bf33b60f2 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -97,6 +97,30 @@ offset. :alt: Demonstration of size height vs bbox top and bottom :align: center +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + API Additions ============= From 531b1e1b9a6b3f83519d9b6687523474f4a18d83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:50:06 +0200 Subject: [PATCH 0208/1178] Remove outdated installation warnings --- docs/installation.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 116bdcf2fb1..980bbd99def 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,15 +9,6 @@ Installation }); -Warnings --------- - -.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. - -.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. - -.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. - Python Support -------------- From e39765d755cc2d37e79d07f58ebc77a8e44812c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 15:41:20 +1100 Subject: [PATCH 0209/1178] Added type hints --- Tests/test_deprecate.py | 6 +++--- Tests/test_file_ico.py | 2 +- Tests/test_file_iptc.py | 2 +- Tests/test_file_msp.py | 2 +- Tests/test_file_png.py | 2 ++ Tests/test_file_psd.py | 2 +- Tests/test_file_tga.py | 4 ++-- Tests/test_file_tiff_metadata.py | 6 ++++-- Tests/test_imagecms.py | 28 ++++++++++++++++++++++------ Tests/test_imagefont.py | 22 ++++++++++++---------- Tests/test_imagegrab.py | 6 ++++-- Tests/test_imagepath.py | 8 +++++--- Tests/test_imageqt.py | 2 +- Tests/test_imageshow.py | 10 ++++++---- Tests/test_imagewin_pointers.py | 2 +- Tests/test_numpy.py | 6 +++--- Tests/test_qt_image_qapplication.py | 4 ++-- Tests/test_qt_image_toqimage.py | 2 +- Tests/test_util.py | 2 +- 19 files changed, 73 insertions(+), 45 deletions(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 6ffc8f6f589..584d8f91d67 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -20,7 +20,7 @@ ), ], ) -def test_version(version, expected) -> None: +def test_version(version: int | None, expected: str) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") @@ -46,7 +46,7 @@ def test_unknown_version() -> None: ), ], ) -def test_old_version(deprecated, plural, expected) -> None: +def test_old_version(deprecated: str, plural: bool, expected: str) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) @@ -76,7 +76,7 @@ def test_replacement_and_action() -> None: "Upgrade to new thing.", ], ) -def test_action(action) -> None: +def test_action(action: str) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 65f090931b4..f69a290fabf 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode) -> None: +def test_save_to_bytes_bmp(mode: str) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 9c0969437ea..88c30d46895 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -98,7 +98,7 @@ def test_i() -> None: assert ret == 97 -def test_dump(monkeypatch) -> None: +def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f9f81d11413..b0964aabe12 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -52,7 +52,7 @@ def test_open_windows_v1() -> None: assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path) -> None: +def _assert_file_image_equal(source_path: str, target_path: str) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index d4a63431647..c51f56ce7c0 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -6,6 +6,7 @@ import zlib from io import BytesIO from pathlib import Path +from types import ModuleType from typing import Any import pytest @@ -23,6 +24,7 @@ skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 7eca8d9b151..e60638b224d 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None: ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises) -> None: +def test_crashes(test_file: str, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd8e522c7b5..3c6da50c532 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -22,8 +22,8 @@ @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path: Path) -> None: - def roundtrip(original_im) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: + def roundtrip(original_im: Image.Image) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index bb6225d075b..d7a18c72504 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None: @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: +def test_writing_other_types_to_ascii( + value: bytes | int, expected: str, tmp_path: Path +) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: +def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 83fc38ed3fd..21a0dd75b98 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -237,7 +237,7 @@ def test_invalid_color_temperature() -> None: @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag) -> None: +def test_invalid_flag(flag: str | int) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -335,12 +335,26 @@ def test_extended_information() -> None: o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: + def assert_truncated_tuple_equal( + tup1: tuple[tuple[float, float, float], ...] | tuple[float], + tup2: ( + tuple[tuple[tuple[float, float, float], ...], ...] + | tuple[tuple[float, float, float], ...] + | tuple[float] + ), + digits: int = 10, + ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple(tuple_or_float): + def truncate_tuple( + tuple_or_float: ( + tuple[tuple[tuple[float, float, float], ...], ...] + | tuple[tuple[float, float, float], ...] + | tuple[float, ...] + ) + ) -> tuple[tuple[float, ...], ...]: return tuple( ( truncate_tuple(val) @@ -504,8 +518,10 @@ def test_profile_typesafety() -> None: ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: - def create_test_image(): +def assert_aux_channel_preserved( + mode: str, transform_in_place: bool, preserved_channel: str +) -> None: + def create_test_image() -> Image.Image: # set up test image with something interesting in the tested aux channel. # fmt: off nine_grid_deltas = [ @@ -633,7 +649,7 @@ def test_auxiliary_channels_isolated() -> None: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode) -> None: +def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index c79b36ca432..05b5d471691 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,7 +7,7 @@ import sys from io import BytesIO from pathlib import Path -from typing import BinaryIO +from typing import Any, BinaryIO import pytest from packaging.version import parse as parse_version @@ -44,7 +44,7 @@ def test_sanity() -> None: pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) -def layout_engine(request): +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: return request.param @@ -535,21 +535,23 @@ def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory) -> None: +def test_find_font( + monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str +) -> None: def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) - def loadable_font(filepath, size, index, encoding, *args, **kwargs): + def loadable_font( + filepath: str, size: int, index: int, encoding: str, *args: Any + ): if filepath == path_to_fake: return ImageFont._FreeTypeFont( - FONT_PATH, size, index, encoding, *args, **kwargs + FONT_PATH, size, index, encoding, *args ) - return ImageFont._FreeTypeFont( - filepath, size, index, encoding, *args, **kwargs - ) + return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) m.setattr(ImageFont, "FreeTypeFont", loadable_font) font = ImageFont.truetype(fontname) @@ -563,7 +565,7 @@ def loadable_font(filepath, size, index, encoding, *args, **kwargs): if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): + def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: if path == font_directory: return [ ( @@ -1101,7 +1103,7 @@ def test_oom(test_file: str) -> None: font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch) -> None: +def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 235a2f993bd..e23adeb7083 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -84,6 +84,7 @@ def test_grabclipboard(self) -> None: @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -94,6 +95,7 @@ def test_grabclipboard_file(self) -> None: @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") $ms = new-object System.IO.MemoryStream(, $bytes) @@ -113,7 +115,7 @@ def test_grabclipboard_png(self) -> None: reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext) -> None: + def test_grabclipboard_wl_clipboard(self, ext: str) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) @@ -128,6 +130,6 @@ def test_grabclipboard_wl_clipboard(self, ext) -> None: reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("arg", ("text", "--clear")) - def test_grabclipboard_wl_clipboard_errors(self, arg): + def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: subprocess.call(["wl-copy", arg]) assert ImageGrab.grabclipboard() is None diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index bd600b17744..9487560af88 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -58,7 +58,9 @@ def test_path() -> None: ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords) -> None: +def test_path_constructors( + coords: Sequence[float] | array.array[float] | ImagePath.Path, +) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -206,9 +208,9 @@ class Evil: def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) - def __getitem__(self, i): + def __getitem__(self, i: int) -> bytes: x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x) -> None: + def __setitem__(self, i: int, x: bytes) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 909f9716700..88ad1f9eee4 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -28,7 +28,7 @@ def test_rgb() -> None: assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b) -> None: + def checkrgb(r: int, g: int, b: int) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index f7269d45b56..8d741d94ada 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from PIL import Image, ImageShow @@ -24,9 +26,9 @@ def test_register() -> None: "order", [-1, 0], ) -def test_viewer_show(order) -> None: +def test_viewer_show(order: int) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options) -> bool: + def show_image(self, image: Image.Image, **options: Any) -> bool: self.methodCalled = True return True @@ -48,7 +50,7 @@ def show_image(self, image, **options) -> bool: reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode) -> None: +def test_show(mode: str) -> None: im = hopper(mode) assert ImageShow.show(im) @@ -73,7 +75,7 @@ def test_viewer() -> None: @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer) -> None: +def test_viewers(viewer: ImageShow.Viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c7f633e6290..f59ee7284b8 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -70,7 +70,7 @@ class BITMAPINFOHEADER(ctypes.Structure): ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP - def serialize_dib(bi, pixels): + def serialize_dib(bi, pixels) -> bytearray: bf = BITMAPFILEHEADER() bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6ba95c2d700..9f4e6534e8a 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -14,7 +14,7 @@ def test_numpy_to_image() -> None: - def to_image(dtype, bands: int = 1, boolean: int = 0): + def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: if bands == 1: if boolean: data = [0, 255] * 50 @@ -99,7 +99,7 @@ def test_1d_array() -> None: assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np) -> None: +def _test_img_equals_nparray(img: Image.Image, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None: ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype) -> None: +def test_to_array(mode: str, dtype) -> None: img = hopper(mode) # Resize to non-square diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 7d6c0a8cb7d..3cd323553fa 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -4,7 +4,7 @@ import pytest -from PIL import ImageQt +from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -37,7 +37,7 @@ def __init__(self) -> None: lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected) -> None: +def roundtrip(expected: Image.Image) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index a222a7d71b9..6110be707f5 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -17,7 +17,7 @@ @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path: Path) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_util.py b/Tests/test_util.py index 73e4acd5555..197ef79eef5 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize( "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] ) -def test_is_path(test_path) -> None: +def test_is_path(test_path: str | Path | PurePath) -> None: # Act it_is = _util.is_path(test_path) From a655d7606e2f12f0e7700ef754ed92a6da45f658 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 21:27:30 +1100 Subject: [PATCH 0210/1178] Simplified type hints --- Tests/test_imagecms.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 21a0dd75b98..a7bb31db512 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -6,6 +6,7 @@ import shutil from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -336,25 +337,13 @@ def test_extended_information() -> None: p = o.profile def assert_truncated_tuple_equal( - tup1: tuple[tuple[float, float, float], ...] | tuple[float], - tup2: ( - tuple[tuple[tuple[float, float, float], ...], ...] - | tuple[tuple[float, float, float], ...] - | tuple[float] - ), - digits: int = 10, + tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple( - tuple_or_float: ( - tuple[tuple[tuple[float, float, float], ...], ...] - | tuple[tuple[float, float, float], ...] - | tuple[float, ...] - ) - ) -> tuple[tuple[float, ...], ...]: + def truncate_tuple(tuple_or_float: tuple[Any, ...]) -> tuple[Any, ...]: return tuple( ( truncate_tuple(val) From 64579510c018440edb1b0c9c67f36d478f969f38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 21:37:06 +1100 Subject: [PATCH 0211/1178] Updated alt text --- docs/deprecations.rst | 2 +- docs/releasenotes/9.2.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 74021a218ef..9fd64fdaa42 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -257,7 +257,7 @@ offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. .. image:: ./example/size_vs_bbox.png - :alt: Demonstration of size height vs bbox top and bottom + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center FreeTypeFont.getmask2 fill parameter diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index db514068f77..b596a6ab20f 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -94,7 +94,7 @@ offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. .. image:: ./example/size_vs_bbox.png - :alt: Demonstration of size height vs bbox top and bottom + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center API Additions From 56a02b76eb403356436e6480c5ada23930825e36 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:37:40 +1100 Subject: [PATCH 0212/1178] Corrected image path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- docs/releasenotes/9.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index b596a6ab20f..677438bc63e 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -93,7 +93,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. -.. image:: ./example/size_vs_bbox.png +.. image:: ../example/size_vs_bbox.png :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center From 5b20811cabd4594fd4fafc596e3f94afe12bb361 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:36:36 +0100 Subject: [PATCH 0213/1178] Add `--bugreport` argument to __main__.py to omit supported formats --- src/PIL/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/__main__.py b/src/PIL/__main__.py index 943789923dc..32de33624db 100644 --- a/src/PIL/__main__.py +++ b/src/PIL/__main__.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + from .features import pilinfo -pilinfo() +pilinfo(supported_formats="--bugreport" not in sys.argv) From 10712be53d575a37d9dc2522c9f8f0f62871f3b6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 21:21:10 +0100 Subject: [PATCH 0214/1178] Build docs for Python changes --- .github/workflows/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4319cc8ff28..92e860cb547 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,10 +7,12 @@ on: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" pull_request: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" workflow_dispatch: permissions: From ab9dfd8181868922abee46dffb11ffcb9a772958 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:37:33 +0100 Subject: [PATCH 0215/1178] Add sys.{executable,base_prefix,prefix} to features.pilinfo --- Tests/test_features.py | 12 +++++++++--- Tests/test_main.py | 12 +++++++++--- src/PIL/features.py | 9 +++++++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 8d2d198ffcd..3fffa032f81 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -129,9 +129,15 @@ def test_pilinfo() -> None: while lines[0].startswith(" "): lines = lines[1:] assert lines[0] == "-" * 68 - assert lines[1].startswith("Python modules loaded from ") - assert lines[2].startswith("Binary modules loaded from ") - assert lines[3] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 jpeg = ( "\n" + "-" * 68 diff --git a/Tests/test_main.py b/Tests/test_main.py index 46259f1dc52..e13e0c5e3ac 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -15,9 +15,15 @@ def test_main() -> None: while lines[0].startswith(" "): lines = lines[1:] assert lines[0] == "-" * 68 - assert lines[1].startswith("Python modules loaded from ") - assert lines[2].startswith("Binary modules loaded from ") - assert lines[3] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 jpeg = ( os.linesep + "-" * 68 diff --git a/src/PIL/features.py b/src/PIL/features.py index b14d6df13f2..7c5112ef6e4 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -249,12 +249,17 @@ def pilinfo(out=None, supported_formats=True): for py_version in py_version[1:]: print(f" {py_version.strip()}", file=out) print("-" * 68, file=out) + print(f"Python executable is {sys.executable or 'unknown'}", file=out) + if sys.prefix != sys.base_prefix: + print(f"Environment Python files loaded from {sys.prefix}", file=out) + print(f"System Python files loaded from {sys.base_prefix}", file=out) + print("-" * 68, file=out) print( - f"Python modules loaded from {os.path.dirname(Image.__file__)}", + f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", file=out, ) print( - f"Binary modules loaded from {os.path.dirname(Image.core.__file__)}", + f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", file=out, ) print("-" * 68, file=out) From 89c44be404081c77e68457c1c50a78e96a6d42b9 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:39:40 +0100 Subject: [PATCH 0216/1178] Mention `python -m PIL --bugreport` in the issue template --- .github/ISSUE_TEMPLATE/ISSUE_REPORT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md index 115f6135dfb..cfd576f3588 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md @@ -48,6 +48,10 @@ Thank you. * Python: * Pillow: +```text +please paste the output of running `python3 -m PIL --bugreport` here +``` +