How Click parses: contexts and parameter order

Click · Expert · 13 min

Subtle behaviours, like which callback sees which value or why defaults misbehave, follow from this pipeline.

When cli.main(args) runs:

  • make_context creates a Context (applying context_settings) and calls parse_args.
  • Parsing. Click's parser (modelled on optparse) splits the argument list into options and positional values according to each parameter's declaration (nargs, flags, -- handling). For groups, parsing stops at the first positional that names a subcommand; the rest is kept in ctx.protected_args/ctx.args for the child.
  • Processing. Each parameter's handle_parse_result resolves the value from its source in precedence order (command line, environment, default_map, default; an option with prompt= asks instead of silently using the last two), converts it with its type, then runs its callback, and stores it in ctx.params. Parameters are processed in the order the user typed them on the command line, with eager parameters first and unspecified ones after. That is why callbacks should not assume another parameter is already in ctx.params.
  • ctx.get_parameter_source(name) reports where a value came from (COMMANDLINE, ENVIRONMENT, DEFAULT, DEFAULT_MAP, PROMPT).
  • invoke. The command callback is called with **ctx.params. For groups, the group callback runs, then the subcommand's context is created as a child (parent=ctx) and the process repeats.

Command.to_info_dict(ctx) dumps the whole structure as data, which tools use to generate documentation. Knowing this pipeline explains things like defaults bypassing callbacks during completion (resilient_parsing) and why options must precede subcommands.

Example

import click
from click.core import ParameterSource

seen = []
def trace(ctx, param, value):
    seen.append(param.name)
    return value

@click.command()
@click.option("--a", default=1, callback=trace)
@click.option("--b", default=2, callback=trace, envvar="B_VAL")
@click.option("--c", default=3, callback=trace)
@click.pass_context
def cmd(ctx, a, b, c):
    for name in ("a", "b", "c"):
        click.echo(f"{name}={ctx.params[name]} from {ctx.get_parameter_source(name).name}")

cmd.main(["--c", "30", "--a", "10"], standalone_mode=False)
# a=10 from COMMANDLINE
# b=2 from DEFAULT
# c=30 from COMMANDLINE
print(seen)   # ['c', 'a', 'b']  (command-line order first, then the rest)

Remember

  • make_context -> parse -> process each parameter -> invoke callback
  • Parameters are processed in command-line order (eager first), unspecified ones last
  • get_parameter_source tells where each value came from

Watch out for

  • Writing a callback for --b that reads ctx.params['a'], which may not be set yet.

Check yourself

  1. A user runs `cmd --c 30 --a 10`. In which order are the callbacks for a, b and c called?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: click.palletsprojects.com/en/stable/advanced/