Coverage for rendercv/cli/printer.py: 95%
133 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
1"""
2The `rendercv.cli.printer` module contains all the functions and classes that are used
3to print nice-looking messages to the terminal.
4"""
6import functools
7from collections.abc import Callable
8from typing import Optional
10import jinja2
11import packaging.version
12import pydantic
13import rich
14import rich.live
15import rich.panel
16import rich.progress
17import rich.table
18import rich.text
19import ruamel.yaml
20import typer
21from rich import print
23from .. import __version__, data
24from . import utilities
27class LiveProgressReporter(rich.live.Live):
28 """This class is a wrapper around `rich.live.Live` that provides the live progress
29 reporting functionality.
31 Args:
32 number_of_steps: The number of steps to be finished.
33 end_message: The message to be printed when the progress is finished. Defaults
34 to "Your CV is rendered!".
35 """
37 def __init__(self, number_of_steps: int, end_message: str = "Your CV is rendered!"):
38 class TimeElapsedColumn(rich.progress.ProgressColumn):
39 def render(self, task: "rich.progress.Task") -> rich.text.Text:
40 elapsed = task.finished_time if task.finished else task.elapsed
41 assert elapsed is not None
42 elapsed = elapsed * 1000 # Convert to milliseconds
43 delta = f"{elapsed:.0f} ms"
44 return rich.text.Text(str(delta), style="progress.elapsed")
46 self.step_progress = rich.progress.Progress(
47 TimeElapsedColumn(), rich.progress.TextColumn("{task.description}")
48 )
50 self.overall_progress = rich.progress.Progress(
51 TimeElapsedColumn(),
52 rich.progress.BarColumn(),
53 rich.progress.TextColumn("{task.description}"),
54 )
56 self.group = rich.console.Group(
57 rich.panel.Panel(rich.console.Group(self.step_progress)),
58 self.overall_progress,
59 )
61 self.overall_task_id = self.overall_progress.add_task("", total=number_of_steps)
62 self.number_of_steps = number_of_steps
63 self.end_message = end_message
64 self.current_step = 0
65 self.overall_progress.update(
66 self.overall_task_id,
67 description=(
68 f"[bold #AAAAAA]({self.current_step} out of"
69 f" {self.number_of_steps} steps finished)"
70 ),
71 )
72 super().__init__(self.group)
74 def __enter__(self) -> "LiveProgressReporter":
75 """Overwrite the `__enter__` method for the correct return type."""
76 self.start(refresh=self._renderable is not None)
77 return self
79 def start_a_step(self, step_name: str):
80 """Start a step and update the progress bars.
82 Args:
83 step_name: The name of the step.
84 """
85 self.current_step_name = step_name
86 self.current_step_id = self.step_progress.add_task(
87 f"{self.current_step_name} has started."
88 )
90 def finish_the_current_step(self):
91 """Finish the current step and update the progress bars."""
92 self.step_progress.stop_task(self.current_step_id)
93 self.step_progress.update(
94 self.current_step_id, description=f"{self.current_step_name} has finished."
95 )
96 self.current_step += 1
97 self.overall_progress.update(
98 self.overall_task_id,
99 description=(
100 f"[bold #AAAAAA]({self.current_step} out of"
101 f" {self.number_of_steps} steps finished)"
102 ),
103 advance=1,
104 )
105 if self.current_step == self.number_of_steps:
106 self.end()
108 def end(self):
109 """End the live progress reporting."""
110 self.overall_progress.update(
111 self.overall_task_id,
112 description=f"[yellow]{self.end_message}",
113 )
116def warn_if_new_version_is_available() -> bool:
117 """Check if a new version of RenderCV is available and print a warning message if
118 there is a new version. Also, return True if there is a new version, and False
119 otherwise.
121 Returns:
122 True if there is a new version, and False otherwise.
123 """
124 latest_version = utilities.get_latest_version_number_from_pypi()
125 version = packaging.version.Version(__version__)
126 if latest_version is not None and version < latest_version:
127 warning(
128 f"A new version of RenderCV is available! You are using v{__version__},"
129 f" and the latest version is v{latest_version}."
130 )
131 return True
132 return False
135def welcome():
136 """Print a welcome message to the terminal."""
137 warn_if_new_version_is_available()
139 table = rich.table.Table(
140 title=(
141 "\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some"
142 " useful links:"
143 ),
144 title_justify="left",
145 )
147 table.add_column("Title", style="magenta", justify="left")
148 table.add_column("Link", style="cyan", justify="right", no_wrap=True)
150 table.add_row("[bold]RenderCV App", "https://rendercv.com")
151 table.add_row("Documentation", "https://docs.rendercv.com")
152 table.add_row("Source code", "https://github.com/rendercv/rendercv/")
153 table.add_row("Bug reports", "https://github.com/rendercv/rendercv/issues/")
154 table.add_row("Feature requests", "https://github.com/rendercv/rendercv/issues/")
155 table.add_row("Discussions", "https://github.com/rendercv/rendercv/discussions/")
156 table.add_row("RenderCV Pipeline", "https://github.com/rendercv/rendercv-pipeline/")
158 print(table)
161def warning(text: str):
162 """Print a warning message to the terminal.
164 Args:
165 text: The text of the warning message.
166 """
167 print(f"[bold yellow]{text}")
170def error(text: Optional[str] = None, exception: Optional[Exception] = None):
171 """Print an error message to the terminal and exit the program. If an exception is
172 given, then print the exception's message as well. If neither text nor exception is
173 given, then print an empty line and exit the program.
175 Args:
176 text: The text of the error message.
177 exception: An exception object. Defaults to None.
178 """
179 if exception is not None:
180 exception_messages = [str(arg) for arg in exception.args]
181 exception_message = "\n\n".join(exception_messages)
182 if text is None:
183 text = "An error occurred:"
185 print(
186 f"\n[bold red]{text}[/bold red]\n\n[orange4]{exception_message}[/orange4]\n"
187 )
188 elif text is not None:
189 print(f"\n[bold red]{text}\n")
190 else:
191 print()
194def information(text: str):
195 """Print an information message to the terminal.
197 Args:
198 text: The text of the information message.
199 """
200 print(f"[green]{text}")
203def print_validation_errors(exception: pydantic.ValidationError):
204 """Take a Pydantic validation error and print the error messages in a nice table.
206 Pydantic's `ValidationError` object is a complex object that contains a lot of
207 information about the error. This function takes a `ValidationError` object and
208 extracts the error messages, locations, and the input values. Then, it prints them
209 in a nice table with [Rich](https://rich.readthedocs.io/en/latest/).
211 Args:
212 exception: The Pydantic validation error object.
213 """
214 errors = data.parse_validation_errors(exception)
216 # Print the errors in a nice table:
217 table = rich.table.Table(
218 title="[bold red]\nThere are some errors in the data model!\n",
219 title_justify="left",
220 show_lines=True,
221 )
222 table.add_column("Location", style="cyan", no_wrap=True)
223 table.add_column("Input Value", style="magenta")
224 table.add_column("Error Message", style="orange4")
226 for error_object in errors:
227 table.add_row(
228 ".".join(error_object["loc"]),
229 error_object["input"],
230 error_object["msg"],
231 )
233 print(table)
236def handle_and_print_raised_exceptions_without_exit(function: Callable) -> Callable:
237 """Return a wrapper function that handles exceptions. It does not exit the program
238 after an exception is raised. It just prints the error message and continues the
239 execution.
241 A decorator in Python is a syntactic convenience that allows a Python to interpret
242 the code below:
244 ```python
245 @handle_exceptions
246 def my_function():
247 pass
248 ```
250 as
252 ```python
253 my_function = handle_exceptions(my_function)
254 ```
256 which means that the function `my_function` is modified by the `handle_exceptions`.
258 Args:
259 function: The function to be wrapped.
261 Returns:
262 The wrapped function.
263 """
265 @functools.wraps(function)
266 def wrapper(*args, **kwargs):
267 code = 4
268 try:
269 function(*args, **kwargs)
270 except pydantic.ValidationError as e:
271 print_validation_errors(e)
272 except ruamel.yaml.YAMLError as e:
273 error(
274 "There is a YAML error in the input file!\n\nTry to use quotation marks"
275 " to make sure the YAML parser understands the field is a string.",
276 e,
277 )
278 except FileNotFoundError as e:
279 error(exception=e)
280 except UnicodeDecodeError as e:
281 # find the problematic character that cannot be decoded with utf-8
282 bad_character = str(e.object[e.start : e.end])
283 try:
284 bad_character_context = str(e.object[e.start - 16 : e.end + 16])
285 except IndexError:
286 bad_character_context = ""
288 error(
289 "The input file contains a character that cannot be decoded with"
290 f" UTF-8 ({bad_character}):\n {bad_character_context}",
291 )
292 except ValueError as e:
293 error(exception=e)
294 except typer.Exit:
295 pass
296 except jinja2.exceptions.TemplateSyntaxError as e:
297 error(
298 f"There is a problem with the template ({e.filename}) at line"
299 f" {e.lineno}!",
300 e,
301 )
302 except RuntimeError as e:
303 error(exception=e)
304 except Exception as e:
305 raise e
306 else:
307 code = 0
309 return code
311 return wrapper
314def handle_and_print_raised_exceptions(function: Callable) -> Callable:
315 """Return a wrapper function that handles exceptions. It exits the program after an
316 exception is raised.
318 A decorator in Python is a syntactic convenience that allows a Python to interpret
319 the code below:
321 ```python
322 @handle_exceptions
323 def my_function():
324 pass
325 ```
327 as
329 ```python
330 my_function = handle_exceptions(my_function)
331 ```
333 which means that the function `my_function` is modified by the `handle_exceptions`.
335 Args:
336 function: The function to be wrapped.
338 Returns:
339 The wrapped function.
340 """
342 @functools.wraps(function)
343 def wrapper(*args, **kwargs):
344 without_exit_wrapper = handle_and_print_raised_exceptions_without_exit(function)
346 code = without_exit_wrapper(*args, **kwargs)
348 if code != 0:
349 raise typer.Exit(code)
351 return wrapper