Python argparse: Jak wstawić znak nowej linii w tekście pomocy?

340

Używam argparsew Pythonie 2.7 do analizowania opcji wprowadzania. Jedną z moich opcji jest wielokrotny wybór. Chcę zrobić listę w tekście pomocy, np

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

argparseUsuwa jednak wszystkie znaki nowej linii i kolejne spacje. Wynik wygląda jak

~ / Pobrane: 52 $ python2.7 x.py -h
użycie: x.py [-h] [-g {a, b, g, d, e}]

test

opcjonalne argumenty:
  -h, --help pokaż ten komunikat pomocy i wyjdź
  -g {a, b, g, d, e} Pewna opcja, gdzie a = alfa b = beta g = gamma d = delta e
                  = epsilon

Jak wstawić znaki nowej linii w tekście pomocy?

kennytm
źródło
Nie mam ze sobą Pythona 2.7, więc mogę przetestować swoje pomysły. Co powiesz na użycie tekstu pomocy w potrójnych cudzysłowach („” „” „”). Czy nowe linie przetrwają dzięki temu?
pyfunc
4
@pyfunc: Nie. Usuwanie jest wykonywane w czasie wykonywania przez argparseinterpretera, więc przełączenie na """..."""nie pomoże.
kennytm
To zadziałało dla mnie
kardamon

Odpowiedzi:

393

Spróbuj użyć RawTextHelpFormatter:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)
Michał Kwiatkowski
źródło
6
Myślę, że nie jest. Można go podklasować, ale niestety Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail. więc prawdopodobnie nie jest to świetny pomysł, chociaż może nie mieć to znaczenia, ponieważ 2.7 ma być ostatnim pythonem w wersji 2.x i od tego czasu oczekuje się refaktoryzacji wielu rzeczy dla wersji 3.x. W rzeczywistości używam 2.6 z argparsezainstalowanym przez, easy_installwięc sama dokumentacja może być nieaktualna.
intuicyjnie
3
Niektóre linki: dla Pythona 2.7 i Pythona 3. * . Pakiet 2.6 powinien, zgodnie z jego wiki , być zgodny z oficjalnym 2.7. Z dokumentu: „Przekazanie RawDescriptionHelpFormatter jako formatter_class = wskazuje, że opis i epilog są już poprawnie sformatowane i nie powinny być zawijane w wiersz”
Stefano,
83
Spróbuj zamiast tego formatter_class =, RawDescriptionHelpFormatterktóry działa tylko na opis i epilog, a nie na tekst pomocy.
MarkHu
3
Zauważyłem, że nawet z RawTextHelpFormatterwiodącymi i końcowymi znakami nowej linii są usuwane. Aby obejść ten problem, możesz po prostu dodać dwie lub więcej kolejnych linii; wszystkie oprócz jednej nowej linii przetrwają.
MrMas
11
Możesz także łączyć formatery, np. class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): passI wtedy formatter_class=Formatter.
Terry Brown
79

