Multi-value options override the value returned from `callback` when not all specified

Aperta
#2,786 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

  • #3261 di @Dhi13man — chiusa senza merge
  • #3485 di @sjh9714 — chiusa senza merge
  • #3529 di @dhaval2025 — chiusa senza merge
  • #3539 di @yzhkali — chiusa senza merge
  • #3543 di @Xavrir — chiusa senza merge
  • #3545 di @viraatdas — chiusa senza merge
  • #3567 di @vincere-mori — chiusa senza merge
  • #3605 di @lxingy3 — chiusa senza merge

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
45/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
python
Ambito
cli

Direzione di ricerca

Inizia eseguendo il riproduttore bad.py con --fetch e con --custom seguito da --fetch. Leggi il comportamento di Option.handle_parse_result di Click e l'elaborazione dei callback per le opzioni che condividono un nome di parametro. Il lavoro è completato quando il callback di fetch restituisce il valore recuperato usando --fetch da solo, senza richiedere un callback su --custom né invocare il fetch esterno più di una volta.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

parsing

I discovered this when I wanted to add a flag_value-style option to shortcut a custom str parameter option.

I want to have 2 options:

  1. one that accepts a parameter of any str value like the name of a file or something
  2. another that is a flag that fetches the value from a remote server instead of me specifying it

Since these both represent the same thing, my initial thought was just to pass them under the same parameter name to the function, which seems to almost work.

the problem seems to be that if the "custom" value is not specified while the "flag" option is set, then click will use the flag_value from the "flag" as the default value for the "custom" value.

And click seems to process unspecified options last (which I can see why this was done), but since "custom" is not specified, it processes the default after the "flag" option has already set a value for the function parameter that they share.

# bad.py
import click

SENTINEL = "$_fetch"

def fetch():
    # some logic dependent on an external resource
    # return requests.get("...").json()["value")
    print('resource fetched')
    return "foo"

def callback(ctx, param, value):
    if value is SENTINEL:
        return fetch()
    return value

@click.command()
@click.option("--custom", "custom")
@click.option("--fetch", "custom", flag_value=SENTINEL, callback=callback)
def cli(custom):
    print(f'custom={custom}')

if __name__ == '__main__':
    cli()

results in the SENTINEL internal value being passed to the function when --fetch is specified alone:

# call with just --custom <arg>, get <arg>, as expected
$ python bad.py --custom bar
custom=bar

# call with just --fetch, get SENTINEL, the internal value that should not ever appear in the values
$ python bad.py --fetch
resource fetched
custom=$_fetch

# call with --custom <arg> first and --fetch after, get the fetched resource, as expected
$ python bad.py --custom bar --fetch
resource fetched
custom=foo

It's that last use-case that really rubs the dirt in; paradoxically, I have to specify the longer option I was trying to avoid using in order to get my "shortcut" flag option to work.

However, we can use a workaround to fix it, sort of, by passing the same callback to both options:

# bad.py
# ..
@click.option("--custom", "custom", callback=callback)
@click.option("--fetch", "custom", flag_value=SENTINEL, callback=callback)
# ..

results in fetch being called twice when --fetch is the last specified option:

# call with just --custom <arg>, get <arg>, as expected
$ python bad.py --custom bar
custom=bar

# call with just --fetch, get foo, but the external resource is pinged twice
$ python bad.py --fetch
resource fetched
resource fetched
custom=foo

# call with --custom <arg> first and --fetch after, get the fetched resource, as expected, resource is pinged once
$ python bad.py --custom bar --fetch
resource fetched
custom=foo

I could have some caching mechanism to avoid the repeated call to the external resource, but that should not be necessary, I should not have to add the callback argument to both options.

I can write a custom Option class that resolved this though:

# good.py
import click

class FixOption(click.Option):
    def handle_parse_result(self, ctx, opts, args):
        val = super().handle_parse_result(ctx, opts, args)
        # even though opts is type-hinted as non-mutable, this still works
        opts[self.name] = val[0]
        return val

SENTINEL = "$_fetch"

def fetch():
    # some logic dependent on an external resource
    # return requests.get("...").json()["value")
    print('resource fetched')
    return "foo"

def callback(ctx, param, value):
    if value is SENTINEL:
        return fetch()
    return value

@click.command()
@click.option("--custom", "custom")
@click.option("--fetch", "custom", flag_value=SENTINEL, callback=callback, cls=FixOption)
def cli(custom):
    print(f'custom={custom}')

if __name__ == '__main__':
    cli()

But I'd prefer not to have to keep a whole class around just for this, especially when it does work without one, just only if I pass both my options.

Environment:

  • Python version: 3.12.5
  • Click version: 8.1.7
Lingua principale
Python
Stelle
17.8k
Fork
3.3k
Merge medio
3g 3h
PR unite (30g)
13

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di pallets/click

Tutte le issue di pallets/click

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.