Argparse: argumento obrigatório 'y' se 'x' estiver presente

118

Tenho os seguintes requisitos:

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

para o argumento prox, uso action = 'store_true' para verificar se está presente ou não. Eu não exijo nenhum dos argumentos. Mas, se --prox for definido, eu também exijo rport e lport. Existe uma maneira fácil de fazer isso com argparse sem escrever codificação condicional personalizada.

Mais código:

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
fonte
Conectando, mas eu queria mencionar minha biblioteca Joffrey . Permite que você faça o que esta pergunta deseja, por exemplo, sem fazer você validar tudo sozinho (como na resposta aceita) ou contar com um hack lacônico (como na segunda resposta mais votada).
MI Wright
Para quem chega aqui, outra solução incrível: stackoverflow.com/a/44210638/6045800
Tomerikoo

Respostas:

120

Não, não há nenhuma opção em argparse para criar conjuntos de opções mutuamente inclusivos .

A maneira mais simples de lidar com isso seria:

if args.prox and (args.lport is None or args.rport is None):
    parser.error("--prox requires --lport and --rport.")
borntyping
fonte
2
Isso é o que eu acabei fazendo
asudhak
20
Obrigado pelo parser.errormétodo, é isso que eu estava procurando!
MarSoft
7
você não deveria usar 'ou'? afinal, você precisa de ambos os argumentos if args.prox and (args.lport is None or args.rport is None):
yossiz74
1
Em vez de args.lport is None, você pode simplesmente usar not args.lport. Acho que é um pouco mais pitônico.
CGFoX
7
Isso o impediria de definir --lportou --rportpara 0, que pode ser uma entrada válida para o programa.
borntyping
53

Você está falando sobre ter argumentos exigidos condicionalmente. Como @borntyping disse, você pode verificar o erro e fazer isso parser.error(), ou pode apenas aplicar um requisito relacionado a --proxquando adiciona um novo argumento.

Uma solução simples para o seu exemplo poderia ser:

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)

Desta forma, requiredrecebe Trueou Falsedependendo se o usuário for usado --prox. Isso também garante que -lporte -rporttenham um comportamento independente entre si.

Mira
fonte
8
Observe que ArgumentParserpode ser usado para analisar argumentos de uma lista diferente de sys.argv, caso em que isso falharia.
BallpointBen de
Além disso, isso falhará se a --prox=<value>sintaxe for usada.
fnkr 01 de
11

Que tal usar o parser.parse_known_args()método e, em seguida, adicionar os argumentos --lporte --rportconforme necessário, se --proxestiver presente.

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

Além disso, lembre-se de que você pode fornecer o namespace optsgerado após a primeira análise, enquanto analisa os argumentos restantes na segunda vez. Dessa forma, no final, depois que toda a análise for feita, você terá um único namespace com todas as opções.

Desvantagens:

  • Se --proxnão estiver presente, as outras duas opções dependentes nem mesmo estão presentes no namespace. Embora com base no seu caso de uso, se --proxnão estiver presente, o que acontece com as outras opções é irrelevante.
  • É necessário modificar a mensagem de uso, pois o analisador não conhece a estrutura completa
  • --lporte --rportnão apareça na mensagem de ajuda
Aditya Sriram
fonte
5

Você usa lportquando proxnão está definido. Se não, por que não fazer lporte rportargumentos de prox? por exemplo

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

Isso economiza a digitação dos usuários. É tão fácil de testar if args.prox is not None:quanto if args.prox:.

hpaulj
fonte
1
Para completar o exemplo, quando seu nargs é> 1, você obterá uma lista no argumento analisado etc., que você pode endereçar da maneira usual. Por exemplo, a,b = args.prox, a = args.prox[0], etc.
Dannid
1

A resposta aceita funcionou muito bem para mim! Como todo o código é quebrado sem testes, testei a resposta aceita aqui. parser.error()não gera um argparse.ArgumentErrorerro, em vez disso, sai do processo. Você tem que testar SystemExit.

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

com testes de unidade

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

inspirado em: Usando unittest para testar argparse - erros de saída

Daniel Butler
fonte