diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 5b4e8ed20db3..b23cb67116ed 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2,8 +2,10 @@ Implementation details for :mod:`.mathtext`. """ +from __future__ import annotations + +import abc import copy -from collections import namedtuple import enum import functools import logging @@ -12,6 +14,8 @@ import types import unicodedata import string +import typing as T +from typing import NamedTuple import numpy as np from pyparsing import ( @@ -25,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 @@ -34,6 +38,10 @@ else: from pyparsing import nested_expr +if T.TYPE_CHECKING: + from collections.abc import Iterable + from .ft2font import Glyph + ParserElement.enablePackrat() _log = logging.getLogger("matplotlib.mathtext") @@ -42,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*. @@ -64,24 +72,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: @@ -92,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) @@ -106,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], @@ -143,7 +176,49 @@ def to_raster(self, *, antialiased): return RasterParse(0, 0, w, h + d, d, image) -class Fonts: +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(abc.ABC): """ An abstract base class for a system of fonts to use for mathtext. @@ -152,7 +227,7 @@ class Fonts: 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 ---------- @@ -166,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*. @@ -175,7 +251,15 @@ def get_kern(self, font1, fontclass1, sym1, fontsize1, """ return 0. - def get_metrics(self, font, font_class, sym, fontsize, dpi): + 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: str, font_class: str, sym: str, fontsize: float, + dpi: float) -> FontMetrics: r""" Parameters ---------- @@ -197,25 +281,13 @@ 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 - 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). @@ -223,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 @@ -252,50 +326,59 @@ 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). """ - 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] = {} filename = findfont(self.default_font_prop) default_font = get_font(filename) 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. + 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): + 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) 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 +391,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, @@ -318,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') @@ -330,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) @@ -364,11 +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) - self.fontmap = {} + super().__init__(default_font_prop, load_glyph_flags) for key, val in self._fontmap.items(): fullpath = findfont(val) self.fontmap[key] = fullpath @@ -376,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] @@ -448,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)]) @@ -472,17 +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) - self.fontmap = {} + 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) @@ -508,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 @@ -573,24 +661,25 @@ 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) return [(fontname, sym)] -class DejaVuFonts(UnicodeFonts): +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.fontmap = {} + 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', @@ -604,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) @@ -668,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', @@ -683,35 +773,39 @@ 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) - self.fontmap = {} + 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. - 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) @@ -744,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}', @@ -810,33 +907,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): @@ -899,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 @@ -913,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.) @@ -980,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 @@ -992,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 == ' ': @@ -1005,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. @@ -1024,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 @@ -1044,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) @@ -1064,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) @@ -1095,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() @@ -1107,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. @@ -1138,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 @@ -1220,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. @@ -1307,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 @@ -1328,13 +1420,19 @@ 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) -_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), @@ -1355,17 +1453,20 @@ 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] + glue_spec = _GlueSpec._named[glue_type] # type: ignore[attr-defined] elif isinstance(glue_type, _GlueSpec): glue_spec = glue_type else: 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 @@ -1378,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) @@ -1388,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')]) @@ -1406,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 @@ -1428,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) @@ -1445,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) @@ -1467,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) @@ -1486,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`. @@ -1503,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 @@ -1531,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): @@ -1567,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 @@ -1591,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): @@ -1624,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 @@ -1632,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) @@ -1651,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. @@ -1689,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) @@ -1862,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 @@ -1880,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: @@ -1888,11 +1997,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") @@ -2036,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*. @@ -2045,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 = {} - self._expression.resetCache() - return result[0] + ParserElement.resetCache() + 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, s, loc, toks): - return [Hlist(toks)] + def main(self, toks: ParseResults) -> list[Hlist]: + return [Hlist(toks.asList())] - def math_string(self, s, loc, toks): + def math_string(self, toks: ParseResults) -> ParseResults: return self._math_expression.parseString(toks[0][1:-1], parseAll=True) - def math(self, s, loc, toks): - hlist = Hlist(toks) + def math(self, toks: ParseResults) -> T.Any: + hlist = Hlist(toks.asList()) self.pop_state() return [hlist] - def non_math(self, s, loc, 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) @@ -2092,7 +2204,7 @@ def non_math(self, s, loc, toks): float_literal = staticmethod(pyparsing_common.convertToFloat) - def text(self, s, loc, toks): + def text(self, toks: ParseResults) -> T.Any: self.push_state() state = self.get_state() state.font = 'rm' @@ -2100,7 +2212,7 @@ def text(self, s, loc, 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 @@ -2132,15 +2244,16 @@ def _make_space(self, percentage): r'\!': -0.16667, # -3/18 em = -3 mu } - def space(self, s, loc, toks): + def space(self, toks: ParseResults) -> T.Any: num = self._space_widths[toks["space"]] box = self._make_space(num) return [box] - def customspace(self, s, loc, 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 @@ -2187,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 = { @@ -2215,11 +2328,12 @@ 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: 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) @@ -2236,16 +2350,16 @@ def accent(self, s, loc, 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: @@ -2276,54 +2390,51 @@ def operatorname(self, s, loc, toks): return Hlist(hlist_list) - def start_group(self, s, loc, 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, s, loc, toks): + def group(self, toks: ParseResults) -> T.Any: grp = Hlist(toks.get("group", [])) return [grp] - def required_group(self, s, loc, toks): + def required_group(self, toks: ParseResults) -> T.Any: return Hlist(toks.get("group", [])) optional_group = required_group - def end_group(self, s, loc, toks): + 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, s, loc, 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 is_between_brackets(self, s, loc): - 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", [])) @@ -2384,9 +2495,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 @@ -2436,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 @@ -2485,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() @@ -2520,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, s, loc, toks): + def style_literal(self, toks: ParseResults) -> T.Any: return self._MathStyle(int(toks["style_literal"])) - def genfrac(self, s, loc, 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, s, loc, 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, s, loc, 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, s, loc, 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() @@ -2579,7 +2698,7 @@ def _genset(self, s, loc, toks): overset = underset = _genset - def sqrt(self, s, loc, toks): + def sqrt(self, toks: ParseResults) -> T.Any: root = toks.get("root") body = toks["value"] state = self.get_state() @@ -2619,7 +2738,7 @@ def sqrt(self, s, loc, toks): rightside]) # Body return [hlist] - def overline(self, s, loc, toks): + def overline(self, toks: ParseResults) -> T.Any: body = toks["body"] state = self.get_state() @@ -2638,7 +2757,9 @@ def overline(self, s, loc, 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)]) @@ -2646,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, s, loc, 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, s, loc, 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): @@ -2703,24 +2828,24 @@ def boldsymbol(self, s, loc, toks): return Hlist(hlist) - def substack(self, s, loc, toks): + def substack(self, toks: ParseResults) -> T.Any: 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 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': 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"]