Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions docs/source/examples/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1323,4 +1323,130 @@ Actually run it:
<strong style="opacity: 0.7; padding-bottom: 0.5em; display: inline-block"><span style="user-select: none">$ </span>python ./15_compact_help.py --host localhost --port 8080</strong>
Starting server with config:
ServerConfig(host='localhost', port=8080, workers=4, timeout=30, ssl_enabled=False, ssl_cert_path='/etc/ssl/cert.pem', ssl_key_path='/etc/ssl/key.pem', max_request_size=10485760, cors_origins='*', log_level='info', log_file='/var/log/server.log', cache_enabled=True, cache_size=1000, compression_enabled=True, keepalive_timeout=5, db_host='localhost', db_port=5432, db_name='appdb')
</pre>
.. _example-16_verbosity:

Verbosity flags
---------------

:class:`tyro.extras.Verbosity` provides standard ``-v``/``--verbose`` and
``-q``/``--quiet`` count flags that map to Python :mod:`logging` levels,
with the two flags being mutually exclusive.

By default, when ``Verbosity`` is a nested field, long flags carry the field
prefix (``--verbosity.verbose``). Annotating with
:data:`tyro.conf.OmitArgPrefixes` promotes them to the top level
(``--verbose``, ``--quiet``). Short aliases ``-v`` and ``-q`` always work
regardless of nesting.

Inspired by `clap-verbosity-flag <https://docs.rs/clap-verbosity-flag>`_ from
the Rust/clap ecosystem.


.. code-block:: python
:linenos:

# 16_verbosity.py
import dataclasses
import logging
from pathlib import Path

from typing_extensions import Annotated

import tyro
from tyro.conf import OmitArgPrefixes, Positional
from tyro.extras import Verbosity

logger = logging.getLogger(__name__)

@dataclasses.dataclass
class Args:
"""Process files with configurable log verbosity."""

path: Positional[Path] = dataclasses.field(default_factory=Path.cwd)
"""Path to process."""

# Log verbosity. OmitArgPrefixes promotes --verbosity.verbose/--verbosity.quiet
# to --verbose/--quiet at the top level.
verbosity: Annotated[Verbosity, OmitArgPrefixes] = dataclasses.field(
default_factory=Verbosity
)

if __name__ == "__main__":
args = tyro.cli(Args)
logging.basicConfig(level=args.verbosity.log_level())
logger.info("path=%s", args.path)




.. raw:: html

<pre class="highlight" style="padding: 1em; box-sizing: border-box; font-size: 0.85em; line-height: 1.2em;">
<strong style="opacity: 0.7; padding-bottom: 0.5em; display: inline-block"><span style="user-select: none">$ </span>python ./16_verbosity.py --help /home</strong>
<span style="font-weight: bold">usage:</span> ./16_verbosity.py [-h] <span style="font-weight: bold">[PATH]</span> [-v] [-q]

Process files with configurable log verbosity.

<span style="font-weight: lighter; color: #808080">╭</span><span style="font-weight: lighter; color: #808080">─</span> <span style="font-weight: lighter; color: #808080">positional</span><span style="font-weight: lighter; color: #808080"> arguments</span> <span style="font-weight: lighter; color: #808080">───────────────────────────────────────────────────────╮</span>
<span style="font-weight: lighter; color: #808080">│</span> [PATH] <span style="font-weight: lighter">Path</span><span style="font-weight: lighter"> to</span><span style="font-weight: lighter"> process.</span> <span style="color: #008080">(default:</span><span style="color: #008080"> /home/nobackup/Documents/github.co</span> <span style="font-weight: lighter; color: #808080">│</span>
<span style="font-weight: lighter; color: #808080">│</span> <span style="color: #008080">m/jRimbault/tyro/examples/01_basics) </span> <span style="font-weight: lighter; color: #808080">│</span>
<span style="font-weight: lighter; color: #808080">╰</span><span style="font-weight: lighter; color: #808080">──────────────────────────────────────────────────────────────────────────────</span><span style="font-weight: lighter; color: #808080">╯</span>
<span style="font-weight: lighter; color: #808080">╭</span><span style="font-weight: lighter; color: #808080">─</span> <span style="font-weight: lighter; color: #808080">options</span> <span style="font-weight: lighter; color: #808080">────────────────────────────────────────────────────────────────────╮</span>
<span style="font-weight: lighter; color: #808080">│</span> -h, --help <span style="font-weight: lighter">show</span><span style="font-weight: lighter"> this</span><span style="font-weight: lighter"> help</span><span style="font-weight: lighter"> message</span><span style="font-weight: lighter"> and</span><span style="font-weight: lighter"> exit </span> <span style="font-weight: lighter; color: #808080">│</span>
<span style="font-weight: lighter; color: #808080">╰</span><span style="font-weight: lighter; color: #808080">──────────────────────────────────────────────────────────────────────────────</span><span style="font-weight: lighter; color: #808080">╯</span>
<span style="font-weight: lighter; color: #808080">╭</span><span style="font-weight: lighter; color: #808080">─</span> <span style="font-weight: lighter; color: #808080">verbosity</span> <span style="font-weight: lighter; color: #808080">──────────────────────────────────────────────────────────────────╮</span>
<span style="font-weight: lighter; color: #808080">│</span> At most one argument can be overridden. <span style="font-weight: lighter; color: #808080">│</span>
<span style="font-weight: lighter; color: #808080">│</span> <span style="font-weight: lighter; color: #808080">────────────────────────────────────────────────────────────────────────────</span> <span style="font-weight: lighter; color: #808080">│</span>
<span style="font-weight: lighter; color: #808080">│</span> -v, --verbose <span style="font-weight: lighter">Increase</span><span style="font-weight: lighter"> log</span><span style="font-weight: lighter"> verbosity.</span> <span style="color: #008080">(repeatable) </span> <span style="font-weight: lighter; color: #808080">│</span>
<span style="font-weight: lighter; color: #808080">│</span> -q, --quiet <span style="font-weight: lighter">Decrease</span><span style="font-weight: lighter"> log</span><span style="font-weight: lighter"> verbosity.</span> <span style="color: #008080">(repeatable) </span> <span style="font-weight: lighter; color: #808080">│</span>
<span style="font-weight: lighter; color: #808080">╰</span><span style="font-weight: lighter; color: #808080">──────────────────────────────────────────────────────────────────────────────</span><span style="font-weight: lighter; color: #808080">╯</span>
</pre>



