Version

Click Extra provides its own version option which, compared to Click’s built-in:

Defaults

Here is how the defaults looks like:

import click
import click_extra

@click.command
@click_extra.version_option(fields={"version": "1.2.3"})
def cli():
    pass
$ cli --help
Usage: cli [OPTIONS]

Options:
  --version  Show the version and exit.
  --help     Show this message and exit.

The default version message is the same as Click’s default, but colored:

$ cli --version
cli, version 1.2.3

Hint

In the examples of this page the version is hard-coded to 1.2.3 for the sake of demonstration.

Without a hard-coded value, the version is fetched from the package metadata of the CLI or the __version__ attribute of the command.

Note

For drop-in compatibility with Click, the version can also be passed as the first positional argument: @version_option("1.2.3") is shorthand for @version_option(fields={"version": "1.2.3"}). A positional value starting with - is treated as a custom flag name instead, like every other option decorator.

Variables

The message template is a format string, which defaults to:

f"{prog_name}, version {version}"

Caution

This is different from Click, which uses the %(prog)s, version %(version)s template.

Click is based on old-school printf-style formatting, which relies on variables of the %(variable)s form.

Click Extra uses modern format string syntax, with variables of the {variable} form, to provide a more flexible and powerful templating.

You can customize the message template with the following variables:

Variable

Description

{module}

The module object in which the command is implemented.

{module_name}

The __name__ of the module in which the command is implemented.

{module_file}

The full path of the file in which the command is implemented.

{module_version}

The string found in the local __version__ variable of the module.

{package_name}

The name of the package in which the CLI is distributed.

{package_version}

The version from the package metadata in which the CLI is distributed.

{author}

The package author(s) from the core metadata, or None if not declared.

{license}

The package license from the core metadata: SPDX License-Expression, classifier, or free-form License.

{exec_name}

User-friendly name of the executed CLI. Returns {module_name}, {package_name} or script’s filename, in this order.

{version}

Version of the CLI. Returns {module_version}, {package_version} or None, in this order. For .dev versions, automatically appends the Git commit hash.

{git_repo_path}

The full path to the Git repository root directory, or None if not in a Git repository.

{git_branch}

The current Git branch name, or None if not in a Git repository or Git is not available.

{git_long_hash}

The full Git commit hash of the current HEAD, or None if not in a Git repository or Git is not available.

{git_short_hash}

The short Git commit hash of the current HEAD, or None if not in a Git repository or Git is not available.

{git_date}

The commit date of the current HEAD in ISO format (YYYY-MM-DD HH:MM:SS +ZZZZ), or None if not in a Git repository or Git is not available.

{git_tag}

The Git tag pointing at HEAD, or None if HEAD is not at a tagged commit.

{git_tag_sha}

The full commit SHA that the current tag points at, or None if HEAD is not at a tagged commit.

{git_distance}

The number of commits since the most recent tag, or None if no tag is reachable or Git is not available.

{git_dirty}

The work-tree state: dirty for uncommitted changes, clean otherwise, or None if not in a Git repository or Git is not available.

{build_time}

When the distribution was built, as an RFC 3339 UTC timestamp, or None if never pre-baked.

{build_os}

The operating system the build ran on, or None if never pre-baked.

{build_target}

The platform a distribution built there installs on (macosx-15.0-arm64, linux-x86_64, win-amd64), or None if never pre-baked.

{build_target_arch}

The CPU architecture the build ran on, or None if never pre-baked.

{prog_name}

The display name of the program. Defaults to Click’s info_name, but can be overridden via prog_name on the command decorator.

Note

The git_* variables are evaluated at runtime by calling git. They return None in environments where Git is not available (like standalone Nuitka binaries or Docker containers without Git).

All git_* fields can be pre-baked at build time by defining __<field>__ dunder variables in the CLI module, or in the __init__.py of its package. Pre-baked values take priority over subprocess calls.

The hash, date, branch, tag and distance fields also fall back to a .git_archival.json file, so they keep working when a CLI runs from a git archive export (like a GitHub source tarball) that has no .git directory. The two hash fields then fall back to the commit recorded for a CLI installed from a Git URL.

Hint

The {version} variable is resolved in this order:

  1. A __version__ variable defined alongside your CLI (see standalone scripts).

  2. A __version__ variable in the parent package’s __init__.py, for a CLI that sits in a sibling module (like a __main__ entry point, or the CLI of a compiled binary).

  3. The version from package metadata via importlib.metadata: this is the most common source for installed packages.

  4. None if none of the above succeeds (like unpackaged scripts without __version__).

Both {exec_name} and {version} are derived through a short fallback chain ({version} additionally appends the Git short hash for .dev builds):

        flowchart TD
    subgraph EXEC["{exec_name}"]
        direction TB
        x1["{module_name}"] -->|"a __main__ entry point"| x2["{package_name}"]
        x2 -->|"not packaged"| x3["script filename"]
    end
    subgraph VER["{version}"]
        direction TB
        m["{module_version}"] -->|unset| p["{package_version}"]
        p -->|unset| nil["None"]
    end
    VER -.->|"if .dev without +local, and git available"| gh["append the Git short hash<br/>for example 1.2.3.dev0+abc1234"]
    click EXEC "#click_extra.version.VersionOption.exec_name" "exec_name property"
    click x1 "#click_extra.version.VersionOption.module_name" "module_name property"
    click x2 "#click_extra.version.VersionOption.package_name" "package_name property"
    click VER "#click_extra.version.VersionOption.version" "version property"
    click m "#click_extra.version.VersionOption.module_version" "module_version property"
    click p "#click_extra.version.VersionOption.package_version" "package_version property"
    

Error

Some Click’s built-in variables are not recognized:

  • %(package)s should be replaced by {package_name}

  • %(prog)s should be replaced by {prog_name}

  • All other %(variable)s should be replaced by their {variable} counterpart

You can compose your own version string by passing the message argument:

import click
import click_extra

@click.command
@click_extra.version_option(
    message="✨ {prog_name} v{version} - {package_name}",
    fields={"version": "1.2.3"},
)
def my_own_cli():
    pass
$ my-own-cli --version
✨ my-own-cli v1.2.3 - click_extra.sphinx

Caution

This result reports the package name as click_extra.sphinx because the example runs in the click-extra documentation build environment. This is a quirk of the documentation setup and does not affect your own CLI.

Overriding variables from the command

The version_fields parameter on @command and @group lets you override any template field without touching the default params list.

Fields can also be forced directly on the VersionOption instance via the params= argument:

import click
from click_extra import VersionOption

@click.command(params=[
    VersionOption(
        message="{prog_name} {version} (branch: {git_branch})",
        fields={
            "prog_name": "Acme CLI",
            "version": "42.0",
            "git_branch": "release/42",
        },
    ),
])
def acme():
    pass
$ acme --version
Acme CLI 42.0 (branch: release/42)

Standalone script

The --version option works with standalone scripts.

Put this code in a file named greet.py:

greet.py
 1#!/usr/bin/env -S uv run --script
 2# /// script
 3# dependencies = ["click-extra"]
 4# ///
 5
 6import click_extra
 7
 8
 9@click_extra.command
10def greet():
11    print("Hello world")
12
13
14if __name__ == "__main__":
15    greet()

Here is the result of the --version option:

$ greet --version
greet, version None

Because the script is not packaged, the {version} variable is None.

But Click Extra recognize the __version__ variable, to force it in your script:

greet.py
 1#!/usr/bin/env -S uv run --script
 2# /// script
 3# dependencies = ["click-extra"]
 4# ///
 5
 6import click_extra
 7
 8
 9__version__ = "0.9.3-alpha"
10
11
12@click_extra.command
13def greet():
14    print("Hello world")
15
16
17if __name__ == "__main__":
18    greet()
$ greet --version
greet, version 0.9.3-alpha

Caution

The __version__ variable is not an enforced Python standard and more like a tradition.

It is supported by Click Extra as a convenience for script developers.

Development versions

When the version string contains .dev (as in PEP 440 development releases), Click Extra automatically appends the Git short commit hash as a PEP 440 local version identifier.

This lets you identify exactly which commit a development build was produced from:

import click
import click_extra

__version__ = "1.2.3.dev0"

@click.command
@click_extra.version_option()
def dev_cli():
    pass
$ dev-cli --version
dev-cli, version 1.2.3.dev0+bab2fd8

For example, a version like 1.2.3.dev0 becomes 1.2.3.dev0+6e59c8c1 during development. Release versions (without .dev) are never modified.

If Git is not available or the CLI is not running from a Git repository, the plain .dev version is returned as-is.

Pre-baked versions

If the version string already contains a + (a PEP 440 local version identifier), Click Extra assumes the hash was pre-baked at build time and returns the version as-is, without appending a second hash.

