Argparse do Python: Como inserir uma nova linha no texto de ajuda?

340

Estou usando argparseno Python 2.7 para analisar as opções de entrada. Uma das minhas opções é de múltipla escolha. Quero fazer uma lista em seu texto de ajuda, por exemplo

from argparse import ArgumentParser

parser = ArgumentParser(description='test')

parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
    help="Some option, where\n"
         " a = alpha\n"
         " b = beta\n"
         " g = gamma\n"
         " d = delta\n"
         " e = epsilon")

parser.parse_args()

No entanto, argparseretira todas as novas linhas e espaços consecutivos. O resultado parece

~ / Downloads: 52 $ python2.7 x.py -h
uso: x.py [-h] [-g {a, b, g, d, e}]

teste

argumentos opcionais:
  -h, --help mostra esta mensagem de ajuda e sai
  -g {a, b, g, d, e} Alguma opção, onde a = alfa b = beta g = gama d = delta e
                  = epsilon

Como inserir novas linhas no texto de ajuda?

kennytm
fonte
Não tenho python 2.7 comigo para testar minhas idéias. Que tal usar o texto de ajuda entre aspas triplas ("" "" ""). As novas linhas sobrevivem usando isso?
pyfunc
4
@pyfunc: Não. A remoção é feita em tempo de execução argparse, não pelo intérprete, portanto, mudar para """..."""não ajuda.
Kennytm 4/10/10
Isto funcionou para mim
cardamomo

Respostas:

393

Tente usar RawTextHelpFormatter:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)
Michał Kwiatkowski
fonte
6
Eu acho que não é. Você pode subclassificá-lo, mas, infelizmente, Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail. provavelmente não é uma ótima idéia, embora possa não importar, pois 2,7 deve ser o último python 2.x e você deverá refatorar muitas coisas para 3.x de qualquer maneira. Na verdade, estou executando o 2.6 com argparseinstalado via easy_installpara que a documentação possa estar desatualizada.
intuited
3
Alguns links: para python 2.7 e python 3. * . O pacote 2.6 deve, de acordo com seu wiki , estar em conformidade com o pacote oficial 2.7. Do documento: "Passar RawDescriptionHelpFormatter como formatter_class = indica que a descrição e o epilog já estão formatados corretamente e não devem ser agrupados em linhas"
Stefano
83
Tente, em vez disso, formatter_class =, RawDescriptionHelpFormatterque funciona apenas na descrição e no epilog, em vez de no texto de ajuda.
MarkHu 24/05
3
Percebi que mesmo com as RawTextHelpFormatternovas linhas iniciais e finais são removidas. Para contornar isso, você pode simplesmente adicionar duas ou mais novas linhas consecutivas; todos, exceto uma nova linha, sobreviverão.
MrMas
11
Você também pode combinar formatadores, por exemplo, class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): passe depois formatter_class=Formatter.
Terry Brown
78

Se você deseja substituir apenas uma opção, não deve usar RawTextHelpFormatter. Em vez disso, subclasse HelpFormattere forneça uma introdução especial para as opções que devem ser tratadas como "brutas" (eu uso "R|rest of help"):

import argparse

class SmartFormatter(argparse.HelpFormatter):

    def _split_lines(self, text, width):
        if text.startswith('R|'):
            return text[2:].splitlines()  
        # this is the RawTextHelpFormatter._split_lines
        return argparse.HelpFormatter._split_lines(self, text, width)

E use-o:

from argparse import ArgumentParser

parser = ArgumentParser(description='test', formatter_class=SmartFormatter)

parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
    help="R|Some option, where\n"
         " a = alpha\n"
         " b = beta\n"
         " g = gamma\n"
         " d = delta\n"
         " e = epsilon")

parser.parse_args()

Quaisquer outras chamadas para as .add_argument()quais a ajuda não começa R|serão encerradas normalmente.

Isso faz parte das minhas melhorias no argparse . O SmartFormatter completo também suporta a adição dos padrões a todas as opções e a entrada bruta da descrição dos utilitários. A versão completa possui seu próprio _split_linesmétodo, de forma que qualquer formatação feita para, por exemplo, seqüências de caracteres de versão seja preservada:

parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")
Anthon
fonte
Quero fazer isso para uma mensagem de versão, mas esse SmartFormatter parece funcionar apenas com texto de ajuda, não com o texto da versão especial. parser.add_argument('-v', '--version', action='version',version=get_version_str()) É possível estendê-lo a esse caso?
Mc_electron
@mc_electron a versão completa do SmartFormatter também tem sua própria _split_linese preserva as quebras de linha (não há necessidade de especificar "R |" no início, se você quiser essa opção, corrigir o _VersionAction.__call__método
Anthon
Não estou grocando totalmente a primeira parte do seu comentário, embora eu possa ver _VersionAction.__call__que eu provavelmente gostaria que ele apenas em parser.exit(message=version)vez de usar a versão formatada. Existe alguma maneira de fazer isso sem liberar uma cópia corrigida do argparse?
Mc_electron
@mc_electron Estou me referindo às melhorias que publiquei no bitbucket (conforme o link para minhas melhorias no argparse na resposta). Mas você também pode corrigir o __call__em _VersionActionfazendo argparse._VersionAction.__call__ = smart_versiondepois de definirdef smart_version(self, parser, namespace, values, option_string=None): ...
Anthon
Boa ideia. Não me ajudou como o epílogo e descrição não parecem correr através _split_lines :(
Pod
31

Outra maneira fácil de fazer isso é incluir o pacote de texto .

Por exemplo,

import argparse, textwrap
parser = argparse.ArgumentParser(description='some information',
        usage='use "python %(prog)s --help" for more information',
        formatter_class=argparse.RawTextHelpFormatter)

parser.add_argument('--argument', default=somedefault, type=sometype,
        help= textwrap.dedent('''\
        First line
        Second line
        More lines ... '''))

Dessa forma, podemos evitar o longo espaço vazio na frente de cada linha de saída.

usage: use "python your_python_program.py --help" for more information

Prepare input file

optional arguments:
-h, --help            show this help message and exit
--argument ARGUMENT
                      First line
                      Second line
                      More lines ...
Wang Zong'an
fonte
11

Eu já enfrentei um problema semelhante (Python 2.7.6). Eu tentei dividir a seção de descrição em várias linhas usando RawTextHelpFormatter:

parser = ArgumentParser(description="""First paragraph 

                                       Second paragraph

                                       Third paragraph""",  
                                       usage='%(prog)s [OPTIONS]', 
                                       formatter_class=RawTextHelpFormatter)

options = parser.parse_args()

E pegou:

uso: play-with-argparse.py [OPTIONS]

Primeiro parágrafo 

                        Segundo parágrafo

                        Terceiro parágrafo

argumentos opcionais:
  -h, --help mostra esta mensagem de ajuda e sai

Então RawTextHelpFormatternão é uma solução. Porque ele imprime a descrição como aparece no código-fonte, preservando todos os caracteres de espaço em branco (quero manter guias extras no meu código-fonte para facilitar a leitura, mas não quero imprimi-los todos. O formatador bruto também não quebra a linha quando é muito longo, mais de 80 caracteres, por exemplo).

Obrigado a @Anton, que inspirou a direção correta acima . Mas essa solução precisa de pequenas modificações para formatar a seção de descrição .

De qualquer forma, é necessário um formatador personalizado. Estendi a HelpFormatterclasse existente e substituí o _fill_textmétodo como este:

import textwrap as _textwrap
class MultilineFormatter(argparse.HelpFormatter):
    def _fill_text(self, text, width, indent):
        text = self._whitespace_matcher.sub(' ', text).strip()
        paragraphs = text.split('|n ')
        multiline_text = ''
        for paragraph in paragraphs:
            formatted_paragraph = _textwrap.fill(paragraph, width, initial_indent=indent, subsequent_indent=indent) + '\n\n'
            multiline_text = multiline_text + formatted_paragraph
        return multiline_text

Compare com o código fonte original do módulo argparse :

def _fill_text(self, text, width, indent):
    text = self._whitespace_matcher.sub(' ', text).strip()
    return _textwrap.fill(text, width, initial_indent=indent,
                                       subsequent_indent=indent)

No código original, toda a descrição está sendo quebrada. No formatador personalizado acima, o texto inteiro é dividido em vários blocos e cada um deles é formatado independentemente.

Assim, com a ajuda do formatador personalizado:

parser = ArgumentParser(description= """First paragraph 
                                        |n                              
                                        Second paragraph
                                        |n
                                        Third paragraph""",  
                usage='%(prog)s [OPTIONS]',
                formatter_class=MultilineFormatter)

options = parser.parse_args()

a saída é:

uso: play-with-argparse.py [OPTIONS]

Primeiro parágrafo

Segundo parágrafo

Terceiro parágrafo

argumentos opcionais:
  -h, --help mostra esta mensagem de ajuda e sai
flaz14
fonte
11
Isso é maravilhoso - aconteceu nisso depois de quase desistir e contemplar apenas reimplementar completamente o argumento de ajuda ... me salvou de uma boa quantidade de problemas.
Paul Gowder 30/11/2015
2
A subclasse HelpFormatteré problemática, pois os desenvolvedores do argparse garantem apenas que o nome da classe sobreviverá nas versões futuras do argparse. Basicamente, eles mesmos fizeram um cheque em branco para que pudessem alterar os nomes dos métodos, se assim fosse conveniente. Eu acho isso frustrante; o mínimo que eles poderiam ter feito é expor alguns métodos na API.
MrMas 18/03
Não é exatamente o que o OP estava pedindo, mas exatamente o que eu queria, obrigado!
Huw Walters
2

Eu queria ter quebras de linha manuais no texto de descrição e quebra automática de código; mas nenhuma das sugestões aqui funcionou para mim - então acabei modificando a classe SmartFormatter fornecida nas respostas aqui; apesar dos problemas com os nomes dos métodos argparse que não são uma API pública, eis o que eu tenho (como um arquivo chamado test.py):

import argparse
from argparse import RawDescriptionHelpFormatter

# call with: python test.py -h

class SmartDescriptionFormatter(argparse.RawDescriptionHelpFormatter):
  #def _split_lines(self, text, width): # RawTextHelpFormatter, although function name might change depending on Python
  def _fill_text(self, text, width, indent): # RawDescriptionHelpFormatter, although function name might change depending on Python
    #print("splot",text)
    if text.startswith('R|'):
      paragraphs = text[2:].splitlines()
      rebroken = [argparse._textwrap.wrap(tpar, width) for tpar in paragraphs]
      #print(rebroken)
      rebrokenstr = []
      for tlinearr in rebroken:
        if (len(tlinearr) == 0):
          rebrokenstr.append("")
        else:
          for tlinepiece in tlinearr:
            rebrokenstr.append(tlinepiece)
      #print(rebrokenstr)
      return '\n'.join(rebrokenstr) #(argparse._textwrap.wrap(text[2:], width))
    # this is the RawTextHelpFormatter._split_lines
    #return argparse.HelpFormatter._split_lines(self, text, width)
    return argparse.RawDescriptionHelpFormatter._fill_text(self, text, width, indent)

parser = argparse.ArgumentParser(formatter_class=SmartDescriptionFormatter, description="""R|Blahbla bla blah blahh/blahbla (bla blah-blabla) a blahblah bl a blaha-blah .blah blah

Blah blah bla blahblah, bla blahblah blah blah bl blblah bl blahb; blah bl blah bl bl a blah, bla blahb bl:

  blah blahblah blah bl blah blahblah""")

options = parser.parse_args()

É assim que funciona em 2.7 e 3.4:

$ python test.py -h
usage: test.py [-h]

Blahbla bla blah blahh/blahbla (bla blah-blabla) a blahblah bl a blaha-blah
.blah blah

Blah blah bla blahblah, bla blahblah blah blah bl blblah bl blahb; blah bl
blah bl bl a blah, bla blahb bl:

  blah blahblah blah bl blah blahblah

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

A partir do SmartFomatter descrito acima, terminei com essa solução:

class SmartFormatter(argparse.HelpFormatter):
    '''
         Custom Help Formatter used to split help text when '\n' was 
         inserted in it.
    '''

    def _split_lines(self, text, width):
        r = []
        for t in text.splitlines(): r.extend(argparse.HelpFormatter._split_lines(self, t, width))
        return r

Observe que, estranhamente, o argumento formatter_class passado para o analisador de nível superior não é herdado por sub_parsers, é preciso passá-lo novamente para cada sub_parser criado.

ermitz
fonte
0

Prefácio

Para esta pergunta, argparse.RawTextHelpFormatteré útil para mim.

Agora, quero compartilhar como uso o argparse.

Eu sei que pode não estar relacionado à pergunta,

mas essas perguntas me incomodam há um tempo.

Então, eu quero compartilhar minha experiência, espero que seja útil para alguém.

Aqui vamos nós.

Módulos de terceiros

colorama : para alterar a cor do texto:pip install colorama

Faz com que as seqüências de caracteres de escape ANSI (para produzir texto colorido do terminal e posicionamento do cursor) funcionem no MS Windows

Exemplo

import colorama
from colorama import Fore, Back
from pathlib import Path
from os import startfile, system

SCRIPT_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = SCRIPT_DIR.joinpath('.')


def main(args):
    ...


if __name__ == '__main__':
    colorama.init(autoreset=True)

    from argparse import ArgumentParser, RawTextHelpFormatter

    format_text = FormatText([(20, '<'), (60, '<')])
    yellow_dc = format_text.new_dc(fore_color=Fore.YELLOW)
    green_dc = format_text.new_dc(fore_color=Fore.GREEN)
    red_dc = format_text.new_dc(fore_color=Fore.RED, back_color=Back.LIGHTYELLOW_EX)

    script_description = \
        '\n'.join([desc for desc in
                   [f'\n{green_dc(f"python {Path(__file__).name} [REFERENCE TEMPLATE] [OUTPUT FILE NAME]")} to create template.',
                    f'{green_dc(f"python {Path(__file__).name} -l *")} to get all available template',
                    f'{green_dc(f"python {Path(__file__).name} -o open")} open template directory so that you can put your template file there.',
                    # <- add your own description
                    ]])
    arg_parser = ArgumentParser(description=yellow_dc('CREATE TEMPLATE TOOL'),
                                # conflict_handler='resolve',
                                usage=script_description, formatter_class=RawTextHelpFormatter)

    arg_parser.add_argument("ref", help="reference template", nargs='?')
    arg_parser.add_argument("outfile", help="output file name", nargs='?')
    arg_parser.add_argument("action_number", help="action number", nargs='?', type=int)
    arg_parser.add_argument('--list', "-l", dest='list',
                            help=f"example: {green_dc('-l *')} \n"
                                 "description: list current available template. (accept regex)")

    arg_parser.add_argument('--option', "-o", dest='option',
                            help='\n'.join([format_text(msg_data_list) for msg_data_list in [
                                ['example', 'description'],
                                [green_dc('-o open'), 'open template directory so that you can put your template file there.'],
                                [green_dc('-o run'), '...'],
                                [green_dc('-o ...'), '...'],
                                # <- add your own description
                            ]]))

    g_args = arg_parser.parse_args()
    task_run_list = [[False, lambda: startfile('.')] if g_args.option == 'open' else None,
                     [False, lambda: [print(template_file_path.stem) for template_file_path in TEMPLATE_DIR.glob(f'{g_args.list}.py')]] if g_args.list else None,
                     # <- add your own function
                     ]
    for leave_flag, func in [task_list for task_list in task_run_list if task_list]:
        func()
        if leave_flag:
            exit(0)

    # CHECK POSITIONAL ARGUMENTS
    for attr_name, value in vars(g_args).items():
        if attr_name.startswith('-') or value is not None:
            continue
        system('cls')
        print(f'error required values of {red_dc(attr_name)} is None')
        print(f"if you need help, please use help command to help you: {red_dc(f'python {__file__} -h')}")
        exit(-1)
    main(g_args)

Onde a classe de FormatTexté a seguinte

class FormatText:
    __slots__ = ['align_list']

    def __init__(self, align_list: list, autoreset=True):
        """
        USAGE::

            format_text = FormatText([(20, '<'), (60, '<')])
            red_dc = format_text.new_dc(fore_color=Fore.RED)
            print(red_dc(['column 1', 'column 2']))
            print(red_dc('good morning'))
        :param align_list:
        :param autoreset:
        """
        self.align_list = align_list
        colorama.init(autoreset=autoreset)

    def __call__(self, text_list: list):
        if len(text_list) != len(self.align_list):
            if isinstance(text_list, str):
                return text_list
            raise AttributeError
        return ' '.join(f'{txt:{flag}{int_align}}' for txt, (int_align, flag) in zip(text_list, self.align_list))

    def new_dc(self, fore_color: Fore = Fore.GREEN, back_color: Back = ""):  # DECORATOR
        """create a device context"""
        def wrap(msgs):
            return back_color + fore_color + self(msgs) + Fore.RESET
        return wrap

insira a descrição da imagem aqui

Carson
fonte