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

1""" 

2The `rendercv.cli.commands` module contains all the command-line interface (CLI) 

3commands of RenderCV. 

4""" 

5 

6import copy 

7import pathlib 

8from typing import Annotated, Optional 

9 

10import typer 

11from rich import print 

12 

13from .. import __version__, data 

14from . import printer, utilities 

15 

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) 

26 

27 

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() 

172 

173 from . import utilities as u 

174 

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} 

181 

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 ) 

185 

186 watch = input_file_as_a_dict["rendercv_settings"]["render_command"]["watch"] 

187 

188 if watch: 

189 

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 ) 

198 

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 ) 

204 

205 

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 = [] 

243 

244 input_file_name = f"{full_name.replace(' ', '_')}_CV.yaml" 

245 input_file_path = pathlib.Path(input_file_name) 

246 

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) 

261 

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 ) 

272 

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 ) 

283 

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 ) 

290 

291 

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 ) 

321 

322 theme_folder = utilities.copy_templates( 

323 based_on, pathlib.Path.cwd(), new_folder_name=theme_name 

324 ) 

325 

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 

332 

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() 

338 

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) 

345 

346 # create the new __init__.py file: 

347 (theme_folder / "__init__.py").write_text(new_init_file_contents) 

348 

349 printer.information(f'The theme folder "{theme_folder.name}" has been created.') 

350 

351 

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__}")