This is useful for CI pipelines or Nuitka binaries where git is not available at runtime but the build step can inject the commit hash into __version__ before compilation:

import click
import click_extra

__version__ = "1.2.3.dev0+abc1234"

@click.command
@click_extra.version_option()
def prebaked_cli():
    pass
$ prebaked-cli --version
prebaked-cli, version 1.2.3.dev0+abc1234

Hint

Click Extra ships prebake_version(), a utility to automate this injection. It parses a Python source file with ast, locates the __version__ assignment, and appends a +<local_version> suffix in place. Call it in your build step before Nuitka/PyInstaller compilation.

Version lifecycle

The version resolution adapts to the runtime environment:

Scenario

__version__ in source

Git available?

{version} output

Local dev (from source)

1.0.0.dev0

Yes

1.0.0.dev0+abc1234

Nuitka binary (pre-baked)

1.0.0.dev0+abc1234

No

1.0.0.dev0+abc1234

Nuitka binary (not pre-baked)

1.0.0.dev0

No

1.0.0.dev0

Release

1.0.0

N/A

1.0.0

For Nuitka binaries, the recommended workflow is to inject the commit hash into __version__ before compilation. The release workflow of Repomatic runs click-extra prebake all before compiling its binaries.

Pre-baking git metadata

All git_* template fields support pre-baking. If the CLI module, or the __init__.py of its package, defines a __<field>__ dunder variable with a non-empty string value, that value is used instead of calling git at runtime. This is the recommended approach for compiled binaries (Nuitka, PyInstaller) where git is unavailable.

The supported dunders are:

Dunder variable

Template field

Subprocess fallback

__git_branch__

{git_branch}

git rev-parse --abbrev-ref HEAD

__git_long_hash__

{git_long_hash}

git rev-parse HEAD

__git_short_hash__

{git_short_hash}

git rev-parse --short HEAD

__git_date__

{git_date}

git show -s --format=%ci HEAD

__git_tag__

{git_tag}

git describe --tags --exact-match HEAD

__git_tag_sha__

{git_tag_sha}

git rev-list -1 <tag> (if {git_tag} resolves)

__git_distance__

{git_distance}

git describe --tags --long (commit count parsed)

__git_dirty__

{git_dirty}

git status --porcelain (mapped to dirty/clean)

To pre-bake a value, declare the dunder with an empty string placeholder in your __init__.py:

mypackage/__init__.py
__version__ = "1.0.0.dev0"
__git_branch__ = ""
__git_short_hash__ = ""

Then inject values at build time using prebake_dunder():

from pathlib import Path
from click_extra.prebake import prebake_dunder

prebake_dunder(Path("mypackage/__init__.py"), "__git_branch__", "main")
prebake_dunder(Path("mypackage/__init__.py"), "__git_short_hash__", "abc1234")

prebake_dunder() only replaces empty strings, so running it twice is safe (idempotent). It preserves the quoting style and surrounding file content.

discover_package_init_files() can auto-discover __init__.py paths from the console scripts a project declares, in [project.scripts] of pyproject.toml or [options.entry_points] of setup.cfg, whether the package sits at the project root or under src/.

Pre-baking build metadata

The build_* template fields answer a question the git_* ones cannot: which machine produced this binary, and when. A cross-built executable carries no trace of its build host, and env_summary() reports the machine running it, so the two describe different things and a bug report often needs both.

They are pre-baked exactly like the git fields, through the same empty-string dunders:

Dunder variable

Template field

Value baked at build time

__build_time__

{build_time}

The build instant, as an RFC 3339 UTC timestamp

__build_os__

{build_os}

The operating system the build ran on

__build_target__

{build_target}

The wheel platform tag (macosx-15.0-arm64, win-amd64)

__build_target_arch__

{build_target_arch}

The CPU architecture the build ran on

The column is titled differently from the git table on purpose: a git field falls back to a git subprocess when its dunder is empty, and a build field has no fallback at all. Nothing at runtime can recover the host that compiled a binary, so an unbaked build_* field stays None forever.

{build_target} is the one that states an ABI floor rather than a description. macosx-15.0-arm64 says the binary needs macOS 15, where {build_os} only says it was built on macOS.

Tip

__build_time__ reads SOURCE_DATE_EPOCH when the build sets it, so two runs of a reproducible build stamp the same instant instead of drifting apart. build_moment() is the function that resolves it, shared with the man page’s own date stamp.

CLI usage

The click-extra prebake command exposes these utilities from the command line, without writing Python:

$ # Bake __version__ and all git fields in one pass
$ click-extra prebake all

$ # Only inject Git hash into __version__
$ click-extra prebake version
$ click-extra prebake version --hash abc1234

$ # Set a specific field (double underscores added automatically)
$ click-extra prebake field git_tag_sha abc123def456...
$ click-extra prebake field git_branch main --module mypackage/__init__.py

All subcommands resolve the target file by precedence: an explicit --module, then the module key of the [tool.click-extra.prebake] configuration, then auto-discovery from the console scripts the project declares, in [project.scripts] of pyproject.toml or [options.entry_points] of setup.cfg. Pin the target once to drop --module from repeated build invocations:

[tool.click-extra.prebake]
module = "mypackage/__init__.py"

Git metadata in archives

The git_* variables normally shell out to git, so they go blank when a CLI runs from a tree that has no .git directory. The most common case is a source archive: the tar.gz GitHub generates for a tag, or any git archive export.

Git can bake the metadata into such archives at export time. Commit a .git_archival.json file holding git archive placeholders, and mark it for substitution in .gitattributes:

.git_archival.json
{
    "node": "$Format:%H$",
    "node-date": "$Format:%cI$",
    "describe-name": "$Format:%(describe:tags=true,match=*[0-9]*)$",
    "ref-names": "$Format:%D$"
}
.gitattributes
.git_archival.json  export-subst

When git archive packs the file (GitHub does this for its source tarballs), it replaces each $Format:…$ token with the real value. Click Extra reads the result and populates {git_long_hash}, {git_short_hash}, {git_date}, {git_branch}, {git_tag}, {git_tag_sha} and {git_distance} from it. {git_dirty} is not covered: an archive has no work tree, so its state is unknowable.

Note

This is the schema used by setuptools-scm and Dunamai, so a single committed .git_archival.json works with all three.

Important

Substitution happens only inside git archive output. In a normal checkout the file still holds the literal $Format:…$ placeholders, which Click Extra ignores in favor of live git calls. The resolution order for each field is: pre-baked dunder, then live git, then .git_archival.json, then direct_url.json.

Commit of an install from a Git URL

A CLI installed straight from a Git URL, like with uvx --from git+https://... or pip install git+https://..., runs from a wheel built out of a clone. That wheel holds no .git directory and no substituted .git_archival.json.

The installer records the commit it built in a direct_url.json file, beside the metadata of the distribution. Click Extra reads {git_long_hash} and {git_short_hash} from it, so the dev version of such an install still names its commit, as in 1.2.3.dev0+0123456. The other git_* fields stay empty: the record holds no date, and the revision it names can be a branch, a tag or a commit.

Colors

Each variable listed in the section above can be rendered in its own style. Pass a styles mapping to the version_option decorator to set the style of individual fields, keyed by field name:

  • styles={"version": Style(fg="green")} paints the {version} field green.

  • styles={"version": None} clears the field’s style, so it falls back to message_style.

The message_style parameter sets the style of the message literals (the text around the fields) and of any field that has no style of its own. It defaults to None (no color).

Fields not listed in styles keep the defaults below, taken from VersionOption.default_styles. Fields absent from this table have no style of their own and fall back to message_style:

Field

Default style

build_os

Style(fg="bright_black")

build_target

Style(fg="bright_black")

build_target_arch

Style(fg="bright_black")

build_time

Style(fg="bright_black")

exec_name

theme_slot("invoked_command")

git_branch

Style(fg="cyan")

git_date

Style(fg="bright_black")

git_distance

theme_slot("success")

git_dirty

Style(fg="red")

git_long_hash

Style(fg="yellow")

git_repo_path

Style(fg="bright_black")

git_short_hash

Style(fg="yellow")

git_tag

Style(fg="cyan")

git_tag_sha

Style(fg="yellow")

module_name

theme_slot("invoked_command")

module_version

theme_slot("success")

package_name

theme_slot("invoked_command")

package_version

theme_slot("success")

prog_name

theme_slot("invoked_command")

version

theme_slot("success")

theme_slot() reads the style off the active theme each time the message renders, so these fields follow --theme. Under the default dark theme, invoked_command is bright white bold and success is green.

The remaining fields (module, module_file, author, license) have no default style and fall back to message_style.

Here is an example:

import click
from click_extra import version_option, Style

