Execution¶
Click Extra bundles a few pre-configured options that control how a CLI runs: how long it takes (--time), how many parallel jobs it may use (--jobs), and what exit code it returns (-0/--zero-exit). Each publishes its resolved value on ctx.meta for downstream code to consume.
Timer¶
Click Extra can measure the execution time of a CLI via a dedicated --time/--no-time option.
Here how to use the standalone decorator:
from time import sleep
from click import command, echo, pass_context
from click_extra import timer_option
@command
@timer_option
def timer():
sleep(0.2)
echo("Hello world!")
$ timer --help
Usage: timer [OPTIONS]
Options:
--time / --no-time Measure and print elapsed execution time.
--help Show this message and exit.
$ timer --time
Hello world!
Execution time: 0.200 seconds.
You can get the timestamp of the CLI start from the context:
from click import command, echo, pass_context
from click_extra import timer_option
@command
@timer_option
@pass_context
def timer_command(ctx):
start_time = ctx.meta["click_extra.start_time"]
echo(f"Start timestamp: {start_time}")
$ timer --time
Start timestamp: 95.745613244
Execution time: 0.000 seconds.
Parallel jobs¶
A pre-configured --jobs option to control parallel execution. It accepts an integer, or one of two keywords: auto (the default: one fewer than the available logical CPU cores, leaving a core free for the main process and system tasks) and max (every available logical CPU core). A value of 0 disables parallelism and runs sequentially.
The option itself does not drive any concurrency: it only captures the user’s intent.
Important
The core count is the number of logical CPUs reported by Python’s os.cpu_count(): hardware threads, not physical cores. On a CPU with simultaneous multi-threading (Intel Hyper-Threading, AMD SMT) a 4-physical-core chip reports 8. This is deliberately the logical count, since subprocess- and I/O-bound work overlaps well across hardware threads. It can differ from the physical-core counts used elsewhere (psutil.cpu_count(logical=False), or pytest-xdist’s -n auto), so --jobs auto may pick a higher number than a physical-core heuristic would.
from click import command, echo, pass_context
from click_extra import jobs_option
@command
@jobs_option
@pass_context
def build(ctx):
"""Build the project."""
jobs = ctx.meta["click_extra.jobs"]
echo(f"Building with {jobs} parallel jobs.")
$ build --help
Usage: build [OPTIONS]
Build the project.
Options:
--jobs [auto|max|INTEGER] Number of parallel jobs. Accepts an integer, 'auto'
(one fewer than the host's logical CPUs) or 'max'
(all logical CPUs). 0 runs sequentially. [default:
auto]
--help Show this message and exit.
$ build --jobs 4
warning: Requested 4 jobs exceeds available CPU cores (1).
Building with 4 parallel jobs.
The auto and max keywords resolve to a core count, keeping the same command portable across machines with different CPU counts:
$ build --jobs max
warning: '--jobs max' resolved to a single job: only 1 logical CPU is available, so execution will be sequential, not parallel.
Building with 1 parallel jobs.
Warning
A value of 0 disables parallelism: it is rounded up to 1 and a warning notes that execution will run sequentially. Negative values are likewise clamped to 1. When the count exceeds the available logical CPU cores, a warning is logged but the value is honored.
Warning
auto and max express a wish for parallelism, but on hosts with few logical CPUs they resolve to a single job and run sequentially: max on a single-core host, or auto on a one- or two-core host (it reserves one core). A warning is then logged, so the silent sequential fallback is not mistaken for parallel execution. An explicit --jobs 1 is treated as a deliberate sequential choice and stays silent.
Tip
The resolved (clamped, validated) job count is published on ctx.meta as JOBS for downstream code to consume. See the available keys table to read it from your own callbacks. It is also logged at info level alongside the host’s os.cpu_count(), so --verbosity INFO reveals how many workers a --jobs command will use.
Running jobs in parallel¶
run_jobs(func, items) maps func over items using the resolved --jobs count, so a command with @jobs_option parallelizes its work with no extra plumbing. It reads the worker count from the context (or an explicit jobs= override), runs sequentially when that count is 1 or there is a single item, and otherwise spreads the work across a thread pool. Results are yielded in submission order, like map.
from click import command, echo
from click_extra import jobs_option, run_jobs
@command
@jobs_option
def bake():
"""Bake several items in parallel."""
items = ("apple", "banana", "cherry")
for baked in run_jobs(str.upper, items):
echo(f"Baked {baked}")
$ bake --jobs 2
warning: Requested 2 jobs exceeds available CPU cores (1).
Baked APPLE
Baked BANANA
Baked CHERRY
The pool is thread-based, which fits the I/O- and subprocess-bound work CLIs usually parallelize (each child releases the GIL). With a single worker the run stays lazy, so a caller can stop on the first result, for example to abort on the first failure.
Zero exit code¶
A pre-configured -0/--zero-exit option flag, following the convention popularized by linters and static analysers: they exit with a non-zero code whenever they report findings, so automation can gate on it. Setting this flag flips that behavior, so the CLI returns 0 as long as it ran to completion, reserving non-zero codes for actual execution failures.
The option itself does not alter the exit code: it only captures the user’s intent.
from click import command, echo, pass_context
from click_extra import zero_exit_option
@command
@zero_exit_option
@pass_context
def inspect(ctx):
"""Inspect a basket of fruits."""
bruised = 2
echo(f"Found {bruised} bruised fruits.")
if bruised and not ctx.meta["click_extra.zero_exit"]:
ctx.exit(1)
$ inspect --help
Usage: inspect [OPTIONS]
Inspect a basket of fruits.
Options:
-0, --zero-exit Always exit with a status code of 0, even when problems are
found.
--help Show this message and exit.
By default the command reports a non-zero exit code when it finds problems:
$ inspect
Found 2 bruised fruits.
With --zero-exit (or its -0 shorthand) the command still reports its findings but always exits with 0:
$ inspect --zero-exit
Found 2 bruised fruits.
Tip
The resolved flag is published on ctx.meta as ZERO_EXIT for downstream code to consume. See the available keys table to read it from your own callbacks.
click_extra.execution API¶
classDiagram
ExtraOption <|-- JobsOption
ExtraOption <|-- TimerOption
ExtraOption <|-- ZeroExitOption
ParamType <|-- JobCount
Options controlling how a CLI runs: timing, parallelism and exit code.
These options share the same shape: each is a pre-configured
ExtraOption that publishes its resolved value
on ctx.meta for downstream code to consume. Only TimerOption acts
on its own (printing the elapsed time); JobsOption and
ZeroExitOption are contracts the framework records but does not enforce.
- click_extra.execution.CPU_COUNT = 1
Number of logical CPUs available, or
Noneif undetermined.This is
os.cpu_count(), which counts logical processors (hardware threads). On a CPU with simultaneous multi-threading (Intel Hyper-Threading, AMD SMT) a 4-physical-core chip reports8. It is therefore not a count of physical cores, and is usually larger than what physical-core tools report, such aspsutil.cpu_count(logical=False)or pytest-xdist’s-n auto(which counts physical cores). Parallelism here is keyed on the logical count on purpose: subprocess- and I/O-bound work overlaps well across hardware threads.
- click_extra.execution.DEFAULT_JOBS = 1
Default number of parallel jobs: one fewer than
CPU_COUNT(logical CPUs).Leaves one logical CPU free for the main process and system tasks. Falls back to
1(sequential) when the count cannot be determined.Caution
This resolves to
1not only on single-core hosts but also on two-core hosts, since it reserves one core. There, the default silently runs sequentially.JobCount.convert()logs a warning whenever a parallel-intent keyword collapses to a single job this way.
- class click_extra.execution.JobCount[source]
Bases:
ParamTypeParse a
--jobsvalue: an integer or theauto/maxkeyword.Resolves the symbolic keywords against the host’s logical CPU count (
CPU_COUNT), counting hardware threads, not physical cores:autoresolves toDEFAULT_JOBS(one fewer than the available logical CPUs), the same heuristic used as the option’s default.maxresolves toCPU_COUNT(every available logical CPU).
Any other token is parsed as an integer and left to
JobsOption.validate_jobs()for clamping and range-checking. Resolving the keywords here keeps the value handed downstream a plainint, so consumers never have to know about the keywords.- name: str = 'jobs'
the descriptive name of this type
- choices = ('auto', 'max')
Symbolic keywords accepted besides an integer count, in render order.
Exposed as
choicesso the help colorizer highlights them likeclick.Choicevalues: the keyword collector duck-types on this attribute (see thegetattr(param.type, "choices", ...)branch in_HelpColorsMixin._collect_params). It is also the single source of truth reused byget_metavar()andconvert(), so the metavar and the parser never drift apart.
- get_metavar(param, ctx=None)[source]
Render
[auto|max|INTEGER](brackets included, asChoicedoes).
- convert(value, param, ctx)[source]
Resolve a keyword to a logical-core count, else parse as an integer.
An already-resolved integer is returned untouched, so option defaults and re-validation can flow back through conversion unharmed. When a parallel-intent keyword (
auto/max) resolves to a single job, a warning is logged: the request reads as “use several cores”, but the host has too few logical CPUs, so execution is silently sequential.- Return type:
- shell_complete(ctx, param, incomplete)[source]
Suggest the
auto/maxkeywords; an integer count is free-form.Completion proposes only the symbolic keywords, matched case-insensitively to mirror how
convert()lower-cases its input. An integer has no finite set to enumerate, so none is offered, yetconvert()still accepts one.- Return type:
- class click_extra.execution.JobsOption(param_decls=None, default='auto', expose_value=False, show_default=True, type=<click_extra.execution.JobCount object>, help="Number of parallel jobs. Accepts an integer, 'auto' (one fewer than the host's logical CPUs) or 'max' (all logical CPUs). 0 runs sequentially.", **kwargs)[source]
Bases:
ExtraOptionA pre-configured
--jobsoption to control parallel execution.Accepts an integer or one of two keywords resolved by
JobCount:auto(the default: one fewer than the available logical CPU cores, leaving a core free for the main process and system tasks) andmax(every available logical CPU core). A value of0disables parallelism and runs sequentially.The core count is the number of logical CPUs (hardware threads) reported by
os.cpu_count(), not physical cores: seeCPU_COUNT. On a host with too few logical CPUs,auto/maxresolve to a single job andJobCountlogs a warning that execution will be sequential.The resolved value is stored as an
intinctx.meta[click_extra.context.JOBS].Warning
JobsOptiononly resolves and publishes the job count: it does not drive any concurrency by itself. Pass it torun_jobs()(which reads the resolvedctx.meta[click_extra.context.JOBS]count), or read that value yourself and act on it.- validate_jobs(ctx, param, value)[source]
Validate the resolved job count and store it in context metadata.
JobCounthas already resolved anyauto/maxkeyword to an integer by the time this runs. A value of0disables parallelism: it is rounded up to1(sequential execution) with a warning. Negative values are likewise clamped to1, and a count above the available cores is honored but warned about. The resolved count is then logged at info level next to the host’s logical CPU count (CPU_COUNT), so a CLI’s parallelism is visible under--verbosity INFO.- Return type:
- click_extra.execution.run_jobs(func, items, *, jobs=None)[source]
Run
funcoveritems, parallelized per the resolved--jobscount.The worker count is taken from
jobswhen given, else from the active command’sJobsOptionvalue (ctx.meta[click_extra.context.JOBS]), else1. With a single worker (or at most one item) the items run sequentially and lazily, so a caller can stop early on the first result (for example to abort on the first failure); otherwise they run in a thread pool. Either way results are yielded in submission order, likemap().The pool is thread-based, which suits the I/O- and subprocess-bound work CLI tools usually parallelize (each child releases the GIL). The count is a number of logical CPUs: see
CPU_COUNT.- Parameters:
func (
Callable[[TypeVar(T)],TypeVar(R)]) – Called once per item; its return value is yielded.items (
Iterable[TypeVar(T)]) – The work items. Materialized up front to size the pool.jobs (
int|None) – Override the worker count instead of reading it from the context.1or fewer forces sequential execution.
- Return type:
- Returns:
An iterator over
func’s results, in the order ofitems.
- class click_extra.execution.TimerOption(param_decls=None, default=False, expose_value=False, is_eager=True, help='Measure and print elapsed execution time.', **kwargs)[source]
Bases:
ExtraOptionA pre-configured option that is adding a
--time/--no-timeflag to print elapsed time at the end of CLI execution.The start time is made available in the context in
ctx.meta[click_extra.context.START_TIME].- print_timer()[source]
Compute and print elapsed execution time.
Always prints, even when a sibling eager option (
--version,--show-params,--show-config…) short-circuited the command body viactx.exit(). That makes--timea usable probe for the cost of Click Extra’s own machinery (option parsing, config loading, eager callbacks), not just user command bodies.- Return type:
- init_timer(ctx, param, value)[source]
Set up the execution-timer machinery for the current invocation.
Captures
time.perf_counter()as the start time, stores it onctx.metaunderclick_extra.context.START_TIME, and queuesprint_timer()as a context-close callback so the elapsed duration is printed even when a sibling eager option (--version,--show-params…) short-circuits the command body.Renamed from
register_timer_on_closeto align with theinit_<system>convention shared withinit_formatterandinit_sort.- Return type:
- class click_extra.execution.ZeroExitOption(param_decls=None, default=False, expose_value=False, is_flag=True, help='Always exit with a status code of 0, even when problems are found.', **kwargs)[source]
Bases:
ExtraOptionA pre-configured
-0/--zero-exitoption flag.Follows the convention popularized by linters and static analysers, which exit with a non-zero code whenever they report findings so that automation can gate on it. Setting this flag flips that behavior: the CLI returns
0as long as it ran to completion, reserving non-zero codes for actual execution failures.The resolved value is stored in
ctx.meta[click_extra.context.ZERO_EXIT], aligning with every other Click Extra option’s per-invocation context-meta storage pattern.Warning
This option is a placeholder: it does not alter the CLI’s exit code by itself. Downstream code must read
ctx.meta[click_extra.context.ZERO_EXIT]and act on it.- set_zero_exit(ctx, param, value)[source]
Store the resolved zero-exit flag on the context’s
metadict.Read via
click_extra.context.get(ctx, click_extra.context.ZERO_EXIT).- Return type: