2014-09-02 14 views
19

Zrobiłem tyle badań, jak to możliwe, ale nie znalazłem najlepszy sposób, aby niektóre argumenty cmdline konieczne tylko pod pewnymi warunkami, w tym przypadku tylko wtedy, gdy inne argumenty zostały podane. Oto, co chcę robić w bardzo podstawowym poziomie:Python Argparse warunkowo wymagane argumenty

p = argparse.ArgumentParser(description='...') 
p.add_argument('--argument', required=False) 
p.add_argument('-a', required=False) # only required if --argument is given 
p.add_argument('-b', required=False) # only required if --argument is given 

Z tego co widziałem, inni ludzie wydają się po prostu dodaj swój czek na koniec:

if args.argument and (args.a is None or args.b is None): 
    # raise argparse error here 

Czy istnieje sposób, aby zrobić to natywnie w pakiecie argparse?

+1

Czy spojrzałeś na "argumenty argparse"? Pozwalają ci na takie rzeczy jak '$ git commit ' lub '$ git merge . –

+0

Joel, dzięki za komentarz. Widziałem aspekt akapitu argparse, ale miałem nadzieję, że zrobię to bez argumentów pozycyjnych. Jeśli to jedyny sposób, ale to nie jest wielka sprawa – DJMcCarthy12

+0

Czy '--a' i' --b' mogą być podawane niezależnie? – hpaulj

Odpowiedz

9

Możesz zaimplementować kontrolę, podając niestandardową akcję dla --argument, która pobiera dodatkowy argument ze słowem kluczowym, aby określić, które inne działania powinny być wymagane, jeśli zostanie użyty --argument.

import argparse 

class CondAction(argparse.Action): 
    def __init__(self, option_strings, dest, nargs=None, **kwargs): 
     x = kwargs.pop('to_be_required', []) 
     super(CondAction, self).__init__(option_strings, dest, **kwargs) 
     self.make_required = x 

    def __call__(self, parser, namespace, values, option_string=None): 
     for x in self.make_required: 
      x.required = True 
     try: 
      return super(CondAction, self).__call__(parser, namespace, values, option_string) 
     except NotImplementedError: 
      pass 

p = argparse.ArgumentParser() 
x = p.add_argument("--a") 
p.add_argument("--argument", action=CondAction, to_be_required=[x]) 

Dokładna definicja CondAction będzie zależeć od tego, co dokładnie, --argument powinien robić. Ale na przykład, jeśli --argument jest typowym działaniem typu "przyjmuj jeden argument" i "zapisz", wystarczy, że odziedziczysz po argparse._StoreAction.

Na przykład parsera, możemy zaoszczędzić odniesienie do opcji wewnątrz opcją --argument--a, a kiedy --argument widać w wierszu poleceń, ustawia flagę required na --a do True. Po przetworzeniu wszystkich opcji, argparse sprawdza, czy została ustawiona opcja oznaczona jako wymagana.

+1

Jak napisano, nie działa, ponieważ 'Action .__ call__' zwraca błąd' not implement'. Ale podstawowa idea poprawiania "wymaganego" atrybutu 'x' powinna działać. – hpaulj

+0

Dobra uwaga. Jeśli dziedziczysz po argparse.Action', nie ma potrzeby wywoływania (niezatwierdzonej) klasy macierzystej '__call__'. Jeśli dziedziczysz z jednej z innych podklas "Action", powinieneś. (Jako kompromis, zredagowałem odpowiedź, aby opuścić wywołanie superklasy, ale złapałem i zignorowałem 'NotImplementedError'.) – chepner

3

Twój test sprawdzania końcowego jest w porządku, zwłaszcza jeśli testowanie domyślne z is None odpowiada Twoim potrzebom.

http://bugs.python.org/issue11588'Add "necessarily inclusive" groups to argparse' analizuje implementację takich testów za pomocą mechanizmu groups (generalizacja mutuall_exclusive_groups).

Pisałem zestaw UsageGroups które wdrażają testy jak xor (wykluczają się wzajemnie), and, or i not. Myślałem, że są one wszechstronne, ale nie byłem w stanie wyrazić waszej sprawy w kategoriach tych operacji. (wygląda na to, że potrzebuję nand - nie i zobacz poniżej)

Ten skrypt używa niestandardowej klasy Test, która zasadniczo implementuje test końcowy. seen_actions to lista akcji, które zostały przeanalizowane.

