Argument Python: comment insérer une nouvelle ligne dans le texte d'aide?

341

J'utilise argparseen Python 2.7 pour analyser les options d'entrée. Une de mes options est un choix multiple. Je veux faire une liste dans son texte d'aide, par exemple

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

Cependant, argparsesupprime toutes les nouvelles lignes et les espaces consécutifs. Le résultat ressemble à

~ / Téléchargements: 52 $ python2.7 x.py -h
utilisation: x.py [-h] [-g {a, b, g, d, e}]

tester

arguments facultatifs:
  -h, --help afficher ce message d'aide et quitter
  -g {a, b, g, d, e} Une option, où a = alpha b = bêta g = gamma d = delta e
                  = epsilon

Comment insérer des sauts de ligne dans le texte d'aide?

kennytm
la source
Je n'ai pas python 2.7 avec moi, donc je peux tester mes idées. Que diriez-vous d'utiliser le texte d'aide entre guillemets ("" "" ""). Les nouvelles lignes survivent-elles en utilisant cela?
pyfunc
4
@pyfunc: Non. La suppression est effectuée au moment de l'exécution par argparse, et non par l'interpréteur, donc passer à """..."""n'aidera pas.
kennytm
Cela a fonctionné pour moi
cardamome

Réponses:

394

Essayez d'utiliser RawTextHelpFormatter:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)
Michał Kwiatkowski
la source
6
Je pense que non. Vous pouvez le sous-classer, mais malheureusement Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail. ce n'est probablement pas une bonne idée, même si cela n'a pas d'importance, puisque 2.7 est censé être le dernier python 2.x et vous devrez de toute façon refactoriser beaucoup de choses pour 3.x. J'utilise actuellement 2.6 avec argparseinstallé via easy_installafin que la documentation soit elle-même obsolète.
intuition du
3
Quelques liens: pour python 2.7 et python 3. * . Le package 2.6 devrait, selon son wiki , être conforme à celui officiel 2.7. Extrait du document: "Passer RawDescriptionHelpFormatter comme formatter_class = indique que la description et l'épilogue sont déjà correctement formatés et ne doivent pas être encapsulés"
Stefano
83
Essayez plutôt formatter_class = RawDescriptionHelpFormatterqui ne fonctionne que sur la description et l'épilogue plutôt que sur le texte d'aide.
MarkHu
3
J'ai remarqué que même avec RawTextHelpFormatter, les sauts de ligne de début et de fin sont supprimés. Pour contourner ce problème, vous pouvez simplement ajouter deux ou plusieurs sauts de ligne consécutifs; toutes les lignes sauf une survivront.
MrMas
11
Vous pouvez également combiner des formateurs, par exemple class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): passet ensuite formatter_class=Formatter.
Terry Brown
79

