Python argparse: ¿Cómo insertar nueva línea en el texto de ayuda?

340

Estoy usando argparsePython 2.7 para analizar las opciones de entrada. Una de mis opciones es una opción múltiple. Quiero hacer una lista en su texto de ayuda, p. Ej.

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

Sin embargo, argparseelimina todas las líneas nuevas y espacios consecutivos. El resultado se ve como

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

prueba

argumentos opcionales:
  -h, - ayuda a mostrar este mensaje de ayuda y salir
  -g {a, b, g, d, e} Alguna opción, donde a = alpha b = beta g = gamma d = delta e
                  = épsilon

¿Cómo insertar nuevas líneas en el texto de ayuda?

kennytm
fuente
No tengo Python 2.7 conmigo, así que puedo probar mis ideas. ¿Qué tal usar texto de ayuda en comillas triples ("" "" "")? ¿Sobreviven las nuevas líneas con esto?
pyfunc
44
@pyfunc: No. La eliminación se realiza en tiempo de ejecución argparse, no por el intérprete, por lo que cambiar a """..."""no ayudará.
kennytm
Esto funcionó para mí
cardamomo

Respuestas:

393

Intenta usar RawTextHelpFormatter:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)
Michał Kwiatkowski
fuente
66
Creo que no lo es. Podría subclasificarlo, pero desafortunadamente, Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail. probablemente no sea una gran idea, aunque podría no importar, ya que 2.7 está destinado a ser la última python 2.x y se espera que refactorice muchas cosas para 3.x de todos modos. Realmente estoy ejecutando 2.6 con argparseinstalado vía, easy_installpor lo que la documentación puede estar desactualizada.
intuido
3
Algunos enlaces: para python 2.7 y python 3. * . El paquete 2.6 debería, según su wiki , cumplir con el 2.7 oficial. Del documento: "Pasar RawDescriptionHelpFormatter como formatter_class = indica que la descripción y el epílogo ya están formateados correctamente y no se deben ajustar en línea"
Stefano
83
Pruebe en su lugar formatter_class = RawDescriptionHelpFormatterque solo funciona en la descripción y el epílogo en lugar del texto de ayuda.
MarkHu
3
He notado que incluso con las RawTextHelpFormatternuevas líneas iniciales y finales se eliminan. Para evitar esto, simplemente puede agregar dos o más líneas nuevas consecutivas; todas menos una nueva línea sobrevivirán.
MrMas
11
También puede combinar formateadores, por ejemplo, class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): passy luego formatter_class=Formatter.
Terry Brown
78

Si solo desea anular la única opción, no debe usarla RawTextHelpFormatter. En su lugar, subclase HelpFormattery proporcione una introducción especial para las opciones que deben manejarse "en bruto" (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)

Y úsalo:

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

Cualquier otra llamada a .add_argument()donde no comience la ayuda R|se finalizará normalmente.

Esto es parte de mis mejoras en argparse . El SmartFormatter completo también admite agregar los valores predeterminados a todas las opciones y la entrada sin formato de la descripción de las utilidades. La versión completa tiene su propio _split_linesmétodo, por lo que se conserva cualquier formato realizado, por ejemplo, a cadenas de versión:

parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")
Anthon
fuente
Quiero hacer esto para un mensaje de versión, pero este SmartFormatter solo parece funcionar con texto de ayuda, no con el texto de versión especial. parser.add_argument('-v', '--version', action='version',version=get_version_str()) ¿Es posible extenderlo a ese caso?
mc_electron
@mc_electron, la versión completa de SmartFormatter también tiene su propio _split_linesy conserva los saltos de línea (no es necesario especificar "R |" al principio, si desea esa opción, parchee el _VersionAction.__call__método
Anthon
No estoy asimilando por completo la primera parte de tu comentario, aunque puedo ver _VersionAction.__call__que probablemente querría que lo haga en parser.exit(message=version)lugar de usar la versión formateada. ¿Hay alguna forma de hacerlo sin lanzar una copia parcheada de argparse?
mc_electron
@mc_electron Me refiero a las mejoras que publiqué en bitbucket (según el enlace a mis mejoras en argparse en la respuesta). Pero también se puede parchear el __call__en _VersionActionhaciendo argparse._VersionAction.__call__ = smart_versiondespués de definirdef smart_version(self, parser, namespace, values, option_string=None): ...
Anthon
Gran idea. No me ayudó, ya que el epílogo y la descripción no parecen pasar por _split_lines :(
Pod
31

Otra forma fácil de hacerlo es incluir textwrap .

Por ejemplo,

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

De esta manera, podemos evitar el largo espacio vacío frente a cada línea de salida.

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
fuente
11

Me he enfrentado a un problema similar (Python 2.7.6). Intenté dividir la sección de descripción en varias líneas usando RawTextHelpFormatter:

parser = ArgumentParser(description="""First paragraph 

                                       Second paragraph

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

options = parser.parse_args()

Y consiguió:

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

Primer párrafo 

                        Segundo parrafo

                        Tercer párrafo

argumentos opcionales:
  -h, - ayuda a mostrar este mensaje de ayuda y salir

Entonces RawTextHelpFormatterno es una solución. Debido a que imprime la descripción tal como aparece en el código fuente, preservando todos los caracteres de espacio en blanco (quiero mantener pestañas adicionales en mi código fuente para facilitar la lectura, pero no quiero imprimirlas todas. Además, el formateador sin formato no ajusta la línea cuando está demasiado largo, más de 80 caracteres, por ejemplo).

Gracias a @Anton que inspiró la dirección correcta arriba . Pero esa solución necesita una ligera modificación para formatear la sección de descripción .

De todos modos, se necesita formateador personalizado. Extendí la HelpFormatterclase existente y el _fill_textmétodo anulado 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 con el código fuente original que proviene del 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)

En el código original se está envolviendo la descripción completa. En el formateador personalizado arriba, todo el texto se divide en varios fragmentos, y cada uno de ellos se formatea de forma independiente.

Entonces, con la ayuda del formateador personalizado:

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

options = parser.parse_args()

la salida es:

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

Primer párrafo

Segundo parrafo

Tercer párrafo

argumentos opcionales:
  -h, - ayuda a mostrar este mensaje de ayuda y salir
flaz14
fuente
1
Esto es maravilloso --- sucedió en esto después de casi rendirme y contemplar simplemente volver a implementar el argumento de ayuda por completo ... me ahorró una buena cantidad de problemas.
Paul Gowder
2
La subclase HelpFormatteres problemática ya que los desarrolladores de argparse solo garantizan que el nombre de la clase sobrevivirá en futuras versiones de argparse. Básicamente, se han escrito un cheque en blanco para que puedan cambiar los nombres de los métodos si les resulta conveniente hacerlo. Esto me parece frustrante; Lo menos que podrían haber hecho es exponer algunos métodos en la API.
MrMas
No exactamente lo que pedía el OP, pero exactamente lo que quería, ¡gracias!
Huw Walters
2

Quería tener saltos de línea manuales en el texto de la descripción y ajuste automático del mismo; pero ninguna de las sugerencias aquí funcionó para mí, así que terminé modificando la clase SmartFormatter dada en las respuestas aquí; A pesar de los problemas con los nombres de métodos argparse que no son una API pública, esto es lo que tengo (como un archivo llamado 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()

Así es como funciona en 2.7 y 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
fuente
1

A partir de SmartFomatter descrito anteriormente, terminé con esa solución:

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

Tenga en cuenta que extrañamente el argumento formatter_class pasado al analizador de nivel superior no es heredado por sub_parsers, uno debe pasarlo nuevamente por cada sub_parser creado.

ermitz
fuente
0

Prefacio

Para esta pregunta, argparse.RawTextHelpFormatteres útil para mí.

Ahora, quiero compartir cómo uso el argparse.

Sé que puede no estar relacionado con la pregunta,

pero estas preguntas me han molestado por un tiempo.

Así que quiero compartir mi experiencia, espero que sea útil para alguien.

Aquí vamos.

Módulos de terceros

colorama : para cambiar el color del texto:pip install colorama

Hace que las secuencias de caracteres de escape ANSI (para producir texto de terminal de color y posicionamiento del cursor) funcionen en MS Windows

Ejemplo

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)

Donde la clase de FormatTextes la siguiente

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

ingrese la descripción de la imagen aquí

Carson
fuente