# SPDX-FileCopyrightText: © 2024 The "Whiteprints" contributors <whiteprints@pm.me>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Command Line Interface app entrypoint."""
from __future__ import annotations
import importlib
import os
import sys
from functools import cached_property
from pathlib import Path
from typing import Final, 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 if an object is a command.
Args:
obj: a Python object.
Returns:
True if the object is 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) -> Command | None:
"""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"],
None,
)
@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"],
)