Coverage for src / rendercv / cli / render_command / run_rendercv.py: 100%

62 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-25 13:49 +0000

1import contextlib 

2import pathlib 

3import time 

4from collections.abc import Callable 

5from typing import Literal, Unpack 

6 

7import jinja2 

8 

9from rendercv.exception import RenderCVUserError, RenderCVUserValidationError 

10from rendercv.renderer.html import generate_html 

11from rendercv.renderer.markdown import generate_markdown 

12from rendercv.renderer.pdf_png import generate_pdf, generate_png 

13from rendercv.renderer.typst import generate_typst 

14from rendercv.schema.rendercv_model_builder import ( 

15 BuildRendercvModelArguments, 

16 build_rendercv_dictionary_and_model, 

17 read_yaml_with_validation_errors, 

18) 

19 

20from .progress_panel import ProgressPanel 

21 

22 

23def timed_step[T, **P]( 

24 message: str, 

25 progress_panel: ProgressPanel, 

26 func: Callable[P, T], 

27 *args: P.args, 

28 **kwargs: P.kwargs, 

29) -> T: 

30 """Execute function, measure timing, and update progress panel with result. 

31 

32 Why: 

33 Each generation step (Typst, PDF, PNG) returns file paths. This wrapper 

34 times execution and automatically displays results in progress panel. 

35 

36 Example: 

37 ```py 

38 pdf_path = timed_step( 

39 "Generated PDF", progress, generate_pdf, rendercv_model, typst_path 

40 ) 

41 # Progress shows: ✓ 150 ms Generated PDF: ./cv.pdf 

42 ``` 

43 

44 Args: 

45 message: Step description for progress display. 

46 progress_panel: Progress panel to update. 

47 func: Function to execute and time. 

48 args: Positional arguments for func. 

49 kwargs: Keyword arguments for func. 

50 

51 Returns: 

52 Function result. 

53 """ 

54 start = time.perf_counter() 

55 result = func(*args, **kwargs) 

56 end = time.perf_counter() 

57 timing_ms = f"{(end - start) * 1000:.0f}" 

58 

59 paths: list[pathlib.Path] = [] 

60 if isinstance(result, pathlib.Path): 

61 paths = [result] 

62 elif isinstance(result, list) and result: 

63 if len(result) > 1: 

64 message = f"{message}s" 

65 paths = [p for p in result if isinstance(p, pathlib.Path)] 

66 

67 if paths: 

68 progress_panel.update_progress( 

69 time_took=timing_ms, message=message, paths=paths 

70 ) 

71 

72 return result 

73 

74 

75def collect_input_file_paths( 

76 input_file_path: pathlib.Path, 

77 design: pathlib.Path | None = None, 

78 locale: pathlib.Path | None = None, 

79 settings: pathlib.Path | None = None, 

80) -> dict[Literal["input", "design", "locale", "settings"], pathlib.Path]: 

81 """Collect all input file paths involved in a render. 

82 

83 Why: 

84 A render may involve multiple files: the main YAML, plus overlay 

85 files for design/locale/settings provided via CLI flags or referenced 

86 in settings.render_command. Watch mode needs this complete list to 

87 monitor all of them for changes, and the render pipeline needs the 

88 resolved paths to read overlay file contents. 

89 

90 Args: 

91 input_file_path: Path to the main YAML input file. 

92 design: CLI-provided design file path. 

93 locale: CLI-provided locale file path. 

94 settings: CLI-provided settings file path. 

95 

96 Returns: 

97 Mapping from role ("input", "design", "locale", "settings") to path. 

98 """ 

99 files: dict[Literal["input", "design", "locale", "settings"], pathlib.Path] = { 

100 "input": input_file_path 

101 } 

102 

103 if design: 

104 files["design"] = design 

105 if locale: 

106 files["locale"] = locale 

107 if settings: 

108 files["settings"] = settings 

109 

110 # Also include design/locale files referenced in the YAML itself 

111 # (CLI flags take precedence, so skip if already provided). 

112 # If YAML is invalid, watch mode should still start by watching the main file. 

113 with contextlib.suppress(RenderCVUserValidationError): 

114 main_dict = read_yaml_with_validation_errors( 

115 input_file_path.read_text(encoding="utf-8"), 

116 "main_yaml_file", 

117 ) 

118 rc = main_dict.get("settings", {}).get("render_command", {}) 

119 if "design" not in files and rc.get("design"): 

120 files["design"] = (input_file_path.parent / rc["design"]).resolve() 

121 if "locale" not in files and rc.get("locale"): 

122 files["locale"] = (input_file_path.parent / rc["locale"]).resolve() 

123 

124 return files 

125 

126 

127def run_rendercv( 

128 input_file_path: pathlib.Path, 

129 progress: ProgressPanel, 

130 **kwargs: Unpack[BuildRendercvModelArguments], 

131) -> None: 

132 """Execute complete CV generation pipeline with progress tracking and error handling. 

133 

134 Args: 

135 input_file_path: Path to the main YAML input file. 

136 progress: Progress panel for output display. 

137 kwargs: Optional YAML overlay strings, output paths, and generation flags. 

138 """ 

139 try: 

140 main_yaml = input_file_path.read_text(encoding="utf-8") 

141 

142 _, rendercv_model = timed_step( 

143 "Validated the input file", 

144 progress, 

145 build_rendercv_dictionary_and_model, 

146 main_yaml, 

147 input_file_path=input_file_path, 

148 **kwargs, 

149 ) 

150 typst_path = timed_step( 

151 "Generated Typst", 

152 progress, 

153 generate_typst, 

154 rendercv_model, 

155 ) 

156 timed_step( 

157 "Generated PDF", 

158 progress, 

159 generate_pdf, 

160 rendercv_model, 

161 typst_path, 

162 ) 

163 timed_step( 

164 "Generated PNG", 

165 progress, 

166 generate_png, 

167 rendercv_model, 

168 typst_path, 

169 ) 

170 md_path = timed_step( 

171 "Generated Markdown", 

172 progress, 

173 generate_markdown, 

174 rendercv_model, 

175 ) 

176 timed_step( 

177 "Generated HTML", 

178 progress, 

179 generate_html, 

180 rendercv_model, 

181 md_path, 

182 ) 

183 progress.finish_progress() 

184 except RenderCVUserError as e: 

185 progress.print_user_error(e) 

186 except jinja2.exceptions.TemplateSyntaxError as e: 

187 progress.print_user_error( 

188 RenderCVUserError( 

189 message=( 

190 f"There is a problem with the template ({e.filename}) at line" 

191 f" {e.lineno}!\n\n{e}" 

192 ) 

193 ) 

194 ) 

195 except OSError as e: 

196 progress.print_user_error(RenderCVUserError(message=f"OS Error: {e}")) 

197 except RenderCVUserValidationError as e: 

198 progress.print_validation_errors(e.validation_errors)