Skip to content

Bump click upper bound to <8.4#7324

Open
GeigerJ2 wants to merge 13 commits into
aiidateam:mainfrom
GeigerJ2:deps/click-8.3
Open

Bump click upper bound to <8.4#7324
GeigerJ2 wants to merge 13 commits into
aiidateam:mainfrom
GeigerJ2:deps/click-8.3

Conversation

@GeigerJ2
Copy link
Copy Markdown
Collaborator

@GeigerJ2 GeigerJ2 commented Apr 16, 2026

While exploring some of the stack proposed by potential contributors to GSoC, I noticed that some of the AI libraries require click 8.3. Hence, I thought why not try and loosen our pin.

At least local CLI tests seem to run through. Just opening the PR for the full CI pipeline.

UPDATED PR DESCRIPTION

Bump click upper bound from <8.3 to <8.4, fixing three breaking changes introduced by click 8.3's new UNSET sentinel (an enum member that distinguishes "no value provided" from "explicitly None"). All fixes are backwards-compatible with click 8.2 (identical runtime behavior).

1. Config-file values re-prompted instead of accepted

Click 8.3 added ParameterSource.DEFAULT_MAP to the prompt trigger condition in Option.consume_value. Values provided via --config file.yaml (which populate ctx.default_map) were silently accepted in 8.2 but now trigger prompt_for_value, aborting in non-interactive environments like CI.

InteractiveOption.prompt_for_value and TemplateInteractiveOption.prompt_for_value now check ctx.default_map first and return the config-file value immediately, before any interactive/non-interactive branching. On click 8.2 this code path is never reached (click doesn't call prompt_for_value for DEFAULT_MAP sources), so behavior is unchanged.

Files: src/aiida/cmdline/params/options/interactive.py

2. ctx.params pre-populated with truthy UNSET sentinels

Click 8.3 changed Parameter.consume_value from opts.get(self.name) to opts.get(self.name, UNSET), and pre-populates ctx.params with UNSET for unprocessed parameters. The sentinel is truthy (bool(UNSET) is True), which broke interactive_default in the transport CLI: it reads non_interactive, user, and computer from ctx.params to compute defaults, and UNSET passed boolean/None checks it shouldn't have.

New module-level helper _get_processed_param(ctx, name, default) checks ctx.get_parameter_source(name) before reading ctx.params. Returns the safe default if the parameter hasn't been processed yet. On click 8.2, get_parameter_source already returns None for unprocessed params and the actual source for processed ones, so the helper produces the same values as the old direct ctx.params access.

Files: src/aiida/transports/cli.py

3. ! on required options silently accepted as None

Click 8.3 changed Parameter.value_is_missing from value is None to value is UNSET. InteractiveOption translates the ! prompt input to None and relied on super().process_value raising MissingParameter for required options, triggering a re-prompt. With 8.3, None passes the missing-value check and is silently accepted.

The fix checks requiredness explicitly before delegating to super(), using self.is_required(ctx) (from ConditionalOption) instead of the bare self.required flag. This also fixes a pre-existing bug: ConditionalOption sets self.required = False when required_fn is provided, so the old raise-and-catch path via super().process_value never re-prompted for required_fn-required options, even on click 8.2. The new explicit check handles both required=True and required_fn correctly.

Files: src/aiida/cmdline/params/options/interactive.py

Other files

  • tests/cmdline/params/options/test_interactive.py: three new tests covering required_fn re-prompt on !, default_map precedence (interactive + non-interactive), and default_map overriding contextual_default

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 94.44444% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.26%. Comparing base (06968d4) to head (d0d12d2).

Files with missing lines Patch % Lines
src/aiida/cmdline/params/options/interactive.py 88.89% 1 Missing ⚠️
src/aiida/cmdline/utils/common.py 92.31% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7324      +/-   ##
==========================================
- Coverage   80.26%   80.26%   -0.00%     
==========================================
  Files         577      577              
  Lines       45497    45522      +25     
==========================================
+ Hits        36514    36532      +18     
- Misses       8983     8990       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@agoscinski
Copy link
Copy Markdown
Collaborator

the CI denied your proposal

@GeigerJ2
Copy link
Copy Markdown
Collaborator Author

GeigerJ2 commented Apr 17, 2026

the CI denied your proposal

image

@GeigerJ2
Copy link
Copy Markdown
Collaborator Author

the CI denied your proposal

Now it says yes! Just gotta actually look at the vibe-coded code myself 😅

{5B0226D1-6778-4B13-B3C0-4C0BDA6503DB}

As of click 8.3, `Option.consume_value` re-prompts for options whose
value is sourced from `default_map` when the option has a prompt and
is required. In non-interactive mode, the prompt call falls through
to `get_default`, which for `InteractiveOption` returns the
`contextual_default` — silently overwriting the value provided via a
config file.

This broke `verdi setup --non-interactive --config profile.yaml`, as
the `db_username`, `db_name`, and other config-sourced values were
replaced by the auto-computed defaults (e.g. `aiida_qs_<user>_<hash>`),
causing connection failures against postgres roles that do not exist.

Override `InteractiveOption.prompt_for_value` so that in
non-interactive mode it returns the `default_map` value if one is
present, falling back to `get_default` otherwise. This restores the
pre-8.3 behaviour where config-file values take precedence over the
contextual default.
`click >= 8.3` pre-populates `ctx.params` with an `UNSET` sentinel for
parameters that have not yet been processed. The sentinel is truthy,
so the pattern

    user = ctx.params.get('user', None) or orm.User.collection.get_default()

no longer falls back to the default user: it yields `UNSET`, which is
later dereferenced as `user.pk` and raises

    AttributeError: 'Sentinel' object has no attribute 'pk'

This breaks `verdi computer configure core.ssh` when the option order
causes the `user`, `computer`, or `non_interactive` parameters to be
looked up before they have been processed (e.g. from a contextual
default of another option), which surfaces during the `test-install`
CI job.

Normalise the relevant context params via an explicit `isinstance`
check, returning `None` for any value that is not of the expected
type (including the `UNSET` sentinel).
The previous fix only returned config-file values from
`prompt_for_value` when running in non-interactive mode
(`-n`/`--non-interactive`). But `verdi computer configure` in CI runs
without `-n`, relying on `--config` alone.

Click 8.3 re-triggers `prompt_for_value` for options whose value was
sourced from `default_map`. In interactive mode this fell through to the
actual prompt, which aborted in CI (no terminal on stdin).

Move the `default_map` check before the `is_interactive` gate so
config-file values always short-circuit prompting, restoring
pre-click-8.3 behavior.
Click 8.3 changed `value_is_missing` to only flag the `UNSET` sentinel,
not `None`. This broke the `!` (ignore default) flow for required
options: `process_value` set the value to `None` and relied on
`super().process_value` to raise `MissingParameter`, but the
missing-value check no longer fires for `None`.

Handle required options explicitly before delegating to `super()`: if
the option is required and we are in interactive mode, print the error
and re-prompt immediately.
GeigerJ2 and others added 4 commits April 21, 2026 16:55
Use `is_required(ctx)` instead of bare `self.required` so
`required_fn` callbacks are respected when re-prompting for
`!` on required options.

Extract `_get_processed_param` as a standalone module-level
helper in `transports/cli.py` instead of a nested closure.

Add `default_map` guard to `TemplateInteractiveOption` to
match the fix already applied to `InteractiveOption`.

Add tests for `required_fn` re-prompt, `default_map`
precedence (interactive and non-interactive), and
`default_map` overriding `contextual_default`.
Click 8.3 sets `get_parameter_source` to `DEFAULT` even
when the actual value in `ctx.params` is the `UNSET`
sentinel (e.g. `USER` option whose `default=UNSET`).
Checking source alone was not sufficient: `user` passed
the source check but held `UNSET`, a truthy non-User
object that skipped the `or` fallback and crashed on
`.pk` access.

Add an explicit `value is UNSET` guard after the source
check. Import the sentinel with a try/except fallback
for click < 8.3 compatibility.
@GeigerJ2 GeigerJ2 marked this pull request as ready for review April 22, 2026 06:58
@GeigerJ2 GeigerJ2 removed the request for review from agoscinski April 22, 2026 06:59
Extract `resolve_param` from `transports/cli.py` into
`cmdline.utils.common` and apply it to every `ctx.params`
read that runs during parameter processing (contextual
defaults, prompt/required callbacks, validators, type
converters).

In click 8.3, unprocessed parameters are pre-populated
with a truthy UNSET sentinel. Raw `ctx.params` reads in
callbacks could see UNSET instead of None, silently
passing boolean/None checks or crashing on attribute
access.

Guarded call sites:
- `InteractiveOption.is_interactive` (non_interactive)
- `interactive_default` (non_interactive, user, computer)
- `is_on_computer` (on_computer)
- `validate_label_uniqueness` (computer, on_computer)
- `get_job_resource_cls` (scheduler)
- `MpirunCommandParamType.convert` (scheduler)

Also add a test for `TemplateInteractiveOption` with
`default_map`.
`test_validate_label_uniqueness` constructs a `click.Context`
manually and sets `ctx.params` directly. After the switch to
`resolve_param`, the helper checks `get_parameter_source`
before reading `ctx.params`, so the test must also register
sources for the parameters it provides.
Copy link
Copy Markdown
Collaborator

@danielhollas danielhollas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooof, the changes here are quite subtle. 😨

I can't say I went deep enough to fully understand everything (and I haven't looked much at the added tests), but as long as some manual testing is done to verify that things work (especially around the interactive use which seems to impacted the most?), this looks fine. Thanks!


ctx = click.Context(cmd_code.setup_code)
ctx.params = {'on_computer': False}
ctx.set_parameter_source('on_computer', click.core.ParameterSource.COMMANDLINE)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this required?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants