Argparse Python: faça com que pelo menos um argumento seja obrigatório


92

Tenho usado argparsepara um programa Python que pode -process, -uploadou ambos:

parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('-process', action='store_true')
parser.add_argument('-upload',  action='store_true')
args = parser.parse_args()

O programa não tem sentido sem pelo menos um parâmetro. Como posso configurar argparsepara forçar a escolha de pelo menos um parâmetro?

ATUALIZAR:

Seguindo os comentários: Qual é a maneira Pythônica de parametrizar um programa com pelo menos uma opção?


9
-xé universalmente uma bandeira e opcional. Corte o -se for necessário.

1
Você não poderia fazer processo comportamento padrão (sem a necessidade de especificar nenhuma opção) e permitir que o usuário altere para uploadse essa opção estiver definida? Normalmente, as opções devem ser opcionais, daí o nome. As opções obrigatórias devem ser evitadas (isso também está nos argparse documentos).
Tim Pietzcker

@AdamMatan Já se passaram quase três anos desde que fez a sua pergunta, mas gostei do desafio escondido nela e aproveitei a vantagem de estarem disponíveis novas soluções para este tipo de tarefas.
Jan Vlcinsky

Respostas:


105
if not (args.process or args.upload):
    parser.error('No action requested, add -process or -upload')

1
Essa é provavelmente a única maneira, se argparsenão houver opção embutida para isso.
Adam Matan

27
args = vars(parser.parse_args())
if not any(args.values()):
    parser.error('No arguments provided.')

3
+1 para uma solução generalizada. Também como o uso de vars(), que também é útil para passar opções nomeadas com cuidado para um construtor com **.
Lenna

Que é exatamente o que estou fazendo com ele. Obrigado!
brentlance

1
Droga, eu gosto disso vars. Eu apenas fiz .__dict__e me senti idiota antes.
Theo Belaire

1
ótimas respostas. Ambos "vars" e "any" eram novos para mim :-)
Vivek Jha

21

Se não for a parte 'ou ambas' (inicialmente não percebi isso), você poderia usar algo assim:

parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('--process', action='store_const', const='process', dest='mode')
parser.add_argument('--upload',  action='store_const', const='upload', dest='mode')
args = parser.parse_args()
if not args.mode:
    parser.error("One of --process or --upload must be given")

Porém, provavelmente seria uma ideia melhor usar subcomandos .


4
Acho que ele quer permitir --processOR --upload, não XOR. Isso evita que as duas opções sejam definidas ao mesmo tempo.
phihag

+1 porque você mencionou subcomandos. Ainda - como alguém apontou nos comentários -xe --xxxsão parâmetros normalmente opcionais.
mac

20

Eu sei que isso é muito antigo, mas a maneira de exigir uma opção, mas proibir mais de uma (XOR) é assim:

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-process', action='store_true')
group.add_argument('-upload',  action='store_true')
args = parser.parse_args()
print args

Resultado:

>opt.py  
usage: multiplot.py [-h] (-process | -upload)  
multiplot.py: error: one of the arguments -process -upload is required  

>opt.py -upload  
Namespace(process=False, upload=True)  

>opt.py -process  
Namespace(process=True, upload=False)  

>opt.py -upload -process  
usage: multiplot.py [-h] (-process | -upload)  
multiplot.py: error: argument -process: not allowed with argument -upload  

3
Infelizmente, o OP não quer um XOR. É um ou ambos, mas não nenhum, então seu último caso de teste não atende aos requisitos.
kdopen

2
@kdopen: o entrevistado esclareceu que esta é uma variação da pergunta original, que achei útil: "a maneira de exigir uma opção, mas proibir mais de uma" Talvez a etiqueta do Stack Exchange exija uma nova pergunta . Mas ter esta resposta aqui me ajudou ...
erik.weathers

2
Esta postagem não responde à pergunta inicial
Marc

2
Como isso responde à pergunta de "pelo menos um"?
xaxxon

2
Infelizmente, o OP não quer um XOR.
duckman_1991 01 de

8

Revisão de Requisitos

  • usar argparse(vou ignorar este)
  • permitir que uma ou duas ações sejam chamadas (pelo menos uma é necessária).
  • tente por Pythonic (prefiro chamá-lo de "POSIX")