@click.command
@version_option(
    message="{prog_name} v{version} 🔥 {package_name} ( ͡❛ ͜ʖ ͡❛)",
    message_style=Style(fg="cyan"),
    styles={
        "prog_name": Style(fg="green", bold=True),
        "version": Style(fg="bright_yellow", bg="red"),
        "package_name": Style(fg="bright_blue", italic=True),
    },
    fields={"version": "1.2.3"},
)
def cli():
    pass
$ cli --version
cli v1.2.3 🔥 click_extra.sphinx ( ͡❛ ͜ʖ ͡❛)

You can pass None as a field’s style to disable styling for the corresponding variable, and set message_style=None to strip the style of the message literals:

import click
from click_extra import version_option

@click.command
@version_option(
    message_style=None,
    styles={"version": None, "prog_name": None},
    fields={"version": "1.2.3"},
)
def cli():
    pass
$ cli --version
cli, version 1.2.3

Environment information

env_summary() names the interpreter and the platform in one line, the two facts a bug report needs first. Add it to the message of a plain --version:

import click
from click_extra import version_option
from click_extra.version_facts import env_summary

@click.command
@version_option(
    message="{prog_name} {version} (" + env_summary() + ")",
    fields={"version": "1.2.3"},
)
def weather():
    pass
$ weather --version
weather 1.2.3 (Python 3.14.4, Ubuntu x86-64 (AMD64))

The message is a plain string, built when the decorator runs, so any other fact joins it the same way: from the standard library’s platform module, or from anywhere else. The version screen seats the same two facts beside a logo.

Todo

Regroup the bug report helpers of kdeldycke/mail-deduplicate and kdeldycke/meta-package-manager in Click Extra, so every CLI built on it invites a report the same way:

Each project then keeps only the content of its own reports: the mail fingerprint, the override table.

Version screen

--version can draw a screen instead of a line: a logo, with the facts worth pasting into a bug report seated beside it. Click Extra’s own CLI does it, and the machinery is yours to point at any artwork.

VersionScreen owns the layout and nothing else. The mark arrives already rendered, as a string or the lines of one, so it can be ASCII line art, half-blocks, or whatever your renderer produces. Pass a callable returning either, and it runs when --version draws the screen:

from functools import partial

from click_extra import group
from click_extra.commands import default_params
from click_extra.version import VersionScreen
from click_extra.version_facts import default_facts

SCREEN = VersionScreen(
    logo=" __ \n/\\_\\\n\\/_/",
    tagline="A CLI of my own",
    facts=lambda: default_facts() | {"Docs": "https://example.com"},
)

@group(params=partial(default_params, screen=SCREEN), version_fields={"version": "1.2.3"})
def my_cli():
    pass
$ my-cli --version

 __    my-cli, version 1.2.3
/\_\   A CLI of my own
\/_/
       Python     3.14.4
       Platform   Ubuntu x86-64 (AMD64)
       Docs       https://example.com

Three things are worth knowing about that snippet.

The screen measures the logo; you never declare its size. Lines of unequal width are padded out to the widest, so a renderer that trims its own trailing blanks still lays out square. Repairing that afterwards is not something a caller can do anyway: str.ljust counts the escape sequences it cannot see, so on a styled line it does nothing at all.

Facts are an ordered mapping, so one row can be changed without restating the rest. dict keeps insertion order, and replacing a key leaves it where it sat:

default_facts() | {"Platform": my_own_label}   # replaces, in place
default_facts() | {"Docs": DOCS_URL}           # appends, at the end

default_facts() carries the interpreter and the platform. dependency_versions() is there for the Click and Cloup releases underneath you, opt-in rather than default. Labels get a column sized to the longest of them, separated from their values by the same gutter that separates the whole block from the logo.

Passing facts or logo a callable defers it to render time. Values that cost something to compute should be charged to the invocation that asks for --version, not to every invocation that might have: a plugin count, a registry probe, or a mark painted from its geometry.

When the screen is skipped

Three conditions gate it, and failing any one falls back to the plain message template unchanged. That is a deliberate guarantee rather than a default: the plain form is the one every machine reader parses.

  • Color reaches the output. A flat-shaded mark carries its shape in the difference between its faces, and collapses into a single silhouette once the escapes are stripped. Color is also the one lever a caller already has: a redirected --version, or one run under --no-color or NO_COLOR, is asking for something parseable.

  • The terminal is wide enough to seat the facts beside the mark without wrapping them.

  • Accessible mode is off. A mark read out character by character is noise to a screen reader, so --accessible keeps the plain message.

Click Extra’s own screen

BRAND_SCREEN is the worked example: the six cubes of docs/assets/logo-square.svg, flat-shaded and painted with half blocks, two sub-pixels to a line. Its logo is the render_logo() callable, so the mark is painted when --version asks for it, not when the CLI starts.

$ click-extra --version

         ▄▄██▄▄
        █▀▀██▀▀█           Click Extra, version 10.3.0.dev0+bab2fd8
        ████████           Drop-in replacement for Click and Cloup
     ▄▄██▀▀██▀▀██▄▄
    █▀▀██▀▀██▀▀██▀▀█       Python     3.14.4
    ████████████████       Platform   Ubuntu x86-64 (AMD64)
 ▄▄██▀▀██▀▀██▀▀██▀▀██▄▄    Built on   Click 8.5.1, Cloup 4.0.0
█▀▀██▀▀██▀▀██▀▀██▀▀██▀▀█   Docs       https://kdeldycke.github.io/click-extra
████████████████████████
 ▀▀██▀▀  ▀▀██▀▀  ▀▀██▀▀

Hint

Two choices make the mark hold up where a copy of the artwork would not.

Its faces are flat and unoutlined, which is what lets one palette serve a light terminal and a dark one. An outlined mark carries its shape in the outline, so every stroke has to out-contrast the background, and the artwork’s own palette spans too wide a range for that: six of its colors vanish on white. A flat mark carries its shape in the difference between its three planes, which no background touches, leaving only the silhouette to keep its distance.

It is drawn in 2:1 dimetric rather than the artwork’s 30° isometric. A 30° edge advances 1.732 sub-pixels per row, which no grid can hold, so it comes out as a stair of alternating treads that reads as fraying at every size. Two across for every one down tiles a square grid exactly.

Debug logs

When the DEBUG level is enabled, all available variables will be printed in the log:

import click
from click_extra import version_option, verbosity_option, echo

@click.command
@version_option(fields={"version": "1.2.3"})
@verbosity_option
def version_in_logs():
    echo("Standard operation")

Which is great to see how each variable is populated and styled:

$ version-in-logs --verbosity DEBUG
debug: Set <Logger click_extra (DEBUG)> to DEBUG.
debug: Set <RootLogger root (DEBUG)> to DEBUG.
debug: 'click_extra.sphinx' package not found or not installed.
debug: Version string template variables:
debug: {module}           : <module 'click_extra.sphinx.click_runner' from '/home/runner/work/click-extra/click-extra/click_extra/sphinx/click_runner.py'>
debug: {module_name}      : click_extra.sphinx.click_runner
debug: {module_file}      : /home/runner/work/click-extra/click-extra/click_extra/sphinx/click_runner.py
debug: {module_version}   : 1.2.3.dev0+abc1234
debug: {package_name}     : click_extra.sphinx
debug: 'click_extra.sphinx' package not found or not installed.
debug: {package_version}  : None
debug: {author}           : None
debug: {license}          : None
debug: {exec_name}        : click_extra.sphinx.click_runner
debug: {version}          : 1.2.3
debug: {git_repo_path}    : /home/runner/work/click-extra/click-extra
debug: {git_branch}       : main
debug: {git_long_hash}    : bab2fd889e08b059ae947ef9b5aaeb3fdf82c410
debug: {git_short_hash}   : bab2fd8
debug: {git_date}         : 2026-11-13 20:18:21 +0400
debug: {git_tag}          : None
debug: {git_tag_sha}      : None
debug: {git_distance}     : None
debug: {git_dirty}        : clean
debug: {build_time}       : None
debug: {build_os}         : None
debug: {build_target}     : None
debug: {build_target_arch}: None
debug: {prog_name}        : version-in-logs
Standard operation
debug: Reset <RootLogger root (DEBUG)> to WARNING.
debug: Reset <Logger click_extra (DEBUG)> to WARNING.

Get metadata values

You can get the uncolored, Python values used in the composition of the version message from the context:

import click
from click_extra import echo, pass_context, version_option

@click.command
@version_option(fields={"version": "1.2.3"})
@pass_context
def version_metadata(ctx):
    version = ctx.meta["click_extra.version"]
    package_name = ctx.meta["click_extra.package_name"]
    prog_name = ctx.meta["click_extra.prog_name"]

    echo(f"version = {version}")
    echo(f"package_name = {package_name}")
    echo(f"prog_name = {prog_name}")
$ version-metadata --version
version-metadata, version 1.2.3
$ version-metadata
version = 1.2.3
package_name = click_extra.sphinx
prog_name = version-metadata

Hint

These variables are presented in their original Python type: most are strings, module is the module object itself, and a field with nothing to report is None.

Note

Metadata values in ctx.meta are lazily evaluated: a field like git_long_hash is only computed the first time you access it. If your command only reads ctx.meta["click_extra.version"], the Git subprocess calls are never executed.

Template rendering

You can render the version string manually by calling the option’s internal methods:

import click
from click_extra import echo, pass_context, version_option, VersionOption, search_params

@click.command
@version_option(fields={"version": "1.2.3"})
@pass_context
def template_rendering(ctx):
    # Search for a ``--version`` parameter.
    version_opt = search_params(ctx.command.params, VersionOption)
    version_string = version_opt.render_message()
    echo(f"Version string ~> {version_string}")

Hint

To fetch the --version parameter defined on the command, we rely on click_extra.search_params.

$ template-rendering --version
template-rendering, version 1.2.3
$ template-rendering
Version string ~> template-rendering, version 1.2.3

That way you can collect the rendered version_string, as if it was printed to the terminal by a call to --version, and use it in your own way.

Other internal methods to build-up and render the version string are available in the API below.

click_extra.version API

        classDiagram
  ExtraOption <|-- VersionOption
    

Introspect CLI metadata at runtime and print a colored --version string.

VersionOption gathers the executed CLI’s metadata (module and package names, distribution version, author and license, environment profile, and the live Git state) and renders them through a customizable, colorized message template.

Git fields (git_branch, git_short_hash, …) are resolved at runtime by shelling out to git, with three fallbacks for git-less environments: a pre-baked __<field>__ dunder in the CLI module or its package (injected before build by click_extra.prebake), then a committed .git_archival.json populated by git archive, then the commit an installer records for a distribution installed from a Git URL. The resolvers behind each field live in click_extra.version_facts.

click_extra.version.MUTED = Style(fg='bright_black')

The recessive style of what a version message says beside its name and version.

The tagline and the fact labels of a VersionScreen take it, and so do the template fields that locate a build rather than identify it: the repository path, the commit date, the build facts and the environment profile.

click_extra.version.is_main_module(module_name)[source]

Is module_name a __main__ entry point, of a package or of a script?

An entry point is where the interpreter started: python -m package, a console script, or a compiled binary. It is never plumbing a stack walk lands on after running out of user frames, so it is exempt from CLI_ECOSYSTEM_PACKAGES even when it sits under one of those packages, as click_extra.__main__ does in Click Extra’s own binary.

Return type:

bool

click_extra.version.theme_slot(slot)[source]

A style reading its palette slot off the active theme, on every call.

The lookup happens at call time, so the message follows --theme, CLICK_EXTRA_THEME and the background-sniffing auto alike, and drops its color under the monochrome manpage palette. A style captured at import would paint every theme with one palette: the bright white program name of dark is white on white on a light terminal.

get_current_theme() answers with the colorless theme outside an invocation, and with a full palette inside one, so no slot can come back missing.

Return type:

Callable[[str], str]

click_extra.version.unstyled(text)[source]

Identity style, for a segment left with no color of its own.

Return type:

str

class click_extra.version.VersionScreen(logo, tagline='', facts=<function default_facts>, gutter='   ')[source]

Bases: object

A logo, and the facts to seat beside it, as --version should draw them.

Owns the layout only. The artwork arrives already rendered, as a string or the lines of one, so a CLI is free to draw its mark however it likes, in ASCII line art, half-blocks or anything else, without this class knowing. Hand one to VersionOption through its screen argument, or to a whole CLI through default_params(screen=…).

logo: str | Sequence[str] | Callable[[], str | Sequence[str]]

The mark, pre-rendered, or a callable producing it. A string splits on newlines.

A callable defers the rendering to the first --version that draws the screen, as facts does for its values: a mark painted by code, from an image say, then costs nothing to the invocations that never ask for it.

tagline: str = ''

One line under the program name. Omitted, with its blank line, when empty.

facts()

Label-to-value rows under the tagline, or a callable producing them.

A callable defers the work to render time, which matters when a value costs something to compute: a CLI counting plugins should not pay for that on every invocation just to have the number ready in case --version is asked for.

Return type:

dict[str, str]

gutter: str = '   '

Blank columns between the mark and the facts, and between label and value.

property lines: tuple[str, ...]

The mark’s lines, every one padded out to width.

Padding here rather than asking for it is what lets a caller hand over whatever its renderer produced. Trailing blanks are invisible on a line by itself and ragged the moment anything is placed beside it, and a caller cannot repair that afterwards: pad_to() is what measures a styled line right and closes the style it left open.

property width: int

Columns the mark occupies, taken from its widest line.

rows(prog_name, version, styles)[source]

The column of facts, as (plain, styled) pairs.

Both forms are built together because the styled one cannot be measured: its escape sequences take columns that never reach the screen, and the plain twin is what render() sizes the layout against.

The program name and version take the same styles the plain message gives them, so the two renderings of --version cannot drift apart on color.

Return type:

tuple[tuple[str, str], ...]

render(prog_name, version, styles)[source]

Compose the mark and the facts into the screen, or decline to.

The facts are centred against the mark’s height, and either column may be the taller of the two: a line missing from one side simply renders blank.

Returns None when the terminal is too narrow to seat the two columns side by side, leaving the caller to fall back rather than emit a wrapped mess. The threshold is measured off the facts actually built, since their widest row grows with whatever a CLI chose to report. A non-interactive stream reports shutil’s 80-column default, wide enough that a redirected-but-forced-color run still gets the screen it asked for.

Return type:

str | None

click_extra.version.colors_reach_output()[source]

Will ANSI codes survive all the way to the user’s terminal?

Resolves Click Extra’s color tri-state, deferring to the output stream’s TTY status on its auto default, exactly as click.echo does when it decides whether to strip the codes itself.

Note

The stream probed is sys.stdout, not Click’s own resolution of it. click.echo reaches stdout through a private cached wrapper, which fixes the output encoding and delegates isatty() to the stream beneath it, so both answer alike. Click deprecates the public accessor of that wrapper, click.get_text_stream().

Return type:

bool

click_extra.version.template_segments(template)[source]

Split a version message template into its literal and field segments.

Yields one (literal, field_id, field) triple per field of template, in order: the literal text before the field, the identifier the field resolves on the option (module for {module.__name__}, the part before any index or attribute), and the field as written, braces and format spec included. A doubled brace of a literal comes back single. Literal text left after the last field comes in a final triple carrying no field.

Return type:

Iterator[tuple[str, str | None, str | None]]

class click_extra.version.VersionOption(param_decls=None, *, message=None, fields=None, styles=None, message_style=None, screen=None, is_flag=True, expose_value=False, is_eager=True, help='Show the version and exit.', **kwargs)[source]

Bases: ExtraOption

Gathers CLI metadata and prints a colored version string.

Note

This is not a drop-in replacement for the standard @click.version_option() decorator.

It addresses the following Click issues:

  • click#2324, to allow its use with the declarative params= argument.

  • click#2331, by distinguishing the module from the package.

  • click#1756, by allowing path and Python version.

Preconfigured as a --version option flag.

Parameters:
  • message (str | None) – the message template to print, in format string syntax. Defaults to {prog_name}, version {version}.

  • fields (Mapping[str, Any] | None) – mapping of template field name to a forced value, overriding the value auto-computed for that field. Keys must be members of template_fields (for example {"version": "1.2.3"}).

  • styles (Mapping[str, Callable[[str], str] | None] | None) – mapping of template field name to its Style, merged over default_styles. Pass None as a value to clear a field’s default style. Keys must be members of template_fields.

  • message_style (Callable[[str], str] | None) – fallback style for the message literals and for any field that has no style of its own.

  • screen (VersionScreen | None) – a VersionScreen to draw instead of the one-line message, whenever the terminal can take it. Left unset, --version prints the one-line message.

Raises:

TypeError – when a key of fields or styles names no template field.

template_fields: tuple[str, ...] = ('module', 'module_name', 'module_file', 'module_version', 'package_name', 'package_version', 'author', 'license', 'exec_name', 'version', 'git_repo_path', 'git_branch', 'git_long_hash', 'git_short_hash', 'git_date', 'git_tag', 'git_tag_sha', 'git_distance', 'git_dirty', 'build_time', 'build_os', 'build_target', 'build_target_arch', 'prog_name')

List of field IDs recognized by the message template.

