Skip to content

Commit a0414dc

Browse files
henrikmidtibyfmuenkelchopan050behackl
authored
Add type annotations to manim/_config/utils.py (#4230)
* Stop ignoring manim._config erros in mypy.ini * Aspect ratio should be a float. * Handled more mypy issues in _config/utils.py * Handled more mypy issues in _config/utils.py * Removed two assert statements that triggered errors in the unittests. * Fix last mypy issue in utils.py and activate mypy checking * Fix type of window_size in opengl_renderer_window.py * ... --------- Co-authored-by: F. Muenkel <25496279+fmuenkel@users.noreply.github.com> Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com> Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
1 parent 33a0e56 commit a0414dc

2 files changed

Lines changed: 63 additions & 49 deletions

File tree

manim/_config/utils.py

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import os
2121
import re
2222
import sys
23-
from collections.abc import Iterable, Iterator, Mapping, MutableMapping
23+
from collections.abc import Iterator, Mapping, MutableMapping
2424
from pathlib import Path
2525
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
2626

@@ -134,7 +134,7 @@ def make_config_parser(
134134
return parser
135135

136136

137-
def _determine_quality(qual: str) -> str:
137+
def _determine_quality(qual: str | None) -> str:
138138
for quality, values in constants.QUALITIES.items():
139139
if values["flag"] is not None and values["flag"] == qual:
140140
return quality
@@ -338,6 +338,7 @@ def __len__(self) -> int:
338338

339339
def __contains__(self, key: object) -> bool:
340340
try:
341+
assert isinstance(key, str)
341342
self.__getitem__(key)
342343
return True
343344
except AttributeError:
@@ -428,7 +429,7 @@ def __deepcopy__(self, memo: dict[str, Any]) -> Self:
428429
# Deepcopying the underlying dict is enough because all properties
429430
# either read directly from it or compute their value on the fly from
430431
# values read directly from it.
431-
c._d = copy.deepcopy(self._d, memo)
432+
c._d = copy.deepcopy(self._d, memo) # type: ignore[arg-type]
432433
return c
433434

434435
# helper type-checking methods
@@ -655,13 +656,15 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self:
655656
"window_size"
656657
] # if not "default", get a tuple of the position
657658
if window_size != "default":
658-
window_size = tuple(map(int, re.split(r"[;,\-]", window_size)))
659-
self.window_size = window_size
659+
window_size_numbers = tuple(map(int, re.split(r"[;,\-]", window_size)))
660+
self.window_size = window_size_numbers
661+
else:
662+
self.window_size = window_size
660663

661664
# plugins
662665
plugins = parser["CLI"].get("plugins", fallback="", raw=True)
663-
plugins = [] if plugins == "" else plugins.split(",")
664-
self.plugins = plugins
666+
plugin_list = [] if plugins is None or plugins == "" else plugins.split(",")
667+
self.plugins = plugin_list
665668
# the next two must be set AFTER digesting pixel_width and pixel_height
666669
self["frame_height"] = parser["CLI"].getfloat("frame_height", 8.0)
667670
width = parser["CLI"].getfloat("frame_width", None)
@@ -671,31 +674,31 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self:
671674
self["frame_width"] = width
672675

673676
# other logic
674-
val = parser["CLI"].get("tex_template_file")
675-
if val:
676-
self.tex_template_file = val
677+
tex_template_file = parser["CLI"].get("tex_template_file")
678+
if tex_template_file:
679+
self.tex_template_file = Path(tex_template_file)
677680

678-
val = parser["CLI"].get("progress_bar")
679-
if val:
680-
self.progress_bar = val
681+
progress_bar = parser["CLI"].get("progress_bar")
682+
if progress_bar:
683+
self.progress_bar = progress_bar
681684

682-
val = parser["ffmpeg"].get("loglevel")
683-
if val:
684-
self.ffmpeg_loglevel = val
685+
ffmpeg_loglevel = parser["ffmpeg"].get("loglevel")
686+
if ffmpeg_loglevel:
687+
self.ffmpeg_loglevel = ffmpeg_loglevel
685688

686689
try:
687-
val = parser["jupyter"].getboolean("media_embed")
690+
media_embed = parser["jupyter"].getboolean("media_embed")
688691
except ValueError:
689-
val = None
690-
self.media_embed = val
692+
media_embed = None
693+
self.media_embed = media_embed
691694

692-
val = parser["jupyter"].get("media_width")
693-
if val:
694-
self.media_width = val
695+
media_width = parser["jupyter"].get("media_width")
696+
if media_width:
697+
self.media_width = media_width
695698

696-
val = parser["CLI"].get("quality", fallback="", raw=True)
697-
if val:
698-
self.quality = _determine_quality(val)
699+
quality = parser["CLI"].get("quality", fallback="", raw=True)
700+
if quality:
701+
self.quality = _determine_quality(quality)
699702

700703
return self
701704

@@ -1044,7 +1047,7 @@ def verbosity(self, val: str) -> None:
10441047
logger.setLevel(val)
10451048

10461049
@property
1047-
def format(self) -> str:
1050+
def format(self) -> str | None:
10481051
"""File format; "png", "gif", "mp4", "webm" or "mov"."""
10491052
return self._d["format"]
10501053

@@ -1076,7 +1079,7 @@ def ffmpeg_loglevel(self, val: str) -> None:
10761079
logging.getLogger("libav").setLevel(self.ffmpeg_loglevel)
10771080

10781081
@property
1079-
def media_embed(self) -> bool:
1082+
def media_embed(self) -> bool | None:
10801083
"""Whether to embed videos in Jupyter notebook."""
10811084
return self._d["media_embed"]
10821085

@@ -1112,8 +1115,10 @@ def pixel_height(self, value: int) -> None:
11121115
self._set_pos_number("pixel_height", value, False)
11131116

11141117
@property
1115-
def aspect_ratio(self) -> int:
1118+
def aspect_ratio(self) -> float:
11161119
"""Aspect ratio (width / height) in pixels (--resolution, -r)."""
1120+
assert isinstance(self._d["pixel_width"], int)
1121+
assert isinstance(self._d["pixel_height"], int)
11171122
return self._d["pixel_width"] / self._d["pixel_height"]
11181123

11191124
@property
@@ -1137,22 +1142,22 @@ def frame_width(self, value: float) -> None:
11371142
@property
11381143
def frame_y_radius(self) -> float:
11391144
"""Half the frame height (no flag)."""
1140-
return self._d["frame_height"] / 2
1145+
return self._d["frame_height"] / 2 # type: ignore[operator]
11411146

11421147
@frame_y_radius.setter
11431148
def frame_y_radius(self, value: float) -> None:
1144-
self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__(
1149+
self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value]
11451150
"frame_height", 2 * value
11461151
)
11471152