Existem também alguns requisitos implícitos ao viver na linha de comando:

  • explicar o uso para o usuário de uma maneira que seja fácil de entender
  • as opções devem ser opcionais
  • permite especificar sinalizadores e opções
  • permite a combinação com outros parâmetros (como nome ou nomes de arquivo).

Solução de amostra usando docopt (arquivo managelog.py):

"""Manage logfiles
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  Password

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
    from docopt import docopt
    args = docopt(__doc__)
    print args

Tente executá-lo:

$ python managelog.py
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Mostre a ajuda:

$ python managelog.py -h
Manage logfiles
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  P    managelog.py [options] upload -- <logfile>...

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>

E use-o:

$ python managelog.py -V -U user -P secret upload -- alfa.log beta.log
{'--': True,
 '--pswd': 'secret',
 '--user': 'user',
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': False,
 'upload': True}

Alternativa curta short.py

Pode haver uma variante ainda mais curta:

"""Manage logfiles
Usage:
    short.py [options] (process|upload)... -- <logfile>...
    short.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  Password

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
    from docopt import docopt
    args = docopt(__doc__)
    print args

O uso é parecido com este:

$ python short.py -V process upload  -- alfa.log beta.log
{'--': True,
 '--pswd': None,
 '--user': None,
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': 1,
 'upload': 1}

Observe que, em vez de valores booleanos para as chaves de "processo" e "upload", existem contadores.

Acontece que não podemos evitar a duplicação dessas palavras:

$ python short.py -V process process upload  -- alfa.log beta.log
{'--': True,
 '--pswd': None,
 '--user': None,
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': 2,
 'upload': 1}

Conclusões

Projetar uma boa interface de linha de comando pode ser um desafio às vezes.

Existem vários aspectos do programa baseado em linha de comando:

  • bom design de linha de comando
  • selecionando / usando o analisador adequado

argparse oferece muito, mas restringe cenários possíveis e pode se tornar muito complexo.

Com as docoptcoisas ficam muito mais curtas, preservando a legibilidade e oferecendo alto grau de flexibilidade. Se você consegue obter argumentos analisados ​​do dicionário e faz algumas conversões (para inteiro, abrindo arquivos ..) manualmente (ou por outra biblioteca chamada schema), você pode achar um docoptbom ajuste para análise de linha de comando.


Nunca ouvi falar do docopt, ótima sugestão!
Ton van den Heuvel

@TonvandenHeuvel Good. Só quero confirmar, ainda estou usando como minha solução preferida para interfaces de linha de comando.
Jan Vlcinsky

Melhor resposta evar, obrigado pelos exemplos detalhados.
jnovack

5

Se você precisar que um programa python seja executado com pelo menos um parâmetro, adicione um argumento que não tenha o prefixo de opção (- ou - por padrão) e defina nargs=+(mínimo de um argumento necessário). O problema com esse método que descobri é que, se você não especificar o argumento, argparse gerará um erro de "poucos argumentos" e não imprimirá o menu de ajuda. Se você não precisa dessa funcionalidade, veja como fazê-lo no código:

import argparse

parser = argparse.ArgumentParser(description='Your program description')
parser.add_argument('command', nargs="+", help='describe what a command is')
args = parser.parse_args()

Eu acho que quando você adiciona um argumento com os prefixos de opção, nargs governa todo o analisador de argumento e não apenas a opção. (O que quero dizer é que, se você tiver um --optionsinalizador com nargs="+", o --optionsinalizador espera pelo menos um argumento. Se você tiver optioncom nargs="+", ele espera pelo menos um argumento geral.)


Você poderia acrescentar choices=['process','upload']a esse argumento.
hpaulj de

5

Para http://bugs.python.org/issue11588 , estou explorando maneiras de generalizar o mutually_exclusive_groupconceito para lidar com casos como este.

Com este desenvolvimento argparse.py , https://github.com/hpaulj/argparse_issues/blob/nested/argparse.py eu sou capaz de escrever:

parser = argparse.ArgumentParser(prog='PROG', 
    description='Log archiver arguments.')
group = parser.add_usage_group(kind='any', required=True,
    title='possible actions (at least one is required)')
group.add_argument('-p', '--process', action='store_true')
group.add_argument('-u', '--upload',  action='store_true')
args = parser.parse_args()
print(args)

