Argparse: wymagany argument „y”, jeśli występuje „x”

118

Mam następujące wymagania:

./xyifier --prox --lport lport --rport rport

dla argumentu prox używam action = 'store_true', aby sprawdzić, czy jest obecny, czy nie. Nie potrzebuję żadnych argumentów. Ale jeśli ustawione jest --prox, potrzebuję również rport i lport. Czy istnieje łatwy sposób na zrobienie tego za pomocą argparse bez pisania niestandardowego kodowania warunkowego.

Więcej kodu:

non_int.add_argument('--prox', action='store_true', help='Flag to turn on proxy')
non_int.add_argument('--lport', type=int, help='Listen Port.')
non_int.add_argument('--rport', type=int, help='Proxy port.')
asudhak
źródło
Podłączam, ale chciałem wspomnieć o mojej bibliotece Joffrey . Pozwala na zrobienie tego, czego chce to pytanie, na przykład bez zmuszania Cię do samodzielnego sprawdzania poprawności wszystkiego (jak w zaakceptowanej odpowiedzi) lub polegania na luki w zabezpieczeniach (jak w przypadku drugiej pod względem liczby głosów odpowiedzi).
MI Wright
Dla każdego, kto tu
przyjedzie

Odpowiedzi:

120

Nie, w argparse nie ma opcji tworzenia wzajemnie obejmujących się zestawów opcji.

Najprostszym sposobem rozwiązania tego problemu byłoby:

if args.prox and (args.lport is None or args.rport is None):
    parser.error("--prox requires --lport and --rport.")
urodzony
źródło
2
Tak właśnie skończyłem
asudhak
20
Dziękuję za parser.errormetodę, tego szukałem!
MarSoft
7
nie powinieneś używać „lub”? w końcu potrzebujesz obu argumentów if args.prox and (args.lport is None or args.rport is None):
yossiz74
1
Zamiast tego args.lport is Nonemożesz po prostu użyć not args.lport. Myślę, że jest trochę bardziej pytoniczny.
CGFoX
7
To powstrzymałoby cię od ustawiania --lportlub --rportdo 0, co może być prawidłowym wejściem do programu.
data urodzenia
53

Mówisz o warunkowo wymaganych argumentach. Podobnie jak @borntyping powiedział, że możesz sprawdzić błąd i zrobić parser.error(), lub możesz po prostu zastosować wymaganie związane z --proxdodaniem nowego argumentu.

Prostym rozwiązaniem na przykład może być:

non_int.add_argument('--prox', action='store_true', help='Flag to turn on proxy')
non_int.add_argument('--lport', required='--prox' in sys.argv, type=int)
non_int.add_argument('--rport', required='--prox' in sys.argv, type=int)

W ten sposób requiredotrzymuje albo Truealbo Falsejako stosowane w zależności od tego, czy użytkownik --prox. To również gwarantuje, że -lporti -rportmają niezależne zachowanie między sobą.

Mira
źródło
8
Zauważ, że ArgumentParsermoże być używany do analizowania argumentów z listy innej niż sys.argv, w takim przypadku zakończy się to niepowodzeniem.
BallpointBen
Również to się nie powiedzie, jeśli --prox=<value>zostanie użyta składnia.
fnkr
11

Co powiesz na użycie parser.parse_known_args()metody, a następnie dodanie argumentów --lporti --rportargumentów jako wymaganych argumentów, jeśli --proxjest obecny.

# just add --prox arg now
non_int = argparse.ArgumentParser(description="stackoverflow question", 
                                  usage="%(prog)s [-h] [--prox --lport port --rport port]")
non_int.add_argument('--prox', action='store_true', 
                     help='Flag to turn on proxy, requires additional args lport and rport')
opts, rem_args = non_int.parse_known_args()
if opts.prox:
    non_int.add_argument('--lport', required=True, type=int, help='Listen Port.')
    non_int.add_argument('--rport', required=True, type=int, help='Proxy port.')
    # use options and namespace from first parsing
    non_int.parse_args(rem_args, namespace = opts)

Pamiętaj również, że możesz podać przestrzeń nazw optswygenerowaną po pierwszym przeanalizowaniu podczas analizowania pozostałych argumentów za drugim razem. W ten sposób, po zakończeniu całego parsowania, będziesz mieć jedną przestrzeń nazw ze wszystkimi opcjami.

Wady:

  • Jeśli --proxnie jest obecny, pozostałe dwie opcje zależne nie są nawet obecne w przestrzeni nazw. Chociaż w oparciu o Twój przypadek użycia, jeśli --proxnie jest obecny, to, co dzieje się z innymi opcjami, nie ma znaczenia.
  • Trzeba zmodyfikować komunikat o użytkowaniu, ponieważ parser nie zna pełnej struktury
  • --lporti --rportnie pokazuj się w wiadomościach pomocy
Aditya Sriram
źródło
5

Czy używasz lportkiedy proxnie jest ustawione. Jeśli nie, dlaczego nie zrobić lporti rportargumentować prox? na przykład

parser.add_argument('--prox', nargs=2, type=int, help='Prox: listen and proxy ports')

To oszczędza użytkownikom pisania. Jest to równie łatwe do przetestowania, if args.prox is not None:jak if args.prox:.

hpaulj
źródło
1
Dla kompletności przykładu, gdy twoje nargs są> 1, otrzymasz listę w parsowanym argumencie itp., Do której możesz się odnieść w zwykły sposób. Na przykład a,b = args.prox, a = args.prox[0]itd
Dannid
1

Przyjęta odpowiedź zadziałała dla mnie świetnie! Ponieważ cały kod jest uszkodzony bez testów, oto jak przetestowałem zaakceptowaną odpowiedź. parser.error()nie zgłasza argparse.ArgumentErrorbłędu, zamiast tego kończy proces. Musisz przetestować SystemExit.

z pytest

import pytest
from . import parse_arguments  # code that rasises parse.error()


def test_args_parsed_raises_error():
    with pytest.raises(SystemExit):
        parse_arguments(["argument that raises error"])

z napisami

from unittest import TestCase
from . import parse_arguments  # code that rasises parse.error()

class TestArgs(TestCase):

    def test_args_parsed_raises_error():
        with self.assertRaises(SystemExit) as cm:
            parse_arguments(["argument that raises error"])

zainspirowany: Używanie unittest do testowania argparse - błędy wyjścia

Daniel Butler
źródło