Skip to content

Commit 4c6b6b6

Browse files
e4coderjsonvillanuevakilacoda
authored
Added project management commands (#1418)
* added update_cfg() and copy_template_files() * added select_resolution() and project command * removed curses * added init command * added graph scene template * worked on doc strings and finished new project command * added moving camera scene template * changed pixel_height and pixel width * added docstring for select resolution changed default.cfg to template.cfg * removed the import status as i will be adding it programatically * added add_import_statment for programatically adding import statment of manim * fixed console.print() * fixed class name * add class name and prompt user for template name, if template name not given * added scene command * fixed extension issue * worked on docstrings * fixed formating on docstring * added docstring to add_import_statement() * added manim init test * manim init test complete passing * fixed issue with test * added manim new command test * fixed class names and class name clashes * added TEMPLATE_NAMES * fixed scene.replace * changed default to Default * removed TEMPLATE_NAMES and added get_template_names function * changed templates file extension to avoid check fails * moving init command from manim/cli/projects to manim/cli/init * moving templates from manim/cli/projects/templates to manim/templates * removed init command from manim/cli/project/commands.py * fixed docstring and removed console * moving new command group from manim/cli/project/commands.py to manim/cli/new/group.py * fixed docstrings * moved utility functions to file_ops.py * added import statments of new and init from their respective directories * clean up * fixed docstrings * changed file name of manim/cli/cfg/commands.py to group.py * fixed punctuation * removed unnecessory statements and improved docstring * removed unnecessory comment * clean up of manim/cli/new/group.py * fixed major issue with code * further cleanup * new command group final touches * Update manim/cli/init/commands.py Co-authored-by: Jason Villanueva <a@jsonvillanueva.com> * Update manim/cli/new/group.py Co-authored-by: Jason Villanueva <a@jsonvillanueva.com> * fixed broken tests * Update manim/cli/init/commands.py Co-authored-by: Raghav Goel <kilacoda@gmail.com> Co-authored-by: Jason Villanueva <a@jsonvillanueva.com> Co-authored-by: Raghav Goel <kilacoda@gmail.com>
1 parent 6d8e8a5 commit 4c6b6b6

File tree

12 files changed

+389
-1
lines changed

12 files changed

+389
-1
lines changed

manim/__main__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from click_default_group import DefaultGroup
55

66
from . import __version__, console
7-
from .cli.cfg.commands import cfg
7+
from .cli.cfg.group import cfg
8+
from .cli.init.commands import init
9+
from .cli.new.group import new
810
from .cli.plugins.commands import plugins
911
from .cli.render.commands import render
1012
from .constants import EPILOG
@@ -41,6 +43,8 @@ def main(ctx):
4143

4244
main.add_command(cfg)
4345
main.add_command(plugins)
46+
main.add_command(init)
47+
main.add_command(new)
4448
main.add_command(render)
4549

4650
if __name__ == "__main__":
File renamed without changes.

manim/cli/init/__init__.py

Whitespace-only changes.

manim/cli/init/commands.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Manim's init subcommand.
2+
3+
Manim's init subcommand is accessed in the command-line interface via ``manim
4+
init``. Here you can specify options, subcommands, and subgroups for the init
5+
group.
6+
7+
"""
8+
from pathlib import Path
9+
10+
import click
11+
12+
from ...constants import CONTEXT_SETTINGS, EPILOG
13+
from ...utils.file_ops import copy_template_files
14+
15+
16+
@click.command(
17+
context_settings=CONTEXT_SETTINGS,
18+
epilog=EPILOG,
19+
)
20+
def init():
21+
"""Sets up a project in current working directory with default settings.
22+
23+
It copies files from templates directory and pastes them in the current working dir.
24+
25+
The new project is set up with default settings.
26+
"""
27+
cfg = Path("manim.cfg")
28+
if cfg.exists():
29+
raise FileExistsError(f"\t{cfg} exists\n")
30+
else:
31+
copy_template_files()

manim/cli/new/__init__.py

Whitespace-only changes.

manim/cli/new/group.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import configparser
2+
from pathlib import Path
3+
4+
import click
5+
6+
from ... import console
7+
from ...constants import CONTEXT_SETTINGS, EPILOG, QUALITIES
8+
from ...utils.file_ops import (
9+
add_import_statement,
10+
copy_template_files,
11+
get_template_names,
12+
get_template_path,
13+
)
14+
15+
CFG_DEFAULTS = {
16+
"frame_rate": 30,
17+
"background_color": "BLACK",
18+
"background_opacity": 1,
19+
"scene_names": "Default",
20+
"resolution": (854, 480),
21+
}
22+
23+
24+
def select_resolution():
25+
"""Prompts input of type click.Choice from user. Presents options from QUALITIES constant.
26+
27+
Returns
28+
-------
29+
:class:`tuple`
30+
Tuple containing height and width.
31+
"""
32+
resolution_options = []
33+
for quality in QUALITIES.items():
34+
resolution_options.append(
35+
(quality[1]["pixel_height"], quality[1]["pixel_width"])
36+
)
37+
resolution_options.pop()
38+
choice = click.prompt(
39+
"\nSelect resolution:\n",
40+
type=click.Choice([f"{i[0]}p" for i in resolution_options]),
41+
show_default=False,
42+
default="480p",
43+
)
44+
return [res for res in resolution_options if f"{res[0]}p" == choice][0]
45+
46+
47+
def update_cfg(cfg_dict, project_cfg_path):
48+
"""Updates the manim.cfg file after reading it from the project_cfg_path.
49+
50+
Parameters
51+
----------
52+
cfg : :class:`dict`
53+
values used to update manim.cfg found project_cfg_path.
54+
project_cfg_path : :class:`Path`
55+
Path of manim.cfg file.
56+
"""
57+
config = configparser.ConfigParser()
58+
config.read(project_cfg_path)
59+
cli_config = config["CLI"]
60+
for key, value in cfg_dict.items():
61+
if key == "resolution":
62+
cli_config["pixel_height"] = str(value[0])
63+
cli_config["pixel_width"] = str(value[1])
64+
else:
65+
cli_config[key] = str(value)
66+
67+
with open(project_cfg_path, "w") as conf:
68+
config.write(conf)
69+
70+
71+
@click.command(
72+
context_settings=CONTEXT_SETTINGS,
73+
epilog=EPILOG,
74+
)
75+
@click.argument("project_name", type=Path, required=False)
76+
@click.option(
77+
"-d",
78+
"--default",
79+
"default_settings",
80+
is_flag=True,
81+
help="Default settings for project creation.",
82+
nargs=1,
83+
)
84+
def project(default_settings, **args):
85+
"""Creates a new project.
86+
87+
PROJECT_NAME is the name of the folder in which the new project will be initialized.
88+
"""
89+
if args["project_name"]:
90+
project_name = args["project_name"]
91+
else:
92+
project_name = click.prompt("Project Name", type=Path)
93+
94+
# in the future when implementing a full template system. Choices are going to be saved in some sort of config file for templates
95+
template_name = click.prompt(
96+
"Template",
97+
type=click.Choice(get_template_names(), False),
98+
default="Default",
99+
)
100+
101+
if project_name.is_dir():
102+
console.print(
103+
f"\nFolder [red]{project_name}[/red] exists. Please type another name\n"
104+
)
105+
else:
106+
project_name.mkdir()
107+
new_cfg = dict()
108+
new_cfg_path = Path.resolve(project_name / "manim.cfg")
109+
110+
if not default_settings:
111+
for key, value in CFG_DEFAULTS.items():
112+
if key == "scene_names":
113+
new_cfg[key] = template_name + "Template"
114+
elif key == "resolution":
115+
new_cfg[key] = select_resolution()
116+
else:
117+
new_cfg[key] = click.prompt(f"\n{key}", default=value)
118+
119+
console.print("\n", new_cfg)
120+
if click.confirm("Do you want to continue?", default=True, abort=True):
121+
copy_template_files(project_name, template_name)
122+
update_cfg(new_cfg, new_cfg_path)
123+
else:
124+
copy_template_files(project_name, template_name)
125+
update_cfg(CFG_DEFAULTS, new_cfg_path)
126+
127+
128+
@click.command(
129+
context_settings=CONTEXT_SETTINGS,
130+
no_args_is_help=True,
131+
epilog=EPILOG,
132+
)
133+
@click.argument("scene_name", type=str, required=True)
134+
@click.argument("file_name", type=str, required=False)
135+
def scene(**args):
136+
"""Inserts a SCENE to an existing FILE or creates a new FILE.
137+
138+
SCENE is the name of the scene that will be inserted.
139+
140+
FILE is the name of file in which the SCENE will be inserted.
141+
"""
142+
if not Path("main.py").exists():
143+
raise FileNotFoundError(f"{Path('main.py')} : Not a valid project direcotory.")
144+
145+
template_name = click.prompt(
146+
"template",
147+
type=click.Choice(get_template_names(), False),
148+
default="Default",
149+
)
150+
scene = ""
151+
with open(Path.resolve(get_template_path() / f"{template_name}.mtp")) as f:
152+
scene = f.read()
153+
scene = scene.replace(template_name + "Template", args["scene_name"], 1)
154+
155+
if args["file_name"]:
156+
file_name = Path(args["file_name"] + ".py")
157+
158+
if file_name.is_file():
159+
# file exists so we are going to append new scene to that file
160+
with open(file_name, "a") as f:
161+
f.write("\n\n\n" + scene)
162+
pass
163+
else:
164+
# file does not exist so we create a new file, append the scene and prepend the import statement
165+
with open(file_name, "w") as f:
166+
f.write("\n\n\n" + scene)
167+
168+
add_import_statement(file_name)
169+
else:
170+
# file name is not provided so we assume it is main.py
171+
# if main.py does not exist we do not continue
172+
with open(Path("main.py"), "a") as f:
173+
f.write("\n\n\n" + scene)
174+
175+
176+
@click.group(
177+
context_settings=CONTEXT_SETTINGS,
178+
invoke_without_command=True,
179+
no_args_is_help=True,
180+
epilog=EPILOG,
181+
help="Create a new project or insert a new scene.",
182+
)
183+
@click.pass_context
184+
def new(ctx):
185+
pass
186+
187+
188+
new.add_command(project)
189+
new.add_command(scene)

manim/templates/Axes.mtp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class AxesTemplate(Scene):
2+
def construct(self):
3+
graph = Axes(
4+
x_range=[-1,10,1],
5+
y_range=[-1,10,1],
6+
x_length=9,
7+
y_length=6,
8+
axis_config={"include_tip":False}
9+
)
10+
labels = graph.get_axis_labels()
11+
self.add(graph, labels)

manim/templates/Default.mtp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class DefaultTemplate(Scene):
2+
def construct(self):
3+
circle = Circle() # create a circle
4+
circle.set_fill(PINK, opacity=0.5) # set color and transparency
5+
6+
square = Square() # create a square
7+
square.flip(RIGHT) # flip horizontally
8+
square.rotate(-3 * TAU / 8) # rotate a certain amount
9+
10+
self.play(Create(square)) # animate the creation of the square
11+
self.play(Transform(square, circle)) # interpolate the square into the circle
12+
self.play(FadeOut(square)) # fade out animation

manim/templates/MovingCamera.mtp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class MovingCameraTemplate(MovingCameraScene):
2+
def construct(self):
3+
text = Text("Hello World").set_color(BLUE)
4+
self.add(text)
5+
self.camera.frame.save_state()
6+
self.play(self.camera.frame.animate.set(width=text.width * 1.2))
7+
self.wait(0.3)
8+
self.play(Restore(self.camera.frame))

manim/templates/template.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[CLI]
2+
frame_rate = 30
3+
pixel_height = 480
4+
pixel_width = 854
5+
background_color = BLACK
6+
background_opacity = 1
7+
scene_names = DefaultScene

manim/utils/file_ops.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
import subprocess as sp
1515
import time
1616
from pathlib import Path
17+
from shutil import copyfile
1718

1819
from manim import __version__, config, logger
1920

21+
from .. import console
22+
2023

2124
def add_extension_if_not_present(file_name, extension):
2225
if file_name.suffix != extension:
@@ -96,3 +99,67 @@ def open_media_file(file_writer):
9699
open_file(file_path, False)
97100

98101
logger.info(f"Previewed File at: {file_path}")
102+
103+
104+
def get_template_names():
105+
"""Returns template names from the templates directory.
106+
107+
Returns
108+
-------
109+
:class:`list`
110+
"""
111+
template_path = Path.resolve(Path(__file__).parent.parent / "templates")
112+
return [template_name.stem for template_name in template_path.glob("*.mtp")]
113+
114+
115+
def get_template_path():
116+
"""Returns the Path of templates directory.
117+
118+
Returns
119+
-------
120+
:class:`Path`
121+
"""
122+
return Path.resolve(Path(__file__).parent.parent / "templates")
123+
124+
125+
def add_import_statement(file):
126+
"""Prepends an import statment in a file
127+
128+
Parameters
129+
----------
130+
file : :class:`Path`
131+
"""
132+
with open(file, "r+") as f:
133+
import_line = "from manim import *"
134+
content = f.read()
135+
f.seek(0, 0)
136+
f.write(import_line.rstrip("\r\n") + "\n" + content)
137+
138+
139+
def copy_template_files(project_dir=Path("."), template_name="Default"):
140+
"""Copies template files from templates dir to project_dir.
141+
142+
Parameters
143+
----------
144+
project_dir : :class:`Path`
145+
Path to project directory.
146+
template_name : :class:`str`
147+
Name of template.
148+
"""
149+
template_cfg_path = Path.resolve(
150+
Path(__file__).parent.parent / "templates/template.cfg"
151+
)
152+
template_scene_path = Path.resolve(
153+
Path(__file__).parent.parent / f"templates/{template_name}.mtp"
154+
)
155+
156+
if not template_cfg_path.exists():
157+
raise FileNotFoundError(f"{template_cfg_path} : file does not exist")
158+
if not template_scene_path.exists():
159+
raise FileNotFoundError(f"{template_scene_path} : file does not exist")
160+
161+
copyfile(template_cfg_path, Path.resolve(project_dir / "manim.cfg"))
162+
console.print("\n\t[green]copied[/green] [blue]manim.cfg[/blue]\n")
163+
copyfile(template_scene_path, Path.resolve(project_dir / "main.py"))
164+
console.print("\n\t[green]copied[/green] [blue]main.py[/blue]\n")
165+
add_import_statement(Path.resolve(project_dir / "main.py"))

0 commit comments

Comments
 (0)