Skip to content
Merged
95 changes: 51 additions & 44 deletions manim/_config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -134,7 +134,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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -655,13 +656,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)
Expand All @@ -671,31 +674,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

Expand Down Expand Up @@ -1044,7 +1047,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"]

Expand Down Expand Up @@ -1076,7 +1079,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"]

Expand Down Expand Up @@ -1112,8 +1115,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
Expand All @@ -1137,22 +1142,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
)

Expand Down Expand Up @@ -1285,7 +1290,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]
)

Expand All @@ -1295,7 +1300,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

Expand All @@ -1312,6 +1317,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
Expand Down Expand Up @@ -1421,12 +1427,12 @@ 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. '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:
Expand Down Expand Up @@ -1455,7 +1461,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"]

Expand Down Expand Up @@ -1639,6 +1645,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)
Expand Down Expand Up @@ -1738,7 +1745,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"]

Expand Down Expand Up @@ -1767,7 +1774,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)
Expand Down Expand Up @@ -1803,7 +1810,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

@property
Expand Down Expand Up @@ -1861,15 +1868,15 @@ 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:
return self._CONSTANTS[key]
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:
Expand All @@ -1887,4 +1894,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]
17 changes: 12 additions & 5 deletions manim/renderer/opengl_renderer_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down