Skip to content

_json_requested() misdetects combined --json/--no-json declarations and ignores explicit negation #212

Description

@codeforester

Summary

_json_requested(), the raw-argv heuristic run_app() uses to decide whether a pre-parse Click error should be rendered as a JSON envelope or plain text, does not understand combined boolean-flag declarations ("--json/--no-json") the way the framework's own _lifecycle_flag_declarations/_leading_output_flags do, and short-circuits to True whenever the option's default is True regardless of what the user actually typed.

Details

lib/python/base_cli/app.py:2900-2915:

def _json_requested(args: list[str], lifecycle_options: LifecycleOptions) -> bool:
    option = lifecycle_options.json
    if option is None:
        return False
    if option.default is True:
        return True
    ...
    declarations = tuple(declaration for declaration in option.param_decls if declaration.startswith(("-", "/")))
    return any(
        argument == declaration or argument.startswith(f"{declaration}=")
        for argument in args
        for declaration in declarations
    )

Combined declarations like "--debug/--no-debug" are a real, tested, documented pattern elsewhere in this exact framework (tests/test_lifecycle_options.py:134,648,1112), and nothing prevents a consumer from configuring LifecycleOptions.json the same way. But _json_requested never splits on / the way _lifecycle_flag_declarations (app.py:3011-3026) does for debug/quiet — it compares raw argv tokens against the unsplit string "--json/--no-json", which a user never types.

Reproduced directly against the current code:

opt = LifecycleOption('--json/--no-json', default=False)
_json_requested(['--json', '--nonexistent-flag'], LifecycleOptions(json=opt))
# -> False (should be True: the user explicitly passed --json)

opt2 = LifecycleOption('--json/--no-json', default=True)
_json_requested(['--no-json', '--nonexistent-flag'], LifecycleOptions(json=opt2))
# -> True (should be False: the user explicitly passed --no-json)

Impact

run_app() (app.py:2772-2784) uses this value as state.json_output, which determines how an unexpected pre-parse error is rendered (_show_unexpected_error/_emit_json_error vs. plain text). A consumer using the documented combined-declaration pattern for their JSON flag gets the wrong rendering whenever Click aborts parsing before the option callback runs (e.g. an unrelated unknown-option error): a script that explicitly requested --json gets a plain-text error instead of a JSON envelope, or a script that explicitly passed --no-json gets an unwanted JSON envelope anyway. This is a narrower follow-up to #139 (which fixed the larger "prose+JSON both land on stdout" problem by always buffering output when JSON is configured) — the remaining gap is specifically in how the buffered output gets rendered when Click never reaches the real option parser.

Suggested fix

Reuse _lifecycle_flag_declarations (already correct for debug/quiet) to split positive/negative declarations for the json option too, and check argv against the negative declarations before trusting option.default is True.

Metadata

Metadata

Assignees

Labels

bugSomething is not working

Type

No type

Projects

Status
Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions