Python argparse: Wie füge ich eine neue Zeile in den Hilfetext ein?

340

Ich benutze argparse in Python 2.7 zum Parsen von Eingabeoptionen. Eine meiner Optionen ist eine Multiple-Choice-Option. Ich möchte eine Liste in seinem Hilfetext erstellen, z

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

argparseEntfernt jedoch alle Zeilenumbrüche und aufeinander folgenden Leerzeichen. Das Ergebnis sieht aus wie

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

Prüfung

optionale Argumente:
  -h, --help Diese Hilfemeldung anzeigen und beenden
  -g {a, b, g, d, e} Eine Option, bei der a = alpha b = beta g = gamma d = delta e
                  = Epsilon

Wie füge ich Zeilenumbrüche in den Hilfetext ein?

kennytm
quelle
Ich habe Python 2.7 nicht dabei, damit ich meine Ideen testen kann. Wie wäre es mit Hilfetext in dreifachen Anführungszeichen ("" "" ""). Überleben die neuen Linien damit?
Pyfunc
4
@pyfunc: Nein. Das Strippen wird zur Laufzeit von argparsenicht dem Interpreter durchgeführt, daher """..."""hilft das Umschalten auf nicht.
Kennytm
Dies funktionierte für mich
Kardamom

Antworten:

392

Versuchen Sie es mit RawTextHelpFormatter:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)
Michał Kwiatkowski
quelle
6
Ich denke es ist nicht. Sie könnten es in eine Unterklasse unterteilen, aber leider ist dies Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail. wahrscheinlich keine großartige Idee, obwohl es möglicherweise keine Rolle spielt, da 2.7 die letzte 2.x-Python sein soll und Sie ohnehin viele Dinge für 3.x umgestalten müssen. Ich verwende tatsächlich 2.6 mit argparseinstalliertem Via, easy_installdamit die Dokumentation selbst möglicherweise veraltet ist.
Intuitiert
3
Einige Links: für Python 2.7 und Python 3. * . Das 2.6-Paket sollte laut Wiki dem offiziellen 2.7- Paket entsprechen . Aus dem Dokument: "Das Übergeben von RawDescriptionHelpFormatter als formatter_class = zeigt an, dass Beschreibung und Epilog bereits korrekt formatiert sind und nicht in Zeilen eingeschlossen werden sollten"
Stefano
83
Versuchen Sie stattdessen formatter_class =, RawDescriptionHelpFormatterdas nur für Beschreibung und Epilog und nicht für Hilfetext funktioniert.
MarkHu
3
Mir ist aufgefallen, dass auch mit RawTextHelpFormatter, führende und nachfolgende Zeilenumbrüche entfernt werden. Um dies zu umgehen, können Sie einfach zwei oder mehr aufeinanderfolgende Zeilenumbrüche hinzufügen. Alle bis auf eine Newline werden überleben.
MrMas
11
Sie können auch Formatierer kombinieren, z . B. class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): passund dann formatter_class=Formatter.
Terry Brown
78

