8000 Merge pull request #24220 from anntzer/pfp · matplotlib/matplotlib@0c3ba6d · GitHub
[go: up one dir, main page]

Skip to content

Commit 0c3ba6d

Browse files
authored
Merge pull request #24220 from anntzer/pfp
Simplify and tighten parse_fontconfig_pattern.
2 parents aefe56f + c216c09 commit 0c3ba6d

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)
0