Si vous souhaitez simplement remplacer la seule option, vous ne devez pas utiliser RawTextHelpFormatter. Au lieu de cela, sous-classe le HelpFormatteret fournissez une introduction spéciale pour les options qui doivent être traitées "brutes" (j'utilise "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)

Et utilisez-le:

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

Tout autre appel à l' .add_argument()endroit où l'aide ne démarre pas R|sera encapsulé normalement.

Cela fait partie de mes améliorations sur argparse . Le SmartFormatter complet prend également en charge l'ajout des valeurs par défaut à toutes les options et la saisie brute de la description des utilitaires. La version complète a sa propre _split_linesméthode, de sorte que toute mise en forme effectuée par exemple pour les chaînes de version est préservée:

parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")
Anthon
la source
Je veux le faire pour un message de version, mais ce SmartFormatter ne semble fonctionner qu'avec le texte d'aide, pas le texte de la version spéciale. parser.add_argument('-v', '--version', action='version',version=get_version_str()) Est-il possible de l'étendre à ce cas?
mc_electron
@mc_electron la version complète du SmartFormatter a aussi sa propre _split_lineset conserve les sauts de ligne (pas besoin de spécifier "R |" au début, si vous voulez cette option, corrigez la _VersionAction.__call__méthode
Anthon
Je ne suis pas tout à fait en train de bouder la première partie de votre commentaire, bien que je puisse voir _VersionAction.__call__que je le souhaiterais probablement au parser.exit(message=version)lieu d'utiliser la version formatée. Est-il possible de le faire sans publier une copie corrigée d'argparse?
mc_electron
@mc_electron Je fais référence aux améliorations que j'ai publiées sur bitbucket (selon le lien vers mes améliorations sur argparse dans la réponse). Mais vous pouvez aussi patcher le __call__dans _VersionActionen faisant , argparse._VersionAction.__call__ = smart_versionaprès avoir définidef smart_version(self, parser, namespace, values, option_string=None): ...
Anthon
Bonne idée. Cela ne m'a pas aidé car l'épilogue et la description ne semblent pas passer par _split_lines :(
Pod
31

Une autre façon simple de le faire est d'inclure un habillage de texte .

Par exemple,

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 cette façon, nous pouvons éviter le long espace vide devant chaque ligne de sortie.

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
la source
11

J'ai rencontré un problème similaire (Python 2.7.6). J'ai essayé de décomposer la section de description en plusieurs lignes en utilisant RawTextHelpFormatter:

parser = ArgumentParser(description="""First paragraph 

                                       Second paragraph

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

options = parser.parse_args()

Et j'ai:

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

Premier paragraphe 

                        La deuxième paragraphe

                        Troisième paragraphe

arguments facultatifs:
  -h, --help afficher ce message d'aide et quitter

Ce RawTextHelpFormattern'est donc pas une solution. Parce qu'il imprime la description telle qu'elle apparaît dans le code source, en préservant tous les caractères d'espaces (je veux garder des onglets supplémentaires dans mon code source pour plus de lisibilité mais je ne veux pas les imprimer tous. trop long, plus de 80 caractères par exemple).

Merci à @Anton qui a inspiré la bonne direction ci-dessus . Mais cette solution a besoin d'une légère modification afin de formater la section de description .

Quoi qu'il en soit, un formateur personnalisé est nécessaire. J'ai étendu la HelpFormatterclasse existante et la _fill_textméthode de remplacement comme ceci:

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

Comparez avec le code source d'origine provenant du module 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)

Dans le code d'origine, la description entière est en cours d'emballage. Dans le formateur personnalisé ci-dessus, le texte entier est divisé en plusieurs morceaux et chacun d'eux est formaté indépendamment.

Donc, à l'aide d'un formateur personnalisé:

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

options = parser.parse_args()

la sortie est:

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

Premier paragraphe

La deuxième paragraphe

Troisième paragraphe

arguments facultatifs:
  -h, --help afficher ce message d'aide et quitter
flaz14
la source
1
C'est merveilleux - cela s'est produit après avoir presque abandonné et envisagé de simplement réimplémenter l'argument d'aide ... m'a sauvé beaucoup de tracas.
Paul Gowder
2
le sous HelpFormatter-classement est problématique car les développeurs argparse garantissent seulement que le nom de classe survivra dans les futures versions d'argparse. En gros, ils se sont fait un chèque en blanc afin de pouvoir changer les noms de méthode si cela leur convient. Je trouve cela frustrant; le moins qu'ils auraient pu faire est d'exposer quelques méthodes dans l'API.
MrMas
Pas tout à fait ce que le PO demandait, mais exactement ce que je voulais, merci!
Huw Walters
2

Je voulais avoir à la fois des sauts de ligne manuels dans le texte de description et un habillage automatique de celui-ci; mais aucune des suggestions ici n'a fonctionné pour moi - j'ai donc fini par modifier la classe SmartFormatter donnée dans les réponses ici; les problèmes avec les noms de méthode argparse n'étant pas une API publique nonobstant, voici ce que j'ai (comme un fichier appelé 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()

Voici comment cela fonctionne en 2.7 et 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
la source
1

En partant de SmartFomatter décrit ci-dessus, je suis arrivé à cette solution:

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

Notez que l'étrange argument formatter_class passé à l'analyseur de niveau supérieur n'est pas hérité par sub_parsers, il faut le passer à nouveau pour chaque sub_parser créé.

ermitz
la source
0

Préface

Pour cette question, argparse.RawTextHelpFormatterest utile pour moi.

Maintenant, je veux partager comment utiliser le argparse.

Je sais que ce n'est peut-être pas lié à la question,

mais ces questions me dérangent depuis un moment.

Je veux donc partager mon expérience, j'espère que cela sera utile pour quelqu'un.

Et c'est parti.

Modules tiers

colorama : pour changer la couleur du texte:pip install colorama

Fait fonctionner les séquences de caractères d'échappement ANSI (pour produire du texte de terminal coloré et le positionnement du curseur) sous MS Windows

Exemple

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)

Où la classe de FormatTextest la suivante

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

entrez la description de l'image ici

Carson
la source