Skip to content

Commit

Permalink
Merge pull request #1 from radarhere/typing
Browse files Browse the repository at this point in the history
Move type hints and repair tests
  • Loading branch information
AsfhtgkDavid authored Jan 26, 2025
2 parents 5373c5c + eed5926 commit 09a9ee8
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 82 deletions.
28 changes: 15 additions & 13 deletions Tests/test_imagefont.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
)
def test_render_multiline_text_align(
font: ImageFont.FreeTypeFont, align: str, ext: str
font: ImageFont.FreeTypeFont, align: ImageDraw.Align, ext: str
) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
Expand All @@ -272,7 +272,7 @@ def test_unknown_align(font: ImageFont.FreeTypeFont) -> None:

# Act/Assert
with pytest.raises(ValueError):
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") # type: ignore[arg-type]


def test_draw_align(font: ImageFont.FreeTypeFont) -> None:
Expand Down Expand Up @@ -795,7 +795,7 @@ def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
)
def test_anchor(
layout_engine: ImageFont.Layout, anchor: str, left: int, top: int
layout_engine: ImageFont.Layout, anchor: ImageFont.Anchor, left: int, top: int
) -> None:
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
Expand Down Expand Up @@ -842,7 +842,7 @@ def test_anchor(
),
)
def test_anchor_multiline(
layout_engine: ImageFont.Layout, anchor: str, align: str
layout_engine: ImageFont.Layout, anchor: ImageFont.Anchor, align: ImageDraw.Align
) -> None:
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
text = "a\nlong\ntext sample"
Expand All @@ -868,22 +868,24 @@ def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None:

for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
with pytest.raises(ValueError):
font.getmask2("hello", anchor=anchor)
font.getmask2("hello", anchor=anchor) # type: ignore[arg-type]
with pytest.raises(ValueError):
font.getbbox("hello", anchor=anchor)
font.getbbox("hello", anchor=anchor) # type: ignore[arg-type]
with pytest.raises(ValueError):
d.text((0, 0), "hello", anchor=anchor)
d.text((0, 0), "hello", anchor=anchor) # type: ignore[arg-type]
with pytest.raises(ValueError):
d.textbbox((0, 0), "hello", anchor=anchor)
d.textbbox((0, 0), "hello", anchor=anchor) # type: ignore[arg-type]
with pytest.raises(ValueError):
d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
d.multiline_text((0, 0), "foo\nbar", anchor=anchor) # type: ignore[arg-type]
with pytest.raises(ValueError):
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
for anchor in ["lt", "lb"]:
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) # type: ignore[arg-type]

anchors: list[ImageFont.Anchor] = ["lt", "lb"]
for anchor2 in anchors:
with pytest.raises(ValueError):
d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
d.multiline_text((0, 0), "foo\nbar", anchor=anchor2)
with pytest.raises(ValueError):
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor2)


@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
Expand Down
28 changes: 17 additions & 11 deletions Tests/test_imagefontctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def test_getlength(
d = ImageDraw.Draw(im)

try:
assert d.textlength(text, ttf, direction) == expected
assert d.textlength(text, ttf, direction) == expected # type: ignore[arg-type]
except ValueError as ex:
if (
direction == "ttb"
Expand All @@ -232,7 +232,9 @@ def test_getlength(
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
ids=("caron-above", "caron-below", "double-breve", "overline"),
)
def test_getlength_combine(mode: str, direction: str, text: str) -> None:
def test_getlength_combine(
mode: str, direction: ImageFont.Direction, text: str
) -> None:
if text == "i\u0305i" and direction == "ttb":
pytest.skip("fails with this font")

Expand All @@ -252,7 +254,7 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None:


@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
def test_anchor_ttb(anchor: str) -> None:
def test_anchor_ttb(anchor: ImageFont.Anchor) -> None:
text = "f"
path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)
Expand Down Expand Up @@ -309,7 +311,11 @@ def test_anchor_ttb(anchor: str) -> None:
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
)
def test_combine(
name: str, text: str, dir: str | None, anchor: str | None, epsilon: float
name: str,
text: str,
anchor: ImageFont.Anchor | None,
dir: ImageFont.Direction | None,
epsilon: float,
) -> None:
path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
Expand Down Expand Up @@ -341,7 +347,7 @@ def test_combine(
("rm", "right"), # pass with getsize
),
)
def test_combine_multiline(anchor: str, align: str) -> None:
def test_combine_multiline(anchor: ImageFont.Anchor, align: ImageDraw.Align) -> None:
# test that multiline text uses getlength, not getsize or getbbox

