diff --git a/manim/_config/utils.py b/manim/_config/utils.py index 31290f802a..b1e6180ebe 100644 --- a/manim/_config/utils.py +++ b/manim/_config/utils.py @@ -20,7 +20,7 @@ import os import re import sys -from collections.abc import Iterable, Iterator, Mapping, MutableMapping +from collections.abc import Iterator, Mapping, MutableMapping from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, NoReturn @@ -135,7 +135,7 @@ def make_config_parser( return parser -def _determine_quality(qual: str) -> str: +def _determine_quality(qual: str | None) -> str: for quality, values in constants.QUALITIES.items(): if values["flag"] is not None and values["flag"] == qual: return quality @@ -338,6 +338,7 @@ def __len__(self) -> int: def __contains__(self, key: object) -> bool: try: + assert isinstance(key, str) self.__getitem__(key) return True except AttributeError: @@ -428,7 +429,7 @@ def __deepcopy__(self, memo: dict[str, Any]) -> Self: # Deepcopying the underlying dict is enough because all properties # either read directly from it or compute their value on the fly from # values read directly from it. - c._d = copy.deepcopy(self._d, memo) + c._d = copy.deepcopy(self._d, memo) # type: ignore[arg-type] return c # helper type-checking methods @@ -652,13 +653,15 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self: "window_size" ] # if not "default", get a tuple of the position if window_size != "default": - window_size = tuple(map(int, re.split(r"[;,\-]", window_size))) - self.window_size = window_size + window_size_numbers = tuple(map(int, re.split(r"[;,\-]", window_size))) + self.window_size = window_size_numbers + else: + self.window_size = window_size # plugins plugins = parser["CLI"].get("plugins", fallback="", raw=True) - plugins = [] if plugins == "" else plugins.split(",") - self.plugins = plugins + plugin_list = [] if plugins is None or plugins == "" else plugins.split(",") + self.plugins = plugin_list # the next two must be set AFTER digesting pixel_width and pixel_height self["frame_height"] = parser["CLI"].getfloat("frame_height", 8.0) width = parser["CLI"].getfloat("frame_width", None) @@ -668,31 +671,31 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self: self["frame_width"] = width # other logic - val = parser["CLI"].get("tex_template_file") - if val: - self.tex_template_file = val + tex_template_file = parser["CLI"].get("tex_template_file") + if tex_template_file: + self.tex_template_file = Path(tex_template_file) - val = parser["CLI"].get("progress_bar") - if val: - self.progress_bar = val + progress_bar = parser["CLI"].get("progress_bar") + if progress_bar: + self.progress_bar = progress_bar - val = parser["ffmpeg"].get("loglevel") - if val: - self.ffmpeg_loglevel = val + ffmpeg_loglevel = parser["ffmpeg"].get("loglevel") + if ffmpeg_loglevel: + self.ffmpeg_loglevel = ffmpeg_loglevel try: - val = parser["jupyter"].getboolean("media_embed") + media_embed = parser["jupyter"].getboolean("media_embed") except ValueError: - val = None - self.media_embed = val + media_embed = None + self.media_embed = media_embed - val = parser["jupyter"].get("media_width") - if val: - self.media_width = val + media_width = parser["jupyter"].get("media_width") + if media_width: + self.media_width = media_width - val = parser["CLI"].get("quality", fallback="", raw=True) - if val: - self.quality = _determine_quality(val) + quality = parser["CLI"].get("quality", fallback="", raw=True) + if quality: + self.quality = _determine_quality(quality) return self @@ -1040,7 +1043,7 @@ def verbosity(self, val: str) -> None: logger.setLevel(val) @property - def format(self) -> str: + def format(self) -> str | None: """File format; "png", "gif", "mp4", "webm" or "mov".""" return self._d["format"] @@ -1072,7 +1075,7 @@ def ffmpeg_loglevel(self, val: str) -> None: logging.getLogger("libav").setLevel(self.ffmpeg_loglevel) @property - def media_embed(self) -> bool: + def media_embed(self) -> bool | None: """Whether to embed videos in Jupyter notebook.""" return self._d["media_embed"] @@ -1108,8 +1111,10 @@ def pixel_height(self, value: int) -> None: self._set_pos_number("pixel_height", value, False) @property - def aspect_ratio(self) -> int: + def aspect_ratio(self) -> float: """Aspect ratio (width / height) in pixels (--resolution, -r).""" + assert isinstance(self._d["pixel_width"], int) + assert isinstance(self._d["pixel_height"], int) return self._d["pixel_width"] / self._d["pixel_height"] @property @@ -1133,22 +1138,22 @@ def frame_width(self, value: float) -> None: @property def frame_y_radius(self) -> float: """Half the frame height (no flag).""" - return self._d["frame_height"] / 2 + return self._d["frame_height"] / 2 # type: ignore[operator] @frame_y_radius.setter def frame_y_radius(self, value: float) -> None: - self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__( + self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value] "frame_height", 2 * value ) @property def frame_x_radius(self) -> float: """Half the frame width (no flag).""" - return self._d["frame_width"] / 2 + return self._d["frame_width"] / 2 # type: ignore[operator] @frame_x_radius.setter def frame_x_radius(self, value: float) -> None: - self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__( + self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value] "frame_width", 2 * value ) @@ -1281,7 +1286,7 @@ def frame_size(self) -> tuple[int, int]: @frame_size.setter def frame_size(self, value: tuple[int, int]) -> None: - self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__( + self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__( # type: ignore[func-returns-value] "pixel_height", value[1] ) @@ -1291,7 +1296,7 @@ def quality(self) -> str | None: keys = ["pixel_width", "pixel_height", "frame_rate"] q = {k: self[k] for k in keys} for qual in constants.QUALITIES: - if all(q[k] == constants.QUALITIES[qual][k] for k in keys): + if all(q[k] == constants.QUALITIES[qual][k] for k in keys): # type: ignore[literal-required] return qual return None @@ -1308,6 +1313,7 @@ def quality(self, value: str | None) -> None: @property def transparent(self) -> bool: """Whether the background opacity is less than 1.0 (-t).""" + assert isinstance(self._d["background_opacity"], float) return self._d["background_opacity"] < 1.0 @transparent.setter @@ -1417,12 +1423,14 @@ def window_position(self, value: str) -> None: self._d.__setitem__("window_position", value) @property - def window_size(self) -> str: - """The size of the opengl window as 'width,height' or 'default' to automatically scale the window based on the display monitor.""" + def window_size(self) -> str | tuple[int, ...]: + """The size of the opengl window as ``(width, height)`` or ``'default'`` to automatically + scale the window based on the display monitor. + """ return self._d["window_size"] @window_size.setter - def window_size(self, value: str) -> None: + def window_size(self, value: str | tuple[int, ...]) -> None: self._d.__setitem__("window_size", value) def resolve_movie_file_extension(self, is_transparent: bool) -> None: @@ -1451,7 +1459,7 @@ def enable_gui(self, value: bool) -> None: self._set_boolean("enable_gui", value) @property - def gui_location(self) -> tuple[Any]: + def gui_location(self) -> tuple[int, ...]: """Location parameters for the GUI window (e.g., screen coordinates or layout settings).""" return self._d["gui_location"] @@ -1635,6 +1643,7 @@ def get_dir(self, key: str, **kwargs: Any) -> Path: all_args["quality"] = f"{self.pixel_height}p{self.frame_rate:g}" path = self._d[key] + assert isinstance(path, str) while "{" in path: try: path = path.format(**all_args) @@ -1734,7 +1743,7 @@ def custom_folders(self, value: str | Path) -> None: self._set_dir("custom_folders", value) @property - def input_file(self) -> str: + def input_file(self) -> str | Path: """Input file name.""" return self._d["input_file"] @@ -1763,7 +1772,7 @@ def scene_names(self, value: list[str]) -> None: @property def tex_template(self) -> TexTemplate: """Template used when rendering Tex. See :class:`.TexTemplate`.""" - if not hasattr(self, "_tex_template") or not self._tex_template: + if not hasattr(self, "_tex_template") or not self._tex_template: # type: ignore[has-type] fn = self._d["tex_template_file"] if fn: self._tex_template = TexTemplate.from_file(fn) @@ -1799,7 +1808,7 @@ def plugins(self) -> list[str]: return self._d["plugins"] @plugins.setter - def plugins(self, value: list[str]): + def plugins(self, value: list[str]) -> None: self._d["plugins"] = value @@ -1846,7 +1855,7 @@ def __init__(self, c: ManimConfig) -> None: self.__dict__["_c"] = c # there are required by parent class Mapping to behave like a dict - def __getitem__(self, key: str | int) -> Any: + def __getitem__(self, key: str) -> Any: if key in self._OPTS: return self._c[key] elif key in self._CONSTANTS: @@ -1854,7 +1863,7 @@ def __getitem__(self, key: str | int) -> Any: else: raise KeyError(key) - def __iter__(self) -> Iterable[str]: + def __iter__(self) -> Iterator[Any]: return iter(list(self._OPTS) + list(self._CONSTANTS)) def __len__(self) -> int: @@ -1872,4 +1881,4 @@ def __delitem__(self, key: Any) -> NoReturn: for opt in list(ManimFrame._OPTS) + list(ManimFrame._CONSTANTS): - setattr(ManimFrame, opt, property(lambda self, o=opt: self[o])) + setattr(ManimFrame, opt, property(lambda self, o=opt: self[o])) # type: ignore[misc] diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 4472ba6c2a..3cf2215d26 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -25,14 +25,23 @@ class Window(PygletWindow): def __init__( self, renderer: OpenGLRenderer, - window_size: str = config.window_size, + window_size: str | tuple[int, ...] = config.window_size, **kwargs: Any, ) -> None: monitors = get_monitors() mon_index = config.window_monitor monitor = monitors[min(mon_index, len(monitors) - 1)] - if window_size == "default": + invalid_window_size_error_message = ( + "window_size must be specified either as 'default', a string of the form " + "'width,height', or a tuple of 2 ints of the form (width, height)." + ) + + if isinstance(window_size, tuple): + if len(window_size) != 2: + raise ValueError(invalid_window_size_error_message) + size = window_size + elif window_size == "default": # make window_width half the width of the monitor # but make it full screen if --fullscreen window_width = monitor.width @@ -48,9 +57,7 @@ def __init__( (window_width, window_height) = tuple(map(int, window_size.split(","))) size = (window_width, window_height) else: - raise ValueError( - "Window_size must be specified as 'width,height' or 'default'.", - ) + raise ValueError(invalid_window_size_error_message) super().__init__(size=size)