class Test(argparse.UsageGroup): 
    def _add_test(self): 
     self.usage = '(if --argument then -a and -b are required)' 
     def testfn(parser, seen_actions, *vargs, **kwargs): 
      "custom error" 
      actions = self._group_actions 
      if actions[0] in seen_actions: 
       if actions[1] not in seen_actions or actions[2] not in seen_actions: 
        msg = '%s - 2nd and 3rd required with 1st' 
        self.raise_error(parser, msg) 
      return True 
     self.testfn = testfn 
     self.dest = 'Test' 
p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter) 
g1 = p.add_usage_group(kind=Test) 
g1.add_argument('--argument') 
g1.add_argument('-a') 
g1.add_argument('-b') 
print(p.parse_args()) 

Przykładowe wyjście jest:

1646:~/mypy/argdev/usage_groups$ python3 issue25626109.py --arg=1 -a1 
usage: issue25626109.py [-h] [--argument ARGUMENT] [-a A] [-b B] 
         (if --argument then -a and -b are required) 
issue25626109.py: error: group Test: argument, a, b - 2nd and 3rd required with 1st 

usage komunikaty o błędach i nadal wymaga pracy. I nie robi niczego, czego nie może przeprowadzić test po parsowaniu.


Twój test wzbudza błąd, jeśli (argument & (!a or !b)). Odwrotnie, dozwolone jest !(argument & (!a or !b)) = !(argument & !(a and b)).Dodając test nand do moich UsageGroup zajęciach, mogę realizować swoje sprawy jak:

p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter) 
g1 = p.add_usage_group(kind='nand', dest='nand1') 
arg = g1.add_argument('--arg', metavar='C') 
g11 = g1.add_usage_group(kind='nand', dest='nand2') 
g11.add_argument('-a') 
g11.add_argument('-b') 

Stosowanie się (za pomocą !() oznaczyć test 'NAND'):

usage: issue25626109.py [-h] !(--arg C & !(-a A & -b B)) 

myślę, że to jest najkrótszy i najczystszy sposób wyrażenia tego problemu za pomocą grup ogólnego przeznaczenia.


W moich testach, wejść, które są z powodzeniem zanalizować:

'' 
'-a1' 
'-a1 -b2' 
'--arg=3 -a1 -b2' 

Ones, które powinny podnieść błędy to:

'--arg=3' 
'--arg=3 -a1' 
'--arg=3 -b2' 
0

Do http://bugs.python.org/issue11588 jest rozwiązany, to bym po prostu użyć nargs:

p = argparse.ArgumentParser(description='...') 
p.add_argument('--arguments', required=False, nargs=2, metavar=('A', 'B')) 

W ten sposób, jeśli ktoś dostarczy --arguments, będzie miał 2 wartości.

Może jego wynik w CLI jest mniej czytelny, ale kod jest znacznie mniejszy. Możesz to naprawić dzięki dobrym dokumentom/pomocy.

-1

Na argumenty wymyśliłem szybkie i brudne rozwiązanie. Założenia: (1) „help” powinien wyświetlić pomoc i nie narzekają wymaganego argumentu i (2) mamy do analizowania sys.argv

p = argparse.ArgumentParser(...) 
p.add_argument('-required', ..., required = '--help' not in sys.argv) 

ten może być łatwo modyfikowane, aby dopasować konkretne ustawienia. Dla wymaganych positionals (co stanie się niepotrzebny, jeśli np „help” jest podana w wierszu poleceń) Mam wymyślić, co następuje: [positionals nie pozwalają na arg required=... kluczowego!]

p.add_argument('pattern', ..., narg = '+' if '--help' not in sys.argv else '*') 

zasadniczo zmienia to liczbę wymaganych wystąpień "wzorca" w wierszu poleceń z jednego lub więcej na zero lub więcej w przypadku określenia "--help".

+0

Nie mam nic przeciwko, ale chciałbym się dowiedzieć dlaczego, szczególnie od innej odpowiedzi https://stackoverflow.com/a/44210638/26083 (z upvote 5 w tym momencie) zasadniczo robi to samo – haavee

7

Od pewnego czasu szukam prostej odpowiedzi na tego rodzaju pytanie. Wszystko, co musisz zrobić, to sprawdzić, czy '--argument' jest w sys.argv, więc w zasadzie dla próbki kodu można po prostu zrobić:

import argparse 
import sys 

if __name__ == '__main__': 
    p = argparse.ArgumentParser(description='...') 
    p.add_argument('--argument', required=False) 
    p.add_argument('-a', required='--argument' in sys.argv) #only required if --argument is given 
    p.add_argument('-b', required='--argument' in sys.argv) #only required if --argument is given 
    args = p.parse_args() 

Ten sposób required odbiera albo True lub False w zależności od tego, czy użytkownik stosowany --argument. Już przetestowane, wydaje się działać i gwarantuje, że -a i mają niezależne zachowanie między sobą.

Powiązane problemy