Skip to content

Commit 8b84a13

Browse files
author
Oliver Strait
committed
cli_ utils:
- fixing typos and added full docstring - replace regex with python string split module_ops: - full docstrings, typo fixing - handling python internal type-madness with exceptions to please mypy commands: - docstrings, typos, more descriptive names - cleaning version fetch functionality
1 parent a23bc18 commit 8b84a13

File tree

3 files changed

+110
-68
lines changed

3 files changed

+110
-68
lines changed

manim/cli/cli_utils.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
from __future__ import annotations
22

3-
import re
43
import sys
54

65
from manim._config import console
76
from manim.constants import CHOOSE_NUMBER_MESSAGE
87

98
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."
119

10+
NOT_FOUND_IMPORT = "Import statement for Manim was not found. Importing is added."
1211
INPUT_CODE_ENTER = f"Enter the animation code & end with an EOF: {ESCAPE_CHAR}:"
1312

1413

1514
def code_input_prompt() -> str:
15+
"""Little CLI interface in which user can insert code."""
1616
console.print(INPUT_CODE_ENTER)
1717
code = sys.stdin.read()
1818
if len(code.strip()) == 0:
@@ -24,22 +24,27 @@ def code_input_prompt() -> str:
2424
return code
2525

2626

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):
27+
def prompt_user_with_list(items: list[str]) -> list[int]:
28+
"""Prompt user with choices and return indices of chosen items
29+
30+
Parameters
31+
-----------
32+
items
33+
list of strings representing items to be chosen
34+
"""
35+
max_index = len(items) - 1
36+
for count, name in enumerate(items, 1):
3137
console.print(f"{count}: {name}", style="logging.level.info")
3238

3339
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)
40+
result = user_input.strip().rstrip(",").split(",")
41+
cleaned = [n.strip() for n in result]
3742

38-
if not all(a.isnumeric() for a in result):
39-
raise ValueError("Invalid non-numeric input: ", user_input)
43+
if not all(a.isnumeric() for a in cleaned):
44+
raise ValueError(f"Invalid non-numeric input(s): {result}")
4045

41-
indices = [int(i_str.strip()) - 1 for i_str in result]
46+
indices = [int(int_str) - 1 for int_str in cleaned]
4247
if all(a <= max_index >= 0 for a in indices):
4348
return indices
4449
else:
45-
raise KeyError("One or more chooses is outside of range")
50+
raise KeyError("One or more choice is outside of range")

