8000 Simplify and tighten parse_fontconfig_pattern. by anntzer · Pull Request #24220 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Simplify and tighten parse_fontconfig_pattern. #24220

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 1 commit into from
Oct 25, 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
5 changes: 5 additions & 0 deletions doc/api/next_api_changes/deprecations/24220-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
``parse_fontconfig_pattern`` will no longer ignore unknown constant names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Previously, in a fontconfig pattern like ``DejaVu Sans:foo``, the unknown
``foo`` constant name would be silently ignored. This now raises a warning,
and will become an error in the future.
136 changes: 45 additions & 91 deletions lib/matplotlib/_fontconfig_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,29 @@
# there would have created cyclical dependency problems, because it also needs
# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files).

from functools import lru_cache
from functools import lru_cache, partial
import re

import numpy as np
from pyparsing import (
Literal, Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore,
)
Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore)

from matplotlib import _api


family_punc = r'\\\-:,'
family_unescape = re.compile(r'\\([%s])' % family_punc).sub
_family_unescape = partial(re.compile(r'\\(?=[%s])' % family_punc).sub, '')
family_escape = re.compile(r'([%s])' % family_punc).sub

value_punc = r'\\=_:,'
value_unescape = re.compile(r'\\([%s])' % value_punc).sub
_value_unescape = partial(re.compile(r'\\(?=[%s])' % value_punc).sub, '')
value_escape = re.compile(r'([%s])' % value_punc).sub

# Remove after module deprecation elapses (3.8); then remove underscores
# from _family_unescape and _value_unescape.
family_unescape = re.compile(r'\\([%s])' % family_punc).sub
value_unescape = re.compile(r'\\([%s])' % value_punc).sub


class FontconfigPatternParser:
"""
Expand Down Expand Up @@ -58,63 +65,27 @@ class FontconfigPatternParser:
'semicondensed': ('width', 'semi-condensed'),
'expanded': ('width', 'expanded'),
'extraexpanded': ('width', 'extra-expanded'),
'ultraexpanded': ('width', 'ultra-expanded')
}
'ultraexpanded': ('width', 'ultra-expanded'),
}

def __init__(self):

family = Regex(
r'([^%s]|(\\[%s]))*' % (family_punc, family_punc)
).setParseAction(self._family)

size = Regex(
r"([0-9]+\.?[0-9]*|\.[0-9]+)"
).setParseAction(self._size)

name = Regex(
r'[a-z]+'
).setParseAction(self._name)

value = Regex(
r'([^%s]|(\\[%s]))*' % (value_punc, value_punc)
).setParseAction(self._value)

families = (
family
+ ZeroOrMore(
Literal(',')
+ family)
).setParseAction(self._families)

point_sizes = (
size
+ ZeroOrMore(
Literal(',')
+ size)
).setParseAction(self._point_sizes)

property = (
(name
+ Suppress(Literal('='))
+ value
+ ZeroOrMore(
Suppress(Literal(','))
+ value))
| name
).setParseAction(self._property)

def comma_separated(elem):
return elem + ZeroOrMore(Suppress(",") + elem)

family = Regex(r"([^%s]|(\\[%s]))*" % (family_punc, family_punc))
size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)")
name = Regex(r"[a-z]+")
value = Regex(r"([^%s]|(\\[%s]))*" % (value_punc, value_punc))
prop = (
(name + Suppress("=") + comma_separated(value))
| name # replace by oneOf(self._constants) in mpl 3.9.
)
pattern = (
Optional(
families)
+ Optional(
Literal('-')
+ point_sizes)
+ ZeroOrMore(
Literal(':')
+ property)
Optional(comma_separated(family)("families"))
+ Optional("-" + comma_separated(size)("sizes"))
+ ZeroOrMore(":" + prop("properties*"))
+ StringEnd()
)

self._parser = pattern
self.ParseException = ParseException

Expand All @@ -124,47 +95,30 @@ def parse(self, pattern):
of key/value pairs useful for initializing a
`.font_manager.FontProperties` object.
"""
props = self._properties = {}
try:
self._parser.parseString(pattern)
parse = self._parser.parseString(pattern)
except ParseException as err:
# explain becomes a plain method on pyparsing 3 (err.explain(0)).
raise ValueError("\n" + ParseException.explain(err, 0)) from None
self._properties = None
self._parser.resetCache()
props = {}
if "families" in parse:
props["family"] = [*map(_family_unescape, parse["families"])]
if "sizes" in parse:
props["size"] = [*parse["sizes"]]
for prop in parse.get("properties", []):
if len(prop) == 1:
if prop[0] not in self._constants:
_api.warn_deprecated(
"3.7", message=f"Support for unknown constants "
f"({prop[0]!r}) is deprecated since %(since)s and "
f"will be removed %(removal)s.")
continue
prop = self._constants[prop[0]]
k, *v = prop
props.setdefault(k, []).extend(map(_value_unescape, v))
return props

def _family(self, s, loc, tokens):
return [family_unescape(r'\1', str(tokens[0]))]

def _size(self, s, loc, tokens):
return [float(tokens[0])]

def _name(self, s, loc, tokens):
return [str(tokens[0])]

def _value(self, s, loc, tokens):
return [value_unescape(r'\1', str(tokens[0]))]

def _families(self, s, loc, tokens):
self._properties['family'] = [str(x) for x in tokens]
return []

def _point_sizes(self, s, loc, tokens):
self._properties['size'] = [str(x) for x in tokens]
return []

def _property(self, s, loc, tokens):
if len(tokens) == 1:
if tokens[0] in self._constants:
key, val = self._constants[tokens[0]]
self._properties.setdefault(key, []).append(val)
else:
key = tokens[0]
val = tokens[1:]
self._properties.setdefault(key, []).extend(val)
return []


# `parse_fontconfig_pattern` is a bottleneck during the tests because it is
# repeatedly called when the rcParams are reset (to validate the default
Expand Down
9 changes: 8 additions & 1 deletion lib/matplotlib/tests/test_fontconfig_pattern.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from matplotlib.font_manager import FontProperties


Expand Down Expand Up @@ -60,11 +62,16 @@ def test_fontconfig_str():
assert getattr(font, k)() == getattr(right, k)(), test + k

test = "full "
s = ("serif:size=24:style=oblique:variant=small-caps:weight=bold"
s = ("serif-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


def test_fontconfig_unknown_constant():
with pytest.warns(DeprecationWarning):
FontProperties(":unknown")
0