que produz o seguinte help :

usage: PROG [-h] (-p | -u)

Log archiver arguments.

optional arguments:
  -h, --help     show this help message and exit

possible actions (at least one is required):
  -p, --process
  -u, --upload

Isso aceita entradas como '-u', '-up', '--proc --up' etc.

Ele acaba executando um teste semelhante a https://stackoverflow.com/a/6723066/901925 , embora a mensagem de erro precise ser mais clara:

usage: PROG [-h] (-p | -u)
PROG: error: some of the arguments process upload is required

Eu me pergunto:

  • são os parâmetros kind='any', required=True claros o suficiente (aceitar qualquer um do grupo; pelo menos um é necessário)?

  • é uso (-p | -u) claro? Um mutually_exclusive_group necessário produz a mesma coisa. Existe alguma notação alternativa?

  • é usar um grupo como este mais intuitivo do que phihag'sum teste simples?


Não consigo encontrar nenhuma menção add_usage_groupa nesta página: docs.python.org/2/library/argparse.html ; você poderia fornecer um link para a documentação para isso?
P. Myer Nore

@ P.MyerNore, forneci um link - no início desta resposta. Isso não foi colocado em produção.
hpaulj

5

A melhor maneira de fazer isso é usando o módulo embutido python add_mutually_exclusive_group .

parser = argparse.ArgumentParser(description='Log archiver arguments.')
group = parser.add_mutually_exclusive_group()
group.add_argument('-process', action='store_true')
group.add_argument('-upload',  action='store_true')
args = parser.parse_args()

Se você quiser que apenas um argumento seja selecionado pela linha de comando, use apenas required = True como um argumento para o grupo

group = parser.add_mutually_exclusive_group(required=True)

2
Como isso te leva a "pelo menos um" - não te dá "exatamente um"?
xaxxon

3
Infelizmente, o OP não quer um XOR. OP está procurando por OR
duckman_1991

Isso não respondeu à pergunta de OP, mas respondeu a minha, então obrigado de qualquer maneira ¯_ (ツ) _ / ¯
rosstex

2

Talvez use sub-analisadores?

import argparse

parser = argparse.ArgumentParser(description='Log archiver arguments.')
subparsers = parser.add_subparsers(dest='subparser_name', help='sub-command help')
parser_process = subparsers.add_parser('process', help='Process logs')
parser_upload = subparsers.add_parser('upload', help='Upload logs')
args = parser.parse_args()

print("Subparser: ", args.subparser_name)

Agora --helpmostra:

$ python /tmp/aaa.py --help
usage: aaa.py [-h] {process,upload} ...

Log archiver arguments.

positional arguments:
  {process,upload}  sub-command help
    process         Process logs
    upload          Upload logs

optional arguments:
  -h, --help        show this help message and exit
$ python /tmp/aaa.py
usage: aaa.py [-h] {process,upload} ...
aaa.py: error: too few arguments
$ python3 /tmp/aaa.py upload
Subparser:  upload

Você também pode adicionar opções adicionais a esses subanalisadores. Além disso, em vez de usar isso, dest='subparser_name'você também pode vincular funções a serem chamadas diretamente em um determinado subcomando (consulte a documentação).


2

Isso atinge o objetivo e também será afetado na --helpsaída gerada automaticamente do argparse , que é o que a maioria dos programadores sãos desejam (também funciona com argumentos opcionais):

parser.add_argument(
    'commands',
    nargs='+',                      # require at least 1
    choices=['process', 'upload'],  # restrict the choice
    help='commands to execute'
)

Documentos oficiais sobre isso: https://docs.python.org/3/library/argparse.html#choices


1

Use append_const para uma lista de ações e, em seguida, verifique se a lista está preenchida:

parser.add_argument('-process', dest=actions, const="process", action='append_const')
parser.add_argument('-upload',  dest=actions, const="upload", action='append_const')

args = parser.parse_args()

if(args.actions == None):
    parser.error('Error: No actions requested')

Você pode até especificar os métodos diretamente nas constantes.

def upload:
    ...

parser.add_argument('-upload',  dest=actions, const=upload, action='append_const')
args = parser.parse_args()

if(args.actions == None):
    parser.error('Error: No actions requested')

else:
    for action in args.actions:
        action()
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.