Python argparse: как вставить новую строку в текст справки?

341

Я использую argparseв Python 2.7 для анализа параметров ввода. Один из моих вариантов - это множественный выбор. Я хочу сделать список в тексте справки, например,

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

Тем не менее, argparseудаляет все новые строки и последовательные пробелы. Результат выглядит как

~ / Загрузки: 52 $ python2.7 x.py -h
использование: x.py [-h] [-g {a, b, g, d, e}]

тест

необязательные аргументы:
  -h, --help показать это справочное сообщение и выйти
  -g {a, b, g, d, e} Некоторая опция, где a = альфа b = бета g = гамма d = дельта e
                  = эпсилон

Как вставить новые строки в тексте справки?

kennytm
источник
У меня нет с собой Python 2.7, поэтому я могу проверить свои идеи. Как насчет использования текста справки в тройных кавычках ("" "" ""). Выживают ли новые линии, используя это?
pyfunc
4
@pyfunc: Нет. Удаление выполняется во время выполнения argparse, а не переводчиком, поэтому переключение на """..."""не поможет.
Кеннитм
Это сработало для меня
кардамон

Ответы:

394

Попробуйте использовать RawTextHelpFormatter:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)
Михал Квятковский
источник
6
Я думаю, что это не так. Вы можете Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail. создать подкласс этого класса, но, к сожалению, так что, вероятно, это не очень хорошая идея, хотя это может и не иметь значения, поскольку предполагается, что 2.7 будет последним 2.x питоном, и вы все равно должны будете реорганизовать многие вещи для 3.x Я на самом деле запускаю 2.6 с argparseустановленным через, easy_installтак что документация сама может быть устаревшей.
интуитивно
3
Некоторые ссылки: для python 2.7 и python 3. * . Пакет 2.6 должен, согласно его вики , соответствовать официальному 2.7. Из документа: «Передача RawDescriptionHelpFormatter как formatter_class = указывает на то, что описание и эпилог уже правильно отформатированы и не должны быть перенесены в строку»
Стефано
83
Попробуйте вместо formatter_class =, RawDescriptionHelpFormatterкоторый работает только на описание и эпилог, а не текст справки.
MarkHu
3
Я заметил, что даже с RawTextHelpFormatter, ведущие и конечные переводы строки удаляются. Чтобы обойти это, вы можете просто добавить два или более последовательных символа новой строки; все, кроме одного перевода строки, выживут.
MrMas
11
Вы также можете комбинировать форматеры, например, class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): passи тогда formatter_class=Formatter.
Терри Браун
79

