Coverage for rendercv/cli/commands.py: 93%
76 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
1"""
2The `rendercv.cli.commands` module contains all the command-line interface (CLI)
3commands of RenderCV.
4"""
6import copy
7import pathlib
8from typing import Annotated, Optional
10import typer
11from rich import print
13from .. import __version__, data
14from . import printer, utilities
16app = typer.Typer(
17 rich_markup_mode="rich",
18 add_completion=False,
19 # to make `rendercv --version` work:
20 invoke_without_command=True,
21 no_args_is_help=True,
22 context_settings={"help_option_names": ["-h", "--help"]},
23 # don't show local variables in unhandled exceptions:
24 pretty_exceptions_show_locals=False,
25)
28@app.command(
29 name="render",
30 help=(
31 "Render a YAML input file. Example: [yellow]rendercv render"
32 " John_Doe_CV.yaml[/yellow]. Details: [cyan]rendercv render --help[/cyan]"
33 ),
34 # allow extra arguments for updating the data model (for overriding the values of
35 # the input file):
36 context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
37)
38@printer.handle_and_print_raised_exceptions
39def cli_command_render(
40 input_file_name: Annotated[str, typer.Argument(help="The YAML input file.")],
41 design: Annotated[
42 Optional[str],
43 typer.Option(
44 "--design",
45 "-d",
46 help='The "design" field\'s YAML input file.',
47 ),
48 ] = None,
49 locale: Annotated[
50 Optional[str],
51 typer.Option(
52 "--locale-catalog",
53 "-lc",
54 help='The "locale" field\'s YAML input file.',
55 ),
56 ] = None,
57 rendercv_settings: Annotated[
58 Optional[str],
59 typer.Option(
60 "--rendercv-settings",
61 "-rs",
62 help='The "rendercv_settings" field\'s YAML input file.',
63 ),
64 ] = None,
65 output_folder_name: Annotated[
66 str,
67 typer.Option(
68 "--output-folder-name",
69 "-o",
70 help="Name of the output folder",
71 ),
72 ] = "rendercv_output",
73 typst_path: Annotated[
74 Optional[str],
75 typer.Option(
76 "--typst-path",
77 "-typst",
78 help="Copy the Typst file to the given path",
79 ),
80 ] = None,
81 pdf_path: Annotated[
82 Optional[str],
83 typer.Option(
84 "--pdf-path",
85 "-pdf",
86 help="Copy the PDF file to the given path",
87 ),
88 ] = None,
89 markdown_path: Annotated[
90 Optional[str],
91 typer.Option(
92 "--markdown-path",
93 "-md",
94 help="Copy the Markdown file to the given path",
95 ),
96 ] = None,
97 html_path: Annotated[
98 Optional[str],
99 typer.Option(
100 "--html-path",
101 "-html",
102 help="Copy the HTML file to the given path",
103 ),
104 ] = None,
105 png_path: Annotated[
106 Optional[str],
107 typer.Option(
108 "--png-path",
109 "-png",
110 help="Copy the PNG file to the given path",
111 ),
112 ] = None,
113 dont_generate_markdown: Annotated[
114 bool,
115 typer.Option(
116 "--dont-generate-markdown",
117 "-nomd",
118 help="Don't generate the Markdown and HTML file",
119 ),
120 ] = False,
121 dont_generate_html: Annotated[
122 bool,
123 typer.Option(
124 "--dont-generate-html",
125 "-nohtml",
126 help="Don't generate the HTML file",
127 ),
128 ] = False,
129 dont_generate_png: Annotated[
130 bool,
131 typer.Option(
132 "--dont-generate-png",
133 "-nopng",
134 help="Don't generate the PNG file",
135 ),
136 ] = False,
137 watch: Annotated[
138 bool,
139 typer.Option(
140 "--watch",
141 "-w",
142 help="Automatically re-run RenderCV when the input file is updated",
143 ),
144 ] = False,
145 # This is a dummy argument for the help message for
146 # extra_data_model_override_argumets:
147 _: Annotated[
148 Optional[str],
149 typer.Option(
150 "--YAMLLOCATION",
151 help="Overrides the value of YAMLLOCATION. For example,"
152 ' [cyan bold]--cv.phone "123-456-7890"[/cyan bold].',
153 ),
154 ] = None,
155 extra_data_model_override_arguments: typer.Context = None, # type: ignore
156):
157 """Render a CV from a YAML input file."""
158 printer.welcome()
159 original_working_directory = pathlib.Path.cwd()
160 input_file_path = pathlib.Path(input_file_name).absolute()
162 from . import utilities as u
164 argument_names = list(u.get_default_render_command_cli_arguments().keys())
165 argument_names.remove("_")
166 argument_names.remove("extra_data_model_override_arguments")
167 # This is where the user input is accessed and stored:
168 variables = copy.copy(locals())
169 cli_render_arguments = {name: variables[name] for name in argument_names}
171 input_file_as_a_dict = u.read_and_construct_the_input(
172 input_file_path, cli_render_arguments, extra_data_model_override_arguments
173 )
175 watch = input_file_as_a_dict["rendercv_settings"]["render_command"]["watch"]
177 if watch:
179 @printer.handle_and_print_raised_exceptions_without_exit
180 def run_rendercv():
181 input_file_as_a_dict = u.update_render_command_settings_of_the_input_file(
182 data.read_a_yaml_file(input_file_path), cli_render_arguments
183 )
184 u.run_rendercv_with_printer(
185 input_file_as_a_dict, original_working_directory, input_file_path
186 )
188 u.run_a_function_if_a_file_changes(input_file_path, run_rendercv)
189 else:
190 u.run_rendercv_with_printer(
191 input_file_as_a_dict, original_working_directory, input_file_path
192 )
195@app.command(
196 name="new",
197 help=(
198 "Generate a YAML input file to get started. Example: [yellow]rendercv new"
199 ' "John Doe"[/yellow]. Details: [cyan]rendercv new --help[/cyan]'
200 ),
201)
202def cli_command_new(
203 full_name: Annotated[str, typer.Argument(help="Your full name")],
204 theme: Annotated[
205 str,
206 typer.Option(
207 help=(
208 "The name of the theme (available themes are:"
209 f" {', '.join(data.available_themes)})"
210 )
211 ),
212 ] = "classic",
213 dont_create_theme_source_files: Annotated[
214 bool,
215 typer.Option(
216 "--dont-create-theme-source-files",
217 "-notypst",
218 help="Don't create theme source files",
219 ),
220 ] = False,
221 dont_create_markdown_source_files: Annotated[
222 bool,
223 typer.Option(
224 "--dont-create-markdown-source-files",
225 "-nomd",
226 help="Don't create the Markdown source files",
227 ),
228 ] = False,
229):
230 """Generate a YAML input file and the Typst and Markdown source files"""
231 created_files_and_folders = []
233 input_file_name = f"{full_name.replace(' ', '_')}_CV.yaml"
234 input_file_path = pathlib.Path(input_file_name)
236 if input_file_path.exists():
237 printer.warning(
238 f'The input file "{input_file_name}" already exists! A new input file is'
239 " not created"
240 )
241 else:
242 try:
243 data.create_a_sample_yaml_input_file(
244 input_file_path, name=full_name, theme=theme
245 )
246 created_files_and_folders.append(input_file_path.name)
247 except ValueError as e:
248 # if the theme is not in the available themes, then raise an error
249 printer.error(exception=e)
251 if not dont_create_theme_source_files:
252 # copy the package's theme files to the current directory
253 theme_folder = utilities.copy_templates(theme, pathlib.Path.cwd())
254 if theme_folder is not None:
255 created_files_and_folders.append(theme_folder.name)
256 else:
257 printer.warning(
258 f'The theme folder "{theme}" already exists! The theme files are not'
259 " created"
260 )
262 if not dont_create_markdown_source_files:
263 # copy the package's markdown files to the current directory
264 markdown_folder = utilities.copy_templates("markdown", pathlib.Path.cwd())
265 if markdown_folder is not None:
266 created_files_and_folders.append(markdown_folder.name)
267 else:
268 printer.warning(
269 'The "markdown" folder already exists! The Markdown files are not'
270 " created"
271 )
273 if len(created_files_and_folders) > 0:
274 created_files_and_folders_string = ",\n".join(created_files_and_folders)
275 printer.information(
276 "The following RenderCV input file and folders have been"
277 f" created:\n{created_files_and_folders_string}"
278 )
281@app.command(
282 name="create-theme",
283 help=(
284 "Create a custom theme folder based on an existing theme. Example:"
285 " [yellow]rendercv create-theme customtheme[/yellow]. Details: [cyan]rendercv"
286 " create-theme --help[/cyan]"
287 ),
288)
289def cli_command_create_theme(
290 theme_name: Annotated[
291 str,
292 typer.Argument(help="The name of the new theme"),
293 ],
294 based_on: Annotated[
295 str,
296 typer.Option(
297 help=(
298 "The name of the existing theme to base the new theme on (available"
299 f" themes are: {', '.join(data.available_themes)})"
300 )
301 ),
302 ] = "classic",
303):
304 """Create a custom theme based on an existing theme"""
305 if based_on not in data.available_themes:
306 printer.error(
307 f'The theme "{based_on}" is not in the list of available themes:'
308 f" {', '.join(data.available_themes)}"
309 )
311 theme_folder = utilities.copy_templates(
312 based_on, pathlib.Path.cwd(), new_folder_name=theme_name
313 )
315 if theme_folder is None:
316 printer.warning(
317 f'The theme folder "{theme_name}" already exists! The theme files are not'
318 " created"
319 )
320 return
322 based_on_theme_directory = (
323 pathlib.Path(__file__).parent.parent / "themes" / based_on
324 )
325 based_on_theme_init_file = based_on_theme_directory / "__init__.py"
326 based_on_theme_init_file_contents = based_on_theme_init_file.read_text()
328 # generate the new init file:
329 class_name = f"{theme_name.capitalize()}ThemeOptions"
330 literal_name = f'Literal["{theme_name}"]'
331 new_init_file_contents = based_on_theme_init_file_contents.replace(
332 f'Literal["{based_on}"]', literal_name
333 ).replace(f"{based_on.capitalize()}ThemeOptions", class_name)
335 # create the new __init__.py file:
336 (theme_folder / "__init__.py").write_text(new_init_file_contents)
338 printer.information(f'The theme folder "{theme_folder.name}" has been created.')
341@app.callback()
342def cli_command_no_args(
343 version_requested: Annotated[
344 Optional[bool], typer.Option("--version", "-v", help="Show the version")
345 ] = None,
346):
347 if version_requested:
348 there_is_a_new_version = printer.warn_if_new_version_is_available()
349 if not there_is_a_new_version:
350 print(f"RenderCV v{__version__}")