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 complete environment information in JSON
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.
In most cases, you do not need to force it, as the version will be automatically 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: |
|
The display name of the program. Defaults to Click’s |
|
The environment information in JSON. |
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. 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.
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__main__entry points, like Nuitka-compiled binaries).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}"] -->|"is __main__"| 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 results reports the package name as click_extra.sphinx because we are running the example from the click-extra documentation build environment. This is just a quirk of the documentation setup and will 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.
Let’s 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+b4fe11b
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. Repomatic automates this via its prebake-version command.
Pre-baking git metadata¶
All git_* template fields support pre-baking. If the CLI module 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 [project.scripts] in pyproject.toml, so you don’t need to hardcode paths in your build scripts.
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 [project.scripts] in pyproject.toml. 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.
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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¶
The {env_info} variable compiles all sorts of environment information.
Here is how it looks like:
import click
from click_extra import version_option
@click.command
@version_option(message="{env_info}")
def env_info_cli():
pass
$ env-info-cli --version
{'username': '-', 'guid': '429f037bc25a8922a17a6bb01cab638', 'hostname': '-', 'hostfqdn': '-', 'uname': {'system': 'Linux', 'node': '-', 'release': '7.0.0-1011-azure', 'version': '#11-Ubuntu SMP PREEMPT Thu Jul 23 02:08:37 UTC 2026', 'machine': 'x86_64', 'processor': ''}, 'linux_dist_name': '', 'linux_dist_version': '', 'cpu_count': 4, 'fs_encoding': 'utf-8', 'ulimit_soft': 65536, 'ulimit_hard': 65536, 'cwd': '-', 'umask': '0o2', 'python': {'argv': '-', 'bin': '-', 'version': '3.14.4 (main, Jun 18 2026, 14:25:02) [GCC 15.2.0]', 'compiler': 'GCC 15.2.0', 'build_date': 'Jun 18 2026 14:25:02', 'version_info': [3, 14, 4, 'final', 0], 'features': {'openssl': 'OpenSSL 3.5.5 27 Jan 2026', 'expat': 'expat_2.7.4', 'sqlite': '3.46.1', 'tkinter': '', 'zlib': '1.3.1', 'unicode_wide': True, 'readline': True, '64bit': True, 'ipv6': True, 'threading': True, 'urandom': True}}, 'time_utc': '2026-08-21 10:17:14.009169+00:00', 'time_utc_offset': 0.0, '_eco_version': '1.1.0'}
It’s verbose but it’s helpful for debugging and reporting of issues from end users.
Important
The JSON output is scrubbed out of identifiable information by default: current working directory, hostname, Python executable path, command-line arguments and username are replaced with -.
Another trick consist in picking into the content of {env_info} to produce highly customized version strings. This can be done because {env_info} is kept as a dict:
import click
from click_extra import version_option
@click.command
@version_option(
message="{prog_name} {version}, from {module_file} (Python {env_info[python][version]})",
fields={"version": "1.2.3"},
)
def custom_env_info():
pass
$ custom-env-info --version
custom-env-info 1.2.3, from /home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py (Python 3.14.4 (main, Jun 18 2026, 14:25:02) [GCC 15.2.0])
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 — a string, or the lines of one — so it can be ASCII line art, half-blocks, or whatever your renderer produces:
from functools import partial
from click_extra import group
from click_extra.commands import default_params
from click_extra.version import VersionScreen, 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 a callable defers it to render time. Values that cost something to compute — a plugin count, a registry probe — should be charged to the invocation that asks for --version, not to every invocation that might have.
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. Partly because a mark may need it — a flat-shaded one carries its shape in the difference between its faces, and collapses into a single silhouette once the escapes are stripped — and partly because it is 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.
Caution
visible_width() counts characters, not display cells, so a logo drawn with double-width characters (CJK, emoji) measures short and lays out ragged. Anything a terminal renders one cell wide is fine, which covers ASCII, the block and box-drawing ranges, and braille.
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.
$ click-extra --version
▄▄██▄▄
█▀▀██▀▀█ Click Extra, version 9.1.0
████████ Drop-in replacement for Click and Cloup
▄▄██▀▀██▀▀██▄▄
█▀▀██▀▀██▀▀██▀▀█ Python 3.14.4
████████████████ Platform Ubuntu x86-64 (AMD64)
▄▄██▀▀██▀▀██▀▀██▀▀██▄▄ Built on Click 8.4.2, Cloup 3.1.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: Version string template variables:
debug: {module} : <module 'click_extra.sphinx.click' from '/home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py'>
debug: {module_name} : click_extra.sphinx.click
debug: {module_file} : /home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py
debug: {module_version} : 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
debug: {version} : 1.2.3
debug: {git_repo_path} : /home/runner/work/click-extra/click-extra
debug: {git_branch} : main
debug: {git_long_hash} : b4fe11ba9e5c85fd20a6a7c15b45da8c95cfadd5
debug: {git_short_hash} : b4fe11b
debug: {git_date} : 2026-08-21 14:12:52 +0400
debug: {git_tag} : None
debug: {git_tag_sha} : None
debug: {git_distance} : None
debug: {git_dirty} : clean
debug: {prog_name} : version-in-logs
debug: {env_info} :
debug: {
debug: "_eco_version": "1.1.0",
debug: "cpu_count": 4,
debug: "cwd": "-",
debug: "fs_encoding": "utf-8",
debug: "guid": "429f037bc25a8922a17a6bb01cab638",
debug: "hostfqdn": "-",
debug: "hostname": "-",
debug: "linux_dist_name": "",
debug: "linux_dist_version": "",
debug: "python": {
debug: "argv": "-",
debug: "bin": "-",
debug: "build_date": "Jun 18 2026 14:25:02",
debug: "compiler": "GCC 15.2.0",
debug: "features": {
debug: "64bit": true,
debug: "expat": "expat_2.7.4",
debug: "ipv6": true,
debug: "openssl": "OpenSSL 3.5.5 27 Jan 2026",
debug: "readline": true,
debug: "sqlite": "3.46.1",
debug: "threading": true,
debug: "tkinter": "",
debug: "unicode_wide": true,
debug: "urandom": true,
debug: "zlib": "1.3.1"
debug: },
debug: "version": "3.14.4 (main, Jun 18 2026, 14:25:02) [GCC 15.2.0]",
debug: "version_info": [
debug: 3,
debug: 14,
debug: 4,
debug: "final",
debug: 0
debug: ]
debug: },
debug: "time_utc": "2026-08-21 10:17:14.009169+00:00",
debug: "time_utc_offset": 0.0,
debug: "ulimit_hard": 65536,
debug: "ulimit_soft": 65536,
debug: "umask": "0o2",
debug: "uname": {
debug: "machine": "x86_64",
debug: "node": "-",
debug: "processor": "",
debug: "release": "7.0.0-1011-azure",
debug: "system": "Linux",
debug: "version": "#11-Ubuntu SMP PREEMPT Thu Jul 23 02:08:37 UTC 2026"
debug: },
debug: "username": "-"
debug: }
Standard operation
debug: Reset <RootLogger root (DEBUG)> to WARNING.
debug: Reset <Logger click_extra (DEBUG)> to WARNING.
A variable holding a nested structure is dumped as indented JSON under its own label, rather than the single line a template renders it as. Only {env_info} is built that way, and it is the one variable a thousand characters wide.
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"]
env_info = ctx.meta["click_extra.env_info"]
echo(f"version = {version}")
echo(f"package_name = {package_name}")
echo(f"prog_name = {prog_name}")
echo(f"env_info = {env_info}")
$ version-metadata --version
version-metadata, version 1.2.3
$ version-metadata
version = 1.2.3
package_name = click_extra.sphinx
prog_name = version-metadata
env_info = {'username': '-', 'guid': '429f037bc25a8922a17a6bb01cab638', 'hostname': '-', 'hostfqdn': '-', 'uname': {'system': 'Linux', 'node': '-', 'release': '7.0.0-1011-azure', 'version': '#11-Ubuntu SMP PREEMPT Thu Jul 23 02:08:37 UTC 2026', 'machine': 'x86_64', 'processor': ''}, 'linux_dist_name': '', 'linux_dist_version': '', 'cpu_count': 4, 'fs_encoding': 'utf-8', 'ulimit_soft': 65536, 'ulimit_hard': 65536, 'cwd': '-', 'umask': '0o2', 'python': {'argv': '-', 'bin': '-', 'version': '3.14.4 (main, Jun 18 2026, 14:25:02) [GCC 15.2.0]', 'compiler': 'GCC 15.2.0', 'build_date': 'Jun 18 2026 14:25:02', 'version_info': [3, 14, 4, 'final', 0], 'features': {'openssl': 'OpenSSL 3.5.5 27 Jan 2026', 'expat': 'expat_2.7.4', 'sqlite': '3.46.1', 'tkinter': '', 'zlib': '1.3.1', 'unicode_wide': True, 'readline': True, '64bit': True, 'ipv6': True, 'threading': True, 'urandom': True}}, 'time_utc': '2026-08-21 10:17:14.009169+00:00', 'time_utc_offset': 0.0, '_eco_version': '1.1.0'}
Hint
These variables are presented in their original Python type. If most of these variables are strings, others like env_info retains their original dict type.
Note
Metadata values in ctx.meta are lazily evaluated: a field like env_info or git_long_hash is only computed the first time you access it. If your command only reads ctx.meta["click_extra.version"], the expensive Git subprocess calls and environment profiling 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
Label-to-value rows of a version screen, in the order they are drawn.
- click_extra.version.RESET = '\x1b[0m'
The sequence closing every style, for padding that must inherit none of one.
- click_extra.version.theme_slot(slot)[source]
A style reading its palette slot off the active theme, on every call.
The version template’s fields used to hold a style captured from the
darkpalette at import, on the reasoning that a default binds once anyway. That froze the message to one palette:--theme lightrecolored every help screen and left--versionpainting the program name bright white, which on a light terminal is white on white. Deferring the lookup to call time is what makes the message follow--theme,CLICK_EXTRA_THEMEand the background-sniffingautoalike, and what drops its color entirely under the monochromemanpagepalette.get_current_theme()already 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:
- click_extra.version.Facts
Label-to-value rows of a version screen, in the order they are drawn.
- click_extra.version.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.run_git(*args, cwd=None, allow_empty=False)[source]
Run a
gitcommand and return its stripped output, orNone.cwd defaults to the current working directory when not provided.
By default an empty output is collapsed to
None(treated like a failure). Set allow_empty to keep an empty string instead, which some commands use meaningfully:git status --porcelainprints nothing for a clean work tree, and that is distinct from the command failing.
- click_extra.version.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.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.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.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.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.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.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.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 (for example the program name with-` / `_variants) before reading its metadata.
- click_extra.version.meta_value(meta, *keys)[source]
Return the first non-empty value among core-metadata keys.
Accessed through
in+[](rather than.get()) to dodge the deprecated implicit-Nonereturn on missing keys.
- click_extra.version.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.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.platform_label()[source]
Current platform and CPU architecture, as displayed to the user.
- Return type:
- click_extra.version.env_summary()[source]
One-line interpreter and platform summary.
The same two facts
default_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(fields={"env_info": env_summary()}).- Return type:
- click_extra.version.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 rather than the packages’ own
__version__, which Click deprecated in8.4.0and removes in9.1.- Return type:
- click_extra.version.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.version.visible_width(text)[source]
Columns text occupies once its escape sequences are discounted.
Caution
Counts characters, not display cells, so a logo drawn with double-width characters (CJK, emoji) measures short and its screen lays out ragged. Every character a terminal renders one cell wide is fine, which covers ASCII, the block and box-drawing ranges, and braille.
- 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 — 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=…).- 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:
str.ljustcounts the escape sequences it cannot see, so on a styled line it silently does nothing.
- 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.- Return type:
- 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:
ExtraOptionGather CLI metadata and prints a colored version string.
Note
This started as a copy of the standard @click.version_option() decorator, but is no longer a drop-in replacement. Hence the
Extraprefix.This address 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,--versionbehaves exactly as it always has.
- 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', 'prog_name', 'env_info')
List of field IDs recognized by the message template.
- default_styles: ClassVar[dict[str, IStyle]] = {'env_info': 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 defer to the active palette through
theme_slot()rather than naming a color. Both slots render exactly what the literals they replaced did under thedarkdefault —invoked_commandis bright white bold,successis green — so nothing moves for a CLI that never touches--theme, while one that does finally gets a version message to match.
- message: str = '{prog_name}, version {version}'
Default message template used to render the version string.
- static cli_frame()[source]
Returns the frame in which the CLI is implemented.
Inspects the execution stack frames to find the package in which the user’s CLI is implemented.
Returns the frame itself.
- Return type:
- property module: ModuleType[source]
Returns the module in which the CLI resides.
- property module_version: str | None[source]
Returns the string found in the local
__version__variable.Hint
__version__is an old pattern from early Python packaging. It is not a standard variable and is not defined in the packaging PEPs.You should prefer using the
package_versionproperty below instead, which uses the standard libraryimportlib.metadataAPI.We’re still supporting it for backward compatibility with existing codebases, as Click removed it in version 8.2.0.
- 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.
Delegates to
resolve_author(): prefers theAuthorfield, then theMaintainerfield, then the display name parsed out of theAuthor-email/Maintainer-emailfields (Name <email>). ReturnsNoneif no author can be determined.
- property license: str | None[source]
Returns the package license from its core metadata.
Delegates to
resolve_license(): prefers the SPDXLicense-Expressionfield, falls back to the human-readable name of the firstLicense ::trove classifier, then to the free-formLicensefield. ReturnsNoneif no license can be determined.
- property exec_name: str[source]
User-friendly name of the executed CLI.
Returns the module name. But if the later is
__main__, returns the package name.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).
- 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. If Git is unavailable, 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_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.
- 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(where it is derived 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 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.
- property env_info: dict[str, Any][source]
Various environment info.
Returns the data produced by boltons.ecoutils.get_profile().
- 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.This step is necessary because we need to linearize the template to apply the ANSI codes on the string segments. This is a consequence of the nature of ANSI, directives which cannot be encapsulated within another (unlike markup tags like 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. Not because a mark needs it — a good one survives having its escapes stripped — but because it 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:
- print_debug_message()[source]
Render in debug logs all template fields in color.
A field resolving to a nested structure is dumped as indented JSON under its own label, instead of the single-line
repra template would produce for it. Only:env_info:is built that way today, and it alone accounts for two thirds of this listing: a thousand characters on one line is what a bug report carries otherwise. Upstream reads its profile the same way, through boltons.ecoutils.get_profile_json(indent=True).- Return type:
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 — one color per face, 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.DOCS_URL = 'https://kdeldycke.github.io/click-extra'
Canonical documentation host, advertised on the version screen.
- 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.
- 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=(' \x1b[38;2;43;184;127m▄▄██▄▄\x1b[0m ', ' \x1b[38;2;27;114;79m█\x1b[0m\x1b[38;2;43;184;127m\x1b[48;2;27;114;79m▀▀\x1b[0m\x1b[38;2;43;184;127m██\x1b[0m\x1b[38;2;43;184;127m\x1b[48;2;34;146;101m▀▀\x1b[0m\x1b[38;2;34;146;101m█\x1b[0m ', ' \x1b[38;2;27;114;79m████\x1b[0m\x1b[38;2;34;146;101m████\x1b[0m ', ' \x1b[38;2;197;140;228m▄▄██\x1b[0m\x1b[38;2;27;114;79m\x1b[48;2;197;140;228m▀▀\x1b[0m\x1b[38;2;27;114;79m█\x1b[0m\x1b[38;2;34;146;101m█\x1b[0m\x1b[38;2;34;146;101m\x1b[48;2;112;167;221m▀▀\x1b[0m\x1b[38;2;112;167;221m██▄▄\x1b[0m ', ' \x1b[38;2;151;48;204m█\x1b[0m\x1b[38;2;197;140;228m\x1b[48;2;151;48;204m▀▀\x1b[0m\x1b[38;2;197;140;228m██\x1b[0m\x1b[38;2;197;140;228m\x1b[48;2;174;94;217m▀▀\x1b[0m\x1b[38;2;174;94;217m█\x1b[0m\x1b[38;2;39;103;168m█\x1b[0m\x1b[38;2;112;167;221m\x1b[48;2;39;103;168m▀▀\x1b[0m\x1b[38;2;112;167;221m██\x1b[0m\x1b[38;2;112;167;221m\x1b[48;2;57;133;208m▀▀\x1b[0m\x1b[38;2;57;133;208m█\x1b[0m ', ' \x1b[38;2;151;48;204m████\x1b[0m\x1b[38;2;174;94;217m████\x1b[0m\x1b[38;2;39;103;168m████\x1b[0m\x1b[38;2;57;133;208m████\x1b[0m ', ' \x1b[38;2;203;154;48m▄▄██\x1b[0m\x1b[38;2;151;48;204m\x1b[48;2;203;154;48m▀▀\x1b[0m\x1b[38;2;151;48;204m█\x1b[0m\x1b[38;2;174;94;217m█\x1b[0m\x1b[38;2;174;94;217m\x1b[48;2;102;181;42m▀▀\x1b[0m\x1b[38;2;102;181;42m██\x1b[0m\x1b[38;2;39;103;168m\x1b[48;2;102;181;42m▀▀\x1b[0m\x1b[38;2;39;103;168m█\x1b[0m\x1b[38;2;57;133;208m█\x1b[0m\x1b[38;2;57;133;208m\x1b[48;2;225;127;216m▀▀\x1b[0m\x1b[38;2;225;127;216m██▄▄\x1b[0m ', '\x1b[38;2;127;97;30m█\x1b[0m\x1b[38;2;203;154;48m\x1b[48;2;127;97;30m▀▀\x1b[0m\x1b[38;2;203;154;48m██\x1b[0m\x1b[38;2;203;154;48m\x1b[48;2;163;123;38m▀▀\x1b[0m\x1b[38;2;163;123;38m█\x1b[0m\x1b[38;2;64;114;27m█\x1b[0m\x1b[38;2;102;181;42m\x1b[48;2;64;114;27m▀▀\x1b[0m\x1b[38;2;102;181;42m██\x1b[0m\x1b[38;2;102;181;42m\x1b[48;2;81;145;34m▀▀\x1b[0m\x1b[38;2;81;145;34m█\x1b[0m\x1b[38;2;173;40;161m█\x1b[0m\x1b[38;2;225;127;216m\x1b[48;2;173;40;161m▀▀\x1b[0m\x1b[38;2;225;127;216m██\x1b[0m\x1b[38;2;225;127;216m\x1b[48;2;211;68;198m▀▀\x1b[0m\x1b[38;2;211;68;198m█\x1b[0m', '\x1b[38;2;127;97;30m████\x1b[0m\x1b[38;2;163;123;38m████\x1b[0m\x1b[38;2;64;114;27m████\x1b[0m\x1b[38;2;81;145;34m████\x1b[0m\x1b[38;2;173;40;161m████\x1b[0m\x1b[38;2;211;68;198m████\x1b[0m', ' \x1b[38;2;127;97;30m▀▀█\x1b[0m\x1b[38;2;163;123;38m█▀▀\x1b[0m \x1b[38;2;64;114;27m▀▀█\x1b[0m\x1b[38;2;81;145;34m█▀▀\x1b[0m \x1b[38;2;173;40;161m▀▀█\x1b[0m\x1b[38;2;211;68;198m█▀▀\x1b[0m '), 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
The mark is painted once, here, since it never changes.
brand_facts()is passed uncalled so its values are read when--versionis, not when this module is imported — the habit that keeps a costlier fact from being charged to every invocation.
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.
Todo
Add the following build-time template fields, mirroring the constants shadow-rs injects:
{build_time}: when the distribution was built (shadow-rs exposes it asBUILD_TIME, with RFC 2822 and RFC 3339 variantsBUILD_TIME_2822/BUILD_TIME_3339).{build_os}/{build_target}/{build_target_arch}: the OS, target triple and architecture the build ran on. These describe the build host, unlike{env_info}which reports the runtime Python, OS and architecture, so both are worth keeping for cross-built binaries.
- click_extra.prebake.prebake_version(file_path, local_version)[source]
Pre-bake a
__version__string with a PEP 440 local version identifier.Reads file_path, finds the
__version__assignment viaast, and, if the version contains.devand does not already contain+, appends+<local_version>.This is the compile-time complement to the runtime
click_extra.version.VersionOption.versionproperty: Nuitka/PyInstaller binaries cannot rungitat runtime, so the hash must be baked into__version__in the source file before compilation.Returns the new version string on success, or
Noneif no change was made (release version, already pre-baked, or no__version__found).
- click_extra.prebake.prebake_dunder(file_path, name, value)[source]
Replace an empty dunder variable’s value in a Python source file.
Reads file_path, finds a top-level
name = ""assignment viaast, and, if the current value is an empty string, replaces it with value.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).This is the generic counterpart to
prebake_version(): whereprebake_versionappends a PEP 440 local identifier to__version__, this function does a full replacement of any dunder variable that starts empty. Typical use case: injecting a release commit SHA into__git_tag_sha__ = ""at build time.Returns the new value on success, or
Noneif no change was made (variable not found, or already has a non-empty value).
- click_extra.prebake.discover_package_init_files()[source]
Discover
__init__.pyfiles from[project.scripts].Reads the
pyproject.tomlin the current working directory, extracts[project.scripts]entry points, and returns the unique__init__.pypaths for each top-level package.Only returns paths that exist on disk. Returns an empty list if
pyproject.tomlis missing or has no[project.scripts].