path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
Expand All @@ -367,17 +373,17 @@ def test_anchor_invalid_ttb() -> None:

for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]:
with pytest.raises(ValueError):
font.getmask2("hello", anchor=anchor, direction="ttb")
font.getmask2("hello", anchor=anchor, direction="ttb") # type: ignore[arg-type]
with pytest.raises(ValueError):
font.getbbox("hello", anchor=anchor, direction="ttb")
font.getbbox("hello", anchor=anchor, direction="ttb") # type: ignore[arg-type]
with pytest.raises(ValueError):
d.text((0, 0), "hello", anchor=anchor, direction="ttb")
d.text((0, 0), "hello", anchor=anchor, direction="ttb") # type: ignore[arg-type]
with pytest.raises(ValueError):
d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb")
d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") # type: ignore[arg-type]
with pytest.raises(ValueError):
d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # type: ignore[arg-type]
with pytest.raises(ValueError):
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # type: ignore[arg-type]
# ttb multiline text does not support anchors at all
with pytest.raises(ValueError):
d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb")
Expand Down
11 changes: 7 additions & 4 deletions docs/example/anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16)


def test(anchor: str) -> Image.Image:
def test(anchor: ImageFont.Anchor) -> Image.Image:
im = Image.new("RGBA", (200, 100), "white")
d = ImageDraw.Draw(im)
d.line(((100, 0), (100, 100)), "gray")
Expand All @@ -17,9 +17,12 @@ def test(anchor: str) -> Image.Image:
if __name__ == "__main__":
im = Image.new("RGBA", (600, 300), "white")
d = ImageDraw.Draw(im)
for y, row in enumerate(
(("ma", "mt", "mm"), ("ms", "mb", "md"), ("ls", "ms", "rs"))
):
anchors: list[list[ImageFont.Anchor]] = [
["ma", "mt", "mm"],
["ms", "mb", "md"],
["ls", "ms", "rs"],
]
for y, row in enumerate(anchors):
for x, anchor in enumerate(row):
im.paste(test(anchor), (x * 200, y * 100))
if x != 0:
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/ImageFont.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ Constants
raise a :py:exc:`ValueError` if the number of characters is over this limit. The
check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.

.. class:: Anchor

Type hint literal with the possible anchor values. See :ref:`text-anchors` for
details.

Dictionaries
------------

Expand Down
22 changes: 12 additions & 10 deletions src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

from . import Image, ImageColor
from ._deprecate import deprecate
from ._typing import Align, Anchor, Coords, Direction
from ._typing import Coords

# experimental access to the outline API
Outline: Callable[[], Image.core._Outline] | None
Expand All @@ -51,6 +51,8 @@
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont

Align = Literal["left", "center", "right"]

_Ink = Union[float, tuple[int, ...], str]

