Skip to content

Commit

Permalink
Add a new --verbose option along side --verbosity
Browse files Browse the repository at this point in the history
  • Loading branch information
kdeldycke committed Feb 1, 2025
1 parent 17710ed commit c172dd7
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 67 deletions.
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
> [!IMPORTANT]
> This version is not released yet and is under active development.
- Add a new `--verbose` option on `@extra_command` and `@extra_group` to increase the verbosity level for each additional repetition.
- Add new `@verbose_option` pre-configured decorator.
- Reassign the short `-v` option from `--verbosity` to `--verbose`.
- Improve logging documentation.
- Align `ExtraStreamHandler` behavior to `logging.StreamHandler`.
- Move `stream_handler_class` and `formatter_class` arguments from `new_extra_logger` to `extraBasicConfig`.
Expand Down
4 changes: 4 additions & 0 deletions click_extra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@
table_format_option,
telemetry_option,
timer_option,
verbose_option,
verbosity_option,
)
from .logging import ( # noqa: E402
ExtraFormatter,
ExtraStreamHandler,
VerboseOption,
VerbosityOption,
extraBasicConfig,
new_extra_logger,
Expand Down Expand Up @@ -191,6 +193,8 @@
"unstyle", # noqa: F405
"UsageError", # noqa: F405
"UUID", # noqa: F405
"verbose_option", # noqa: F405
"VerboseOption", # noqa: F405
"verbosity_option", # noqa: F405
"VerbosityOption", # noqa: F405
"version_option", # noqa: F405
Expand Down
6 changes: 4 additions & 2 deletions click_extra/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from . import Command, Group, Option
from .colorize import ColorOption, ExtraHelpColorsMixin, HelpExtraFormatter
from .config import ConfigOption
from .logging import VerbosityOption
from .logging import VerboseOption, VerbosityOption
from .parameters import (
ExtraOption,
ShowParamsOption,
Expand Down Expand Up @@ -146,7 +146,8 @@ def default_extra_params() -> list[Option]:
behavior and value of the other options.
#. ``--color``, ``--ansi`` / ``--no-color``, ``--no-ansi``
#. ``--show-params``
#. ``-v``, ``--verbosity LEVEL``
#. ``--verbosity LEVEL``
#. ``-v``, ``--verbose``
#. ``--version``
#. ``-h``, ``--help``
.. attention::
Expand Down Expand Up @@ -181,6 +182,7 @@ def default_extra_params() -> list[Option]:
ConfigOption(),
ShowParamsOption(),
VerbosityOption(),
VerboseOption(),
ExtraVersionOption(),
# click.decorators.HelpOption(),
]
Expand Down
3 changes: 2 additions & 1 deletion click_extra/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from .colorize import ColorOption
from .commands import DEFAULT_HELP_NAMES, ExtraCommand, ExtraGroup, default_extra_params
from .config import ConfigOption
from .logging import VerbosityOption
from .logging import VerboseOption, VerbosityOption
from .parameters import ShowParamsOption
from .tabulate import TableFormatOption
from .telemetry import TelemetryOption
Expand Down Expand Up @@ -120,4 +120,5 @@ def decorator(*args, **kwargs):
table_format_option = decorator_factory(dec=cloup.option, cls=TableFormatOption)
telemetry_option = decorator_factory(dec=cloup.option, cls=TelemetryOption)
timer_option = decorator_factory(dec=cloup.option, cls=TimerOption)
verbose_option = decorator_factory(dec=cloup.option, cls=VerboseOption)
verbosity_option = decorator_factory(dec=cloup.option, cls=VerbosityOption)
169 changes: 142 additions & 27 deletions click_extra/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@
from unittest.mock import patch

import click
from click.types import IntRange

from . import Choice, Context, Parameter
from .colorize import default_theme
from .parameters import ExtraOption
from .parameters import ExtraOption, search_params

if TYPE_CHECKING:
from collections.abc import Generator, Iterable, Sequence
Expand Down Expand Up @@ -314,18 +315,14 @@ def new_extra_logger(
return logger


class VerbosityOption(ExtraOption):
"""A pre-configured ``--verbosity``/``-v`` option.
class ExtraVerbosity(ExtraOption):
"""A base class implementing all the common halpers to manipulated logger's verbosity.
Sets the level of the provided logger. If no logger is provided, sets the level of
the global ``root`` logger.
It is not intended to be used as-is, but is a central place to reconcile the verbosity level
to be set by the competing verbosity options implemented below:
The selected verbosity level name is made available in the context in
``ctx.meta["click_extra.verbosity"]``.
.. important::
The internal ``click_extra`` logger level will be aligned to the value set via
this option.
- ``--verbosity``
- ``--verbose``/``-v``
"""

logger_name: str
Expand Down Expand Up @@ -367,6 +364,19 @@ def set_levels(self, ctx: Context, param: Parameter, value: str) -> None:
Also prints the chosen value as a debug message via the internal
``click_extra`` logger.
"""
# If any of the verbosity-related option has already set the level, do not alter it.
# So in a way, this property in the context serves as a kind of global state shared by
# all verbosity options.

current_level = ctx.meta.get("click_extra.verbosity")

if current_level:
levels = tuple(LOG_LEVELS)
current_level_index = levels.index(current_level)
new_level_index = levels.index(value)
if new_level_index <= current_level_index:
return

ctx.meta["click_extra.verbosity"] = value

for logger in self.all_loggers:
Expand All @@ -375,6 +385,47 @@ def set_levels(self, ctx: Context, param: Parameter, value: str) -> None:

ctx.call_on_close(self.reset_loggers)

def __init__(
self,
param_decls: Sequence[str] | None = None,
default_logger: Logger | str = logging.root.name,
**kwargs,
) -> None:
# A logger object has been provided, fetch its name.
if isinstance(default_logger, Logger):
self.logger_name = default_logger.name
# Use the provided string if it is found in the registry.
elif default_logger in Logger.manager.loggerDict:
self.logger_name = default_logger
# Create a new logger with Click Extra's default configuration.
# XXX That's also the case in which the root logger will fall into, because as
# a special case, it is not registered in Logger.manager.loggerDict.
else:
logger = new_extra_logger(name=default_logger)
self.logger_name = logger.name

kwargs.setdefault("callback", self.set_levels)

super().__init__(
param_decls=param_decls,
**kwargs,
)


class VerbosityOption(ExtraVerbosity):
"""A pre-configured ``--verbosity``/``-v`` option.
Sets the level of the provided logger. If no logger is provided, sets the level of
the global ``root`` logger.
The selected verbosity level name is made available in the context in
``ctx.meta["click_extra.verbosity"]``.
.. important::
The internal ``click_extra`` logger level will be aligned to the value set via
this option.
"""

def __init__(
self,
param_decls: Sequence[str] | None = None,
Expand All @@ -396,25 +447,11 @@ def __init__(
Default to the global ``root`` logger.
"""
if not param_decls:
param_decls = ("--verbosity", "-v")

# A logger object has been provided, fetch its name.
if isinstance(default_logger, Logger):
self.logger_name = default_logger.name
# Use the provided string if it is found in the registry.
elif default_logger in Logger.manager.loggerDict:
self.logger_name = default_logger
# Create a new logger with Click Extra's default configuration.
# XXX That's also the case in which the root logger will fall into, because as
# a special case, it is not registered in Logger.manager.loggerDict.
else:
logger = new_extra_logger(name=default_logger)
self.logger_name = logger.name

kwargs.setdefault("callback", self.set_levels)
param_decls = ("--verbosity",)

super().__init__(
param_decls=param_decls,
default_logger=default_logger,
default=default,
metavar=metavar,
type=type,
Expand All @@ -423,3 +460,81 @@ def __init__(
is_eager=is_eager,
**kwargs,
)


class VerboseOption(ExtraVerbosity):
"""Increase the log level of :class:`VerbosityOption` by a number of step levels.
By default, it will not affect the level set by the ``--verbosity`` parameter.
But if ``-v`` is passed, then it will increase the ``--verbosity``'s level by one level.
The option can be provided multiple times by the user. So if ``-vv`` is passed, then the level
of ``--verbosity`` will be increase by 2 steps.
With ``--verbosity`` defaults set to ``WARNING``:
- ``-v`` will increase the level to ``INFO``.
- ``-vv`` will increase the level to ``DEBUG``.
- Any number of repetition above that point will be set to the maximum level, so for example ``-vvvvv`` will be capped at ``DEBUG``.
If default's ``--verbosity`` is changed to a lower level than ``WARNING``, ``-v`` effect will change its base level accordingly.
"""

def set_levels(self, ctx: Context, param: Parameter, value: int) -> None:
ctx.meta["click_extra.verbose"] = value

# No -v option has been called, skip meddling with log levels.
if value == 0:
return

levels = tuple(LOG_LEVELS)

# Get default verbosity from the --verbosity option.
verbosity_option = search_params(
ctx.command.params, VerbosityOption, include_subclasses=False
)
# If no --verbosity option found, it's because we are use the --verbose option alone. So defaults to
# the global default.
default_level = (
verbosity_option.default if verbosity_option else DEFAULT_LEVEL_NAME
)

default_level_index = levels.index(default_level)

# Cap new index to the last, verbosier level.
new_level_index = min(default_level_index + value, len(levels) - 1)
new_level = levels[new_level_index]

super().set_levels(ctx, param, new_level)

getLogger("click_extra").debug(
f"Increased log verbosity by {value} levels: from {default_level} to {new_level}."
)

def __init__(
self,
param_decls: Sequence[str] | None = None,
count: bool = True,
expose_value=False,
help=_(
f"Increase the default {DEFAULT_LEVEL_NAME} verbosity by one level for each additional repetition of the option."
),
is_eager=True,
**kwargs,
) -> None:
if not param_decls:
param_decls = ("--verbose", "-v")

# Force type and default to align them with counting option original behavior:
# https://github.com/pallets/click/blob/5dd628854c0b61bbdc07f22004c5da8fa8ee9481/src/click/core.py#L2612-L2618
kwargs["type"] = IntRange(min=0)
kwargs["default"] = 0

super().__init__(
param_decls=param_decls,
count=count,
expose_value=expose_value,
help=help,
is_eager=is_eager,
**kwargs,
)
13 changes: 11 additions & 2 deletions click_extra/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,25 @@ def all_envvars(
def search_params(
params: Iterable[click.Parameter],
klass: type[click.Parameter],
include_subclasses: bool = True,
unique: bool = True,
) -> list[click.Parameter] | click.Parameter | None:
"""Search a particular class of parameter in a list and return them.
:param params: list of parameter instances to search in.
:param klass: the class of the parameters to look for.
:param include_subclasses: if ``True``, includes in the results all parameters subclassing
the provided ``klass``. If ``False``, only matches parameters which are strictly instances of ``klass``.
Defaults to ``True``.
:param unique: if ``True``, raise an error if more than one parameter of the
provided ``klass`` is found.
provided ``klass`` is found. Defaults to ``True``.
"""
param_list = [p for p in params if isinstance(p, klass)]
param_list = [
p
for p in params
if (include_subclasses and isinstance(p, klass))
or (not include_subclasses and p.__class__ is klass)
]
if not param_list:
return None
if unique:
Expand Down
23 changes: 20 additions & 3 deletions click_extra/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,11 @@ def _create_config(filename: str | Path, content: str) -> Path:
r" \S+\.{toml,yaml,yml,json,ini,xml}\]\n"
r" --show-params Show all CLI parameters, their provenance, defaults\n"
r" and value, then exit.\n"
r" -v, --verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.\n"
r" --verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.\n"
r" \[default: WARNING\]\n"
r" -v, --verbose Increase the default WARNING verbosity by one level\n"
r" for each additional repetition of the option.\n"
r" \[default: 0\]\n"
r" --version Show the version and exit.\n"
r" -h, --help Show this message and exit.\n"
)
Expand Down Expand Up @@ -236,12 +239,17 @@ def _create_config(filename: str | Path, content: str) -> Path:
r" \x1b\[36m--show-params\x1b\[0m"
r" Show all CLI parameters, their provenance, defaults\n"
r" and value, then exit.\n"
r" \x1b\[36m-v\x1b\[0m, \x1b\[36m--verbosity\x1b\[0m"
r" \x1b\[36m\x1b\[2mLEVEL\x1b\[0m"
r" \x1b\[36m--verbosity\x1b\[0m"
r" \x1b\[36m\x1b\[2mLEVEL\x1b\[0m "
r" Either \x1b\[35mCRITICAL\x1b\[0m, \x1b\[35mERROR\x1b\[0m, "
r"\x1b\[35mWARNING\x1b\[0m, \x1b\[35mINFO\x1b\[0m, \x1b\[35mDEBUG\x1b\[0m.\n"
r" \x1b\[2m\[\x1b\[0m\x1b\[2mdefault: "
r"\x1b\[0m\x1b\[32m\x1b\[2m\x1b\[3mWARNING\x1b\[0m\x1b\[2m\]\x1b\[0m\n"
r" \x1b\[36m-v\x1b\[0m, \x1b\[36m--verbose\x1b\[0m"
r" Increase the default \x1b\[35mWARNING\x1b\[0m verbosity by one level\n"
r" for each additional repetition of the option.\n"
r" \x1b\[2m\[\x1b\[0m\x1b\[2mdefault: "
r"\x1b\[0m\x1b\[32m\x1b\[2m\x1b\[3m0\x1b\[0m\x1b\[2m\]\x1b\[0m\n"
r" \x1b\[36m--version\x1b\[0m Show the version and exit.\n"
r" \x1b\[36m-h\x1b\[0m, \x1b\[36m--help\x1b\[0m"
r" Show this message and exit.\n"
Expand All @@ -258,6 +266,15 @@ def _create_config(filename: str | Path, content: str) -> Path:
)


default_debug_colored_verbose_log = (
r"\x1b\[34mdebug\x1b\[0m: Increased log verbosity "
r"by \d+ levels: from WARNING to [A-Z]+.\n"
)
default_debug_uncolored_verbose_log = (
r"debug: Increased log verbosity by \d+ levels: from WARNING to [A-Z]+.\n"
)


default_debug_uncolored_config = (
r"debug: Load configuration matching .+\*\.{toml,yaml,yml,json,ini,xml}\n"
r"debug: Pattern is not an URL: search local file system.\n"
Expand Down
5 changes: 4 additions & 1 deletion tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,11 @@ def cli():
result = invoke(cli, "--help", color=False)
assert result.exit_code == 0
assert result.stdout.endswith(
" -v, --verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.\n"
" --verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.\n"
" [default: WARNING]\n"
" -v, --verbose Increase the default WARNING verbosity by one level\n"
" for each additional repetition of the option.\n"
" [default: 0]\n"
" --version Show the version and exit.\n"
" --version Show the version and exit.\n"
" -h, --help Show this message and exit.\n"
Expand Down
Loading

0 comments on commit c172dd7

Please sign in to comment.