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.
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+08c7736
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': '22f57e0789c121b5aef96f55c2905ca', 'hostname': '-', 'hostfqdn': '-', 'uname': {'system': 'Linux', 'node': '-', 'release': '6.1.146.1-microsoft-standard', 'version': '#1 SMP Mon Jul 21 20:38:16 UTC 2025', 'machine': 'x86_64', 'processor': 'x86_64'}, 'linux_dist_name': '', 'linux_dist_version': '', 'cpu_count': 1, 'fs_encoding': 'utf-8', 'ulimit_soft': 1048576, 'ulimit_hard': 1048576, 'cwd': '-', 'umask': '0o2', 'python': {'argv': '-', 'bin': '-', 'version': '3.14.6 (main, Jun 11 2026, 04:03:53) [Clang 22.1.3 ]', 'compiler': 'Clang 22.1.3 ', 'build_date': 'Jun 11 2026 04:03:53', 'version_info': [3, 14, 6, 'final', 0], 'features': {'openssl': 'OpenSSL 3.5.7 9 Jun 2026', 'expat': 'expat_2.8.1', 'sqlite': '3.53.1', 'tkinter': '9.0', 'zlib': '1.3.1', 'unicode_wide': True, 'readline': True, '64bit': True, 'ipv6': True, 'threading': True, 'urandom': True}}, 'time_utc': '2026-06-22 15:32:47.664928+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.6 (main, Jun 11 2026, 04:03:53) [Clang 22.1.3 ])
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' 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: {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} : 08c7736b6ce9932ce275ee06a2d2ce7f9b82aa3f
debug: {git_short_hash} : 08c7736
debug: {git_date} : 2026-06-22 19:19:53 +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} : {'username': '-', 'guid': '22f57e0789c121b5aef96f55c2905ca', 'hostname': '-', 'hostfqdn': '-', 'uname': {'system': 'Linux', 'node': '-', 'release': '6.1.146.1-microsoft-standard', 'version': '#1 SMP Mon Jul 21 20:38:16 UTC 2025', 'machine': 'x86_64', 'processor': 'x86_64'}, 'linux_dist_name': '', 'linux_dist_version': '', 'cpu_count': 1, 'fs_encoding': 'utf-8', 'ulimit_soft': 1048576, 'ulimit_hard': 1048576, 'cwd': '-', 'umask': '0o2', 'python': {'argv': '-', 'bin': '-', 'version': '3.14.6 (main, Jun 11 2026, 04:03:53) [Clang 22.1.3 ]', 'compiler': 'Clang 22.1.3 ', 'build_date': 'Jun 11 2026 04:03:53', 'version_info': [3, 14, 6, 'final', 0], 'features': {'openssl': 'OpenSSL 3.5.7 9 Jun 2026', 'expat': 'expat_2.8.1', 'sqlite': '3.53.1', 'tkinter': '9.0', 'zlib': '1.3.1', 'unicode_wide': True, 'readline': True, '64bit': True, 'ipv6': True, 'threading': True, 'urandom': True}}, 'time_utc': '2026-06-22 15:32:47.664928+00:00', 'time_utc_offset': 0.0, '_eco_version': '1.1.0'}
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"]
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': '22f57e0789c121b5aef96f55c2905ca', 'hostname': '-', 'hostfqdn': '-', 'uname': {'system': 'Linux', 'node': '-', 'release': '6.1.146.1-microsoft-standard', 'version': '#1 SMP Mon Jul 21 20:38:16 UTC 2025', 'machine': 'x86_64', 'processor': 'x86_64'}, 'linux_dist_name': '', 'linux_dist_version': '', 'cpu_count': 1, 'fs_encoding': 'utf-8', 'ulimit_soft': 1048576, 'ulimit_hard': 1048576, 'cwd': '-', 'umask': '0o2', 'python': {'argv': '-', 'bin': '-', 'version': '3.14.6 (main, Jun 11 2026, 04:03:53) [Clang 22.1.3 ]', 'compiler': 'Clang 22.1.3 ', 'build_date': 'Jun 11 2026 04:03:53', 'version_info': [3, 14, 6, 'final', 0], 'features': {'openssl': 'OpenSSL 3.5.7 9 Jun 2026', 'expat': 'expat_2.8.1', 'sqlite': '3.53.1', 'tkinter': '9.0', 'zlib': '1.3.1', 'unicode_wide': True, 'readline': True, '64bit': True, 'ipv6': True, 'threading': True, 'urandom': True}}, 'time_utc': '2026-06-22 15:32:47.664928+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
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 two fallbacks for git-less environments: a
pre-baked __<field>__ dunder in the CLI module (injected before build by
click_extra.prebake), then a committed .git_archival.json populated
by git archive.
- 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.
- class click_extra.version.VersionOption(param_decls=None, message=None, fields=None, styles=None, message_style=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.
- 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, Callable[[str], str]]] = {'env_info': Style(fg='bright_black'), 'exec_name': Style(fg='bright_white', bold), 'git_branch': Style(fg='cyan'), 'git_date': Style(fg='bright_black'), 'git_dirty': Style(fg='red'), 'git_distance': Style(fg='green'), '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': Style(fg='bright_white', bold), 'module_version': Style(fg='green'), 'package_name': Style(fg='bright_white', bold), 'package_version': Style(fg='green'), 'prog_name': Style(fg='bright_white', bold), 'version': Style(fg='green')}
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.
- 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 library importlib.metadata API.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 env_info: dict[str, Any][source]
Various environment info.
Returns the data produced by boltons.ecoutils.get_profile().
- 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.- Return type:
- print_debug_message()[source]
Render in debug logs all template fields in color.
Todo
Pretty print JSON output (easier to read in bug reports)?
- Return type:
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].