Если вы просто хотите переопределить одну опцию, вы не должны использовать RawTextHelpFormatter. Вместо этого создайте подкласс HelpFormatterи выделите специальное вступление для параметров, которые должны обрабатываться «raw» (я использую "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)

И использовать это:

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

Любые другие вызовы, с .add_argument()которых не начинается помощь, R|будут упакованы как обычно.

Это часть моих улучшений в argparse . Полный SmartFormatter также поддерживает добавление значений по умолчанию ко всем параметрам и необработанный ввод описания утилит. Полная версия имеет свой собственный _split_linesметод, так что любое форматирование, например, для строк версии, сохраняется:

parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")
Энтон
источник
Я хочу сделать это для сообщения о версии, но кажется, что этот SmartFormatter работает только с текстом справки, а не с текстом специальной версии. parser.add_argument('-v', '--version', action='version',version=get_version_str()) Можно ли распространить это на этот случай?
mc_electron
@mc_electron полная версия SmartFormatter также имеет свою собственную _split_linesи сохраняет разрывы строк (нет необходимости указывать «R |» в начале, если вы хотите эту опцию, исправьте _VersionAction.__call__метод
Anthon
Я не совсем прислушиваюсь к первой части вашего комментария, хотя могу видеть, _VersionAction.__call__что, скорее всего, я бы хотел, чтобы parser.exit(message=version)вместо использования отформатированной версии. Есть ли способ сделать это, не выпуская исправленную копию argparse?
mc_electron
@mc_electron Я имею в виду улучшения, которые я опубликовал для bitbucket (согласно ссылке на мои улучшения argparse в ответе). Но вы также можете пропатчить __call__в _VersionActionделая argparse._VersionAction.__call__ = smart_versionпосле определенияdef smart_version(self, parser, namespace, values, option_string=None): ...
Anthon
Отличная идея. Мне не помогло, так как эпилог и описание, похоже, не проходят через _split_lines :(
Pod
31

Еще один простой способ сделать это - включить текстовую переноску .

Например,

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

Таким образом, мы можем избежать длинного пустого пространства перед каждой выходной строкой.

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 ...
Ван Цзунань
источник
11

Я столкнулся с подобной проблемой (Python 2.7.6). Я попытался разбить раздел описания на несколько строк, используя RawTextHelpFormatter:

parser = ArgumentParser(description="""First paragraph 

                                       Second paragraph

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

options = parser.parse_args()

И получил:

использование: play-with-argparse.py [ОПЦИИ]

Первый параграф 

                        Второй абзац

                        Третий абзац

необязательные аргументы:
  -h, --help показать это справочное сообщение и выйти

Так что RawTextHelpFormatterэто не решение проблемы. Поскольку он печатает описание так, как оно выглядит в исходном коде, сохраняя все пробельные символы (я хочу сохранить дополнительные вкладки в моем исходном коде для удобства чтения, но я не хочу печатать их все. Также необработанный форматировщик не переносит строки, когда это слишком длинный, более 80 символов например).

Спасибо @Anton, который вдохновил правильное направление выше . Но это решение нуждается в небольшой модификации, чтобы отформатировать раздел описания .

В любом случае, нужен специальный форматер. Я расширил существующий HelpFormatterкласс и переопределил _fill_textметод следующим образом:

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

Сравните с исходным исходным кодом из модуля 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)

В оригинальном коде все описание упаковано. В пользовательском редакторе выше весь текст разбит на несколько кусков, и каждый из них форматируется независимо.

Итак, с помощью пользовательского форматера:

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

options = parser.parse_args()

выход:

использование: play-with-argparse.py [ОПЦИИ]

Первый параграф

Второй абзац

Третий абзац

необязательные аргументы:
  -h, --help показать это справочное сообщение и выйти
flaz14
источник
1
Это замечательно - произошло с этим после того, как я почти сдался и обдумывал, просто переосмысливая аргумент помощи в целом ... избавил меня от многих хлопот.
Пол Гоудер
2
HelpFormatterсоздание подклассов проблематично, так как разработчики argparse гарантируют, что имя класса сохранится в будущих версиях argparse. Они в основном написали себе пустую проверку, чтобы они могли изменить имена методов, если им было бы удобно это сделать. Я нахожу это расстраивающим; самое меньшее, что они могли бы сделать, - это раскрыть несколько методов в API.
MrMas
Не совсем то, о чем просил ОП, но именно то , что я хотел, спасибо!
Хью Уолтерс
2

Я хотел, чтобы в тексте описания были как ручные разрывы строк, так и автоматическая обертка; но ни одно из предложенных здесь предложений не сработало для меня - поэтому я в итоге изменил класс SmartFormatter, приведенный в ответах здесь; проблемы с именами методов argparse, не являющимися публичным API, несмотря на это, вот что у меня есть (как файл называется 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()

Вот как это работает в 2.7 и 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
источник
1

Начиная с описанного выше SmartFomatter, я остановился на этом решении:

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

Обратите внимание, что странным образом аргумент formatter_class, передаваемый парсеру верхнего уровня, не наследуется sub_parsers, его необходимо передавать снова для каждого созданного sub_parser.

ermitz
источник
0

Предисловие

На этот вопрос argparse.RawTextHelpFormatter это полезно для меня.

Теперь я хочу поделиться, как я использую argparse.

Я знаю, что это не может быть связано с вопросом,

но эти вопросы меня беспокоили некоторое время.

Поэтому я хочу поделиться своим опытом, надеюсь, что это будет кому-то полезно.

Вот так.

Сторонние модули

колорама : для изменения цвета текста:pip install colorama

Заставляет последовательности escape-символов ANSI (для создания цветного терминального текста и позиционирования курсора) работать под MS Windows

пример

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)

Где класс FormatTextявляется следующим

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

введите описание изображения здесь

Carson
источник