Skip to main content

pyclig

A Python Click wrapper with Command Line Interface Guidelines (CLIG) paradigms.

click owns parsing, help, shell completion & the test runner. pyclig adds the conventions on top: STDOUT for output & STDERR for messages, exit codes, error rendering, logging, output primitives & standard flags. Options, arguments & types are plain click.

Usage

import click
import pyclig

from dkais import exceptions


@pyclig.group(
    "dkais",
    short_help="DMA AIS data manager",
    examples=["dkais list --start 2026-10"],
    errors=(exceptions.Error,),
    epilog="For more information see https://github.com/andyrids/dkais.",
)
def cli() -> None:
    """Download DMA AIS archives and convert them to Parquet."""


@cli.command("list")
@pyclig.options.json_output()
@click.option("--start", metavar="DATE")
@pyclig.pass_context
def list_(ctx: pyclig.Context, start: str | None, json_output: bool) -> int:
    """List bucket archives & local state."""
    ctx.out.print(f"from {start}")
    return pyclig.ExitCode.OK


if __name__ == "__main__":
    cli()

Point the entry point at the group: [project.scripts] dkais = "dkais.__main__:cli". Always name the group - the name is the program name, also under python -m, and sets the shell completion variable (_DKAIS_COMPLETE).

What every group gets

Feature Behaviour
-h, --help Usage, description, options, commands, examples & epilog
Bare prog Concise help on STDOUT, exit 0 - --help without options & epilog
-v, --version prog version from the distribution metadata, else 0.0.0+unknown
-d, --debug On every command, before or after its name; DEBUG logging as soon as parsed
Subcommands @cli.command() & @cli.group() create pyclig.Command & pyclig.Group
Arguments Reach commands as typed - globs & variables are not expanded on Windows either
Streams UTF-8 STDOUT & STDERR, so ✓ or → never fail on a cp1252 pipe

ctx.obj is left to the application.

Errors & exit codes

Commands return an exit code (or None for 0). Anything else is mapped by the runner, with messages on STDERR:

Outcome STDERR Exit
return code - code
click.UsageError, ctx.fail() error:, the failing command's usage, a --help hint 2
Other click.ClickException error: message its code
Ctrl-C, in a command or at a prompt warning: interrupted 130
Declined click.confirm(abort=True) error: aborted 1
A type in errors= error: message, the traceback first with -d 1
Broken STDOUT pipe (prog | head -1) - 1
Any other exception trimmed traceback, error:, bug report hint 70

pyclig.ExitCode names the codes: OK, FAILURE, USAGE, INTERNAL & INTERRUPT. The bug report hint names the distribution's Issues project URL. Messages are never parsed as Rich markup: user input such as [/b] or :warning: prints as typed.

A command may catch Ctrl-C or its own errors to print a tailored message first - a returned code passes through untouched. Wrap third-party failures (e.g. network errors) in an errors= type, so users see a message instead of a crash.

Output

ctx.out (STDOUT) & ctx.err (STDERR) are Rich consoles created for each run, with markup, emoji codes & highlighting off. Pass ctx.err to any Progress or Status. Each console follows its own stream: FORCE_COLOR, TTY_COMPATIBLE & NO_COLOR are honoured, and piped output on Windows keeps its colour when forced.

Primitive On a terminal Otherwise
notice(console, level=..., message=...) Bold red / yellow level:, wrapped One line
info(console, message=...) Dim Plain
hanging(console, head="✓", message=...) Wrapped under the message Same

Arguments after the console are keyword-only. Control characters, including terminal escapes, are dropped from messages. Print machine-readable output - JSON, tab-separated rows - with click.echo, which neither wraps nor expands tabs. Use rich.text.Text with a style for styled output, and TextColumn(markup=False) in a Rich Progress.

Standard flags

pyclig.options holds the flags with standard names, each taking a help= override:

Factory Flag Parameter
json_output() --json json_output
select_all() -a, --all select_all
force() -f, --force force

Logging

Log through the package logger - logging.getLogger(__name__) in any module of the package that defines the group. Libraries add only a NullHandler, in the package __init__.py:

logging.getLogger(__name__).addHandler(logging.NullHandler())

During a run, warnings & errors print as warning: / error: notices on ctx.err. With -d, a RichHandler adds DEBUG records, times ([2026-10-31T18:44:45+0000]) & call sites. The logger's handlers, level & propagation are restored when the run ends.

Testing

from pyclig.testing import invoke


def test_list_rows_when_piped() -> None:
    """Assert `list` prints tab-separated rows when STDOUT is a pipe."""
    result = invoke(cli, ["list"], out_tty=False, err_tty=True)
    assert result.exit_code == 0
    assert "\t" in result.stdout

invoke returns click's Result, with stdout & stderr apart. out_tty, err_tty, color & width set up each console; env sets (or, with None, unsets) variables. Colour & terminal variables from the developer's shell are cleared. Never patch Rich private attributes.

Parser-less entry points

pyclig.run() gives a pre-commit hook shim, which leaves sys.argv to the tool it wraps, the same exit codes, error rendering & logging:

def ruff_format() -> NoReturn:
    pyclig.run(
        lambda: core_ruff_format(sys.argv[1:]),
        errors=(exceptions.Error,),
    )

Migrating from argparse

  • Each subcommand becomes a @cli.command(); shared options become one decorator stacking click.options. Commands are listed in the order they are added.
  • click shows a command's docstring as its help: put \f after the help paragraph, so the Args: & Returns: sections stay out of --help. Pass help= for a description that is not an imperative docstring line.
  • Custom argument types become click.ParamType[T] subclasses calling self.fail().
  • parser.error() becomes raise click.UsageError(...) (or ctx.fail()).
  • A type= function checking a minimum becomes click.IntRange(min=1).
  • An environment variable default becomes envvar=, with show_envvar=True so a missing value names it.
  • Module-level consoles become ctx.out / ctx.err, passed to helpers as arguments.
  • argparse accepted unambiguous option prefixes (--st for --start); click does not.

Metadata

Release files for pyclig 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyclig 0.1.0
File Size Uploaded Provenance Actions
pyclig-0.1.0.tar.gz 21.9 kB Without provenance Details for pyclig-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyclig 0.1.0
File Interpreter ABI Platform Actions
pyclig-0.1.0-py3-none-any.whl Python 3 none any Details for pyclig-0.1.0-py3-none-any.whl

Total release size: 47.6 kB

Release files / pyclig-0.1.0.tar.gz

Download URL pyclig-0.1.0.tar.gz
Size 21.9 kB
Tags Source
SHA-256 checksum
How to use checksums
151bfa24e75ed7e35b7407765f62554976eb089e06946c30f99464ba155f85e0
BLAKE2b-256 checksum
How to use checksums
31184503466fcb5e3e05e864818a333ada562f28ee7cc181a6d9f164bbf8e233
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pyclig-0.1.0-py3-none-any.whl

Download URL pyclig-0.1.0-py3-none-any.whl
Size 25.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
30b55a5b701161636b46696286b7563ad171d1569cd6f23488d0d4345e859b16
BLAKE2b-256 checksum
How to use checksums
a08c19320b0e7bbdfecb350e708a6b6c86e1b7f9ba04ba4e4477f73af914159b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for pyclig 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page