From eabb0eec0832261143bcd68813ca7841a2777f53 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Sun, 5 Jan 2025 20:45:45 +0100 Subject: [PATCH 01/21] Activated typechecking in mobject.text.* Error count: 156 --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index 4d4d586c98..c94a05b886 100644 --- a/mypy.ini +++ b/mypy.ini @@ -70,6 +70,9 @@ ignore_errors = True [mypy-manim.mobject.*] ignore_errors = True +[mypy-manim.mobject.text.*] +ignore_errors = False + [mypy-manim.mobject.geometry.*] ignore_errors = True From 866ea02a8adec4de033b527220cfb8b2c1d8bb7c Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Sun, 5 Jan 2025 22:59:45 +0100 Subject: [PATCH 02/21] Fixed typing errors in mobject/text/code_mobject.py --- manim/mobject/mobject.py | 2 +- manim/mobject/text/code_mobject.py | 57 +++++++++++++++++------------- mypy.ini | 2 +- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 8557490ed9..80b6fccf17 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -2291,7 +2291,7 @@ def __getitem__(self, value): def __iter__(self): return iter(self.split()) - def __len__(self): + def __len__(self) -> int: return len(self.split()) def get_group_class(self) -> type[Group]: diff --git a/manim/mobject/text/code_mobject.py b/manim/mobject/text/code_mobject.py index 999ab3c90e..b2875eca91 100644 --- a/manim/mobject/text/code_mobject.py +++ b/manim/mobject/text/code_mobject.py @@ -10,6 +10,7 @@ import os import re from pathlib import Path +from typing import TYPE_CHECKING import numpy as np from pygments import highlight, styles @@ -25,6 +26,12 @@ from manim.mobject.types.vectorized_mobject import VGroup from manim.utils.color import WHITE +if TYPE_CHECKING: + from typing import Any + + from manim.mobject.mobject import Mobject + from manim.utils.color import ManimColor, ParsableManimColor + class Code(VGroup): """A highlighted source code listing. @@ -172,7 +179,7 @@ def __init__( indentation_chars: str = " ", background: str = "rectangle", # or window background_stroke_width: float = 1, - background_stroke_color: str = WHITE, + background_stroke_color: str | ParsableManimColor = WHITE, corner_radius: float = 0.2, insert_line_no: bool = True, line_no_from: int = 1, @@ -181,13 +188,13 @@ def __init__( language: str | None = None, generate_html_file: bool = False, warn_missing_font: bool = True, - **kwargs, - ): + **kwargs: Any, + ) -> None: super().__init__( stroke_width=stroke_width, **kwargs, ) - self.background_stroke_color = background_stroke_color + self.background_stroke_color = ManimColor(background_stroke_color) self.background_stroke_width = background_stroke_width self.tab_width = tab_width self.line_spacing = line_spacing @@ -209,7 +216,8 @@ def __init__( self.file_name = file_name if self.file_name: self._ensure_valid_file() - self.code_string = self.file_path.read_text(encoding="utf-8") + assert isinstance(self.file_path, Path) + self.code_string: str = self.file_path.read_text(encoding="utf-8") elif code: self.code_string = code else: @@ -242,7 +250,7 @@ def __init__( fill_opacity=1, ) rect.round_corners(self.corner_radius) - self.background_mobject = rect + self.background_mobject: Mobject = rect else: if self.insert_line_no: foreground = VGroup(self.code, self.line_numbers) @@ -251,7 +259,7 @@ def __init__( height = foreground.height + 0.1 * 3 + 2 * self.margin width = foreground.width + 0.1 * 3 + 2 * self.margin - rect = RoundedRectangle( + background_rect = RoundedRectangle( corner_radius=self.corner_radius, height=height, width=width, @@ -271,7 +279,7 @@ def __init__( + LEFT * (width / 2 - 0.1 * 5 - self.corner_radius / 2 - 0.05), ) - self.background_mobject = VGroup(rect, buttons) + self.background_mobject = VGroup(background_rect, buttons) x = (height - foreground.height) / 2 - 0.1 * 3 self.background_mobject.shift(foreground.get_center()) self.background_mobject.shift(UP * x) @@ -289,7 +297,7 @@ def __init__( self.move_to(np.array([0, 0, 0])) @classmethod - def get_styles_list(cls): + def get_styles_list(cls) -> list[str]: """Get list of available code styles. Returns @@ -302,7 +310,7 @@ def get_styles_list(cls): cls._styles_list_cache = list(styles.get_all_styles()) return cls._styles_list_cache - def _ensure_valid_file(self): + def _ensure_valid_file(self) -> None: """Function to validate file.""" if self.file_name is None: raise Exception("Must specify file for Code") @@ -320,7 +328,7 @@ def _ensure_valid_file(self): ) raise OSError(error) - def _gen_line_numbers(self): + def _gen_line_numbers(self) -> Paragraph: """Function to generate line_numbers. Returns @@ -346,7 +354,7 @@ def _gen_line_numbers(self): i.set_color(self.default_color) return line_numbers - def _gen_colored_lines(self): + def _gen_colored_lines(self) -> Paragraph: """Function to generate code. Returns @@ -381,7 +389,7 @@ def _gen_colored_lines(self): line_char_index += self.code_json[line_no][word_index][0].__len__() return code - def _gen_html_string(self): + def _gen_html_string(self) -> None: """Function to generate html string with code highlighted and stores in variable html_string.""" self.html_string = _hilite_me( self.code_string, @@ -398,7 +406,7 @@ def _gen_html_string(self): output_folder.mkdir(parents=True, exist_ok=True) (output_folder / f"{self.file_name}.html").write_text(self.html_string) - def _gen_code_json(self): + def _gen_code_json(self) -> None: """Function to background_color, generate code_json and tab_spaces from html_string. background_color is just background color of displayed code. code_json is 2d array with rows as line numbers @@ -412,7 +420,7 @@ def _gen_code_json(self): or self.background_color == "#202020" or self.background_color == "#000000" ): - self.default_color = "#ffffff" + self.default_color: str = "#ffffff" else: self.default_color = "#000000" # print(self.default_color,self.background_color) @@ -442,12 +450,13 @@ def _gen_code_json(self): start_point = lines[0].find(">") lines[0] = lines[0][start_point + 1 :] # print(lines) - self.code_json = [] - self.tab_spaces = [] + self.code_json: list[list[str]] = [] + self.tab_spaces: list[int] = [] code_json_line_index = -1 for line_index in range(0, lines.__len__()): # print(lines[line_index]) - self.code_json.append([]) + empty_list_of_strings: list[str] = [] + self.code_json.append(empty_list_of_strings) code_json_line_index = code_json_line_index + 1 if lines[line_index].startswith(self.indentation_chars): start_point = lines[line_index].find("<") @@ -498,7 +507,7 @@ def _gen_code_json(self): self.code_json[code_json_line_index].append([text, color]) # print(self.code_json) - def _correct_non_span(self, line_str: str): + def _correct_non_span(self, line_str: str) -> str: """Function put text color to those strings that don't have one according to background_color of displayed code. Parameters @@ -552,13 +561,13 @@ def _correct_non_span(self, line_str: str): def _hilite_me( code: str, - language: str, + language: str | None, style: str, insert_line_no: bool, divstyles: str, - file_path: Path, + file_path: Path | None, line_no_from: int, -): +) -> str: """Function to highlight code from string to html. Parameters @@ -591,7 +600,7 @@ def _hilite_me( ) if language is None and file_path: lexer = guess_lexer_for_filename(file_path, code) - html = highlight(code, lexer, formatter) + html: str = highlight(code, lexer, formatter) elif language is None: raise ValueError( "The code language has to be specified when rendering a code string", @@ -604,7 +613,7 @@ def _hilite_me( return html -def _insert_line_numbers_in_html(html: str, line_no_from: int): +def _insert_line_numbers_in_html(html: str, line_no_from: int) -> str: """Function that inserts line numbers in the highlighted HTML code. Parameters diff --git a/mypy.ini b/mypy.ini index c94a05b886..7ceed72dc0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -70,7 +70,7 @@ ignore_errors = True [mypy-manim.mobject.*] ignore_errors = True -[mypy-manim.mobject.text.*] +[mypy-manim.mobject.text.code_mobject.py] ignore_errors = False [mypy-manim.mobject.geometry.*] From 952b302e11d75551acbb07b96aa874dddf2fe273 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 6 Jan 2025 09:04:16 +0100 Subject: [PATCH 03/21] Add type annotations to mobject/text/numbers.py --- manim/mobject/text/code_mobject.py | 5 ++- manim/mobject/text/numbers.py | 46 +++++++++++++---------- manim/mobject/types/vectorized_mobject.py | 2 +- manim/mobject/value_tracker.py | 4 +- mypy.ini | 5 ++- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/manim/mobject/text/code_mobject.py b/manim/mobject/text/code_mobject.py index b2875eca91..7964b2acad 100644 --- a/manim/mobject/text/code_mobject.py +++ b/manim/mobject/text/code_mobject.py @@ -504,7 +504,10 @@ def _gen_code_json(self) -> None: text = html.unescape(text) if text != "": # print(text, "'" + color + "'") - self.code_json[code_json_line_index].append([text, color]) + # TODO + # Fix the mypy error from the next line of code. + # "Argument 1 to "append" of "list" has incompatible type "list[str]"; expected "str" [arg-type]" + self.code_json[code_json_line_index].append([text, color]) # type: ignore[arg-type] # print(self.code_json) def _correct_non_span(self, line_str: str) -> str: diff --git a/manim/mobject/text/numbers.py b/manim/mobject/text/numbers.py index 1c74cf5f0a..0b5b27a820 100644 --- a/manim/mobject/text/numbers.py +++ b/manim/mobject/text/numbers.py @@ -8,6 +8,7 @@ from typing import Any import numpy as np +from typing_extensions import Self from manim import config from manim.constants import * @@ -17,7 +18,7 @@ from manim.mobject.types.vectorized_mobject import VMobject from manim.mobject.value_tracker import ValueTracker -string_to_mob_map = {} +string_to_mob_map: dict[str, SingleStringMathTex] = {} __all__ = ["DecimalNumber", "Integer", "Variable"] @@ -86,7 +87,7 @@ def __init__( self, number: float = 0, num_decimal_places: int = 2, - mob_class: VMobject = MathTex, + mob_class: type[VMobject] = MathTex, include_sign: bool = False, group_with_commas: bool = True, digit_buff_per_font_unit: float = 0.001, @@ -98,8 +99,8 @@ def __init__( font_size: float = DEFAULT_FONT_SIZE, stroke_width: float = 0, fill_opacity: float = 1.0, - **kwargs, - ): + **kwargs: Any, + ) -> None: super().__init__(**kwargs, stroke_width=stroke_width) self.number = number self.num_decimal_places = num_decimal_places @@ -137,12 +138,12 @@ def __init__( self.init_colors() @property - def font_size(self): + def font_size(self) -> float: """The font size of the tex mobject.""" return self.height / self.initial_height * self._font_size @font_size.setter - def font_size(self, font_val): + def font_size(self, font_val: float) -> None: if font_val <= 0: raise ValueError("font_size must be greater than 0.") elif self.height > 0: @@ -153,7 +154,7 @@ def font_size(self, font_val): # font_size does not depend on current size. self.scale(font_val / self.font_size) - def _set_submobjects_from_number(self, number): + def _set_submobjects_from_number(self, number: float) -> None: self.number = number self.submobjects = [] @@ -197,12 +198,12 @@ def _set_submobjects_from_number(self, number): self.unit_sign.align_to(self, UP) # track the initial height to enable scaling via font_size - self.initial_height = self.height + self.initial_height: float = self.height if self.include_background_rectangle: self.add_background_rectangle() - def _get_num_string(self, number): + def _get_num_string(self, number: float | complex) -> str: if isinstance(number, complex): formatter = self._get_complex_formatter() else: @@ -215,17 +216,22 @@ def _get_num_string(self, number): return num_string - def _string_to_mob(self, string: str, mob_class: VMobject | None = None, **kwargs): + def _string_to_mob( + self, string: str, mob_class: type[VMobject] | None = None, **kwargs: Any + ) -> VMobject: if mob_class is None: mob_class = self.mob_class if string not in string_to_mob_map: - string_to_mob_map[string] = mob_class(string, **kwargs) + # TODO: I am not sure about the type of mob_class. + # I think it should be SingleStringMathTex, as that class has the _fontsize property. + # But this seems to conflict with the default case, where it is set to VMobject. + string_to_mob_map[string] = mob_class(string, **kwargs) # type: ignore[assignment] mob = string_to_mob_map[string].copy() mob.font_size = self._font_size return mob - def _get_formatter(self, **kwargs): + def _get_formatter(self, **kwargs: Any) -> str: """ Configuration is based first off instance attributes, but overwritten by any kew word argument. Relevant @@ -258,7 +264,7 @@ def _get_formatter(self, **kwargs): ], ) - def _get_complex_formatter(self): + def _get_complex_formatter(self) -> str: return "".join( [ self._get_formatter(field_name="0.real"), @@ -267,7 +273,7 @@ def _get_complex_formatter(self): ], ) - def set_value(self, number: float): + def set_value(self, number: float) -> Self: """Set the value of the :class:`~.DecimalNumber` to a new number. Parameters @@ -304,10 +310,10 @@ def set_value(self, number: float): self.init_colors() return self - def get_value(self): + def get_value(self) -> float: return self.number - def increment_value(self, delta_t=1): + def increment_value(self, delta_t: float = 1) -> None: self.set_value(self.get_value() + delta_t) @@ -333,7 +339,7 @@ def __init__( ) -> None: super().__init__(number=number, num_decimal_places=num_decimal_places, **kwargs) - def get_value(self): + def get_value(self) -> int: return int(np.round(super().get_value())) @@ -444,10 +450,10 @@ def __init__( self, var: float, label: str | Tex | MathTex | Text | SingleStringMathTex, - var_type: DecimalNumber | Integer = DecimalNumber, + var_type: type[DecimalNumber] | type[Integer] = DecimalNumber, num_decimal_places: int = 2, - **kwargs, - ): + **kwargs: Any, + ) -> None: self.label = MathTex(label) if isinstance(label, str) else label equals = MathTex("=").next_to(self.label, RIGHT) self.label.add(equals) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index c407dd6a27..0d1305246e 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -628,7 +628,7 @@ def get_color(self) -> ManimColor: return self.get_stroke_color() return self.get_fill_color() - color = property(get_color, set_color) + color: ManimColor = property(get_color, set_color) def set_sheen_direction(self, direction: Vector3D, family: bool = True) -> Self: """Sets the direction of the applied sheen. diff --git a/manim/mobject/value_tracker.py b/manim/mobject/value_tracker.py index 9d81035e89..17cee498f9 100644 --- a/manim/mobject/value_tracker.py +++ b/manim/mobject/value_tracker.py @@ -5,6 +5,8 @@ __all__ = ["ValueTracker", "ComplexValueTracker"] +from typing import Any + import numpy as np from manim.mobject.mobject import Mobject @@ -69,7 +71,7 @@ def construct(self): """ - def __init__(self, value=0, **kwargs): + def __init__(self, value: float = 0, **kwargs: Any) -> None: super().__init__(**kwargs) self.set(points=np.zeros((1, 3))) self.set_value(value) diff --git a/mypy.ini b/mypy.ini index 7ceed72dc0..70bd85cf21 100644 --- a/mypy.ini +++ b/mypy.ini @@ -70,7 +70,10 @@ ignore_errors = True [mypy-manim.mobject.*] ignore_errors = True -[mypy-manim.mobject.text.code_mobject.py] +[mypy-manim.mobject.text.code_mobject.*] +ignore_errors = False + +[mypy-manim.mobject.text.numbers.*] ignore_errors = False [mypy-manim.mobject.geometry.*] From c6f7059443cec04066f1f22a7b9b26b8a4909e86 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 6 Jan 2025 09:18:06 +0100 Subject: [PATCH 04/21] Import ManimColor on every run --- manim/mobject/text/code_mobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/mobject/text/code_mobject.py b/manim/mobject/text/code_mobject.py index 7964b2acad..9bf6169971 100644 --- a/manim/mobject/text/code_mobject.py +++ b/manim/mobject/text/code_mobject.py @@ -24,13 +24,13 @@ from manim.mobject.geometry.shape_matchers import SurroundingRectangle from manim.mobject.text.text_mobject import Paragraph from manim.mobject.types.vectorized_mobject import VGroup -from manim.utils.color import WHITE +from manim.utils.color import WHITE, ManimColor if TYPE_CHECKING: from typing import Any from manim.mobject.mobject import Mobject - from manim.utils.color import ManimColor, ParsableManimColor + from manim.utils.color import ParsableManimColor class Code(VGroup): From 648277a01f129baf32a9ffa94f7913bc69efd89d Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 6 Jan 2025 19:45:17 +0100 Subject: [PATCH 05/21] Add type annotations to mobject/text/tex_mobject.py --- manim/mobject/svg/svg_mobject.py | 3 +- manim/mobject/text/tex_mobject.py | 155 +++++++++++++++++++----------- mypy.ini | 3 + 3 files changed, 103 insertions(+), 58 deletions(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 82c121fce7..4728946048 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -10,6 +10,7 @@ import svgelements as se from manim import config, logger +from manim.utils.color.core import ParsableManimColor from ...constants import RIGHT from ...utils.bezier import get_quadratic_approximation_of_cubic @@ -98,7 +99,7 @@ def __init__( should_center: bool = True, height: float | None = 2, width: float | None = None, - color: str | None = None, + color: ParsableManimColor | None = None, opacity: float | None = None, fill_color: str | None = None, fill_opacity: float | None = None, diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 26334a60d9..4d8a1b7d9b 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -12,7 +12,7 @@ from __future__ import annotations -from manim.utils.color import BLACK, ManimColor, ParsableManimColor +from manim.utils.color import BLACK, ParsableManimColor __all__ = [ "SingleStringMathTex", @@ -26,9 +26,12 @@ import itertools as it import operator as op import re -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from functools import reduce from textwrap import dedent +from typing import Any + +from typing_extensions import Self from manim import config, logger from manim.constants import * @@ -38,7 +41,7 @@ from manim.utils.tex import TexTemplate from manim.utils.tex_file_writing import tex_to_svg_file -tex_string_to_mob_map = {} +tex_string_to_mob_map: dict[str, VMobject] = {} class SingleStringMathTex(SVGMobject): @@ -63,8 +66,8 @@ def __init__( tex_template: TexTemplate | None = None, font_size: float = DEFAULT_FONT_SIZE, color: ParsableManimColor | None = None, - **kwargs, - ): + **kwargs: Any, + ) -> None: if color is None: color = VMobject().color @@ -105,16 +108,16 @@ def __init__( if self.organize_left_to_right: self._organize_submobjects_left_to_right() - def __repr__(self): + def __repr__(self) -> str: return f"{type(self).__name__}({repr(self.tex_string)})" @property - def font_size(self): + def font_size(self) -> float: """The font size of the tex mobject.""" return self.height / self.initial_height / SCALE_FACTOR_PER_FONT_POINT @font_size.setter - def font_size(self, font_val): + def font_size(self, font_val: float) -> None: if font_val <= 0: raise ValueError("font_size must be greater than 0.") elif self.height > 0: @@ -125,13 +128,13 @@ def font_size(self, font_val): # font_size does not depend on current size. self.scale(font_val / self.font_size) - def _get_modified_expression(self, tex_string): + def _get_modified_expression(self, tex_string: str) -> str: result = tex_string result = result.strip() result = self._modify_special_strings(result) return result - def _modify_special_strings(self, tex): + def _modify_special_strings(self, tex: str) -> str: tex = tex.strip() should_add_filler = reduce( op.or_, @@ -184,7 +187,7 @@ def _modify_special_strings(self, tex): tex = "" return tex - def _remove_stray_braces(self, tex): + def _remove_stray_braces(self, tex: str) -> str: r""" Makes :class:`~.MathTex` resilient to unmatched braces. @@ -202,14 +205,19 @@ def _remove_stray_braces(self, tex): num_rights += 1 return tex - def _organize_submobjects_left_to_right(self): + def _organize_submobjects_left_to_right(self) -> Self: self.sort(lambda p: p[0]) return self - def get_tex_string(self): + def get_tex_string(self) -> str: return self.tex_string - def init_colors(self, propagate_colors=True): + def init_colors(self, propagate_colors: bool = True) -> None: # type: ignore[override] + # TODO: + # The base class implementation of init_colors return "Self" + # this does not match well with the current implementation that + # return nothing. + # I see no reason for init_colors to return anything. for submobject in self.submobjects: # needed to preserve original (non-black) # TeX colors of individual submobjects @@ -255,21 +263,24 @@ def construct(self): def __init__( self, - *tex_strings, + *tex_strings: str, arg_separator: str = " ", substrings_to_isolate: Iterable[str] | None = None, - tex_to_color_map: dict[str, ManimColor] = None, + tex_to_color_map: dict[str, ParsableManimColor] | None = None, tex_environment: str = "align*", - **kwargs, - ): + **kwargs: Any, + ) -> None: self.tex_template = kwargs.pop("tex_template", config["tex_template"]) self.arg_separator = arg_separator self.substrings_to_isolate = ( [] if substrings_to_isolate is None else substrings_to_isolate ) - self.tex_to_color_map = tex_to_color_map + if self.tex_to_color_map is None: - self.tex_to_color_map = {} + self.tex_to_color_map: dict[str, ParsableManimColor] = {} + else: + assert isinstance(tex_to_color_map, dict) + self.tex_to_color_map = tex_to_color_map self.tex_environment = tex_environment self.brace_notation_split_occurred = False self.tex_strings = self._break_up_tex_strings(tex_strings) @@ -301,12 +312,14 @@ def __init__( if self.organize_left_to_right: self._organize_submobjects_left_to_right() - def _break_up_tex_strings(self, tex_strings): + def _break_up_tex_strings(self, tex_strings: Sequence[str]) -> list[str]: # Separate out anything surrounded in double braces pre_split_length = len(tex_strings) - tex_strings = [re.split("{{(.*?)}}", str(t)) for t in tex_strings] - tex_strings = sum(tex_strings, []) - if len(tex_strings) > pre_split_length: + # TODO: + # Give meaning full names to tex_strings_2 and tex_strings_3 + tex_strings_2 = [re.split("{{(.*?)}}", str(t)) for t in tex_strings] + tex_strings_3 = sum(tex_strings_2, []) + if len(tex_strings_3) > pre_split_length: self.brace_notation_split_occurred = True # Separate out any strings specified in the isolate @@ -324,19 +337,19 @@ def _break_up_tex_strings(self, tex_strings): pattern = "|".join(patterns) if pattern: pieces = [] - for s in tex_strings: + for s in tex_strings_3: pieces.extend(re.split(pattern, s)) else: - pieces = tex_strings + pieces = tex_strings_3 return [p for p in pieces if p] - def _break_up_by_substrings(self): + def _break_up_by_substrings(self) -> Self: """ Reorganize existing submobjects one layer deeper based on the structure of tex_strings (as a list of tex_strings) """ - new_submobjects = [] + new_submobjects: list[VMobject] = [] curr_index = 0 for tex_string in self.tex_strings: sub_tex_mob = SingleStringMathTex( @@ -358,8 +371,10 @@ def _break_up_by_substrings(self): self.submobjects = new_submobjects return self - def get_parts_by_tex(self, tex, substring=True, case_sensitive=True): - def test(tex1, tex2): + def get_parts_by_tex( + self, tex: str, substring: bool = True, case_sensitive: bool = True + ) -> VGroup: + def test(tex1: str, tex2: str) -> bool: if not case_sensitive: tex1 = tex1.lower() tex2 = tex2.lower() @@ -370,19 +385,25 @@ def test(tex1, tex2): return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string()))) - def get_part_by_tex(self, tex, **kwargs): + def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None: all_parts = self.get_parts_by_tex(tex, **kwargs) return all_parts[0] if all_parts else None - def set_color_by_tex(self, tex, color, **kwargs): + def set_color_by_tex( + self, tex: str, color: ParsableManimColor, **kwargs: Any + ) -> Self: parts_to_color = self.get_parts_by_tex(tex, **kwargs) for part in parts_to_color: part.set_color(color) return self def set_opacity_by_tex( - self, tex: str, opacity: float = 0.5, remaining_opacity: float = None, **kwargs - ): + self, + tex: str, + opacity: float = 0.5, + remaining_opacity: float | None = None, + **kwargs: Any, + ) -> Self: """ Sets the opacity of the tex specified. If 'remaining_opacity' is specified, then the remaining tex will be set to that opacity. @@ -403,7 +424,9 @@ def set_opacity_by_tex( part.set_opacity(opacity) return self - def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs): + def set_color_by_tex_to_color_map( + self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any + ) -> Self: for texs, color in list(texs_to_color_map.items()): try: # If the given key behaves like tex_strings @@ -415,17 +438,21 @@ def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs): self.set_color_by_tex(tex, color, **kwargs) return self - def index_of_part(self, part): + def index_of_part(self, part: MathTex) -> int: split_self = self.split() if part not in split_self: raise ValueError("Trying to get index of part not in MathTex") return split_self.index(part) - def index_of_part_by_tex(self, tex, **kwargs): + def index_of_part_by_tex(self, tex: str, **kwargs: Any) -> int: + # TODO: + # This part is tricky to type annotate. + # The issue is that self.get_part_by_tex return VGroup | None. + # But self.index_of_part only accepts a VGroup / MathTex as input. part = self.get_part_by_tex(tex, **kwargs) - return self.index_of_part(part) + return self.index_of_part(part) # type: ignore[arg-type] - def sort_alphabetically(self): + def sort_alphabetically(self) -> None: self.submobjects.sort(key=lambda m: m.get_tex_string()) @@ -447,8 +474,12 @@ class Tex(MathTex): """ def __init__( - self, *tex_strings, arg_separator="", tex_environment="center", **kwargs - ): + self, + *tex_strings: str, + arg_separator: str = "", + tex_environment: str = "center", + **kwargs: Any, + ) -> None: super().__init__( *tex_strings, arg_separator=arg_separator, @@ -477,18 +508,26 @@ def construct(self): def __init__( self, - *items, - buff=MED_LARGE_BUFF, - dot_scale_factor=2, - tex_environment=None, - **kwargs, - ): + *items: str, + buff: float = MED_LARGE_BUFF, + dot_scale_factor: float = 2, + # TODO: + # I am tempted to change the line to this + # tex_environment: str = "center", + # which matches the default tex_environment in the + # Tex class. + tex_environment: str | None = None, + **kwargs: Any, + ) -> None: self.buff = buff self.dot_scale_factor = dot_scale_factor - self.tex_environment = tex_environment + # TODO: See comment 10 lines above this + self.tex_environment = tex_environment # type: ignore[assignment] line_separated_items = [s + "\\\\" for s in items] super().__init__( - *line_separated_items, tex_environment=tex_environment, **kwargs + *line_separated_items, + tex_environment=tex_environment, # type: ignore[arg-type] + **kwargs, ) for part in self: dot = MathTex("\\cdot").scale(self.dot_scale_factor) @@ -496,10 +535,12 @@ def __init__( part.add_to_back(dot) self.arrange(DOWN, aligned_edge=LEFT, buff=self.buff) - def fade_all_but(self, index_or_string, opacity=0.5): + def fade_all_but(self, index_or_string: str | int, opacity: float = 0.5) -> None: arg = index_or_string if isinstance(arg, str): - part = self.get_part_by_tex(arg) + part: VGroup | VMobject | None = self.get_part_by_tex(arg) + if part is None: + raise Exception("Could not locate part by provided tex string") elif isinstance(arg, int): part = self.submobjects[arg] else: @@ -531,12 +572,12 @@ def construct(self): def __init__( self, - *text_parts, - include_underline=True, - match_underline_width_to_text=False, - underline_buff=MED_SMALL_BUFF, - **kwargs, - ): + *text_parts: str, + include_underline: bool = True, + match_underline_width_to_text: bool = False, + underline_buff: float = MED_SMALL_BUFF, + **kwargs: Any, + ) -> None: self.include_underline = include_underline self.match_underline_width_to_text = match_underline_width_to_text self.underline_buff = underline_buff diff --git a/mypy.ini b/mypy.ini index 70bd85cf21..94f718fc2c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -76,6 +76,9 @@ ignore_errors = False [mypy-manim.mobject.text.numbers.*] ignore_errors = False +[mypy-manim.mobject.text.tex_mobject.*] +ignore_errors = False + [mypy-manim.mobject.geometry.*] ignore_errors = True From f56effd8d0b19700d6541bac4c25d547259ae9fa Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 6 Jan 2025 19:48:15 +0100 Subject: [PATCH 06/21] Return the object itself from init_colors --- manim/mobject/text/tex_mobject.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 4d8a1b7d9b..7848a295c6 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -212,12 +212,7 @@ def _organize_submobjects_left_to_right(self) -> Self: def get_tex_string(self) -> str: return self.tex_string - def init_colors(self, propagate_colors: bool = True) -> None: # type: ignore[override] - # TODO: - # The base class implementation of init_colors return "Self" - # this does not match well with the current implementation that - # return nothing. - # I see no reason for init_colors to return anything. + def init_colors(self, propagate_colors: bool = True) -> Self: for submobject in self.submobjects: # needed to preserve original (non-black) # TeX colors of individual submobjects @@ -228,6 +223,7 @@ def init_colors(self, propagate_colors: bool = True) -> None: # type: ignore[ov submobject.init_colors() elif config.renderer == RendererType.CAIRO: submobject.init_colors(propagate_colors=propagate_colors) + return self class MathTex(SingleStringMathTex): From e76e53b403dd2731580381a7817a8761730e5939 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 6 Jan 2025 19:58:16 +0100 Subject: [PATCH 07/21] Fix issue. --- manim/mobject/text/tex_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 7848a295c6..7b09ec783a 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -272,7 +272,7 @@ def __init__( [] if substrings_to_isolate is None else substrings_to_isolate ) - if self.tex_to_color_map is None: + if tex_to_color_map is None: self.tex_to_color_map: dict[str, ParsableManimColor] = {} else: assert isinstance(tex_to_color_map, dict) From 94382c5e1a2b2490277efd8b5043e9081d743e42 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 7 Jan 2025 22:29:31 +0100 Subject: [PATCH 08/21] Add type annotations to mobject/text/text_mobject.py --- manim/mobject/text/text_mobject.py | 180 +++++++++++++++++------------ mypy.ini | 3 + 2 files changed, 111 insertions(+), 72 deletions(-) diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index ef14267891..27f57a6fd9 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -57,10 +57,11 @@ def construct(self): import copy import hashlib import re -from collections.abc import Iterable, Sequence +from collections.abc import Generator, Iterable, Sequence from contextlib import contextmanager from itertools import chain from pathlib import Path +from typing import TYPE_CHECKING import manimpango import numpy as np @@ -74,6 +75,13 @@ def construct(self): from manim.utils.color import ManimColor, ParsableManimColor, color_gradient from manim.utils.deprecation import deprecated +if TYPE_CHECKING: + from typing import Any + + from typing_extensions import Self + + from manim.typing import Point3D + TEXT_MOB_SCALE_FACTOR = 0.05 DEFAULT_LINE_SPACING_SCALE = 0.3 TEXT2SVG_ADJUSTMENT_FACTOR = 4.8 @@ -81,7 +89,7 @@ def construct(self): __all__ = ["Text", "Paragraph", "MarkupText", "register_font"] -def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject: +def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject | VGroup: """Function to remove unwanted invisible characters from some mobjects. Parameters @@ -101,7 +109,9 @@ def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject: elif mobject.__class__.__name__ == "Code": iscode = True code = mobject - mobject = mobject.code + # TODO: + # error: Incompatible types in assignment (expression has type "MethodType", variable has type "SVGMobject") [assignment] + mobject = mobject.code # type: ignore[assignment] mobject_without_dots = VGroup() if mobject[0].__class__ == VGroup: for i in range(len(mobject)): @@ -110,7 +120,9 @@ def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject: else: mobject_without_dots.add(*(k for k in mobject if k.__class__ != Dot)) if iscode: - code.code = mobject_without_dots + # TODO: + # error: "SVGMobject" has no attribute "code" [attr-defined] + code.code = mobject_without_dots # type: ignore[attr-defined] return code return mobject_without_dots @@ -155,10 +167,10 @@ class Paragraph(VGroup): def __init__( self, - *text: Sequence[str], + *text: str, line_spacing: float = -1, alignment: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: self.line_spacing = line_spacing self.alignment = alignment @@ -420,7 +432,7 @@ def construct(self): @staticmethod @functools.cache def font_list() -> list[str]: - return manimpango.list_fonts() + return manimpango.list_fonts() # type: ignore[no-any-return] def __init__( self, @@ -433,21 +445,21 @@ def __init__( font: str = "", slant: str = NORMAL, weight: str = NORMAL, - t2c: dict[str, str] = None, - t2f: dict[str, str] = None, - t2g: dict[str, tuple] = None, - t2s: dict[str, str] = None, - t2w: dict[str, str] = None, - gradient: tuple = None, + t2c: dict[str, str] | None = None, + t2f: dict[str, str] | None = None, + t2g: dict[str, tuple] | None = None, + t2s: dict[str, str] | None = None, + t2w: dict[str, str] | None = None, + gradient: tuple | None = None, tab_width: int = 4, warn_missing_font: bool = True, # Mobject - height: float = None, - width: float = None, + height: float | None = None, + width: float | None = None, should_center: bool = True, disable_ligatures: bool = False, use_svg_cache: bool = False, - **kwargs, + **kwargs: Any, ) -> None: self.line_spacing = line_spacing if font and warn_missing_font: @@ -489,6 +501,9 @@ def __init__( t2g = kwargs.pop("text2gradient", t2g) t2s = kwargs.pop("text2slant", t2s) t2w = kwargs.pop("text2weight", t2w) + assert isinstance(t2c, dict) + assert isinstance(t2g, dict) + self.t2c = {k: ManimColor(v).to_hex() for k, v in t2c.items()} self.t2f = t2f self.t2g = t2g @@ -508,8 +523,9 @@ def __init__( else: self.line_spacing = self._font_size + self._font_size * self.line_spacing - color: ManimColor = ManimColor(color) if color else VMobject().color - file_name = self._text2svg(color.to_hex()) + parsed_color: ManimColor = ManimColor(color) if color else VMobject().color + # TODO: Should _text2svg receive a str or a ManimColor? + file_name = self._text2svg(parsed_color.to_hex()) PangoUtils.remove_last_M(file_name) super().__init__( file_name, @@ -541,12 +557,12 @@ def __init__( # into a numpy array at the end, rather than creating # new numpy arrays every time a point or fixing line # is added (which is O(n^2) for numpy arrays). - closed_curve_points = [] + closed_curve_points: list[Point3D] = [] # OpenGL has points be part of quadratic Bezier curves; # Cairo uses cubic Bezier curves. if nppc == 3: # RendererType.OPENGL - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -557,7 +573,7 @@ def add_line_to(end): else: # RendererType.CAIRO - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -588,11 +604,11 @@ def add_line_to(end): self.scale(TEXT_MOB_SCALE_FACTOR) self.initial_height = self.height - def __repr__(self): + def __repr__(self) -> str: return f"Text({repr(self.original_text)})" @property - def font_size(self): + def font_size(self) -> float: return ( self.height / self.initial_height @@ -603,14 +619,14 @@ def font_size(self): ) @font_size.setter - def font_size(self, font_val): + def font_size(self, font_val: float) -> None: # TODO: use pango's font size scaling. if font_val <= 0: raise ValueError("font_size must be greater than 0.") else: self.scale(font_val / self.font_size) - def _gen_chars(self): + def _gen_chars(self) -> VGroup: chars = self.get_group_class()() submobjects_char_index = 0 for char_index in range(len(self.text)): @@ -628,7 +644,7 @@ def _gen_chars(self): submobjects_char_index += 1 return chars - def _find_indexes(self, word: str, text: str): + def _find_indexes(self, word: str, text: str) -> Sequence[tuple[int, int]]: """Finds the indexes of ``text`` in ``word``.""" temp = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word) if temp: @@ -636,7 +652,9 @@ def _find_indexes(self, word: str, text: str): end = int(temp.group(2)) if temp.group(2) != "" else len(text) start = len(text) + start if start < 0 else start end = len(text) + end if end < 0 else end - return [(start, end)] + return [ + (start, end), + ] indexes = [] index = text.find(word) while index != -1: @@ -649,19 +667,19 @@ def _find_indexes(self, word: str, text: str): until="v0.15.0", message="This was internal function, you shouldn't be using it anyway.", ) - def _set_color_by_t2c(self, t2c=None): + def _set_color_by_t2c(self, t2c: Any = None) -> None: """Sets color for specified strings.""" t2c = t2c if t2c else self.t2c for word, color in list(t2c.items()): - for start, end in self._find_indexes(word, self.text): - self.chars[start:end].set_color(color) + for for_start, for_end in self._find_indexes(word, self.text): + self.chars[for_start:for_end].set_color(color) @deprecated( since="v0.14.0", until="v0.15.0", message="This was internal function, you shouldn't be using it anyway.", ) - def _set_color_by_t2g(self, t2g=None): + def _set_color_by_t2g(self, t2g: Any = None) -> None: """Sets gradient colors for specified strings. Behaves similarly to ``set_color_by_t2c``. """ @@ -670,7 +688,7 @@ def _set_color_by_t2g(self, t2g=None): for start, end in self._find_indexes(word, self.text): self.chars[start:end].set_color_by_gradient(*gradient) - def _text2hash(self, color: ManimColor): + def _text2hash(self, color: ParsableManimColor) -> str: """Generates ``sha256`` hash for file name.""" settings = ( "PANGO" + self.font + self.slant + self.weight + str(color) @@ -713,7 +731,7 @@ def _get_settings_from_t2xs( self, t2xs: Sequence[tuple[dict[str, str], str]], default_args: dict[str, Iterable[str]], - ) -> Sequence[TextSetting]: + ) -> list[TextSetting]: settings = [] t2xwords = set(chain(*([*t2x.keys()] for t2x, _ in t2xs))) for word in t2xwords: @@ -736,25 +754,33 @@ def _get_settings_from_gradient( if self.gradient: colors = color_gradient(self.gradient, len(self.text)) for i in range(len(self.text)): - args["color"] = colors[i].to_hex() + # TODO: + # error: Item "float" of "ManimColor | float" has no attribute "to_hex" [union-attr] + args["color"] = colors[i].to_hex() # type: ignore[union-attr] settings.append(TextSetting(i, i + 1, **args)) for word, gradient in self.t2g.items(): if isinstance(gradient, str) or len(gradient) == 1: color = gradient if isinstance(gradient, str) else gradient[0] - gradient = [ManimColor(color)] + # TODO: + # error: Incompatible types in assignment (expression has type "list[ManimColor]", variable has type "tuple[Any, ...]") [assignment] + gradient = [ManimColor(color)] # type: ignore[assignment] colors = ( - color_gradient(gradient, len(word)) + # TODO: + # error: Incompatible types in assignment (expression has type "list[ManimColor] | ManimColor | tuple[Any, ...]", variable has type "list[ManimColor] | ManimColor") [assignment] + color_gradient(gradient, len(word)) # type: ignore[assignment] if len(gradient) != 1 else len(word) * gradient ) for start, end in self._find_indexes(word, self.text): for i in range(start, end): - args["color"] = colors[i - start].to_hex() + # TODO: + # error: Item "float" of "ManimColor | float" has no attribute "to_hex" [union-attr] + args["color"] = colors[i - start].to_hex() # type: ignore[union-attr] settings.append(TextSetting(i, i + 1, **args)) return settings - def _text2settings(self, color: str): + def _text2settings(self, color: ParsableManimColor) -> Sequence[TextSetting]: """Converts the texts and styles to a setting for parsing.""" t2xs = [ (self.t2f, "font"), @@ -768,8 +794,13 @@ def _text2settings(self, color: str): arg: getattr(self, arg) if arg != "color" else color for _, arg in t2xs } - settings = self._get_settings_from_t2xs(t2xs, default_args) - settings.extend(self._get_settings_from_gradient(default_args)) + # TODO: + # error: Argument 1 to "_get_settings_from_t2xs" of "Text" has incompatible type "list[tuple[dict[str, str] | None, str]]"; expected "Sequence[tuple[dict[str, str], str]]" [arg-type] + # error: Argument 2 to "_get_settings_from_t2xs" of "Text" has incompatible type "dict[str, Any | ManimColor | int | str | tuple[int, int, int] | tuple[float, float, float] | tuple[int, int, int, int] | tuple[float, float, float, float]]"; expected "dict[str, Iterable[str]]" [arg-type] + settings = self._get_settings_from_t2xs(t2xs, default_args) # type: ignore[arg-type] + # TODO: + # error: Argument 1 to "_get_settings_from_gradient" of "Text" has incompatible type "dict[str, Any | ManimColor | int | str | tuple[int, int, int] | tuple[float, float, float] | tuple[int, int, int, int] | tuple[float, float, float, float]]"; expected "dict[str, Iterable[str]]" [arg-type] + settings.extend(self._get_settings_from_gradient(default_args)) # type: ignore[arg-type] # Handle overlaps @@ -780,7 +811,9 @@ def _text2settings(self, color: str): next_setting = settings[index + 1] if setting.end > next_setting.start: - new_setting = self._merge_settings(setting, next_setting, default_args) + # TODO: + # error: Argument 3 to "_merge_settings" of "Text" has incompatible type "dict[str, Any | ManimColor | int | str | tuple[int, int, int] | tuple[float, float, float] | tuple[int, int, int, int] | tuple[float, float, float, float]]"; expected "dict[str, Iterable[str]]" [arg-type] + new_setting = self._merge_settings(setting, next_setting, default_args) # type: ignore[arg-type] new_index = index + 1 while ( new_index < len(settings) @@ -802,15 +835,15 @@ def _text2settings(self, color: str): line_num = 0 if re.search(r"\n", self.text): - for start, end in self._find_indexes("\n", self.text): + for for_start, for_end in self._find_indexes("\n", self.text): for setting in settings: if setting.line_num == -1: setting.line_num = line_num - if start < setting.end: + if for_start < setting.end: line_num += 1 new_setting = copy.copy(setting) - setting.end = end - new_setting.start = end + setting.end = for_end + new_setting.start = for_end new_setting.line_num = line_num settings.append(new_setting) settings.sort(key=lambda setting: setting.start) @@ -821,7 +854,7 @@ def _text2settings(self, color: str): return settings - def _text2svg(self, color: ManimColor): + def _text2svg(self, color: ParsableManimColor) -> str: """Convert the text to SVG using Pango.""" size = self._font_size line_spacing = self.line_spacing @@ -856,11 +889,12 @@ def _text2svg(self, color: ManimColor): return svg_file - def init_colors(self, propagate_colors=True): + def init_colors(self, propagate_colors: bool = True) -> Self: if config.renderer == RendererType.OPENGL: super().init_colors() elif config.renderer == RendererType.CAIRO: super().init_colors(propagate_colors=propagate_colors) + return self class MarkupText(SVGMobject): @@ -1164,7 +1198,9 @@ def construct(self): @staticmethod @functools.cache def font_list() -> list[str]: - return manimpango.list_fonts() + # TODO: + # Add type annotation to manimpango.list_fonts + return manimpango.list_fonts() # type: ignore[no-any-return] def __init__( self, @@ -1178,17 +1214,17 @@ def __init__( slant: str = NORMAL, weight: str = NORMAL, justify: bool = False, - gradient: tuple = None, + gradient: tuple | None = None, tab_width: int = 4, - height: int = None, - width: int = None, + height: int | None = None, + width: int | None = None, should_center: bool = True, disable_ligatures: bool = False, warn_missing_font: bool = True, - **kwargs, + **kwargs: Any, ) -> None: self.text = text - self.line_spacing = line_spacing + self.line_spacing: float = line_spacing if font and warn_missing_font: fonts_list = Text.font_list() # handle special case of sans/sans-serif @@ -1235,8 +1271,8 @@ def __init__( else: self.line_spacing = self._font_size + self._font_size * self.line_spacing - color: ManimColor = ManimColor(color) if color else VMobject().color - file_name = self._text2svg(color) + parsed_color: ManimColor = ManimColor(color) if color else VMobject().color + file_name = self._text2svg(parsed_color) PangoUtils.remove_last_M(file_name) super().__init__( @@ -1267,12 +1303,12 @@ def __init__( # into a numpy array at the end, rather than creating # new numpy arrays every time a point or fixing line # is added (which is O(n^2) for numpy arrays). - closed_curve_points = [] + closed_curve_points: list[Point3D] = [] # OpenGL has points be part of quadratic Bezier curves; # Cairo uses cubic Bezier curves. if nppc == 3: # RendererType.OPENGL - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -1283,7 +1319,7 @@ def add_line_to(end): else: # RendererType.CAIRO - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -1331,7 +1367,7 @@ def add_line_to(end): self.initial_height = self.height @property - def font_size(self): + def font_size(self) -> float: return ( self.height / self.initial_height @@ -1342,14 +1378,14 @@ def font_size(self): ) @font_size.setter - def font_size(self, font_val): + def font_size(self, font_val: float) -> None: # TODO: use pango's font size scaling. if font_val <= 0: raise ValueError("font_size must be greater than 0.") else: self.scale(font_val / self.font_size) - def _text2hash(self, color: ParsableManimColor): + def _text2hash(self, color: ParsableManimColor) -> str: """Generates ``sha256`` hash for file name.""" settings = ( "MARKUPPANGO" @@ -1366,11 +1402,11 @@ def _text2hash(self, color: ParsableManimColor): hasher.update(id_str.encode()) return hasher.hexdigest()[:16] - def _text2svg(self, color: ParsableManimColor | None): + def _text2svg(self, color: ParsableManimColor | None) -> str: """Convert the text to SVG using Pango.""" color = ManimColor(color) size = self._font_size - line_spacing = self.line_spacing + line_spacing: float = self.line_spacing size /= TEXT2SVG_ADJUSTMENT_FACTOR line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR @@ -1381,7 +1417,7 @@ def _text2svg(self, color: ParsableManimColor | None): file_name = dir_name / (hash_name + ".svg") if file_name.exists(): - svg_file = str(file_name.resolve()) + svg_file: str = str(file_name.resolve()) else: final_text = ( f'{self.text}' @@ -1407,7 +1443,7 @@ def _text2svg(self, color: ParsableManimColor | None): ) return svg_file - def _count_real_chars(self, s): + def _count_real_chars(self, s: str) -> int: """Counts characters that will be displayed. This is needed for partial coloring or gradients, because space @@ -1426,7 +1462,7 @@ def _count_real_chars(self, s): count += 1 return count - def _extract_gradient_tags(self): + def _extract_gradient_tags(self) -> list[dict[str, Any]]: """Used to determine which parts (if any) of the string should be formatted with a gradient. @@ -1437,7 +1473,7 @@ def _extract_gradient_tags(self): self.original_text, re.S, ) - gradientmap = [] + gradientmap: list[dict[str, Any]] = [] for tag in tags: start = self._count_real_chars(self.original_text[: tag.start(0)]) end = start + self._count_real_chars(tag.group(5)) @@ -1460,14 +1496,14 @@ def _extract_gradient_tags(self): ) return gradientmap - def _parse_color(self, col): + def _parse_color(self, col: str) -> ParsableManimColor: """Parse color given in ```` or ```` tags.""" if re.match("#[0-9a-f]{6}", col): return col else: return ManimColor(col).to_hex() - def _extract_color_tags(self): + def _extract_color_tags(self) -> list[dict[str, Any]]: """Used to determine which parts (if any) of the string should be formatted with a custom color. @@ -1482,7 +1518,7 @@ def _extract_color_tags(self): re.S, ) - colormap = [] + colormap: list[dict[str, Any]] = [] for tag in tags: start = self._count_real_chars(self.original_text[: tag.start(0)]) end = start + self._count_real_chars(tag.group(4)) @@ -1504,12 +1540,12 @@ def _extract_color_tags(self): ) return colormap - def __repr__(self): + def __repr__(self) -> str: return f"MarkupText({repr(self.original_text)})" @contextmanager -def register_font(font_file: str | Path): +def register_font(font_file: str | Path) -> Generator[None, None, None]: """Temporarily add a font file to Pango's search path. This searches for the font_file at various places. The order it searches it described below. diff --git a/mypy.ini b/mypy.ini index 94f718fc2c..8614514ecb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -79,6 +79,9 @@ ignore_errors = False [mypy-manim.mobject.text.tex_mobject.*] ignore_errors = False +[mypy-manim.mobject.text.text_mobject.*] +ignore_errors = False + [mypy-manim.mobject.geometry.*] ignore_errors = True From 19157a04b563920885a4ee7c8c0878f2bb6249ee Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 10 Jan 2025 09:13:01 +0100 Subject: [PATCH 09/21] Add type annotations to camera/camera.py --- manim/camera/camera.py | 192 +++++++++++++++------------ manim/mobject/text/tex_mobject.py | 3 + manim/mobject/types/image_mobject.py | 4 +- mypy.ini | 3 + 4 files changed, 118 insertions(+), 84 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index af5899c5c5..79c83e9de3 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -16,6 +16,9 @@ import numpy as np from PIL import Image from scipy.spatial.distance import pdist +from typing_extensions import Self + +from manim.typing import PixelArray from .. import config, logger from ..constants import * @@ -84,8 +87,8 @@ def __init__( frame_rate: float | None = None, background_color: ParsableManimColor | None = None, background_opacity: float | None = None, - **kwargs, - ): + **kwargs: Any, + ) -> None: self.background_image = background_image self.frame_center = frame_center self.image_mode = image_mode @@ -116,11 +119,13 @@ def __init__( self.frame_rate = frame_rate if background_color is None: - self._background_color = ManimColor.parse(config["background_color"]) + self._background_color: ManimColor = ManimColor.parse( + config["background_color"] + ) else: self._background_color = ManimColor.parse(background_color) if background_opacity is None: - self._background_opacity = config["background_opacity"] + self._background_opacity: float = config["background_opacity"] else: self._background_opacity = background_opacity @@ -129,7 +134,7 @@ def __init__( self.max_allowable_norm = config["frame_width"] self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max - self.pixel_array_to_cairo_context = {} + self.pixel_array_to_cairo_context: dict[int, cairo.Context] = {} # Contains the correct method to process a list of Mobjects of the # corresponding class. If a Mobject is not an instance of a class in @@ -140,7 +145,7 @@ def __init__( self.resize_frame_shape() self.reset() - def __deepcopy__(self, memo): + def __deepcopy__(self, memo: Any) -> Camera: # This is to address a strange bug where deepcopying # will result in a segfault, which is somehow related # to the aggdraw library @@ -148,24 +153,26 @@ def __deepcopy__(self, memo): return copy.copy(self) @property - def background_color(self): + def background_color(self) -> ManimColor: return self._background_color @background_color.setter - def background_color(self, color): + def background_color(self, color: ManimColor) -> None: self._background_color = color self.init_background() @property - def background_opacity(self): + def background_opacity(self) -> float: return self._background_opacity @background_opacity.setter - def background_opacity(self, alpha): + def background_opacity(self, alpha: float) -> None: self._background_opacity = alpha self.init_background() - def type_or_raise(self, mobject: Mobject): + def type_or_raise( + self, mobject: Mobject + ) -> type[VMobject] | type[PMobject] | type[AbstractImageMobject] | type[Mobject]: """Return the type of mobject, if it is a type that can be rendered. If `mobject` is an instance of a class that inherits from a class that @@ -206,7 +213,7 @@ def type_or_raise(self, mobject: Mobject): return _type raise TypeError(f"Displaying an object of class {_type} is not supported") - def reset_pixel_shape(self, new_height: float, new_width: float): + def reset_pixel_shape(self, new_height: float, new_width: float) -> None: """This method resets the height and width of a single pixel to the passed new_height and new_width. @@ -223,7 +230,7 @@ def reset_pixel_shape(self, new_height: float, new_width: float): self.resize_frame_shape() self.reset() - def resize_frame_shape(self, fixed_dimension: int = 0): + def resize_frame_shape(self, fixed_dimension: int = 0) -> None: """ Changes frame_shape to match the aspect ratio of the pixels, where fixed_dimension determines @@ -248,7 +255,7 @@ def resize_frame_shape(self, fixed_dimension: int = 0): self.frame_height = frame_height self.frame_width = frame_width - def init_background(self): + def init_background(self) -> None: """Initialize the background. If self.background_image is the path of an image the image is set as background; else, the default @@ -274,7 +281,9 @@ def init_background(self): ) self.background[:, :] = background_rgba - def get_image(self, pixel_array: np.ndarray | list | tuple | None = None): + def get_image( + self, pixel_array: PixelArray | list | tuple | None = None + ) -> PixelArray: """Returns an image from the passed pixel array, or from the current frame if the passed pixel array is none. @@ -290,12 +299,14 @@ def get_image(self, pixel_array: np.ndarray | list | tuple | None = None): The PIL image of the array. """ if pixel_array is None: - pixel_array = self.pixel_array + # TODO: + # error: Cannot determine type of "pixel_array" [has-type] + pixel_array = self.pixel_array # type: ignore[has-type] return Image.fromarray(pixel_array, mode=self.image_mode) def convert_pixel_array( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): + self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False + ) -> PixelArray: """Converts a pixel array from values that have floats in then to proper RGB values. @@ -321,8 +332,8 @@ def convert_pixel_array( return retval def set_pixel_array( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): + self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False + ) -> None: """Sets the pixel array of the camera to the passed pixel array. Parameters @@ -335,7 +346,9 @@ def set_pixel_array( converted_array = self.convert_pixel_array(pixel_array, convert_from_floats) if not ( hasattr(self, "pixel_array") - and self.pixel_array.shape == converted_array.shape + # TODO: + # error: Cannot determine type of "pixel_array" [has-type] + and self.pixel_array.shape == converted_array.shape # type: ignore[has-type] ): self.pixel_array = converted_array else: @@ -343,8 +356,8 @@ def set_pixel_array( self.pixel_array[:, :, :] = converted_array[:, :, :] def set_background( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): + self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False + ) -> None: """Sets the background to the passed pixel_array after converting to valid RGB values. @@ -360,7 +373,7 @@ def set_background( # TODO, this should live in utils, not as a method of Camera def make_background_from_func( self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray] - ): + ) -> PixelArray: """ Makes a pixel array for the background by using coords_to_colors_func to determine each pixel's color. Each input pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not @@ -386,7 +399,7 @@ def make_background_from_func( def set_background_from_func( self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray] - ): + ) -> None: """ Sets the background to a pixel array using coords_to_colors_func to determine each pixel's color. Each input pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not @@ -400,7 +413,7 @@ def set_background_from_func( """ self.set_background(self.make_background_from_func(coords_to_colors_func)) - def reset(self): + def reset(self) -> Self: """Resets the camera's pixel array to that of the background @@ -412,7 +425,7 @@ def reset(self): self.set_pixel_array(self.background) return self - def set_frame_to_background(self, background): + def set_frame_to_background(self, background: PixelArray) -> None: self.set_pixel_array(background) #### @@ -422,7 +435,7 @@ def get_mobjects_to_display( mobjects: Iterable[Mobject], include_submobjects: bool = True, excluded_mobjects: list | None = None, - ): + ) -> list[Mobject]: """Used to get the list of mobjects to display with the camera. @@ -454,7 +467,7 @@ def get_mobjects_to_display( mobjects = list_difference_update(mobjects, all_excluded) return list(mobjects) - def is_in_frame(self, mobject: Mobject): + def is_in_frame(self, mobject: Mobject) -> bool: """Checks whether the passed mobject is in frame or not. @@ -481,7 +494,7 @@ def is_in_frame(self, mobject: Mobject): ], ) - def capture_mobject(self, mobject: Mobject, **kwargs: Any): + def capture_mobject(self, mobject: Mobject, **kwargs: Any) -> None: """Capture mobjects by storing it in :attr:`pixel_array`. This is a single-mobject version of :meth:`capture_mobjects`. @@ -497,7 +510,7 @@ def capture_mobject(self, mobject: Mobject, **kwargs: Any): """ return self.capture_mobjects([mobject], **kwargs) - def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs): + def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None: """Capture mobjects by printing them on :attr:`pixel_array`. This is the essential function that converts the contents of a Scene @@ -525,14 +538,16 @@ def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs): # partition while at the same time preserving order. mobjects = self.get_mobjects_to_display(mobjects, **kwargs) for group_type, group in it.groupby(mobjects, self.type_or_raise): - self.display_funcs[group_type](list(group), self.pixel_array) + # TODO + # error: Call to untyped function (unknown) in typed context [no-untyped-call] + self.display_funcs[group_type](list(group), self.pixel_array) # type: ignore[no-untyped-call] # Methods associated with svg rendering # NOTE: None of the methods below have been mentioned outside of their definitions. Their DocStrings are not as # detailed as possible. - def get_cached_cairo_context(self, pixel_array: np.ndarray): + def get_cached_cairo_context(self, pixel_array: PixelArray) -> cairo.Context: """Returns the cached cairo context of the passed pixel array if it exists, and None if it doesn't. @@ -548,7 +563,7 @@ def get_cached_cairo_context(self, pixel_array: np.ndarray): """ return self.pixel_array_to_cairo_context.get(id(pixel_array), None) - def cache_cairo_context(self, pixel_array: np.ndarray, ctx: cairo.Context): + def cache_cairo_context(self, pixel_array: PixelArray, ctx: cairo.Context) -> None: """Caches the passed Pixel array into a Cairo Context Parameters @@ -560,7 +575,7 @@ def cache_cairo_context(self, pixel_array: np.ndarray, ctx: cairo.Context): """ self.pixel_array_to_cairo_context[id(pixel_array)] = ctx - def get_cairo_context(self, pixel_array: np.ndarray): + def get_cairo_context(self, pixel_array: PixelArray) -> cairo.Context: """Returns the cairo context for a pixel array after caching it to self.pixel_array_to_cairo_context If that array has already been cached, it returns the @@ -606,8 +621,8 @@ def get_cairo_context(self, pixel_array: np.ndarray): return ctx def display_multiple_vectorized_mobjects( - self, vmobjects: list, pixel_array: np.ndarray - ): + self, vmobjects: list[VMobject], pixel_array: PixelArray + ) -> None: """Displays multiple VMobjects in the pixel_array Parameters @@ -630,8 +645,8 @@ def display_multiple_vectorized_mobjects( ) def display_multiple_non_background_colored_vmobjects( - self, vmobjects: list, pixel_array: np.ndarray - ): + self, vmobjects: Iterable[VMobject], pixel_array: PixelArray + ) -> None: """Displays multiple VMobjects in the cairo context, as long as they don't have background colors. @@ -646,7 +661,7 @@ def display_multiple_non_background_colored_vmobjects( for vmobject in vmobjects: self.display_vectorized(vmobject, ctx) - def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context): + def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context) -> Self: """Displays a VMobject in the cairo context Parameters @@ -667,7 +682,7 @@ def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context): self.apply_stroke(ctx, vmobject) return self - def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject): + def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject) -> Self: """Sets a path for the cairo context with the vmobject passed Parameters @@ -686,7 +701,9 @@ def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject): # TODO, shouldn't this be handled in transform_points_pre_display? # points = points - self.get_frame_center() if len(points) == 0: - return + # TODO: + # Here the return value is modified. Is that ok? + return self ctx.new_path() subpaths = vmobject.gen_subpaths_from_points_2d(points) @@ -702,8 +719,8 @@ def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject): return self def set_cairo_context_color( - self, ctx: cairo.Context, rgbas: np.ndarray, vmobject: VMobject - ): + self, ctx: cairo.Context, rgbas: PixelArray, vmobject: VMobject + ) -> Self: """Sets the color of the cairo context Parameters @@ -735,7 +752,7 @@ def set_cairo_context_color( ctx.set_source(pat) return self - def apply_fill(self, ctx: cairo.Context, vmobject: VMobject): + def apply_fill(self, ctx: cairo.Context, vmobject: VMobject) -> Self: """Fills the cairo context Parameters @@ -756,7 +773,7 @@ def apply_fill(self, ctx: cairo.Context, vmobject: VMobject): def apply_stroke( self, ctx: cairo.Context, vmobject: VMobject, background: bool = False - ): + ) -> Self: """Applies a stroke to the VMobject in the cairo context. Parameters @@ -795,7 +812,9 @@ def apply_stroke( ctx.stroke_preserve() return self - def get_stroke_rgbas(self, vmobject: VMobject, background: bool = False): + def get_stroke_rgbas( + self, vmobject: VMobject, background: bool = False + ) -> PixelArray: """Gets the RGBA array for the stroke of the passed VMobject. @@ -814,7 +833,7 @@ def get_stroke_rgbas(self, vmobject: VMobject, background: bool = False): """ return vmobject.get_stroke_rgbas(background) - def get_fill_rgbas(self, vmobject: VMobject): + def get_fill_rgbas(self, vmobject: VMobject) -> PixelArray: """Returns the RGBA array of the fill of the passed VMobject Parameters @@ -829,13 +848,15 @@ def get_fill_rgbas(self, vmobject: VMobject): """ return vmobject.get_fill_rgbas() - def get_background_colored_vmobject_displayer(self): + def get_background_colored_vmobject_displayer( + self, + ) -> BackgroundColoredVMobjectDisplayer: """Returns the background_colored_vmobject_displayer if it exists or makes one and returns it if not. Returns ------- - BackGroundColoredVMobjectDisplayer + BackgroundColoredVMobjectDisplayer Object that displays VMobjects that have the same color as the background. """ @@ -843,11 +864,11 @@ def get_background_colored_vmobject_displayer(self): bcvd = "background_colored_vmobject_displayer" if not hasattr(self, bcvd): setattr(self, bcvd, BackgroundColoredVMobjectDisplayer(self)) - return getattr(self, bcvd) + return getattr(self, bcvd) # type: ignore[no-any-return] def display_multiple_background_colored_vmobjects( - self, cvmobjects: list, pixel_array: np.ndarray - ): + self, cvmobjects: Iterable[VMobject], pixel_array: PixelArray + ) -> Self: """Displays multiple vmobjects that have the same color as the background. Parameters @@ -873,8 +894,8 @@ def display_multiple_background_colored_vmobjects( # As a result, the other methods do not have as detailed docstrings as would be preferred. def display_multiple_point_cloud_mobjects( - self, pmobjects: list, pixel_array: np.ndarray - ): + self, pmobjects: list, pixel_array: PixelArray + ) -> None: """Displays multiple PMobjects by modifying the passed pixel array. Parameters @@ -899,8 +920,8 @@ def display_point_cloud( points: list, rgbas: np.ndarray, thickness: float, - pixel_array: np.ndarray, - ): + pixel_array: PixelArray, + ) -> None: """Displays a PMobject by modifying the pixel array suitably. TODO: Write a description for the rgbas argument. @@ -948,7 +969,7 @@ def display_point_cloud( def display_multiple_image_mobjects( self, image_mobjects: list, pixel_array: np.ndarray - ): + ) -> None: """Displays multiple image mobjects by modifying the passed pixel_array. Parameters @@ -963,7 +984,7 @@ def display_multiple_image_mobjects( def display_image_mobject( self, image_mobject: AbstractImageMobject, pixel_array: np.ndarray - ): + ) -> None: """Displays an ImageMobject by changing the pixel_array suitably. Parameters @@ -1020,7 +1041,9 @@ def display_image_mobject( # Paint on top of existing pixel array self.overlay_PIL_image(pixel_array, full_image) - def overlay_rgba_array(self, pixel_array: np.ndarray, new_array: np.ndarray): + def overlay_rgba_array( + self, pixel_array: np.ndarray, new_array: np.ndarray + ) -> None: """Overlays an RGBA array on top of the given Pixel array. Parameters @@ -1032,7 +1055,7 @@ def overlay_rgba_array(self, pixel_array: np.ndarray, new_array: np.ndarray): """ self.overlay_PIL_image(pixel_array, self.get_image(new_array)) - def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image): + def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image) -> None: """Overlays a PIL image on the passed pixel array. Parameters @@ -1047,7 +1070,7 @@ def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image): dtype="uint8", ) - def adjust_out_of_range_points(self, points: np.ndarray): + def adjust_out_of_range_points(self, points: np.ndarray) -> np.ndarray: """If any of the points in the passed array are out of the viable range, they are adjusted suitably. @@ -1078,9 +1101,10 @@ def adjust_out_of_range_points(self, points: np.ndarray): def transform_points_pre_display( self, - mobject, - points, - ): # TODO: Write more detailed docstrings for this method. + mobject: Mobject, + points: np.ndarray, + ) -> np.ndarray: + # TODO: Write more detailed docstrings for this method. # NOTE: There seems to be an unused argument `mobject`. # Subclasses (like ThreeDCamera) may want to @@ -1093,9 +1117,9 @@ def transform_points_pre_display( def points_to_pixel_coords( self, - mobject, - points, - ): # TODO: Write more detailed docstrings for this method. + mobject: Mobject, + points: np.ndarray, + ) -> np.ndarray: # TODO: Write more detailed docstrings for this method. points = self.transform_points_pre_display(mobject, points) shifted_points = points - self.frame_center @@ -1115,7 +1139,7 @@ def points_to_pixel_coords( result[:, 1] = shifted_points[:, 1] * height_mult + height_add return result.astype("int") - def on_screen_pixels(self, pixel_coords: np.ndarray): + def on_screen_pixels(self, pixel_coords: np.ndarray) -> PixelArray: """Returns array of pixels that are on the screen from a given array of pixel_coordinates @@ -1154,12 +1178,12 @@ def adjusted_thickness(self, thickness: float) -> float: the camera. """ # TODO: This seems...unsystematic - big_sum = op.add(config["pixel_height"], config["pixel_width"]) - this_sum = op.add(self.pixel_height, self.pixel_width) + big_sum: float = op.add(config["pixel_height"], config["pixel_width"]) + this_sum: float = op.add(self.pixel_height, self.pixel_width) factor = big_sum / this_sum return 1 + (thickness - 1) * factor - def get_thickening_nudges(self, thickness: float): + def get_thickening_nudges(self, thickness: float) -> PixelArray: """Determine a list of vectors used to nudge two-dimensional pixel coordinates. @@ -1176,7 +1200,9 @@ def get_thickening_nudges(self, thickness: float): _range = list(range(-thickness // 2 + 1, thickness // 2 + 1)) return np.array(list(it.product(_range, _range))) - def thickened_coordinates(self, pixel_coords: np.ndarray, thickness: float): + def thickened_coordinates( + self, pixel_coords: np.ndarray, thickness: float + ) -> PixelArray: """Returns thickened coordinates for a passed array of pixel coords and a thickness to thicken by. @@ -1198,7 +1224,7 @@ def thickened_coordinates(self, pixel_coords: np.ndarray, thickness: float): return pixel_coords.reshape((size // 2, 2)) # TODO, reimplement using cairo matrix - def get_coords_of_all_pixels(self): + def get_coords_of_all_pixels(self) -> PixelArray: """Returns the cartesian coordinates of each pixel. Returns @@ -1246,20 +1272,20 @@ class BackgroundColoredVMobjectDisplayer: def __init__(self, camera: Camera): self.camera = camera - self.file_name_to_pixel_array_map = {} + self.file_name_to_pixel_array_map: dict[str, PixelArray] = {} self.pixel_array = np.array(camera.pixel_array) self.reset_pixel_array() - def reset_pixel_array(self): + def reset_pixel_array(self) -> None: self.pixel_array[:, :] = 0 def resize_background_array( self, - background_array: np.ndarray, + background_array: PixelArray, new_width: float, new_height: float, mode: str = "RGBA", - ): + ) -> PixelArray: """Resizes the pixel array representing the background. Parameters @@ -1284,8 +1310,8 @@ def resize_background_array( return np.array(resized_image) def resize_background_array_to_match( - self, background_array: np.ndarray, pixel_array: np.ndarray - ): + self, background_array: PixelArray, pixel_array: PixelArray + ) -> PixelArray: """Resizes the background array to match the passed pixel array. Parameters @@ -1304,7 +1330,9 @@ def resize_background_array_to_match( mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB" return self.resize_background_array(background_array, width, height, mode) - def get_background_array(self, image: Image.Image | pathlib.Path | str): + def get_background_array( + self, image: Image.Image | pathlib.Path | str + ) -> PixelArray: """Gets the background array that has the passed file_name. Parameters @@ -1333,7 +1361,7 @@ def get_background_array(self, image: Image.Image | pathlib.Path | str): self.file_name_to_pixel_array_map[image_key] = back_array return back_array - def display(self, *cvmobjects: VMobject): + def display(self, *cvmobjects: VMobject) -> PixelArray | None: """Displays the colored VMobjects. Parameters diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 7b09ec783a..0077fbe70d 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -1,3 +1,6 @@ +# The following line is needed to avoid some strange +# mypy errors related to the code around line 366. +# mypy: disable_error_code = has-type r"""Mobjects representing text rendered using LaTeX. .. important:: diff --git a/manim/mobject/types/image_mobject.py b/manim/mobject/types/image_mobject.py index 56029f941e..5f83dd0240 100644 --- a/manim/mobject/types/image_mobject.py +++ b/manim/mobject/types/image_mobject.py @@ -28,7 +28,7 @@ import numpy.typing as npt from typing_extensions import Self - from manim.typing import StrPath + from manim.typing import PixelArray, StrPath class AbstractImageMobject(Mobject): @@ -57,7 +57,7 @@ def __init__( self.set_resampling_algorithm(resampling_algorithm) super().__init__(**kwargs) - def get_pixel_array(self) -> None: + def get_pixel_array(self) -> PixelArray: raise NotImplementedError() def set_color(self, color, alpha=None, family=True): diff --git a/mypy.ini b/mypy.ini index 8614514ecb..b1cac898ef 100644 --- a/mypy.ini +++ b/mypy.ini @@ -58,6 +58,9 @@ ignore_errors = True [mypy-manim.camera.*] ignore_errors = True +[mypy-manim.camera.camera.*] +ignore_errors = False + [mypy-manim.cli.*] ignore_errors = False From 94215e04e864e89209c7480625ae77be63b04b92 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 29 Jul 2025 09:34:20 +0200 Subject: [PATCH 10/21] Remove deprecated code --- manim/mobject/text/text_mobject.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index c35f874ed5..0b294b1ecf 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -644,32 +644,6 @@ def _find_indexes(self, word: str, text: str): index = text.find(word, index + len(word)) return indexes - @deprecated( - since="v0.14.0", - until="v0.15.0", - message="This was internal function, you shouldn't be using it anyway.", - ) - def _set_color_by_t2c(self, t2c=None): - """Sets color for specified strings.""" - t2c = t2c if t2c else self.t2c - for word, color in list(t2c.items()): - for start, end in self._find_indexes(word, self.text): - self.chars[start:end].set_color(color) - - @deprecated( - since="v0.14.0", - until="v0.15.0", - message="This was internal function, you shouldn't be using it anyway.", - ) - def _set_color_by_t2g(self, t2g=None): - """Sets gradient colors for specified - strings. Behaves similarly to ``set_color_by_t2c``. - """ - t2g = t2g if t2g else self.t2g - for word, gradient in list(t2g.items()): - for start, end in self._find_indexes(word, self.text): - self.chars[start:end].set_color_by_gradient(*gradient) - def _text2hash(self, color: ManimColor): """Generates ``sha256`` hash for file name.""" settings = ( From 7f244843d5fd7928a8faa3726d4ce89a99835a51 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 29 Jul 2025 09:55:54 +0200 Subject: [PATCH 11/21] First pass through text_mobject.py for adding type annotations. --- manim/mobject/text/text_mobject.py | 109 +++++++++++++++-------------- mypy.ini | 2 +- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index 0b294b1ecf..f6711fce0c 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -57,22 +57,24 @@ def construct(self): import copy import hashlib import re -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Iterator, Sequence from contextlib import contextmanager from itertools import chain from pathlib import Path +from typing import Any import manimpango import numpy as np from manimpango import MarkupUtils, PangoUtils, TextSetting +from typing_extensions import Self from manim import config, logger from manim.constants import * from manim.mobject.geometry.arc import Dot from manim.mobject.svg.svg_mobject import SVGMobject from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.typing import Point3D from manim.utils.color import ManimColor, ParsableManimColor, color_gradient -from manim.utils.deprecation import deprecated TEXT_MOB_SCALE_FACTOR = 0.05 DEFAULT_LINE_SPACING_SCALE = 0.3 @@ -81,7 +83,7 @@ def construct(self): __all__ = ["Text", "Paragraph", "MarkupText", "register_font"] -def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject: +def remove_invisible_chars(mobject: SVGMobject) -> VGroup: """Function to remove unwanted invisible characters from some mobjects. Parameters @@ -158,8 +160,8 @@ def __init__( *text: Sequence[str], line_spacing: float = -1, alignment: str | None = None, - **kwargs, - ) -> None: + **kwargs: Any, + ): self.line_spacing = line_spacing self.alignment = alignment self.consider_spaces_as_chars = kwargs.get("disable_ligatures", False) @@ -420,7 +422,8 @@ def construct(self): @staticmethod @functools.cache def font_list() -> list[str]: - return manimpango.list_fonts() + value: list[str] = manimpango.list_fonts() + return value def __init__( self, @@ -433,22 +436,22 @@ def __init__( font: str = "", slant: str = NORMAL, weight: str = NORMAL, - t2c: dict[str, str] = None, - t2f: dict[str, str] = None, - t2g: dict[str, tuple] = None, - t2s: dict[str, str] = None, - t2w: dict[str, str] = None, - gradient: tuple = None, + t2c: dict[str, str] | None = None, + t2f: dict[str, str] | None = None, + t2g: dict[str, tuple] | None = None, + t2s: dict[str, str] | None = None, + t2w: dict[str, str] | None = None, + gradient: tuple | None = None, tab_width: int = 4, warn_missing_font: bool = True, # Mobject - height: float = None, - width: float = None, + height: float | None = None, + width: float | None = None, should_center: bool = True, disable_ligatures: bool = False, use_svg_cache: bool = False, - **kwargs, - ) -> None: + **kwargs: Any, + ): self.line_spacing = line_spacing if font and warn_missing_font: fonts_list = Text.font_list() @@ -508,8 +511,8 @@ def __init__( else: self.line_spacing = self._font_size + self._font_size * self.line_spacing - color: ManimColor = ManimColor(color) if color else VMobject().color - file_name = self._text2svg(color.to_hex()) + parsed_color: ManimColor = ManimColor(color) if color else VMobject().color + file_name = self._text2svg(parsed_color.to_hex()) PangoUtils.remove_last_M(file_name) super().__init__( file_name, @@ -541,12 +544,12 @@ def __init__( # into a numpy array at the end, rather than creating # new numpy arrays every time a point or fixing line # is added (which is O(n^2) for numpy arrays). - closed_curve_points = [] + closed_curve_points: list[Point3D] = [] # OpenGL has points be part of quadratic Bezier curves; # Cairo uses cubic Bezier curves. if nppc == 3: # RendererType.OPENGL - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -557,7 +560,7 @@ def add_line_to(end): else: # RendererType.CAIRO - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -588,11 +591,11 @@ def add_line_to(end): self.scale(TEXT_MOB_SCALE_FACTOR) self.initial_height = self.height - def __repr__(self): + def __repr__(self) -> str: return f"Text({repr(self.original_text)})" @property - def font_size(self): + def font_size(self) -> float: return ( self.height / self.initial_height @@ -603,14 +606,14 @@ def font_size(self): ) @font_size.setter - def font_size(self, font_val): + def font_size(self, font_val: float) -> None: # TODO: use pango's font size scaling. if font_val <= 0: raise ValueError("font_size must be greater than 0.") else: self.scale(font_val / self.font_size) - def _gen_chars(self): + def _gen_chars(self) -> VGroup: chars = self.get_group_class()() submobjects_char_index = 0 for char_index in range(len(self.text)): @@ -628,7 +631,7 @@ def _gen_chars(self): submobjects_char_index += 1 return chars - def _find_indexes(self, word: str, text: str): + def _find_indexes(self, word: str, text: str) -> list[tuple[int, int]]: """Finds the indexes of ``text`` in ``word``.""" temp = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word) if temp: @@ -644,7 +647,7 @@ def _find_indexes(self, word: str, text: str): index = text.find(word, index + len(word)) return indexes - def _text2hash(self, color: ManimColor): + def _text2hash(self, color: ParsableManimColor) -> str: """Generates ``sha256`` hash for file name.""" settings = ( "PANGO" + self.font + self.slant + self.weight + str(color) @@ -729,7 +732,7 @@ def _get_settings_from_gradient( settings.append(TextSetting(i, i + 1, **args)) return settings - def _text2settings(self, color: str): + def _text2settings(self, color: ParsableManimColor) -> Sequence[TextSetting]: """Converts the texts and styles to a setting for parsing.""" t2xs = [ (self.t2f, "font"), @@ -796,7 +799,7 @@ def _text2settings(self, color: str): return settings - def _text2svg(self, color: ManimColor): + def _text2svg(self, color: ParsableManimColor) -> str: """Convert the text to SVG using Pango.""" size = self._font_size line_spacing = self.line_spacing @@ -831,11 +834,12 @@ def _text2svg(self, color: ManimColor): return svg_file - def init_colors(self, propagate_colors=True): + def init_colors(self, propagate_colors: bool = True) -> Self: if config.renderer == RendererType.OPENGL: super().init_colors() elif config.renderer == RendererType.CAIRO: super().init_colors(propagate_colors=propagate_colors) + return self class MarkupText(SVGMobject): @@ -1139,7 +1143,8 @@ def construct(self): @staticmethod @functools.cache def font_list() -> list[str]: - return manimpango.list_fonts() + value: list[str] = manimpango.list_fonts() + return value def __init__( self, @@ -1148,20 +1153,20 @@ def __init__( stroke_width: float = 0, color: ParsableManimColor | None = None, font_size: float = DEFAULT_FONT_SIZE, - line_spacing: int = -1, + line_spacing: float = -1, font: str = "", slant: str = NORMAL, weight: str = NORMAL, justify: bool = False, - gradient: tuple = None, + gradient: tuple | None = None, tab_width: int = 4, - height: int = None, - width: int = None, + height: int | None = None, + width: int | None = None, should_center: bool = True, disable_ligatures: bool = False, warn_missing_font: bool = True, - **kwargs, - ) -> None: + **kwargs: Any, + ): self.text = text self.line_spacing = line_spacing if font and warn_missing_font: @@ -1210,8 +1215,8 @@ def __init__( else: self.line_spacing = self._font_size + self._font_size * self.line_spacing - color: ManimColor = ManimColor(color) if color else VMobject().color - file_name = self._text2svg(color) + parsed_color: ManimColor = ManimColor(color) if color else VMobject().color + file_name = self._text2svg(parsed_color) PangoUtils.remove_last_M(file_name) super().__init__( @@ -1242,12 +1247,12 @@ def __init__( # into a numpy array at the end, rather than creating # new numpy arrays every time a point or fixing line # is added (which is O(n^2) for numpy arrays). - closed_curve_points = [] + closed_curve_points: list[Point3D] = [] # OpenGL has points be part of quadratic Bezier curves; # Cairo uses cubic Bezier curves. if nppc == 3: # RendererType.OPENGL - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -1258,7 +1263,7 @@ def add_line_to(end): else: # RendererType.CAIRO - def add_line_to(end): + def add_line_to(end: Point3D) -> None: nonlocal closed_curve_points start = closed_curve_points[-1] closed_curve_points += [ @@ -1306,7 +1311,7 @@ def add_line_to(end): self.initial_height = self.height @property - def font_size(self): + def font_size(self) -> float: return ( self.height / self.initial_height @@ -1317,14 +1322,14 @@ def font_size(self): ) @font_size.setter - def font_size(self, font_val): + def font_size(self, font_val: float) -> None: # TODO: use pango's font size scaling. if font_val <= 0: raise ValueError("font_size must be greater than 0.") else: self.scale(font_val / self.font_size) - def _text2hash(self, color: ParsableManimColor): + def _text2hash(self, color: ParsableManimColor) -> str: """Generates ``sha256`` hash for file name.""" settings = ( "MARKUPPANGO" @@ -1341,7 +1346,7 @@ def _text2hash(self, color: ParsableManimColor): hasher.update(id_str.encode()) return hasher.hexdigest()[:16] - def _text2svg(self, color: ParsableManimColor | None): + def _text2svg(self, color: ParsableManimColor | None) -> str: """Convert the text to SVG using Pango.""" color = ManimColor(color) size = self._font_size @@ -1382,7 +1387,7 @@ def _text2svg(self, color: ParsableManimColor | None): ) return svg_file - def _count_real_chars(self, s): + def _count_real_chars(self, s: str) -> int: """Counts characters that will be displayed. This is needed for partial coloring or gradients, because space @@ -1401,7 +1406,7 @@ def _count_real_chars(self, s): count += 1 return count - def _extract_gradient_tags(self): + def _extract_gradient_tags(self) -> list[dict[str, Any]]: """Used to determine which parts (if any) of the string should be formatted with a gradient. @@ -1435,14 +1440,14 @@ def _extract_gradient_tags(self): ) return gradientmap - def _parse_color(self, col): + def _parse_color(self, col: str) -> str: """Parse color given in ```` or ```` tags.""" if re.match("#[0-9a-f]{6}", col): return col else: return ManimColor(col).to_hex() - def _extract_color_tags(self): + def _extract_color_tags(self) -> list[dict[str, Any]]: """Used to determine which parts (if any) of the string should be formatted with a custom color. @@ -1479,12 +1484,12 @@ def _extract_color_tags(self): ) return colormap - def __repr__(self): + def __repr__(self) -> str: return f"MarkupText({repr(self.original_text)})" @contextmanager -def register_font(font_file: str | Path): +def register_font(font_file: str | Path) -> Iterator[None]: """Temporarily add a font file to Pango's search path. This searches for the font_file at various places. The order it searches it described below. diff --git a/mypy.ini b/mypy.ini index 5affa9881d..0e6a405c78 100644 --- a/mypy.ini +++ b/mypy.ini @@ -148,7 +148,7 @@ ignore_errors = True ignore_errors = True [mypy-manim.mobject.text.text_mobject] -ignore_errors = True +ignore_errors = False [mypy-manim.mobject.three_d.three_dimensions] ignore_errors = True From 1ae6ac5dd5c3f548adaabe1fd90eae0be9150094 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 31 Jul 2025 10:34:54 +0200 Subject: [PATCH 12/21] More work on typing text_mobject.py --- manim/mobject/text/code_mobject.py | 3 ++- manim/mobject/text/text_mobject.py | 42 ++++++++++++++++++------------ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/manim/mobject/text/code_mobject.py b/manim/mobject/text/code_mobject.py index b3109c23e4..ad029440fc 100644 --- a/manim/mobject/text/code_mobject.py +++ b/manim/mobject/text/code_mobject.py @@ -19,7 +19,6 @@ from manim.mobject.geometry.arc import Dot from manim.mobject.geometry.shape_matchers import SurroundingRectangle from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL -from manim.mobject.text.text_mobject import Paragraph from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.typing import StrPath from manim.utils.color import WHITE, ManimColor @@ -200,6 +199,8 @@ def __init__( base_paragraph_config = self.default_paragraph_config.copy() base_paragraph_config.update(paragraph_config) + from manim.mobject.text.text_mobject import Paragraph + self.code_lines = Paragraph( *code_lines, **base_paragraph_config, diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index f6711fce0c..135d7d32a9 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -72,6 +72,7 @@ def construct(self): from manim.constants import * from manim.mobject.geometry.arc import Dot from manim.mobject.svg.svg_mobject import SVGMobject +from manim.mobject.text.code_mobject import Code from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.typing import Point3D from manim.utils.color import ManimColor, ParsableManimColor, color_gradient @@ -83,7 +84,7 @@ def construct(self): __all__ = ["Text", "Paragraph", "MarkupText", "register_font"] -def remove_invisible_chars(mobject: SVGMobject) -> VGroup: +def remove_invisible_chars(mobject: SVGMobject) -> VGroup | SVGMobject: """Function to remove unwanted invisible characters from some mobjects. Parameters @@ -98,9 +99,9 @@ def remove_invisible_chars(mobject: SVGMobject) -> VGroup: """ # TODO: Refactor needed iscode = False - if mobject.__class__.__name__ == "Text": + if isinstance(mobject, Text): mobject = mobject[:] - elif mobject.__class__.__name__ == "Code": + elif isinstance(mobject, Code): iscode = True code = mobject mobject = mobject.code @@ -112,6 +113,7 @@ def remove_invisible_chars(mobject: SVGMobject) -> VGroup: else: mobject_without_dots.add(*(k for k in mobject if k.__class__ != Dot)) if iscode: + assert isinstance(code, Code) code.code = mobject_without_dots return code return mobject_without_dots @@ -157,7 +159,7 @@ class Paragraph(VGroup): def __init__( self, - *text: Sequence[str], + *text: str, line_spacing: float = -1, alignment: str | None = None, **kwargs: Any, @@ -492,11 +494,16 @@ def __init__( t2g = kwargs.pop("text2gradient", t2g) t2s = kwargs.pop("text2slant", t2s) t2w = kwargs.pop("text2weight", t2w) - self.t2c = {k: ManimColor(v).to_hex() for k, v in t2c.items()} - self.t2f = t2f - self.t2g = t2g - self.t2s = t2s - self.t2w = t2w + assert t2c is not None + assert t2f is not None + assert t2g is not None + assert t2s is not None + assert t2w is not None + self.t2c: dict[str, str] = {k: ManimColor(v).to_hex() for k, v in t2c.items()} + self.t2f: dict[str, str] = t2f + self.t2g: dict[str, tuple] = t2g + self.t2s: dict[str, str] = t2s + self.t2w: dict[str, str] = t2w self.original_text = text self.disable_ligatures = disable_ligatures @@ -690,8 +697,8 @@ def _merge_settings( def _get_settings_from_t2xs( self, t2xs: Sequence[tuple[dict[str, str], str]], - default_args: dict[str, Iterable[str]], - ) -> Sequence[TextSetting]: + default_args: dict[str, Any], + ) -> list[TextSetting]: settings = [] t2xwords = set(chain(*([*t2x.keys()] for t2x, _ in t2xs))) for word in t2xwords: @@ -707,24 +714,27 @@ def _get_settings_from_t2xs( return settings def _get_settings_from_gradient( - self, default_args: dict[str, Iterable[str]] + self, default_args: dict[str, str] ) -> Sequence[TextSetting]: settings = [] args = copy.copy(default_args) if self.gradient: colors = color_gradient(self.gradient, len(self.text)) for i in range(len(self.text)): + # The type error below + # manim/mobject/text/text_mobject.py:724: error: Item "float" of "ManimColor | float" has no attribute "to_hex" [union-attr] + # is caused by the color_gradient function, as it is not guaranteed to return a list of ManimColors. args["color"] = colors[i].to_hex() settings.append(TextSetting(i, i + 1, **args)) for word, gradient in self.t2g.items(): if isinstance(gradient, str) or len(gradient) == 1: color = gradient if isinstance(gradient, str) else gradient[0] - gradient = [ManimColor(color)] + gradient = (ManimColor(color), ManimColor(color)) colors = ( color_gradient(gradient, len(word)) if len(gradient) != 1 - else len(word) * gradient + else len(word) * list(gradient) ) for start, end in self._find_indexes(word, self.text): for i in range(start, end): @@ -734,7 +744,7 @@ def _get_settings_from_gradient( def _text2settings(self, color: ParsableManimColor) -> Sequence[TextSetting]: """Converts the texts and styles to a setting for parsing.""" - t2xs = [ + t2xs: list[tuple[dict[str, str], str]] = [ (self.t2f, "font"), (self.t2s, "slant"), (self.t2w, "weight"), @@ -742,7 +752,7 @@ def _text2settings(self, color: ParsableManimColor) -> Sequence[TextSetting]: ] # setting_args requires values to be strings - default_args = { + default_args: dict[str, Any] = { arg: getattr(self, arg) if arg != "color" else color for _, arg in t2xs } From 001e0c82e0f229b2d0940da4da1eb2dc769c8196 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 8 Aug 2025 12:20:23 +0200 Subject: [PATCH 13/21] Hunting down the last typing issues. --- manim/mobject/text/code_mobject.py | 1 + manim/mobject/text/text_mobject.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manim/mobject/text/code_mobject.py b/manim/mobject/text/code_mobject.py index ad029440fc..e4bb1c370a 100644 --- a/manim/mobject/text/code_mobject.py +++ b/manim/mobject/text/code_mobject.py @@ -118,6 +118,7 @@ def construct(self): "line_spacing": 0.5, "disable_ligatures": True, } + code: VMobject def __init__( self, diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index 135d7d32a9..88c6c40102 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -84,7 +84,7 @@ def construct(self): __all__ = ["Text", "Paragraph", "MarkupText", "register_font"] -def remove_invisible_chars(mobject: SVGMobject) -> VGroup | SVGMobject: +def remove_invisible_chars(mobject: VMobject) -> VMobject: """Function to remove unwanted invisible characters from some mobjects. Parameters From 2649c5f7ee9d9560d1b21d828956cb669b54c203 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 8 Aug 2025 12:20:53 +0200 Subject: [PATCH 14/21] Ensure that color_gradient always returns a list --- manim/utils/color/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/utils/color/core.py b/manim/utils/color/core.py index af25992e59..b251479b47 100644 --- a/manim/utils/color/core.py +++ b/manim/utils/color/core.py @@ -1409,7 +1409,7 @@ def invert_color(color: ManimColorT) -> ManimColorT: def color_gradient( reference_colors: Sequence[ParsableManimColor], length_of_output: int, -) -> list[ManimColor] | ManimColor: +) -> list[ManimColor]: """Create a list of colors interpolated between the input array of colors with a specific number of colors. @@ -1426,7 +1426,7 @@ def color_gradient( A :class:`ManimColor` or a list of interpolated :class:`ManimColor`'s. """ if length_of_output == 0: - return ManimColor(reference_colors[0]) + return [ManimColor(reference_colors[0])] if len(reference_colors) == 1: return [ManimColor(reference_colors[0])] * length_of_output rgbs = [color_to_rgb(color) for color in reference_colors] From 2842c0947bc8e1abcf93c7de3be183db4f8f5b7c Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 8 Aug 2025 12:36:42 +0200 Subject: [PATCH 15/21] Code cleanup --- manim/camera/camera.py | 12 +++--------- manim/mobject/svg/svg_mobject.py | 4 ++-- manim/mobject/text/numbers.py | 7 ++----- manim/mobject/text/tex_mobject.py | 17 ++++++++--------- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 7c33d5b277..dc30cc06c9 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -309,9 +309,7 @@ def get_image( The PIL image of the array. """ if pixel_array is None: - # TODO: - # error: Cannot determine type of "pixel_array" [has-type] - pixel_array = self.pixel_array # type: ignore[has-type] + pixel_array = self.pixel_array return Image.fromarray(pixel_array, mode=self.image_mode) def convert_pixel_array( @@ -358,9 +356,7 @@ def set_pixel_array( ) if not ( hasattr(self, "pixel_array") - # TODO: - # error: Cannot determine type of "pixel_array" [has-type] - and self.pixel_array.shape == converted_array.shape # type: ignore[has-type] + and self.pixel_array.shape == converted_array.shape ): self.pixel_array: PixelArray = converted_array else: @@ -550,9 +546,7 @@ def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None: # partition while at the same time preserving order. mobjects = self.get_mobjects_to_display(mobjects, **kwargs) for group_type, group in it.groupby(mobjects, self.type_or_raise): - # TODO - # error: Call to untyped function (unknown) in typed context [no-untyped-call] - self.display_funcs[group_type](list(group), self.pixel_array) # type: ignore[no-untyped-call] + self.display_funcs[group_type](list(group), self.pixel_array) # Methods associated with svg rendering diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 6e4cc6f6c3..59be5f5935 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -11,7 +11,7 @@ import svgelements as se from manim import config, logger -from manim.utils.color import ParsableManimColor +from manim.utils.color import ManimColor, ParsableManimColor from ...constants import RIGHT from ...utils.bezier import get_quadratic_approximation_of_cubic @@ -120,7 +120,7 @@ def __init__( self.should_center = should_center self.svg_height = height self.svg_width = width - self.color = color + self.color = ManimColor(color) self.opacity = opacity self.fill_color = fill_color self.fill_opacity = fill_opacity # type: ignore[assignment] diff --git a/manim/mobject/text/numbers.py b/manim/mobject/text/numbers.py index 7bea4e12f7..2d8c509dcc 100644 --- a/manim/mobject/text/numbers.py +++ b/manim/mobject/text/numbers.py @@ -225,12 +225,9 @@ def _string_to_mob( mob_class = self.mob_class if string not in string_to_mob_map: - # TODO: I am not sure about the type of mob_class. - # I think it should be SingleStringMathTex, as that class has the _fontsize property. - # But this seems to conflict with the default case, where it is set to VMobject. - string_to_mob_map[string] = mob_class(string, **kwargs) # type: ignore[assignment] + string_to_mob_map[string] = mob_class(string, **kwargs) mob = string_to_mob_map[string].copy() - mob.font_size = self._font_size # type: ignore[attr-defined] + mob.font_size = self._font_size return mob def _get_formatter(self, **kwargs: Any) -> str: diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index da5c02fce1..d6b1518f5e 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -15,7 +15,7 @@ from __future__ import annotations -from manim.utils.color import BLACK, ParsableManimColor +from manim.utils.color import BLACK, ManimColor, ParsableManimColor __all__ = [ "SingleStringMathTex", @@ -272,7 +272,7 @@ def __init__( [] if substrings_to_isolate is None else substrings_to_isolate ) if tex_to_color_map is None: - self.tex_to_color_map: dict[str, ManimColor] = {} + self.tex_to_color_map: dict[str, ParsableManimColor] = {} else: self.tex_to_color_map = tex_to_color_map self.tex_environment = tex_environment @@ -419,17 +419,17 @@ def set_opacity_by_tex( return self def set_color_by_tex_to_color_map( - self, texs_to_color_map: dict[str, ManimColor], **kwargs: Any + self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any ) -> Self: for texs, color in list(texs_to_color_map.items()): try: # If the given key behaves like tex_strings texs + "" - self.set_color_by_tex(texs, color, **kwargs) + self.set_color_by_tex(texs, ManimColor(color), **kwargs) except TypeError: # If the given key is a tuple for tex in texs: - self.set_color_by_tex(tex, color, **kwargs) + self.set_color_by_tex(tex, ManimColor(color), **kwargs) return self def index_of_part(self, part: MathTex) -> int: @@ -508,12 +508,11 @@ def __init__( ): self.buff = buff self.dot_scale_factor = dot_scale_factor - # TODO: See comment 10 lines above this - self.tex_environment = tex_environment # type: ignore[assignment] + self.tex_environment = tex_environment line_separated_items = [s + "\\\\" for s in items] super().__init__( *line_separated_items, - tex_environment=tex_environment, # type: ignore[arg-type] + tex_environment=tex_environment, **kwargs, ) for part in self: @@ -529,7 +528,7 @@ def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None if part is None: raise Exception("Could not locate part by provided tex string") elif isinstance(arg, int): - part = self.submobjects[arg] # type: ignore[assignment] + part = self.submobjects[arg] else: raise TypeError(f"Expected int or string, got {arg}") for other_part in self.submobjects: From eeacbd710dba336bcefb40576c84a72e25b2854d Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 8 Aug 2025 12:47:00 +0200 Subject: [PATCH 16/21] Code cleanup --- manim/mobject/text/text_mobject.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index 4f87878942..83ce943a76 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -111,9 +111,7 @@ def remove_invisible_chars(mobject: VMobject) -> VMobject: elif isinstance(mobject, Code): iscode = True code = mobject - # TODO: - # error: Incompatible types in assignment (expression has type "MethodType", variable has type "SVGMobject") [assignment] - mobject = mobject.code # type: ignore[assignment] + mobject = mobject.code mobject_without_dots = VGroup() if mobject[0].__class__ == VGroup: for i in range(len(mobject)): @@ -766,13 +764,8 @@ def _text2settings(self, color: ParsableManimColor) -> Sequence[TextSetting]: arg: getattr(self, arg) if arg != "color" else color for _, arg in t2xs } - # TODO: - # error: Argument 1 to "_get_settings_from_t2xs" of "Text" has incompatible type "list[tuple[dict[str, str] | None, str]]"; expected "Sequence[tuple[dict[str, str], str]]" [arg-type] - # error: Argument 2 to "_get_settings_from_t2xs" of "Text" has incompatible type "dict[str, Any | ManimColor | int | str | tuple[int, int, int] | tuple[float, float, float] | tuple[int, int, int, int] | tuple[float, float, float, float]]"; expected "dict[str, Iterable[str]]" [arg-type] - settings = self._get_settings_from_t2xs(t2xs, default_args) # type: ignore[arg-type] - # TODO: - # error: Argument 1 to "_get_settings_from_gradient" of "Text" has incompatible type "dict[str, Any | ManimColor | int | str | tuple[int, int, int] | tuple[float, float, float] | tuple[int, int, int, int] | tuple[float, float, float, float]]"; expected "dict[str, Iterable[str]]" [arg-type] - settings.extend(self._get_settings_from_gradient(default_args)) # type: ignore[arg-type] + settings = self._get_settings_from_t2xs(t2xs, default_args) + settings.extend(self._get_settings_from_gradient(default_args)) # Handle overlaps @@ -783,9 +776,7 @@ def _text2settings(self, color: ParsableManimColor) -> Sequence[TextSetting]: next_setting = settings[index + 1] if setting.end > next_setting.start: - # TODO: - # error: Argument 3 to "_merge_settings" of "Text" has incompatible type "dict[str, Any | ManimColor | int | str | tuple[int, int, int] | tuple[float, float, float] | tuple[int, int, int, int] | tuple[float, float, float, float]]"; expected "dict[str, Iterable[str]]" [arg-type] - new_setting = self._merge_settings(setting, next_setting, default_args) # type: ignore[arg-type] + new_setting = self._merge_settings(setting, next_setting, default_args) new_index = index + 1 while ( new_index < len(settings) From de1753f307e2ba2a724b7d909d27741ad61febe6 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 8 Aug 2025 12:51:58 +0200 Subject: [PATCH 17/21] Code cleanup --- manim/mobject/text/tex_mobject.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index d6b1518f5e..6220caf462 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -1,6 +1,3 @@ -# The following line is needed to avoid some strange -# mypy errors related to the code around line 366. -# mypy: disable_error_code = has-type r"""Mobjects representing text rendered using LaTeX. .. important:: From 69486d8a375791263492b3d4923c3c830d63aab5 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 8 Aug 2025 13:17:57 +0200 Subject: [PATCH 18/21] Use the right version from the merge --- manim/mobject/text/text_mobject.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index 83ce943a76..83d68b89be 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -706,9 +706,7 @@ def _merge_settings( def _get_settings_from_t2xs( self, t2xs: Sequence[tuple[dict[str, str], str]], - # default_args: dict[str, Iterable[str]], - default_args: dict[str, Any], - # TODO Look into this + default_args: dict[str, Iterable[str]], ) -> list[TextSetting]: settings = [] t2xwords = set(chain(*([*t2x.keys()] for t2x, _ in t2xs))) From 7d205618eaf6b9125f758f47817c89962c541921 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 11 Aug 2025 22:02:05 +0200 Subject: [PATCH 19/21] Suggestions from Chopan50 --- manim/mobject/text/tex_mobject.py | 2 +- manim/mobject/text/text_mobject.py | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 6220caf462..7358ae8790 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -523,7 +523,7 @@ def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None if isinstance(arg, str): part: VGroup | VMobject | None = self.get_part_by_tex(arg) if part is None: - raise Exception("Could not locate part by provided tex string") + raise Exception(f"Could not locate part by provided tex string '{arg}'.") elif isinstance(arg, int): part = self.submobjects[arg] else: diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index 83d68b89be..95c1b30d51 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -66,7 +66,6 @@ def construct(self): import manimpango import numpy as np from manimpango import MarkupUtils, PangoUtils, TextSetting -from typing_extensions import Self from manim import config, logger from manim.constants import * @@ -78,8 +77,6 @@ def construct(self): from manim.utils.color import ManimColor, ParsableManimColor, color_gradient if TYPE_CHECKING: - from typing import Any - from typing_extensions import Self from manim.typing import Point3D @@ -113,10 +110,11 @@ def remove_invisible_chars(mobject: VMobject) -> VMobject: code = mobject mobject = mobject.code mobject_without_dots = VGroup() - if mobject[0].__class__ == VGroup: - for i in range(len(mobject)): - mobject_without_dots.add(VGroup()) - mobject_without_dots[i].add(*(k for k in mobject[i] if k.__class__ != Dot)) + if isinstance(mobject[0], VGroup): + for submob in mobject: + mobject_without_dots.add( + VGroup(k for k in submob if not isinstance(k, Dot)) + ) else: mobject_without_dots.add(*(k for k in mobject if k.__class__ != Dot)) if iscode: @@ -447,10 +445,10 @@ def __init__( weight: str = NORMAL, t2c: dict[str, str] | None = None, t2f: dict[str, str] | None = None, - t2g: dict[str, tuple] | None = None, + t2g: dict[str, Iterable[ParsableManimColor]] | None = None, t2s: dict[str, str] | None = None, t2w: dict[str, str] | None = None, - gradient: tuple | None = None, + gradient: Iterable[ParsableManimColor] | None = None, tab_width: int = 4, warn_missing_font: bool = True, # Mobject @@ -508,7 +506,7 @@ def __init__( assert t2w is not None self.t2c: dict[str, str] = {k: ManimColor(v).to_hex() for k, v in t2c.items()} self.t2f: dict[str, str] = t2f - self.t2g: dict[str, tuple] = t2g + self.t2g: dict[str, Iterable[ParsableManimColor]] = t2g self.t2s: dict[str, str] = t2s self.t2w: dict[str, str] = t2w @@ -723,8 +721,8 @@ def _get_settings_from_t2xs( return settings def _get_settings_from_gradient( - self, default_args: dict[str, str] - ) -> Sequence[TextSetting]: + self, default_args: dict[str, Any] + ) -> list[TextSetting]: settings = [] args = copy.copy(default_args) if self.gradient: @@ -748,7 +746,7 @@ def _get_settings_from_gradient( settings.append(TextSetting(i, i + 1, **args)) return settings - def _text2settings(self, color: ParsableManimColor) -> Sequence[TextSetting]: + def _text2settings(self, color: ParsableManimColor) -> list[TextSetting]: """Converts the texts and styles to a setting for parsing.""" t2xs: list[tuple[dict[str, str], str]] = [ (self.t2f, "font"), @@ -1174,7 +1172,7 @@ def __init__( slant: str = NORMAL, weight: str = NORMAL, justify: bool = False, - gradient: tuple | None = None, + gradient: Iterable[ParsableManimColor] | None = None, tab_width: int = 4, height: int | None = None, width: int | None = None, From 2ef82acb3e09761dc0e61960b97b5794796ea3f9 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 11 Aug 2025 22:11:31 +0200 Subject: [PATCH 20/21] Fixes --- manim/mobject/text/tex_mobject.py | 4 +++- manim/mobject/text/text_mobject.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 7358ae8790..03bc285e79 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -523,7 +523,9 @@ def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None if isinstance(arg, str): part: VGroup | VMobject | None = self.get_part_by_tex(arg) if part is None: - raise Exception(f"Could not locate part by provided tex string '{arg}'.") + raise Exception( + f"Could not locate part by provided tex string '{arg}'." + ) elif isinstance(arg, int): part = self.submobjects[arg] else: diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index aa705af991..493a23af25 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -726,7 +726,7 @@ def _get_settings_from_gradient( settings = [] args = copy.copy(default_args) if self.gradient: - colors = color_gradient(self.gradient, len(self.text)) + colors: list[ManimColor] = color_gradient(self.gradient, len(self.text)) for i in range(len(self.text)): args["color"] = colors[i].to_hex() settings.append(TextSetting(i, i + 1, **args)) @@ -734,8 +734,8 @@ def _get_settings_from_gradient( for word, gradient in self.t2g.items(): colors = ( color_gradient(gradient, len(word)) - if len(gradient) != 1 - else len(word) * list(gradient) + if len(list(gradient)) != 1 + else len(word) * [ManimColor(gradient)] ) for start, end in self._find_indexes(word, self.text): for i in range(start, end): From 9ee37d3f52095d246871a5607029c96b5eb768db Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 11 Aug 2025 22:54:55 +0200 Subject: [PATCH 21/21] More code simplifications. --- manim/mobject/text/text_mobject.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index 493a23af25..a92b5d1da6 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -116,7 +116,7 @@ def remove_invisible_chars(mobject: VMobject) -> VMobject: VGroup(k for k in submob if not isinstance(k, Dot)) ) else: - mobject_without_dots.add(*(k for k in mobject if k.__class__ != Dot)) + mobject_without_dots.add(*(k for k in mobject if not isinstance(k, Dot))) if iscode: assert isinstance(code, Code) code.code = mobject_without_dots @@ -732,11 +732,7 @@ def _get_settings_from_gradient( settings.append(TextSetting(i, i + 1, **args)) for word, gradient in self.t2g.items(): - colors = ( - color_gradient(gradient, len(word)) - if len(list(gradient)) != 1 - else len(word) * [ManimColor(gradient)] - ) + colors = color_gradient(gradient, len(word)) for start, end in self._find_indexes(word, self.text): for i in range(start, end): args["color"] = colors[i - start].to_hex()