Skip to content

Commit 5d5136e

Browse files
author
Oliver Strait
committed
Cleaner implementation of module and runtime Scene
generation. - Module code does not know manim anymore - Two CLI helper function in new cli_utils.py - Commands are only place that has knowledge of wider program - Every CLI choise and desicion is moved inside of Commands
1 parent 2790a70 commit 5d5136e

File tree

4 files changed

+251
-202
lines changed

4 files changed

+251
-202
lines changed

manim/cli/cli_utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import sys
5+
6+
from manim._config import console
7+
from manim.constants import CHOOSE_NUMBER_MESSAGE
8+
9+
ESCAPE_CHAR = "CTRL+Z" if sys.platform == "win32" else "CTRL+D"
10+
NOT_FOUND_IMPORT = "Import statement for Manim was not found. Importing is added."
11+
12+
INPUT_CODE_ENTER = f"Enter the animation code & end with an EOF: {ESCAPE_CHAR}:"
13+
14+
15+
def code_input_prompt() -> str:
16+
console.print(INPUT_CODE_ENTER)
17+
code = sys.stdin.read()
18+
if len(code.strip()) == 0:
19+
raise ValueError("Empty input of code")
20+
21+
if not code.startswith("from manim import"):
22+
console.print(NOT_FOUND_IMPORT, style="logging.level.warning")
23+
code = "from manim import *\n" + code
24+
return code
25+
26+
27+
def prompt_user_with_choice(choise_list: list[str]) -> list[int]:
28+
"""Prompt user with chooses and return indices of choised items"""
29+
max_index = len(choise_list)
30+
for count, name in enumerate(choise_list, 1):
31+
console.print(f"{count}: {name}", style="logging.level.info")
32+
33+
user_input = console.input(CHOOSE_NUMBER_MESSAGE)
34+
# CTRL + Z, CTRL + D, Remove common EOF escape chars
35+
cleaned = user_input.strip().removesuffix("\x1a").removesuffix("\x04")
36+
result = re.split(r"\s*,\s*", cleaned)
37+
38+
if not all(a.isnumeric() for a in result):
39+
raise ValueError("Invalid non-numeric input: ", user_input)
40+
41+
indices = [int(i_str.strip()) - 1 for i_str in result]
42+
if all(a <= max_index >= 0 for a in indices):
43+
return indices
44+
else:
45+
raise KeyError("One or more chooses is outside of range")

manim/cli/render/commands.py

Lines changed: 143 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import urllib.request
1616
from argparse import Namespace
1717
from pathlib import Path
18-
from typing import Any, cast
18+
from typing import TYPE_CHECKING, Any, cast
1919

2020
import cloup
2121

@@ -27,15 +27,33 @@
2727
logger,
2828
tempconfig,
2929
)
30+
from manim.cli.cli_utils import code_input_prompt, prompt_user_with_choice
3031
from manim.cli.render.ease_of_access_options import ease_of_access_options
3132
from manim.cli.render.global_options import global_options
3233
from manim.cli.render.output_options import output_options
3334
from manim.cli.render.render_options import render_options
34-
from manim.constants import EPILOG, RendererType
35-
from manim.utils.module_ops import scene_classes_from_file
35+
from manim.constants import (
36+
EPILOG,
37+
INVALID_NUMBER_MESSAGE,
38+
NO_SCENE_MESSAGE,
39+
SCENE_NOT_FOUND_MESSAGE,
40+
RendererType,
41+
)
42+
from manim.scene.scene_file_writer import SceneFileWriter
43+
from manim.utils.module_ops import (
44+
module_from_file,
45+
module_from_text,
46+
search_classes_from_module,
47+
)
3648

3749
__all__ = ["render"]
3850

51+
if TYPE_CHECKING:
52+
from ...scene.scene import Scene
53+
54+
INPUT_CODE_RENDER = "Rendering animation from typed code"
55+
MULTIPLE_SCENES = "Found multiple scenes. Choose at least one to continue"
56+
3957

