From b50753ce2db412abc705961fe0921736ada5eec8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 14 Aug 2023 14:57:50 -0400 Subject: [PATCH 1/7] Remove unused parameters to parse actions Pyparsing allows you to leave out the parameters (starting from the left) if they are unused. --- lib/matplotlib/_mathtext.py | 49 +++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 5b4e8ed20db3..ba0f1d4aa81d 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2070,18 +2070,18 @@ def push_state(self): """Push a new `State` onto the stack, copying the current state.""" self._state_stack.append(self.get_state().copy()) - def main(self, s, loc, toks): + def main(self, toks): return [Hlist(toks)] - def math_string(self, s, loc, toks): + def math_string(self, toks): return self._math_expression.parseString(toks[0][1:-1], parseAll=True) - def math(self, s, loc, toks): + def math(self, toks): hlist = Hlist(toks) self.pop_state() return [hlist] - def non_math(self, s, loc, toks): + def non_math(self, toks): s = toks[0].replace(r'\$', '$') symbols = [Char(c, self.get_state()) for c in s] hlist = Hlist(symbols) @@ -2092,7 +2092,7 @@ def non_math(self, s, loc, toks): float_literal = staticmethod(pyparsing_common.convertToFloat) - def text(self, s, loc, toks): + def text(self, toks): self.push_state() state = self.get_state() state.font = 'rm' @@ -2132,12 +2132,12 @@ def _make_space(self, percentage): r'\!': -0.16667, # -3/18 em = -3 mu } - def space(self, s, loc, toks): + def space(self, toks): num = self._space_widths[toks["space"]] box = self._make_space(num) return [box] - def customspace(self, s, loc, toks): + def customspace(self, toks): return [self._make_space(toks["space"])] def symbol(self, s, loc, toks): @@ -2215,7 +2215,7 @@ def unknown_symbol(self, s, loc, toks): _wide_accents = set(r"widehat widetilde widebar".split()) - def accent(self, s, loc, toks): + def accent(self, toks): state = self.get_state() thickness = state.get_current_underline_thickness() accent = toks["accent"] @@ -2276,30 +2276,30 @@ def operatorname(self, s, loc, toks): return Hlist(hlist_list) - def start_group(self, s, loc, toks): + def start_group(self, toks): self.push_state() # Deal with LaTeX-style font tokens if toks.get("font"): self.get_state().font = toks.get("font") return [] - def group(self, s, loc, toks): + def group(self, toks): grp = Hlist(toks.get("group", [])) return [grp] - def required_group(self, s, loc, toks): + def required_group(self, toks): return Hlist(toks.get("group", [])) optional_group = required_group - def end_group(self, s, loc, toks): + def end_group(self): self.pop_state() return [] def unclosed_group(self, s, loc, toks): raise ParseFatalException(s, len(s), "Expected '}'") - def font(self, s, loc, toks): + def font(self, toks): self.get_state().font = toks["font"] return [] @@ -2320,9 +2320,6 @@ def is_slanted(self, nucleus): return nucleus.is_slanted() return False - def is_between_brackets(self, s, loc): - return False - def subsuper(self, s, loc, toks): nucleus = toks.get("nucleus", Hbox(0)) subsuper = toks.get("subsuper", []) @@ -2523,26 +2520,26 @@ def _genfrac(self, ldelim, rdelim, rule, style, num, den): return self._auto_sized_delimiter(ldelim, result, rdelim) return result - def style_literal(self, s, loc, toks): + def style_literal(self, toks): return self._MathStyle(int(toks["style_literal"])) - def genfrac(self, s, loc, toks): + def genfrac(self, toks): return self._genfrac( toks.get("ldelim", ""), toks.get("rdelim", ""), toks["rulesize"], toks.get("style", self._MathStyle.TEXTSTYLE), toks["num"], toks["den"]) - def frac(self, s, loc, toks): + def frac(self, toks): return self._genfrac( "", "", self.get_state().get_current_underline_thickness(), self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) - def dfrac(self, s, loc, toks): + def dfrac(self, toks): return self._genfrac( "", "", self.get_state().get_current_underline_thickness(), self._MathStyle.DISPLAYSTYLE, toks["num"], toks["den"]) - def binom(self, s, loc, toks): + def binom(self, toks): return self._genfrac( "(", ")", 0, self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) @@ -2579,7 +2576,7 @@ def _genset(self, s, loc, toks): overset = underset = _genset - def sqrt(self, s, loc, toks): + def sqrt(self, toks): root = toks.get("root") body = toks["value"] state = self.get_state() @@ -2619,7 +2616,7 @@ def sqrt(self, s, loc, toks): rightside]) # Body return [hlist] - def overline(self, s, loc, toks): + def overline(self, toks): body = toks["body"] state = self.get_state() @@ -2670,14 +2667,14 @@ def _auto_sized_delimiter(self, front, middle, back): hlist = Hlist(parts) return hlist - def auto_delim(self, s, loc, toks): + def auto_delim(self, toks): return self._auto_sized_delimiter( toks["left"], # if "mid" in toks ... can be removed when requiring pyparsing 3. toks["mid"].asList() if "mid" in toks else [], toks["right"]) - def boldsymbol(self, s, loc, toks): + def boldsymbol(self, toks): self.push_state() state = self.get_state() hlist = [] @@ -2703,7 +2700,7 @@ def boldsymbol(self, s, loc, toks): return Hlist(hlist) - def substack(self, s, loc, toks): + def substack(self, toks): parts = toks["parts"] state = self.get_state() thickness = state.get_current_underline_thickness() From 63cfa1685dc0c39934f7cffcb8de256931540b2d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Aug 2023 03:58:53 -0400 Subject: [PATCH 2/7] TYP: Add typed classes around mathtext data objects --- lib/matplotlib/_mathtext.py | 149 ++++++++++++++++++++++++++---------- 1 file changed, 108 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index ba0f1d4aa81d..3dd165a11555 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2,8 +2,9 @@ Implementation details for :mod:`.mathtext`. """ +from __future__ import annotations + import copy -from collections import namedtuple import enum import functools import logging @@ -12,6 +13,8 @@ import types import unicodedata import string +import typing as T +from typing import NamedTuple import numpy as np from pyparsing import ( @@ -34,6 +37,9 @@ else: from pyparsing import nested_expr +if T.TYPE_CHECKING: + from .ft2font import FT2Font, Glyph + ParserElement.enablePackrat() _log = logging.getLogger("matplotlib.mathtext") @@ -64,24 +70,49 @@ def get_unicode_index(symbol): # Publicly exported. ) from err -VectorParse = namedtuple("VectorParse", "width height depth glyphs rects", - module="matplotlib.mathtext") -VectorParse.__doc__ = r""" -The namedtuple type returned by ``MathTextParser("path").parse(...)``. +class VectorParse(NamedTuple): + """ + The namedtuple type returned by ``MathTextParser("path").parse(...)``. -This tuple contains the global metrics (*width*, *height*, *depth*), a list of -*glyphs* (including their positions) and of *rect*\angles. -""" + Attributes + ---------- + width, height, depth : float + The global metrics. + glyphs : list + The glyphs including their positions. + rect : list + The list of rectangles. + """ + width: float + height: float + depth: float + glyphs: list[tuple[FT2Font, float, int, float, float]] + rects: list[tuple[float, float, float, float]] +VectorParse.__module__ = "matplotlib.mathtext" -RasterParse = namedtuple("RasterParse", "ox oy width height depth image", - module="matplotlib.mathtext") -RasterParse.__doc__ = r""" -The namedtuple type returned by ``MathTextParser("agg").parse(...)``. -This tuple contains the global metrics (*width*, *height*, *depth*), and a -raster *image*. The offsets *ox*, *oy* are always zero. -""" +class RasterParse(NamedTuple): + """ + The namedtuple type returned by ``MathTextParser("agg").parse(...)``. + + Attributes + ---------- + ox, oy : float + The offsets are always zero. + width, height, depth : float + The global metrics. + image : FT2Image + A raster image. + """ + ox: float + oy: float + width: float + height: float + depth: float + image: FT2Image + +RasterParse.__module__ = "matplotlib.mathtext" class Output: @@ -143,6 +174,48 @@ def to_raster(self, *, antialiased): return RasterParse(0, 0, w, h + d, d, image) +class FontMetrics(NamedTuple): + """ + Metrics of a font. + + Attributes + ---------- + advance : float + The advance distance (in points) of the glyph. + height : float + The height of the glyph in points. + width : float + The width of the glyph in points. + xmin, xmax, ymin, ymax : float + The ink rectangle of the glyph. + iceberg : float + The distance from the baseline to the top of the glyph. (This corresponds to + TeX's definition of "height".) + slanted : bool + Whether the glyph should be considered as "slanted" (currently used for kerning + sub/superscripts). + """ + advance: float + height: float + width: float + xmin: float + xmax: float + ymin: float + ymax: float + iceberg: float + slanted: bool + + +class FontInfo(NamedTuple): + font: FT2Font + fontsize: float + postscript_name: str + metrics: FontMetrics + num: int + glyph: Glyph + offset: float + + class Fonts: """ An abstract base class for a system of fonts to use for mathtext. @@ -197,19 +270,7 @@ def get_metrics(self, font, font_class, sym, fontsize, dpi): Returns ------- - object - - The returned object has the following attributes (all floats, - except *slanted*): - - - *advance*: The advance distance (in points) of the glyph. - - *height*: The height of the glyph in points. - - *width*: The width of the glyph in points. - - *xmin*, *xmax*, *ymin*, *ymax*: The ink rectangle of the glyph - - *iceberg*: The distance from the baseline to the top of the - glyph. (This corresponds to TeX's definition of "height".) - - *slanted*: Whether the glyph should be considered as "slanted" - (currently used for kerning sub/superscripts). + FontMetrics """ info = self._get_info(font, font_class, sym, fontsize, dpi) return info.metrics @@ -295,7 +356,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi): xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] offset = self._get_offset(font, glyph, fontsize, dpi) - metrics = types.SimpleNamespace( + metrics = FontMetrics( advance = glyph.linearHoriAdvance/65536.0, height = glyph.height/64.0, width = glyph.width/64.0, @@ -308,7 +369,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi): slanted = slanted ) - return types.SimpleNamespace( + return FontInfo( font = font, fontsize = fontsize, postscript_name = font.postscript_name, @@ -810,33 +871,33 @@ class FontConstantsBase: be reliably retrieved from the font metrics in the font itself. """ # Percentage of x-height of additional horiz. space after sub/superscripts - script_space = 0.05 + script_space: T.ClassVar[float] = 0.05 # Percentage of x-height that sub/superscripts drop below the baseline - subdrop = 0.4 + subdrop: T.ClassVar[float] = 0.4 # Percentage of x-height that superscripts are raised from the baseline - sup1 = 0.7 + sup1: T.ClassVar[float] = 0.7 # Percentage of x-height that subscripts drop below the baseline - sub1 = 0.3 + sub1: T.ClassVar[float] = 0.3 # Percentage of x-height that subscripts drop below the baseline when a # superscript is present - sub2 = 0.5 + sub2: T.ClassVar[float] = 0.5 # Percentage of x-height that sub/superscripts are offset relative to the # nucleus edge for non-slanted nuclei - delta = 0.025 + delta: T.ClassVar[float] = 0.025 # Additional percentage of last character height above 2/3 of the # x-height that superscripts are offset relative to the subscript # for slanted nuclei - delta_slanted = 0.2 + delta_slanted: T.ClassVar[float] = 0.2 # Percentage of x-height that superscripts and subscripts are offset for # integrals - delta_integral = 0.1 + delta_integral: T.ClassVar[float] = 0.1 class ComputerModernFontConstants(FontConstantsBase): @@ -1333,8 +1394,14 @@ def __init__(self, state): super().__init__(thickness, np.inf, np.inf, state) -_GlueSpec = namedtuple( - "_GlueSpec", "width stretch stretch_order shrink shrink_order") +class _GlueSpec(NamedTuple): + width: float + stretch: float + stretch_order: int + shrink: float + shrink_order: int + + _GlueSpec._named = { # type: ignore[attr-defined] 'fil': _GlueSpec(0., 1., 1, 0., 0), 'fill': _GlueSpec(0., 1., 2, 0., 0), @@ -1358,7 +1425,7 @@ class Glue(Node): def __init__(self, glue_type): super().__init__() if isinstance(glue_type, str): - glue_spec = _GlueSpec._named[glue_type] + glue_spec = _GlueSpec._named[glue_type] # type: ignore[attr-defined] elif isinstance(glue_type, _GlueSpec): glue_spec = glue_type else: From 7c8ed22173f5880107cb2125ba16c21d6ee36bd0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Aug 2023 04:48:46 -0400 Subject: [PATCH 3/7] Add abstract methods and ABC on Fonts classes mypy rightfully complains that these methods called in the base class don't exist, so correctly indicate what should be implemented there. The ABC is not strictly necessary, but it helps with understanding as a lot of these have code that makes them seem like _not_ an ABC. --- lib/matplotlib/_mathtext.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 3dd165a11555..e41fdee5444c 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -4,6 +4,7 @@ from __future__ import annotations +import abc import copy import enum import functools @@ -216,7 +217,7 @@ class FontInfo(NamedTuple): offset: float -class Fonts: +class Fonts(abc.ABC): """ An abstract base class for a system of fonts to use for mathtext. @@ -248,6 +249,13 @@ def get_kern(self, font1, fontclass1, sym1, fontsize1, """ return 0. + def _get_font(self, font: str) -> FT2Font: + raise NotImplementedError + + def _get_info(self, font: str, font_class: str, sym: str, fontsize: float, + dpi: float) -> FontInfo: + raise NotImplementedError + def get_metrics(self, font, font_class, sym, fontsize, dpi): r""" Parameters @@ -313,7 +321,7 @@ def get_sized_alternatives_for_symbol(self, fontname, sym): return [(fontname, sym)] -class TruetypeFonts(Fonts): +class TruetypeFonts(Fonts, metaclass=abc.ABCMeta): """ A generic base class for all font setups that use Truetype fonts (through FT2Font). @@ -324,6 +332,7 @@ def __init__(self, *args, **kwargs): # Per-instance cache. self._get_info = functools.cache(self._get_info) self._fonts = {} + self.fontmap: dict[str | int, str] = {} filename = findfont(self.default_font_prop) default_font = get_font(filename) @@ -348,6 +357,10 @@ def _get_offset(self, font, glyph, fontsize, dpi): return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72) return 0. + def _get_glyph(self, fontname: str, font_class: str, + sym: str) -> tuple[FT2Font, int, bool]: + raise NotImplementedError + # The return value of _get_info is cached per-instance. def _get_info(self, fontname, font_class, sym, fontsize, dpi): font, num, slanted = self._get_glyph(fontname, font_class, sym) @@ -429,7 +442,6 @@ def __init__(self, *args, **kwargs): self._stix_fallback = StixFonts(*args, **kwargs) super().__init__(*args, **kwargs) - self.fontmap = {} for key, val in self._fontmap.items(): fullpath = findfont(val) self.fontmap[key] = fullpath @@ -543,7 +555,6 @@ def __init__(self, *args, **kwargs): self._fallback_font = font_cls(*args, **kwargs) if font_cls else None super().__init__(*args, **kwargs) - self.fontmap = {} for texfont in "cal rm tt it bf sf bfit".split(): prop = mpl.rcParams['mathtext.' + texfont] font = findfont(prop) @@ -641,7 +652,8 @@ def get_sized_alternatives_for_symbol(self, fontname, sym): return [(fontname, sym)] -class DejaVuFonts(UnicodeFonts): +class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta): + _fontmap: dict[str | int, str] = {} def __init__(self, *args, **kwargs): # This must come first so the backend's owner is set correctly @@ -651,7 +663,6 @@ def __init__(self, *args, **kwargs): self._fallback_font = StixSansFonts(*args, **kwargs) self.bakoma = BakomaFonts(*args, **kwargs) TruetypeFonts.__init__(self, *args, **kwargs) - self.fontmap = {} # Include Stix sized alternatives for glyphs self._fontmap.update({ 1: 'STIXSizeOneSym', @@ -749,7 +760,6 @@ class StixFonts(UnicodeFonts): def __init__(self, *args, **kwargs): TruetypeFonts.__init__(self, *args, **kwargs) - self.fontmap = {} for key, name in self._fontmap.items(): fullpath = findfont(name) self.fontmap[key] = fullpath From d0eca17a75906e47636166debf63f4e729b97e2f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Aug 2023 05:38:17 -0400 Subject: [PATCH 4/7] Re-arrange some mathtext code to avoid variable reuse --- lib/matplotlib/_mathtext.py | 53 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index e41fdee5444c..10e591d55ca6 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -768,21 +768,25 @@ def __init__(self, *args, **kwargs): def _map_virtual_font(self, fontname, font_class, uniindex): # Handle these "fonts" that are actually embedded in # other fonts. - mapping = stix_virtual_fonts.get(fontname) - if (self._sans and mapping is None + font_mapping = stix_virtual_fonts.get(fontname) + if (self._sans and font_mapping is None and fontname not in ('regular', 'default')): - mapping = stix_virtual_fonts['sf'] + font_mapping = stix_virtual_fonts['sf'] doing_sans_conversion = True else: doing_sans_conversion = False - if mapping is not None: - if isinstance(mapping, dict): - try: - mapping = mapping[font_class] - except KeyError: - mapping = mapping['rm'] + if isinstance(font_mapping, dict): + try: + mapping = font_mapping[font_class] + except KeyError: + mapping = font_mapping['rm'] + elif isinstance(font_mapping, list): + mapping = font_mapping + else: + mapping = None + if mapping is not None: # Binary search for the source glyph lo = 0 hi = len(mapping) @@ -1965,11 +1969,14 @@ def csnames(group, names): ends_with_alpha.append(name) else: ends_with_nonalpha.append(name) - return Regex(r"\\(?P<{}>(?:{})(?![A-Za-z]){})".format( - group, - "|".join(map(re.escape, ends_with_alpha)), - "".join(f"|{s}" for s in map(re.escape, ends_with_nonalpha)), - )) + return Regex( + r"\\(?P<{group}>(?:{alpha})(?![A-Za-z]){additional}{nonalpha})".format( + group=group, + alpha="|".join(map(re.escape, ends_with_alpha)), + additional="|" if ends_with_nonalpha else "", + nonalpha="|".join(map(re.escape, ends_with_nonalpha)), + ) + ) p.float_literal = Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)") p.space = oneOf(self._space_widths)("space") @@ -2458,9 +2465,9 @@ def subsuper(self, s, loc, toks): hlist.hpack(width, 'exactly') vlist.extend([Vbox(0, vgap), hlist]) shift = hlist.height + vgap + nucleus.depth - vlist = Vlist(vlist) - vlist.shift_amount = shift - result = Hlist([vlist]) + vlt = Vlist(vlist) + vlt.shift_amount = shift + result = Hlist([vlt]) return [result] # We remove kerning on the last character for consistency (otherwise @@ -2781,20 +2788,20 @@ def substack(self, toks): parts = toks["parts"] state = self.get_state() thickness = state.get_current_underline_thickness() - vlist = [] hlist = [Hlist(k) for k in parts[0]] max_width = max(map(lambda c: c.width, hlist)) + vlist = [] for sub in hlist: cp = HCentered([sub]) cp.hpack(max_width, 'exactly') vlist.append(cp) - vlist = [val for pair in zip(vlist, - [Vbox(0, thickness * 2)] * - len(vlist)) for val in pair] - del vlist[-1] - vlt = Vlist(vlist) + stack = [val + for pair in zip(vlist, [Vbox(0, thickness * 2)] * len(vlist)) + for val in pair] + del stack[-1] + vlt = Vlist(stack) result = [Hlist([vlt])] return result From 2bd74a6fe39490bdaf031f3ada183c487b2ecd31 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Aug 2023 05:49:51 -0400 Subject: [PATCH 5/7] mathtext: Make pyparsing cache reset more explicit Just as enabling the packrat cache occurs on the class overall, so does the reset. --- lib/matplotlib/_mathtext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 10e591d55ca6..5df366de8f6b 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2139,7 +2139,7 @@ def parse(self, s, fonts_object, fontsize, dpi): self._in_subscript_or_superscript = False # prevent operator spacing from leaking into a new expression self._em_width_cache = {} - self._expression.resetCache() + ParserElement.resetCache() return result[0] def get_state(self): From a5f8ec4b3a520b35117b2ee197aea40bc6db9df7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Aug 2023 05:10:21 -0400 Subject: [PATCH 6/7] TYP: Add type hints to all mathtext internals --- lib/matplotlib/_mathtext.py | 418 +++++++++++++++++-------------- lib/matplotlib/_mathtext_data.py | 6 +- 2 files changed, 236 insertions(+), 188 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 5df366de8f6b..b23cb67116ed 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -29,7 +29,7 @@ from ._mathtext_data import ( latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) from .font_manager import FontProperties, findfont, get_font -from .ft2font import FT2Image, KERNING_DEFAULT +from .ft2font import FT2Font, FT2Image, KERNING_DEFAULT from packaging.version import parse as parse_version from pyparsing import __version__ as pyparsing_version @@ -39,7 +39,8 @@ from pyparsing import nested_expr if T.TYPE_CHECKING: - from .ft2font import FT2Font, Glyph + from collections.abc import Iterable + from .ft2font import Glyph ParserElement.enablePackrat() _log = logging.getLogger("matplotlib.mathtext") @@ -49,7 +50,7 @@ # FONTS -def get_unicode_index(symbol): # Publicly exported. +def get_unicode_index(symbol: str) -> int: # Publicly exported. r""" Return the integer index (from the Unicode table) of *symbol*. @@ -124,12 +125,12 @@ class Output: a `RasterParse` by `.MathTextParser.parse`. """ - def __init__(self, box): + def __init__(self, box: Box): self.box = box - self.glyphs = [] # (ox, oy, info) - self.rects = [] # (x1, y1, x2, y2) + self.glyphs: list[tuple[float, float, FontInfo]] = [] # (ox, oy, info) + self.rects: list[tuple[float, float, float, float]] = [] # (x1, y1, x2, y2) - def to_vector(self): + def to_vector(self) -> VectorParse: w, h, d = map( np.ceil, [self.box.width, self.box.height, self.box.depth]) gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) @@ -138,7 +139,7 @@ def to_vector(self): for x1, y1, x2, y2 in self.rects] return VectorParse(w, h + d, d, gs, rs) - def to_raster(self, *, antialiased): + def to_raster(self, *, antialiased: bool) -> RasterParse: # Metrics y's and mathtext y's are oriented in opposite directions, # hence the switch between ymin and ymax. xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs], @@ -226,7 +227,7 @@ class Fonts(abc.ABC): to do the actual drawing. """ - def __init__(self, default_font_prop, load_glyph_flags): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): """ Parameters ---------- @@ -240,8 +241,9 @@ def __init__(self, default_font_prop, load_glyph_flags): self.default_font_prop = default_font_prop self.load_glyph_flags = load_glyph_flags - def get_kern(self, font1, fontclass1, sym1, fontsize1, - font2, fontclass2, sym2, fontsize2, dpi): + def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, + font2: str, fontclass2: str, sym2: str, fontsize2: float, + dpi: float) -> float: """ Get the kerning distance for font between *sym1* and *sym2*. @@ -256,7 +258,8 @@ def _get_info(self, font: str, font_class: str, sym: str, fontsize: float, dpi: float) -> FontInfo: raise NotImplementedError - def get_metrics(self, font, font_class, sym, fontsize, dpi): + def get_metrics(self, font: str, font_class: str, sym: str, fontsize: float, + dpi: float) -> FontMetrics: r""" Parameters ---------- @@ -283,8 +286,8 @@ def get_metrics(self, font, font_class, sym, fontsize, dpi): info = self._get_info(font, font_class, sym, fontsize, dpi) return info.metrics - def render_glyph( - self, output, ox, oy, font, font_class, sym, fontsize, dpi): + def render_glyph(self, output: Output, ox: float, oy: float, font: str, + font_class: str, sym: str, fontsize: float, dpi: float) -> None: """ At position (*ox*, *oy*), draw the glyph specified by the remaining parameters (see `get_metrics` for their detailed description). @@ -292,26 +295,28 @@ def render_glyph( info = self._get_info(font, font_class, sym, fontsize, dpi) output.glyphs.append((ox, oy, info)) - def render_rect_filled(self, output, x1, y1, x2, y2): + def render_rect_filled(self, output: Output, + x1: float, y1: float, x2: float, y2: float) -> None: """ Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*). """ output.rects.append((x1, y1, x2, y2)) - def get_xheight(self, font, fontsize, dpi): + def get_xheight(self, font: str, fontsize: float, dpi: float) -> float: """ Get the xheight for the given *font* and *fontsize*. """ raise NotImplementedError() - def get_underline_thickness(self, font, fontsize, dpi): + def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float: """ Get the line thickness that matches the given font. Used as a base unit for drawing lines such as in a fraction or radical. """ raise NotImplementedError() - def get_sized_alternatives_for_symbol(self, fontname, sym): + def get_sized_alternatives_for_symbol(self, fontname: str, + sym: str) -> list[tuple[str, str]]: """ Override if your font provides multiple sizes of the same symbol. Should return a list of symbols matching *sym* in @@ -327,10 +332,10 @@ class TruetypeFonts(Fonts, metaclass=abc.ABCMeta): (through FT2Font). """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + super().__init__(default_font_prop, load_glyph_flags) # Per-instance cache. - self._get_info = functools.cache(self._get_info) + self._get_info = functools.cache(self._get_info) # type: ignore[method-assign] self._fonts = {} self.fontmap: dict[str | int, str] = {} @@ -339,20 +344,23 @@ def __init__(self, *args, **kwargs): self._fonts['default'] = default_font self._fonts['regular'] = default_font - def _get_font(self, font): + def _get_font(self, font: str | int) -> FT2Font: if font in self.fontmap: basename = self.fontmap[font] else: - basename = font + # NOTE: An int is only passed by subclasses which have placed int keys into + # `self.fontmap`, so we must cast this to confirm it to typing. + basename = T.cast(str, font) cached_font = self._fonts.get(basename) if cached_font is None and os.path.exists(basename): cached_font = get_font(basename) self._fonts[basename] = cached_font self._fonts[cached_font.postscript_name] = cached_font self._fonts[cached_font.postscript_name.lower()] = cached_font - return cached_font + return T.cast(FT2Font, cached_font) # FIXME: Not sure this is guaranteed. - def _get_offset(self, font, glyph, fontsize, dpi): + def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float, + dpi: float) -> float: if font.postscript_name == 'Cmex10': return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72) return 0. @@ -362,7 +370,8 @@ def _get_glyph(self, fontname: str, font_class: str, raise NotImplementedError # The return value of _get_info is cached per-instance. - def _get_info(self, fontname, font_class, sym, fontsize, dpi): + def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, + dpi: float) -> FontInfo: font, num, slanted = self._get_glyph(fontname, font_class, sym) font.set_size(fontsize, dpi) glyph = font.load_char(num, flags=self.load_glyph_flags) @@ -392,7 +401,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi): offset = offset ) - def get_xheight(self, fontname, fontsize, dpi): + def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float: font = self._get_font(fontname) font.set_size(fontsize, dpi) pclt = font.get_sfnt_table('pclt') @@ -404,14 +413,15 @@ def get_xheight(self, fontname, fontsize, dpi): xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0) return xHeight - def get_underline_thickness(self, font, fontsize, dpi): + def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float: # This function used to grab underline thickness from the font # metrics, but that information is just too un-reliable, so it # is now hardcoded. return ((0.75 / 12.0) * fontsize * dpi) / 72.0 - def get_kern(self, font1, fontclass1, sym1, fontsize1, - font2, fontclass2, sym2, fontsize2, dpi): + def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, + font2: str, fontclass2: str, sym2: str, fontsize2: float, + dpi: float) -> float: if font1 == font2 and fontsize1 == fontsize2: info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) @@ -438,10 +448,10 @@ class BakomaFonts(TruetypeFonts): 'ex': 'cmex10', } - def __init__(self, *args, **kwargs): - self._stix_fallback = StixFonts(*args, **kwargs) + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + self._stix_fallback = StixFonts(default_font_prop, load_glyph_flags) - super().__init__(*args, **kwargs) + super().__init__(default_font_prop, load_glyph_flags) for key, val in self._fontmap.items(): fullpath = findfont(val) self.fontmap[key] = fullpath @@ -449,7 +459,8 @@ def __init__(self, *args, **kwargs): _slanted_symbols = set(r"\int \oint".split()) - def _get_glyph(self, fontname, font_class, sym): + def _get_glyph(self, fontname: str, font_class: str, + sym: str) -> tuple[FT2Font, int, bool]: font = None if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] @@ -521,7 +532,8 @@ def _get_glyph(self, fontname, font_class, sym): (r'\]', ']')]: _size_alternatives[alias] = _size_alternatives[target] - def get_sized_alternatives_for_symbol(self, fontname, sym): + def get_sized_alternatives_for_symbol(self, fontname: str, + sym: str) -> list[tuple[str, str]]: return self._size_alternatives.get(sym, [(fontname, sym)]) @@ -545,16 +557,18 @@ class UnicodeFonts(TruetypeFonts): 0x2212: 0x00A1, # Minus sign. } - def __init__(self, *args, **kwargs): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): # This must come first so the backend's owner is set correctly fallback_rc = mpl.rcParams['mathtext.fallback'] - font_cls = {'stix': StixFonts, - 'stixsans': StixSansFonts, - 'cm': BakomaFonts - }.get(fallback_rc) - self._fallback_font = font_cls(*args, **kwargs) if font_cls else None - - super().__init__(*args, **kwargs) + font_cls: type[TruetypeFonts] | None = { + 'stix': StixFonts, + 'stixsans': StixSansFonts, + 'cm': BakomaFonts + }.get(fallback_rc) + self._fallback_font = (font_cls(default_font_prop, load_glyph_flags) + if font_cls else None) + + super().__init__(default_font_prop, load_glyph_flags) for texfont in "cal rm tt it bf sf bfit".split(): prop = mpl.rcParams['mathtext.' + texfont] font = findfont(prop) @@ -580,10 +594,12 @@ def __init__(self, *args, **kwargs): _slanted_symbols = set(r"\int \oint".split()) - def _map_virtual_font(self, fontname, font_class, uniindex): + def _map_virtual_font(self, fontname: str, font_class: str, + uniindex: int) -> tuple[str, int]: return fontname, uniindex - def _get_glyph(self, fontname, font_class, sym): + def _get_glyph(self, fontname: str, font_class: str, + sym: str) -> tuple[FT2Font, int, bool]: try: uniindex = get_unicode_index(sym) found_symbol = True @@ -645,7 +661,8 @@ def _get_glyph(self, fontname, font_class, sym): return font, uniindex, slanted - def get_sized_alternatives_for_symbol(self, fontname, sym): + def get_sized_alternatives_for_symbol(self, fontname: str, + sym: str) -> list[tuple[str, str]]: if self._fallback_font: return self._fallback_font.get_sized_alternatives_for_symbol( fontname, sym) @@ -655,14 +672,14 @@ def get_sized_alternatives_for_symbol(self, fontname, sym): class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta): _fontmap: dict[str | int, str] = {} - def __init__(self, *args, **kwargs): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): # This must come first so the backend's owner is set correctly if isinstance(self, DejaVuSerifFonts): - self._fallback_font = StixFonts(*args, **kwargs) + self._fallback_font = StixFonts(default_font_prop, load_glyph_flags) else: - self._fallback_font = StixSansFonts(*args, **kwargs) - self.bakoma = BakomaFonts(*args, **kwargs) - TruetypeFonts.__init__(self, *args, **kwargs) + self._fallback_font = StixSansFonts(default_font_prop, load_glyph_flags) + self.bakoma = BakomaFonts(default_font_prop, load_glyph_flags) + TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) # Include Stix sized alternatives for glyphs self._fontmap.update({ 1: 'STIXSizeOneSym', @@ -676,7 +693,8 @@ def __init__(self, *args, **kwargs): self.fontmap[key] = fullpath self.fontmap[name] = fullpath - def _get_glyph(self, fontname, font_class, sym): + def _get_glyph(self, fontname: str, font_class: str, + sym: str) -> tuple[FT2Font, int, bool]: # Override prime symbol to use Bakoma. if sym == r'\prime': return self.bakoma._get_glyph(fontname, font_class, sym) @@ -740,7 +758,7 @@ class StixFonts(UnicodeFonts): - handles sized alternative characters for the STIXSizeX fonts. """ - _fontmap = { + _fontmap: dict[str | int, str] = { 'rm': 'STIXGeneral', 'it': 'STIXGeneral:italic', 'bf': 'STIXGeneral:weight=bold', @@ -755,17 +773,18 @@ class StixFonts(UnicodeFonts): 4: 'STIXSizeFourSym', 5: 'STIXSizeFiveSym', } - _fallback_font = False + _fallback_font = None _sans = False - def __init__(self, *args, **kwargs): - TruetypeFonts.__init__(self, *args, **kwargs) + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) for key, name in self._fontmap.items(): fullpath = findfont(name) self.fontmap[key] = fullpath self.fontmap[name] = fullpath - def _map_virtual_font(self, fontname, font_class, uniindex): + def _map_virtual_font(self, fontname: str, font_class: str, + uniindex: int) -> tuple[str, int]: # Handle these "fonts" that are actually embedded in # other fonts. font_mapping = stix_virtual_fonts.get(fontname) @@ -819,7 +838,10 @@ def _map_virtual_font(self, fontname, font_class, uniindex): return fontname, uniindex @functools.cache - def get_sized_alternatives_for_symbol(self, fontname, sym): + def get_sized_alternatives_for_symbol( # type: ignore[override] + self, + fontname: str, + sym: str) -> list[tuple[str, str]] | list[tuple[int, str]]: fixes = { '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']', '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}', @@ -974,7 +996,7 @@ class DejaVuSansFontConstants(FontConstantsBase): } -def _get_font_constant_set(state): +def _get_font_constant_set(state: ParserState) -> type[FontConstantsBase]: constants = _font_constant_mapping.get( state.fontset._get_font(state.font).family_name, FontConstantsBase) # STIX sans isn't really its own fonts, just different code points @@ -988,57 +1010,58 @@ def _get_font_constant_set(state): class Node: """A node in the TeX box model.""" - def __init__(self): + def __init__(self) -> None: self.size = 0 - def __repr__(self): + def __repr__(self) -> str: return type(self).__name__ - def get_kerning(self, next): + def get_kerning(self, next: Node | None) -> float: return 0.0 - def shrink(self): + def shrink(self) -> None: """ Shrinks one level smaller. There are only three levels of sizes, after which things will no longer get smaller. """ self.size += 1 - def render(self, output, x, y): + def render(self, output: Output, x: float, y: float) -> None: """Render this node.""" class Box(Node): """A node with a physical location.""" - def __init__(self, width, height, depth): + def __init__(self, width: float, height: float, depth: float) -> None: super().__init__() self.width = width self.height = height self.depth = depth - def shrink(self): + def shrink(self) -> None: super().shrink() if self.size < NUM_SIZE_LEVELS: self.width *= SHRINK_FACTOR self.height *= SHRINK_FACTOR self.depth *= SHRINK_FACTOR - def render(self, output, x1, y1, x2, y2): + def render(self, output: Output, # type: ignore[override] + x1: float, y1: float, x2: float, y2: float) -> None: pass class Vbox(Box): """A box with only height (zero width).""" - def __init__(self, height, depth): + def __init__(self, height: float, depth: float): super().__init__(0., height, depth) class Hbox(Box): """A box with only width (zero height and depth).""" - def __init__(self, width): + def __init__(self, width: float): super().__init__(width, 0., 0.) @@ -1055,7 +1078,7 @@ class Char(Node): `Hlist`. """ - def __init__(self, c, state): + def __init__(self, c: str, state: ParserState): super().__init__() self.c = c self.fontset = state.fontset @@ -1067,10 +1090,10 @@ def __init__(self, c, state): # pack phase, after we know the real fontsize self._update_metrics() - def __repr__(self): + def __repr__(self) -> str: return '`%s`' % self.c - def _update_metrics(self): + def _update_metrics(self) -> None: metrics = self._metrics = self.fontset.get_metrics( self.font, self.font_class, self.c, self.fontsize, self.dpi) if self.c == ' ': @@ -1080,10 +1103,10 @@ def _update_metrics(self): self.height = metrics.iceberg self.depth = -(metrics.iceberg - metrics.height) - def is_slanted(self): + def is_slanted(self) -> bool: return self._metrics.slanted - def get_kerning(self, next): + def get_kerning(self, next: Node | None) -> float: """ Return the amount of kerning between this and the given character. @@ -1099,12 +1122,12 @@ def get_kerning(self, next): self.dpi) return advance + kern - def render(self, output, x, y): + def render(self, output: Output, x: float, y: float) -> None: self.fontset.render_glyph( output, x, y, self.font, self.font_class, self.c, self.fontsize, self.dpi) - def shrink(self): + def shrink(self) -> None: super().shrink() if self.size < NUM_SIZE_LEVELS: self.fontsize *= SHRINK_FACTOR @@ -1119,18 +1142,18 @@ class Accent(Char): since they are already offset correctly from the baseline in TrueType fonts. """ - def _update_metrics(self): + def _update_metrics(self) -> None: metrics = self._metrics = self.fontset.get_metrics( self.font, self.font_class, self.c, self.fontsize, self.dpi) self.width = metrics.xmax - metrics.xmin self.height = metrics.ymax - metrics.ymin self.depth = 0 - def shrink(self): + def shrink(self) -> None: super().shrink() self._update_metrics() - def render(self, output, x, y): + def render(self, output: Output, x: float, y: float) -> None: self.fontset.render_glyph( output, x - self._metrics.xmin, y + self._metrics.ymin, self.font, self.font_class, self.c, self.fontsize, self.dpi) @@ -1139,23 +1162,24 @@ def render(self, output, x, y): class List(Box): """A list of nodes (either horizontal or vertical).""" - def __init__(self, elements): + def __init__(self, elements: T.Sequence[Node]): super().__init__(0., 0., 0.) self.shift_amount = 0. # An arbitrary offset - self.children = elements # The child nodes of this list + self.children = [*elements] # The child nodes of this list # The following parameters are set in the vpack and hpack functions self.glue_set = 0. # The glue setting of this list self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching self.glue_order = 0 # The order of infinity (0 - 3) for the glue - def __repr__(self): + def __repr__(self) -> str: return '{}[{}]'.format( super().__repr__(), self.width, self.height, self.depth, self.shift_amount, ', '.join([repr(x) for x in self.children])) - def _set_glue(self, x, sign, totals, error_type): + def _set_glue(self, x: float, sign: int, totals: list[float], + error_type: str) -> None: self.glue_order = o = next( # Highest order of glue used by the members of this list. (i for i in range(len(totals))[::-1] if totals[i] != 0), 0) @@ -1170,7 +1194,7 @@ def _set_glue(self, x, sign, totals, error_type): _log.warning("%s %s: %r", error_type, type(self).__name__, self) - def shrink(self): + def shrink(self) -> None: for child in self.children: child.shrink() super().shrink() @@ -1182,13 +1206,15 @@ def shrink(self): class Hlist(List): """A horizontal list of boxes.""" - def __init__(self, elements, w=0., m='additional', do_kern=True): + def __init__(self, elements: T.Sequence[Node], w: float = 0.0, + m: T.Literal['additional', 'exactly'] = 'additional', + do_kern: bool = True): super().__init__(elements) if do_kern: self.kern() self.hpack(w=w, m=m) - def kern(self): + def kern(self) -> None: """ Insert `Kern` nodes between `Char` nodes to set kerning. @@ -1213,21 +1239,8 @@ def kern(self): new_children.append(kern) self.children = new_children - # This is a failed experiment to fake cross-font kerning. -# def get_kerning(self, next): -# if len(self.children) >= 2 and isinstance(self.children[-2], Char): -# if isinstance(next, Char): -# print "CASE A" -# return self.children[-2].get_kerning(next) -# elif (isinstance(next, Hlist) and len(next.children) -# and isinstance(next.children[0], Char)): -# print "CASE B" -# result = self.children[-2].get_kerning(next.children[0]) -# print result -# return result -# return 0.0 - - def hpack(self, w=0., m='additional'): + def hpack(self, w: float = 0.0, + m: T.Literal['additional', 'exactly'] = 'additional') -> None: r""" Compute the dimensions of the resulting boxes, and adjust the glue if one of those dimensions is pre-specified. The computed sizes normally @@ -1295,11 +1308,14 @@ def hpack(self, w=0., m='additional'): class Vlist(List): """A vertical list of boxes.""" - def __init__(self, elements, h=0., m='additional'): + def __init__(self, elements: T.Sequence[Node], h: float = 0.0, + m: T.Literal['additional', 'exactly'] = 'additional'): super().__init__(elements) self.vpack(h=h, m=m) - def vpack(self, h=0., m='additional', l=np.inf): + def vpack(self, h: float = 0.0, + m: T.Literal['additional', 'exactly'] = 'additional', + l: float = np.inf) -> None: """ Compute the dimensions of the resulting boxes, and to adjust the glue if one of those dimensions is pre-specified. @@ -1382,18 +1398,19 @@ class Rule(Box): running in an `Hlist`; the height and depth are never running in a `Vlist`. """ - def __init__(self, width, height, depth, state): + def __init__(self, width: float, height: float, depth: float, state: ParserState): super().__init__(width, height, depth) self.fontset = state.fontset - def render(self, output, x, y, w, h): + def render(self, output: Output, # type: ignore[override] + x: float, y: float, w: float, h: float) -> None: self.fontset.render_rect_filled(output, x, y, x + w, y + h) class Hrule(Rule): """Convenience class to create a horizontal rule.""" - def __init__(self, state, thickness=None): + def __init__(self, state: ParserState, thickness: float | None = None): if thickness is None: thickness = state.get_current_underline_thickness() height = depth = thickness * 0.5 @@ -1403,7 +1420,7 @@ def __init__(self, state, thickness=None): class Vrule(Rule): """Convenience class to create a vertical rule.""" - def __init__(self, state): + def __init__(self, state: ParserState): thickness = state.get_current_underline_thickness() super().__init__(thickness, np.inf, np.inf, state) @@ -1436,7 +1453,10 @@ class Glue(Node): it's easier to stick to what TeX does.) """ - def __init__(self, glue_type): + def __init__(self, + glue_type: _GlueSpec | T.Literal["fil", "fill", "filll", + "neg_fil", "neg_fill", "neg_filll", + "empty", "ss"]): super().__init__() if isinstance(glue_type, str): glue_spec = _GlueSpec._named[glue_type] # type: ignore[attr-defined] @@ -1446,7 +1466,7 @@ def __init__(self, glue_type): raise ValueError("glue_type must be a glue spec name or instance") self.glue_spec = glue_spec - def shrink(self): + def shrink(self) -> None: super().shrink() if self.size < NUM_SIZE_LEVELS: g = self.glue_spec @@ -1459,7 +1479,7 @@ class HCentered(Hlist): centered within its enclosing box. """ - def __init__(self, elements): + def __init__(self, elements: list[Node]): super().__init__([Glue('ss'), *elements, Glue('ss')], do_kern=False) @@ -1469,7 +1489,7 @@ class VCentered(Vlist): centered within its enclosing box. """ - def __init__(self, elements): + def __init__(self, elements: list[Node]): super().__init__([Glue('ss'), *elements, Glue('ss')]) @@ -1487,14 +1507,14 @@ class Kern(Node): height = 0 depth = 0 - def __init__(self, width): + def __init__(self, width: float): super().__init__() self.width = width - def __repr__(self): + def __repr__(self) -> str: return "k%.02f" % self.width - def shrink(self): + def shrink(self) -> None: super().shrink() if self.size < NUM_SIZE_LEVELS: self.width *= SHRINK_FACTOR @@ -1509,7 +1529,8 @@ class AutoHeightChar(Hlist): always just return a scaled version of the glyph. """ - def __init__(self, c, height, depth, state, always=False, factor=None): + def __init__(self, c: str, height: float, depth: float, state: ParserState, + always: bool = False, factor: float | None = None): alternatives = state.fontset.get_sized_alternatives_for_symbol( state.font, c) @@ -1526,7 +1547,7 @@ def __init__(self, c, height, depth, state, always=False, factor=None): if char.height + char.depth >= target_total - 0.2 * xHeight: break - shift = 0 + shift = 0.0 if state.font != 0 or len(alternatives) == 1: if factor is None: factor = target_total / (char.height + char.depth) @@ -1548,7 +1569,8 @@ class AutoWidthChar(Hlist): always just return a scaled version of the glyph. """ - def __init__(self, c, width, state, always=False, char_class=Char): + def __init__(self, c: str, width: float, state: ParserState, always: bool = False, + char_class: type[Char] = Char): alternatives = state.fontset.get_sized_alternatives_for_symbol( state.font, c) @@ -1567,7 +1589,7 @@ def __init__(self, c, width, state, always=False, char_class=Char): self.width = char.width -def ship(box, xy=(0, 0)): +def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output: """ Ship out *box* at offset *xy*, converting it to an `Output`. @@ -1584,10 +1606,10 @@ def ship(box, xy=(0, 0)): off_v = oy + box.height output = Output(box) - def clamp(value): + def clamp(value: float) -> float: return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value - def hlist_out(box): + def hlist_out(box: Hlist) -> None: nonlocal cur_v, cur_h, off_h, off_v cur_g = 0 @@ -1612,9 +1634,11 @@ def hlist_out(box): cur_v = base_line + p.shift_amount if isinstance(p, Hlist): hlist_out(p) - else: + elif isinstance(p, Vlist): # p.vpack(box.height + box.depth, 'exactly') vlist_out(p) + else: + assert False, "unreachable code" cur_h = edge + p.width cur_v = base_line elif isinstance(p, Box): @@ -1648,7 +1672,7 @@ def hlist_out(box): rule_width += cur_g cur_h += rule_width - def vlist_out(box): + def vlist_out(box: Vlist) -> None: nonlocal cur_v, cur_h, off_h, off_v cur_g = 0 @@ -1672,8 +1696,10 @@ def vlist_out(box): p.width = box.width if isinstance(p, Hlist): hlist_out(p) - else: + elif isinstance(p, Vlist): vlist_out(p) + else: + assert False, "unreachable code" cur_v = save_v + p.depth cur_h = left_edge elif isinstance(p, Box): @@ -1705,6 +1731,7 @@ def vlist_out(box): raise RuntimeError( "Internal mathtext error: Char node found in vlist") + assert isinstance(box, Hlist) hlist_out(box) return output @@ -1713,9 +1740,9 @@ def vlist_out(box): # PARSER -def Error(msg): +def Error(msg: str) -> ParserElement: """Helper class to raise parser errors.""" - def raise_error(s, loc, toks): + def raise_error(s: str, loc: int, toks: ParseResults) -> T.Any: raise ParseFatalException(s, loc, msg) return Empty().setParseAction(raise_error) @@ -1732,33 +1759,34 @@ class ParserState: and popped accordingly. """ - def __init__(self, fontset, font, font_class, fontsize, dpi): + def __init__(self, fontset: Fonts, font: str, font_class: str, fontsize: float, + dpi: float): self.fontset = fontset self._font = font self.font_class = font_class self.fontsize = fontsize self.dpi = dpi - def copy(self): + def copy(self) -> ParserState: return copy.copy(self) @property - def font(self): + def font(self) -> str: return self._font @font.setter - def font(self, name): + def font(self, name: str) -> None: if name in ('rm', 'it', 'bf', 'bfit'): self.font_class = name self._font = name - def get_current_underline_thickness(self): + def get_current_underline_thickness(self) -> float: """Return the underline thickness for this state.""" return self.fontset.get_underline_thickness( self.font, self.fontsize, self.dpi) -def cmd(expr, args): +def cmd(expr: str, args: ParserElement) -> ParserElement: r""" Helper to define TeX commands. @@ -1770,7 +1798,7 @@ def cmd(expr, args): the error message. """ - def names(elt): + def names(elt: ParserElement) -> T.Generator[str, None, None]: if isinstance(elt, ParseExpression): for expr in elt.exprs: yield from names(expr) @@ -1943,10 +1971,10 @@ class _MathStyle(enum.Enum): ord('\N{GREEK SMALL LETTER OMEGA}') + 1)]) _latin_alphabets = set(string.ascii_letters) - def __init__(self): + def __init__(self) -> None: p = types.SimpleNamespace() - def set_names_and_parse_actions(): + def set_names_and_parse_actions() -> None: for key, val in vars(p).items(): if not key.startswith('_'): # Set names on (almost) everything -- very useful for debugging @@ -1961,7 +1989,7 @@ def set_names_and_parse_actions(): # Root definitions. # In TeX parlance, a csname is a control sequence name (a "\foo"). - def csnames(group, names): + def csnames(group: str, names: Iterable[str]) -> Regex: ends_with_alpha = [] ends_with_nonalpha = [] for name in names: @@ -2120,7 +2148,7 @@ def csnames(group, names): # To add space to nucleus operators after sub/superscripts self._in_subscript_or_superscript = False - def parse(self, s, fonts_object, fontsize, dpi): + def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hlist: """ Parse expression *s* using the given *fonts_object* for output, at the given *fontsize* and *dpi*. @@ -2129,43 +2157,43 @@ def parse(self, s, fonts_object, fontsize, dpi): """ self._state_stack = [ ParserState(fonts_object, 'default', 'rm', fontsize, dpi)] - self._em_width_cache = {} + self._em_width_cache: dict[tuple[str, float, float], float] = {} try: result = self._expression.parseString(s) except ParseBaseException as err: # explain becomes a plain method on pyparsing 3 (err.explain(0)). raise ValueError("\n" + ParseException.explain(err, 0)) from None - self._state_stack = None + self._state_stack = [] self._in_subscript_or_superscript = False # prevent operator spacing from leaking into a new expression self._em_width_cache = {} ParserElement.resetCache() - return result[0] + return T.cast(Hlist, result[0]) # Known return type from main. - def get_state(self): + def get_state(self) -> ParserState: """Get the current `State` of the parser.""" return self._state_stack[-1] - def pop_state(self): + def pop_state(self) -> None: """Pop a `State` off of the stack.""" self._state_stack.pop() - def push_state(self): + def push_state(self) -> None: """Push a new `State` onto the stack, copying the current state.""" self._state_stack.append(self.get_state().copy()) - def main(self, toks): - return [Hlist(toks)] + def main(self, toks: ParseResults) -> list[Hlist]: + return [Hlist(toks.asList())] - def math_string(self, toks): + def math_string(self, toks: ParseResults) -> ParseResults: return self._math_expression.parseString(toks[0][1:-1], parseAll=True) - def math(self, toks): - hlist = Hlist(toks) + def math(self, toks: ParseResults) -> T.Any: + hlist = Hlist(toks.asList()) self.pop_state() return [hlist] - def non_math(self, toks): + def non_math(self, toks: ParseResults) -> T.Any: s = toks[0].replace(r'\$', '$') symbols = [Char(c, self.get_state()) for c in s] hlist = Hlist(symbols) @@ -2176,7 +2204,7 @@ def non_math(self, toks): float_literal = staticmethod(pyparsing_common.convertToFloat) - def text(self, toks): + def text(self, toks: ParseResults) -> T.Any: self.push_state() state = self.get_state() state.font = 'rm' @@ -2184,7 +2212,7 @@ def text(self, toks): self.pop_state() return [hlist] - def _make_space(self, percentage): + def _make_space(self, percentage: float) -> Kern: # In TeX, an em (the unit usually used to measure horizontal lengths) # is not the width of the character 'm'; it is the same in different # font styles (e.g. roman or italic). Mathtext, however, uses 'm' in @@ -2216,15 +2244,16 @@ def _make_space(self, percentage): r'\!': -0.16667, # -3/18 em = -3 mu } - def space(self, toks): + def space(self, toks: ParseResults) -> T.Any: num = self._space_widths[toks["space"]] box = self._make_space(num) return [box] - def customspace(self, toks): + def customspace(self, toks: ParseResults) -> T.Any: return [self._make_space(toks["space"])] - def symbol(self, s, loc, toks): + def symbol(self, s: str, loc: int, + toks: ParseResults | dict[str, str]) -> T.Any: c = toks["sym"] if c == "-": # "U+2212 minus sign is the preferred representation of the unary @@ -2271,7 +2300,7 @@ def symbol(self, s, loc, toks): return [Hlist([char, self._make_space(0.2)], do_kern=True)] return [char] - def unknown_symbol(self, s, loc, toks): + def unknown_symbol(self, s: str, loc: int, toks: ParseResults) -> T.Any: raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}") _accent_map = { @@ -2299,11 +2328,12 @@ def unknown_symbol(self, s, loc, toks): _wide_accents = set(r"widehat widetilde widebar".split()) - def accent(self, toks): + def accent(self, toks: ParseResults) -> T.Any: state = self.get_state() thickness = state.get_current_underline_thickness() accent = toks["accent"] sym = toks["sym"] + accent_box: Node if accent in self._wide_accents: accent_box = AutoWidthChar( '\\' + accent, sym.width, state, char_class=Accent) @@ -2320,16 +2350,16 @@ def accent(self, toks): Hlist([sym]) ]) - def function(self, s, loc, toks): + def function(self, s: str, loc: int, toks: ParseResults) -> T.Any: hlist = self.operatorname(s, loc, toks) hlist.function_name = toks["name"] return hlist - def operatorname(self, s, loc, toks): + def operatorname(self, s: str, loc: int, toks: ParseResults) -> T.Any: self.push_state() state = self.get_state() state.font = 'rm' - hlist_list = [] + hlist_list: list[Node] = [] # Change the font of Chars, but leave Kerns alone name = toks["name"] for c in name: @@ -2360,51 +2390,51 @@ def operatorname(self, s, loc, toks): return Hlist(hlist_list) - def start_group(self, toks): + def start_group(self, toks: ParseResults) -> T.Any: self.push_state() # Deal with LaTeX-style font tokens if toks.get("font"): self.get_state().font = toks.get("font") return [] - def group(self, toks): + def group(self, toks: ParseResults) -> T.Any: grp = Hlist(toks.get("group", [])) return [grp] - def required_group(self, toks): + def required_group(self, toks: ParseResults) -> T.Any: return Hlist(toks.get("group", [])) optional_group = required_group - def end_group(self): + def end_group(self) -> T.Any: self.pop_state() return [] - def unclosed_group(self, s, loc, toks): + def unclosed_group(self, s: str, loc: int, toks: ParseResults) -> T.Any: raise ParseFatalException(s, len(s), "Expected '}'") - def font(self, toks): + def font(self, toks: ParseResults) -> T.Any: self.get_state().font = toks["font"] return [] - def is_overunder(self, nucleus): + def is_overunder(self, nucleus: Node) -> bool: if isinstance(nucleus, Char): return nucleus.c in self._overunder_symbols elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'): return nucleus.function_name in self._overunder_functions return False - def is_dropsub(self, nucleus): + def is_dropsub(self, nucleus: Node) -> bool: if isinstance(nucleus, Char): return nucleus.c in self._dropsub_symbols return False - def is_slanted(self, nucleus): + def is_slanted(self, nucleus: Node) -> bool: if isinstance(nucleus, Char): return nucleus.is_slanted() return False - def subsuper(self, s, loc, toks): + def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: nucleus = toks.get("nucleus", Hbox(0)) subsuper = toks.get("subsuper", []) napostrophes = len(toks.get("apostrophes", [])) @@ -2517,9 +2547,13 @@ def subsuper(self, s, loc, toks): else: subkern = 0 + x: List if super is None: # node757 - x = Hlist([Kern(subkern), sub]) + # Note: One of super or sub must be a Node if we're in this function, but + # mypy can't know this, since it can't interpret pyparsing expressions, + # hence the cast. + x = Hlist([Kern(subkern), T.cast(Node, sub)]) x.shrink() if self.is_dropsub(last_char): shift_down = lc_baseline + constants.subdrop * xHeight @@ -2566,7 +2600,8 @@ def subsuper(self, s, loc, toks): result = Hlist(spaced_nucleus) return [result] - def _genfrac(self, ldelim, rdelim, rule, style, num, den): + def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathStyle, + num: Hlist, den: Hlist) -> T.Any: state = self.get_state() thickness = state.get_current_underline_thickness() @@ -2601,34 +2636,37 @@ def _genfrac(self, ldelim, rdelim, rule, style, num, den): ldelim = '.' if rdelim == '': rdelim = '.' - return self._auto_sized_delimiter(ldelim, result, rdelim) + return self._auto_sized_delimiter(ldelim, + T.cast(list[T.Union[Box, Char, str]], + result), + rdelim) return result - def style_literal(self, toks): + def style_literal(self, toks: ParseResults) -> T.Any: return self._MathStyle(int(toks["style_literal"])) - def genfrac(self, toks): + def genfrac(self, toks: ParseResults) -> T.Any: return self._genfrac( toks.get("ldelim", ""), toks.get("rdelim", ""), toks["rulesize"], toks.get("style", self._MathStyle.TEXTSTYLE), toks["num"], toks["den"]) - def frac(self, toks): + def frac(self, toks: ParseResults) -> T.Any: return self._genfrac( "", "", self.get_state().get_current_underline_thickness(), self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) - def dfrac(self, toks): + def dfrac(self, toks: ParseResults) -> T.Any: return self._genfrac( "", "", self.get_state().get_current_underline_thickness(), self._MathStyle.DISPLAYSTYLE, toks["num"], toks["den"]) - def binom(self, toks): + def binom(self, toks: ParseResults) -> T.Any: return self._genfrac( "(", ")", 0, self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) - def _genset(self, s, loc, toks): + def _genset(self, s: str, loc: int, toks: ParseResults) -> T.Any: annotation = toks["annotation"] body = toks["body"] thickness = self.get_state().get_current_underline_thickness() @@ -2660,7 +2698,7 @@ def _genset(self, s, loc, toks): overset = underset = _genset - def sqrt(self, toks): + def sqrt(self, toks: ParseResults) -> T.Any: root = toks.get("root") body = toks["value"] state = self.get_state() @@ -2700,7 +2738,7 @@ def sqrt(self, toks): rightside]) # Body return [hlist] - def overline(self, toks): + def overline(self, toks: ParseResults) -> T.Any: body = toks["body"] state = self.get_state() @@ -2719,7 +2757,9 @@ def overline(self, toks): hlist = Hlist([rightside]) return [hlist] - def _auto_sized_delimiter(self, front, middle, back): + def _auto_sized_delimiter(self, front: str, + middle: list[Box | Char | str], + back: str) -> T.Any: state = self.get_state() if len(middle): height = max([x.height for x in middle if not isinstance(x, str)]) @@ -2727,41 +2767,45 @@ def _auto_sized_delimiter(self, front, middle, back): factor = None for idx, el in enumerate(middle): if isinstance(el, str) and el == '\\middle': - c = middle[idx + 1] + c = T.cast(str, middle[idx + 1]) # Should be one of p.delims. if c != '.': middle[idx + 1] = AutoHeightChar( c, height, depth, state, factor=factor) else: middle.remove(c) del middle[idx] + # There should only be \middle and its delimiter as str, which have + # just been removed. + middle_part = T.cast(list[T.Union[Box, Char]], middle) else: height = 0 depth = 0 factor = 1.0 + middle_part = [] - parts = [] + parts: list[Node] = [] # \left. and \right. aren't supposed to produce any symbols if front != '.': parts.append( AutoHeightChar(front, height, depth, state, factor=factor)) - parts.extend(middle) + parts.extend(middle_part) if back != '.': parts.append( AutoHeightChar(back, height, depth, state, factor=factor)) hlist = Hlist(parts) return hlist - def auto_delim(self, toks): + def auto_delim(self, toks: ParseResults) -> T.Any: return self._auto_sized_delimiter( toks["left"], # if "mid" in toks ... can be removed when requiring pyparsing 3. toks["mid"].asList() if "mid" in toks else [], toks["right"]) - def boldsymbol(self, toks): + def boldsymbol(self, toks: ParseResults) -> T.Any: self.push_state() state = self.get_state() - hlist = [] + hlist: list[Node] = [] name = toks["value"] for c in name: if isinstance(c, Hlist): @@ -2784,7 +2828,7 @@ def boldsymbol(self, toks): return Hlist(hlist) - def substack(self, toks): + def substack(self, toks: ParseResults) -> T.Any: parts = toks["parts"] state = self.get_state() thickness = state.get_current_underline_thickness() diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index d37cdff22e57..8f413b2a1673 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -2,6 +2,9 @@ font data tables for truetype and afm computer modern fonts """ +from __future__ import annotations + + latex_to_bakoma = { '\\__sqrt__' : ('cmex10', 0x70), '\\bigcap' : ('cmex10', 0x5c), @@ -1099,7 +1102,8 @@ # Each element is a 4-tuple of the form: # src_start, src_end, dst_font, dst_start # -stix_virtual_fonts = { +stix_virtual_fonts: dict[str, dict[str, list[tuple[int, int, str, int]]] | + list[tuple[int, int, str, int]]] = { 'bb': { 'rm': From 2293d6595798ee6448790d70b02a93abdc4bed99 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Aug 2023 07:08:04 -0400 Subject: [PATCH 7/7] TYP: Add overloads for MathTextParser return types --- lib/matplotlib/backends/backend_agg.py | 2 +- lib/matplotlib/mathtext.pyi | 15 ++++++++++----- lib/matplotlib/textpath.pyi | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 9008096253c6..470ce9d925ad 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -71,7 +71,7 @@ def __init__(self, width, height, dpi): self._filter_renderers = [] self._update_methods() - self.mathtext_parser = MathTextParser('Agg') + self.mathtext_parser = MathTextParser('agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) diff --git a/lib/matplotlib/mathtext.pyi b/lib/matplotlib/mathtext.pyi index 706f83a6b168..607501a275c6 100644 --- a/lib/matplotlib/mathtext.pyi +++ b/lib/matplotlib/mathtext.pyi @@ -1,5 +1,8 @@ import os +from typing import Generic, IO, Literal, TypeVar, overload + from matplotlib.font_manager import FontProperties +from matplotlib.typing import ColorType # Re-exported API from _mathtext. from ._mathtext import ( @@ -8,14 +11,16 @@ from ._mathtext import ( get_unicode_index as get_unicode_index, ) -from typing import IO, Literal -from matplotlib.typing import ColorType +_ParseType = TypeVar("_ParseType", RasterParse, VectorParse) -class MathTextParser: - def __init__(self, output: Literal["path", "agg", "raster", "macosx"]) -> None: ... +class MathTextParser(Generic[_ParseType]): + @overload + def __init__(self: MathTextParser[VectorParse], output: Literal["path"]) -> None: ... + @overload + def __init__(self: MathTextParser[RasterParse], output: Literal["agg", "raster", "macosx"]) -> None: ... def parse( self, s: str, dpi: float = ..., prop: FontProperties | None = ..., *, antialiased: bool | None = ... - ) -> RasterParse | VectorParse: ... + ) -> _ParseType: ... def math_to_image( s: str, diff --git a/lib/matplotlib/textpath.pyi b/lib/matplotlib/textpath.pyi index 6e49a3e8092d..34d4e92ac47e 100644 --- a/lib/matplotlib/textpath.pyi +++ b/lib/matplotlib/textpath.pyi @@ -1,6 +1,6 @@ from matplotlib.font_manager import FontProperties from matplotlib.ft2font import FT2Font -from matplotlib.mathtext import MathTextParser +from matplotlib.mathtext import MathTextParser, VectorParse from matplotlib.path import Path import numpy as np @@ -10,7 +10,7 @@ from typing import Literal class TextToPath: FONT_SCALE: float DPI: float - mathtext_parser: MathTextParser + mathtext_parser: MathTextParser[VectorParse] def __init__(self) -> None: ... def get_text_width_height_descent( self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"]