default_styles: ClassVar[dict[str, Callable[[str], str]]] = {'build_os': Style(fg='bright_black'), 'build_target': Style(fg='bright_black'), 'build_target_arch': Style(fg='bright_black'), 'build_time': Style(fg='bright_black'), 'exec_name': <function theme_slot.<locals>.apply>, 'git_branch': Style(fg='cyan'), 'git_date': Style(fg='bright_black'), 'git_dirty': Style(fg='red'), 'git_distance': <function theme_slot.<locals>.apply>, 'git_long_hash': Style(fg='yellow'), 'git_repo_path': Style(fg='bright_black'), 'git_short_hash': Style(fg='yellow'), 'git_tag': Style(fg='cyan'), 'git_tag_sha': Style(fg='yellow'), 'module_name': <function theme_slot.<locals>.apply>, 'module_version': <function theme_slot.<locals>.apply>, 'package_name': <function theme_slot.<locals>.apply>, 'package_version': <function theme_slot.<locals>.apply>, 'prog_name': <function theme_slot.<locals>.apply>, 'version': <function theme_slot.<locals>.apply>}

Default style for each template field.

Fields absent from this mapping render with no style of their own and fall back to message_style (or no color when that is unset). User-provided styles are merged over these defaults.

The name and version fields read their style off the active palette through theme_slot() rather than naming a color, so the message follows --theme. Under the default dark palette, invoked_command is bright white bold and success is green.

message: str = '{prog_name}, version {version}'

Default message template used to render the version string.

styles: dict[str, Callable[[str], str] | None]
pin_field(field_id, value)[source]

Pin a template field to a caller-supplied value.

The value shadows the cached_property that would otherwise resolve the field, and is recorded so reset_resolution() can tell the two apart. Every caller that pins a field goes through here: a bare setattr sets the value without the record, and the next reset then discards it.

Parameters:
  • field_id (str) – Name of the template field to pin.

  • value (Any) – Value the field reports from now on.

Raises:

TypeError – when field_id names no template field.

Return type:

None

reset_resolution()[source]

Drop every memoized field, so the next read resolves from scratch.

The resolution chain starts on a stack walk, and the option is built once at decoration time, so the first read in a process fixes the answer for every later one. That is what a CLI wants: one invocation, one process, one walk.

A documentation build breaks the assumption. It renders many commands in one process, and a render that carries no CLI frame (writing roff for a command tree, say) resolves the chain from a stack the walk cannot read. The wrong answer then stands for the rest of the build, and a later --version example publishes it. Clearing the memo hands the next reader its own walk, at the cost of repeating one.

A field the caller pinned through fields= is not a resolution and survives: see _field_overrides.

Return type:

None

static cli_frame()[source]

Returns the frame in which the CLI is implemented.

Walks the execution stack from the innermost frame outwards, to find the package in which the user’s CLI is implemented. The walk reads the module and the function name of each frame off the frame itself, with no source lookup: --version is eager, so every CLI pays for this walk at startup.

Returns the frame itself. When every frame belongs to the Click ecosystem and none is an entry point, returns the outermost frame, which has no caller: module then defers to the module of the root command’s callback.

Return type:

FrameType

static command_module(ctx)[source]

Returns the module implementing the root command’s callback.

The stack walk in cli_frame() can only find the CLI when the CLI’s own module is on the stack. That holds for a normal invocation, and for a CliRunner driven from the test module that declares the command, but not when a runner invokes a command it merely imported: the walk then finds no user frame at all and stops on the runner’s own invoke(), inside the Click ecosystem.

The Sphinx click:run directive is that shape: without this, a documented --version would render the version of the ecosystem package owning the runner in place of the CLI’s own.

A launcher is the other shape. A console script or a compiled binary whose entry point is a command object, like orchard.cli:cli, leaves no frame of that module on the stack: the walk ends on the launcher itself.

A command’s callback names its defining module directly and needs no stack, so it settles the case the walk cannot see. Returns None when the command has no callback (a bare click.Group) or its module is not imported, leaving the walk’s own answer in place.

Return type:

ModuleType | None

property module: ModuleType[source]

Returns the module in which the CLI resides.

property module_name: str[source]

Returns the full module name or __main__.

property module_file: str | None[source]

Returns the module’s file full path.

None for a module with no file, like the __main__ of python -c.

property module_version: str | None[source]

Returns the string found in the local __version__ variable.

Hint

__version__ is a convention from early Python packaging. It is not a standard variable and is not defined in the packaging PEPs.

Prefer the package_version property below, which uses the standard library importlib.metadata API.

