Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c216c09

Browse files
committed
Simplify and tighten parse_fontconfig_pattern.
- Warn on unknown constant names, e.g. ``DejaVu Sans:unknown`` (as opposed e.g. to ``DejaVu Sans:bold``); they were previously silently ignored. - Rewrite the parser into something much simpler, moving nearly all the logic into a single block post-processing the result of parseString, instead of splitting everything into many small parse actions. In particular this makes the parser mostly stateless (except for the cache held by pyparsing itself), instead of having the various parts communicate through the _properties attribute.
1 parent 231d1c8 commit c216c09

File tree

3 files changed

+58
-92
lines changed

3 files changed

+58
-92
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``parse_fontconfig_pattern`` will no longer ignore unknown constant names
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Previously, in a fontconfig pattern like ``DejaVu Sans:foo``, the unknown
4+
``foo`` constant name would be silently ignored. This now raises a warning,
5+
and will become an error in the future.

lib/matplotlib/_fontconfig_pattern.py

Lines changed: 45 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,29 @@
99
# there would have created cyclical dependency problems, because it also needs
1010
# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files).
1111

12-
from functools import lru_cache
12+
from functools import lru_cache, partial
1313
import re
1414

1515
import numpy as np
1616
from pyparsing import (
17-
Literal, Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore,
18-
)
17+
Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore)
18+
19+
from matplotlib import _api
20+
1921

2022
family_punc = r'\\\-:,'
21-
family_unescape = re.compile(r'\\([%s])' % family_punc).sub
23+
_family_unescape = partial(re.compile(r'\\(?=[%s])' % family_punc).sub, '')
2224
family_escape = re.compile(r'([%s])' % family_punc).sub
2325

2426
value_punc = r'\\=_:,'
25-
value_unescape = re.compile(r'\\([%s])' % value_punc).sub
27+
_value_unescape = partial(re.compile(r'\\(?=[%s])' % value_punc).sub, '')
2628
value_escape = re.compile(r'([%s])' % value_punc).sub
2729

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

2936
class FontconfigPatternParser:
3037
"""
@@ -58,63 +65,27 @@ class FontconfigPatternParser:
5865
'semicondensed': ('width', 'semi-condensed'),
5966
'expanded': ('width', 'expanded'),
6067
'extraexpanded': ('width', 'extra-expanded'),
61-
'ultraexpanded': ('width', 'ultra-expanded')
62-
}
68+
'ultraexpanded': ('width', 'ultra-expanded'),
69+
}
6370

6471
def __init__(self):
65-
66-
family = Regex(
67-
r'([^%s]|(\\[%s]))*' % (family_punc, family_punc)
68-
).setParseAction(self._family)
69-
70-
size = Regex(
71-
r"([0-9]+\.?[0-9]*|\.[0-9]+)"
72-
).setParseAction(self._size)
73-
74-
name = Regex(
75-
r'[a-z]+'
76-
).setParseAction(self._name)
77-
78-
value = Regex(
79-
r'([^%s]|(\\[%s]))*' % (value_punc, value_punc)
80-
).setParseAction(self._value)
81-
82-
families = (
83-
family
84-
+ ZeroOrMore(
85-
Literal(',')
86-
+ family)
87-
).setParseAction(self._families)
88-
89-
point_sizes = (
90-
size
91-
+ ZeroOrMore(
92-
Literal(',')
93-
+ size)
94-
).setParseAction(self._point_sizes)
95-
96-
property = (
97-
(name
98-
+ Suppress(Literal('='))
99-
+ value
100-
+ ZeroOrMore(
101-
Suppress(Literal(','))
102-
+ value))
103-
| name
104-
).setParseAction(self._property)
105-
72+
def comma_separated(elem):
73+
return elem + ZeroOrMore(Suppress(",") + elem)
74+
75+
family = Regex(r"([^%s]|(\\[%s]))*" % (family_punc, family_punc))
76+
size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)")
77+
name = Regex(r"[a-z]+")
78+
value = Regex(r"([^%s]|(\\[%s]))*" % (value_punc, value_punc))
79+
prop = (
80+
(name + Suppress("=") + comma_separated(value))
81+
| name # replace by oneOf(self._constants) in mpl 3.9.
82+
)
10683
pattern = (
107-
Optional(
108-
families)
109-
+ Optional(
110-
Literal('-')
111-
+ point_sizes)
112-
+ ZeroOrMore(
113-
Literal(':')
114-
+ property)
84+
Optional(comma_separated(family)("families"))
85+
+ Optional("-" + comma_separated(size)("sizes"))
86+
+ ZeroOrMore(":" + prop("properties*"))
11587
+ StringEnd()
11688
)
117-
11889
self._parser = pattern
11990
self.ParseException = ParseException
12091

@@ -124,47 +95,30 @@ def parse(self, pattern):
12495
of key/value pairs useful for initializing a
12596
`.font_manager.FontProperties` object.
12697
"""
127-
props = self._properties = {}
12898
try:
129-
self._parser.parseString(pattern)
99+
parse = self._parser.parseString(pattern)
130100
except ParseException as err:
131101
# explain becomes a plain method on pyparsing 3 (err.explain(0)).
132102
raise ValueError("\n" + ParseException.explain(err, 0)) from None
133-
self._properties = None
134103
self._parser.resetCache()
104+
props = {}
105+
if "families" in parse:
106+
props["family"] = [*map(_family_unescape, parse["families"])]
107+
if "sizes" in parse:
108+
props["size"] = [*parse["sizes"]]
109+
for prop in parse.get("properties", []):
110+
if len(prop) == 1:
111+
if prop[0] not in self._constants:
112+
_api.warn_deprecated(
113+
"3.7", message=f"Support for unknown constants "
114+
f"({prop[0]!r}) is deprecated since %(since)s and "
115+
f"will be removed %(removal)s.")
116+
continue
117+
prop = self._constants[prop[0]]
118+
k, *v = prop
119+
props.setdefault(k, []).extend(map(_value_unescape, v))
135120
return props
136121

