From 0aed31f4de4af9b851d797b89cc8e70de57fa1a0 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Thu, 31 Jul 2025 00:29:02 +0200 Subject: [PATCH 01/12] Some typing --- manim/renderer/cairo_renderer.py | 40 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 0b8de4c13f..5b6bf5c706 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -1,6 +1,7 @@ from __future__ import annotations -import typing +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any import numpy as np @@ -13,9 +14,7 @@ from ..utils.exceptions import EndSceneEarlyException from ..utils.iterables import list_update -if typing.TYPE_CHECKING: - from typing import Any - +if TYPE_CHECKING: from manim.animation.animation import Animation from manim.scene.scene import Scene @@ -48,7 +47,7 @@ def __init__( self.skip_animations = skip_animations self.animations_hashes = [] self.num_plays = 0 - self.time = 0 + self.time = 0.0 self.static_image = None def init_scene(self, scene: Scene) -> None: @@ -61,8 +60,8 @@ def play( self, scene: Scene, *args: Animation | Mobject | _AnimationBuilder, - **kwargs, - ): + **kwargs: Any, + ) -> None: # Reset skip_animations to the original state. # Needed when rendering only some animations, and skipping others. self.skip_animations = self._original_skipping_status @@ -120,7 +119,7 @@ def play( def update_frame( # TODO Description in Docstring self, scene: Scene, - mobjects: typing.Iterable[Mobject] | None = None, + mobjects: Iterable[Mobject] | None = None, include_submobjects: bool = True, ignore_skipping: bool = True, **kwargs: Any, @@ -156,7 +155,12 @@ def update_frame( # TODO Description in Docstring kwargs["include_submobjects"] = include_submobjects self.camera.capture_mobjects(mobjects, **kwargs) - def render(self, scene, time, moving_mobjects): + def render( + self, + scene: Scene, + time: float, + moving_mobjects: Iterable[Mobject] | None = None, + ) -> None: self.update_frame(scene, moving_mobjects) self.add_frame(self.get_frame()) @@ -166,13 +170,13 @@ def get_frame(self) -> PixelArray: Returns ------- - np.array + PixelArray NumPy array of pixel values of each pixel in screen. - The shape of the array is height x width x 3 + The shape of the array is height x width x 3. """ return np.array(self.camera.pixel_array) - def add_frame(self, frame: np.ndarray, num_frames: int = 1): + def add_frame(self, frame: PixelArray, num_frames: int = 1) -> None: """ Adds a frame to the video_file_stream @@ -189,7 +193,7 @@ def add_frame(self, frame: np.ndarray, num_frames: int = 1): self.time += num_frames * dt self.file_writer.write_frame(frame, num_frames=num_frames) - def freeze_current_frame(self, duration: float): + def freeze_current_frame(self, duration: float) -> None: """Adds a static frame to the movie for a given duration. The static frame is the current frame. Parameters @@ -203,7 +207,7 @@ def freeze_current_frame(self, duration: float): num_frames=int(duration / dt), ) - def show_frame(self): + def show_frame(self) -> None: """ Opens the current frame in the Default Image Viewer of your system. @@ -214,8 +218,8 @@ def show_frame(self): def save_static_frame_data( self, scene: Scene, - static_mobjects: typing.Iterable[Mobject], - ) -> typing.Iterable[Mobject] | None: + static_mobjects: Iterable[Mobject], + ) -> Iterable[Mobject] | None: """Compute and save the static frame, that will be reused at each frame to avoid unnecessarily computing static mobjects. @@ -228,7 +232,7 @@ def save_static_frame_data( Returns ------- - typing.Iterable[Mobject] + Iterable[Mobject] The static image computed. """ self.static_image = None @@ -238,7 +242,7 @@ def save_static_frame_data( self.static_image = self.get_frame() return self.static_image - def update_skipping_status(self): + def update_skipping_status(self) -> None: """ This method is used internally to check if the current animation needs to be skipped or not. It also checks if From b245ff4f28cba125bed61cb808fc84693185e6b2 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Sat, 9 Aug 2025 19:51:56 +0200 Subject: [PATCH 02/12] Add type annotations for cairo_renderer --- manim/renderer/cairo_renderer.py | 24 +++++------ manim/scene/scene.py | 10 ++--- manim/utils/hashing.py | 71 +++++++++++++++++--------------- mypy.ini | 4 +- 4 files changed, 54 insertions(+), 55 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 5b6bf5c706..947e00d4b0 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -36,7 +36,7 @@ def __init__( camera_class: type[Camera] | None = None, skip_animations: bool = False, **kwargs: Any, - ) -> None: + ): # All of the following are set to EITHER the value passed via kwargs, # OR the value stored in the global config dict at the time of # _instance construction_. @@ -45,10 +45,10 @@ def __init__( self.camera = camera_cls() self._original_skipping_status = skip_animations self.skip_animations = skip_animations - self.animations_hashes = [] + self.animations_hashes: list[str | None] = [] self.num_plays = 0 self.time = 0.0 - self.static_image = None + self.static_image: PixelArray | None = None def init_scene(self, scene: Scene) -> None: self.file_writer: Any = self._file_writer_class( @@ -165,8 +165,7 @@ def render( self.add_frame(self.get_frame()) def get_frame(self) -> PixelArray: - """ - Gets the current frame as NumPy array. + """Gets the current frame as NumPy array. Returns ------- @@ -177,8 +176,7 @@ def get_frame(self) -> PixelArray: return np.array(self.camera.pixel_array) def add_frame(self, frame: PixelArray, num_frames: int = 1) -> None: - """ - Adds a frame to the video_file_stream + """Adds a frame to the video_file_stream Parameters ---------- @@ -207,12 +205,11 @@ def freeze_current_frame(self, duration: float) -> None: num_frames=int(duration / dt), ) - def show_frame(self) -> None: - """ - Opens the current frame in the Default Image Viewer + def show_frame(self, scene: Scene) -> None: + """Opens the current frame in the Default Image Viewer of your system. """ - self.update_frame(ignore_skipping=True) + self.update_frame(scene, ignore_skipping=True) self.camera.get_image().show() def save_static_frame_data( @@ -228,7 +225,7 @@ def save_static_frame_data( scene The scene played. static_mobjects - Static mobjects of the scene. If None, self.static_image is set to None + Static mobjects of the scene. If None, self.static_image is set to None. Returns ------- @@ -243,8 +240,7 @@ def save_static_frame_data( return self.static_image def update_skipping_status(self) -> None: - """ - This method is used internally to check if the current + """This method is used internally to check if the current animation needs to be skipped or not. It also checks if the number of animations that were played correspond to the number of animations that need to be played, and diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 94d8715d35..996bb5ba9d 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -31,7 +31,9 @@ window = dpg.generate_uuid() except ImportError: dearpygui_imported = False -from typing import TYPE_CHECKING + +from collections.abc import Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Any, Union import numpy as np from tqdm import tqdm @@ -59,11 +61,9 @@ from ..utils.module_ops import scene_classes_from_file if TYPE_CHECKING: - from collections.abc import Iterable, Sequence from types import FrameType - from typing import Any, Callable, TypeAlias, Union - from typing_extensions import Self + from typing_extensions import Self, TypeAlias from manim.typing import Point3D @@ -186,7 +186,7 @@ def __init__( self.moving_mobjects: list[Mobject] = [] self.static_mobjects: list[Mobject] = [] self.time_progression: tqdm[float] | None = None - self.duration: float | None = None + self.duration: float = 0.0 self.last_t = 0.0 self.queue: Queue[SceneInteractAction] = Queue() self.skip_animation_preview = False diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 8619cc7e34..73b6e19310 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -2,21 +2,20 @@ from __future__ import annotations -import collections import copy import inspect import json -import typing import zlib +from collections.abc import Callable, Hashable, Iterable from time import perf_counter from types import FunctionType, MappingProxyType, MethodType, ModuleType -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np from manim._config import config, logger -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from manim.animation.animation import Animation from manim.camera.camera import Camera from manim.mobject.mobject import Mobject @@ -55,14 +54,16 @@ class _Memoizer: THRESHOLD_WARNING = 170_000 @classmethod - def reset_already_processed(cls): + def reset_already_processed(cls: type[_Memoizer]) -> None: cls._already_processed.clear() @classmethod - def check_already_processed_decorator(cls: _Memoizer, is_method: bool = False): + def check_already_processed_decorator( + cls: type[_Memoizer], is_method: bool = False + ) -> Callable: """Decorator to handle the arguments that goes through the decorated function. - Returns _ALREADY_PROCESSED_PLACEHOLDER if the obj has been processed, or lets - the decorated function call go ahead. + Returns the value of ALREADY_PROCESSED_PLACEHOLDER if the obj has been processed, + or lets the decorated function call go ahead. Parameters ---------- @@ -70,7 +71,7 @@ def check_already_processed_decorator(cls: _Memoizer, is_method: bool = False): Whether the function passed is a method, by default False. """ - def layer(func): + def layer(func: Callable[[Any], Any]) -> Callable: # NOTE : There is probably a better way to separate both case when func is # a method or a function. if is_method: @@ -83,9 +84,9 @@ def layer(func): return layer @classmethod - def check_already_processed(cls, obj: Any) -> Any: + def check_already_processed(cls: type[_Memoizer], obj: Any) -> Any: """Checks if obj has been already processed. Returns itself if it has not been, - or the value of _ALREADY_PROCESSED_PLACEHOLDER if it has. + or the value of ALREADY_PROCESSED_PLACEHOLDER if it has. Marks the object as processed in the second case. Parameters @@ -102,7 +103,7 @@ def check_already_processed(cls, obj: Any) -> Any: return cls._handle_already_processed(obj, lambda x: x) @classmethod - def mark_as_processed(cls, obj: Any) -> None: + def mark_as_processed(cls: type[_Memoizer], obj: Any) -> None: """Marks an object as processed. Parameters @@ -115,10 +116,10 @@ def mark_as_processed(cls, obj: Any) -> None: @classmethod def _handle_already_processed( - cls, - obj, - default_function: typing.Callable[[Any], Any], - ): + cls: type[_Memoizer], + obj: Any, + default_function: Callable[[Any], Any], + ) -> str | Any: if isinstance( obj, ( @@ -131,7 +132,7 @@ def _handle_already_processed( # It makes no sense (and it'd slower) to memoize objects of these primitive # types. Hence, we simply return the object. return obj - if isinstance(obj, collections.abc.Hashable): + if isinstance(obj, Hashable): try: return cls._return(obj, hash, default_function) except TypeError: @@ -143,11 +144,11 @@ def _handle_already_processed( @classmethod def _return( - cls, - obj: typing.Any, - obj_to_membership_sign: typing.Callable[[Any], int], - default_func, - memoizing=True, + cls: type[_Memoizer], + obj: Any, + obj_to_membership_sign: Callable[[Any], int], + default_func: Callable[[Any], Any], + memoizing: bool = True, ) -> str | Any: obj_membership_sign = obj_to_membership_sign(obj) if obj_membership_sign in cls._already_processed: @@ -173,9 +174,8 @@ def _return( class _CustomEncoder(json.JSONEncoder): - def default(self, obj: Any): - """ - This method is used to serialize objects to JSON format. + def default(self, obj: Any) -> Any: + """This method is used to serialize objects to JSON format. If obj is a function, then it will return a dict with two keys : 'code', for the code source, and 'nonlocals' for all nonlocalsvalues. (including nonlocals @@ -234,11 +234,11 @@ def default(self, obj: Any): # Serialize it with only the type of the object. You can change this to whatever string when debugging the serialization process. return str(type(obj)) - def _cleaned_iterable(self, iterable: typing.Iterable[Any]): + def _cleaned_iterable(self, iterable: Iterable[Any]) -> list[Any] | dict[Any, Any]: """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format. If a key with a bad format is found (i.e not a int, string, or float), it gets replaced byt its hash using the same process implemented here. - If a circular reference is found within the iterable, it will be replaced by the string "already processed". + If a circular reference is found within the iterable, it will be replaced by the value of ALREADY_PROCESSED_PLACEHOLDER. Parameters ---------- @@ -246,10 +246,10 @@ def _cleaned_iterable(self, iterable: typing.Iterable[Any]): The iterable to check. """ - def _key_to_hash(key): + def _key_to_hash(key) -> int: return zlib.crc32(json.dumps(key, cls=_CustomEncoder).encode()) - def _iter_check_list(lst): + def _iter_check_list(lst: list[Any]) -> list[Any]: processed_list = [None] * len(lst) for i, el in enumerate(lst): el = _Memoizer.check_already_processed(el) @@ -262,7 +262,7 @@ def _iter_check_list(lst): processed_list[i] = new_value return processed_list - def _iter_check_dict(dct): + def _iter_check_dict(dct: dict) -> dict: processed_dict = {} for k, v in dct.items(): v = _Memoizer.check_already_processed(v) @@ -286,8 +286,11 @@ def _iter_check_dict(dct): return _iter_check_list(iterable) elif isinstance(iterable, dict): return _iter_check_dict(iterable) + else: + # mypy requires this line, even though it should not be reached. + return iterable - def encode(self, obj: Any): + def encode(self, obj: Any) -> str: """Overriding of :meth:`JSONEncoder.encode`, to make our own process. Parameters @@ -306,7 +309,7 @@ def encode(self, obj: Any): return super().encode(obj) -def get_json(obj: dict): +def get_json(obj: Any) -> str: """Recursively serialize `object` to JSON using the :class:`CustomEncoder` class. Parameters @@ -325,8 +328,8 @@ def get_json(obj: dict): def get_hash_from_play_call( scene_object: Scene, camera_object: Camera | OpenGLCamera, - animations_list: typing.Iterable[Animation], - current_mobjects_list: typing.Iterable[Mobject], + animations_list: Iterable[Animation], + current_mobjects_list: Iterable[Mobject], ) -> str: """Take the list of animations and a list of mobjects and output their hashes. This is meant to be used for `scene.play` function. diff --git a/mypy.ini b/mypy.ini index 42c31f08ef..dcbeb48575 100644 --- a/mypy.ini +++ b/mypy.ini @@ -145,7 +145,7 @@ ignore_errors = True ignore_errors = True [mypy-manim.renderer.cairo_renderer] -ignore_errors = True +ignore_errors = False [mypy-manim.renderer.opengl_renderer] ignore_errors = True @@ -160,7 +160,7 @@ ignore_errors = True ignore_errors = True [mypy-manim.utils.hashing] -ignore_errors = True +ignore_errors = False From ffc5164a11a037b74a6572be51fa7233a3c621c0 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Mon, 11 Aug 2025 21:27:14 +0200 Subject: [PATCH 03/12] Fix two more type errors --- manim/renderer/cairo_renderer.py | 2 +- manim/utils/hashing.py | 2 +- mypy.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 111f5265ac..ff17089a3d 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -220,7 +220,7 @@ def save_static_frame_data( self, scene: Scene, static_mobjects: Iterable[Mobject], - ) -> Iterable[Mobject] | None: + ) -> PixelArray | None: """Compute and save the static frame, that will be reused at each frame to avoid unnecessarily computing static mobjects. diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 73b6e19310..d706328620 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -328,7 +328,7 @@ def get_json(obj: Any) -> str: def get_hash_from_play_call( scene_object: Scene, camera_object: Camera | OpenGLCamera, - animations_list: Iterable[Animation], + animations_list: Iterable[Animation] | None, current_mobjects_list: Iterable[Mobject], ) -> str: """Take the list of animations and a list of mobjects and output their hashes. This is meant to be used for `scene.play` function. diff --git a/mypy.ini b/mypy.ini index d0dcd812d6..55ae9d02fc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -133,7 +133,7 @@ ignore_errors = True ignore_errors = True [mypy-manim.renderer.cairo_renderer] -ignore_errors = True +ignore_errors = False [mypy-manim.renderer.opengl_renderer] ignore_errors = True From 7e62e3a6b80402b5556f3566a88b7b9bb964d049 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Mon, 11 Aug 2025 21:56:23 +0200 Subject: [PATCH 04/12] Update mypy.ini --- mypy.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index 55ae9d02fc..d04883078d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -132,9 +132,6 @@ ignore_errors = True [mypy-manim.mobject.vector_field] ignore_errors = True -[mypy-manim.renderer.cairo_renderer] -ignore_errors = False - [mypy-manim.renderer.opengl_renderer] ignore_errors = True From aac3de0ebfe01956d90fc02ea77b80d13bd13266 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:25:55 +0200 Subject: [PATCH 05/12] Fix docstring --- manim/renderer/cairo_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index ff17089a3d..66a1822101 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -233,8 +233,8 @@ def save_static_frame_data( Returns ------- - Iterable[Mobject] - The static image computed. + PixelArray | None + The static image computed. The return value is None if there are no static mobjects in the scene. """ self.static_image = None if not static_mobjects: From 0d3ff4e338cd4c225f6dde41f73ac90bdb9b3945 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 27 Aug 2025 00:26:43 +0200 Subject: [PATCH 06/12] Update manim/utils/hashing.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com> --- manim/utils/hashing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index d706328620..b4041ae93a 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -246,7 +246,7 @@ def _cleaned_iterable(self, iterable: Iterable[Any]) -> list[Any] | dict[Any, An The iterable to check. """ - def _key_to_hash(key) -> int: + def _key_to_hash(key: Any) -> int: return zlib.crc32(json.dumps(key, cls=_CustomEncoder).encode()) def _iter_check_list(lst: list[Any]) -> list[Any]: From 75cf984e88a958c7673abaf991364cbf5dcae8b1 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 27 Aug 2025 00:27:49 +0200 Subject: [PATCH 07/12] Update manim/utils/hashing.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com> --- manim/utils/hashing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index b4041ae93a..9394f16a63 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -262,7 +262,7 @@ def _iter_check_list(lst: list[Any]) -> list[Any]: processed_list[i] = new_value return processed_list - def _iter_check_dict(dct: dict) -> dict: + def _iter_check_dict(dct: dict[Any, Any]) -> dict[Any, Any]: processed_dict = {} for k, v in dct.items(): v = _Memoizer.check_already_processed(v) From d289a92869c9ea03b19a8a340ad5db0aad1ae375 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 27 Aug 2025 00:34:42 +0200 Subject: [PATCH 08/12] Apply suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com> --- manim/utils/hashing.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 9394f16a63..4544d4773d 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -234,7 +234,13 @@ def default(self, obj: Any) -> Any: # Serialize it with only the type of the object. You can change this to whatever string when debugging the serialization process. return str(type(obj)) - def _cleaned_iterable(self, iterable: Iterable[Any]) -> list[Any] | dict[Any, Any]: + @overload + def _cleaned_iterable(self, iterable: Sequence[Any]) -> list[Any]: ... + + @overload + def _cleaned_iterable(self, iterable: dict[Any, Any]) -> dict[Any, Any]: ... + + def _cleaned_iterable(self, iterable): """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format. If a key with a bad format is found (i.e not a int, string, or float), it gets replaced byt its hash using the same process implemented here. @@ -249,7 +255,7 @@ def _cleaned_iterable(self, iterable: Iterable[Any]) -> list[Any] | dict[Any, An def _key_to_hash(key: Any) -> int: return zlib.crc32(json.dumps(key, cls=_CustomEncoder).encode()) - def _iter_check_list(lst: list[Any]) -> list[Any]: + def _iter_check_list(lst: Sequence[Any]) -> list[Any]: processed_list = [None] * len(lst) for i, el in enumerate(lst): el = _Memoizer.check_already_processed(el) @@ -287,8 +293,7 @@ def _iter_check_dict(dct: dict[Any, Any]) -> dict[Any, Any]: elif isinstance(iterable, dict): return _iter_check_dict(iterable) else: - # mypy requires this line, even though it should not be reached. - return iterable + raise TypeError("'iterable' is neither an iterable nor a dictionary.") def encode(self, obj: Any) -> str: """Overriding of :meth:`JSONEncoder.encode`, to make our own process. From 687de5e41d141421e4a30e25ee86e8235e6dfa8d Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 27 Aug 2025 02:14:26 +0200 Subject: [PATCH 09/12] Fix imports --- manim/utils/hashing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 4544d4773d..9a933f2b1a 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -6,10 +6,10 @@ import inspect import json import zlib -from collections.abc import Callable, Hashable, Iterable +from collections.abc import Callable, Hashable, Iterable, Sequence from time import perf_counter from types import FunctionType, MappingProxyType, MethodType, ModuleType -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, overload import numpy as np @@ -236,10 +236,10 @@ def default(self, obj: Any) -> Any: @overload def _cleaned_iterable(self, iterable: Sequence[Any]) -> list[Any]: ... - + @overload def _cleaned_iterable(self, iterable: dict[Any, Any]) -> dict[Any, Any]: ... - + def _cleaned_iterable(self, iterable): """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format. From 6997608fbe2f1eb3b6ecf5193aa32b83fcd83355 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 27 Aug 2025 02:24:00 +0200 Subject: [PATCH 10/12] Fix animations_list typing --- manim/renderer/cairo_renderer.py | 1 + manim/utils/hashing.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 66a1822101..bcbe3a4fc7 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -83,6 +83,7 @@ def play( logger.info("Caching disabled.") hash_current_animation = f"uncached_{self.num_plays:05}" else: + assert scene.animations is not None hash_current_animation = get_hash_from_play_call( scene, self.camera, diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 9a933f2b1a..ece745fd5b 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -333,7 +333,7 @@ def get_json(obj: Any) -> str: def get_hash_from_play_call( scene_object: Scene, camera_object: Camera | OpenGLCamera, - animations_list: Iterable[Animation] | None, + animations_list: Iterable[Animation], current_mobjects_list: Iterable[Mobject], ) -> str: """Take the list of animations and a list of mobjects and output their hashes. This is meant to be used for `scene.play` function. From c3195a5f729fa5a743a790a2de84eccced9ba094 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:06:16 +0200 Subject: [PATCH 11/12] More typing improvements --- manim/utils/hashing.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index ece745fd5b..a11b1c96d2 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -243,7 +243,7 @@ def _cleaned_iterable(self, iterable: dict[Any, Any]) -> dict[Any, Any]: ... def _cleaned_iterable(self, iterable): """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format. - If a key with a bad format is found (i.e not a int, string, or float), it gets replaced byt its hash using the same process implemented here. + If a key with a bad format is found (i.e not a int, string, or float), it gets replaced by its hash using the same process implemented here. If a circular reference is found within the iterable, it will be replaced by the value of ALREADY_PROCESSED_PLACEHOLDER. Parameters @@ -259,7 +259,7 @@ def _iter_check_list(lst: Sequence[Any]) -> list[Any]: processed_list = [None] * len(lst) for i, el in enumerate(lst): el = _Memoizer.check_already_processed(el) - if isinstance(el, (list, tuple)): + if isinstance(el, Sequence): new_value = _iter_check_list(el) elif isinstance(el, dict): new_value = _iter_check_dict(el) @@ -274,21 +274,21 @@ def _iter_check_dict(dct: dict[Any, Any]) -> dict[Any, Any]: v = _Memoizer.check_already_processed(v) if k in KEYS_TO_FILTER_OUT: continue - # We check if the k is of the right format (supporter by Json) + # We check if the k is of the right format (supported by JSON) if not isinstance(k, (str, int, float, bool)) and k is not None: k_new = _key_to_hash(k) else: k_new = k if isinstance(v, dict): new_value = _iter_check_dict(v) - elif isinstance(v, (list, tuple)): + elif isinstance(v, Sequence): new_value = _iter_check_list(v) else: new_value = v processed_dict[k_new] = new_value return processed_dict - if isinstance(iterable, (list, tuple)): + if isinstance(iterable, Sequence): return _iter_check_list(iterable) elif isinstance(iterable, dict): return _iter_check_dict(iterable) @@ -309,7 +309,7 @@ def encode(self, obj: Any) -> str: The object encoder with the standard json process. """ _Memoizer.mark_as_processed(obj) - if isinstance(obj, (dict, list, tuple)): + if isinstance(obj, (dict, Sequence)): return super().encode(self._cleaned_iterable(obj)) return super().encode(obj) From b873c0846623783ea6da9ffbedf878c4e2365d54 Mon Sep 17 00:00:00 2001 From: "F. Muenkel" <25496279+fmuenkel@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:20:36 +0200 Subject: [PATCH 12/12] Revert typing improvements --- manim/utils/hashing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index a11b1c96d2..d9824ef83f 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -220,7 +220,7 @@ def default(self, obj: Any) -> Any: if obj.size > 1000: obj = np.resize(obj, (100, 100)) return f"TRUNCATED ARRAY: {repr(obj)}" - # We return the repr and not a list to avoid the JsonEncoder to iterate over it. + # We return the repr and not a list to avoid the JSONEncoder to iterate over it. return repr(obj) elif hasattr(obj, "__dict__"): temp = obj.__dict__ @@ -259,7 +259,7 @@ def _iter_check_list(lst: Sequence[Any]) -> list[Any]: processed_list = [None] * len(lst) for i, el in enumerate(lst): el = _Memoizer.check_already_processed(el) - if isinstance(el, Sequence): + if isinstance(el, (list, tuple)): new_value = _iter_check_list(el) elif isinstance(el, dict): new_value = _iter_check_dict(el) @@ -281,14 +281,14 @@ def _iter_check_dict(dct: dict[Any, Any]) -> dict[Any, Any]: k_new = k if isinstance(v, dict): new_value = _iter_check_dict(v) - elif isinstance(v, Sequence): + elif isinstance(v, (list, tuple)): new_value = _iter_check_list(v) else: new_value = v processed_dict[k_new] = new_value return processed_dict - if isinstance(iterable, Sequence): + if isinstance(iterable, (list, tuple)): return _iter_check_list(iterable) elif isinstance(iterable, dict): return _iter_check_dict(iterable) @@ -309,7 +309,7 @@ def encode(self, obj: Any) -> str: The object encoder with the standard json process. """ _Memoizer.mark_as_processed(obj) - if isinstance(obj, (dict, Sequence)): + if isinstance(obj, (dict, list, tuple)): return super().encode(self._cleaned_iterable(obj)) return super().encode(obj)