Поговорим об улучшении использования argparse и подкоманд в повседневной жизни.
В моей практике почти в каждом проекте есть интерфейс для командной строки, это может быть manage.py в веб проекте, просто скрипт бекапа или даже приложение GTK. В python 2.7 и 3.2 появился очень мощный модуль argparse для обработки параметров командной строки, и в нем есть “из коробки” поддержка подкоманд и это очень круто. Но есть в этом модуле маленький недостаток - интерфейс его использования немного избыточен.
Для начала нужно глянуть что уж такого плохого в интерфейсе, рассмотрим простой пример:
#!/usr/bin/env python import argparse def run_test(module, settings=None): pass def run_server(host, port, no_reload=False, settings=None): pass def parse_args(args=None): parser = argparse.ArgumentParser(prog='app') cmds = parser.add_subparsers(help='commands') cmd_run = cmds.add_parser('run', help='start dev server') cmd_run.add_argument('-s', '--settings', help='application settings') cmd_run.add_argument( '-P', '--port', type=int, default=8000, help='server port' ) cmd_run.add_argument( '-H', '--host', default='localhost', help='server host' ) cmd_run.add_argument( '--no-reload', action='store_true', help='without reloading' ) cmd_run.set_defaults(func=lambda a: ( run_server(a.host, a.port, a.no_reload, settings=a.settings) )) cmd_test = cmds.add_parser('test', aliases=['t', 'te'], help='run tests') cmd_test.add_argument('-s', '--settings', help='application settings') cmd_test.add_argument('target', default='.', help='python module or file') cmd_test.set_defaults( func=lambda a: run_test(a.module, settings=a.settings) ) args = parser.parse_args(args) if not hasattr(args, 'func'): parser.print_usage() else: args.func(args) if __name__ == '__main__': parse_args()
Вроде не так уж все и плохо, обычный интерфейс. Есть дублирование параметра --settings, но чтоб он был привязан к каждой подкоманде его нельзя вешать на базовый парсер. Также нам пришлось переносить строки для соблюдения PEP 8, при том что не помещались считанные символы. Можно укоротить переменные cmd_run, cmd_test до run, test или даже до r, t, но суть не в этом. Эти переменные, в принципе, не нужны, если добавить цепочки вызовов:
cmds.add_parser('run').add_argument('port').set_defaults(func=run_server)
На чистом argparse цепочек вызовов не получится, хотя может в каких-то случаях использования они и не нужны. В моей практике чаще хочется цепочек.
В самом начале примера объявлена пара функций и есть проекты, которые превращают эти функции в подкоманды, типа: opster, argh, komandr. Последние два основаны на argparse, а opster использует getopt.
В некоторых случаях использование подобных улучшаторов выглядит очень клево, например, использование argh:
#!/usr/bin/env python import argh @argh.aliases('t', 'te') def test(module, settings=None): '''run tests''' pass def run(host='localhost', port=8000, no_reload=False, settings=None): '''run dev server''' pass if __name__ == '__main__': argh.dispatch_commands([run, test])
Вывод главного help такой же как из первого примера:
usage: example-argh.py [-h] {run,test,t,te} ... positional arguments: {run,test,t,te} run run dev server test (t, te) run tests optional arguments: -h, --help show this help message and exit
А вот вывод help для определенной подкоманды отличается отсутствием описаний и различием коротких аналогов для параметров:
===pure argparse=== usage: app run [-h] [-s SETTINGS] [-P PORT] [-H HOST] [--no-reload] optional arguments: -h, --help show this help message and exit -s SETTINGS, --settings SETTINGS application settings -P PORT, --port PORT server port -H HOST, --host HOST server host --no-reload without reloading ===argh=== usage: example-argh.py run [-h] [--host HOST] [-p PORT] [-n] [-s SETTINGS] run dev server optional arguments: -h, --help show this help message and exit --host HOST -p PORT, --port PORT -n, --no-reload -s SETTINGS, --settings SETTINGS
В принципе, можно добиться полного соответствия help, но от этого уже будет страдать предельная лаконичность второго примера. Вообще-то, если названия параметров не требуют пояснений, то использовать argh очень заманчиво, тем более он, в принципе, позволяет добраться до обычного argparse, если где-то сталкиваешься с ограничениями.
Вся прелесть argparse, что с python 2.7 и 3.2 он входит в стандартную библиотеку и реально крут по сравнению с тем же getopt и optparse. А перечисленные выше улучшаторы - это отдельные пакеты и таскать их зависимостями в каждый проект не прикольно, особенно если проект минималистичный или небольшой скрипт с подкомандами. Еще в улучшаторах часто присутствует немного магии, argparse же прямой как двери.
Хорошо бы использовать argparse, но как-то покрасивее, чем в первом примере.
Следующий пример - мой любимый способ:
#!/usr/bin/env python import argparse def run_test(module, settings=None): pass def run_server(host, port, no_reload=False, settings=None): pass def parse_args(args=None): parser = argparse.ArgumentParser(prog='app') cmds = parser.add_subparsers(help='commands') def cmd(name, **kw): p = cmds.add_parser(name, **kw) p.set_defaults(cmd=name) p.arg = lambda *a, **kw: p.add_argument(*a, **kw) and p p.exe = lambda f: p.set_defaults(exe=f) and p # global options p.arg('-s', '--settings', help='application settings') return p cmd('run', help='start dev server')\ .arg('-P', '--port', type=int, default=8000, help='server port')\ .arg('-H', '--host', default='localhost', help='server host')\ .arg('--no-reload', action='store_true', help='without reloading')\ .exe(lambda a: ( run_server(a.host, a.port, a.no_reload, settings=a.settings) )) cmd('test', aliases=['t', 'te'], help='run tests')\ .arg('target', default='.', nargs='?', help='python module or file')\ .exe(lambda a: run_test(a.target, settings=a.settings)) args = parser.parse_args(args) if not hasattr(args, 'exe'): parser.print_usage() else: args.exe(args) if __name__ == '__main__': parse_args()
После использования в нескольких местах такого подхода мне все больше нравится отделение интерфейса функции от вызовов командной строки, все таки это немного разные вещи. Хотя раньше мне очень нравилось превращение функций в подкоманды.
Вывод довольно банальный: у python очень крутая стандартная библиотека, argparse - очень мощный инструмент для работы с параметрами командной строки. И даже если есть какие-то библиотеки с красивыми плюшками (argh, opster или docopt) у них скорее всего тоже найдутся свои недостатки, поэтому мой выбор - подточить использование argparse и забыть про дополнительные зависимости.