it-swarm.com.de

Python Argparse benötigte bedingt Argumente

Ich habe so viel recherchiert wie möglich, aber ich habe nicht den besten Weg gefunden, um bestimmte Cmdline-Argumente nur unter bestimmten Bedingungen erforderlich zu machen, in diesem Fall nur, wenn andere Argumente angegeben wurden. Folgendes möchte ich auf einer sehr einfachen Ebene tun:

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

Nach allem, was ich gesehen habe, scheinen andere Leute am Ende nur einen eigenen Check hinzuzufügen:

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

Gibt es eine Möglichkeit, dies nativ im argparse-Paket zu tun?

21
DJMcCarthy12

Ich habe seit einiger Zeit nach einer einfachen Antwort auf diese Art von Frage gesucht. Alles, was Sie tun müssen, ist zu prüfen, ob '--argument' in sys.argv ist. Im Grunde könnten Sie also für Ihr Codebeispiel Folgendes tun:

import argparse
import sys

if __== '__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()

Auf diese Weise erhält required entweder True oder False, je nachdem, ob der Benutzer --argument verwendet wird. Bereits getestet, scheint zu funktionieren und garantiert, dass -a und -b ein unabhängiges Verhalten untereinander haben.

19
Mira

Sie können eine Prüfung implementieren, indem Sie eine benutzerdefinierte Aktion für --argument bereitstellen, die ein zusätzliches Schlüsselwortargument erfordert, um anzugeben, welche anderen Aktionen erforderlich sind, wenn --argument verwendet wird.

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])

Die genaue Definition von CondAction hängt davon ab, was --argument genau tun soll. Wenn zum Beispiel --argument eine reguläre Aktion mit Take-One-Argument-and-save-it ist, dann reicht es aus, nur von argparse._StoreAction zu erben.

Im Beispielparser speichern wir einen Verweis auf die --a-Option innerhalb der --argument-Option. Wenn --argument in der Befehlszeile angezeigt wird, setzt er das required-Flag auf --a auf True. Nachdem alle Optionen verarbeitet wurden, überprüft argparse, ob alle als erforderlich gekennzeichneten Optionen gesetzt wurden.

10
chepner

Ihr Post-Parsing-Test ist in Ordnung, vor allem, wenn das Testen auf Standardwerte mit is None Ihren Anforderungen entspricht.

http://bugs.python.org/issue11588'Add "necessarily inclusive" groups to argparse' untersucht die Implementierung solcher Tests mit Hilfe des groups-Mechanismus (eine Verallgemeinerung von mehreren Exklusivgruppen).

Ich habe einen Satz von UsageGroups geschrieben, der Tests wie xor (gegenseitig ausschließend), and, or und not implementiert. Ich dachte, diese wären umfangreich, aber ich konnte Ihren Fall in Bezug auf diese Vorgänge nicht ausdrücken. (sieht aus wie ich nand brauche - nicht und siehe unten)

Dieses Skript verwendet eine benutzerdefinierte Test-Klasse, die im Wesentlichen Ihren Post-Parsing-Test implementiert. seen_actions ist eine Liste von Aktionen, die der Parse gesehen hat.

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())

Beispielausgabe ist:

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 und Fehlermeldungen müssen noch bearbeitet werden. Und es tut nichts, was der Post-Parsing-Test nicht kann.


Ihr Test löst einen Fehler aus, wenn (argument & (!a or !b)). Umgekehrt ist !(argument & (!a or !b)) = !(argument & !(a and b)) erlaubt. Durch Hinzufügen eines nand-Tests zu meinen UsageGroup-Klassen kann ich Ihren Fall folgendermaßen implementieren:

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')

Die Verwendung ist (mit !(), um einen "Nand" -Test zu markieren):

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

Ich denke, dies ist der kürzeste und klarste Weg, dieses Problem unter Verwendung allgemeiner Verwendungsgruppen auszudrücken.


In meinen Tests sind die erfolgreich analysierten Eingaben:

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

Diejenigen, die Fehler auslösen sollen, sind:

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

Dies ist wirklich die gleiche Antwort wie bei @Mira, aber ich wollte sie für den Fall zeigen, dass, wenn eine Option angegeben wird, ein zusätzliches Argument erforderlich ist:

Zum Beispiel, wenn --option foo ist gegeben, dann sind auch einige Argumente erforderlich, die nicht erforderlich sind, wenn --option bar ist gegeben:

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--option', required=True,
        help='foo and bar need different args')

    if 'foo' in sys.argv:
        parser.add_argument('--foo_opt1', required=True,
           help='--option foo requires "--foo_opt1"')
        parser.add_argument('--foo_opt2', required=True,
           help='--option foo requires "--foo_opt2"')
        ...

    if 'bar' in sys.argv:
        parser.add_argument('--bar_opt', required=True,
           help='--option bar requires "--bar_opt"')
        ...

Es ist nicht perfekt - zum Beispiel proggy --option foo --foo_opt1 bar ist nicht eindeutig, aber für das, was ich tun musste, ist es ok.

0
keithpjolley

Für Argumente habe ich eine schnelle Lösung wie diese gefunden. Annahmen: (1) '--help' sollte Hilfe anzeigen und sich nicht über das erforderliche Argument beschweren, und (2) wir analysieren sys.argv

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

Dies kann leicht an eine bestimmte Einstellung angepasst werden. Für erforderliche Positionsangaben (die nicht mehr benötigt werden, wenn beispielsweise '--help' in der Befehlszeile angegeben wird) habe ich Folgendes gefunden: [Positionsangaben erlauben kein required=... keyword arg!]

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

im Grunde genommen wird dadurch die Anzahl der erforderlichen Vorkommen von "Muster" in der Befehlszeile von "eins" oder "mehr" in "null" oder "mehr" umgewandelt, wenn "--help" angegeben wird.

0
haavee

Bis http://bugs.python.org/issue11588 gelöst ist, würde ich einfach nargs verwenden:

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

Wenn also jemand --arguments liefert, hat er 2 Werte.

Vielleicht ist das CLI-Ergebnis weniger lesbar, aber der Code ist viel kleiner. Sie können das mit guten Dokumenten/Hilfe beheben.

0
Yajo