Skip to content

Commit 994d3cd

Browse files
committed
Add os.PathLike support to FT2Font constructor, and FontManager
Since we pass the filename to `io.open`, we can accept everything it can. Also, fix the return value of `FT2Font.fname`, which could be `bytes` if that was initially provided.
1 parent 43d5d4e commit 994d3cd

File tree

6 files changed

+70
-23
lines changed

6 files changed

+70
-23
lines changed

lib/matplotlib/font_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,10 +1611,10 @@ def get_font(font_filepaths, hinting_factor=None):
16111611
16121612
Parameters
16131613
----------
1614-
font_filepaths : Iterable[str, Path, bytes], str, Path, bytes
1614+
font_filepaths : Iterable[str, bytes, os.PathLike], str, bytes, os.PathLike
16151615
Relative or absolute paths to the font files to be used.
16161616
1617-
If a single string, bytes, or `pathlib.Path`, then it will be treated
1617+
If a single string, bytes, or `os.PathLike`, then it will be treated
16181618
as a list with that entry only.
16191619
16201620
If more than one filepath is passed, then the returned FT2Font object
@@ -1626,7 +1626,7 @@ def get_font(font_filepaths, hinting_factor=None):
16261626
`.ft2font.FT2Font`
16271627
16281628
"""
1629-
if isinstance(font_filepaths, (str, Path, bytes)):
1629+
if isinstance(font_filepaths, (str, bytes, os.PathLike)):
16301630
paths = (_cached_realpath(font_filepaths),)
16311631
else:
16321632
paths = tuple(_cached_realpath(fname) for fname in font_filepaths)

lib/matplotlib/font_manager.pyi

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def list_fonts(directory: str, extensions: Iterable[str]) -> list[str]: ...
2424
def win32FontDirectory() -> str: ...
2525
def _get_fontconfig_fonts() -> list[Path]: ...
2626
def findSystemFonts(
27-
fontpaths: Iterable[str | os.PathLike | Path] | None = ..., fontext: str = ...
27+
fontpaths: Iterable[str | os.PathLike] | None = ..., fontext: str = ...
2828
) -> list[str]: ...
2929
@dataclass
3030
class FontEntry:
@@ -50,7 +50,7 @@ class FontProperties:
5050
weight: int | str | None = ...,
5151
stretch: int | str | None = ...,
5252
size: float | str | None = ...,
53-
fname: str | os.PathLike | Path | None = ...,
53+
fname: str | os.PathLike | None = ...,
5454
math_fontfamily: str | None = ...,
5555
) -> None: ...
5656
def __hash__(self) -> int: ...
@@ -72,7 +72,7 @@ class FontProperties:
7272
def set_weight(self, weight: int | str | None) -> None: ...
7373
def set_stretch(self, stretch: int | str | None) -> None: ...
7474
def set_size(self, size: float | str | None) -> None: ...
75-
def set_file(self, file: str | os.PathLike | Path | None) -> None: ...
75+
def set_file(self, file: str | os.PathLike | None) -> None: ...
7676
def set_fontconfig_pattern(self, pattern: str) -> None: ...
7777
def get_math_fontfamily(self) -> str: ...
7878
def set_math_fontfamily(self, fontfamily: str | None) -> None: ...
@@ -83,8 +83,8 @@ class FontProperties:
8383
set_slant = set_style
8484
get_size_in_points = get_size
8585

86-
def json_dump(data: FontManager, filename: str | Path | os.PathLike) -> None: ...
87-
def json_load(filename: str | Path | os.PathLike) -> FontManager: ...
86+
def json_dump(data: FontManager, filename: str | os.PathLike) -> None: ...
87+
def json_load(filename: str | os.PathLike) -> FontManager: ...
8888

8989
class FontManager:
9090
__version__: str
@@ -93,7 +93,7 @@ class FontManager:
9393
afmlist: list[FontEntry]
9494
ttflist: list[FontEntry]
9595
def __init__(self, size: float | None = ..., weight: str = ...) -> None: ...
96-
def addfont(self, path: str | Path | os.PathLike) -> None: ...
96+
def addfont(self, path: str | os.PathLike) -> None: ...
9797
@property
9898
def defaultFont(self) -> dict[str, str]: ...
9999
def get_default_weight(self) -> str: ...
@@ -120,7 +120,7 @@ class FontManager:
120120

121121
def is_opentype_cff_font(filename: str) -> bool: ...
122122
def get_font(
123-
font_filepaths: Iterable[str | Path | bytes] | str | Path | bytes,
123+
font_filepaths: Iterable[str | bytes | os.PathLike] | str | bytes | os.PathLike,
124124
hinting_factor: int | None = ...,
125125
) -> ft2font.FT2Font: ...
126126

lib/matplotlib/ft2font.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from enum import Enum, Flag
2+
from os import PathLike
23
import sys
34
from typing import BinaryIO, Literal, NewType, TypeAlias, TypedDict, cast, final, overload
45
from typing_extensions import Buffer # < Py 3.12
@@ -194,7 +195,7 @@ class _SfntPcltDict(TypedDict):
194195
class FT2Font(Buffer):
195196
def __init__(
196197
self,
197-
filename: str | BinaryIO,
198+
filename: str | bytes | PathLike | BinaryIO,
198199
hinting_factor: int = ...,
199200
*,
200201
_fallback_list: list[FT2Font] | None = ...,
@@ -256,7 +257,7 @@ class FT2Font(Buffer):
256257
@property
257258
def family_name(self) -> str: ...
258259
@property
259-
def fname(self) -> str: ...
260+
def fname(self) -> str | bytes: ...
260261
@property
261262
def height(self) -> int: ...
262263
@property

lib/matplotlib/tests/test_font_manager.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from io import BytesIO, StringIO
1+
from io import BytesIO
22
import gc
33
import multiprocessing
44
import os
@@ -137,6 +137,32 @@ def test_find_noto():
137137
fig.savefig(BytesIO(), format=fmt)
138138

139139

140+
def test_find_valid():
141+
class PathLikeClass:
142+
def __init__(self, filename):
143+
self.filename = filename
144+
145+
def __fspath__(self):
146+
return self.filename
147+
148+
file_str = findfont('DejaVu Sans')
149+
file_bytes = os.fsencode(file_str)
150+
151+
font = get_font(file_str)
152+
assert font.fname == file_str
153+
font = get_font(file_bytes)
154+
assert font.fname == file_bytes
155+
font = get_font(PathLikeClass(file_str))
156+
assert font.fname == file_str
157+
font = get_font(PathLikeClass(file_bytes))
158+
assert font.fname == file_bytes
159+
160+
# Note, fallbacks are not currently accessible.
161+
font = get_font([file_str, file_bytes,
162+
PathLikeClass(file_str), PathLikeClass(file_bytes)])
163+
assert font.fname == file_str
164+
165+
140166
def test_find_invalid(tmp_path):
141167

142168
with pytest.raises(FileNotFoundError):
@@ -148,11 +174,6 @@ def test_find_invalid(tmp_path):
148174
with pytest.raises(FileNotFoundError):
149175
get_font(bytes(tmp_path / 'non-existent-font-name.ttf'))
150176

151-
# Not really public, but get_font doesn't expose non-filename constructor.
152-
from matplotlib.ft2font import FT2Font
153-
with pytest.raises(TypeError, match='font file or a binary-mode file'):
154-
FT2Font(StringIO()) # type: ignore[arg-type]
155-
156177

157178
@pytest.mark.skipif(sys.platform != 'linux' or not has_fclist,
158179
reason='only Linux with fontconfig installed')

lib/matplotlib/tests/test_ft2font.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import itertools
22
import io
3+
import os
34
from pathlib import Path
45
from typing import cast
56

@@ -134,6 +135,27 @@ def test_ft2font_stix_bold_attrs():
134135
assert font.bbox == (4, -355, 1185, 2095)
135136

136137

138+
def test_ft2font_valid_args():
139+
class PathLikeClass:
140+
def __init__(self, filename):
141+
self.filename = filename
142+
143+
def __fspath__(self):
144+
return self.filename
145+
146+
file_str = fm.findfont('DejaVu Sans')
147+
file_bytes = os.fsencode(file_str)
148+
149+
font = ft2font.FT2Font(file_str)
150+
assert font.fname == file_str
151+
font = ft2font.FT2Font(file_bytes)
152+
assert font.fname == file_bytes
153+
font = ft2font.FT2Font(PathLikeClass(file_str))
154+
assert font.fname == file_str
155+
font = ft2font.FT2Font(PathLikeClass(file_bytes))
156+
assert font.fname == file_bytes
157+
158+
137159
def test_ft2font_invalid_args(tmp_path):
138160
# filename argument.
139161
with pytest.raises(TypeError, match='to a font file or a binary-mode file object'):

src/ft2font_wrapper.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ close_file_callback(FT_Stream stream)
424424
const char *PyFT2Font_init__doc__ = R"""(
425425
Parameters
426426
----------
427-
filename : str or file-like
427+
filename : str, bytes, os.PathLike, or io.BinaryIO
428428
The source of the font data in a format (ttf or ttc) that FreeType can read.
429429
430430
hinting_factor : int, optional
@@ -488,7 +488,10 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8,
488488
open_args.flags = FT_OPEN_STREAM;
489489
open_args.stream = &self->stream;
490490

491-
if (py::isinstance<py::bytes>(filename) || py::isinstance<py::str>(filename)) {
491+
auto PathLike = py::module_::import("os").attr("PathLike");
492+
if (py::isinstance<py::bytes>(filename) || py::isinstance<py::str>(filename) ||
493+
py::isinstance(filename, PathLike))
494+
{
492495
self->py_file = py::module_::import("io").attr("open")(filename, "rb");
493496
self->stream.close = &close_file_callback;
494497
} else {
@@ -511,13 +514,13 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8,
511514
return self;
512515
}
513516

514-
static py::str
517+
static py::object
515518
PyFT2Font_fname(PyFT2Font *self)
516519
{
517-
if (self->stream.close) { // Called passed a filename to the constructor.
520+
if (self->stream.close) { // User passed a filename to the constructor.
518521
return self->py_file.attr("name");
519522
} else {
520-
return py::cast<py::str>(self->py_file);
523+
return self->py_file;
521524
}
522525
}
523526

0 commit comments

Comments
 (0)