137-
def _family(self, s, loc, tokens):
138-
return [family_unescape(r'\1', str(tokens[0]))]
139-
140-
def _size(self, s, loc, tokens):
141-
return [float(tokens[0])]
142-
143-
def _name(self, s, loc, tokens):
144-
return [str(tokens[0])]
145-
146-
def _value(self, s, loc, tokens):
147-
return [value_unescape(r'\1', str(tokens[0]))]
148-
149-
def _families(self, s, loc, tokens):
150-
self._properties['family'] = [str(x) for x in tokens]
151-
return []
152-
153-
def _point_sizes(self, s, loc, tokens):
154-
self._properties['size'] = [str(x) for x in tokens]
155-
return []
156-
157-
def _property(self, s, loc, tokens):
158-
if len(tokens) == 1:
159-
if tokens[0] in self._constants:
160-
key, val = self._constants[tokens[0]]
161-
self._properties.setdefault(key, []).append(val)
162-
else:
163-
key = tokens[0]
164-
val = tokens[1:]
165-
self._properties.setdefault(key, []).extend(val)
166-
return []
167-
168122

169123
# `parse_fontconfig_pattern` is a bottleneck during the tests because it is
170124
# repeatedly called when the rcParams are reset (to validate the default

lib/matplotlib/tests/test_fontconfig_pattern.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
from matplotlib.font_manager import FontProperties
24

35

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

6264
test = "full "
63-
s = ("serif:size=24:style=oblique:variant=small-caps:weight=bold"
65+
s = ("serif-24:style=oblique:variant=small-caps:weight=bold"
6466
":stretch=expanded")
6567
font = FontProperties(s)
6668
right = FontProperties(family="serif", size=24, weight="bold",
6769
style="oblique", variant="small-caps",
6870
stretch="expanded")
6971
for k in keys:
7072
assert getattr(font, k)() == getattr(right, k)(), test + k
73+
74+
75+
def test_fontconfig_unknown_constant():
76+
with pytest.warns(DeprecationWarning):
77+
FontProperties(":unknown")

0 commit comments

Comments
 (0)