diff --git a/rendercv/cli/__init__.py b/rendercv/cli/__init__.py index e69de29b..9672f018 100644 --- a/rendercv/cli/__init__.py +++ b/rendercv/cli/__init__.py @@ -0,0 +1,8 @@ +""" +The `rendercv.cli` package contains the functions and classes that handle RenderCV's +command-line interface (CLI). It uses [Typer](https://typer.tiangolo.com/) to create the +CLI and [Rich](https://rich.readthedocs.io/en/latest/) to provide a nice-looking +terminal output. +""" + +__all__ = ["commands", "printer", "utilities"] diff --git a/rendercv/cli/commands.py b/rendercv/cli/commands.py index 5460f21b..124e5bc6 100644 --- a/rendercv/cli/commands.py +++ b/rendercv/cli/commands.py @@ -1,3 +1,8 @@ +""" +`rendercv.cli.commands` module contains all the command-line interface (CLI) commands of +RenderCV. +""" + import os import pathlib import shutil @@ -7,6 +12,22 @@ import pydantic import typer from rich import print +from .. import __version__ +from .. import data_models as dm +from .. import renderer as r +from .printer import ( + LiveProgressReporter, + error, + information, + warn_if_new_version_is_available, + warning, + welcome, +) +from .utilities import ( + copy_templates, + handle_exceptions, + parse_render_command_override_arguments, +) app = typer.Typer( rich_markup_mode="rich", @@ -127,7 +148,7 @@ def cli_command_render( ] = None, extra_data_model_override_argumets: typer.Context = None, ): - """Generate a $\\LaTeX$ CV from a YAML input file.""" + """Render a CV from a YAML input file.""" welcome() input_file_path = pathlib.Path(input_file_name).absolute() @@ -161,7 +182,7 @@ def cli_command_render( key_and_values = dict() if extra_data_model_override_argumets: - key_and_values = parse_data_model_override_arguments( + key_and_values = parse_render_command_override_arguments( extra_data_model_override_argumets ) for key, value in key_and_values.items(): @@ -269,7 +290,7 @@ def cli_command_new( ), ] = False, ): - """Generate a YAML input file to get started.""" + """Generate a YAML input file and the LaTeX and Markdown source files.""" created_files_and_folders = [] input_file_name = f"{full_name.replace(' ', '_')}_CV.yaml" @@ -333,7 +354,7 @@ def cli_command_create_theme( ), ] = "classic", ): - """Create a custom theme folder based on an existing theme.""" + """Create a custom theme based on an existing theme.""" if based_on not in dm.available_themes: error( f'The theme "{based_on}" is not in the list of available themes:' @@ -378,6 +399,8 @@ def main( Optional[bool], typer.Option("--version", "-v", help="Show the version.") ] = None, ): + """If the `--version` option is used, then show the version. Otherwise, show the + help message (see `no_args_is_help` argument of `typer.Typer` object).""" if version_requested: there_is_a_new_version = warn_if_new_version_is_available() if not there_is_a_new_version: diff --git a/rendercv/cli/printer.py b/rendercv/cli/printer.py new file mode 100644 index 00000000..391085da --- /dev/null +++ b/rendercv/cli/printer.py @@ -0,0 +1,184 @@ +""" +`rendercv.cli.printer` module contains all the functions and classes that are used to +print nice-looking messages to the terminal. +""" + +from typing import Optional + +import rich +import typer +from rich import print + +from .. import __version__ +from .utilities import get_latest_version_number_from_pypi + + +class LiveProgressReporter(rich.live.Live): + """This class is a wrapper around `rich.live.Live` that provides the live progress + reporting functionality. + + Args: + number_of_steps (int): The number of steps to be finished. + """ + + def __init__(self, number_of_steps: int, end_message: str = "Your CV is rendered!"): + class TimeElapsedColumn(rich.progress.ProgressColumn): + def render(self, task: "rich.progress.Task") -> rich.text.Text: + elapsed = task.finished_time if task.finished else task.elapsed + delta = f"{elapsed:.1f} s" + return rich.text.Text(str(delta), style="progress.elapsed") + + self.step_progress = rich.progress.Progress( + TimeElapsedColumn(), rich.progress.TextColumn("{task.description}") + ) + + self.overall_progress = rich.progress.Progress( + TimeElapsedColumn(), + rich.progress.BarColumn(), + rich.progress.TextColumn("{task.description}"), + ) + + self.group = rich.console.Group( + rich.panel.Panel(rich.console.Group(self.step_progress)), + self.overall_progress, + ) + + self.overall_task_id = self.overall_progress.add_task("", total=number_of_steps) + self.number_of_steps = number_of_steps + self.end_message = end_message + self.current_step = 0 + self.overall_progress.update( + self.overall_task_id, + description=( + f"[bold #AAAAAA]({self.current_step} out of" + f" {self.number_of_steps} steps finished)" + ), + ) + super().__init__(self.group) + + def __enter__(self) -> "LiveProgressReporter": + """Overwrite the `__enter__` method for the correct return type.""" + self.start(refresh=self._renderable is not None) + return self + + def start_a_step(self, step_name: str): + """Start a step and update the progress bars.""" + self.current_step_name = step_name + self.current_step_id = self.step_progress.add_task( + f"{self.current_step_name} has started." + ) + + def finish_the_current_step(self): + """Finish the current step and update the progress bars.""" + self.step_progress.stop_task(self.current_step_id) + self.step_progress.update( + self.current_step_id, description=f"{self.current_step_name} has finished." + ) + self.current_step += 1 + self.overall_progress.update( + self.overall_task_id, + description=( + f"[bold #AAAAAA]({self.current_step} out of" + f" {self.number_of_steps} steps finished)" + ), + advance=1, + ) + if self.current_step == self.number_of_steps: + self.end() + + def end(self): + """End the live progress reporting.""" + self.overall_progress.update( + self.overall_task_id, + description=f"[yellow]{self.end_message}", + ) + + +def warn_if_new_version_is_available() -> bool: + """Check if a new version of RenderCV is available and print a warning message if + there is a new version. Also, return True if there is a new version, and False + otherwise. + + Returns: + bool: True if there is a new version, and False otherwise. + """ + latest_version = get_latest_version_number_from_pypi() + if latest_version is not None and __version__ != latest_version: + warning( + f"A new version of RenderCV is available! You are using v{__version__}," + f" and the latest version is v{latest_version}." + ) + return True + else: + return False + + +def welcome(): + """Print a welcome message to the terminal.""" + warn_if_new_version_is_available() + + table = rich.table.Table( + title=( + "\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some" + " useful links:" + ), + title_justify="left", + ) + + table.add_column("Title", style="magenta", justify="left") + table.add_column("Link", style="cyan", justify="right", no_wrap=True) + + table.add_row("Documentation", "https://docs.rendercv.com") + table.add_row("Source code", "https://github.com/sinaatalay/rendercv/") + table.add_row("Bug reports", "https://github.com/sinaatalay/rendercv/issues/") + table.add_row("Feature requests", "https://github.com/sinaatalay/rendercv/issues/") + table.add_row("Discussions", "https://github.com/sinaatalay/rendercv/discussions/") + table.add_row( + "RenderCV Pipeline", "https://github.com/sinaatalay/rendercv-pipeline/" + ) + + print(table) + + +def warning(text: str): + """Print a warning message to the terminal. + + Args: + text (str): The text of the warning message. + """ + print(f"[bold yellow]{text}") + + +def error(text: Optional[str] = None, exception: Optional[Exception] = None): + """Print an error message to the terminal and exit the program. If an exception is + given, then print the exception's message as well. If neither text nor exception is + given, then print an empty line and exit the program. + + Args: + text (str): The text of the error message. + exception (Exception, optional): An exception object. Defaults to None. + """ + if exception is not None: + exception_messages = [str(arg) for arg in exception.args] + exception_message = "\n\n".join(exception_messages) + if text is None: + text = "An error occurred:" + + print( + f"\n[bold red]{text}[/bold red]\n\n[orange4]{exception_message}[/orange4]\n" + ) + elif text is not None: + print(f"\n[bold red]{text}\n") + else: + print() + + raise typer.Exit(code=4) + + +def information(text: str): + """Print an information message to the terminal. + + Args: + text (str): The text of the information message. + """ + print(f"[yellow]{text}") diff --git a/rendercv/cli/cli.py b/rendercv/cli/utilities.py similarity index 68% rename from rendercv/cli/cli.py rename to rendercv/cli/utilities.py index b83a92c4..92aea483 100644 --- a/rendercv/cli/cli.py +++ b/rendercv/cli/utilities.py @@ -1,18 +1,14 @@ """ -This module contains the functions and classes that handle the command line interface -(CLI) of RenderCV. It uses [Typer](https://typer.tiangolo.com/) to create the CLI and -[Rich](https://rich.readthedocs.io/en/latest/) to provide a nice looking terminal -output. +`rendercv.cli.utilities` module contains utility functions that are required by CLI. """ import functools import json -import os import pathlib import re import shutil import urllib.request -from typing import Annotated, Callable, Optional +from typing import Callable, Optional import jinja2 import pydantic @@ -27,9 +23,8 @@ import ruamel.yaml.parser import typer from rich import print -from . import __version__ -from . import data_models as dm -from . import renderer as r +from .printer import error, warning + def get_latest_version_number_from_pypi() -> Optional[str]: """Get the latest version number of RenderCV from PyPI. @@ -59,96 +54,6 @@ def get_latest_version_number_from_pypi() -> Optional[str]: return version -def warn_if_new_version_is_available() -> bool: - """Check if a new version of RenderCV is available and print a warning message if - there is a new version. Also, return True if there is a new version, and False - otherwise. - - Returns: - bool: True if there is a new version, and False otherwise. - """ - latest_version = get_latest_version_number_from_pypi() - if latest_version is not None and __version__ != latest_version: - warning( - f"A new version of RenderCV is available! You are using v{__version__}," - f" and the latest version is v{latest_version}." - ) - return True - else: - return False - - -def welcome(): - """Print a welcome message to the terminal.""" - warn_if_new_version_is_available() - - table = rich.table.Table( - title=( - "\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some" - " useful links:" - ), - title_justify="left", - ) - - table.add_column("Title", style="magenta", justify="left") - table.add_column("Link", style="cyan", justify="right", no_wrap=True) - - table.add_row("Documentation", "https://docs.rendercv.com") - table.add_row("Source code", "https://github.com/sinaatalay/rendercv/") - table.add_row("Bug reports", "https://github.com/sinaatalay/rendercv/issues/") - table.add_row("Feature requests", "https://github.com/sinaatalay/rendercv/issues/") - table.add_row("Discussions", "https://github.com/sinaatalay/rendercv/discussions/") - table.add_row( - "RenderCV Pipeline", "https://github.com/sinaatalay/rendercv-pipeline/" - ) - - print(table) - - -def warning(text: str): - """Print a warning message to the terminal. - - Args: - text (str): The text of the warning message. - """ - print(f"[bold yellow]{text}") - - -def error(text: Optional[str] = None, exception: Optional[Exception] = None): - """Print an error message to the terminal and exit the program. If an exception is - given, then print the exception's message as well. If neither text nor exception is - given, then print an empty line and exit the program. - - Args: - text (str): The text of the error message. - exception (Exception, optional): An exception object. Defaults to None. - """ - if exception is not None: - exception_messages = [str(arg) for arg in exception.args] - exception_message = "\n\n".join(exception_messages) - if text is None: - text = "An error occurred:" - - print( - f"\n[bold red]{text}[/bold red]\n\n[orange4]{exception_message}[/orange4]\n" - ) - elif text is not None: - print(f"\n[bold red]{text}\n") - else: - print() - - raise typer.Exit(code=4) - - -def information(text: str): - """Print an information message to the terminal. - - Args: - text (str): The text of the information message. - """ - print(f"[yellow]{text}") - - def get_error_message_and_location_and_value_from_a_custom_error( error_string: str, ) -> tuple[Optional[str], Optional[str], Optional[str]]: @@ -397,87 +302,6 @@ def handle_exceptions(function: Callable) -> Callable: return wrapper -class LiveProgressReporter(rich.live.Live): - """This class is a wrapper around `rich.live.Live` that provides the live progress - reporting functionality. - - Args: - number_of_steps (int): The number of steps to be finished. - """ - - def __init__(self, number_of_steps: int, end_message: str = "Your CV is rendered!"): - class TimeElapsedColumn(rich.progress.ProgressColumn): - def render(self, task: "rich.progress.Task") -> rich.text.Text: - elapsed = task.finished_time if task.finished else task.elapsed - delta = f"{elapsed:.1f} s" - return rich.text.Text(str(delta), style="progress.elapsed") - - self.step_progress = rich.progress.Progress( - TimeElapsedColumn(), rich.progress.TextColumn("{task.description}") - ) - - self.overall_progress = rich.progress.Progress( - TimeElapsedColumn(), - rich.progress.BarColumn(), - rich.progress.TextColumn("{task.description}"), - ) - - self.group = rich.console.Group( - rich.panel.Panel(rich.console.Group(self.step_progress)), - self.overall_progress, - ) - - self.overall_task_id = self.overall_progress.add_task("", total=number_of_steps) - self.number_of_steps = number_of_steps - self.end_message = end_message - self.current_step = 0 - self.overall_progress.update( - self.overall_task_id, - description=( - f"[bold #AAAAAA]({self.current_step} out of" - f" {self.number_of_steps} steps finished)" - ), - ) - super().__init__(self.group) - - def __enter__(self) -> "LiveProgressReporter": - """Overwrite the `__enter__` method for the correct return type.""" - self.start(refresh=self._renderable is not None) - return self - - def start_a_step(self, step_name: str): - """Start a step and update the progress bars.""" - self.current_step_name = step_name - self.current_step_id = self.step_progress.add_task( - f"{self.current_step_name} has started." - ) - - def finish_the_current_step(self): - """Finish the current step and update the progress bars.""" - self.step_progress.stop_task(self.current_step_id) - self.step_progress.update( - self.current_step_id, description=f"{self.current_step_name} has finished." - ) - self.current_step += 1 - self.overall_progress.update( - self.overall_task_id, - description=( - f"[bold #AAAAAA]({self.current_step} out of" - f" {self.number_of_steps} steps finished)" - ), - advance=1, - ) - if self.current_step == self.number_of_steps: - self.end() - - def end(self): - """End the live progress reporting.""" - self.overall_progress.update( - self.overall_task_id, - description=f"[yellow]{self.end_message}", - ) - - def copy_templates( folder_name: str, copy_to: pathlib.Path, @@ -524,11 +348,11 @@ def copy_templates( return destination -def parse_data_model_override_arguments( +def parse_render_command_override_arguments( extra_arguments: typer.Context, ) -> dict["str", "str"]: - """Parse extra arguments as data model key and value pairs and return them as a - dictionary. + """Parse extra arguments given to the `render` command as data model key and value + pairs and return them as a dictionary. Args: extra_arguments (typer.Context): The extra arguments context. @@ -562,4 +386,3 @@ def parse_data_model_override_arguments( key_and_values[key] = value return key_and_values -