Skip to content

Fix Path/str-discrepancy in FontManager.addpath and improve documentation #22591

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api/font_manager_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@
:undoc-members:
:show-inheritance:

.. data:: fontManager

The global instance of `FontManager`.

.. autoclass:: FontEntry
:no-undoc-members:
81 changes: 53 additions & 28 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
A module for finding, managing, and using fonts across platforms.

This module provides a single `FontManager` instance that can
This module provides a single `FontManager` instance, ``fontManager``, that can
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not manage to link to fontManager when the definition is in font_manager_api.rst.

be shared across backends and platforms. The `findfont`
function returns the best TrueType (TTF) font file in the local or
system font path that matches the specified `FontProperties`
Expand Down Expand Up @@ -627,32 +627,33 @@ class FontProperties:

- family: A list of font names in decreasing order of priority.
The items may include a generic font family name, either
'sans-serif' (default), 'serif', 'cursive', 'fantasy', or 'monospace'.
'sans-serif', 'serif', 'cursive', 'fantasy', or 'monospace'.
In that case, the actual font to be used will be looked up
from the associated rcParam.
from the associated rcParam. Default: :rc:`font.family`

- style: Either 'normal' (default), 'italic' or 'oblique'.
- style: Either 'normal', 'italic' or 'oblique'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Highlight the properties using italics (or alternatively literal format)

Suggested change
- style: Either 'normal', 'italic' or 'oblique'.
- *style*: Either 'normal', 'italic' or 'oblique'.

This is also quite redundant with the setters. This is almost completely a data class. IMHO it would be reasonable to add properties and move all the documentation there, i.e. delete this list and the setter docstrings (setters may link to the property docs. But that's for a later time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would make sense, but I think it is better to try to get the information from the setmethods at a later stage.

Default: :rc:`font.style`

- variant: Either 'normal' (default) or 'small-caps'.
- variant: Either 'normal' or 'small-caps'.
Default: :rc:`font.variant`

- stretch: A numeric value in the range 0-1000 or one of
'ultra-condensed', 'extra-condensed', 'condensed',
'semi-condensed', 'normal' (default), 'semi-expanded', 'expanded',
'extra-expanded' or 'ultra-expanded'.
'semi-condensed', 'normal', 'semi-expanded', 'expanded',
'extra-expanded' or 'ultra-expanded'. Default: :rc:`font.stretch`

- weight: A numeric value in the range 0-1000 or one of
'ultralight', 'light', 'normal' (default), 'regular', 'book', 'medium',
'ultralight', 'light', 'normal', 'regular', 'book', 'medium',
'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy',
'extra bold', 'black'.
'extra bold', 'black'. Default: :rc:`font.weight`

- size: Either an relative value of 'xx-small', 'x-small',
'small', 'medium', 'large', 'x-large', 'xx-large' or an
absolute font size, e.g., 10 (default).
absolute font size, e.g., 10. Default: :rc:`font.size`

- math_fontfamily: The family of fonts used to render math text; overrides
:rc:`mathtext.fontset`. Supported values are the same as the ones
supported by :rc:`mathtext.fontset`: 'dejavusans', 'dejavuserif', 'cm',
'stix', 'stixsans' and 'custom'.
- math_fontfamily: The family of fonts used to render math text.
Supported values are: 'dejavusans', 'dejavuserif', 'cm',
'stix', 'stixsans' and 'custom'. Default: :rc:`mathtext.fontset`

Alternatively, a font may be specified using the absolute path to a font
file, by using the *fname* kwarg. However, in this case, it is typically
Expand Down Expand Up @@ -807,7 +808,7 @@ def set_family(self, family):
is CSS parlance), such as: 'serif', 'sans-serif', 'cursive',
'fantasy', or 'monospace', a real font name or a list of real
font names. Real font names are not supported when
:rc:`text.usetex` is `True`.
:rc:`text.usetex` is `True`. Default: :rc:`font.family`
"""
if family is None:
family = rcParams['font.family']
Expand All @@ -817,7 +818,11 @@ def set_family(self, family):

def set_style(self, style):
"""
Set the font style. Values are: 'normal', 'italic' or 'oblique'.
Set the font style.

Parameters
----------
style : {'normal', 'italic', 'oblique'}, default: :rc:`font.style`
"""
if style is None:
style = rcParams['font.style']
Expand All @@ -826,7 +831,11 @@ def set_style(self, style):

def set_variant(self, variant):
"""
Set the font variant. Values are: 'normal' or 'small-caps'.
Set the font variant.

Parameters
----------
variant : {'normal', 'small-caps'}, default: :rc:`font.variant`
"""
if variant is None:
variant = rcParams['font.variant']
Expand All @@ -835,10 +844,14 @@ def set_variant(self, variant):

def set_weight(self, weight):
"""
Set the font weight. May be either a numeric value in the
range 0-1000 or one of 'ultralight', 'light', 'normal',
'regular', 'book', 'medium', 'roman', 'semibold', 'demibold',
'demi', 'bold', 'heavy', 'extra bold', 'black'
Set the font weight.

