Skip to content

Commit d2b5e11

Browse files
authored
Merge pull request #8032 from nulano/type_hints
Added type hints for PixelAccess related methods and others
2 parents a941f09 + ab18395 commit d2b5e11

File tree

9 files changed

+78
-71
lines changed

9 files changed

+78
-71
lines changed

Tests/test_imagegrab.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def test_grabclipboard_file(self) -> None:
8989
p.communicate()
9090

9191
im = ImageGrab.grabclipboard()
92+
assert isinstance(im, list)
9293
assert len(im) == 1
9394
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
9495

@@ -105,6 +106,7 @@ def test_grabclipboard_png(self) -> None:
105106
p.communicate()
106107

107108
im = ImageGrab.grabclipboard()
109+
assert isinstance(im, Image.Image)
108110
assert_image_equal_tofile(im, "Tests/images/hopper.png")
109111

110112
@pytest.mark.skipif(
@@ -120,6 +122,7 @@ def test_grabclipboard_wl_clipboard(self, ext: str) -> None:
120122
with open(image_path, "rb") as fp:
121123
subprocess.call(["wl-copy"], stdin=fp)
122124
im = ImageGrab.grabclipboard()
125+
assert isinstance(im, Image.Image)
123126
assert_image_equal_tofile(im, image_path)
124127

125128
@pytest.mark.skipif(

docs/reference/ImageDraw.rst

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -691,23 +691,7 @@ Methods
691691
:param hints: An optional list of hints.
692692
:returns: A (drawing context, drawing resource factory) tuple.
693693

694-
.. py:method:: floodfill(image, xy, value, border=None, thresh=0)
695-
696-
.. warning:: This method is experimental.
697-
698-
Fills a bounded region with a given color.
699-
700-
:param image: Target image.
701-
:param xy: Seed position (a 2-item coordinate tuple).
702-
:param value: Fill color.
703-
:param border: Optional border value. If given, the region consists of
704-
pixels with a color different from the border color. If not given,
705-
the region consists of pixels having the same color as the seed
706-
pixel.
707-
:param thresh: Optional threshold value which specifies a maximum
708-
tolerable difference of a pixel value from the 'background' in
709-
order for it to be replaced. Useful for filling regions of non-
710-
homogeneous, but similar, colors.
694+
.. autofunction:: PIL.ImageDraw.floodfill
711695

712696
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
713697
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist

docs/reference/PixelAccess.rst

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,42 +44,23 @@ Access using negative indexes is also possible. ::
4444
-----------------------------
4545

4646
.. class:: PixelAccess
47+
:canonical: PIL.Image.core.PixelAccess
4748

48-
.. method:: __setitem__(self, xy, color):
49+
.. method:: __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]
4950

50-
Modifies the pixel at x,y. The color is given as a single
51-
numerical value for single band images, and a tuple for
52-
multi-band images
53-
54-
:param xy: The pixel coordinate, given as (x, y).
55-
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
56-
57-
.. method:: __getitem__(self, xy):
58-
59-
Returns the pixel at x,y. The pixel is returned as a single
60-
value for single band images or a tuple for multiple band
61-
images
51+
Returns the pixel at x,y. The pixel is returned as a single
52+
value for single band images or a tuple for multi-band images.
6253

6354
:param xy: The pixel coordinate, given as (x, y).
6455
:returns: a pixel value for single band images, a tuple of
65-
pixel values for multiband images.
56+
pixel values for multiband images.
6657

67-
.. method:: putpixel(self, xy, color):
58+
.. method:: __setitem__(self, xy: tuple[int, int], color: float | tuple[int, ...]) -> None
6859

6960
Modifies the pixel at x,y. The color is given as a single
7061
numerical value for single band images, and a tuple for
71-
multi-band images. In addition to this, RGB and RGBA tuples
72-
are accepted for P and PA images.
62+
multi-band images.
7363

7464
:param xy: The pixel coordinate, given as (x, y).
75-
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
76-
77-
.. method:: getpixel(self, xy):
78-
79-
Returns the pixel at x,y. The pixel is returned as a single
80-
value for single band images or a tuple for multiple band
81-
images
82-
83-
:param xy: The pixel coordinate, given as (x, y).
84-
:returns: a pixel value for single band images, a tuple of
85-
pixel values for multiband images.
65+
:param color: The pixel value according to its mode,
66+
e.g. tuple (r, g, b) for RGB mode.

docs/reference/PyAccess.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::
4444

4545
.. autoclass:: PIL.PyAccess.PyAccess()
4646
:members:
47+
:special-members: __getitem__, __setitem__

src/PIL/Image.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@
4141
from collections.abc import Callable, MutableMapping
4242
from enum import IntEnum
4343
from types import ModuleType
44-
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast
44+
from typing import (
45+
IO,
46+
TYPE_CHECKING,
47+
Any,
48+
Literal,
49+
Protocol,
50+
Sequence,
51+
Tuple,
52+
cast,
53+
)
4554

4655
# VERSION was removed in Pillow 6.0.0.
4756
# PILLOW_VERSION was removed in Pillow 9.0.0.
@@ -218,7 +227,7 @@ class Quantize(IntEnum):
218227
# Registries
219228

220229
if TYPE_CHECKING:
221-
from . import ImageFile
230+
from . import ImageFile, PyAccess
222231
ID: list[str] = []
223232
OPEN: dict[
224233
str,
@@ -871,7 +880,7 @@ def frombytes(
871880
msg = "cannot decode image data"
872881
raise ValueError(msg)
873882

874-
def load(self):
883+
def load(self) -> core.PixelAccess | PyAccess.PyAccess | None:
875884
"""
876885
Allocates storage for the image and loads the pixel data. In
877886
normal cases, you don't need to call this method, since the
@@ -884,7 +893,7 @@ def load(self):
884893
operations. See :ref:`file-handling` for more information.
885894
886895
:returns: An image access object.
887-
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
896+
:rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess`
888897
"""
889898
if self.im is not None and self.palette and self.palette.dirty:
890899
# realize palette
@@ -913,6 +922,7 @@ def load(self):
913922
if self.pyaccess:
914923
return self.pyaccess
915924
return self.im.pixel_access(self.readonly)
925+
return None
916926

917927
def verify(self) -> None:
918928
"""
@@ -1102,7 +1112,10 @@ def convert_transparency(
11021112
del new_im.info["transparency"]
11031113
if trns is not None:
11041114
try:
1105-
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
1115+
new_im.info["transparency"] = new_im.palette.getcolor(
1116+
cast(Tuple[int, int, int], trns), # trns was converted to RGB
1117+
new_im,
1118+
)
11061119
except Exception:
11071120
# if we can't make a transparent color, don't leave the old
11081121
# transparency hanging around to mess us up.
@@ -1152,7 +1165,10 @@ def convert_transparency(
11521165
if trns is not None:
11531166
if new_im.mode == "P":
11541167
try:
1155-
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
1168+
new_im.info["transparency"] = new_im.palette.getcolor(
1169+
cast(Tuple[int, int, int], trns), # trns was converted to RGB
1170+
new_im,
1171+
)
11561172
except ValueError as e:
11571173
del new_im.info["transparency"]
11581174
if str(e) != "cannot allocate more than 256 colors":
@@ -1657,7 +1673,9 @@ def apply_transparency(self) -> None:
16571673

16581674
del self.info["transparency"]
16591675

1660-
def getpixel(self, xy):
1676+
def getpixel(
1677+
self, xy: tuple[int, int] | list[int]
1678+
) -> float | tuple[int, ...] | None:
16611679
"""
16621680
Returns the pixel value at a given position.
16631681
@@ -1941,15 +1959,14 @@ def point(self, data):
19411959
flatLut = [round(i) for i in flatLut]
19421960
return self._new(self.im.point(flatLut, mode))
19431961

1944-
def putalpha(self, alpha):
1962+
def putalpha(self, alpha: Image | int) -> None:
19451963
"""
19461964
Adds or replaces the alpha layer in this image. If the image
19471965
does not have an alpha layer, it's converted to "LA" or "RGBA".
19481966
The new layer must be either "L" or "1".
19491967
19501968
:param alpha: The new alpha layer. This can either be an "L" or "1"
1951-
image having the same size as this image, or an integer or
1952-
other color value.
1969+
image having the same size as this image, or an integer.
19531970
"""
19541971

19551972
self._ensure_mutable()
@@ -1988,6 +2005,7 @@ def putalpha(self, alpha):
19882005
alpha = alpha.convert("L")
19892006
else:
19902007
# constant alpha
2008+
alpha = cast(int, alpha) # see python/typing#1013
19912009
try:
19922010
self.im.fillband(band, alpha)
19932011
except (AttributeError, ValueError):
@@ -2056,7 +2074,9 @@ def putpalette(self, data, rawmode="RGB") -> None:
20562074
self.palette.mode = "RGBA" if "A" in rawmode else "RGB"
20572075
self.load() # install new palette
20582076

2059-
def putpixel(self, xy, value):
2077+
def putpixel(
2078+
self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int]
2079+
) -> None:
20602080
"""
20612081
Modifies the pixel at the given position. The color is given as
20622082
a single numerical value for single-band images, and a tuple for
@@ -2094,9 +2114,8 @@ def putpixel(self, xy, value):
20942114
if self.mode == "PA":
20952115
alpha = value[3] if len(value) == 4 else 255
20962116
value = value[:3]
2097-
value = self.palette.getcolor(value, self)
2098-
if self.mode == "PA":
2099-
value = (value, alpha)
2117+
palette_index = self.palette.getcolor(value, self)
2118+
value = (palette_index, alpha) if self.mode == "PA" else palette_index
21002119
return self.im.putpixel(xy, value)
21012120

21022121
def remap_palette(self, dest_map, source_palette=None):

src/PIL/ImageDraw.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,9 @@ def floodfill(
10071007
thresh: float = 0,
10081008
) -> None:
10091009
"""
1010-
(experimental) Fills a bounded region with a given color.
1010+
.. warning:: This method is experimental.
1011+
1012+
Fills a bounded region with a given color.
10111013
10121014
:param image: Target image.
10131015
:param xy: Seed position (a 2-item coordinate tuple). See
@@ -1025,6 +1027,7 @@ def floodfill(
10251027
# based on an implementation by Eric S. Raymond
10261028
# amended by yo1995 @20180806
10271029
pixel = image.load()
1030+
assert pixel is not None
10281031
x, y = xy
10291032
try:
10301033
background = pixel[x, y]

src/PIL/ImageGrab.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@
2626
from . import Image
2727

2828

29-
def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
29+
def grab(
30+
bbox: tuple[int, int, int, int] | None = None,
31+
include_layered_windows: bool = False,
32+
all_screens: bool = False,
33+
xdisplay: str | None = None,
34+
) -> Image.Image:
35+
im: Image.Image
3036
if xdisplay is None:
3137
if sys.platform == "darwin":
3238
fh, filepath = tempfile.mkstemp(".png")
@@ -63,14 +69,16 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
6369
left, top, right, bottom = bbox
6470
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
6571
return im
72+
# Cast to Optional[str] needed for Windows and macOS.
73+
display_name: str | None = xdisplay
6674
try:
6775
if not Image.core.HAVE_XCB:
6876
msg = "Pillow was built without XCB support"
6977
raise OSError(msg)
70-
size, data = Image.core.grabscreen_x11(xdisplay)
78+
size, data = Image.core.grabscreen_x11(display_name)
7179
except OSError:
7280
if (
73-
xdisplay is None
81+
display_name is None
7482
and sys.platform not in ("darwin", "win32")
7583
and shutil.which("gnome-screenshot")
7684
):
@@ -94,7 +102,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
94102
return im
95103

96104

97-
def grabclipboard():
105+
def grabclipboard() -> Image.Image | list[str] | None:
98106
if sys.platform == "darwin":
99107
fh, filepath = tempfile.mkstemp(".png")
100108
os.close(fh)

src/PIL/PyAccess.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ def __init__(self, img: Image.Image, readonly: bool = False) -> None:
7777
def _post_init(self) -> None:
7878
pass
7979

80-
def __setitem__(self, xy, color):
80+
def __setitem__(
81+
self,
82+
xy: tuple[int, int] | list[int],
83+
color: float | tuple[int, ...] | list[int],
84+
) -> None:
8185
"""
8286
Modifies the pixel at x,y. The color is given as a single
8387
numerical value for single band images, and a tuple for
@@ -107,13 +111,12 @@ def __setitem__(self, xy, color):
107111
if self._im.mode == "PA":
108112
alpha = color[3] if len(color) == 4 else 255
109113
color = color[:3]
110-
color = self._palette.getcolor(color, self._img)
111-
if self._im.mode == "PA":
112-
color = (color, alpha)
114+
palette_index = self._palette.getcolor(color, self._img)
115+
color = (palette_index, alpha) if self._im.mode == "PA" else palette_index
113116

114117
return self.set_pixel(x, y, color)
115118

116-
def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]:
119+
def __getitem__(self, xy: tuple[int, int] | list[int]) -> float | tuple[int, ...]:
117120
"""
118121
Returns the pixel at x,y. The pixel is returned as a single
119122
value for single band images or a tuple for multiple band
@@ -145,7 +148,9 @@ def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]:
145148
def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]:
146149
raise NotImplementedError()
147150

148-
def set_pixel(self, x: int, y: int, color: float | tuple[int, ...]) -> None:
151+
def set_pixel(
152+
self, x: int, y: int, color: float | tuple[int, ...] | list[int]
153+
) -> None:
149154
raise NotImplementedError()
150155

151156

src/PIL/_imaging.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ class ImagingDraw:
1010
def __getattr__(self, name: str) -> Any: ...
1111

1212
class PixelAccess:
13-
def __getattr__(self, name: str) -> Any: ...
13+
def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ...
14+
def __setitem__(
15+
self, xy: tuple[int, int], color: float | tuple[int, ...]
16+
) -> None: ...
1417

1518
class ImagingDecoder:
1619
def __getattr__(self, name: str) -> Any: ...

0 commit comments

Comments
 (0)