Click’s own version_option reads the package metadata only (pallets/click#2598). Click Extra reads __version__ too, for the scripts and codebases that set it.

property package_name: str | None[source]

Returns the package name.

property package_version: str | None[source]

Returns the package version if installed.

Resolved from the distribution name (see _distribution_name) via importlib.metadata.version(). Returns None if the package is not installed or cannot be resolved.

property author: str | None[source]

Returns the package author(s) from its core metadata.

See resolve_author() for the fields read, in order. None if no author can be determined.

property license: str | None[source]

Returns the package license from its core metadata.

See resolve_license() for the fields read, in order. None if no license can be determined.

property exec_name: str[source]

User-friendly name of the executed CLI.

Returns the module name. But if the module is a __main__ entry point, returns the package name: python -m runs it as __main__, while a console script or a compiled binary imports it as package.__main__.

If not packaged, the CLI is assumed to be a simple standalone script, and the returned name is the script’s file name (including its extension). Code with no file, like a CLI run by python -c, keeps the __main__ module name.

property version: str | None[source]

Return the version of the CLI.

Returns the module version if a __version__ variable is set alongside the CLI in its module.

Else returns the package version if the CLI is implemented in a package, using importlib.metadata.version().

For development versions (containing .dev), automatically appends the Git short hash as a PEP 440 local version identifier, producing versions like 1.2.3.dev0+abc1234. This helps identify the exact commit a dev build was produced from. When git_short_hash finds no commit, the plain dev version is returned.

Versions that already contain a + (a pre-baked local version identifier, typically set at build time by CI pipelines) are returned as-is to avoid producing invalid double-suffixed versions like 1.2.3.dev0+abc1234+xyz5678.

property git_repo_path: Path | None[source]

Find the Git repository root directory.

Walks up from _search_start to the first directory holding a .git entry: see find_git_root().

property git_branch: str | None[source]

Returns the current Git branch name.

Checks for a pre-baked __git_branch__ dunder first, then git rev-parse --abbrev-ref HEAD, then .git_archival.json.

property git_long_hash: str | None[source]

Returns the full Git commit hash.

Checks for a pre-baked __git_long_hash__ dunder first, then git rev-parse HEAD, then .git_archival.json, then the commit recorded for an install from a Git URL.

property git_short_hash: str | None[source]

Returns the short Git commit hash.

Checks for a pre-baked __git_short_hash__ dunder first, then git rev-parse --short HEAD, then .git_archival.json, then the commit recorded for an install from a Git URL. Both fallbacks derive it from the first 7 characters of the full hash.

Hint

The short hash is usually the first 7 characters of the full hash, but this is not guaranteed to be the case.

But it is at least guaranteed to be unique within the repository, and a minimum of 4 characters.

property git_date: str | None[source]

Returns the commit date in ISO format: YYYY-MM-DD HH:MM:SS +ZZZZ.

Checks for a pre-baked __git_date__ dunder first, then git show -s --format=%ci HEAD, then .git_archival.json (whose node-date is strict ISO 8601, like 2021-01-01T12:00:00+00:00).

property git_tag: str | None[source]

Returns the Git tag pointing at HEAD, if any.

Checks for a pre-baked __git_tag__ dunder first, then git describe --tags --exact-match HEAD, then .git_archival.json.

Returns None if HEAD is not at a tagged commit.

property git_tag_sha: str | None[source]

Returns the commit SHA that the current tag points at.

Checks for a pre-baked __git_tag_sha__ dunder first, then git rev-list -1 on the tag returned by git_tag, then .git_archival.json. Returns None if HEAD is not at a tag.

property git_distance: str | None[source]

Number of commits since the most recent tag, or None.

Checks for a pre-baked __git_distance__ dunder first, then parses git describe --tags --long, then falls back to .git_archival.json. None when no tag is reachable or Git is unavailable.

property git_dirty: str | None[source]

Work-tree state: "dirty", "clean" or None.

Checks for a pre-baked __git_dirty__ dunder first, then runs git status --porcelain. None when not in a Git repository or Git is unavailable. There is no .git_archival.json fallback: an archive has no work tree, so its state is unknowable.

property build_time: str | None[source]

When the distribution was built, as an RFC 3339 UTC timestamp.

Reads the pre-baked __build_time__ dunder, the only source of a build fact: see BUILD_RESOLVERS and resolve_build_time().

property build_os: str | None[source]

The operating system the build ran on.

Reads the pre-baked __build_os__ dunder. See resolve_build_os().

property build_target: str | None[source]

The platform a distribution built here installs on.

Reads the pre-baked __build_target__ dunder. See resolve_build_target().

property build_target_arch: str | None[source]

The CPU architecture the build ran on.

Reads the pre-baked __build_target_arch__ dunder. See resolve_build_target_arch().

property prog_name: str | None

Return the name of the CLI, from Click’s point of view.

Get the info_name of the root command.

Note

Unlike its siblings, this field is resolved on every access instead of being cached on the instance: it is the one template field whose value legitimately varies between invocations of the same option instance sharing a process. multicall dispatch relies on that, running one CLI under many names in sequence, and a prog_name passed to main() varies it without any multicall at all. A cached value would pin the first name seen forever.

field_style(field_id=None)[source]

Style painting the field_id segment of a rendered message.

A field carrying no style of its own falls back to message_style, and one left unset by the caller too renders bare. Call with no field_id for the style of the template’s literal segments, which is message_style alone.

Return type:

Callable[[str], str]

colored_template(template=None)[source]

Insert ANSI styles to a message template.

Accepts a custom template as parameter, otherwise uses the default message defined on the Option instance.

The template is linearized so the ANSI codes apply to its string segments: an ANSI directive cannot be nested within another, unlike a markup tag of HTML.

Return type:

str

render_message(template=None)[source]

Render the version string from the provided template.

Accepts a custom template as parameter, otherwise uses the default self.colored_template() produced by the instance.

A CLI carrying a VersionScreen gets that drawn instead, whenever three conditions hold. Failing any one of them falls back to the plain template unchanged, which is a deliberate guarantee rather than a default: that form is the one every machine reader parses.

  • Color reaches the output. A flat-shaded mark carries its shape in its colors, and color is the one lever a caller already has. A redirected --version, or one run under --no-color or NO_COLOR, is asking for something parseable.

  • The terminal is wide enough to seat the facts beside the mark without wrapping them.

  • Accessible mode is off. A mark read out character by character is noise to a screen reader, so --accessible keeps the plain message.

Return type:

str

print_debug_message()[source]

Render in debug logs all template fields in color.

Return type:

None

print_and_exit(ctx, param, value)[source]

Print the version string and exits.

Also stores all version string elements in the Context’s meta dict.

Return type:

None

click_extra.version.reset_version_resolution(command)[source]

Reset every VersionOption in a command tree.

Walks the tree once, cycle-safe, and calls VersionOption.reset_resolution() on each option it finds.

Written for a documentation build, which renders many commands in one process where a CLI renders one. See that method for what a stale resolution costs a published page.

Note

The walk reads the subcommands each group has registered, rather than going through iter_subcommand_contexts() like the renderers do. A lazy subcommand not imported yet holds no resolution to reset, and the context that function builds for each subcommand runs its --version callback, which resolves the very chain this function clears.

Return type:

None

click_extra.version_facts API

Resolvers for the facts a --version message reports.

Each function answers one fact on its own, with no option or command around it: the live Git state (GIT_RESOLVERS, run_git()), the build host (BUILD_RESOLVERS), the .git_archival.json and direct_url.json fallbacks, the core metadata of a distribution (resolve_author(), resolve_license()), and the interpreter and platform (default_facts(), env_summary()). VersionOption chains them into its template fields, and click-extra prebake all calls them to bake the values into a build.

click_extra.version_facts.distribution_of(package_name)[source]

Resolve an import package name to the installed distribution providing it.

An import (top-level module) name may differ from the distribution name (PIL vs Pillow, jwt vs PyJWT). A name already matching an installed distribution is returned as-is; otherwise it is resolved through importlib.metadata.packages_distributions(). Ambiguous mappings (one import name to several distributions) return None: pass package_name explicitly to disambiguate.

Returns None when nothing installed provides the name, which is what makes it usable as a test of whether a module belongs to a real distribution.

Return type:

str | None

click_extra.version_facts.GIT_FIELDS: dict[str, tuple[str, ...]] = {'git_branch': ('rev-parse', '--abbrev-ref', 'HEAD'), 'git_date': ('show', '-s', '--format=%ci', 'HEAD'), 'git_long_hash': ('rev-parse', 'HEAD'), 'git_short_hash': ('rev-parse', '--short', 'HEAD'), 'git_tag': ('describe', '--tags', '--exact-match', 'HEAD')}

Git fields whose live value is the stripped output of one static git subcommand, mapped to that subcommand’s args.

git_tag_sha, git_distance and git_dirty are excluded: their resolution is not a single static git invocation whose stripped output is the value. git_tag_sha dereferences the tag (git rev-list -1 <tag>), git_distance parses git describe and git_dirty maps the porcelain status to a label. See resolve_git_tag_sha(), resolve_git_distance() and resolve_git_dirty().

For the resolver of every pre-bakeable git field (these five plus the three computed ones), keyed uniformly by field ID, see GIT_RESOLVERS.

click_extra.version_facts.GIT_TIMEOUT: float = 5

Seconds run_git() lets a git command run before it gives up on it.

A local read answers in milliseconds. The bound is for a repository that blocks, like one on a network share that stopped answering.

click_extra.version_facts.run_git(*args, cwd=None, allow_empty=False, check=False, timeout=5)[source]

Run a git command and return its output, without the final newline.

A command that fails returns None: no git on the PATH, a non-zero exit status, like the one of a cwd outside a repository, or a command still running after timeout seconds. Set check to raise instead: an OSError when git does not start, a subprocess.CalledProcessError on a non-zero exit status, and a subprocess.TimeoutExpired when the time runs out.

By default an empty output returns None too, like a failure. Set allow_empty to keep the empty string, which some commands use meaningfully: git status --porcelain prints nothing for a clean work tree, and that is distinct from the command failing.

The output decodes as UTF-8, and a byte that is no UTF-8 becomes a replacement character: Git prints a ref name as the bytes it stores, which a repository can hold in any encoding.

Parameters:

cwd (Path | None) – the folder to run in. The current working directory when None.

Return type:

str | None

click_extra.version_facts.resolve_git_dirty(cwd=None)[source]

Report the work-tree state as "dirty", "clean" or None.

Returns "dirty" when git status --porcelain reports uncommitted changes, "clean" when it reports none, and None when the state cannot be determined (not a Git repository, or git is unavailable).

The empty output of a clean work tree is meaningful here, so the command is run with allow_empty to tell it apart from a failure.

Return type:

str | None

click_extra.version_facts.resolve_git_distance(cwd=None)[source]

Count commits since the most recent tag, as a string, or None.

Parses git describe --tags --long, whose output has the form <tag>-<distance>-g<short_hash>. Returns None when no tag is reachable, the directory is not a Git repository, or git is unavailable.

Return type:

str | None

click_extra.version_facts.resolve_git_tag_sha(cwd=None)[source]

Resolve the commit SHA the tag at HEAD points at, or None.

Runs git describe --tags --exact-match HEAD to find the tag, then git rev-list -1 <tag> to dereference it to a commit SHA. Returns None when HEAD is not at a tagged commit, the directory is not a Git repository, or git is unavailable.

Return type:

str | None

click_extra.version_facts.GIT_RESOLVERS: dict[str, Callable[[Path | None], str | None]] = {'git_branch': <function _direct_git_resolver.<locals>.resolver>, 'git_date': <function _direct_git_resolver.<locals>.resolver>, 'git_dirty': <function resolve_git_dirty>, 'git_distance': <function resolve_git_distance>, 'git_long_hash': <function _direct_git_resolver.<locals>.resolver>, 'git_short_hash': <function _direct_git_resolver.<locals>.resolver>, 'git_tag': <function _direct_git_resolver.<locals>.resolver>, 'git_tag_sha': <function resolve_git_tag_sha>}

Canonical live resolver for every pre-bakeable git_* field.

Maps each field ID to a callable that takes an optional working directory and returns the field’s value by shelling out to git (or None when it cannot be resolved). This is the single source of truth for how each git field is computed live, shared by two consumers:

  • VersionOption’s runtime accessors, which wrap each resolver with the pre-baked-dunder and .git_archival.json fallbacks.

  • the click-extra prebake all command, which calls every resolver to bake values into source files at build time.

Keeping it here means adding a new git field is a one-line edit in this module, with no matching change needed in the CLI.

click_extra.version_facts.SOURCE_DATE_EPOCH = 'SOURCE_DATE_EPOCH'

Environment variable a reproducible build sets to pin every timestamp it writes.

See the reproducible-builds.org specification.

click_extra.version_facts.build_moment()[source]

The instant a build happens, in UTC.

Reads SOURCE_DATE_EPOCH when the build sets it, so two runs of a reproducible build stamp the same instant. Falls back to the current time.

Return type:

datetime

click_extra.version_facts.resolve_build_time()[source]

The moment the distribution is built, as an RFC 3339 UTC timestamp.

See build_moment() for where the instant comes from.

Return type:

str

click_extra.version_facts.resolve_build_os()[source]

The operating system the build runs on.

Return type:

str

click_extra.version_facts.resolve_build_target()[source]

The platform a distribution built here installs on.

This is the wheel platform tag (macosx-15.0-arm64, linux-x86_64, win-amd64), which is Python’s answer to the target triple shadow-rs bakes into a Rust binary. It states an ABI floor the two fields beside it cannot: macosx-15.0-arm64 says the binary needs macOS 15, where resolve_build_os() only says it was built on macOS.

Return type:

str

click_extra.version_facts.resolve_build_target_arch()[source]

The CPU architecture the build runs on.

Return type:

str

click_extra.version_facts.BUILD_RESOLVERS: dict[str, Callable[[], str]] = {'build_os': <function resolve_build_os>, 'build_target': <function resolve_build_target>, 'build_target_arch': <function resolve_build_target_arch>, 'build_time': <function resolve_build_time>}

Canonical resolver for every pre-bakeable build_* field.

These describe the machine and moment a distribution was built, which GIT_RESOLVERS and env_summary() both leave unanswered: git states what source went in, env_summary() states where the binary is running now, and neither one identifies the host that produced it. That gap is what a cross-built binary turns into a support question, and what shadow-rs answers for Rust with its BUILD_TIME, BUILD_OS and BUILD_TARGET constants.

A build fact has no live fallback, unlike a git one: nothing at runtime can recover the host a binary was compiled on, so click-extra prebake all is the only thing that ever writes these. A field left unbaked stays empty, and its VersionOption accessor answers None.

Adding a field here is a one-line edit, with no matching change in the CLI.

click_extra.version_facts.dunder_name(field_id)[source]

The module-level name a pre-baked template field is stored under.

git_branch is baked as __git_branch__: the one spelling VersionOption reads back, and click-extra prebake writes. Keeping the contract in one place is what lets the two sides stay agreed.

Return type:

str

click_extra.version_facts.find_git_root(start)[source]

Walk up from start to the first folder holding a .git entry.

The entry is a folder in a plain clone, and a file in a worktree or a submodule. Returns that folder, start included, or None.

Return type:

Path | None

click_extra.version_facts.find_archival_file(start)[source]

Walk up from start to find a .git_archival.json file.

Returns the first match in start or any of its parents, or None.

Return type:

Path | None

click_extra.version_facts.read_archival(path)[source]

Parse a .git_archival.json file into a string mapping.

Returns an empty mapping when the file is missing, unreadable, or not a valid JSON object.

Return type:

dict[str, str]

click_extra.version_facts.archival_field(data, field_id)[source]

Resolve a git_* field from parsed .git_archival.json data.

data follows the setuptools-scm archival schema: node (full hash), node-date, describe-name and ref-names. The same file is read by setuptools-scm and Dunamai, so a single committed .git_archival.json serves all three.

Returns None when the field is absent, empty, or still holds an unsubstituted $Format:…$ placeholder. That last case is what a plain checkout contains: git archive performs the substitution, so values are real only inside an exported archive (including GitHub’s source tarballs).

There is no entry for git_dirty: an archive has no work tree, so its state is unknowable.

Return type:

str | None

click_extra.version_facts.read_direct_url(distribution)[source]

Parse the direct_url.json record of an installed distribution.

PEP 610 has installers write this record beside the metadata of a distribution installed from a URL, like uvx --from git+https://... or pip install git+https://....

Returns an empty mapping when the distribution is not installed, carries no record, or holds a record that is not a JSON object.

Return type:

dict[str, Any]

click_extra.version_facts.direct_url_field(data, field_id)[source]

Resolve a git_* field from a parsed direct_url.json record.

Only an install from a Git URL records a commit: the full hash of the checkout the installer built, in vcs_info.commit_id. It answers git_long_hash, and its first 7 characters git_short_hash.

Every other field answers None. The vcs_info.requested_revision the record may also hold names a branch, a tag or a commit, with nothing to tell which.

Return type:

str | None

click_extra.version_facts.resolve_distribution(names)[source]

Return the first installed distribution among names, or None.

Probes each candidate name in order with importlib.metadata.distribution() and returns the first that resolves to an installed distribution. Used to pick a distribution from a set of plausible spellings (like the program name with - / _ variants) before reading its metadata.

Return type:

str | None

click_extra.version_facts.meta_value(meta, *keys)[source]

Return the first non-empty value among core-metadata keys.

Each key is tested with in before [] reads it: [] on a missing key returns None with a DeprecationWarning, and is to raise KeyError.

Return type:

str | None

click_extra.version_facts.resolve_author(meta)[source]

Return the author(s) from meta’s core metadata, or None.

Prefers the Author field, then the Maintainer field, then the display name parsed out of the Author-email / Maintainer-email fields (Name <email>). Returns None when meta is None or no author can be determined.

Return type:

str | None

click_extra.version_facts.resolve_license(meta)[source]

Return the license from meta’s core metadata, or None.

Prefers the SPDX License-Expression field (core metadata 2.4+). Falls back to the human-readable name of the first License :: trove classifier, then to the free-form License field (which may hold the full license text). Returns None when meta is None or no license can be determined.

Return type:

str | None

click_extra.version_facts.platform_label()[source]

Current platform and CPU architecture, as displayed to the user.

Return type:

str

click_extra.version_facts.env_summary()[source]

One-line interpreter and platform summary.

Reads Python 3.14.7, macOS ARM64 (AArch64) on an Apple Silicon Mac: the same two facts default_facts() puts on the version screen, joined for a CLI that would rather spend one line of its plain --version on them than draw a screen at all: @version_option(message="{prog_name} {version} (" + env_summary() + ")").

Return type:

str

click_extra.version_facts.dependency_versions()[source]

The Click and Cloup releases this install is sitting on.

Worth a row on a screen whose main job is to be pasted into a bug report: Click Extra subclasses both, so which of the three is at fault is the first question any such report raises. Not a default fact, since a CLI built on Click Extra may reasonably consider that its own business rather than its user’s.

Read from the installed distributions: Click deprecates its own __version__ attribute.

Return type:

str

click_extra.version_facts.default_facts()[source]

The facts every version screen carries, as an ordered label-to-value map.

A mapping rather than a sequence of pairs so a CLI can adjust one row without restating the rest, dict preserving insertion order and replacement keeping a key where it already sat:

default_facts() | {"Platform": my_own_label}   # replaces, in place
default_facts() | {"Docs": DOCS_URL}           # appends, at the end
Return type:

dict[str, str]

click_extra.logo API

Terminal rendition of the Click Extra brand mark.

The mark of docs/assets/logo-square.svg is six cubes stacked three-two-one. Here they are rebuilt as flat shaded solids, with one color per face and no outline anywhere, and painted with half blocks, two sub-pixels to a terminal line.

Flat faces are what let one palette serve a light terminal and a dark one. An outlined mark carries its shape in the outline, so every stroke has to out-contrast whatever sits behind it, and the artwork’s own palette spans too wide a range for that: six of its colors vanish on white, two more on black. A flat mark carries its shape in the difference between its three planes, which no background can touch, leaving only the silhouette to keep its distance. That fits comfortably in the middle of the range, which is where FACE_COLORS sits.

Caution

The mark’s structure is carried by color: strip the ANSI codes and it collapses into one silhouette. VersionScreen therefore only draws it where color reaches the output, falling back to the plain --version line everywhere else, which is also the form machine readers parse.

click_extra.logo.TAGLINE = 'Drop-in replacement for Click and Cloup'

What the project is, spelled out under the program name.

click_extra.logo.UNIT = 2

Scale of the whole mark, in sub-pixels.

Every dimension is a multiple of it, so the mark is 12 * UNIT columns wide and 5 * UNIT lines tall. Two is the largest that still seats the mark beside the version screen’s facts inside eighty columns: 24 columns of mark, three of gutter and fifty of facts comes to 77. Three would need 89, and the screen would decline to draw itself on any standard terminal.

click_extra.logo.LEVELS: tuple[tuple[int, int], ...] = ((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (2, 0))

Each cube’s (row, column) up the pyramid, bottom row first.

Also the order FACE_COLORS is written in, and the order the mark is painted in: a cube resting on two others has to be laid down after them.

click_extra.logo.BRAND_HUES: tuple[int, ...] = (41, 94, 306, 279, 210, 156)

Each cube’s hue in degrees, in LEVELS order.

Read off the ribbon that outlines each cube’s top face in the artwork. The mark has no per-cube hue of its own: cube one alone carries gold, green and olive across its six ribbons. So this is a reading of the artwork rather than a recovery of it. It keeps the terminal mark recognizably the same six colors people already associate with the logo, which an evenly spaced spectrum would not.

click_extra.logo.SATURATION = 0.62

HLS saturation shared by every face. Vivid enough to tell six hues apart.

click_extra.logo.PLANE_LUMINANCE: dict[str, float] = {'l': 0.13, 'r': 0.22, 't': 0.36}

WCAG relative luminance per plane: the lid, then the two walls.

Chosen as a band rather than per color, so every hue is equally far from both backgrounds. The floor of the band is what the mark shows against white and the ceiling what it shows against black, which is why it sits in the middle: the widest band tried reads beautifully on black and falls to 1.74:1 on white.

The 2.25:1 it leaves between a cube’s lid and its dark wall is well past what an eye needs to see a facet, and it survives every dichromacy, planes differing in luminance alone.

click_extra.logo.FACE_COLORS: tuple[dict[str, str], ...] = ({'l': '#7F611E', 'r': '#A37B26', 't': '#CB9A30'}, {'l': '#40721B', 'r': '#519122', 't': '#66B52A'}, {'l': '#AD28A1', 'r': '#D344C6', 't': '#E17FD8'}, {'l': '#9730CC', 'r': '#AE5ED9', 't': '#C58CE4'}, {'l': '#2767A8', 'r': '#3985D0', 't': '#70A7DD'}, {'l': '#1B724F', 'r': '#229265', 't': '#2BB87F'})

Each cube’s three face colors, in LEVELS order.

Derived, not picked: every entry is its cube’s BRAND_HUES hue at SATURATION, taken to the luminance its plane declares in PLANE_LUMINANCE. Written out rather than computed at import so the palette stays greppable and a designer can hand-tune one value; test_palette_follows_its_derivation fails if a tuned value drifts off the system.

Emitted as 24-bit color. The 256-color cube cannot separate all eighteen, and the usual reason to prefer it does not apply: no touching pair of faces collides there, so a terminal that downsamples still shows six distinct cubes.

click_extra.logo.geometry()[source]

One cube’s dimensions, in sub-pixels.

A lid four units wide and two tall is 2:1 dimetric, and the smallest such rhombus whose half-height is a whole number, which the row pitch needs. The body matches the lid’s width over two, making every box a true cube.

Return type:

dict[str, int]

click_extra.logo.size()[source]

The mark’s footprint in sub-pixels, columns by rows.

Return type:

tuple[int, int]

click_extra.logo.LOGO_WIDTH = 24

Columns the mark occupies.

click_extra.logo.LOGO_LINES = 10

Terminal lines the mark renders to, two sub-pixel rows making one.

click_extra.logo.faces()[source]

Every visible face, bottom row first, as whole-sub-pixel polygons.

2:1 dimetric rather than the artwork’s 30 degrees, and integer vertices rather than a sampled rendering, for the same reason: a 30-degree edge advances 1.732 sub-pixels per row, which no grid can hold, so it comes out as a stair of uneven treads that the eye reads as fraying. Two across for every one down tiles a square grid exactly, and every tread is then the same.

Return type:

list[tuple[int, str, list[tuple[int, int]]]]

click_extra.logo.sub_pixels()[source]

The mark as a grid of flat colors, hit-tested against the face polygons.

Rasterizing the mark and reading pixels back would hand the antialiaser a say in the palette: every boundary it smooths mints a color belonging to neither face, and a rendition this size comes back carrying over a hundred of them. Testing each sub-pixel’s centre against the polygons keeps it to the eighteen declared, three per cube, which is what makes the edges read as steps rather than smudge.

Return type:

list[list[tuple[int, int, int] | None]]

click_extra.logo.render_logo()[source]

Paint the mark into styled lines, each exactly LOGO_WIDTH wide.

A cell whose two sub-pixels carry different colors paints the top one as foreground over the bottom one as background, which is what fits two independent colors on one line. Runs of cells sharing a color pair are styled together rather than one escape sequence per character, keeping the mark from tripling in size.

Trailing blanks are kept: VersionScreen pads a ragged logo for us, but a mark that arrives square costs it nothing to measure.

Cached: the mark never changes, so it is painted on the first call that needs it and read back by every later one.

Return type:

tuple[str, ...]

click_extra.logo.brand_facts()[source]

The interpreter and platform every screen reports, plus this project’s own.

Return type:

dict[str, str]

click_extra.logo.BRAND_SCREEN = VersionScreen(logo=<functools._lru_cache_wrapper object>, tagline='Drop-in replacement for Click and Cloup', facts=<function brand_facts>, gutter='   ')

Click Extra’s own --version screen.

Mounted on the click-extra CLI, and the shape a CLI of your own would copy:

from functools import partial
from click_extra import group
from click_extra.commands import default_params

@group(params=partial(default_params, screen=BRAND_SCREEN))
def cli():
    pass

Both render_logo() and brand_facts() are passed uncalled, so the mark is painted and the facts are read when --version is drawn, not when this module is imported: the habit that keeps a costlier value from being charged to every invocation. The mark is then cached, since it never changes.

click_extra.prebake API

Bake build-time metadata into Python source files before compilation.

Compiled binaries (Nuitka, PyInstaller) and git-less runtimes (Docker images, archive checkouts) cannot resolve version or Git metadata at runtime the way click_extra.version.VersionOption does. The values must instead be written into the source before the build, by rewriting the relevant dunder assignments (__version__, __git_short_hash__, …) in place with ast.

This mirrors shadow-rs, which injects build-time constants (BRANCH, SHORT_COMMIT, COMMIT_HASH, COMMIT_DATE, TAG, …) into Rust binaries at compile time. Its BUILD_TIME, BUILD_OS and BUILD_TARGET constants have counterparts here too, in click_extra.version_facts.BUILD_RESOLVERS: those describe the host that produced the binary, which neither the git fields nor the running interpreter can answer once it is running somewhere else.

click_extra.project.discover_package_init_files() finds the files to bake from a project’s packaging metadata.

click_extra.prebake.prebake_version(file_path, local_version)[source]

Append a PEP 440 local version identifier to __version__.

The compile-time complement to the runtime click_extra.version.VersionOption.version property: a Nuitka or PyInstaller binary cannot run git at runtime, so the hash must be baked into __version__ in the source file before compilation.

Only a .dev version is touched, and only when it carries no + suffix yet: a release version and an already pre-baked one are left alone, which makes a second run safe.

Parameters:
  • file_path (Path) – the Python source file declaring __version__.

  • local_version (str) – the identifier to append, like a Git short hash.

Return type:

str | None

Returns:

the new version, or None when nothing changed: a release version, an already pre-baked one, or no __version__ at all.

click_extra.prebake.prebake_dunders(file_path, fields)[source]

Fill the empty dunder placeholders of a Python source file in one pass.

Reads and parses file_path once, then writes it once, with every placeholder of fields that was found empty filled in. A field whose value is a callable is resolved only when its placeholder is present and still empty, so a resolver that shells out (to git, say) runs for the placeholders that need it, never for a file carrying none. A callable answering None or an empty string leaves its placeholder alone, as does a placeholder already holding a value: running the pass twice is safe.

Placeholders must use empty strings (__field__ = "", not None). The AST matcher only recognizes string literals, and the empty string acts as a falsy sentinel that stays type-consistent with baked values (always str).

A name with no placeholder in the file is skipped: a build bakes every field it knows into every file of a package, and most files declare a few of them. prebake_dunder() is the single-field form, which does warn.

Parameters:
  • file_path (Path) – the Python source file to rewrite.

  • fields (Mapping[str, str | Callable[[], str | None]]) – dunder names (__git_branch__) mapped to the value to bake, or to a callable resolving it on demand.

Return type:

dict[str, str]

Returns:

the names written, each mapped to the value it received. Empty when the file was left untouched.

click_extra.prebake.prebake_dunder(file_path, name, value)[source]

Replace an empty dunder variable’s value in a Python source file.

The single-field form of prebake_dunders(), which carries the placeholder rules, with one addition: a file holding no name assignment at all is reported with a warning, since a lone field is asked for on purpose and a typo in its name would otherwise pass unnoticed.

Where prebake_version() appends a PEP 440 local identifier to __version__, this replaces the whole value of any dunder variable that starts empty. Typical use case: injecting a release commit SHA into __git_tag_sha__ = "" at build time.

Parameters:
  • file_path (Path) – the Python source file to rewrite.

  • name (str) – the dunder variable to fill, like __git_tag_sha__.

  • value (str) – the value to bake.

Return type:

str | None

Returns:

the value written, or None when nothing changed: no such variable, or one already holding a value.