Coverage for rendercv/cli/commands.py: 100%
110 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +0000
1"""
2The `rendercv.cli.commands` module contains all the command-line interface (CLI)
3commands of RenderCV.
4"""
6import os
7import pathlib
8from typing import Annotated, Optional
10import typer
11from rich import print
13from .. import __version__, data, renderer
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[
41 str, typer.Argument(help="Name of the YAML input file.")
42 ],
43 use_local_latex_command: Annotated[
44 Optional[str],
45 typer.Option(
46 "--use-local-latex-command",
47 "-use",
48 help=(
49 "Use the local LaTeX installation with the given command instead of the"
50 " RenderCV's TinyTeX."
51 ),
52 ),
53 ] = None,
54 output_folder_name: Annotated[
55 str,
56 typer.Option(
57 "--output-folder-name",
58 "-o",
59 help="Name of the output folder.",
60 ),
61 ] = "rendercv_output",
62 latex_path: Annotated[
63 Optional[str],
64 typer.Option(
65 "--latex-path",
66 "-latex",
67 help="Copy the LaTeX file to the given path.",
68 ),
69 ] = None,
70 pdf_path: Annotated[
71 Optional[str],
72 typer.Option(
73 "--pdf-path",
74 "-pdf",
75 help="Copy the PDF file to the given path.",
76 ),
77 ] = None,
78 markdown_path: Annotated[
79 Optional[str],
80 typer.Option(
81 "--markdown-path",
82 "-md",
83 help="Copy the Markdown file to the given path.",
84 ),
85 ] = None,
86 html_path: Annotated[
87 Optional[str],
88 typer.Option(
89 "--html-path",
90 "-html",
91 help="Copy the HTML file to the given path.",
92 ),
93 ] = None,
94 png_path: Annotated[
95 Optional[str],
96 typer.Option(
97 "--png-path",
98 "-png",
99 help="Copy the PNG file to the given path.",
100 ),
101 ] = None,
102 dont_generate_markdown: Annotated[
103 bool,
104 typer.Option(
105 "--dont-generate-markdown",
106 "-nomd",
107 help="Don't generate the Markdown and HTML file.",
108 ),
109 ] = False,
110 dont_generate_html: Annotated[
111 bool,
112 typer.Option(
113 "--dont-generate-html",
114 "-nohtml",
115 help="Don't generate the HTML file.",
116 ),
117 ] = False,
118 dont_generate_png: Annotated[
119 bool,
120 typer.Option(
121 "--dont-generate-png",
122 "-nopng",
123 help="Don't generate the PNG file.",
124 ),
125 ] = False,
126 # This is a dummy argument for the help message for
127 # extra_data_model_override_argumets:
128 _: Annotated[
129 Optional[str],
130 typer.Option(
131 "--YAMLLOCATION",
132 help="Overrides the value of YAMLLOCATION. For example,"
133 ' [cyan bold]--cv.phone "123-456-7890"[/cyan bold].',
134 ),
135 ] = None,
136 extra_data_model_override_argumets: typer.Context = None, # type: ignore
137):
138 """Render a CV from a YAML input file."""
139 printer.welcome()
141 input_file_path: pathlib.Path = pathlib.Path(input_file_name).absolute()
142 original_working_directory = pathlib.Path.cwd()
144 input_file_as_a_dict = data.read_a_yaml_file(input_file_path)
146 # Update the input file if there are extra override arguments (for example,
147 # --cv.phone "123-456-7890"):
148 if extra_data_model_override_argumets:
149 key_and_values = utilities.parse_render_command_override_arguments(
150 extra_data_model_override_argumets
151 )
152 input_file_as_a_dict = utilities.set_or_update_values(
153 input_file_as_a_dict, key_and_values
154 )
156 # If non-default CLI arguments are provided, override the `rendercv_settings.render_command`:
157 cli_render_arguments = {
158 "use_local_latex_command": use_local_latex_command,
159 "output_folder_name": output_folder_name,
160 "latex_path": latex_path,
161 "pdf_path": pdf_path,
162 "markdown_path": markdown_path,
163 "html_path": html_path,
164 "png_path": png_path,
165 "dont_generate_png": dont_generate_png,
166 "dont_generate_markdown": dont_generate_markdown,
167 "dont_generate_html": dont_generate_html,
168 }
169 input_file_as_a_dict = utilities.update_render_command_settings_of_the_input_file(
170 input_file_as_a_dict, cli_render_arguments
171 )
172 render_command_settings_dict = input_file_as_a_dict["rendercv_settings"][
173 "render_command"
174 ]
176 # Compute the number of steps
177 # 1. Validate the input file.
178 # 2. Create the LaTeX file.
179 # 3. Render PDF from LaTeX.
180 # 4. Render PNGs from PDF.
181 # 5. Create the Markdown file.
182 # 6. Render HTML from Markdown.
183 number_of_steps = 6
184 if render_command_settings_dict["dont_generate_png"]:
185 number_of_steps -= 1
187 if render_command_settings_dict["dont_generate_markdown"]:
188 number_of_steps -= 2
189 else:
190 if render_command_settings_dict["dont_generate_html"]:
191 number_of_steps -= 1
193 with printer.LiveProgressReporter(number_of_steps=number_of_steps) as progress:
194 progress.start_a_step("Validating the input file")
196 data_model = data.validate_input_dictionary_and_return_the_data_model(
197 input_file_as_a_dict
198 )
200 render_command_settings: data.models.RenderCommandSettings = (
201 data_model.rendercv_settings.render_command # type: ignore
202 ) # type: ignore
203 output_directory = (
204 original_working_directory / render_command_settings.output_folder_name # type: ignore
205 )
207 progress.finish_the_current_step()
209 # Change the current working directory to the input file's directory (because
210 # the template overrides are looked up in the current working directory). The
211 # output files will be in the original working directory.
212 os.chdir(input_file_path.parent)
214 progress.start_a_step("Generating the LaTeX file")
216 latex_file_path_in_output_folder = (
217 renderer.create_a_latex_file_and_copy_theme_files(
218 data_model, output_directory
219 )
220 )
221 if render_command_settings.latex_path:
222 utilities.copy_files(
223 latex_file_path_in_output_folder,
224 render_command_settings.latex_path,
225 )
227 progress.finish_the_current_step()
229 progress.start_a_step("Rendering the LaTeX file to a PDF")
231 pdf_file_path_in_output_folder = renderer.render_a_pdf_from_latex(
232 latex_file_path_in_output_folder,
233 render_command_settings.use_local_latex_command,
234 )
235 if render_command_settings.pdf_path:
236 utilities.copy_files(
237 pdf_file_path_in_output_folder,
238 render_command_settings.pdf_path,
239 )
241 progress.finish_the_current_step()
243 if not render_command_settings.dont_generate_png:
244 progress.start_a_step("Rendering PNG files from the PDF")
246 png_file_paths_in_output_folder = renderer.render_pngs_from_pdf(
247 pdf_file_path_in_output_folder
248 )
249 if render_command_settings.png_path:
250 utilities.copy_files(
251 png_file_paths_in_output_folder,
252 render_command_settings.png_path,
253 )
255 progress.finish_the_current_step()
257 if not render_command_settings.dont_generate_markdown:
258 progress.start_a_step("Generating the Markdown file")
260 markdown_file_path_in_output_folder = renderer.create_a_markdown_file(
261 data_model, output_directory
262 )
263 if render_command_settings.markdown_path:
264 utilities.copy_files(
265 markdown_file_path_in_output_folder,
266 render_command_settings.markdown_path,
267 )
269 progress.finish_the_current_step()
271 if not render_command_settings.dont_generate_html:
272 progress.start_a_step(
273 "Rendering the Markdown file to a HTML (for Grammarly)"
274 )
276 html_file_path_in_output_folder = renderer.render_an_html_from_markdown(
277 markdown_file_path_in_output_folder
278 )
279 if render_command_settings.html_path:
280 utilities.copy_files(
281 html_file_path_in_output_folder,
282 render_command_settings.html_path,
283 )
285 progress.finish_the_current_step()
288@app.command(
289 name="new",
290 help=(
291 "Generate a YAML input file to get started. Example: [yellow]rendercv new"
292 ' "John Doe"[/yellow]. Details: [cyan]rendercv new --help[/cyan]'
293 ),
294)
295def cli_command_new(
296 full_name: Annotated[str, typer.Argument(help="Your full name.")],
297 theme: Annotated[
298 str,
299 typer.Option(
300 help=(
301 "The name of the theme. Available themes are:"
302 f" {', '.join(data.available_themes)}."
303 )
304 ),
305 ] = "classic",
306 dont_create_theme_source_files: Annotated[
307 bool,
308 typer.Option(
309 "--dont-create-theme-source-files",
310 "-nolatex",
311 help="Don't create theme source files.",
312 ),
313 ] = False,
314 dont_create_markdown_source_files: Annotated[
315 bool,
316 typer.Option(
317 "--dont-create-markdown-source-files",
318 "-nomd",
319 help="Don't create the Markdown source files.",
320 ),
321 ] = False,
322):
323 """Generate a YAML input file and the LaTeX and Markdown source files."""
324 created_files_and_folders = []
326 input_file_name = f"{full_name.replace(' ', '_')}_CV.yaml"
327 input_file_path = pathlib.Path(input_file_name)
329 if input_file_path.exists():
330 printer.warning(
331 f'The input file "{input_file_name}" already exists! A new input file is'
332 " not created."
333 )
334 else:
335 try:
336 data.create_a_sample_yaml_input_file(
337 input_file_path, name=full_name, theme=theme
338 )
339 created_files_and_folders.append(input_file_path.name)
340 except ValueError as e:
341 # if the theme is not in the available themes, then raise an error
342 printer.error(exception=e)
344 if not dont_create_theme_source_files:
345 # copy the package's theme files to the current directory
346 theme_folder = utilities.copy_templates(theme, pathlib.Path.cwd())
347 if theme_folder is not None:
348 created_files_and_folders.append(theme_folder.name)
349 else:
350 printer.warning(
351 f'The theme folder "{theme}" already exists! The theme files are not'
352 " created."
353 )
355 if not dont_create_markdown_source_files:
356 # copy the package's markdown files to the current directory
357 markdown_folder = utilities.copy_templates("markdown", pathlib.Path.cwd())
358 if markdown_folder is not None:
359 created_files_and_folders.append(markdown_folder.name)
360 else:
361 printer.warning(
362 'The "markdown" folder already exists! The Markdown files are not'
363 " created."
364 )
366 if len(created_files_and_folders) > 0:
367 created_files_and_folders_string = ",\n".join(created_files_and_folders)
368 printer.information(
369 "The following RenderCV input file and folders have been"
370 f" created:\n{created_files_and_folders_string}"
371 )
374@app.command(
375 name="create-theme",
376 help=(
377 "Create a custom theme folder based on an existing theme. Example:"
378 " [yellow]rendercv create-theme customtheme[/yellow]. Details: [cyan]rendercv"
379 " create-theme --help[/cyan]"
380 ),
381)
382def cli_command_create_theme(
383 theme_name: Annotated[
384 str,
385 typer.Argument(help="The name of the new theme."),
386 ],
387 based_on: Annotated[
388 str,
389 typer.Option(
390 help=(
391 "The name of the existing theme to base the new theme on. Available"
392 f" themes are: {', '.join(data.available_themes)}."
393 )
394 ),
395 ] = "classic",
396):
397 """Create a custom theme based on an existing theme."""
398 if based_on not in data.available_themes:
399 printer.error(
400 f'The theme "{based_on}" is not in the list of available themes:'
401 f' {", ".join(data.available_themes)}'
402 )
404 theme_folder = utilities.copy_templates(
405 based_on, pathlib.Path.cwd(), new_folder_name=theme_name
406 )
408 if theme_folder is None:
409 printer.warning(
410 f'The theme folder "{theme_name}" already exists! The theme files are not'
411 " created."
412 )
413 return
415 based_on_theme_directory = (
416 pathlib.Path(__file__).parent.parent / "themes" / based_on
417 )
418 based_on_theme_init_file = based_on_theme_directory / "__init__.py"
419 based_on_theme_init_file_contents = based_on_theme_init_file.read_text()
421 # generate the new init file:
422 class_name = f"{theme_name.capitalize()}ThemeOptions"
423 literal_name = f'Literal["{theme_name}"]'
424 new_init_file_contents = based_on_theme_init_file_contents.replace(
425 f'Literal["{based_on}"]', literal_name
426 ).replace(f"{based_on.capitalize()}ThemeOptions", class_name)
428 # create the new __init__.py file:
429 (theme_folder / "__init__.py").write_text(new_init_file_contents)
431 printer.information(f'The theme folder "{theme_folder.name}" has been created.')
434@app.callback()
435def cli_command_no_args(
436 version_requested: Annotated[
437 Optional[bool], typer.Option("--version", "-v", help="Show the version.")
438 ] = None,
439):
440 if version_requested:
441 there_is_a_new_version = printer.warn_if_new_version_is_available()
442 if not there_is_a_new_version:
443 print(f"RenderCV v{__version__}")