Coverage for rendercv/cli/commands.py: 93%
76 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-12-25 23:06 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-12-25 23:06 +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_catalog: Annotated[
50 Optional[str],
51 typer.Option(
52 "--locale-catalog",
53 "-lc",
54 help='The "locale_catalog" 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 use_local_latex_command: Annotated[
66 Optional[str],
67 typer.Option(
68 "--use-local-latex-command",
69 "-use",
70 help=(
71 "Use the local LaTeX installation with the given command instead of the"
72 " RenderCV's TinyTeX"
73 ),
74 ),
75 ] = None,
76 output_folder_name: Annotated[
77 str,
78 typer.Option(
79 "--output-folder-name",
80 "-o",
81 help="Name of the output folder",
82 ),
83 ] = "rendercv_output",
84 latex_path: Annotated[
85 Optional[str],
86 typer.Option(
87 "--latex-path",
88 "-latex",
89 help="Copy the LaTeX file to the given path",
90 ),
91 ] = None,
92 pdf_path: Annotated[
93 Optional[str],
94 typer.Option(
95 "--pdf-path",
96 "-pdf",
97 help="Copy the PDF file to the given path",
98 ),
99 ] = None,
100 markdown_path: Annotated[
101 Optional[str],
102 typer.Option(
103 "--markdown-path",
104 "-md",
105 help="Copy the Markdown file to the given path",
106 ),
107 ] = None,
108 html_path: Annotated[
109 Optional[str],
110 typer.Option(
111 "--html-path",
112 "-html",
113 help="Copy the HTML file to the given path",
114 ),
115 ] = None,
116 png_path: Annotated[
117 Optional[str],
118 typer.Option(
119 "--png-path",
120 "-png",
121 help="Copy the PNG file to the given path",
122 ),
123 ] = None,
124 dont_generate_markdown: Annotated[
125 bool,
126 typer.Option(
127 "--dont-generate-markdown",
128 "-nomd",
129 help="Don't generate the Markdown and HTML file",
130 ),
131 ] = False,
132 dont_generate_html: Annotated[
133 bool,
134 typer.Option(
135 "--dont-generate-html",
136 "-nohtml",
137 help="Don't generate the HTML file",
138 ),
139 ] = False,
140 dont_generate_png: Annotated[
141 bool,
142 typer.Option(
143 "--dont-generate-png",
144 "-nopng",
145 help="Don't generate the PNG file",
146 ),
147 ] = False,
148 watch: Annotated[
149 bool,
150 typer.Option(
151 "--watch",
152 "-w",
153 help="Automatically re-run RenderCV when the input file is updated",
154 ),
155 ] = False,
156 # This is a dummy argument for the help message for
157 # extra_data_model_override_argumets:
158 _: Annotated[
159 Optional[str],
160 typer.Option(
161 "--YAMLLOCATION",
162 help="Overrides the value of YAMLLOCATION. For example,"
163 ' [cyan bold]--cv.phone "123-456-7890"[/cyan bold].',
164 ),
165 ] = None,
166 extra_data_model_override_arguments: typer.Context = None, # type: ignore
167):
168 """Render a CV from a YAML input file."""
169 printer.welcome()
170 original_working_directory = pathlib.Path.cwd()
171 input_file_path = pathlib.Path(input_file_name).absolute()
173 from . import utilities as u
175 argument_names = list(u.get_default_render_command_cli_arguments().keys())
176 argument_names.remove("_")
177 argument_names.remove("extra_data_model_override_arguments")
178 # This is where the user input is accessed and stored:
179 variables = copy.copy(locals())
180 cli_render_arguments = {name: variables[name] for name in argument_names}
182 input_file_as_a_dict = u.read_and_construct_the_input(
183 input_file_path, cli_render_arguments, extra_data_model_override_arguments
184 )
186 watch = input_file_as_a_dict["rendercv_settings"]["render_command"]["watch"]
188 if watch:
190 @printer.handle_and_print_raised_exceptions_without_exit
191 def run_rendercv():
192 input_file_as_a_dict = u.update_render_command_settings_of_the_input_file(
193 data.read_a_yaml_file(input_file_path), cli_render_arguments
194 )
195 u.run_rendercv_with_printer(
196 input_file_as_a_dict, original_working_directory, input_file_path
197 )
199 u.run_a_function_if_a_file_changes(input_file_path, run_rendercv)
200 else:
201 u.run_rendercv_with_printer(
202 input_file_as_a_dict, original_working_directory, input_file_path
203 )
206@app.command(
207 name="new",
208 help=(
209 "Generate a YAML input file to get started. Example: [yellow]rendercv new"
210 ' "John Doe"[/yellow]. Details: [cyan]rendercv new --help[/cyan]'
211 ),
212)
213def cli_command_new(
214 full_name: Annotated[str, typer.Argument(help="Your full name")],
215 theme: Annotated[
216 str,
217 typer.Option(
218 help=(
219 "The name of the theme (available themes are:"
220 f" {', '.join(data.available_themes)})"
221 )
222 ),
223 ] = "classic",
224 dont_create_theme_source_files: Annotated[
225 bool,
226 typer.Option(
227 "--dont-create-theme-source-files",
228 "-nolatex",
229 help="Don't create theme source files",
230 ),
231 ] = False,
232 dont_create_markdown_source_files: Annotated[
233 bool,
234 typer.Option(
235 "--dont-create-markdown-source-files",
236 "-nomd",
237 help="Don't create the Markdown source files",
238 ),
239 ] = False,
240):
241 """Generate a YAML input file and the LaTeX and Markdown source files"""
242 created_files_and_folders = []
244 input_file_name = f"{full_name.replace(' ', '_')}_CV.yaml"
245 input_file_path = pathlib.Path(input_file_name)
247 if input_file_path.exists():
248 printer.warning(
249 f'The input file "{input_file_name}" already exists! A new input file is'
250 " not created"
251 )
252 else:
253 try:
254 data.create_a_sample_yaml_input_file(
255 input_file_path, name=full_name, theme=theme
256 )
257 created_files_and_folders.append(input_file_path.name)
258 except ValueError as e:
259 # if the theme is not in the available themes, then raise an error
260 printer.error(exception=e)
262 if not dont_create_theme_source_files:
263 # copy the package's theme files to the current directory
264 theme_folder = utilities.copy_templates(theme, pathlib.Path.cwd())
265 if theme_folder is not None:
266 created_files_and_folders.append(theme_folder.name)
267 else:
268 printer.warning(
269 f'The theme folder "{theme}" already exists! The theme files are not'
270 " created"
271 )
273 if not dont_create_markdown_source_files:
274 # copy the package's markdown files to the current directory
275 markdown_folder = utilities.copy_templates("markdown", pathlib.Path.cwd())
276 if markdown_folder is not None:
277 created_files_and_folders.append(markdown_folder.name)
278 else:
279 printer.warning(
280 'The "markdown" folder already exists! The Markdown files are not'
281 " created"
282 )
284 if len(created_files_and_folders) > 0:
285 created_files_and_folders_string = ",\n".join(created_files_and_folders)
286 printer.information(
287 "The following RenderCV input file and folders have been"
288 f" created:\n{created_files_and_folders_string}"
289 )
292@app.command(
293 name="create-theme",
294 help=(
295 "Create a custom theme folder based on an existing theme. Example:"
296 " [yellow]rendercv create-theme customtheme[/yellow]. Details: [cyan]rendercv"
297 " create-theme --help[/cyan]"
298 ),
299)
300def cli_command_create_theme(
301 theme_name: Annotated[
302 str,
303 typer.Argument(help="The name of the new theme"),
304 ],
305 based_on: Annotated[
306 str,
307 typer.Option(
308 help=(
309 "The name of the existing theme to base the new theme on (available"
310 f" themes are: {', '.join(data.available_themes)})"
311 )
312 ),
313 ] = "classic",
314):
315 """Create a custom theme based on an existing theme"""
316 if based_on not in data.available_themes:
317 printer.error(
318 f'The theme "{based_on}" is not in the list of available themes:'
319 f' {", ".join(data.available_themes)}'
320 )
322 theme_folder = utilities.copy_templates(
323 based_on, pathlib.Path.cwd(), new_folder_name=theme_name
324 )
326 if theme_folder is None:
327 printer.warning(
328 f'The theme folder "{theme_name}" already exists! The theme files are not'
329 " created"
330 )
331 return
333 based_on_theme_directory = (
334 pathlib.Path(__file__).parent.parent / "themes" / based_on
335 )
336 based_on_theme_init_file = based_on_theme_directory / "__init__.py"
337 based_on_theme_init_file_contents = based_on_theme_init_file.read_text()
339 # generate the new init file:
340 class_name = f"{theme_name.capitalize()}ThemeOptions"
341 literal_name = f'Literal["{theme_name}"]'
342 new_init_file_contents = based_on_theme_init_file_contents.replace(
343 f'Literal["{based_on}"]', literal_name
344 ).replace(f"{based_on.capitalize()}ThemeOptions", class_name)
346 # create the new __init__.py file:
347 (theme_folder / "__init__.py").write_text(new_init_file_contents)
349 printer.information(f'The theme folder "{theme_folder.name}" has been created.')
352@app.callback()
353def cli_command_no_args(
354 version_requested: Annotated[
355 Optional[bool], typer.Option("--version", "-v", help="Show the version")
356 ] = None,
357):
358 if version_requested:
359 there_is_a_new_version = printer.warn_if_new_version_is_available()
360 if not there_is_a_new_version:
361 print(f"RenderCV v{__version__}")