manim/cli/render/commands.py

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
logger,
2727
tempconfig,
2828
)
29-
from manim.cli.cli_utils import code_input_prompt, prompt_user_with_choice
29+
from manim.cli.cli_utils import code_input_prompt, prompt_user_with_list
3030
from manim.cli.render.ease_of_access_options import ease_of_access_options
3131
from manim.cli.render.global_options import global_options
3232
from manim.cli.render.output_options import output_options
@@ -92,15 +92,15 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
9292
9393
SCENES is an optional list of scenes in the file.
9494
"""
95-
warn_and_change_deprecated_args(kwargs)
95+
warn_and_change_deprecated_arguments(kwargs)
9696

9797
click_args = ClickArgs(kwargs)
9898
if kwargs["jupyter"]:
9999
return click_args
100100

101101
config.digest_args(click_args)
102102

103-
scenes = solve_rendrered_scenes(config.input_file)
103+
scenes = scenes_from_input(config.input_file)
104104

105105
if config.renderer == RendererType.OPENGL:
106106
from manim.renderer.opengl_renderer import OpenGLRenderer
@@ -142,33 +142,40 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
142142

143143

144144
def version_notification() -> None:
145-
"""Fetch version from Internet or use cache"""
146-
file = Path(os.path.dirname(__file__)) / ".version_cache.log"
147-
stable = None
148-
149-
if file.exists():
150-
with file.open() as f:
151-
last_time = f.readline()
152-
if not time.time() - int(last_time) > 86_400:
153-
stable = f.readline()
154-
155-
if stable is None:
156-
new_stable = fetch_version()
157-
if new_stable:
158-
with file.open(mode="w") as f:
159-
f.write(str(int(time.time())) + "\n" + str(new_stable))
160-
stable = new_stable
161-
162-
if stable != __version__:
145+
"""Compare used version to latest version of manim.
146+
Version info is fetched from internet once a day and cached into a file.
147+
"""
148+
stable_version = None
149+
150+
cache_file = Path(os.path.dirname(__file__)) / ".version_cache.log"
151+
152+
if cache_file.exists():
153+
with cache_file.open() as f:
154+
cache_lifetime = int(f.readline())
155+
if time.time() < cache_lifetime:
156+
stable_version = f.readline()
157+
158+
if stable_version is None:
159+
version = fetch_version()
160+
if version is None:
161+
return None
162+
163+
with cache_file.open(mode="w") as f:
164+
timecode = int(time.time()) + 86_400
165+
f.write(str(timecode) + "\n" + str(version))
166+
stable_version = version
167+
168+
if stable_version != __version__:
163169
console.print(
164-
f"You are using manim version [red]v{__version__}[/red], but version [green]v{stable}[/green] is available.",
170+
f"You are using manim version [red]v{__version__}[/red], but version [green]v{stable_version}[/green] is available.",
165171
)
166172
console.print(
167173
"You should consider upgrading via [yellow]pip install -U manim[/yellow]",
168174
)
169175

170176

171177
def fetch_version() -> str | None:
178+
"""Fetch latest manim version from PYPI-database"""
172179
import http.client
173180
import urllib.error
174181
import urllib.request
@@ -185,15 +192,17 @@ def fetch_version() -> str | None:
185192
logger.debug(f"{e}: {warn_prompt} ")
186193
return None
187194
except json.JSONDecodeError:
188-
logger.debug(f"Error while decoding JSON from [{manim_info_url}]: warn_prompt")
195+
logger.debug(
196+
f"Error while decoding JSON from [{manim_info_url}]: {warn_prompt}"
197+
)
189198
return None
190199
else:
191200
return str(json_data["info"]["version"])
192201

193202

194-
def warn_and_change_deprecated_args(kwargs: dict[str, Any]) -> None:
195-
"""Helper function to print info about deprecated functions
196-
and mutate inserted dict to contain proper format
203+
def warn_and_change_deprecated_arguments(kwargs: dict[str, Any]) -> None:
204+
"""Helper function to print info about deprecated arguments
205+
and mutate inserted dictionary to use new format
197206
"""
198207
if kwargs["save_as_gif"]:
199208
logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
@@ -210,11 +219,14 @@ def warn_and_change_deprecated_args(kwargs: dict[str, Any]) -> None:
210219

211220

212221
def select_scenes(scene_classes: list[type[Scene]]) -> list[type[Scene]]:
213-
"""Collection of selection checks for inserted scenes"""
214-
if not scene_classes:
215-
logger.error(NO_SCENE_MESSAGE)
216-
return []
217-
elif config.write_all:
222+
"""Assortment of selection functionality in which one or more Scenes are selected from list.
223+
224+
Parameters
225+
----------
226+
scene_classes
227+
list of scene classes that
228+
"""
229+
if config.write_all:
218230
return scene_classes
219231

220232
result = []
@@ -229,13 +241,14 @@ def select_scenes(scene_classes: list[type[Scene]]) -> list[type[Scene]]:
229241
logger.error(SCENE_NOT_FOUND_MESSAGE.format(scene_name))
230242
if result:
231243
return result
244+
232245
if len(scene_classes) == 1:
233246
config.scene_names = [scene_classes[0].__name__]
234247
return [scene_classes[0]]
235248

236249
try:
237250
console.print(f"{MULTIPLE_SCENES}:\n", style="underline white")
238-
scene_indices = prompt_user_with_choice([a.__name__ for a in scene_classes])
251+
scene_indices = prompt_user_with_list([a.__name__ for a in scene_classes])
239252
except Exception as e:
240253
logger.error(f"{e}\n{INVALID_NUMBER_MESSAGE} ")
241254
sys.exit(2)
@@ -248,22 +261,31 @@ def select_scenes(scene_classes: list[type[Scene]]) -> list[type[Scene]]:
248261
return classes
249262

250263

251-
def solve_rendrered_scenes(file_path_input: str) -> list[type[Scene]]:
252-
"""Return scenes from file path or create CLI prompt for input"""
264+
def scenes_from_input(file_path_input: str) -> list[type[Scene]]:
265+
"""Return scenes from file path or create CLI prompt for input
266+
267+
Parameters
268+
----------
269+
file_path_input
270+
file path or '-' that will open a code prompt
271+
"""
253272
from ...scene.scene import Scene
254273

