From bb14c7fb723991be096c748ae3e096c4626cc8b5 Mon Sep 17 00:00:00 2001 From: Ted Drain Date: Mon, 4 Nov 2019 08:28:57 -0800 Subject: [PATCH 1/4] Fixed FontProperties to/from strings --- lib/matplotlib/fontconfig_pattern.py | 29 +++++++--- lib/matplotlib/tests/test_fontconfig.py | 70 +++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 lib/matplotlib/tests/test_fontconfig.py diff --git a/lib/matplotlib/fontconfig_pattern.py b/lib/matplotlib/fontconfig_pattern.py index d15b937b249a..5f8020e57e3a 100644 --- a/lib/matplotlib/fontconfig_pattern.py +++ b/lib/matplotlib/fontconfig_pattern.py @@ -177,19 +177,34 @@ def _property(self, s, loc, tokens): parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse) +def _escape_val(val, escape_func): + if type(val) == list: + val = [escape_func(r'\\\1', str(x)) for x in val + if x is not None] + if val != []: + val = ','.join(val) + else: + val = escape_func(r'\\\1', str(val)) + + return val + + def generate_fontconfig_pattern(d): """ Given a dictionary of key/value pairs, generates a fontconfig pattern string. """ props = [] - for key in 'family style variant weight stretch file size'.split(): + + # Family is added first w/o a keyword + family = d.get_family() + if family is not None and family != []: + props.append("%s" % (_escape_val(family, family_escape))) + + # The other keys are added as key=value + for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']: val = getattr(d, 'get_' + key)() if val is not None and val != []: - if type(val) == list: - val = [value_escape(r'\\\1', str(x)) for x in val - if x is not None] - if val != []: - val = ','.join(val) - props.append(":%s=%s" % (key, val)) + props.append(":%s=%s" % (key, _escape_val(val, value_escape))) + return ''.join(props) diff --git a/lib/matplotlib/tests/test_fontconfig.py b/lib/matplotlib/tests/test_fontconfig.py new file mode 100644 index 000000000000..521a3e882d70 --- /dev/null +++ b/lib/matplotlib/tests/test_fontconfig.py @@ -0,0 +1,70 @@ +from matplotlib.font_manager import FontProperties + + +# Attributes on FontProperties object to check for consistency +keys = [ + "get_family", + "get_style", + "get_variant", + "get_weight", + "get_size", + ] + + +def test_fontconfig_pattern(): + "Test converting a FontProperties to string then back." + + # Defaults + test = "defaults " + f1 = FontProperties() + s = str(f1) + + f2 = FontProperties(s) + for k in keys: + assert getattr(f1, k)() == getattr(f2, k)(), test + k + + # Basic inputs + test = "basic " + f1 = FontProperties(family="serif", size=20, style="italic") + s = str(f1) + + f2 = FontProperties(s) + for k in keys: + assert getattr(f1, k)() == getattr(f2, k)(), test + k + + # Full set of inputs. + test = "full " + f1 = FontProperties(family="sans-serif", size=24, weight="bold", + style="oblique", variant="small-caps", + stretch="expanded") + s = str(f1) + + f2 = FontProperties(s) + for k in keys: + assert getattr(f1, k)() == getattr(f2, k)(), test + k + + +def test_fontconfig_str(): + "Test FontProperties string conversions for correctness" + + # Known good strings taken from actual font config specs on a linux box + # and modified for MPL defaults. + + # Default values found by inspection. + test = "defaults " + s = ("sans\\-serif:style=normal:variant=normal:weight=normal" + ":stretch=normal:size=12.0") + font = FontProperties(s) + right = FontProperties() + for k in keys: + assert getattr(font, k)() == getattr(right, k)(), test + k + + test = "full " + s = ("serif:size=24:style=oblique:variant=small-caps:weight=bold" + ":stretch=expanded") + font = FontProperties(s) + right = FontProperties(family="serif", size=24, weight="bold", + style="oblique", variant="small-caps", + stretch="expanded") + for k in keys: + assert getattr(font, k)() == getattr(right, k)(), test + k From 27e2eb11c6515af9853c710387f75d4a4e5c6551 Mon Sep 17 00:00:00 2001 From: Ted Drain Date: Tue, 5 Nov 2019 13:34:04 -0800 Subject: [PATCH 2/4] Renamed per MPL review --- .../tests/{test_fontconfig.py => test_fontconfig_pattern.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/matplotlib/tests/{test_fontconfig.py => test_fontconfig_pattern.py} (100%) diff --git a/lib/matplotlib/tests/test_fontconfig.py b/lib/matplotlib/tests/test_fontconfig_pattern.py similarity index 100% rename from lib/matplotlib/tests/test_fontconfig.py rename to lib/matplotlib/tests/test_fontconfig_pattern.py From 6e02a3b5d58dc74b350e24a709ab9fb11ef14937 Mon Sep 17 00:00:00 2001 From: Ted Drain Date: Fri, 29 Nov 2019 09:18:31 -0800 Subject: [PATCH 3/4] Update from MPL code review --- lib/matplotlib/fontconfig_pattern.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/fontconfig_pattern.py b/lib/matplotlib/fontconfig_pattern.py index 5f8020e57e3a..517aaf5b22f9 100644 --- a/lib/matplotlib/fontconfig_pattern.py +++ b/lib/matplotlib/fontconfig_pattern.py @@ -178,11 +178,15 @@ def _property(self, s, loc, tokens): def _escape_val(val, escape_func): - if type(val) == list: + """ + Given a string value or a list of string values, run each value through + the input escape function to make the values into legal font config + strings. The result is returned as a string. + """ + if isinstance(val, list): val = [escape_func(r'\\\1', str(x)) for x in val if x is not None] - if val != []: - val = ','.join(val) + val = ','.join(val) else: val = escape_func(r'\\\1', str(val)) @@ -199,7 +203,7 @@ def generate_fontconfig_pattern(d): # Family is added first w/o a keyword family = d.get_family() if family is not None and family != []: - props.append("%s" % (_escape_val(family, family_escape))) + props.append(_escape_val(family, family_escape)) # The other keys are added as key=value for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']: From cf37d541758a360d988701b69175cd72cc351ea4 Mon Sep 17 00:00:00 2001 From: Ted Drain Date: Mon, 2 Dec 2019 13:41:01 -0800 Subject: [PATCH 4/4] Simplified escape function to a single path --- lib/matplotlib/fontconfig_pattern.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/fontconfig_pattern.py b/lib/matplotlib/fontconfig_pattern.py index 517aaf5b22f9..1f4df32c0fcd 100644 --- a/lib/matplotlib/fontconfig_pattern.py +++ b/lib/matplotlib/fontconfig_pattern.py @@ -14,7 +14,7 @@ from functools import lru_cache import re - +import numpy as np from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd, ParseException, Suppress) @@ -183,14 +183,11 @@ def _escape_val(val, escape_func): the input escape function to make the values into legal font config strings. The result is returned as a string. """ - if isinstance(val, list): - val = [escape_func(r'\\\1', str(x)) for x in val - if x is not None] - val = ','.join(val) - else: - val = escape_func(r'\\\1', str(val)) + if not np.iterable(val) or isinstance(val, str): + val = [val] - return val + return ','.join(escape_func(r'\\\1', str(x)) for x in val + if x is not None) def generate_fontconfig_pattern(d): @@ -208,6 +205,7 @@ def generate_fontconfig_pattern(d): # The other keys are added as key=value for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']: val = getattr(d, 'get_' + key)() + # Don't use 'if not val' because 0 is a valid input. if val is not None and val != []: props.append(":%s=%s" % (key, _escape_val(val, value_escape)))