Logging¶
The Python’s standard library logging module is a bit tricky to use. Click Extra provides pre-configured helpers with sane defaults to simplify the logging configuration.
Colored verbosity¶
@command and @group add a colored --verbosity option out of the box. Crank it to DEBUG and you see exactly which loggers were touched, at which level, when:
from click_extra import command, echo
@command
def my_cli():
echo("It works!")
$ my-cli --verbosity DEBUG
debug: Set <Logger click_extra (DEBUG)> to DEBUG.
debug: Set <RootLogger root (DEBUG)> to DEBUG.
debug: Load configuration matching /home/runner/.config/my-cli/{*.toml,*.yaml,*.yml,*.json,*.json5,*.jsonc,*.hjson,*.ini,*.xml,pyproject.toml}
debug: Found /home/runner/work/click-extra/click-extra/pyproject.toml, parsing as pyproject.toml.
debug: pyproject.toml parsing successful, got {'uv': {'dependency-groups': {'docs': {'requires-python': '>= 3.14'}}, 'exclude-newer': '1 week', 'build-backend': {'module-root': ''}, 'required-version': '>=0.11.23,<0.12'}, 'nuitka': {'include-package-data': 'click_extra'}, 'ruff': {'preview': True, 'fix': True, 'unsafe-fixes': True, 'show-fixes': True, 'format': {'docstring-code-format': True}, 'lint': {'ignore': ['B008', 'D400', 'ERA001', 'LOG015'], 'isort': {'combine-as-imports': True}, 'future-annotations': True}}, 'typos': {'default': {'extend-identifiers': {'Github': 'GitHub', 'IOS': 'iOS', 'Javascript': 'JavaScript', 'MacOS': 'macOS', 'PyPi': 'PyPI', 'Typescript': 'TypeScript'}, 'extend-ignore-re': ['(?s)<!-- typos:off -->.*?<!-- typos:on -->', 'chalk\\.ist'], 'extend-ignore-identifiers-re': ['valuE', 'SeCoNd', 'sUper', 'fAlsE', 'nd', 'mey', 'certifi', 'optoin']}}, 'pytest': {'markers': ["network: Tests that require network access (excluded with -m 'not network').", 'once: Tests that only need to run once, not across the full CI matrix.'], 'addopts': ['--durations=10'], 'xfail_strict': True}, 'coverage': {'run': {'branch': True, 'source': ['click_extra']}, 'report': {'precision': 2}}, 'bumpversion': {'current_version': '8.0.2.dev0', 'parse': '(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?', 'serialize': ['{major}.{minor}.{patch}.dev{dev}', '{major}.{minor}.{patch}'], 'ignore_missing_files': True, 'allow_dirty': True, 'parts': {'dev': {'optional_value': 'release', 'values': ['0', 'release']}}, 'files': [{'glob': './**/__init__.py', 'ignore_missing_version': True}, {'filename': './pyproject.toml', 'regex': True, 'search': '(?m)^version = "{current_version}"', 'replace': 'version = "{new_version}"'}, {'filename': './pyproject.toml', 'search': 'releases/tag/v{current_version}', 'replace': 'releases/tag/v{new_version}'}, {'filename': './pyproject.toml', 'ignore_missing_version': True, 'serialize': ['{major}.{minor}.{patch}'], 'search': '-version = "{current_version}"', 'replace': '-version = "{new_version}"'}, {'filename': './changelog.md', 'search': '## [`{current_version}` (unreleased)](', 'replace': '## [`{new_version}` (unreleased)]('}, {'filename': './citation.cff', 'search': 'version: {current_version}', 'replace': 'version: {new_version}'}, {'filename': './citation.cff', 'regex': True, 'search': 'date-released: \\d{{4}}-\\d{{2}}-\\d{{2}}', 'replace': 'date-released: {utcnow:%Y-%m-%d}'}, {'filename': './readme.md', 'ignore_missing_version': True, 'include_bumps': ['dev'], 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/main/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/v{new_version}/'}, {'filename': './docs/tutorial.md', 'ignore_missing_version': True, 'include_bumps': ['dev'], 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/main/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/v{new_version}/'}, {'filename': './readme.md', 'ignore_missing_version': True, 'exclude_bumps': ['dev'], 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/v{current_version}/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/main/'}, {'filename': './docs/tutorial.md', 'ignore_missing_version': True, 'exclude_bumps': ['dev'], 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/v{current_version}/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/main/'}]}, 'mypy': {'check_untyped_defs': True, 'warn_unused_configs': True, 'warn_redundant_casts': True, 'warn_unused_ignores': True, 'warn_return_any': True, 'warn_unreachable': True, 'pretty': True, 'overrides': [{'ignore_missing_imports': True, 'module': ['hjson', 'jsonc', 'pymdownx', 'pymdownx.*']}, {'ignore_missing_imports': True, 'module': ['sphinx.*'], 'follow_imports': 'skip'}]}, 'lychee': {'exclude': ['^https://github.com/.+/issues/[0-9]+#issuecomment-.*$', '^https://github.com/.+/blob/.+#L[0-9]+', '^https://github.com/.+/releases/.+/download/.*$', '^https://doi.org/.+/zenodo.*$', '^https://star-history.com/#.*$', '^https://crates.io/crates/.*$', '^https://asciinema.org.*$', '^https://no-color.org.*$', 'theme.md#(dark|dracula|light|manpage|monokai|nord|solarized-dark)$']}, 'repomatic': {'dependency-graph': {'all-extras': True, 'all-groups': False}, 'exclude': ['skills', 'workflows/debug.yaml'], 'manpages': {'script': 'click_extra.cli:demo'}, 'test-matrix': {'remove': {'os': ['macos-26-intel', 'windows-11-arm']}, 'include': [{'click-version': 'released'}, {'cloup-version': 'released'}], 'exclude': [{'os': 'ubuntu-slim', 'python-version': '3.15'}, {'os': 'macos-26', 'python-version': '3.15'}, {'os': 'windows-2025', 'python-version': '3.15'}], 'full-include': [{'os': 'ubuntu-24.04-arm', 'python-version': '3.10', 'click-version': '8.3.1', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.10', 'click-version': '8.3.2', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.10', 'click-version': '8.3.3', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.10', 'click-version': '8.4.0', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14', 'click-version': 'stable', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14t', 'click-version': 'stable', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14', 'click-version': 'main', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14t', 'click-version': 'main', 'cloup-version': 'released'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14', 'click-version': 'released', 'cloup-version': 'master'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14t', 'click-version': 'released', 'cloup-version': 'master'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14', 'click-version': 'stable', 'cloup-version': 'master'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14t', 'click-version': 'stable', 'cloup-version': 'master'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14', 'click-version': 'main', 'cloup-version': 'master'}, {'os': 'ubuntu-24.04-arm', 'python-version': '3.14t', 'click-version': 'main', 'cloup-version': 'master'}]}}}.
debug: /home/runner/work/click-extra/click-extra/pyproject.toml has no [tool.my-cli] section; falling back to app-dir search.
debug: pyproject.toml CWD search stopped at /home/runner/work/click-extra.
debug: Search filesystem for /home/runner/.config/my-cli/{*.toml,*.yaml,*.yml,*.json,*.json5,*.jsonc,*.hjson,*.ini,*.xml,pyproject.toml}
debug: No configuration file found.
debug: 'click_extra.sphinx' package not found or not installed.
debug: Version string template variables:
debug: {module} : <module 'click_extra.sphinx.click' from '/home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py'>
debug: {module_name} : click_extra.sphinx.click
debug: {module_file} : /home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py
debug: {module_version} : None
debug: {package_name} : click_extra.sphinx
debug: {package_version}: None
debug: {author} : None
debug: {license} : None
debug: {exec_name} : click_extra.sphinx.click
debug: {version} : None
debug: {git_repo_path} : /home/runner/work/click-extra/click-extra
debug: {git_branch} : main
debug: {git_long_hash} : 08c7736b6ce9932ce275ee06a2d2ce7f9b82aa3f
debug: {git_short_hash} : 08c7736
debug: {git_date} : 2026-06-22 19:19:53 +0400
debug: {git_tag} : None
debug: {git_tag_sha} : None
debug: {git_distance} : None
debug: {git_dirty} : clean
debug: {prog_name} : my-cli
debug: {env_info} : {'username': '-', 'guid': '22f57e0789c121b5aef96f55c2905ca', 'hostname': '-', 'hostfqdn': '-', 'uname': {'system': 'Linux', 'node': '-', 'release': '6.1.146.1-microsoft-standard', 'version': '#1 SMP Mon Jul 21 20:38:16 UTC 2025', 'machine': 'x86_64', 'processor': 'x86_64'}, 'linux_dist_name': '', 'linux_dist_version': '', 'cpu_count': 1, 'fs_encoding': 'utf-8', 'ulimit_soft': 1048576, 'ulimit_hard': 1048576, 'cwd': '-', 'umask': '0o2', 'python': {'argv': '-', 'bin': '-', 'version': '3.14.6 (main, Jun 11 2026, 04:03:53) [Clang 22.1.3 ]', 'compiler': 'Clang 22.1.3 ', 'build_date': 'Jun 11 2026 04:03:53', 'version_info': [3, 14, 6, 'final', 0], 'features': {'openssl': 'OpenSSL 3.5.7 9 Jun 2026', 'expat': 'expat_2.8.1', 'sqlite': '3.53.1', 'tkinter': '9.0', 'zlib': '1.3.1', 'unicode_wide': True, 'readline': True, '64bit': True, 'ipv6': True, 'threading': True, 'urandom': True}}, 'time_utc': '2026-06-22 15:32:47.664928+00:00', 'time_utc_offset': 0.0, '_eco_version': '1.1.0'}
It works!
debug: Reset <RootLogger root (DEBUG)> to WARNING.
debug: Reset <Logger click_extra (DEBUG)> to WARNING.
Each logger’s level prints as a debug message. They are set to DEBUG at the start of the command and reset back to their default WARNING at the end. The --verbosity flag also surfaces in --help:
$ my-cli --help
Usage: my-cli [OPTIONS]
Options:
--time / --no-time Measure and print elapsed execution time.
[default: no-time]
--config CONFIG_PATH Location of the configuration file. Supports
local path with glob patterns or remote URL.
[default: ~/.config/my-cli/{*.toml,*.yaml,*.yml,*
.json,*.json5,*.jsonc,*.hjson,*.ini,*.xml,pyproje
ct.toml}]
--no-config Ignore all configuration files and only use
command line parameters and environment
variables.
--validate-config FILE Validate the configuration file and exit.
--accessible Accessibility mode: disable colors and render
tables in a plain, screen-reader-friendly format.
--color [auto|always|never] Colorize the output. A bare --color is the same
as --color=always. [default: auto]
--no-color Disable colorization (alias of --color=never).
--progress / --no-progress Show progress indicators during long operations.
Disabled for non-interactive output (pipes, dumb
terminals, CI) and by --accessible. [default:
progress]
--theme [dark|dracula|light|manpage|monokai|nord|solarized_dark]
Color theme used for help screens. [default:
dark]
--show-params Show all CLI parameters, their provenance,
defaults and value, then exit.
--table-format [aligned|asciidoc|colon-grid|csv|csv-excel|csv-excel-tab|csv-unix|double-grid|double-outline|fancy-grid|fancy-outline|github|grid|heavy-grid|heavy-outline|hjson|html|jira|json|json5|jsonc|latex|latex-booktabs|latex-longtable|latex-raw|mediawiki|mixed-grid|mixed-outline|moinmoin|orgtbl|outline|pipe|plain|presto|pretty|psql|rounded-grid|rounded-outline|rst|simple|simple-grid|simple-outline|textile|toml|tsv|unsafehtml|vertical|xml|yaml|youtrack]
Rendering style of tables. [default: rounded-
outline]
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
[default: WARNING]
-v, --verbose Increase the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
-q, --quiet Decrease the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
--man Show the command's man page (roff) and exit.
--version Show the version and exit.
-h, --help Show this message and exit.
Default behavior of the option:
print to
<stderr>,send messages to the
rootlogger,show
WARNING-level messages and above,use
{-style format strings,render logs with the
{levelname}: {message}format,color the log’s
{levelname}variable.
Tip
The reconciled log level chosen between --verbosity and --verbose/-v is published on ctx.meta as VERBOSITY_LEVEL, alongside the raw VERBOSITY and VERBOSE values from each option. See the available keys table to read them from your own callbacks.
Important
Besides --verbosity, there are the -v/--verbose and -q/--quiet options. They both work relative to --verbosity: -v raises the level in steps and -q lowers it, simply by repeating them.
In the rest of this documentation, we will mainly focus on the canonical --verbosity option to keep things simple (logging is already complicated enough…).
Standalone option¶
The verbosity option can be used independently of @command, and you can attach it to a vanilla Click command:
import logging
import click
import click_extra
@click.command
@click_extra.verbosity_option
def vanilla_command():
click.echo("It works!")
logging.info("We're printing stuff.")
$ vanilla --help
Usage: vanilla [OPTIONS]
Options:
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
--help Show this message and exit.
$ vanilla
It works!
$ vanilla --verbosity INFO
It works!
info: We're printing stuff.
The -v/--verbose option can also be used as a standalone option:
import logging
import click
import click_extra
@click.command
@click_extra.verbose_option
def verbose_command():
click.echo("It works!")
logging.info("We're printing stuff.")
$ verbose --help
Usage: verbose [OPTIONS]
Options:
-v, --verbose Increase the default WARNING verbosity by one level for each
additional repetition of the option.
--help Show this message and exit.
$ verbose
It works!
$ verbose -v
It works!
info: We're printing stuff.
Symmetric -q/--quiet¶
-q/--quiet is the mirror image of -v/--verbose: each repetition lowers the verbosity one level instead of raising it. Both flags share a single counter centered on the default WARNING level, so passing both cancels out.
import logging
import click
from click_extra import quiet_option, verbose_option
@click.command
@verbose_option
@quiet_option
def weather():
logging.critical("Storm warning issued.")
logging.error("Sensor offline.")
logging.warning("Strong winds expected.")
logging.info("Sky is clear.")
logging.debug("Humidity at 47%.")
By default the command reports WARNING and above:
$ weather
critical: Storm warning issued.
error: Sensor offline.
warning: Strong winds expected.
A single -q drops the level one step to ERROR, hiding the warning:
$ weather -q
critical: Storm warning issued.
error: Sensor offline.
Repeat it to go quieter still; -qq keeps only CRITICAL:
$ weather -qq
critical: Storm warning issued.
Since -v and -q form a single signed counter, mixing them nets out. -v -q leaves the level untouched:
$ weather -v -q
critical: Storm warning issued.
error: Sensor offline.
warning: Strong winds expected.
Note
-q stops at CRITICAL, the quietest level: it never suppresses CRITICAL messages themselves. It also only lowers the logging verbosity, and does not silence regular echo output.
Default logger¶
The --verbosity option is by default attached to the global root logger.
This allows you to use module-level helpers like logging.debug. That way you don’t have to worry about setting up your own logger. And logging messages can be easily produced with minimal code:
import logging
import click
import click_extra
@click.command
@click_extra.verbosity_option
def my_cli():
# Print a message for each level.
logging.critical("Complete meltdown!")
logging.error("Does not compute.")
logging.warning("Mad scientist at work!")
logging.info("This is a message.")
logging.debug("We're printing stuff.")
The --verbosity option print by default all messages at the WARNING level and above:
$ my-cli
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
But each level can be selected with the option:
$ my-cli --verbosity CRITICAL
critical: Complete meltdown!
$ my-cli --verbosity ERROR
critical: Complete meltdown!
error: Does not compute.
$ my-cli --verbosity WARNING
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
$ my-cli --verbosity INFO
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
info: This is a message.
$ my-cli --verbosity DEBUG
debug: Set <Logger click_extra (DEBUG)> to DEBUG.
debug: Set <RootLogger root (DEBUG)> to DEBUG.
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
info: This is a message.
debug: We're printing stuff.
debug: Reset <RootLogger root (DEBUG)> to WARNING.
debug: Reset <Logger click_extra (DEBUG)> to WARNING.
Caution
root is the default logger associated with --verbosity. Which means that, if not configured, all loggers will be printed at the verbosity level set by the option:
import logging
import click
import click_extra
@click.command
@click_extra.verbosity_option
def multiple_loggers_cli():
# Use the default root logger.
root_logger = logging.getLogger()
root_logger.warning("Root warning message")
root_logger.info("Root info message")
# Create a custom logger and use it.
my_logger = logging.getLogger("my_logger")
my_logger.warning("My warning message")
my_logger.info("My info message")
In that case, a normal invocation will only print the default WARNING messages:
$ multiple-loggers-cli
warning: Root warning message
warning: My warning message
And calling --verbosity INFO will print both root and my_logger messages of that level:
$ multiple-loggers-cli --verbosity INFO
warning: Root warning message
info: Root info message
warning: My warning message
info: My info message
To prevent this behavior, you can associate the --verbosity option with your own custom logger. This is explained in the next section.
Custom logger¶
The preferred way to customize log messages is to create your own logger and attach it to the --verbosity option.
This can be done with new_logger(). Here is how we can for example change the format of the log messages:
import logging
from click import command
from click_extra import new_logger, verbosity_option
new_logger(
name="app_logger",
format="{levelname} | {name} | {message}"
)
@command
@verbosity_option(default_logger="app_logger")
def custom_logger_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("app_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
That way the root logger keeps its default format, while the custom logger uses the new one:
$ custom-logger-cli
warning: Root logger warning
warning | app_logger | Custom warning
And changing the verbosity level will only affect the custom logger:
$ custom-logger-cli --verbosity INFO
warning: Root logger warning
warning | app_logger | Custom warning
info | app_logger | Custom info
Now if we don’t explicitly pass the custom logger to the --verbosity option, the default root logger will be tied to it instead:
import logging
from click import command
from click_extra import new_logger, verbosity_option
new_logger(
name="app_logger",
format="{levelname} | {name} | {message}"
)
@command
@verbosity_option
def root_verbosity_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("app_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
In that case the default behavior doesn’t change and messages are rendered in their own logger’s format, at the default WARNING level:
$ root-verbosity-cli
warning: Root logger warning
warning | app_logger | Custom warning
But changing the verbosity level only affects root, in the opposite of the previous example:
$ root-verbosity-cli --verbosity INFO
warning: Root logger warning
info: Root logger info
warning | app_logger | Custom warning
Important
By design, new loggers are always created as sub-loggers of root. And as such, their messages are propagated back to it.
But new_logger() always creates new loggers by setting their propagate attribute to False. This means that messages of new loggers won’t be propagated to their parents.
This is the reason why, in the example above, the root and app_logger loggers are independent.
Let’s experiment with that property and set the propagate attribute to True:
import logging
from click import command
from click_extra import new_logger, verbosity_option
new_logger(
name="app_logger",
propagate=True,
format="{levelname} | {name} | {message}"
)
@command
@verbosity_option
def logger_propagation_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("app_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
$ logger-propagation-cli
warning: Root logger warning
warning | app_logger | Custom warning
warning: Custom warning
Here you can immediately spot the issue with propagation: app_logger’s messages are displayed twice. Once in their custom format, and once in the format of the root logger.
See also
The reason for that hierarchycal design is to allow for dot-separated logger names, like foo.bar.baz. Which allows for even more granular control of loggers by filtering.
Tip
You can creatively configure loggers to produce any kind of messages, like this JSON-like format:
import logging
from click import command
from click_extra import new_logger, verbosity_option
new_logger(
name="json_logger",
format='{{"time": "{asctime}", "name": "{name}", "level": "{levelname}", "msg": "{message}"}}',
)
@command
@verbosity_option(default_logger="json_logger")
def json_logs_cli():
my_logger = logging.getLogger("json_logger")
my_logger.info("This is an info message.")
$ json-logs-cli --verbosity INFO
{"time": "2026-06-22 15:32:47,721", "name": "json_logger", "level": "info", "msg": "This is an info message."}
Hint
Because loggers are registered in a global registry, you can set them up in one place and use them in another. That is the idiomatic approach, which consist in always referring to them by name, as in all examples above.
But for convenience, you can pass the logger object directly to the option:
import logging
from click import command
from click_extra import new_logger, verbosity_option
my_logger = new_logger(name="app_logger")
@command
@verbosity_option(default_logger=my_logger)
def logger_object_cli():
# Default root logger.
logging.warning("Root warning message")
logging.info("Root info message")
# My custom logger.
my_logger.warning("My warning message")
my_logger.info("My info message")
$ logger-object-cli --verbosity INFO
warning: Root warning message
warning: My warning message
info: My info message
Global configuration¶
If you want to change the global configuration of all loggers, you can rely on new_logger. Because the latter defaults to the root logger, any default logger propagating their messages to it will be affected:
import logging
from click import command
from click_extra import new_logger, verbosity_option
root_logger = new_logger(format="{levelname} | {name} | {message}")
@command
@verbosity_option(default_logger=root_logger)
def root_format_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("my_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
$ root-format-cli
warning | root | Root logger warning
warning | my_logger | Custom warning
$ root-format-cli --verbosity INFO
warning | root | Root logger warning
info | root | Root logger info
warning | my_logger | Custom warning
info | my_logger | Custom info
Verbose base level¶
When both --verbosity and -v/--verbose are present on the same command, -v increments from the default level of --verbosity. If you change that default, the base level of -v shifts accordingly:
import logging
import click
from click_extra import LogLevel, verbose_option, verbosity_option
@click.command
@verbosity_option(default=LogLevel.ERROR)
@verbose_option
def shifted_cli():
logging.critical("critical message")
logging.error("error message")
logging.warning("warning message")
logging.info("info message")
Without -v, only CRITICAL and ERROR messages are shown:
$ shifted-cli
critical: critical message
error: error message
With -v, the level increases by one step from ERROR to WARNING:
$ shifted-cli -v
critical: critical message
error: error message
warning: warning message
The help message reflects the custom base level:
$ shifted-cli --help
Usage: shifted-cli [OPTIONS]
Options:
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
-v, --verbose Increase the default ERROR verbosity by one level for each
additional repetition of the option.
--help Show this message and exit.
Caution
You can attach several @verbose_option decorators to the same command, each targeting a different logger via default_logger. Individual options work in isolation, but combining them on the same invocation (like -d -H) does not work as expected: an internal reconciliation mechanism shares a single verbosity level across all options, so the second option may be silently skipped if its target level is equal to or less verbose than the first’s.
Until this limitation is resolved, prefer a single @verbosity_option with a shared logger if you need to control multiple loggers from the CLI.
Get verbosity level¶
You can get the name of the current verbosity level from the context or the logger itself:
import logging
from click import command, echo, pass_context
from click_extra import verbosity_option
@command
@verbosity_option
@pass_context
def vanilla_command(ctx):
level_from_context = ctx.meta["click_extra.verbosity_level"]
level_from_logger = logging._levelToName[logging.getLogger().getEffectiveLevel()]
echo(f"Level from context: {level_from_context!r}")
echo(f"Level from logger: {level_from_logger!r}")
$ vanilla --verbosity INFO
Level from context: <LogLevel.INFO: 20>
Level from logger: 'INFO'
Internal click_extra logger¶
Click Extra has its own logger, named click_extra, which is used to print debug messages to inspect its own internal behavior.
click_extra.logging API¶
classDiagram
ExtraOption <|-- _VerbosityOption
Formatter <|-- Formatter
IntEnum <|-- LogLevel
StreamHandler <|-- StreamHandler
_VerbosityOption <|-- QuietOption
_VerbosityOption <|-- VerboseOption
_VerbosityOption <|-- VerbosityOption
Logging utilities.
- class click_extra.logging.LogLevel(*values)[source]
Bases:
IntEnumMapping of canonical log level names to their integer level.
That’s our own version of logging._nameToLevel, but:
sorted from lowest to highest verbosity,
- excludes the following levels:
NOTSET, which is considered internalWARN, whichis obsoleteFATAL, which shouldn’t be used and has been replaced by CRITICAL
- CRITICAL = 50
- ERROR = 40
- WARNING = 30
- INFO = 20
- DEBUG = 10
- click_extra.logging.DEFAULT_LEVEL: LogLevel = LogLevel.WARNING
WARNINGis the default level we expect any loggers to starts their lives at.WARNINGhas been chosen as it is the level at which the default Python’s global root logger is set up.This value is also used as the default level for
VerbosityOption.
- class click_extra.logging.StreamHandler(stream=None)[source]
Bases:
StreamHandlerA handler to output logs to the console.
Wraps
logging.StreamHandler, but useclick.echo()to support color printing.Only
<stderr>or<stdout>are allowed as output stream.If stream is not specified,
<stderr>is used by defaultInitialize the handler.
If stream is not specified, sys.stderr is used.
- property stream: IO[Any]
The stream to which logs are written.
A proxy of the parent
logging.StreamHandler’s stream attribute.Redefined here to enforce checks on the stream value.
- emit(record)[source]
Use
click.echo()to print to the console.- Return type:
- class click_extra.logging.Formatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None)[source]
Bases:
FormatterClick Extra’s default log formatter.
Initialize the formatter with specified format strings.
Initialize the formatter either with the specified format string, or a default as described above. Allow for specialized date formatting with the optional datefmt argument. If datefmt is omitted, you get an ISO8601-like (or RFC 3339-like) format.
Use a style parameter of ‘%’, ‘{’ or ‘$’ to specify that you want to use one of %-formatting,
str.format()({}) formatting orstring.Templateformatting in your format string.Changed in version 3.2: Added the
styleparameter.- formatMessage(record)[source]
Colorize the record’s log level name before calling the standard formatter.
Colors are sourced from a
click_extra.theme.HelpTheme, resolved per-invocation viaclick_extra.theme.get_current_theme().- Return type:
- click_extra.logging.basicConfig(*, filename=None, filemode='a', format='{levelname}: {message}', datefmt=None, style='{', level=None, stream=None, handlers=None, force=False, encoding=None, errors='backslashreplace', stream_handler_class=<class 'click_extra.logging.StreamHandler'>, file_handler_class=<class 'logging.FileHandler'>, formatter_class=<class 'click_extra.logging.Formatter'>)[source]
Configure the global
rootlogger.This function is a wrapper around Python standard library’s
logging.basicConfig(), but with additional parameters and tweaked defaults.It sets up the global
rootlogger, and optionally adds a file or stream handler to it.Differences in default values:
Argument
basicConfig()defaultlogging.basicConfig()defaultstyle{%format{levelname}: {message}%(levelname)s:%(name)s:%(message)sThis function takes the same parameters as
logging.basicConfig(), but require them to be all passed as explicit keywords arguments.- Parameters:
filename (
str|None) – Specifies that alogging.FileHandlerbe created, using the specified filename, rather than anStreamHandler.filemode (
str) –If filename is specified, open the file in this
mode.Defaults to
a.Use the specified format string for the handler.
Defaults to
{levelname}: {message}.datefmt (
str|None) – Use the specified date/time format, as accepted bytime.strftime().style (
Literal['%','{','$']) –If format is specified, use this style for the format string:
%for printf-style,{forstr.format(),$forstring.Template.
Defaults to
{.level (
int|str|None) – Set therootlogger level to the specified level.stream (
IO[Any] |None) – Use the specified stream to initialize theStreamHandler. Note that this argument is incompatible with filename - if both are present, aValueErroris raised.handlers (
Iterable[Handler] |None) – If specified, this should be an iterable of already created handlers to add to therootlogger. Any handlers which don’t already have a formatter set will be assigned the default formatter created in this function. Note that this argument is incompatible with filename or stream - if both are present, aValueErroris raised.force (
bool) – If this argument is specified asTrue, any existing handlers attached to therootlogger are removed and closed, before carrying out the configuration as specified by the other arguments.encoding (
str|None) – Name of the encoding used to decode or encode the file. To be specified along with filename, and passed tologging.FileHandlerfor opening the output file.errors (
str|None) – Optional string that specifies how encoding and decoding errors are to be handled by thelogging.FileHandler. Defaults tobackslashreplace. Note that ifNoneis specified, it will be passed as such toopen().
- Return type:
Important
Always keep the signature of this function, the default values of its parameters and its documentation in sync with the one from Python’s standard library.
These new arguments are available for better configurability:
- Parameters:
stream_handler_class (
type[Handler]) – Alogging.Handlerclass that will be used inlogging.basicConfig()to create a default stream-based handler. Defaults toStreamHandler.file_handler_class (
type[Handler]) – Alogging.Handlerclass that will be used inlogging.basicConfig()to create a default file-based handler. Defaults tologging.FileHandler.formatter_class (
type[Formatter]) – Alogging.Formatterclass of the formatter that will be used inlogging.basicConfig()to setup the default formatter. Defaults toFormatter.
Note
I don’t like the camel-cased name of this function and would have called it
basic_config(), but it’s kept this way for consistency with Python’s standard librarylogging.basicConfig().
- click_extra.logging.new_logger(name='root', *, propagate=False, force=True, **kwargs)[source]
Setup a logger in the style of Click Extra.
By default, this helper will:
Fetch the loggerregistered under thenameparameter, or creates a new one with that name if it doesn’t exist,Set the logger’s
propagateattribute toFalse,Force removal of any existing handlers and formatters attached to the logger,
Attach a new
StreamHandlerwithFormatter,Return the logger object.
This function is a wrapper around
basicConfig()and takes the same keywords arguments.- Parameters:
name (
str) – ID of the logger to setup. IfNone, Python’srootlogger will be used. If a logger with the provided name is not found in the global registry, a new logger with that name will be created.propagate (
bool) – Sets the logger’spropagateattribute. Defaults toFalse.force (
bool) – Same as the force parameter fromlogging.basicConfig()andbasicConfig(). Defaults toTrue.kwargs – Any other keyword parameters supported by
logging.basicConfig()andbasicConfig().
- Return type:
- class click_extra.logging.VerbosityOption(param_decls=None, default_logger='root', default=LogLevel.WARNING, metavar='LEVEL', type=EnumChoice('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'), help='Either CRITICAL, ERROR, WARNING, INFO, DEBUG.', **kwargs)[source]
Bases:
_VerbosityOption--verbosity LEVELoption to set the log level of_VerbosityOption.Set up a verbosity-altering option.
- Parameters:
default_logger (
Logger|str) – If alogging.Loggerobject is provided, that’s the instance to which we will set the level to. If the parameter is a string and is found in the global registry, we will use it as the logger’s ID. Otherwise, we will create a new logger withnew_logger()Default to the globalrootlogger.
- class click_extra.logging.VerboseOption(param_decls=None, count=True, **kwargs)[source]
Bases:
_VerbosityOption--verbose/-voption to raise the log level of_VerbosityOptionby one step per repetition.Each
-vraises the verbosity by oneLogLevelstep. The option can be repeated, so-vv(or-v -v) raises it by two steps.The base level the counter starts from is sourced from
VerbosityOption.default. So with--verbosity’s default left atWARNING:-vraises the level toINFO,-vvraises the level toDEBUG,any further repetition is clamped at the loudest level, so
-vvvvvfor example resolves toDEBUG.
-vshares a single signed counter withQuietOption’s-q, so the two cancel out:-v -qleaves the level unchanged. See_VerbosityOption.resolve_levelfor the full reconciliation rule with--verbosity.Set up a verbosity-altering option.
- Parameters:
default_logger – If a
logging.Loggerobject is provided, that’s the instance to which we will set the level to. If the parameter is a string and is found in the global registry, we will use it as the logger’s ID. Otherwise, we will create a new logger withnew_logger()Default to the globalrootlogger.
- class click_extra.logging.QuietOption(param_decls=None, count=True, **kwargs)[source]
Bases:
_VerbosityOption--quiet/-qoption to lower the log level of_VerbosityOptionby one step per repetition.The symmetric counterpart of
VerboseOption: where-vraises the verbosity oneLogLevelstep at a time,-qlowers it. Starting fromVerbosityOption.default(WARNINGby default):-qlowers the level toERROR,-qqlowers the level toCRITICAL,any further repetition is clamped at the quietest level, so
-qqqqqfor example resolves toCRITICAL.
-qshares a single signed counter withVerboseOption’s-v, so the two cancel out:-v -qleaves the level unchanged. See_VerbosityOption.resolve_levelfor the full reconciliation rule with--verbosity.Set up a verbosity-altering option.
- Parameters:
default_logger – If a
logging.Loggerobject is provided, that’s the instance to which we will set the level to. If the parameter is a string and is found in the global registry, we will use it as the logger’s ID. Otherwise, we will create a new logger withnew_logger()Default to the globalrootlogger.