# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Wraps vanilla Click and Cloup commands with extra features.
Our flavor of commands, groups and context are all subclasses of their vanilla
counterparts, but are pre-configured with good and common defaults. You can still
leverage the mixins in here to build up your own custom variants.
"""
from __future__ import annotations
from typing import Any
import click
import cloup
from . import Command, Group, Option
from .colorize import ColorOption, ExtraHelpColorsMixin, HelpExtraFormatter
from .config import ConfigOption
from .envvar import clean_envvar_id, param_envvar_ids
from .logging import VerboseOption, VerbosityOption
from .parameters import ExtraOption, ShowParamsOption, search_params
from .timer import TimerOption
from .version import ExtraVersionOption
DEFAULT_HELP_NAMES: tuple[str, ...] = ("--help", "-h")
[docs]
class ExtraCommand(ExtraHelpColorsMixin, Command): # type: ignore[misc]
"""Like ``cloup.command``, with sane defaults and extra help screen colorization."""
context_class: type[cloup.Context] = ExtraContext
def __init__(
self,
*args,
version: str | None = None,
extra_option_at_end: bool = True,
populate_auto_envvars: bool = True,
**kwargs: Any,
) -> None:
"""List of extra parameters:
:param version: allows a version string to be set directly on the command. Will
be passed to the first instance of ``ExtraVersionOption`` parameter
attached to the command.
:param extra_option_at_end: `reorders all parameters attached to the command
<https://kdeldycke.github.io/click-extra/commands.html#option-order>`_, by
moving all instances of ``ExtraOption`` at the end of the parameter list.
The original order of the options is preserved among themselves.
:param populate_auto_envvars: forces all parameters to have their auto-generated
environment variables registered. This address the shortcoming of ``click``
which only evaluates them dynamiccaly. By forcing their registration, the
auto-generated environment variables gets displayed in the help screen,
fixing `click#2483 issue <https://github.com/pallets/click/issues/2483>`_.
On Windows, environment variable names are case-insensitive, so we normalize
them to uppercase.
By default, these `Click context settings
<https://click.palletsprojects.com/en/stable/api/#click.Context>`_ are applied:
- ``auto_envvar_prefix = self.name`` (*Click feature*)
Auto-generate environment variables for all options, using the command ID as
prefix. The prefix is normalized to be uppercased and all non-alphanumerics
replaced by underscores.
- ``help_option_names = ("--help", "-h")`` (*Click feature*)
`Allow help screen to be invoked with either --help or -h options
<https://click.palletsprojects.com/en/stable/documentation/#help-parameter-customization>`_.
- ``show_default = True`` (*Click feature*)
`Show all default values
<https://click.palletsprojects.com/en/stable/api/#click.Context.show_default>`_
in help screen.
Additionally, these `Cloup context settings
<https://cloup.readthedocs.io/en/stable/pages/formatting.html#formatting-settings>`_
are set:
- ``align_option_groups = False`` (*Cloup feature*)
`Aligns option groups in help screen
<https://cloup.readthedocs.io/en/stable/pages/option-groups.html#aligned-vs-non-aligned-groups>`_.
- ``show_constraints = True`` (*Cloup feature*)
`Show all constraints in help screen
<https://cloup.readthedocs.io/en/stable/pages/constraints.html#the-constraint-decorator>`_.
- ``show_subcommand_aliases = True`` (*Cloup feature*)
`Show all subcommand aliases in help screen
<https://cloup.readthedocs.io/en/stable/pages/aliases.html?highlight=show_subcommand_aliases#help-output-of-the-group>`_.
Click Extra also adds its own ``context_settings``:
- ``show_choices = None`` (*Click Extra feature*)
If set to ``True`` or ``False``, will force that value on all options, so we
can globally show or hide choices when prompting a user for input. Only makes
sense for options whose ``prompt`` property is set.
Defaults to ``None``, which will leave all options untouched, and let them
decide of their own ``show_choices`` setting.
- ``show_envvar = None`` (*Click Extra feature*)
If set to ``True`` or ``False``, will force that value on all options, so we
can globally enable or disable the display of environment variables in help
screen.
Defaults to ``None``, which will leave all options untouched, and let them
decide of their own ``show_envvar`` setting. The rationale being that
discoverability of environment variables is enabled by the ``--show-params``
option, which is active by default on extra commands. So there is no need to
surcharge the help screen.
This addresses the
`click#2313 issue <https://github.com/pallets/click/issues/2313>`_.
To override these defaults, you can pass your own settings with the
``context_settings`` parameter:
.. code-block:: python
@extra_command(
context_settings={
"show_default": False,
...
}
)
"""
super().__init__(*args, **kwargs)
# List of additional global settings for options.
extra_option_settings = [
"show_choices",
"show_envvar",
]
default_ctx_settings: dict[str, Any] = {
# Click settings:
# "default_map": {"verbosity": "DEBUG"},
"help_option_names": DEFAULT_HELP_NAMES,
"show_default": True,
# Cloup settings:
"align_option_groups": False,
"show_constraints": True,
"show_subcommand_aliases": True,
# Click Extra settings:
"show_choices": None,
"show_envvar": None,
}
# Generate environment variables for all options based on the command name.
if self.name:
default_ctx_settings["auto_envvar_prefix"] = clean_envvar_id(self.name)
# Merge defaults and user settings.
default_ctx_settings.update(self.context_settings)
# If set, force extra settings on all options.
for setting in extra_option_settings:
if default_ctx_settings[setting] is not None:
for param in self.params:
# These attributes are specific to options.
if isinstance(param, click.Option):
param.show_envvar = default_ctx_settings[setting]
# Remove Click Extra-specific settings, before passing it to Cloup and Click.
for setting in extra_option_settings:
del default_ctx_settings[setting]
self.context_settings: dict[str, Any] = default_ctx_settings
if populate_auto_envvars:
for param in self.params:
param.envvar = param_envvar_ids(param, self.context_settings)
if version:
version_param = search_params(self.params, ExtraVersionOption)
if version_param:
version_param.version = version # type: ignore[union-attr]
if extra_option_at_end:
self.params.sort(key=lambda p: isinstance(p, ExtraOption))
# Forces re-identification of grouped and non-grouped options as we re-ordered
# them above and added our own extra options since initialization.
_grouped_params = self._group_params(self.params) # type: ignore[attr-defined]
self.arguments, self.option_groups, self.ungrouped_options = _grouped_params
[docs]
def main(self, *args, **kwargs):
"""Pre-invocation step that is instantiating the context, then call ``invoke()``
within it.
During context instantiation, each option's callbacks are called. Beware that
these might break the execution flow (like ``--help`` or ``--version``).
"""
return super().main(*args, **kwargs)
[docs]
def make_context(
self,
info_name: str | None,
args: list[str],
parent: click.Context | None = None,
**extra: Any,
) -> Any:
"""Intercept the call to the original ``click.core.Command.make_context`` so
we can keep a copy of the raw, pre-parsed arguments provided to the CLI.
The result are passed to our own ``ExtraContext`` constructor which is able to
initialize the context's ``meta`` property under our own
``click_extra.raw_args`` entry. This will be used in
``ShowParamsOption.print_params()`` to print the table of parameters fed to the
CLI.
.. seealso::
This workaround is being discussed upstream in `click#1279
<https://github.com/pallets/click/issues/1279#issuecomment-1493348208>`_.
"""
# ``args`` needs to be copied: its items are consumed by the parsing process.
extra.update({"meta": {"click_extra.raw_args": args.copy()}})
return super().make_context(info_name, args, parent, **extra)
[docs]
def invoke(self, ctx: click.Context) -> Any:
"""Main execution of the command, just after the context has been instantiated
in ``main()``.
"""
return super().invoke(ctx)