Wenn Sie nur die eine Option überschreiben möchten, sollten Sie sie nicht verwenden RawTextHelpFormatter. Unterklasse stattdessen HelpFormatterund bieten ein spezielles Intro für die Optionen, die "roh" behandelt werden sollen (ich benutze "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)

Und benutze es:

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

Alle anderen Anrufe, bei .add_argument()denen die Hilfe nicht beginnt, R|werden wie gewohnt verpackt.

Dies ist Teil meiner Verbesserungen bei Argparse . Der vollständige SmartFormatter unterstützt auch das Hinzufügen der Standardeinstellungen zu allen Optionen und die Roheingabe der Dienstprogrammbeschreibung. Die Vollversion verfügt über eine eigene _split_linesMethode, sodass alle Formatierungen, z. B. für Versionszeichenfolgen, beibehalten werden:

parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")
Anthon
quelle
Ich möchte dies für eine Versionsnachricht tun, aber dieser SmartFormatter scheint nur mit Hilfetext zu funktionieren, nicht mit dem speziellen Versionstext. parser.add_argument('-v', '--version', action='version',version=get_version_str()) Ist es möglich, es auf diesen Fall auszudehnen?
mc_electron
@mc_electron Die Vollversion des SmartFormatter hat auch eine eigene _split_linesund behält Zeilenumbrüche bei (keine Notwendigkeit, "R |" am Anfang anzugeben, wenn Sie diese Option wünschen, patchen Sie die _VersionAction.__call__Methode
Anthon
Ich bin nicht ganz begeistert von dem ersten Teil Ihres Kommentars, obwohl ich darin sehen kann, _VersionAction.__call__dass ich es wahrscheinlich nur wollen würde, parser.exit(message=version)anstatt die formatierte Version zu verwenden. Gibt es eine Möglichkeit, dies zu tun, ohne eine gepatchte Kopie von argparse zu veröffentlichen?
mc_electron
@mc_electron Ich beziehe mich auf die Verbesserungen, die ich auf bitbucket veröffentlicht habe (gemäß dem Link zu meinen Verbesserungen an argparse in der Antwort). Aber man kann das auch Patch __call__in _VersionActionindem Sie argparse._VersionAction.__call__ = smart_versionnach der Definitiondef smart_version(self, parser, namespace, values, option_string=None): ...
Anthon
Großartige Idee. Hat mir nicht geholfen, da der Epilog und die Beschreibung nicht durch _split_lines zu laufen scheinen :(
Pod
31

Eine andere einfache Möglichkeit besteht darin, Textwrap einzuschließen .

Zum Beispiel,

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

Auf diese Weise können wir den langen leeren Raum vor jeder Ausgabezeile vermeiden.

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

Ich habe ein ähnliches Problem (Python 2.7.6). Ich habe versucht, den Beschreibungsabschnitt in mehrere Zeilen zu unterteilen, indem ich RawTextHelpFormatter:

parser = ArgumentParser(description="""First paragraph 

                                       Second paragraph

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

options = parser.parse_args()

Und bekam:

Verwendung: play-with-argparse.py [OPTIONEN]

Erster Paragraph 

                        Zweiter Absatz

                        Dritter Absatz

optionale Argumente:
  -h, --help Diese Hilfemeldung anzeigen und beenden

Ist RawTextHelpFormatteralso keine Lösung. Da die Beschreibung so gedruckt wird, wie sie im Quellcode angezeigt wird, werden alle Leerzeichen beibehalten (ich möchte aus Gründen der Lesbarkeit zusätzliche Registerkarten in meinem Quellcode behalten, aber nicht alle drucken. Auch der Rohformatierer umbricht keine Zeile, wenn dies der Fall ist zu lang, zum Beispiel mehr als 80 Zeichen).

Vielen Dank an @Anton, der die richtige Richtung oben inspiriert hat . Diese Lösung muss jedoch geringfügig geändert werden, um die Beschreibung zu formatieren .

Auf jeden Fall wird ein benutzerdefinierter Formatierer benötigt. Ich habe die vorhandene HelpFormatterKlasse erweitert und die _fill_textMethode wie folgt überschrieben :

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

Vergleichen Sie mit dem ursprünglichen Quellcode aus dem argparse- Modul:

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)

Im Originalcode wird die gesamte Beschreibung verpackt. Im obigen benutzerdefinierten Formatierer wird der gesamte Text in mehrere Abschnitte aufgeteilt und jeder von ihnen wird unabhängig formatiert.

Also mit Hilfe eines benutzerdefinierten Formatierers:

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

options = parser.parse_args()

Die Ausgabe ist:

Verwendung: play-with-argparse.py [OPTIONEN]

Erster Paragraph

Zweiter Absatz

Dritter Absatz

optionale Argumente:
  -h, --help Diese Hilfemeldung anzeigen und beenden
flaz14
quelle
1
Das ist wunderbar - geschah dies, nachdem ich beinahe aufgegeben und darüber nachgedacht hatte, das Hilfsargument insgesamt neu umzusetzen ... ersparte mir eine Menge Ärger.
Paul Gowder
2
Unterklassen HelpFormattersind problematisch, da die Argparse-Entwickler nur garantieren, dass der Klassenname in zukünftigen Versionen von Argparse überlebt. Sie haben sich im Grunde genommen einen Blankoscheck ausgestellt, damit sie die Methodennamen ändern können, wenn dies für sie zweckmäßig wäre. Ich finde das frustrierend; Das Mindeste, was sie hätten tun können, sind einige Methoden in der API.
MrMas
Nicht ganz das, wonach das OP gefragt hatte, aber genau das , was ich wollte, danke!
Huw Walters
2

Ich wollte sowohl manuelle Zeilenumbrüche im Beschreibungstext als auch einen automatischen Zeilenumbruch. Aber keiner der Vorschläge hier hat für mich funktioniert. Daher habe ich die SmartFormatter-Klasse geändert, die in den Antworten hier angegeben ist. Obwohl die Probleme mit den Namen der argparse-Methode keine öffentliche API sind, habe ich Folgendes (als Datei mit dem Namen 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()

So funktioniert es in 2.7 und 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
quelle
1

Ausgehend von dem oben beschriebenen SmartFomatter endete ich mit dieser Lösung:

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

Beachten Sie, dass das an den Parser der obersten Ebene übergebene Argument formatter_class seltsamerweise nicht von sub_parsers geerbt wird. Es muss für jeden erstellten sub_parser erneut übergeben werden.

ermitz
quelle
0

Vorwort

Für diese Frage argparse.RawTextHelpFormatterist mir hilfreich.

Jetzt möchte ich mitteilen, wie ich das verwende argparse.

Ich weiß, dass es möglicherweise nicht mit Fragen zusammenhängt,

Aber diese Fragen haben mich eine Weile beschäftigt.

Ich möchte meine Erfahrungen teilen und hoffe, dass dies für jemanden hilfreich ist.

Auf geht's.

Module von Drittanbietern

colorama : zum Ändern der Textfarbe :pip install colorama

Lässt ANSI-Escape-Zeichenfolgen (zur Erzeugung von farbigem Terminaltext und zur Positionierung des Cursors) unter MS Windows funktionieren

Beispiel

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)

Wo die Klasse von FormatTextist die folgende

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

Geben Sie hier die Bildbeschreibung ein

Carson
quelle