Jeśli chcesz tylko zastąpić jedną opcję, nie powinieneś jej używać RawTextHelpFormatter. Zamiast tego podklasuj HelpFormatteri zapewnij specjalne wprowadzenie dla opcji, które powinny być obsługiwane jako „surowe” (używam "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)

I użyj tego:

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

Wszelkie inne połączenia z .add_argument()miejscem, w którym pomoc się nie zaczyna, R|będą pakowane normalnie.

Jest to część moich ulepszeń dotyczących argparse . Pełny SmartFormatter obsługuje także dodawanie wartości domyślnych do wszystkich opcji i nieprzetworzone wprowadzanie opisu narzędzi. Pełna wersja ma własną _split_linesmetodę, dzięki czemu zachowane jest formatowanie np. Ciągów wersji:

parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")
Anthon
źródło
Chcę to zrobić dla komunikatu wersji, ale ten SmartFormatter wydaje się działać tylko z tekstem pomocy, a nie tekstem wersji specjalnej. parser.add_argument('-v', '--version', action='version',version=get_version_str()) Czy można to rozszerzyć na tę sprawę?
mc_electron
@mc_electron pełna wersja SmartFormatter ma również własne _split_linesi zachowuje podziały wierszy (nie trzeba podawać „R |” na początku, jeśli chcesz tę opcję, załataj _VersionAction.__call__metodę
Anthon
Nie do końca zastanawiam się nad pierwszą częścią twojego komentarza, chociaż widzę w _VersionAction.__call__tym, że prawdopodobnie wolałbym, aby parser.exit(message=version)zamiast używać sformatowanej wersji. Czy jest jednak jakiś sposób, aby to zrobić bez wydawania poprawionej kopii argparse?
mc_electron
@mc_electron Mam na myśli ulepszenia, które opublikowałem na bitbucket (zgodnie z linkiem do moich ulepszeń dotyczących argparse w odpowiedzi). Ale można też łaty __call__na _VersionActionwykonując argparse._VersionAction.__call__ = smart_versionpo zdefiniowaniudef smart_version(self, parser, namespace, values, option_string=None): ...
Anthon
Świetny pomysł. Nie pomogło mi, ponieważ epilog i opis nie wydają się przebiegać przez _split_lines :(
Pod
31

Innym łatwym sposobem na to jest zawijanie tekstu .

Na przykład,

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

W ten sposób możemy uniknąć długiej pustej przestrzeni przed każdą linią wyjściową.

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
źródło
11

Napotkałem podobny problem (Python 2.7.6). Próbowałem rozbić sekcję opisu na kilka wierszy, używając RawTextHelpFormatter:

parser = ArgumentParser(description="""First paragraph 

                                       Second paragraph

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

options = parser.parse_args()

I dostał:

wykorzystanie: play-with-argparse.py [OPCJE]

Pierwszy paragraf 

                        Akapit drugi

                        Akapit trzeci

opcjonalne argumenty:
  -h, --help pokaż ten komunikat pomocy i wyjdź

To RawTextHelpFormatternie jest rozwiązanie. Ponieważ drukuje opis w takiej postaci, w jakiej pojawia się w kodzie źródłowym, zachowując wszystkie znaki spacji (chcę zachować dodatkowe tabulatory w kodzie źródłowym dla czytelności, ale nie chcę drukować ich wszystkich. Również formatter raw nie zawija wiersza, gdy jest za długa, na przykład ponad 80 znaków).

Dzięki @Anton, który zainspirował właściwy kierunek powyżej . Ale to rozwiązanie wymaga niewielkiej modyfikacji, aby sformatować sekcję opisu .

W każdym razie potrzebny jest niestandardowy formatyzator. Rozszerzyłem istniejącą HelpFormatterklasę i zastąpiłem _fill_texttaką metodę:

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

Porównaj z oryginalnym kodem źródłowym pochodzącym z modułu 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)

W oryginalnym kodzie cały opis jest zawijany. W niestandardowym formatorze powyżej cały tekst jest podzielony na kilka części i każdy z nich jest formatowany niezależnie.

Tak więc za pomocą niestandardowego formatyzatora:

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

options = parser.parse_args()

dane wyjściowe to:

wykorzystanie: play-with-argparse.py [OPCJE]

Pierwszy paragraf

Akapit drugi

Akapit trzeci

opcjonalne argumenty:
  -h, --help pokaż ten komunikat pomocy i wyjdź
flaz14
źródło
1
To jest cudowne - stało się to po tym, jak prawie się poddałem i zastanawiałem się nad ponownym zaimplementowaniem argumentu pomocy ... uratowało mi to sporo kłopotów.
Paul Gowder
2
podklasowanie HelpFormatterjest problematyczne, ponieważ programiści argparse gwarantują, że nazwa klasy przetrwa w przyszłych wersjach argparse. Zasadniczo napisali sobie czek in blanco, aby mogli zmienić nazwy metod, jeśli byłoby to dla nich wygodne. To mnie frustruje; przynajmniej mogliby zrobić, to ujawnić kilka metod w API.
MrMas
Nie do końca to, o co prosił PO, ale dokładnie to , czego chciałem, dzięki!
Huw Walters
2

Chciałem mieć zarówno ręczne podziały wierszy w tekście opisu, jak i automatyczne zawijanie go; ale żadna z sugestii tutaj nie działała dla mnie - więc ostatecznie zmodyfikowałem klasę SmartFormatter podaną w odpowiedziach tutaj; pomimo tego, że nazwy metod argparse nie są publicznymi interfejsami API, oto co mam (jako plik o nazwie 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()

Tak to działa w wersjach 2.7 i 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
źródło
1

Począwszy od SmartFomatter opisanego powyżej, skończyłem na tym rozwiązaniu:

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

Zauważ, że o dziwo argument formatter_class przekazany do parsera najwyższego poziomu nie jest dziedziczony przez sub_parsers, należy przekazać go ponownie dla każdego utworzonego sub_parser.

Ermitz
źródło
0

Przedmowa

To pytanie argparse.RawTextHelpFormatterjest dla mnie pomocne.

Teraz chcę podzielić się, jak korzystać z argparse.

Wiem, że to nie może być związane z pytaniem,

ale te pytania niepokoiły mnie przez jakiś czas.

Chcę więc podzielić się swoim doświadczeniem, mam nadzieję, że będzie to dla kogoś pomocne.

No to ruszamy.

Moduły stron trzecich

colorama : aby zmienić kolor tekstu:pip install colorama

Sprawia, że ​​sekwencje znaków ANSI (do tworzenia kolorowego tekstu terminala i pozycjonowania kursora) działają w MS Windows

Przykład

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)

Gdzie klasa FormatTextjest następująca

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

wprowadź opis zdjęcia tutaj

Carson
źródło