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

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

161 

162 from . import utilities as u 

163 

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} 

170 

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 ) 

174 

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

176 

177 if watch: 

178 

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 ) 

187 

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 ) 

193 

194 

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

232 

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

234 input_file_path = pathlib.Path(input_file_name) 

235 

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) 

250 

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 ) 

261 

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 ) 

272 

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 ) 

279 

280 

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 ) 

310 

311 theme_folder = utilities.copy_templates( 

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

313 ) 

314 

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 

321 

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

327 

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) 

334 

335 # create the new __init__.py file: 

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

337 

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

339 

340 

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