cli: refactor cli

This commit is contained in:
Sina Atalay
2024-06-30 18:25:18 +03:00
parent 38f3c6e6d2
commit c0f501df21
4 changed files with 226 additions and 188 deletions

View File

@@ -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"]

View File

@@ -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:

184
rendercv/cli/printer.py Normal file
View File

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

View File

@@ -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