diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py new file mode 100644 index 000000000..30ed4a808 --- /dev/null +++ b/Tests/test_deprecate.py @@ -0,0 +1,91 @@ +import pytest + +from PIL import _deprecate + + +@pytest.mark.parametrize( + "version, expected", + [ + ( + 10, + "Old thing is deprecated and will be removed in Pillow 10 " + r"\(2023-07-01\)\. Use new thing instead\.", + ), + ( + None, + r"Old thing is deprecated and will be removed in a future version\. " + r"Use new thing instead\.", + ), + ], +) +def test_version(version, expected): + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", version, "new thing") + + +def test_unknown_version(): + expected = r"Unknown removal version, update PIL\._deprecate\?" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate("Old thing", 12345, "new thing") + + +@pytest.mark.parametrize( + "deprecated, plural, expected", + [ + ( + "Old thing", + False, + r"Old thing is deprecated and should be removed\.", + ), + ( + "Old things", + True, + r"Old things are deprecated and should be removed\.", + ), + ], +) +def test_old_version(deprecated, plural, expected): + expected = r"" + with pytest.raises(RuntimeError, match=expected): + _deprecate.deprecate(deprecated, 1, plural=plural) + + +def test_plural(): + expected = ( + r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Use new thing instead\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old things", 10, "new thing", plural=True) + + +def test_replacement_and_action(): + expected = "Use only one of 'replacement' and 'action'" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate( + "Old thing", 10, replacement="new thing", action="Upgrade to new thing" + ) + + +@pytest.mark.parametrize( + "action", + [ + "Upgrade to new thing", + "Upgrade to new thing.", + ], +) +def test_action(action): + expected = ( + r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Upgrade to new thing\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 10, action=action) + + +def test_no_replacement_or_action(): + expected = ( + r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)" + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 10) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 1105ff76e..363a67d9b 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -9,6 +9,14 @@ Internal Modules :undoc-members: :show-inheritance: +:mod:`~PIL._deprecate` Module +----------------------------- + +.. automodule:: PIL._deprecate + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL._tkinter_finder` Module ---------------------------------- diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index ecd3da5df..3fd61c50f 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -31,11 +31,11 @@ BLP files come in many different flavours: import os import struct -import warnings from enum import IntEnum from io import BytesIO from . import Image, ImageFile +from ._deprecate import deprecate class Format(IntEnum): @@ -55,7 +55,6 @@ class AlphaEncoding(IntEnum): def __getattr__(name): - deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " for enum, prefix in { Format: "BLP_FORMAT_", Encoding: "BLP_ENCODING_", @@ -64,19 +63,7 @@ def __getattr__(name): if name.startswith(prefix): name = name[len(prefix) :] if name in enum.__members__: - warnings.warn( - prefix - + name - + " is " - + deprecated - + "Use " - + enum.__name__ - + "." - + name - + " instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 9eed02999..440240a99 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -9,9 +9,8 @@ # See the README file for information on usage and redistribution. # -import warnings - from . import FitsImagePlugin, Image, ImageFile +from ._deprecate import deprecate _handler = None @@ -25,11 +24,11 @@ def register_handler(handler): global _handler _handler = handler - warnings.warn( - "FitsStubImagePlugin is deprecated and will be removed in Pillow " - "10 (2023-07-01). FITS images can now be read without a handler through " - "FitsImagePlugin instead.", - DeprecationWarning, + deprecate( + "FitsStubImagePlugin", + 10, + action="FITS images can now be read without " + "a handler through FitsImagePlugin instead", ) # Override FitsImagePlugin with this handler diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 55d28e1ff..d58a21ac3 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -52,11 +52,11 @@ Note: All data is stored in little-Endian (Intel) byte order. """ import struct -import warnings from enum import IntEnum from io import BytesIO from . import Image, ImageFile +from ._deprecate import deprecate MAGIC = b"FTEX" @@ -67,24 +67,11 @@ class Format(IntEnum): def __getattr__(name): - deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " for enum, prefix in {Format: "FORMAT_"}.items(): if name.startswith(prefix): name = name[len(prefix) :] if name in enum.__members__: - warnings.warn( - prefix - + name - + " is " - + deprecated - + "Use " - + enum.__name__ - + "." - + name - + " instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a117dfada..3c36178bd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -50,28 +50,17 @@ except ImportError: # Use __version__ instead. from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins from ._binary import i32le, o32be, o32le +from ._deprecate import deprecate from ._util import DeferredError, is_path def __getattr__(name): - deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} if name in categories: - warnings.warn( - "Image categories are " + deprecated + "Use is_animated instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate("Image categories", 10, "is_animated", plural=True) return categories[name] elif name in ("NEAREST", "NONE"): - warnings.warn( - name - + " is " - + deprecated - + "Use Resampling.NEAREST or Dither.NONE instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(name, 10, "Resampling.NEAREST or Dither.NONE") return 0 old_resampling = { "LINEAR": "BILINEAR", @@ -79,31 +68,11 @@ def __getattr__(name): "ANTIALIAS": "LANCZOS", } if name in old_resampling: - warnings.warn( - name - + " is " - + deprecated - + "Use Resampling." - + old_resampling[name] - + " instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(name, 10, f"Resampling.{old_resampling[name]}") return Resampling[old_resampling[name]] for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): if name in enum.__members__: - warnings.warn( - name - + " is " - + deprecated - + "Use " - + enum.__name__ - + "." - + name - + " instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(name, 10, f"{enum.__name__}.{name}") return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") @@ -538,12 +507,7 @@ class Image: def __getattr__(self, name): if name == "category": - warnings.warn( - "Image categories are deprecated and will be removed in Pillow 10 " - "(2023-07-01). Use is_animated instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate("Image categories", 10, "is_animated", plural=True) return self._category raise AttributeError(name) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 0da39b513..ab59e5856 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -16,11 +16,12 @@ # below for the original description. import sys -import warnings from enum import IntEnum from PIL import Image +from ._deprecate import deprecate + try: from PIL import _imagingcms except ImportError as ex: @@ -117,24 +118,11 @@ class Direction(IntEnum): def __getattr__(name): - deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): if name.startswith(prefix): name = name[len(prefix) :] if name in enum.__members__: - warnings.warn( - prefix - + name - + " is " - + deprecated - + "Use " - + enum.__name__ - + "." - + name - + " instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 8af13255c..5b6059bb0 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,6 +33,7 @@ from enum import IntEnum from io import BytesIO from . import Image +from ._deprecate import deprecate from ._util import is_directory, is_path @@ -42,24 +43,11 @@ class Layout(IntEnum): def __getattr__(name): - deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " for enum, prefix in {Layout: "LAYOUT_"}.items(): if name.startswith(prefix): name = name[len(prefix) :] if name in enum.__members__: - warnings.warn( - prefix - + name - + " is " - + deprecated - + "Use " - + enum.__name__ - + "." - + name - + " instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") @@ -196,8 +184,6 @@ class FreeTypeFont: if core.HAVE_RAQM: layout_engine = Layout.RAQM elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: - import warnings - warnings.warn( "Raqm layout was requested, but Raqm is not available. " "Falling back to basic layout." diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index cfc72c254..853147ac2 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -17,9 +17,9 @@ # import array -import warnings from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +from ._deprecate import deprecate class ImagePalette: @@ -40,11 +40,7 @@ class ImagePalette: self.palette = palette or bytearray() self.dirty = None if size != 0: - warnings.warn( - "The size parameter is deprecated and will be removed in Pillow 10 " - "(2023-07-01).", - DeprecationWarning, - ) + deprecate("The size parameter", 10, None) if size != len(self.palette): raise ValueError("wrong palette size") diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 395bb2258..66f86211f 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -15,11 +15,12 @@ import os import shutil import subprocess import sys -import warnings from shlex import quote from PIL import Image +from ._deprecate import deprecate + _viewers = [] @@ -120,11 +121,7 @@ class Viewer: """ if path is None: if "file" in options: - warnings.warn( - "The 'file' argument is deprecated and will be removed in Pillow " - "10 (2023-07-01). Use 'path' instead.", - DeprecationWarning, - ) + deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") @@ -176,11 +173,7 @@ class MacViewer(Viewer): """ if path is None: if "file" in options: - warnings.warn( - "The 'file' argument is deprecated and will be removed in Pillow " - "10 (2023-07-01). Use 'path' instead.", - DeprecationWarning, - ) + deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") @@ -228,11 +221,7 @@ class XDGViewer(UnixViewer): """ if path is None: if "file" in options: - warnings.warn( - "The 'file' argument is deprecated and will be removed in Pillow " - "10 (2023-07-01). Use 'path' instead.", - DeprecationWarning, - ) + deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") @@ -261,11 +250,7 @@ class DisplayViewer(UnixViewer): """ if path is None: if "file" in options: - warnings.warn( - "The 'file' argument is deprecated and will be removed in Pillow " - "10 (2023-07-01). Use 'path' instead.", - DeprecationWarning, - ) + deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") @@ -296,11 +281,7 @@ class GmDisplayViewer(UnixViewer): """ if path is None: if "file" in options: - warnings.warn( - "The 'file' argument is deprecated and will be removed in Pillow " - "10 (2023-07-01). Use 'path' instead.", - DeprecationWarning, - ) + deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") @@ -325,11 +306,7 @@ class EogViewer(UnixViewer): """ if path is None: if "file" in options: - warnings.warn( - "The 'file' argument is deprecated and will be removed in Pillow " - "10 (2023-07-01). Use 'path' instead.", - DeprecationWarning, - ) + deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") @@ -360,11 +337,7 @@ class XVViewer(UnixViewer): """ if path is None: if "file" in options: - warnings.warn( - "The 'file' argument is deprecated and will be removed in Pillow " - "10 (2023-07-01). Use 'path' instead.", - DeprecationWarning, - ) + deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index c9a9135b0..98faf50ff 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -26,10 +26,10 @@ # import tkinter -import warnings from io import BytesIO from . import Image +from ._deprecate import deprecate # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -187,11 +187,7 @@ class PhotoImage: """ if box is not None: - warnings.warn( - "The box parameter is deprecated and will be removed in Pillow 10 " - "(2023-07-01).", - DeprecationWarning, - ) + deprecate("The box parameter", 10, None) # convert to blittable im.load() diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 93741ec6e..b6ab7b0ee 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -45,6 +45,7 @@ from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 +from ._deprecate import deprecate from .JpegPresets import presets # @@ -603,11 +604,7 @@ samplings = { def convert_dict_qtables(qtables): - warnings.warn( - "convert_dict_qtables is deprecated and will be removed in Pillow 10" - "(2023-07-01). Conversion is no longer needed.", - DeprecationWarning, - ) + deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed") return qtables diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 53525e22e..c939b86e7 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -45,6 +45,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._deprecate import deprecate logger = logging.getLogger(__name__) @@ -131,24 +132,11 @@ class Blend(IntEnum): def __getattr__(name): - deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): if name.startswith(prefix): name = name[len(prefix) :] if name in enum.__members__: - warnings.warn( - prefix - + name - + " is " - + deprecated - + "Use " - + enum.__name__ - + "." - + name - + " instead.", - DeprecationWarning, - stacklevel=2, - ) + deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py new file mode 100644 index 000000000..30a8a8971 --- /dev/null +++ b/src/PIL/_deprecate.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import warnings + +from . import __version__ + + +def deprecate( + deprecated: str, + when: int | None, + replacement: str | None = None, + *, + action: str | None = None, + plural: bool = False, +) -> None: + """ + Deprecations helper. + + :param deprecated: Name of thing to be deprecated. + :param when: Pillow major version to be removed in. + :param replacement: Name of replacement. + :param action: Instead of "replacement", give a custom call to action + e.g. "Upgrade to new thing". + :param plural: if the deprecated thing is plural, needing "are" instead of "is". + + Usually of the form: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + Use [replacement] instead." + + You can leave out the replacement sentence: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)" + + Or with another call to action: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + [action]." + """ + + is_ = "are" if plural else "is" + + if when is None: + removed = "a future version" + elif when <= int(__version__.split(".")[0]): + raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.") + elif when == 10: + removed = "Pillow 10 (2023-07-01)" + else: + raise ValueError(f"Unknown removal version, update {__name__}?") + + if replacement and action: + raise ValueError("Use only one of 'replacement' and 'action'") + + if replacement: + action = f". Use {replacement} instead." + elif action: + action = f". {action.rstrip('.')}." + else: + action = "" + + warnings.warn( + f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", + DeprecationWarning, + stacklevel=3, + ) diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 5253f0759..5cd7e9b1f 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -2,9 +2,10 @@ """ import sys import tkinter -import warnings from tkinter import _tkinter as tk +from ._deprecate import deprecate + try: if hasattr(sys, "pypy_find_executable"): TKINTER_LIB = tk.tklib_cffi.__file__ @@ -17,9 +18,6 @@ except AttributeError: tk_version = str(tkinter.TkVersion) if tk_version == "8.4": - warnings.warn( - "Support for Tk/Tcl 8.4 is deprecated and will be removed" - " in Pillow 10 (2023-07-01). Please upgrade to Tk/Tcl 8.5 " - "or newer.", - DeprecationWarning, + deprecate( + "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer" )