Version¶
Click Extra provides its own version option which, compared to Click’s built-in:
adds new variable to compose your version string
adds colors
adds an environment summary for bug reports
works with standalone scripts
expose metadata in the context
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 |
|---|---|
The module object in which the command is implemented. |
|
The |
|
The full path of the file in which the command is implemented. |
|
The string found in the local |
|
The name of the package in which the CLI is distributed. |
|
The version from the package metadata in which the CLI is distributed. |
|
The package author(s) from the core metadata, or |
|
The package license from the core metadata: SPDX |
|
User-friendly name of the executed CLI. Returns |
|
Version of the CLI. Returns |
|
The full path to the Git repository root directory, or |
|
The current Git branch name, or |
|
The full Git commit hash of the current |
|
The short Git commit hash of the current |
|
The commit date of the current |
|
The Git tag pointing at |
|
The full commit SHA that the current tag points at, or |
|
The number of commits since the most recent tag, or |
|
The work-tree state: |
|
When the distribution was built, as an RFC 3339 UTC timestamp, or |
|
The operating system the build ran on, or |
|
The platform a distribution built there installs on ( |
|
The CPU architecture the build ran on, or |
|
The display name of the program. Defaults to Click’s |
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:
A
__version__variable defined alongside your CLI (see standalone scripts).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).The version from package metadata via
importlib.metadata: this is the most common source for installed packages.Noneif 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)sshould be replaced by{package_name}%(prog)sshould be replaced by{prog_name}All other
%(variable)sshould 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 |
|
Git available? |
|
|---|---|---|---|
Local dev (from source) |
|
Yes |
|
Nuitka binary (pre-baked) |
|
No |
|
Nuitka binary (not pre-baked) |
|
No |
|
Release |
|
N/A |
|
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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|---|
|
|
The build instant, as an RFC 3339 UTC timestamp |
|
|
The operating system the build ran on |
|
|
The wheel platform tag ( |
|
|
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 tomessage_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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 ( ͡❛ ͜ʖ ͡❛)
Hint
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:
mail-deduplicate’s
report_url()andrender_report()print the link to a pre-filled issue form, a block to paste between8<markers, and a caution for an AI agent.meta-package-manager’s
_build_issue_url()pre-fills an issue from a user override, and itsoverride_experiment()suggests the override to test when the CLI cannot use a tool.
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-colororNO_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
--accessiblekeeps 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
VersionScreentake 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 fromCLI_ECOSYSTEM_PACKAGESeven when it sits under one of those packages, asclick_extra.__main__does in Click Extra’s own binary.- Return type:
- 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_THEMEand the background-sniffingautoalike, and drops its color under the monochromemanpagepalette. A style captured at import would paint every theme with one palette: the bright white program name ofdarkis 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.
- click_extra.version.unstyled(text)[source]
Identity style, for a segment left with no color of its own.
- Return type:
- class click_extra.version.VersionScreen(logo, tagline='', facts=<function default_facts>, gutter=' ')[source]
Bases:
objectA logo, and the facts to seat beside it, as
--versionshould 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
VersionOptionthrough itsscreenargument, or to a whole CLI throughdefault_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
--versionthat draws the screen, asfactsdoes 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
--versionis asked for.
- 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
--versioncannot drift apart on color.
- 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
Nonewhen 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 reportsshutil’s 80-column default, wide enough that a redirected-but-forced-color run still gets the screen it asked for.
- 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
autodefault, exactly asclick.echodoes when it decides whether to strip the codes itself.Note
The stream probed is
sys.stdout, not Click’s own resolution of it.click.echoreaches stdout through a private cached wrapper, which fixes the output encoding and delegatesisatty()to the stream beneath it, so both answer alike. Click deprecates the public accessor of that wrapper,click.get_text_stream().- Return type:
- 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 oftemplate, in order: the literal text before the field, the identifier the field resolves on the option (modulefor{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.
- 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:
ExtraOptionGathers 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
--versionoption 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 oftemplate_fields(for example{"version": "1.2.3"}).styles (
Mapping[str,Callable[[str],str] |None] |None) – mapping of template field name to itsStyle, merged overdefault_styles. PassNoneas a value to clear a field’s default style. Keys must be members oftemplate_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) – aVersionScreento draw instead of the one-line message, whenever the terminal can take it. Left unset,--versionprints the one-line message.
- Raises:
TypeError – when a key of
fieldsorstylesnames 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-providedstylesare 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 defaultdarkpalette,invoked_commandis bright white bold andsuccessis green.
- message: str = '{prog_name}, version {version}'
Default message template used to render the version string.
- pin_field(field_id, value)[source]
Pin a template field to a caller-supplied value.
The value shadows the
cached_propertythat would otherwise resolve the field, and is recorded soreset_resolution()can tell the two apart. Every caller that pins a field goes through here: a baresetattrsets the value without the record, and the next reset then discards it.
- 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
--versionexample 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:
- 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:
--versionis 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:
modulethen defers to the module of the root command’s callback.- Return type:
- 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 aCliRunnerdriven 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 owninvoke(), inside the Click ecosystem.The Sphinx
click:rundirective is that shape: without this, a documented--versionwould 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
Nonewhen the command has no callback (a bareclick.Group) or its module is not imported, leaving the walk’s own answer in place.- Return type:
- property module: ModuleType[source]
Returns the module in which the CLI resides.
- property module_file: str | None[source]
Returns the module’s file full path.
Nonefor a module with no file, like the__main__ofpython -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_versionproperty below, which uses the standard libraryimportlib.metadataAPI.Click’s own
version_optionreads the package metadata only (pallets/click#2598). Click Extra reads__version__too, for the scripts and codebases that set it.
- property package_version: str | None[source]
Returns the package version if installed.
Resolved from the distribution name (see
_distribution_name) viaimportlib.metadata.version(). ReturnsNoneif 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.Noneif 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.Noneif 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 -mruns it as__main__, while a console script or a compiled binary imports it aspackage.__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 like1.2.3.dev0+abc1234. This helps identify the exact commit a dev build was produced from. Whengit_short_hashfinds 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 like1.2.3.dev0+abc1234+xyz5678.
- property git_repo_path: Path | None[source]
Find the Git repository root directory.
Walks up from
_search_startto the first directory holding a.gitentry: seefind_git_root().
- property git_branch: str | None[source]
Returns the current Git branch name.
Checks for a pre-baked
__git_branch__dunder first, thengit 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, thengit 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, thengit 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, thengit show -s --format=%ci HEAD, then.git_archival.json(whosenode-dateis strict ISO 8601, like2021-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, thengit describe --tags --exact-match HEAD, then.git_archival.json.Returns
Noneif 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, thengit rev-list -1on the tag returned bygit_tag, then.git_archival.json. ReturnsNoneif 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 parsesgit describe --tags --long, then falls back to.git_archival.json.Nonewhen no tag is reachable or Git is unavailable.
- property git_dirty: str | None[source]
Work-tree state:
"dirty","clean"orNone.Checks for a pre-baked
__git_dirty__dunder first, then runsgit status --porcelain.Nonewhen not in a Git repository or Git is unavailable. There is no.git_archival.jsonfallback: 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: seeBUILD_RESOLVERSandresolve_build_time().
- property build_os: str | None[source]
The operating system the build ran on.
Reads the pre-baked
__build_os__dunder. Seeresolve_build_os().
- property build_target: str | None[source]
The platform a distribution built here installs on.
Reads the pre-baked
__build_target__dunder. Seeresolve_build_target().
- property build_target_arch: str | None[source]
The CPU architecture the build ran on.
Reads the pre-baked
__build_target_arch__dunder. Seeresolve_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.
multicalldispatch relies on that, running one CLI under many names in sequence, and aprog_namepassed tomain()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 nofield_idfor the style of the template’s literal segments, which ismessage_stylealone.
- colored_template(template=None)[source]
Insert ANSI styles to a message template.
Accepts a custom
templateas 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:
- render_message(template=None)[source]
Render the version string from the provided template.
Accepts a custom
templateas parameter, otherwise uses the defaultself.colored_template()produced by the instance.A CLI carrying a
VersionScreengets 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-coloror 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
--accessiblekeeps the plain message.
- Return type:
- click_extra.version.reset_version_resolution(command)[source]
Reset every
VersionOptionin 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--versioncallback, which resolves the very chain this function clears.- Return type:
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 (
PILvsPillow,jwtvsPyJWT). A name already matching an installed distribution is returned as-is; otherwise it is resolved throughimportlib.metadata.packages_distributions(). Ambiguous mappings (one import name to several distributions) returnNone: passpackage_nameexplicitly to disambiguate.Returns
Nonewhen nothing installed provides the name, which is what makes it usable as a test of whether a module belongs to a real distribution.
- 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
gitsubcommand, mapped to that subcommand’s args.git_tag_sha,git_distanceandgit_dirtyare excluded: their resolution is not a single staticgitinvocation whose stripped output is the value.git_tag_shadereferences the tag (git rev-list -1 <tag>),git_distanceparsesgit describeandgit_dirtymaps the porcelain status to a label. Seeresolve_git_tag_sha(),resolve_git_distance()andresolve_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 agitcommand 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
gitcommand and return its output, without the final newline.A command that fails returns
None: nogiton thePATH, a non-zero exit status, like the one of acwdoutside a repository, or a command still running aftertimeoutseconds. Setcheckto raise instead: anOSErrorwhengitdoes not start, asubprocess.CalledProcessErroron a non-zero exit status, and asubprocess.TimeoutExpiredwhen the time runs out.By default an empty output returns
Nonetoo, like a failure. Setallow_emptyto keep the empty string, which some commands use meaningfully:git status --porcelainprints 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.
- click_extra.version_facts.resolve_git_dirty(cwd=None)[source]
Report the work-tree state as
"dirty","clean"orNone.Returns
"dirty"whengit status --porcelainreports uncommitted changes,"clean"when it reports none, andNonewhen the state cannot be determined (not a Git repository, orgitis unavailable).The empty output of a clean work tree is meaningful here, so the command is run with
allow_emptyto tell it apart from a failure.
- 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>. ReturnsNonewhen no tag is reachable, the directory is not a Git repository, orgitis unavailable.
- click_extra.version_facts.resolve_git_tag_sha(cwd=None)[source]
Resolve the commit SHA the tag at
HEADpoints at, orNone.Runs
git describe --tags --exact-match HEADto find the tag, thengit rev-list -1 <tag>to dereference it to a commit SHA. ReturnsNonewhenHEADis not at a tagged commit, the directory is not a Git repository, orgitis unavailable.
- 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(orNonewhen 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.jsonfallbacks.the
click-extra prebake allcommand, 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.
- click_extra.version_facts.build_moment()[source]
The instant a build happens, in UTC.
Reads
SOURCE_DATE_EPOCHwhen the build sets it, so two runs of a reproducible build stamp the same instant. Falls back to the current time.- Return type:
- 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:
- click_extra.version_facts.resolve_build_os()[source]
The operating system the build runs on.
- Return type:
- 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-arm64says the binary needs macOS 15, whereresolve_build_os()only says it was built on macOS.- Return type:
- click_extra.version_facts.resolve_build_target_arch()[source]
The CPU architecture the build runs on.
- Return type:
- 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_RESOLVERSandenv_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 itsBUILD_TIME,BUILD_OSandBUILD_TARGETconstants.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 allis the only thing that ever writes these. A field left unbaked stays empty, and itsVersionOptionaccessor answersNone.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_branchis baked as__git_branch__: the one spellingVersionOptionreads back, andclick-extra prebakewrites. Keeping the contract in one place is what lets the two sides stay agreed.- Return type:
- click_extra.version_facts.find_git_root(start)[source]
Walk up from start to the first folder holding a
.gitentry.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.
- click_extra.version_facts.find_archival_file(start)[source]
Walk up from start to find a
.git_archival.jsonfile.Returns the first match in start or any of its parents, or
None.
- click_extra.version_facts.read_archival(path)[source]
Parse a
.git_archival.jsonfile into a string mapping.Returns an empty mapping when the file is missing, unreadable, or not a valid JSON object.
- click_extra.version_facts.archival_field(data, field_id)[source]
Resolve a
git_*field from parsed.git_archival.jsondata.data follows the setuptools-scm archival schema:
node(full hash),node-date,describe-nameandref-names. The same file is read by setuptools-scm and Dunamai, so a single committed.git_archival.jsonserves all three.Returns
Nonewhen the field is absent, empty, or still holds an unsubstituted$Format:…$placeholder. That last case is what a plain checkout contains:git archiveperforms 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.
- click_extra.version_facts.read_direct_url(distribution)[source]
Parse the
direct_url.jsonrecord 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://...orpip 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.
- click_extra.version_facts.direct_url_field(data, field_id)[source]
Resolve a
git_*field from a parseddirect_url.jsonrecord.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 answersgit_long_hash, and its first 7 charactersgit_short_hash.Every other field answers
None. Thevcs_info.requested_revisionthe record may also hold names a branch, a tag or a commit, with nothing to tell which.
- 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.
- click_extra.version_facts.meta_value(meta, *keys)[source]
Return the first non-empty value among core-metadata keys.
Each key is tested with
inbefore[]reads it:[]on a missing key returnsNonewith aDeprecationWarning, and is to raiseKeyError.
- click_extra.version_facts.resolve_author(meta)[source]
Return the author(s) from meta’s core metadata, or
None.Prefers the
Authorfield, then theMaintainerfield, then the display name parsed out of theAuthor-email/Maintainer-emailfields (Name <email>). ReturnsNonewhen meta isNoneor no author can be determined.
- click_extra.version_facts.resolve_license(meta)[source]
Return the license from meta’s core metadata, or
None.Prefers the SPDX
License-Expressionfield (core metadata 2.4+). Falls back to the human-readable name of the firstLicense ::trove classifier, then to the free-formLicensefield (which may hold the full license text). ReturnsNonewhen meta isNoneor no license can be determined.
- click_extra.version_facts.platform_label()[source]
Current platform and CPU architecture, as displayed to the user.
- Return type:
- 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 factsdefault_facts()puts on the version screen, joined for a CLI that would rather spend one line of its plain--versionon them than draw a screen at all:@version_option(message="{prog_name} {version} (" + env_summary() + ")").- Return type:
- 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:
- 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,
dictpreserving 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
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 * UNITcolumns wide and5 * UNITlines 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_COLORSis 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
LEVELSorder.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
LEVELSorder.Derived, not picked: every entry is its cube’s
BRAND_HUEShue atSATURATION, taken to the luminance its plane declares inPLANE_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_derivationfails 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.
- click_extra.logo.size()[source]
The mark’s footprint in sub-pixels, columns by rows.
- 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.
- 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.
- click_extra.logo.render_logo()[source]
Paint the mark into styled lines, each exactly
LOGO_WIDTHwide.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:
VersionScreenpads 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.
- click_extra.logo.brand_facts()[source]
The interpreter and platform every screen reports, plus this project’s own.
- 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
--versionscreen.Mounted on the
click-extraCLI, 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()andbrand_facts()are passed uncalled, so the mark is painted and the facts are read when--versionis 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.versionproperty: a Nuitka or PyInstaller binary cannot rungitat runtime, so the hash must be baked into__version__in the source file before compilation.Only a
.devversion 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.
- 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_pathonce, then writes it once, with every placeholder offieldsthat 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 (togit, say) runs for the placeholders that need it, never for a file carrying none. A callable answeringNoneor 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__ = "", notNone). The AST matcher only recognizes string literals, and the empty string acts as a falsy sentinel that stays type-consistent with baked values (alwaysstr).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:
- Return type:
- 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 nonameassignment 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.