Argparse: argument obligatoire 'y' si 'x' est présent

118

J'ai une exigence comme suit:

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

pour l'argument prox, j'utilise action = 'store_true' pour vérifier s'il est présent ou non. Je n'ai besoin d'aucun argument. Mais, si --prox est défini, j'ai également besoin de rport et de lport. Existe-t-il un moyen simple de faire cela avec argparse sans écrire de codage conditionnel personnalisé.

Plus de code:

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
la source
Brancher, mais je voulais mentionner ma bibliothèque joffrey . Vous permet de faire ce que cette question veut, par exemple, sans vous obliger à tout valider vous-même (comme dans la réponse acceptée) ou à vous fier à un hack d'échappatoire (comme dans la deuxième réponse la plus votée).
MI Wright
Pour tous ceux qui arrivent ici, une autre solution étonnante: stackoverflow.com/a/44210638/6045800
Tomerikoo

Réponses:

120

Non, il n'y a aucune option dans argparse pour créer des ensembles d'options mutuellement inclusifs .

La façon la plus simple de gérer cela serait:

if args.prox and (args.lport is None or args.rport is None):
    parser.error("--prox requires --lport and --rport.")
borntypage
la source
2
C'est ce que j'ai fini par faire
asudhak
20
Merci pour la parser.errorméthode, c'est ce que je cherchais!
MarSoft
7
ne devriez-vous pas utiliser 'ou'? après tout, vous avez besoin des deux if args.prox and (args.lport is None or args.rport is None):
arguments
1
Au lieu de args.lport is None, vous pouvez simplement utiliser not args.lport. Je pense que c'est un peu plus pythonique.
CGFoX
7
Cela vous empêcherait de définir --lportou --rportà 0, ce qui pourrait être une entrée valide pour le programme.
borntyping
53

Vous parlez d'avoir des arguments conditionnellement requis. Comme @borntyping l'a dit, vous pouvez vérifier l'erreur et le faire parser.error(), ou vous pouvez simplement appliquer une exigence liée au --proxmoment où vous ajoutez un nouvel argument.

Une solution simple pour votre exemple pourrait être:

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)

De cette façon requiredreçoit soit Trueou Falseselon que l'utilisateur est utilisé --prox. Cela garantit également cela -lportet -rportavoir un comportement indépendant entre eux.

Mira
la source
8
Notez que cela ArgumentParserpeut être utilisé pour analyser les arguments d'une liste autre que sys.argv, auquel cas cela échouerait.
BallpointBen
Cela échouera également si la --prox=<value>syntaxe est utilisée.
fnkr
11

Que diriez-vous d'utiliser la parser.parse_known_args()méthode et d'ajouter ensuite les arguments --lportet les --rportarguments requis si les arguments --proxsont présents.

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

Gardez également à l'esprit que vous pouvez fournir l'espace de noms optsgénéré après la première analyse tout en analysant les arguments restants la deuxième fois. De cette façon, à la fin, une fois l'analyse terminée, vous aurez un seul espace de noms avec toutes les options.

Désavantages:

  • Si --proxn'est pas présent, les deux autres options dépendantes ne sont même pas présentes dans l'espace de noms. Bien que basé sur votre cas d'utilisation, si --proxn'est pas présent, ce qui arrive aux autres options n'est pas pertinent.
  • Besoin de modifier le message d'utilisation car l'analyseur ne connaît pas la structure complète
  • --lportet --rportn'apparaissent pas dans le message d'aide
Aditya Sriram
la source
5

Utilisez-vous lportquand proxn'est pas défini. Sinon, pourquoi ne pas faire lportet rportarguments prox? par exemple

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

Cela évite à vos utilisateurs de taper. C'est aussi simple à tester if args.prox is not None:que if args.prox:.

hpaulj
la source
1
Par souci d'exhaustivité de l'exemple, lorsque votre nargs est> 1, vous obtiendrez une liste dans l'argument analysé, etc., que vous pouvez traiter de la manière habituelle. Par exemple, a,b = args.prox, a = args.prox[0], etc.
Dannid
1

La réponse acceptée a très bien fonctionné pour moi! Puisque tout le code est cassé sans tests, voici comment j'ai testé la réponse acceptée. parser.error()ne génère pas d' argparse.ArgumentErrorerreur, il quitte le processus. Vous devez tester SystemExit.

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

avec unittests

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

inspiré de: Utilisation d'unittest pour tester argparse - erreurs de sortie

Daniel Butler
la source