11481153
@property
11491154
def frame_x_radius(self) -> float:
11501155
"""Half the frame width (no flag)."""
1151-
return self._d["frame_width"] / 2
1156+
return self._d["frame_width"] / 2 # type: ignore[operator]
11521157

11531158
@frame_x_radius.setter
11541159
def frame_x_radius(self, value: float) -> None:
1155-
self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__(
1160+
self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value]
11561161
"frame_width", 2 * value
11571162
)
11581163

@@ -1285,7 +1290,7 @@ def frame_size(self) -> tuple[int, int]:
12851290

12861291
@frame_size.setter
12871292
def frame_size(self, value: tuple[int, int]) -> None:
1288-
self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__(
1293+
self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__( # type: ignore[func-returns-value]
12891294
"pixel_height", value[1]
12901295
)
12911296

@@ -1295,7 +1300,7 @@ def quality(self) -> str | None:
12951300
keys = ["pixel_width", "pixel_height", "frame_rate"]
12961301
q = {k: self[k] for k in keys}
12971302
for qual in constants.QUALITIES:
1298-
if all(q[k] == constants.QUALITIES[qual][k] for k in keys):
1303+
if all(q[k] == constants.QUALITIES[qual][k] for k in keys): # type: ignore[literal-required]
12991304
return qual
13001305
return None
13011306

@@ -1312,6 +1317,7 @@ def quality(self, value: str | None) -> None:
13121317
@property
13131318
def transparent(self) -> bool:
13141319
"""Whether the background opacity is less than 1.0 (-t)."""
1320+
assert isinstance(self._d["background_opacity"], float)
13151321
return self._d["background_opacity"] < 1.0
13161322

13171323
@transparent.setter
@@ -1421,12 +1427,12 @@ def window_position(self, value: str) -> None:
14211427
self._d.__setitem__("window_position", value)
14221428

14231429
@property
1424-
def window_size(self) -> str:
1425-
"""The size of the opengl window as 'width,height' or 'default' to automatically scale the window based on the display monitor."""
1430+
def window_size(self) -> str | tuple[int, ...]:
1431+
"""The size of the opengl window. 'default' to automatically scale the window based on the display monitor."""
14261432
return self._d["window_size"]
14271433

14281434
@window_size.setter
1429-
def window_size(self, value: str) -> None:
1435+
def window_size(self, value: str | tuple[int, ...]) -> None:
14301436
self._d.__setitem__("window_size", value)
14311437

14321438
def resolve_movie_file_extension(self, is_transparent: bool) -> None:
@@ -1455,7 +1461,7 @@ def enable_gui(self, value: bool) -> None:
14551461
self._set_boolean("enable_gui", value)
14561462

14571463
@property
1458-
def gui_location(self) -> tuple[Any]:
1464+
def gui_location(self) -> tuple[int, ...]:
14591465
"""Location parameters for the GUI window (e.g., screen coordinates or layout settings)."""
14601466
return self._d["gui_location"]
14611467

@@ -1639,6 +1645,7 @@ def get_dir(self, key: str, **kwargs: Any) -> Path:
16391645
all_args["quality"] = f"{self.pixel_height}p{self.frame_rate:g}"
16401646

16411647
path = self._d[key]
1648+
assert isinstance(path, str)
16421649
while "{" in path:
16431650
try:
16441651
path = path.format(**all_args)
@@ -1738,7 +1745,7 @@ def custom_folders(self, value: str | Path) -> None:
17381745
self._set_dir("custom_folders", value)
17391746

17401747
@property
1741-
def input_file(self) -> str:
1748+
def input_file(self) -> str | Path:
17421749
"""Input file name."""
17431750
return self._d["input_file"]
17441751

@@ -1767,7 +1774,7 @@ def scene_names(self, value: list[str]) -> None:
17671774
@property
17681775
def tex_template(self) -> TexTemplate:
17691776
"""Template used when rendering Tex. See :class:`.TexTemplate`."""
1770-
if not hasattr(self, "_tex_template") or not self._tex_template:
1777+
if not hasattr(self, "_tex_template") or not self._tex_template: # type: ignore[has-type]
17711778
fn = self._d["tex_template_file"]
17721779
if fn:
17731780
self._tex_template = TexTemplate.from_file(fn)
@@ -1803,7 +1810,7 @@ def plugins(self) -> list[str]:
18031810
return self._d["plugins"]
18041811

18051812
@plugins.setter
1806-
def plugins(self, value: list[str]):
1813+
def plugins(self, value: list[str]) -> None:
18071814
self._d["plugins"] = value
18081815

18091816
@property
@@ -1861,15 +1868,15 @@ def __init__(self, c: ManimConfig) -> None:
18611868
self.__dict__["_c"] = c
18621869

18631870
# there are required by parent class Mapping to behave like a dict
1864-
def __getitem__(self, key: str | int) -> Any:
1871+
def __getitem__(self, key: str) -> Any:
18651872
if key in self._OPTS:
18661873
return self._c[key]
18671874
elif key in self._CONSTANTS:
18681875
return self._CONSTANTS[key]
18691876
else:
18701877
raise KeyError(key)
18711878

1872-
def __iter__(self) -> Iterable[str]:
1879+
def __iter__(self) -> Iterator[Any]:
18731880
return iter(list(self._OPTS) + list(self._CONSTANTS))
18741881

18751882
def __len__(self) -> int:
@@ -1887,4 +1894,4 @@ def __delitem__(self, key: Any) -> NoReturn:
18871894

18881895

18891896
for opt in list(ManimFrame._OPTS) + list(ManimFrame._CONSTANTS):
1890-
setattr(ManimFrame, opt, property(lambda self, o=opt: self[o]))
1897+
setattr(ManimFrame, opt, property(lambda self, o=opt: self[o])) # type: ignore[misc]

manim/renderer/opengl_renderer_window.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,23 @@ class Window(PygletWindow):
2525
def __init__(
2626
self,
2727
renderer: OpenGLRenderer,
28-
window_size: str = config.window_size,
28+
window_size: str | tuple[int, ...] = config.window_size,
2929
**kwargs: Any,
3030
) -> None:
3131
monitors = get_monitors()
3232
mon_index = config.window_monitor
3333
monitor = monitors[min(mon_index, len(monitors) - 1)]
3434

35-
if window_size == "default":
35+
invalid_window_size_error_message = (
36+
"window_size must be specified either as 'default', a string of the form "
37+
"'width,height', or a tuple of 2 ints of the form (width, height)."
38+
)
39+
40+
if isinstance(window_size, tuple):
41+
if len(window_size) != 2:
42+
raise ValueError(invalid_window_size_error_message)
43+
size = window_size
44+
elif window_size == "default":
3645
# make window_width half the width of the monitor
3746
# but make it full screen if --fullscreen
3847
window_width = monitor.width
@@ -48,9 +57,7 @@ def __init__(
4857
(window_width, window_height) = tuple(map(int, window_size.split(",")))
4958
size = (window_width, window_height)
5059
else:
51-
raise ValueError(
52-
"Window_size must be specified as 'width,height' or 'default'.",
53-
)
60+
raise ValueError(invalid_window_size_error_message)
5461

5562
super().__init__(size=size)
5663

0 commit comments

Comments
 (0)