4058
class ClickArgs(Namespace):
4159
def __init__(self, args: dict[str, Any]) -> None:
@@ -75,50 +93,41 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
7593
7694
SCENES is an optional list of scenes in the file.
7795
"""
78-
if kwargs["save_as_gif"]:
79-
logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
80-
kwargs["format"] = "gif"
81-
82-
if kwargs["save_pngs"]:
83-
logger.warning("--save_pngs is deprecated, please use --format=png instead!")
84-
kwargs["format"] = "png"
85-
86-
if kwargs["show_in_file_browser"]:
87-
logger.warning(
88-
"The short form of show_in_file_browser is deprecated and will be moved to support --format.",
89-
)
96+
warn_and_change_deprecated_args(kwargs)
9097

9198
click_args = ClickArgs(kwargs)
9299
if kwargs["jupyter"]:
93100
return click_args
94101

95102
config.digest_args(click_args)
96103
file = Path(config.input_file)
104+
scenes = solve_rendrered_scenes(file)
105+
97106
if config.renderer == RendererType.OPENGL:
98107
from manim.renderer.opengl_renderer import OpenGLRenderer
99108

100109
try:
101110
renderer = OpenGLRenderer()
102111
keep_running = True
103112
while keep_running:
104-
for SceneClass in scene_classes_from_file(file):
113+
for SceneClass in scenes:
105114
with tempconfig({}):
106115
scene = SceneClass(renderer)
107116
rerun = scene.render()
108-
if rerun or config["write_all"]:
117+
if rerun or config.write_all:
109118
renderer.num_plays = 0
110119
continue
111120
else:
112121
keep_running = False
113122
break
114-
if config["write_all"]:
123+
if config.write_all:
115124
keep_running = False
116125

117126
except Exception:
118127
error_console.print_exception()
119128
sys.exit(1)
120129
else:
121-
for SceneClass in scene_classes_from_file(file):
130+
for SceneClass in scenes:
122131
try:
123132
with tempconfig({}):
124133
scene = SceneClass()
@@ -128,34 +137,121 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
128137
sys.exit(1)
129138

130139
if config.notify_outdated_version:
131-
manim_info_url = "https://pypi.org/pypi/manim/json"
132-
warn_prompt = "Cannot check if latest release of manim is installed"
140+
version_notification()
133141

134-
try:
135-
with urllib.request.urlopen(
136-
urllib.request.Request(manim_info_url),
137-
timeout=10,
138-
) as response:
139-
response = cast(http.client.HTTPResponse, response)
140-
json_data = json.loads(response.read())
141-
except urllib.error.HTTPError:
142-
logger.debug("HTTP Error: %s", warn_prompt)
143-
except urllib.error.URLError:
144-
logger.debug("URL Error: %s", warn_prompt)
145-
except json.JSONDecodeError:
146-
logger.debug(
147-
"Error while decoding JSON from %r: %s", manim_info_url, warn_prompt
142+
return kwargs
143+
144+
145+
def version_notification() -> None:
146+
### NOTE TODO This has fundamental problem of connecting every time into internet
147+
### As many times Renders are executed during a day.
148+
### There should be a caching mechanisim that will safe simple timecode and result in
149+
### Cached file to be fetched in most of times.
150+
151+
manim_info_url = "https://pypi.org/pypi/manim/json"
152+
warn_prompt = "Cannot check if latest release of manim is installed"
153+
154+
try:
155+
with urllib.request.urlopen(
156+
urllib.request.Request(manim_info_url),
157+
timeout=10,
158+
) as response:
159+
response = cast(http.client.HTTPResponse, response)
160+
json_data = json.loads(response.read())
161+
except urllib.error.HTTPError:
162+
logger.debug("HTTP Error: %s", warn_prompt)
163+
except urllib.error.URLError:
164+
logger.debug("URL Error: %s", warn_prompt)
165+
except json.JSONDecodeError:
166+
logger.debug(
167+
"Error while decoding JSON from %r: %s", manim_info_url, warn_prompt
168+
)
169+
except Exception:
170+
logger.debug("Something went wrong: %s", warn_prompt)
171+
else:
172+
stable = json_data["info"]["version"]
173+
174+
if stable != __version__:
175+
console.print(
176+
f"You are using manim version [red]v{__version__}[/red], but version [green]v{stable}[/green] is available.",
177+
)
178+
console.print(
179+
"You should consider upgrading via [yellow]pip install -U manim[/yellow]",
148180
)
149-
except Exception:
150-
logger.debug("Something went wrong: %s", warn_prompt)
151-
else:
152-
stable = json_data["info"]["version"]
153-
if stable != __version__:
154-
console.print(
155-
f"You are using manim version [red]v{__version__}[/red], but version [green]v{stable}[/green] is available.",
156-
)
157-
console.print(
158-
"You should consider upgrading via [yellow]pip install -U manim[/yellow]",
159-
)
160181

161-
return kwargs
182+
183+
def warn_and_change_deprecated_args(kwargs: dict[str, Any]) -> None:
184+
"""Helper function to print info about deprecated functions
185+
and mutate inserted dict to contain proper format
186+
"""
187+
if kwargs["save_as_gif"]:
188+
logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
189+
kwargs["format"] = "gif"
190+
191+
if kwargs["save_pngs"]:
192+
logger.warning("--save_pngs is deprecated, please use --format=png instead!")
193+
kwargs["format"] = "png"
194+
195+
if kwargs["show_in_file_browser"]:
196+
logger.warning(
197+
"The short form of show_in_file_browser is deprecated and will be moved to support --format.",
198+
)
199+
200+
201+
def get_scenes_to_render(scene_classes: list[type[Scene]]) -> list[type[Scene]]:
202+
result = []
203+
for scene_name in config.scene_names:
204+
found = False
205+
for scene_class in scene_classes:
206+
if scene_class.__name__ == scene_name:
207+
result.append(scene_class)
208+
found = True
209+
break
210+
if not found and (scene_name != ""):
211+
logger.error(SCENE_NOT_FOUND_MESSAGE.format(scene_name))
212+
if result:
213+
return result
214+
if len(scene_classes) == 1:
215+
config.scene_names = [scene_classes[0].__name__]
216+
return [scene_classes[0]]
217+
218+
try:
219+
console.print(f"{MULTIPLE_SCENES}:\n", style="underline white")
220+
scene_indices = prompt_user_with_choice([a.__name__ for a in scene_classes])
221+
except Exception as e:
222+
logger.error(f"{e}\n{INVALID_NUMBER_MESSAGE} ")
223+
sys.exit(2)
224+
225+
classes = [scene_classes[i] for i in scene_indices]
226+
227+
config.scene_names = [scene_class.__name__ for scene_class in classes]
228+
SceneFileWriter.force_output_as_scene_name = True
229+
230+
return classes
231+
232+
233+
def solve_rendrered_scenes(file_path_input: Path | str) -> list[type[Scene]]:
234+
"""Return scenes from file path or create CLI prompt for input"""
235+
from ...scene.scene import Scene
236+
237+
if str(file_path_input) == "-":
238+
try:
239+
code = code_input_prompt()
240+
module = module_from_text(code)
241+
except Exception as e:
242+
logger.error(f" Failed to create from input code: {e}")
243+
sys.exit(2)
244+
245+
logger.info(INPUT_CODE_RENDER)
246+
else:
247+
module = module_from_file(Path(file_path_input))
248+
249+
scenes = search_classes_from_module(module, Scene)
250+
251+
if not scenes:
252+
logger.error(NO_SCENE_MESSAGE)
253+
return []
254+
elif config.write_all:
255+
return scenes
256+
else:
257+
return get_scenes_to_render(scenes)

manim/constants.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@
8383
{} is not in the script
8484
"""
8585
CHOOSE_NUMBER_MESSAGE = """
86-
Choose number corresponding to desired scene/arguments.
87-
(Use comma separated list for multiple entries)
86+
Select one or more numbers separated by commas (e.q. 3,1,2).
8887
Choice(s): """
8988
INVALID_NUMBER_MESSAGE = "Invalid scene numbers have been specified. Aborting."
9089
NO_SCENE_MESSAGE = """

0 commit comments

Comments
 (0)