.. raw:: html

<pre class="highlight" style="padding: 1em; box-sizing: border-box; font-size: 0.85em; line-height: 1.2em;">
<strong style="opacity: 0.7; padding-bottom: 0.5em; display: inline-block"><span style="user-select: none">$ </span>python ./16_verbosity.py -v /home</strong>
INFO:__main__:path=/home
</pre>



.. raw:: html

<pre class="highlight" style="padding: 1em; box-sizing: border-box; font-size: 0.85em; line-height: 1.2em;">
<strong style="opacity: 0.7; padding-bottom: 0.5em; display: inline-block"><span style="user-select: none">$ </span>python ./16_verbosity.py -vv /home</strong>
INFO:__main__:path=/home
</pre>



.. raw:: html

<pre class="highlight" style="padding: 1em; box-sizing: border-box; font-size: 0.85em; line-height: 1.2em;">
<strong style="opacity: 0.7; padding-bottom: 0.5em; display: inline-block"><span style="user-select: none">$ </span>python ./16_verbosity.py -q /home</strong>
</pre>



.. raw:: html

<pre class="highlight" style="padding: 1em; box-sizing: border-box; font-size: 0.85em; line-height: 1.2em;">
<strong style="opacity: 0.7; padding-bottom: 0.5em; display: inline-block"><span style="user-select: none">$ </span>python ./16_verbosity.py --verbose /home</strong>
INFO:__main__:path=/home
</pre>



.. raw:: html

<pre class="highlight" style="padding: 1em; box-sizing: border-box; font-size: 0.85em; line-height: 1.2em;">
<strong style="opacity: 0.7; padding-bottom: 0.5em; display: inline-block"><span style="user-select: none">$ </span>python ./16_verbosity.py --quiet --verbose /home</strong>
<span style="color: #800000">╭</span><span style="color: #800000">─</span> <span style="font-weight: bold; color: #800000">Mutually</span><span style="font-weight: bold; color: #800000"> exclusive</span><span style="font-weight: bold; color: #800000"> arguments</span> <span style="color: #800000">────────────────────────────╮</span>
<span style="color: #800000">│</span> Arguments --quiet and --verbose are not allowed together! <span style="color: #800000">│</span>
<span style="color: #800000">│</span> <span style="color: #800000">─────────────────────────────────────────────────────────</span> <span style="color: #800000">│</span>
<span style="color: #800000">│</span> For full helptext, run <span style="font-weight: bold">./16_verbosity.py</span><span style="font-weight: bold"> --help </span> <span style="color: #800000">│</span>
<span style="color: #800000">╰</span><span style="color: #800000">───────────────────────────────────────────────────────────</span><span style="color: #800000">╯</span>
</pre>
56 changes: 56 additions & 0 deletions examples/01_basics/16_verbosity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Verbosity flags

:class:`tyro.extras.Verbosity` provides standard ``-v``/``--verbose`` and
``-q``/``--quiet`` count flags that map to Python :mod:`logging` levels,
with the two flags being mutually exclusive.

By default, when ``Verbosity`` is a nested field, long flags carry the field
prefix (``--verbosity.verbose``). Annotating with
:data:`tyro.conf.OmitArgPrefixes` promotes them to the top level
(``--verbose``, ``--quiet``). Short aliases ``-v`` and ``-q`` always work
regardless of nesting.

Inspired by `clap-verbosity-flag <https://docs.rs/clap-verbosity-flag>`_ from
the Rust/clap ecosystem.

Usage:

