# SPDX-FileCopyrightText: © 2024 The "Whiteprints" contributors <whiteprints@pm.me>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Command Line Interface app entrypoint."""
import importlib
import os
import sys
from functools import cached_property
from pathlib import Path
from typing import Final, Optional, TextIO, TypedDict, get_args
import rich_click as click
from rich_click import Context, File, Option
from rich_click.rich_command import RichCommand as Command
from rich_click.rich_command import RichGroup as Group
from whiteprints import __version__
from whiteprints.cli import APP_NAME, __app_name__
from whiteprints.cli.logs import LogLevel, configure_logging
from whiteprints.loc import _
if sys.version_info >= (3, 11):
from typing import Unpack
else:
from typing_extensions import Unpack
if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override
__all__: Final = ["whiteprints"]
"""Public module attributes."""
COMMAND_DIRECTORY_NAME: Final = "command"
COMMAND_ROOT: Final = Path(__file__).parent / COMMAND_DIRECTORY_NAME
class LazyCommandLoader(Group):
"""Lazy commands loader.
Loads lazily all the commands in the submodule .command.
"""
@staticmethod
def _is_command(obj: object) -> bool:
"""Check whether an object represents a Click Command.
This function first tries to unwrap the object (if wrapped by beartype)
and then returns True if the unwrapped object is an instance of
Command. If that fails, it also checks whether the object is callable
and has a __self__ attribute that is an instance of Command.
Returns:
True if the object is recognized as a Command, False otherwise.
"""
return isinstance(obj, Command)
@cached_property
def command_lookup(self) -> dict[str, dict[str, str]]:
"""A command lookup table."""
pkgutil = importlib.import_module("pkgutil")
commands_modules = [
".".join((__package__ or "", COMMAND_DIRECTORY_NAME, name))
for _module_finder, name, _ispkg in pkgutil.walk_packages(
path=map(str, [COMMAND_ROOT])
)
]
inspect = importlib.import_module("inspect")
command_lookup: dict[str, dict[str, str]] = {}
for module in commands_modules:
for command in inspect.getmembers(
importlib.import_module(module, __package__),
LazyCommandLoader._is_command,
):
command_lookup[command[1].name] = {
"module": module,
"function_name": command[0],
}
return command_lookup
@cached_property
def _list_commands(self) -> list[str]:
"""A list all the commands."""
return sorted(self.command_lookup.keys())
@override
def list_commands(self, ctx: Context) -> list[str]:
"""List all the commands.
Args:
ctx: unused.
Returns:
A list of commands names.
"""
return self._list_commands
@override
def get_command(self, ctx: Context, cmd_name: str) -> Optional[Command]:
"""Invoke a command.
The command must have the name of the module.
Args:
ctx: the click context (unused).
cmd_name: the name of the command to invoke.
Returns:
A command.
"""
if (command := self.command_lookup.get(cmd_name)) is None:
return None
return getattr(
importlib.import_module(
command["module"],
__package__,
),
command["function_name"],
)
@override
def print_copyright(ctx: Context, _param: Option, value: bool) -> None:
"""Print the code copyright information.
Args:
ctx: the click Context.
_param: the click Option.
value: the value of the option.
"""
if not value:
return
console = importlib.import_module("whiteprints.console")
console.STDOUT.print(
_(
'Copyright © 2024 The "Whiteprints" contributors'
" <whiteprints@pm.me>."
)
)
ctx.exit()
@override
def print_license(ctx: Context, _param: Option, value: bool) -> None:
"""Print the code licenses information.
Args:
ctx: the click Context.
_param: the click Option.
value: the value of the option.
"""
if not value:
return
console = importlib.import_module("whiteprints.console")
package_metadata = importlib.import_module("whiteprints.package_metadata")
console.STDOUT.print(
_("Code released under license '{}'.").format(
package_metadata.__license__
)
)
console.STDOUT.print(
_(
"\nThis project is REUSE compliant ('https://reuse.software/')."
" Please check the SPDX header of each source code file for "
"detailed licensing information.\nSources located at '{}'.\n"
).format(Path(__file__).parent.parent)
)
panel = importlib.import_module("rich.panel")
for license_path in package_metadata.__license_file__:
license_panel = panel.Panel(
license_path.read_text(),
title=license_path.stem,
)
console.STDOUT.print(license_panel)
console.STDOUT.print(str(license_path.locate()) + "\n")
ctx.exit()
@override
def print_debug_info(ctx: Context, _param: Option, value: bool) -> None:
"""Print system information for debug.
Args:
ctx: the click Context.
_param: the click Option.
value: the value of the option.
"""
if not value:
return
console = importlib.import_module("whiteprints.console")
console.STDOUT.print(
importlib.import_module(
"whiteprints.debug_info",
__package__,
).gather_debug_info()
)
ctx.exit()
class CLIArgsType(TypedDict):
"""The CLI arguments types."""
log_level: LogLevel
log_file: TextIO
@click.command(
cls=LazyCommandLoader,
name=__app_name__,
help=_(
"A Copier-based cookiecutter for creating Python projects "
"managed by uv."
).format(__app_name__),
no_args_is_help=True,
)
@click.option(
"-l",
"--log-level",
type=click.Choice(
get_args(LogLevel),
case_sensitive=False,
),
help=_("Logging verbosity."),
default=os.environ.get(f"{APP_NAME}_LOG_LEVEL", "ERROR"),
show_default=True,
)
@click.option(
"-f",
"--log-file",
type=File(
mode="w",
encoding="utf-8",
lazy=True,
),
help=_("A file in which to write the log."),
default=os.environ.get(f"{APP_NAME}_LOG_FILE", "-"),
show_default=True,
)
@click.option(
"--copyright",
is_flag=True,
callback=print_copyright,
expose_value=False,
is_eager=True,
help=_("Print the copyright information."),
)
@click.option(
"--license",
is_flag=True,
callback=print_license,
expose_value=False,
is_eager=True,
help=_("Print the license information."),
)
@click.option(
"--debug-info",
is_flag=True,
callback=print_debug_info,
expose_value=False,
is_eager=True,
help=_(
"Print system information. Useful for reporting errors and debugging."
),
)
@click.version_option(version=__version__, prog_name=__app_name__)
[docs]
def whiteprints(**kwargs: Unpack[CLIArgsType]) -> None:
"""The Whiteprint CLI."""
configure_logging(
level=kwargs["log_level"],
file=kwargs["log_file"],
)