Parameters
----------
weight : int or {'ultralight', 'light', 'normal', 'regular', 'book', \
'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', \
'extra bold', 'black'}, default: :rc:`font.weight`
If int, must be in the range 0-1000.
"""
if weight is None:
weight = rcParams['font.weight']
Expand All @@ -853,10 +866,14 @@ def set_weight(self, weight):

def set_stretch(self, stretch):
"""
Set the font stretch or width. Options are: 'ultra-condensed',
'extra-condensed', 'condensed', 'semi-condensed', 'normal',
'semi-expanded', 'expanded', 'extra-expanded' or
'ultra-expanded', or a numeric value in the range 0-1000.
Set the font stretch or width.

Parameters
----------
stretch : int or {'ultra-condensed', 'extra-condensed', 'condensed', \
'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', \
'ultra-expanded'}, default: :rc:`font.stretch`
If int, must be in the range 0-1000.
"""
if stretch is None:
stretch = rcParams['font.stretch']
Expand All @@ -871,9 +888,14 @@ def set_stretch(self, stretch):

def set_size(self, size):
"""
Set the font size. Either an relative value of 'xx-small',
'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'
or an absolute font size, e.g., 12.
Set the font size.

Parameters
----------
size : float or {'xx-small', 'x-small', 'small', 'medium', \
'large', 'x-large', 'xx-large'}, default: :rc:`font.size`
If float, the font size in points. The string values denote sizes
relative to the default font size.
"""
if size is None:
size = rcParams['font.size']
Expand Down Expand Up @@ -1091,6 +1113,9 @@ def addfont(self, path):
----------
path : str or path-like
"""
# Convert to string in case of a path as
# afmFontProperty and FT2Font expect this
path = os.fsdecode(path)
if Path(path).suffix.lower() == ".afm":
with open(path, "rb") as fh:
font = _afm.AFM(fh)
Expand Down
16 changes: 15 additions & 1 deletion lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,20 @@ def validate_fontweight(s):
raise ValueError(f'{s} is not a valid font weight.') from e


def validate_fontstretch(s):
stretchvalues = [
'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',
'normal', 'semi-expanded', 'expanded', 'extra-expanded',
'ultra-expanded']
# Note: Historically, stretchvalues have been case-sensitive in Matplotlib
if s in stretchvalues:
return s
try:
return int(s)
except (ValueError, TypeError) as e:
raise ValueError(f'{s} is not a valid font stretch.') from e


def validate_font_properties(s):
parse_fontconfig_pattern(s)
return s
Expand Down Expand Up @@ -900,7 +914,7 @@ def _convert_validator_spec(key, conv):
"font.family": validate_stringlist, # used by text object
"font.style": validate_string,
"font.variant": validate_string,
"font.stretch": validate_string,
"font.stretch": validate_fontstretch,
"font.weight": validate_fontweight,
"font.size": validate_float, # Base font size in points
"font.serif": validate_stringlist,
Expand Down
7 changes: 7 additions & 0 deletions lib/matplotlib/tests/test_font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ def test_user_fonts_linux(tmpdir, monkeypatch):
_get_fontconfig_fonts.cache_clear()


def test_addfont_as_path():
"""Smoke test that addfont() accepts pathlib.Path."""
font_test_file = 'mpltest.ttf'
path = Path(__file__).parent / font_test_file
fontManager.addfont(path)


@pytest.mark.skipif(sys.platform != 'win32', reason='Windows only')
def test_user_fonts_win32():
if not (os.environ.get('APPVEYOR') or os.environ.get('TF_BUILD')):
Expand Down
21 changes: 21 additions & 0 deletions lib/matplotlib/tests/test_rcparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
_validate_color_or_linecolor,
validate_cycler,
validate_float,
validate_fontstretch,
validate_fontweight,
validate_hatch,
validate_hist_bins,
Expand Down Expand Up @@ -469,6 +470,26 @@ def test_validate_fontweight(weight, parsed_weight):
assert validate_fontweight(weight) == parsed_weight


@pytest.mark.parametrize('stretch, parsed_stretch', [
('expanded', 'expanded'),
('EXPANDED', ValueError), # stretch is case-sensitive
(100, 100),
('100', 100),
(np.array(100), 100),
# fractional fontweights are not defined. This should actually raise a
# ValueError, but historically did not.
(20.6, 20),
('20.6', ValueError),
([100], ValueError),
])
def test_validate_fontstretch(stretch, parsed_stretch):
if parsed_stretch is ValueError:
with pytest.raises(ValueError):
validate_fontstretch(stretch)
else:
assert validate_fontstretch(stretch) == parsed_stretch


def test_keymaps():
key_list = [k for k in mpl.rcParams if 'keymap' in k]
for k in key_list:
Expand Down