Skip to content

Commit 73cbd3d

Browse files
committed
MNT: Cleanup FontProperties __init__ API
The cleanup approach is to not modify code logic during a deprecation period, but only detect deprecated call patterns through a decorator and warn on them.
1 parent 5a0cdf1 commit 73cbd3d

File tree

4 files changed

+111
-4
lines changed

4 files changed

+111
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FontProperties initialization
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
`.FontProperties` initialization is limited to the two call patterns:
5+
6+
- single positional parameter, interpreted as fontconfig pattern
7+
- only keyword parameters for setting individual properties
8+
9+
All other previously supported call patterns are deprecated.

lib/matplotlib/font_manager.py

+61-3
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929

3030
from base64 import b64encode
3131
from collections import namedtuple
32+
from collections.abc import Iterable
3233
import copy
3334
import dataclasses
3435
from functools import lru_cache
36+
import functools
3537
from io import BytesIO
3638
import json
3739
import logging
@@ -536,6 +538,56 @@ def afmFontProperty(fontpath, font):
536538
return FontEntry(fontpath, name, style, variant, weight, stretch, size)
537539

538540

541+
def _cleanup_fontproperties_init(init_method):
542+
"""
543+
A decorator to limit the call signature to single a positional argument
544+
or alternatively only keyword arguments.
545+
546+
We still accept but deprecate all other call signatures.
547+
548+
When the deprecation expires we can switch the signature to::
549+
550+
__init__(self, pattern=None, /, *, family=None, style=None, ...)
551+
552+
plus a runtime check that pattern is not used alongside with the
553+
keyword arguments. This results eventually in the two possible
554+
call signatures::
555+
556+
FontProperties(pattern)
557+
FontProperties(family=..., size=..., ...)
558+
559+
"""
560+
@functools.wraps(init_method)
561+
def wrapper(self, *args, **kwargs):
562+
# multiple args with at least some positional ones
563+
if len(args) > 1 or len(args) == 1 and kwargs:
564+
# Note: Both cases were previously handled as individual properties.
565+
# Therefore, we do not mention the case of font properties here.
566+
_api.warn_deprecated(
567+
"3.10",
568+
message="Passing individual properties to FontProperties() "
569+
"positionally is deprecated. Please pass all properties "
570+
"via keyword arguments."
571+
)
572+
# single non-string arg -> clearly a family not a pattern
573+
if (len(args) == 1 and not kwargs
574+
and isinstance(args[0], Iterable) and not isinstance(args[0], str)):
575+
# Case font-family list passed as single argument
576+
_api.warn_deprecated(
577+
"3.10",
578+
message="Passing family as positional argument to FontProperties() "
579+
"is deprecated. Please pass family names as keyword"
580+
"argument."
581+
)
582+
# Note on single string arg:
583+
# This has been interpreted as pattern so far. We are already raising if a
584+
# non-pattern compatible family string was given. Therefore, we do not need
585+
# to warn for this case.
586+
return init_method(self, *args, **kwargs)
587+
588+
return wrapper
589+
590+
539591
class FontProperties:
540592
"""
541593
A class for storing and manipulating font properties.
@@ -585,9 +637,14 @@ class FontProperties:
585637
approach allows all text sizes to be made larger or smaller based
586638
on the font manager's default font size.
587639
588-
This class will also accept a fontconfig_ pattern_, if it is the only
589-
argument provided. This support does not depend on fontconfig; we are
590-
merely borrowing its pattern syntax for use here.
640+
This class accepts a single positional string as fontconfig_ pattern_,
641+
or alternatively individual properties as keyword arguments::
642+
643+
FontProperties(pattern)
644+
FontProperties(*, family=None, style=None, variant=None, ...)
645+
646+
This support does not depend on fontconfig; we are merely borrowing its
647+
pattern syntax for use here.
591648
592649
.. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/
593650
.. _pattern:
@@ -599,6 +656,7 @@ class FontProperties:
599656
fontconfig.
600657
"""
601658

659+
@_cleanup_fontproperties_init
602660
def __init__(self, family=None, style=None, variant=None, weight=None,
603661
stretch=None, size=None,
604662
fname=None, # if set, it's a hardcoded filename to use

lib/matplotlib/tests/test_font_manager.py

+40
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import numpy as np
1212
import pytest
1313

14+
import matplotlib as mpl
1415
from matplotlib.font_manager import (
1516
findfont, findSystemFonts, FontEntry, FontProperties, fontManager,
1617
json_dump, json_load, get_font, is_opentype_cff_font,
@@ -367,3 +368,42 @@ def inner():
367368
for obj in gc.get_objects():
368369
if isinstance(obj, SomeObject):
369370
pytest.fail("object from inner stack still alive")
371+
372+
373+
def test_fontproperties_init_deprecation():
374+
"""
375+
Test the deprecated API of FontProperties.__init__.
376+
377+
The deprecation does not change behavior, it only adds a deprecation warning
378+
via a decorator. Therefore, the purpose of this test is limited to check
379+
which calls do and do not issue deprecation warnings. Behavior is still
380+
tested via the existing regular tests.
381+
"""
382+
with pytest.warns(mpl.MatplotlibDeprecationWarning):
383+
# multiple positional arguments
384+
FontProperties("Times", "italic")
385+
386+
with pytest.warns(mpl.MatplotlibDeprecationWarning):
387+
# Mixed positional and keyword arguments
388+
FontProperties("Times", size=10)
389+
390+
with pytest.warns(mpl.MatplotlibDeprecationWarning):
391+
# passing a family list positionally
392+
FontProperties(["Times"])
393+
394+
# still accepted:
395+
FontProperties(family="Times", style="italic")
396+
FontProperties(family="Times")
397+
FontProperties("Times") # works as pattern and family
398+
FontProperties("serif-24:style=oblique:weight=bold") # pattern
399+
400+
# also still accepted:
401+
# passing as pattern via family kwarg was not covered by the docs but
402+
# historically worked. This is left unchanged for now.
403+
# AFAICT, we cannot detect this: We can determine whether a string
404+
# works as pattern, but that doesn't help, because there are strings
405+
# that are both pattern and family. We would need to identify, whether
406+
# a string is *not* a valid family.
407+
# Since this case is not covered by docs, I've refrained from jumping
408+
# extra hoops to detect this possible API misuse.
409+
FontProperties(family="serif-24:style=oblique:weight=bold")

lib/matplotlib/ticker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ def set_useMathText(self, val):
574574
from matplotlib import font_manager
575575
ufont = font_manager.findfont(
576576
font_manager.FontProperties(
577-
mpl.rcParams["font.family"]
577+
family=mpl.rcParams["font.family"]
578578
),
579579
fallback_to_default=False,
580580
)

0 commit comments

Comments
 (0)