Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/_fontconfig_pattern.py: 88%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

42 statements  

1""" 

2A module for parsing and generating `fontconfig patterns`_. 

3 

4.. _fontconfig patterns: 

5 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html 

6""" 

7 

8# This class logically belongs in `matplotlib.font_manager`, but placing it 

9# there would have created cyclical dependency problems, because it also needs 

10# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files). 

11 

12from functools import lru_cache, partial 

13import re 

14 

15from pyparsing import ( 

16 Group, Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore, oneOf) 

17 

18 

19_family_punc = r'\\\-:,' 

20_family_unescape = partial(re.compile(r'\\(?=[%s])' % _family_punc).sub, '') 

21_family_escape = partial(re.compile(r'(?=[%s])' % _family_punc).sub, r'\\') 

22_value_punc = r'\\=_:,' 

23_value_unescape = partial(re.compile(r'\\(?=[%s])' % _value_punc).sub, '') 

24_value_escape = partial(re.compile(r'(?=[%s])' % _value_punc).sub, r'\\') 

25 

26 

27_CONSTANTS = { 

28 'thin': ('weight', 'light'), 

29 'extralight': ('weight', 'light'), 

30 'ultralight': ('weight', 'light'), 

31 'light': ('weight', 'light'), 

32 'book': ('weight', 'book'), 

33 'regular': ('weight', 'regular'), 

34 'normal': ('weight', 'normal'), 

35 'medium': ('weight', 'medium'), 

36 'demibold': ('weight', 'demibold'), 

37 'semibold': ('weight', 'semibold'), 

38 'bold': ('weight', 'bold'), 

39 'extrabold': ('weight', 'extra bold'), 

40 'black': ('weight', 'black'), 

41 'heavy': ('weight', 'heavy'), 

42 'roman': ('slant', 'normal'), 

43 'italic': ('slant', 'italic'), 

44 'oblique': ('slant', 'oblique'), 

45 'ultracondensed': ('width', 'ultra-condensed'), 

46 'extracondensed': ('width', 'extra-condensed'), 

47 'condensed': ('width', 'condensed'), 

48 'semicondensed': ('width', 'semi-condensed'), 

49 'expanded': ('width', 'expanded'), 

50 'extraexpanded': ('width', 'extra-expanded'), 

51 'ultraexpanded': ('width', 'ultra-expanded'), 

52} 

53 

54 

55@lru_cache # The parser instance is a singleton. 

56def _make_fontconfig_parser(): 

57 def comma_separated(elem): 

58 return elem + ZeroOrMore(Suppress(",") + elem) 

59 

60 family = Regex(fr"([^{_family_punc}]|(\\[{_family_punc}]))*") 

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

62 name = Regex(r"[a-z]+") 

63 value = Regex(fr"([^{_value_punc}]|(\\[{_value_punc}]))*") 

64 prop = Group((name + Suppress("=") + comma_separated(value)) | oneOf(_CONSTANTS)) 

65 return ( 

66 Optional(comma_separated(family)("families")) 

67 + Optional("-" + comma_separated(size)("sizes")) 

68 + ZeroOrMore(":" + prop("properties*")) 

69 + StringEnd() 

70 ) 

71 

72 

73# `parse_fontconfig_pattern` is a bottleneck during the tests because it is 

74# repeatedly called when the rcParams are reset (to validate the default 

75# fonts). In practice, the cache size doesn't grow beyond a few dozen entries 

76# during the test suite. 

77@lru_cache 

78def parse_fontconfig_pattern(pattern): 

79 """ 

80 Parse a fontconfig *pattern* into a dict that can initialize a 

81 `.font_manager.FontProperties` object. 

82 """ 

83 parser = _make_fontconfig_parser() 

84 try: 

85 parse = parser.parseString(pattern) 

86 except ParseException as err: 

87 # explain becomes a plain method on pyparsing 3 (err.explain(0)). 

88 raise ValueError("\n" + ParseException.explain(err, 0)) from None 

89 parser.resetCache() 

90 props = {} 

91 if "families" in parse: 

92 props["family"] = [*map(_family_unescape, parse["families"])] 

93 if "sizes" in parse: 

94 props["size"] = [*parse["sizes"]] 

95 for prop in parse.get("properties", []): 

96 if len(prop) == 1: 

97 prop = _CONSTANTS[prop[0]] 

98 k, *v = prop 

99 props.setdefault(k, []).extend(map(_value_unescape, v)) 

100 return props 

101 

102 

103def generate_fontconfig_pattern(d): 

104 """Convert a `.FontProperties` to a fontconfig pattern string.""" 

105 kvs = [(k, getattr(d, f"get_{k}")()) 

106 for k in ["style", "variant", "weight", "stretch", "file", "size"]] 

107 # Families is given first without a leading keyword. Other entries (which 

108 # are necessarily scalar) are given as key=value, skipping Nones. 

109 return (",".join(_family_escape(f) for f in d.get_family()) 

110 + "".join(f":{k}={_value_escape(str(v))}" 

111 for k, v in kvs if v is not None))