8000 Simplify and tighten parse_fontconfig_pattern. · matplotlib/matplotlib@f11731a · GitHub
[go: up one dir, main page]

Skip to content

Commit f11731a

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 36685eb commit f11731a

File tree

3 files changed

+87
-118
lines changed

3 files changed

+87
-118
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
4+
unknown ``foo`` constant name would be silently ignored. This is now raises
5+
a warning, and will become an error in the future.

lib/matplotlib/_fontconfig_pattern.py

Lines changed: 75 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +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
import numpy as np
15-
from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd,
16-
ParseException, Suppress)
15+
from pyparsing import (
16+
Optional, Group, ParseException, Regex, StringEnd, Suppress, ZeroOrMore)
17+
18+
from matplotlib import _api
19+
1720

1821
family_punc = r'\\\-:,'
19-
family_unescape = re.compile(r'\\([%s])' % family_punc).sub
22+
_family_unescape = partial(re.compile(r'\\([%s])' % family_punc).sub, r'\1')
2023
family_escape = re.compile(r'([%s])' % family_punc).sub
2124

2225
value_punc = r'\\=_:,'
23-
value_unescape = re.compile(r'\\([%s])' % value_punc).sub
26+
_value_unescape = partial(re.compile(r'\\([%s])' % value_punc).sub, r'\1')
2427
value_escape = re.compile(r'([%s])' % value_punc).sub
2528

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

2736
class FontconfigPatternParser:
2837
"""
@@ -33,86 +42,53 @@ class FontconfigPatternParser:
3342
"""
3443

3544
_constants = {
36-
'thin': ('weight', 'light'),
37-
'extralight': ('weight', 'light'),
38-
'ultralight': ('weight', 'light'),
39-
'light': ('weight', 'light'),
40-
'book': ('weight', 'book'),
41-
'regular': ('weight', 'regular'),
42-
'normal': ('weight', 'normal'),
43-
'medium': ('weight', 'medium'),
44-
'demibold': ('weight', 'demibold'),
45-
'semibold': ('weight', 'semibold'),
46-
'bold': ('weight', 'bold'),
47-
'extrabold': ('weight', 'extra bold'),
48-
'black': ('weight', 'black'),
49-
'heavy': ('weight', 'heavy'),
50-
'roman': ('slant', 'normal'),
51-
'italic': ('slant', 'italic'),
52-
'oblique': ('slant', 'oblique'),
53-
'ultracondensed': ('width', 'ultra-condensed'),
54-
'extracondensed': ('width', 'extra-condensed'),
55-
'condensed': ('width', 'condensed'),
56-
'semicondensed': ('width', 'semi-condensed'),
57-
'expanded': ('width', 'expanded'),
58-
'extraexpanded': ('width', 'extra-expanded'),
59-
'ultraexpanded': ('width', 'ultra-expanded')
60-
}
45+
'thin': ('weight', ['light']),
46+
'extralight': ('weight', ['light']),
47+
'ultralight': ('weight', ['light']),
48+
'light': ('weight', ['light']),
49+
'book': ('weight', ['book']),
50+
'regular': ('weight', ['regular']),
51+
'normal': ('weight', ['normal']),
52+
'medium': ('weight', ['medium']),
53+
'demibold': ('weight', ['demibold']),
54+
'semibold': ('weight', ['semibold']),
55+
'bold': ('weight', ['bold']),
56+
'extrabold': ('weight', ['extra bold']),
57+
'black': ('weight', ['black']),
58+
'heavy': ('weight', ['heavy']),
59+
'roman': ('slant', ['normal']),
60+
'italic': ('slant', ['italic']),
61+
'oblique': ('slant', ['oblique']),
62+
'ultracondensed': ('width', ['ultra-condensed']),
63+
'extracondensed': ('width', ['extra-condensed']),
64+
'condensed': ('width', ['condensed']),
65+
'semicondensed': ('width', ['semi-condensed']),
66+
'expanded': ('width', ['expanded']),
67+
'extraexpanded': ('width', ['extra-expanded']),
68+
'ultraexpanded': ('width', ['ultra-expanded']),
69+
}
6170

6271
def __init__(self):
63-
64-
family = Regex(
65-
r'([^%s]|(\\[%s]))*' % (family_punc, family_punc)
66-
).setParseAction(self._family)
67-
68-
size = Regex(
69-
r"([0-9]+\.?[0-9]*|\.[0-9]+)"
70-
).setParseAction(self._size)
71-
72-
name = Regex(
73-
r'[a-z]+'
74-
).setParseAction(self._name)
75-
76-
value = Regex(
77-
r'([^%s]|(\\[%s]))*' % (value_punc, value_punc)
78-
).setParseAction(self._value)
79-
80-
families = (
81-
family
82-
+ ZeroOrMore(
83-
Literal(',')
84-
+ family)
85-
).setParseAction(self._families)
86-
87-
point_sizes = (
88-
size
89-
+ ZeroOrMore(
90-
Literal(',')
91-
+ size)
92-
).setParseAction(self._point_sizes)
93-
94-
property = (
95-
(name
96-
+ Suppress(Literal('='))
97-
+ value
98-
+ ZeroOrMore(
99-
Suppress(Literal(','))
100-
+ value))
101-
| name
102-
).setParseAction(self._property)
103-
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+
Suppress(":")
81+
+ (
82+
Group(name + Suppress("=") + comma_separated(value))
83+
| name # replace by oneOf(self._constants) in mpl 3.9.
84+
)
85+
)
10486
pattern = (
105-
Optional(
106-
families)
107-
+ Optional(
108-
Literal('-')
109-
+ point_sizes)
110-
+ ZeroOrMore(
111-
Literal(':')
112-
+ property)
87+
Optional(comma_separated(family)("families"))
88+
+ Optional("-" + comma_separated(size)("sizes"))
89+
+ ZeroOrMore(prop)("properties")
11390
+ StringEnd()
11491
)
115-
11692
self._parser = pattern
11793
self.ParseException = ParseException
11894

@@ -122,50 +98,31 @@ def parse(self, pattern):
12298
of key/value pairs useful for initializing a
12399
`.font_manager.FontProperties` object.
124100
"""
125-
props = self._properties = {}
126101
try:
127-
self._parser.parseString(pattern)
128-
except self.ParseException as e:
102+
parse = self._parser.parseString(pattern)
103+
except ParseException as e:
129104
raise ValueError(
130105
"Could not parse font string: '%s'\n%s" % (pattern, e)) from e
131-
132-
self._properties = None
133-
134106
self._parser.resetCache()
135-
107+
props = {}
108+
if "families" in parse:
109+
props["family"] = [*map(_family_unescape, parse['families'])]
110+
if "size" in parse:
111+
props["size"] = parse["sizes"]
112+
for prop in parse.get("properties", []):
113+
if isinstance(prop, str):
114+
if prop not in self._constants:
115+
_api.warn_deprecated(
116+
"3.7", message=f"Support for unknown constants "
117+
f"({prop!r}) is deprecated since %(since)s and will "
118+
f"be removed %(removal)s.")
119+
continue
120+
k, v = self._constants[prop]
121+
else:
122+
k, *v = prop
123+
props.setdefault(k, []).extend(map(_value_unescape, v))
136124
return props
137125

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

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

lib/matplotlib/tests/test_fontconfig_pattern.py

Lines changed: 7 additions & 0 deletions
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

@@ -68,3 +70,8 @@ def test_fontconfig_str():
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