Merge pull request #6184 from hugovk/deprecations-helper

Add deprecations helper
This commit is contained in:
Andrew Murray 2022-04-16 18:58:46 +10:00 committed by GitHub
commit 535c45717f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 207 additions and 183 deletions

91
Tests/test_deprecate.py Normal file
View File

@ -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)

View File

@ -9,6 +9,14 @@ Internal Modules
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL._deprecate` Module
-----------------------------
.. automodule:: PIL._deprecate
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL._tkinter_finder` Module :mod:`~PIL._tkinter_finder` Module
---------------------------------- ----------------------------------

View File

@ -31,11 +31,11 @@ BLP files come in many different flavours:
import os import os
import struct import struct
import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._deprecate import deprecate
class Format(IntEnum): class Format(IntEnum):
@ -55,7 +55,6 @@ class AlphaEncoding(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in { for enum, prefix in {
Format: "BLP_FORMAT_", Format: "BLP_FORMAT_",
Encoding: "BLP_ENCODING_", Encoding: "BLP_ENCODING_",
@ -64,19 +63,7 @@ def __getattr__(name):
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -9,9 +9,8 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
import warnings
from . import FitsImagePlugin, Image, ImageFile from . import FitsImagePlugin, Image, ImageFile
from ._deprecate import deprecate
_handler = None _handler = None
@ -25,11 +24,11 @@ def register_handler(handler):
global _handler global _handler
_handler = handler _handler = handler
warnings.warn( deprecate(
"FitsStubImagePlugin is deprecated and will be removed in Pillow " "FitsStubImagePlugin",
"10 (2023-07-01). FITS images can now be read without a handler through " 10,
"FitsImagePlugin instead.", action="FITS images can now be read without "
DeprecationWarning, "a handler through FitsImagePlugin instead",
) )
# Override FitsImagePlugin with this handler # Override FitsImagePlugin with this handler

View File

@ -52,11 +52,11 @@ Note: All data is stored in little-Endian (Intel) byte order.
""" """
import struct import struct
import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._deprecate import deprecate
MAGIC = b"FTEX" MAGIC = b"FTEX"
@ -67,24 +67,11 @@ class Format(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Format: "FORMAT_"}.items(): for enum, prefix in {Format: "FORMAT_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -50,28 +50,17 @@ except ImportError:
# Use __version__ instead. # Use __version__ instead.
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._util import deferred_error, isPath from ._util import deferred_error, isPath
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
if name in categories: if name in categories:
warnings.warn( deprecate("Image categories", 10, "is_animated", plural=True)
"Image categories are " + deprecated + "Use is_animated instead.",
DeprecationWarning,
stacklevel=2,
)
return categories[name] return categories[name]
elif name in ("NEAREST", "NONE"): elif name in ("NEAREST", "NONE"):
warnings.warn( deprecate(name, 10, "Resampling.NEAREST or Dither.NONE")
name
+ " is "
+ deprecated
+ "Use Resampling.NEAREST or Dither.NONE instead.",
DeprecationWarning,
stacklevel=2,
)
return 0 return 0
old_resampling = { old_resampling = {
"LINEAR": "BILINEAR", "LINEAR": "BILINEAR",
@ -79,31 +68,11 @@ def __getattr__(name):
"ANTIALIAS": "LANCZOS", "ANTIALIAS": "LANCZOS",
} }
if name in old_resampling: if name in old_resampling:
warnings.warn( deprecate(name, 10, f"Resampling.{old_resampling[name]}")
name
+ " is "
+ deprecated
+ "Use Resampling."
+ old_resampling[name]
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return Resampling[old_resampling[name]] return Resampling[old_resampling[name]]
for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(name, 10, f"{enum.__name__}.{name}")
name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@ -538,12 +507,7 @@ class Image:
def __getattr__(self, name): def __getattr__(self, name):
if name == "category": if name == "category":
warnings.warn( deprecate("Image categories", 10, "is_animated", plural=True)
"Image categories are deprecated and will be removed in Pillow 10 "
"(2023-07-01). Use is_animated instead.",
DeprecationWarning,
stacklevel=2,
)
return self._category return self._category
raise AttributeError(name) raise AttributeError(name)

View File

@ -16,11 +16,12 @@
# below for the original description. # below for the original description.
import sys import sys
import warnings
from enum import IntEnum from enum import IntEnum
from PIL import Image from PIL import Image
from ._deprecate import deprecate
try: try:
from PIL import _imagingcms from PIL import _imagingcms
except ImportError as ex: except ImportError as ex:
@ -117,24 +118,11 @@ class Direction(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -33,6 +33,7 @@ from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image from . import Image
from ._deprecate import deprecate
from ._util import isDirectory, isPath from ._util import isDirectory, isPath
@ -42,24 +43,11 @@ class Layout(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Layout: "LAYOUT_"}.items(): for enum, prefix in {Layout: "LAYOUT_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@ -196,8 +184,6 @@ class FreeTypeFont:
if core.HAVE_RAQM: if core.HAVE_RAQM:
layout_engine = Layout.RAQM layout_engine = Layout.RAQM
elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: elif layout_engine == Layout.RAQM and not core.HAVE_RAQM:
import warnings
warnings.warn( warnings.warn(
"Raqm layout was requested, but Raqm is not available. " "Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout." "Falling back to basic layout."

View File

@ -17,9 +17,9 @@
# #
import array import array
import warnings
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
from ._deprecate import deprecate
class ImagePalette: class ImagePalette:
@ -40,11 +40,7 @@ class ImagePalette:
self.palette = palette or bytearray() self.palette = palette or bytearray()
self.dirty = None self.dirty = None
if size != 0: if size != 0:
warnings.warn( deprecate("The size parameter", 10, None)
"The size parameter is deprecated and will be removed in Pillow 10 "
"(2023-07-01).",
DeprecationWarning,
)
if size != len(self.palette): if size != len(self.palette):
raise ValueError("wrong palette size") raise ValueError("wrong palette size")

View File

@ -15,11 +15,12 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import warnings
from shlex import quote from shlex import quote
from PIL import Image from PIL import Image
from ._deprecate import deprecate
_viewers = [] _viewers = []
@ -120,11 +121,7 @@ class Viewer:
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -176,11 +173,7 @@ class MacViewer(Viewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -228,11 +221,7 @@ class XDGViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -261,11 +250,7 @@ class DisplayViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -296,11 +281,7 @@ class GmDisplayViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -325,11 +306,7 @@ class EogViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -360,11 +337,7 @@ class XVViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")

View File

@ -26,10 +26,10 @@
# #
import tkinter import tkinter
import warnings
from io import BytesIO from io import BytesIO
from . import Image from . import Image
from ._deprecate import deprecate
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Check for Tkinter interface hooks # Check for Tkinter interface hooks
@ -187,11 +187,7 @@ class PhotoImage:
""" """
if box is not None: if box is not None:
warnings.warn( deprecate("The box parameter", 10, None)
"The box parameter is deprecated and will be removed in Pillow 10 "
"(2023-07-01).",
DeprecationWarning,
)
# convert to blittable # convert to blittable
im.load() im.load()

View File

@ -45,6 +45,7 @@ from . import Image, ImageFile, TiffImagePlugin
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._deprecate import deprecate
from .JpegPresets import presets from .JpegPresets import presets
# #
@ -603,11 +604,7 @@ samplings = {
def convert_dict_qtables(qtables): def convert_dict_qtables(qtables):
warnings.warn( deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed")
"convert_dict_qtables is deprecated and will be removed in Pillow 10"
"(2023-07-01). Conversion is no longer needed.",
DeprecationWarning,
)
return qtables return qtables

View File

@ -45,6 +45,7 @@ from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
from ._deprecate import deprecate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -131,24 +132,11 @@ class Blend(IntEnum):
def __getattr__(name): 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(): for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

66
src/PIL/_deprecate.py Normal file
View File

@ -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,
)

View File

@ -2,9 +2,10 @@
""" """
import sys import sys
import tkinter import tkinter
import warnings
from tkinter import _tkinter as tk from tkinter import _tkinter as tk
from ._deprecate import deprecate
try: try:
if hasattr(sys, "pypy_find_executable"): if hasattr(sys, "pypy_find_executable"):
TKINTER_LIB = tk.tklib_cffi.__file__ TKINTER_LIB = tk.tklib_cffi.__file__
@ -17,9 +18,6 @@ except AttributeError:
tk_version = str(tkinter.TkVersion) tk_version = str(tkinter.TkVersion)
if tk_version == "8.4": if tk_version == "8.4":
warnings.warn( deprecate(
"Support for Tk/Tcl 8.4 is deprecated and will be removed" "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer"
" in Pillow 10 (2023-07-01). Please upgrade to Tk/Tcl 8.5 "
"or newer.",
DeprecationWarning,
) )