255274
if file_path_input == "-":
256275
try:
257276
code = code_input_prompt()
258277
module = module_from_text(code)
259278
except Exception as e:
260-
logger.error(f" Failed to create from input code: {e}")
279+
logger.error(f"Failed to create from input code: {e}")
261280
sys.exit(2)
262281

263282
logger.info(INPUT_CODE_RENDER)
264283
else:
265284
module = module_from_file(Path(file_path_input))
266285

267-
scenes = search_classes_from_module(module, Scene)
268-
269-
return select_scenes(scenes)
286+
try:
287+
scenes = search_classes_from_module(module, Scene)
288+
return select_scenes(scenes)
289+
except ValueError:
290+
logger.error(NO_SCENE_MESSAGE)
291+
return []

manim/utils/module_ops.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import importlib.machinery
65
import importlib.util
76
import inspect
87
import sys
@@ -15,7 +14,13 @@
1514

1615

1716
def module_from_text(code: str) -> types.ModuleType:
18-
"""Creates a input prompt in which user can insert a code that will be asserted and executed."""
17+
"""Creates a input prompt in which user can insert a code that will be asserted and executed.
18+
19+
Parameters
20+
----------
21+
code
22+
code string
23+
"""
1924
module = types.ModuleType("RuntimeTEXT")
2025
try:
2126
# NOTE Code executer: is needed to resolve imports and other code
@@ -30,25 +35,27 @@ def module_from_file(file_path: Path) -> types.ModuleType:
3035
3136
Parameters
3237
----------
33-
3438
file_path
35-
location of file as path-object
39+
location of python file as path-object
3640
"""
3741
if not file_path.exists() and file_path.suffix == ".py":
3842
raise ValueError(f"{file_path} is not a valid python script.")
3943

4044
module_name = "runtimeFile" + ".".join(file_path.with_suffix("").parts)
4145

4246
warnings.filterwarnings("default", category=DeprecationWarning, module=module_name)
47+
4348
try:
4449
spec = importlib.util.spec_from_file_location(module_name, file_path)
45-
if isinstance(spec, importlib.machinery.ModuleSpec):
46-
module = importlib.util.module_from_spec(spec)
47-
sys.modules[module_name] = module
48-
sys.path.insert(0, str(file_path.parent.absolute()))
49-
spec.loader.exec_module(module)
50-
else:
50+
if spec is None:
5151
raise ValueError("Failed to create ModuleSpec")
52+
elif spec.loader is None:
53+
raise RuntimeError("ModuleSpec has no loader")
54+
55+
module = importlib.util.module_from_spec(spec)
56+
sys.modules[module_name] = module
57+
sys.path.insert(0, str(file_path.parent.absolute()))
58+
spec.loader.exec_module(module)
5259

5360
except Exception as e:
5461
raise RuntimeError("Module creation from file failed") from e
@@ -59,14 +66,14 @@ def module_from_file(file_path: Path) -> types.ModuleType:
5966
def search_classes_from_module(
6067
module: types.ModuleType, class_type: type[T]
6168
) -> list[type[T]]:
62-
"""Search and return all occurrence of specified type classes.
69+
"""Search and return all occurrence of specified class-type.
6370
6471
Parameters
6572
-----------
6673
module
6774
Module object
68-
class_type:
69-
Type of searched classes
75+
class_type
76+
Type of class
7077
"""
7178

7279
def is_child_scene(obj: Any) -> bool:
@@ -77,14 +84,22 @@ def is_child_scene(obj: Any) -> bool:
7784
and obj.__module__.startswith(module.__name__)
7885
)
7986

80-
classes = [member[1] for member in inspect.getmembers(module, is_child_scene)]
87+
classes = [member for __void, member in inspect.getmembers(module, is_child_scene)]
8188

8289
if len(classes) == 0:
8390
raise ValueError(f"Could not found any classes of type {class_type.__name__}")
8491
return classes
8592

8693

87-
def scene_classes_for_gui(path: str, class_type: type[T]) -> list[type[T]]:
88-
"""Specified interface of dearpyGUI to fetch Scene-class instances"""
89-
module = module_from_file(Path(path))
94+
def scene_classes_for_gui(file_path: str | Path, class_type: type[T]) -> list[type[T]]:
95+
"""Special interface only for dearpyGUI to fetch Scene-class instances.
96+
97+
Parameters
98+
-----------
99+
path
100+
file path
101+
class_type
102+
Type of class
103+
"""
104+
module = module_from_file(Path(file_path))
90105
return search_classes_from_module(module, class_type)

0 commit comments

Comments
 (0)