python ./16_verbosity.py --help /home
python ./16_verbosity.py -v /home
python ./16_verbosity.py -vv /home
python ./16_verbosity.py -q /home
python ./16_verbosity.py --verbose /home
python ./16_verbosity.py --quiet --verbose /home
"""

import dataclasses
import logging
from pathlib import Path

from typing_extensions import Annotated

import tyro
from tyro.conf import OmitArgPrefixes, Positional
from tyro.extras import Verbosity

logger = logging.getLogger(__name__)


@dataclasses.dataclass
class Args:
"""Process files with configurable log verbosity."""

path: Positional[Path] = dataclasses.field(default_factory=Path.cwd)
"""Path to process."""

# Log verbosity. OmitArgPrefixes promotes --verbosity.verbose/--verbosity.quiet
# to --verbose/--quiet at the top level.
verbosity: Annotated[Verbosity, OmitArgPrefixes] = dataclasses.field(
default_factory=Verbosity
)


if __name__ == "__main__":
args = tyro.cli(Args)
logging.basicConfig(level=args.verbosity.log_level())
logger.info("path=%s", args.path)
29 changes: 27 additions & 2 deletions src/tyro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,40 @@

from . import conf as conf
from . import constructors as constructors
from . import extras as extras
from ._cli import cli as cli
from ._settings import _experimental_options as _experimental_options
from ._singleton import MISSING as MISSING
from ._singleton import MISSING_NONPROP as MISSING_NONPROP

if TYPE_CHECKING:
from . import extras as extras

# Deprecated interface.
if not TYPE_CHECKING:
from ._deprecated import * # noqa
from .constructors._primitive_spec import (
UnsupportedTypeAnnotationError as UnsupportedTypeAnnotationError,
)

_DEPRECATED_LAZY = {
"parse": ("._cli", "cli"),
"from_yaml": (".extras._serialization", "from_yaml"),
"to_yaml": (".extras._serialization", "to_yaml"),
}


def __getattr__(name: str):
if name == "extras":
import importlib

extras = importlib.import_module(".extras", __name__)
globals()["extras"] = extras
return extras
if name in _DEPRECATED_LAZY:
import importlib

module_path, attr = _DEPRECATED_LAZY[name]
mod = importlib.import_module(module_path, __name__)
val = getattr(mod, attr)
globals()[name] = val
return val
raise AttributeError(f"module 'tyro' has no attribute {name!r}")
1 change: 1 addition & 0 deletions src/tyro/extras/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
from ._subcommand_cli_from_dict import (
subcommand_cli_from_dict as subcommand_cli_from_dict,
)
from ._verbosity import Verbosity as Verbosity
83 changes: 83 additions & 0 deletions src/tyro/extras/_verbosity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Verbosity type for ``-v``/``-q`` count flags with log level computation.

Inspired by `clap-verbosity-flag <https://docs.rs/clap-verbosity-flag>`_ from the
Rust/clap ecosystem, which is maintained by the clap maintainers and provides the
same pattern for Rust CLIs.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass

from typing_extensions import Annotated

from .. import conf
from ..conf import UseCounterAction

# Shared mutex group: at most one of --verbose / --quiet can be specified.
_verbosity_mutex: object = conf.create_mutex_group(
required=False,
title="verbosity",
)


@dataclass(frozen=True)
class Verbosity:
"""Parsed verbosity counters from ``-v``/``-q`` CLI flags.

Drop into any tyro CLI struct to get standard ``--verbose``/``-v`` and
``--quiet``/``-q`` count flags that map to Python :mod:`logging` levels.
The two flags are mutually exclusive.

Example::

import logging
import tyro
from dataclasses import dataclass, field
from tyro.extras import Verbosity

@dataclass
class Args:
verbosity: Verbosity = Verbosity()

args = tyro.cli(Args)
logging.basicConfig(level=args.verbosity.log_level())

Default level mapping (baseline: ``logging.WARNING``):

.. code-block:: text

(none) → WARNING (30)
-v → INFO (20)
-vv → DEBUG (10)
-q → ERROR (40)
-qq → CRITICAL (50)

Values are clamped to the ``DEBUG``..``CRITICAL`` range.
"""

verbose: Annotated[
UseCounterAction[int],
conf.arg(aliases=["-v"], help="Increase log verbosity."),
_verbosity_mutex,
] = 0
quiet: Annotated[
UseCounterAction[int],
conf.arg(aliases=["-q"], help="Decrease log verbosity."),
_verbosity_mutex,
] = 0

def log_level(self, *, default: int = logging.WARNING) -> int:
"""Compute the effective logging level, clamped to ``DEBUG``..``CRITICAL``.

Formula: ``default + (quiet - verbose) * 10``.

Args:
default: Baseline logging level. Defaults to ``logging.WARNING``.

Returns:
An integer logging level suitable for :func:`logging.basicConfig`.
"""
level = default + (self.quiet - self.verbose) * 10
return max(logging.DEBUG, min(logging.CRITICAL, level))
Loading
Loading