From f5b761d4654521ec11b6432df55e32a611a00b4d Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 19 Feb 2023 05:50:28 +1000 Subject: [PATCH] Add types for PIL.ImageTk (#9733) --- stdlib/tkinter/__init__.pyi | 27 ++++++--- .../@tests/test_cases/check_tk_compat.py | 15 +++++ stubs/Pillow/PIL/ImageTk.pyi | 60 +++++++++++++++---- 3 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 stubs/Pillow/@tests/test_cases/check_tk_compat.py diff --git a/stdlib/tkinter/__init__.pyi b/stdlib/tkinter/__init__.pyi index 1d30e4b73..9dc13c809 100644 --- a/stdlib/tkinter/__init__.pyi +++ b/stdlib/tkinter/__init__.pyi @@ -6,7 +6,7 @@ from enum import Enum from tkinter.constants import * from tkinter.font import _FontDescription from types import TracebackType -from typing import Any, Generic, NamedTuple, Protocol, TypeVar, overload +from typing import Any, Generic, NamedTuple, Protocol, TypeVar, overload, type_check_only from typing_extensions import Literal, TypeAlias, TypedDict if sys.version_info >= (3, 9): @@ -666,7 +666,7 @@ class Wm: iconmask = wm_iconmask def wm_iconname(self, newName: Incomplete | None = None) -> str: ... iconname = wm_iconname - def wm_iconphoto(self, default: bool, __image1: Image, *args: Image) -> None: ... + def wm_iconphoto(self, default: bool, __image1: _PhotoImageLike | str, *args: _PhotoImageLike | str) -> None: ... iconphoto = wm_iconphoto def wm_iconposition(self, x: int | None = None, y: int | None = None) -> tuple[int, int] | None: ... iconposition = wm_iconposition @@ -3206,12 +3206,19 @@ class OptionMenu(Menubutton): # configure, config, cget are inherited from Menubutton # destroy and __getitem__ are overridden, signature does not change -class _Image(Protocol): - tk: _tkinter.TkappType - def height(self) -> int: ... - def width(self) -> int: ... +# Marker to indicate that it is a valid bitmap/photo image. PIL implements compatible versions +# which don't share a class hierachy. The actual API is a __str__() which returns a valid name, +# not something that type checkers can detect. +@type_check_only +class _Image: ... -class Image: +@type_check_only +class _BitmapImageLike(_Image): ... + +@type_check_only +class _PhotoImageLike(_Image): ... + +class Image(_Image): name: Incomplete tk: _tkinter.TkappType def __init__( @@ -3226,7 +3233,8 @@ class Image: def type(self): ... def width(self) -> int: ... -class PhotoImage(Image): +class PhotoImage(Image, _PhotoImageLike): + # This should be kept in sync with PIL.ImageTK.PhotoImage.__init__() def __init__( self, name: str | None = None, @@ -3278,7 +3286,8 @@ class PhotoImage(Image): def transparency_get(self, x: int, y: int) -> bool: ... def transparency_set(self, x: int, y: int, boolean: bool) -> None: ... -class BitmapImage(Image): +class BitmapImage(Image, _BitmapImageLike): + # This should be kept in sync with PIL.ImageTK.BitmapImage.__init__() def __init__( self, name: Incomplete | None = None, diff --git a/stubs/Pillow/@tests/test_cases/check_tk_compat.py b/stubs/Pillow/@tests/test_cases/check_tk_compat.py new file mode 100644 index 000000000..05332d505 --- /dev/null +++ b/stubs/Pillow/@tests/test_cases/check_tk_compat.py @@ -0,0 +1,15 @@ +# Verify that ImageTK images are valid to pass to TK code. +from __future__ import annotations + +import tkinter + +from PIL import ImageTk + +photo = ImageTk.PhotoImage() +bitmap = ImageTk.BitmapImage() + +tkinter.Label(image=photo) +tkinter.Label(image=bitmap) + +tkinter.Label().configure(image=photo) +tkinter.Label().configure(image=bitmap) diff --git a/stubs/Pillow/PIL/ImageTk.pyi b/stubs/Pillow/PIL/ImageTk.pyi index fd4d362e3..af3bd4fc2 100644 --- a/stubs/Pillow/PIL/ImageTk.pyi +++ b/stubs/Pillow/PIL/ImageTk.pyi @@ -1,18 +1,52 @@ -from _typeshed import Incomplete +import _tkinter +import tkinter +from _typeshed import ReadableBuffer, StrOrBytesPath, SupportsRead from typing import Any -class PhotoImage: - tk: Any - def __init__(self, image: Incomplete | None = ..., size: Incomplete | None = ..., **kw) -> None: ... - def __del__(self) -> None: ... - def width(self): ... - def height(self): ... - def paste(self, im, box: Incomplete | None = ...) -> None: ... +from PIL.Image import Image, _Box, _Mode, _Size -class BitmapImage: - def __init__(self, image: Incomplete | None = ..., **kw) -> None: ... +class PhotoImage(tkinter._PhotoImageLike): + tk: _tkinter.TkappType + def __init__( + self, + image: Image | _Mode | None = None, + size: _Size | None = None, + *, + file: StrOrBytesPath | SupportsRead[bytes] = ..., + data: ReadableBuffer = ..., + # These are forwarded to tkinter.PhotoImage.__init__(): + name: str | None = None, + cnf: dict[str, Any] = ..., + format: str = ..., + gamma: float = ..., + height: int = ..., + palette: int | str = ..., + width: int = ..., + ) -> None: ... def __del__(self) -> None: ... - def width(self): ... - def height(self): ... + def width(self) -> int: ... + def height(self) -> int: ... + # box is deprecated and unused + def paste(self, im: Image, box: _Box | None = ...) -> None: ... -def getimage(photo): ... +class BitmapImage(tkinter._BitmapImageLike): + def __init__( + self, + image: Image | None = None, + *, + file: StrOrBytesPath | SupportsRead[bytes] = ..., + data: ReadableBuffer = ..., + # These are forwarded to tkinter.Bitmap.__init__(): + name: str | None = None, + cnf: dict[str, Any] = ..., + master: tkinter.Misc | _tkinter.TkappType | None = None, + background: tkinter._Color = ..., + foreground: tkinter._Color = ..., + maskdata: str = ..., + maskfile: StrOrBytesPath = ..., + ) -> None: ... + def __del__(self) -> None: ... + def width(self) -> int: ... + def height(self) -> int: ... + +def getimage(photo: tkinter.PhotoImage) -> Image: ...