From 3656bf9b40509db3cdbb107aaf0ff2ab454a5835 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Thu, 3 Mar 2022 18:47:22 +0100 Subject: [PATCH 1/4] Convert path to str in FontManager.addpath --- lib/matplotlib/font_manager.py | 2 ++ lib/matplotlib/tests/test_font_manager.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index e17d847ea87c..05ef4e6dc3ec 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1091,6 +1091,8 @@ def addfont(self, path): ---------- path : str or path-like """ + path = str(path) # Convert to string in case of a path as + # afmFontProperty and FT2Font expect this if Path(path).suffix.lower() == ".afm": with open(path, "rb") as fh: font = _afm.AFM(fh) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index fb1119e33489..a20cbd85e197 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -176,6 +176,13 @@ def test_user_fonts_linux(tmpdir, monkeypatch): _get_fontconfig_fonts.cache_clear() +def test_addfont(): + font_test_file = 'mpltest.ttf' + path = Path(__file__).parent / font_test_file + # Add font using Path, which should not produce an error. See #22582 + 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')): From 25d2d559e3a8d2141b98b7532758d615d47a9194 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Thu, 3 Mar 2022 19:10:59 +0100 Subject: [PATCH 2/4] Improve documentation of FontManager --- lib/matplotlib/font_manager.py | 81 ++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 05ef4e6dc3ec..d8da217a0e36 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -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'. + 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 @@ -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'] @@ -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'] @@ -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'] @@ -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'] @@ -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'] @@ -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'] @@ -1091,8 +1113,9 @@ def addfont(self, path): ---------- path : str or path-like """ - path = str(path) # Convert to string in case of a path as - # afmFontProperty and FT2Font expect this + # Convert to string in case of a path as + # afmFontProperty and FT2Font expect this + path = str(path) if Path(path).suffix.lower() == ".afm": with open(path, "rb") as fh: font = _afm.AFM(fh) From 337765628104959ca509e8451df3e5588e4565f0 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Thu, 3 Mar 2022 21:00:05 +0100 Subject: [PATCH 3/4] Add documention for fontManager. Closes #22586 --- lib/matplotlib/font_manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index d8da217a0e36..c620f292af1f 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -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 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` @@ -11,6 +11,10 @@ The design is based on the `W3C Cascading Style Sheet, Level 1 (CSS1) font specification `_. Future versions may implement the Level 2 or 2.1 specifications. + +.. data:: fontManager + + The singleton instance of `FontManager`. """ # KNOWN ISSUES From 7f21bcab3265daa5b72fbc7f11fa81e327cd9c32 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Fri, 4 Mar 2022 20:07:57 +0100 Subject: [PATCH 4/4] Add validation for fontstretch --- doc/api/font_manager_api.rst | 4 ++++ lib/matplotlib/font_manager.py | 8 ++------ lib/matplotlib/rcsetup.py | 16 +++++++++++++++- lib/matplotlib/tests/test_font_manager.py | 4 ++-- lib/matplotlib/tests/test_rcparams.py | 21 +++++++++++++++++++++ 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/doc/api/font_manager_api.rst b/doc/api/font_manager_api.rst index 8b698bacf0fe..3e043112380b 100644 --- a/doc/api/font_manager_api.rst +++ b/doc/api/font_manager_api.rst @@ -7,5 +7,9 @@ :undoc-members: :show-inheritance: + .. data:: fontManager + + The global instance of `FontManager`. + .. autoclass:: FontEntry :no-undoc-members: diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index c620f292af1f..d582bc936902 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1,7 +1,7 @@ """ A module for finding, managing, and using fonts across platforms. -This module provides a single `FontManager` instance, `fontManager`, that can +This module provides a single `FontManager` instance, ``fontManager``, that can 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` @@ -11,10 +11,6 @@ The design is based on the `W3C Cascading Style Sheet, Level 1 (CSS1) font specification `_. Future versions may implement the Level 2 or 2.1 specifications. - -.. data:: fontManager - - The singleton instance of `FontManager`. """ # KNOWN ISSUES @@ -1119,7 +1115,7 @@ def addfont(self, path): """ # Convert to string in case of a path as # afmFontProperty and FT2Font expect this - path = str(path) + path = os.fsdecode(path) if Path(path).suffix.lower() == ".afm": with open(path, "rb") as fh: font = _afm.AFM(fh) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 0eafec792b04..37015bc0d76a 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -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 @@ -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, diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index a20cbd85e197..254b9fdff38b 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -176,10 +176,10 @@ def test_user_fonts_linux(tmpdir, monkeypatch): _get_fontconfig_fonts.cache_clear() -def test_addfont(): +def test_addfont_as_path(): + """Smoke test that addfont() accepts pathlib.Path.""" font_test_file = 'mpltest.ttf' path = Path(__file__).parent / font_test_file - # Add font using Path, which should not produce an error. See #22582 fontManager.addfont(path) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 75b6f727f799..6f0edf3ae1f3 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -20,6 +20,7 @@ _validate_color_or_linecolor, validate_cycler, validate_float, + validate_fontstretch, validate_fontweight, validate_hatch, validate_hist_bins, @@ -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: