(полу-) автоматическая генерация аргументов для функций

tldnr: есть ли способ для данной функции автоматически создать ArgumentParser из ее подписи?

У меня есть куча функций, которые я хотел бы открыть для командной строки. Итак, в основном модуль:

 def copy(foo, bar, baz):
    ...
 def move(from, to):
    ...
 def unlink(parrot, nomore=True):
    ...

 if __name__ == '__main__':
     argparse stuff

который можно вызвать из командной строки следующим образом:

 python commands.py move spam ham
 python commands.py unlink --parrot Polly

Хотя это довольно просто реализовать, здесь задействовано много проводки:

parser = argparse.ArgumentParser(...)
subparsers = parser.add_subparsers()
...
c = subparsers.add_parser('unlink', description='Unlink a parrot')
c.add_argument('--parrot', help='parrots name', required=True)
c.add_argument('--nomore', help='this parrot is no more', action='store_true')
...
c = subparsers.add_parser('move', description='Move stuff')
...

и так далее для каждой функции. Хуже всего то, что если аргументы функции меняются (а они меняются), argparse нужно синхронизировать вручную.

Было бы намного лучше, если бы функции могли сами предоставлять argparse, чтобы основной код выглядел так:

parser = argparse.ArgumentParser(...)
subparsers = parser.add_subparsers()

copy.register(subparsers)
move.register(subparsers)
unlink.register(subparsers)
...

Я подумал о чем-то в этом духе:

@args(
    description='Unlink a parrot',
    parrot={'required':True, 'help':'parrots name'},
    nomore={'action': 'store_true', 'help': 'this parrot is no more'}
)
def unlink(parrot, nomore=True):
    ...

Мои вопросы:

  • есть ли библиотека, которая делает что-то подобное?
  • если нет, то можно ли написать такой декоратор и как?
  • есть ли другой/лучший способ реализовать то, что я хочу?

Обновление:

plac кажется решением. Вот как сделать то, что я хочу, с помощью plac:

модуль команд: cmds.py:

import plac

@plac.annotations(
    foo=('the foo thing'),
    bar=('the bar thing'),
    fast=('do a fast copy', 'flag')
)
def copy(foo, bar, fast=False):
    """Copy some foo to bar."""
    pass
        
@plac.annotations(
    parrots=('parrots names'),
    nomore=('these parrots are no more', 'flag'),
    repeat=('repeat n times', 'option', 'r', int)
)
def unlink(nomore=False, repeat=1, *parrots):
    """Unlink some parrots."""
    pass

#more commands...

# export commands so that plac knows about them
commands = 'copy', 'unlink'

и вот основной модуль:

import plac
import cmds

plac.call(cmds)

Довольно аккуратно, если вы спросите меня.


person georg    schedule 06.11.2012    source источник
comment
Вероятно, вам придется добавить аннотации к вашим функциям. Тогда должна быть возможность написать функцию, которая с учетом аннотации функции возвращает парсер для функции.   -  person Bakuriu    schedule 06.11.2012


Ответы (5)


Вы пробовали использовать plac?

Пример в документах:

# dbcli.py
import plac
from sqlalchemy.ext.sqlsoup import SqlSoup

@plac.annotations(
    db=plac.Annotation("Connection string", type=SqlSoup),
    header=plac.Annotation("Header", 'flag', 'H'),
    sqlcmd=plac.Annotation("SQL command", 'option', 'c', str, metavar="SQL"),
    delimiter=plac.Annotation("Column separator", 'option', 'd'),
    scripts=plac.Annotation("SQL scripts"),
    )
def main(db, header, sqlcmd, delimiter="|", *scripts):
    "A script to run queries and SQL scripts on a database"
    yield 'Working on %s' % db.bind.url

    if sqlcmd:
        result = db.bind.execute(sqlcmd)
        if header: # print the header
            yield delimiter.join(result.keys())
        for row in result: # print the rows
            yield delimiter.join(map(str, row))

    for script in scripts:
        db.bind.execute(open(script).read())
        yield 'executed %s' % script

if __name__ == '__main__':
    for output in plac.call(main):
        print(output)

Выход:

usage: dbcli.py [-h] [-H] [-c SQL] [-d |] db [scripts [scripts ...]]

A script to run queries and SQL scripts on a database

positional arguments:
  db                    Connection string
  scripts               SQL scripts

optional arguments:
  -h, --help            show this help message and exit
  -H, --header          Header
  -c SQL, --sqlcmd SQL  SQL command
  -d |, --delimiter |   Column separator
person iMom0    schedule 06.11.2012

Функциональность, аналогичная plac, предоставляется argh, который особенно отличается простым созданием подпарсеров (подобных тем, что находятся в git или django-admin.py).

Пример из его документов:

from argh import *

def dump(args):
    return db.find()

@command
def load(path, format='json'):
    print loaders[format].load(path)

p = ArghParser()
p.add_commands([load, dump])

if __name__ == '__main__':
    p.dispatch()

Производит следующий ответ --help:

usage: prog.py [-h] {load,dump} ...

positional arguments:
  {load,dump}
    load
    dump

optional arguments:
  -h, --help   show this help message and exit

и следующее с load --help:

usage: prog.py load [-h] [-f FORMAT] path

positional arguments:
  path

optional arguments:
  -h, --help            show this help message and exit
  -f FORMAT, --format FORMAT

Аргументы могут быть аннотированы:

@arg('path')
@arg('--format', choices=['yaml','json'], default='json')
@arg('--dry-run', default=False)
@arg('-v', '--verbosity', choices=range(0,3), default=1)
def load(args, LOADERS={'json': json.load, 'yaml': yaml.load}):
    loader = loaders[args.format]
    data = loader(open(args.path))
    ...

А с @plain_signature аргумент args для load расширяется до аргументов ключевого слова:

@arg('path')
@arg('--format', choices=['yaml','json'], default='json')
@arg('--dry-run', default=False)
@arg('-v', '--verbosity', choices=range(0,3), default=1)
@plain_signature
def load(path, format, dry_run, verbosity):
    ...
person joeln    schedule 11.11.2012
comment
Спасибо, выглядит интересно. Там также есть список похожих проектов в plac docs. - person georg; 11.11.2012
comment
Кажется, у argh тоже есть такой список... и он длиннее :P - person joeln; 11.11.2012
comment
Argh теперь также поддерживает естественный синтаксис, то есть вы пишете простые функции Python, а остальное — дело Argh. - person Andy Mikhaylenko; 29.10.2013

Библиотека с наименьшим шаблоном, которую я нашел, это fire (pip install fire).

Создать синтаксический анализатор командной строки для вашего примера так же просто, как:

import fire

def copy(foo, bar, baz):
...
def unlink(parrot, nomore=True):
...

if __name__ == '__main__':
    fire.Fire()

и это превращает ваш модуль в CLI «Fire»:

python your_module.py copy sim sala bim
person Jakub Kukul    schedule 07.03.2017

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

Например, вам также нужно будет дать описание. Эту информацию вы можете в значительной степени предоставить, создав строку документации в соответствующем формате. Существуют парсеры для строк документации (например, Sphynx). Используя эту дополнительную информацию, я думаю, вы сможете автоматически генерировать argparse вызовы для ваших функций.

Я не думаю, что декоратор требуется, так как, вероятно, вся информация может храниться в вашей строке документации.

Дайте мне знать, как у вас дела, мне интересны результаты вашего проекта.

person Hans Then    schedule 06.11.2012
comment
Спасибо! Разбор строк документации звучит заманчиво, но я думаю, что пока останусь с plac (см. обновление). - person georg; 09.11.2012

Другой интересной альтернативой является commando модуль python в качестве декларативного интерфейса для argparse с дополнительными утилитами.

Пример

Без коммандос:

def main():
    parser = argparse.ArgumentParser(description='hyde - a python static website generator',
                                  epilog='Use %(prog)s {command} -h to get help on individual commands')
    parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__)
    parser.add_argument('-s', '--sitepath', action='store', default='.', help="Location of the hyde site")
    subcommands = parser.add_subparsers(title="Hyde commands",
                                     description="Entry points for hyde")
    init_command = subcommands.add_parser('init', help='Create a new hyde site')
    init_command.set_defaults(run=init)
    init_command.add_argument('-t', '--template', action='store', default='basic', dest='template',
                     help='Overwrite the current site if it exists')
    init_command.add_argument('-f', '--force', action='store_true', default=False, dest='force',
                     help='Overwrite the current site if it exists')
    args = parser.parse_args()
    args.run(args)

def init(self, params):
    print params.sitepath
    print params.template
    print params.overwrite

С коммандос:

class Engine(Application):

    @command(description='hyde - a python static website generator',
            epilog='Use %(prog)s {command} -h to get help on individual commands')
    @param('-v', '--version', action='version', version='%(prog)s ' + __version__)
    @param('-s', '--sitepath', action='store', default='.', help="Location of the hyde site")
    def main(self, params): pass

    @subcommand('init', help='Create a new hyde site')
    @param('-t', '--template', action='store', default='basic', dest='template',
            help='Overwrite the current site if it exists')
    @param('-f', '--force', action='store_true', default=False, dest='overwrite',
            help='Overwrite the current site if it exists')
    def init(self, params):
        print params.sitepath
        print params.template
        print params.overwrite
person shakaran    schedule 02.04.2013
comment
Спасибо, выглядит интересно! - person georg; 02.04.2013