"""
Expand Down Expand Up @@ -583,10 +585,10 @@ def text(
| ImageFont.TransposedFont
| None
) = None,
anchor: Anchor | None = None,
anchor: ImageFont.Anchor | None = None,
spacing: float = 4,
align: Align = "left",
direction: Direction | None = None,
direction: ImageFont.Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
Expand Down Expand Up @@ -708,10 +710,10 @@ def multiline_text(
| ImageFont.TransposedFont
| None
) = None,
anchor: Anchor | None = None,
anchor: ImageFont.Anchor | None = None,
spacing: float = 4,
align: Align = "left",
direction: Direction | None = None,
direction: ImageFont.Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
Expand Down Expand Up @@ -798,7 +800,7 @@ def textlength(
| ImageFont.TransposedFont
| None
) = None,
direction: Direction | None = None,
direction: ImageFont.Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
embedded_color: bool = False,
Expand Down Expand Up @@ -828,10 +830,10 @@ def textbbox(
| ImageFont.TransposedFont
| None
) = None,
anchor: Anchor | None = None,
anchor: ImageFont.Anchor | None = None,
spacing: float = 4,
align: Align = "left",
direction: Direction | None = None,
direction: ImageFont.Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
Expand Down Expand Up @@ -878,10 +880,10 @@ def multiline_textbbox(
| ImageFont.TransposedFont
| None
) = None,
anchor: Anchor | None = None,
anchor: ImageFont.Anchor | None = None,
spacing: float = 4,
align: Align = "left",
direction: Direction | None = None,
direction: ImageFont.Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
Expand Down
45 changes: 37 additions & 8 deletions src/PIL/ImageFont.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from enum import IntEnum
from io import BytesIO
from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast
from typing import IO, TYPE_CHECKING, Any, BinaryIO, Literal, TypedDict, cast

from . import Image, features
from ._typing import StrOrBytesPath
Expand All @@ -45,6 +45,35 @@
from ._imaging import ImagingFont
from ._imagingft import Font

Anchor = Literal[
"la",
"lt",
"lm",
"ls",
"lb",
"ld",
"ma",
"mt",
"mm",
"ms",
"mb",
"md",
"ra",
"rt",
"rm",
"rs",
"rb",
"rd",
"sa",
"st",
"sm",
"ss",
"sb",
"sd",
]

Direction = Literal["rtl", "ltr", "ttb"]


class Axis(TypedDict):
minimum: int | None
Expand Down Expand Up @@ -313,7 +342,7 @@ def getlength(
self,
text: str | bytes,
mode: str = "",
direction: str | None = None,
direction: Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
) -> float:
Expand Down Expand Up @@ -392,11 +421,11 @@ def getbbox(
self,
text: str | bytes,
mode: str = "",
direction: str | None = None,
direction: Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
anchor: str | None = None,
anchor: Anchor | None = None,
) -> tuple[float, float, float, float]:
"""
Returns bounding box (in pixels) of given text relative to given anchor
Expand Down Expand Up @@ -458,11 +487,11 @@ def getmask(
self,
text: str | bytes,
mode: str = "",
direction: str | None = None,
direction: Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
anchor: str | None = None,
anchor: Anchor | None = None,
ink: int = 0,
start: tuple[float, float] | None = None,
) -> Image.core.ImagingCore:
Expand Down Expand Up @@ -549,11 +578,11 @@ def getmask2(
self,
text: str | bytes,
mode: str = "",
direction: str | None = None,
direction: Direction | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
anchor: str | None = None,
anchor: Anchor | None = None,
ink: int = 0,
start: tuple[float, float] | None = None,
*args: Any,
Expand Down
10 changes: 5 additions & 5 deletions src/PIL/_imagingft.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ class Font:
string: str | bytes,
fill: Callable[[int, int], _imaging.ImagingCore],
mode: str,
dir: str | None,
dir: ImageFont.Direction | None,
features: list[str] | None,
lang: str | None,
stroke_width: float,
anchor: str | None,
anchor: ImageFont.Anchor | None,
foreground_ink_long: int,
x_start: float,
y_start: float,
Expand All @@ -38,17 +38,17 @@ class Font:
self,
string: str | bytes | bytearray,
mode: str,
dir: str | None,
dir: ImageFont.Direction | None,
features: list[str] | None,
lang: str | None,
anchor: str | None,
anchor: ImageFont.Anchor | None,
/,
) -> tuple[tuple[int, int], tuple[int, int]]: ...
def getlength(
self,
string: str | bytes,
mode: str,
dir: str | None,
dir: ImageFont.Direction | None,
features: list[str] | None,
lang: str | None,
/,
Expand Down
Loading

0 comments on commit 09a9ee8

Please sign in to comment.