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

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

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.


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

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:.


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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.