2013-09-06 12 views
7

Czy argparse udostępnia wbudowane funkcje umożliwiające przekształcanie grup lub parserów w ich własne przestrzenie nazw? Czuję, że muszę gdzieś opuścić jakąś opcję.podkomendy argparse z zagnieżdżonymi przestrzeniami nazw

Edycja: Ten przykład prawdopodobnie nie jest dokładnie tym, co powinienem zrobić, aby utworzyć strukturę parsera, aby spełnić mój cel, ale to właśnie opracowałem do tej pory. Moim konkretnym celem jest zapewnienie grupom opcji opcji, które są analizowane w polach nazw. Pomysł, jaki miałem z rodzicem, polegał po prostu na użyciu wspólnych opcji w tym samym celu.

Przykład:

import argparse 

# Main parser 
main_parser = argparse.ArgumentParser() 
main_parser.add_argument("-common") 

# filter parser 
filter_parser = argparse.ArgumentParser(add_help=False) 
filter_parser.add_argument("-filter1") 
filter_parser.add_argument("-filter2") 

# sub commands 
subparsers = main_parser.add_subparsers(help='sub-command help') 

parser_a = subparsers.add_parser('command_a', help="command_a help", parents=[filter_parser]) 
parser_a.add_argument("-foo") 
parser_a.add_argument("-bar") 

parser_b = subparsers.add_parser('command_b', help="command_b help", parents=[filter_parser]) 
parser_b.add_argument("-biz") 
parser_b.add_argument("-baz") 

# parse 
namespace = main_parser.parse_args() 
print namespace 

To co mam, oczywiście:

$ python test.py command_a -foo bar -filter1 val 
Namespace(bar=None, common=None, filter1='val', filter2=None, foo='bar') 

Ale to kim jestem naprawdę, po:

Namespace(bar=None, common=None, foo='bar', 
      filter=Namespace(filter1='val', filter2=None)) 

I wtedy nawet więcej grup opcje już przeanalizowane w przestrzeni nazw:

Namespace(common=None, 
      foo='bar', bar=None, 
      filter=Namespace(filter1='val', filter2=None), 
      anotherGroup=Namespace(bazers='val'), 
      anotherGroup2=Namespace(fooers='val'), 
     ) 

Znalazłem related question here, ale wymaga to niestandardowego parsowania i wydaje się, że obejmuje tylko naprawdę specyficzną okoliczność.

Czy istnieje opcja, aby powiedzieć argparse, aby przeanalizować określone grupy w polach z polami nazw?

+0

Nie jestem pewien, jak można się spodziewać to zadziałało. Jak już napisałeś, 'filter1' i' filter2' są w parserze najwyższego poziomu, a nie w parserze potomnym o nazwie 'filter'. W jaki sposób argparse może wiedzieć, że chcesz, aby działał jako dziecko w każdym pod-analizatorze, jeśli tak nie jest? – abarnert

+0

@abarnert: Prawdopodobnie powinienem sformatować mój przykład na podstawie twojego pytania. Ponieważ struktura, którą zestawiłem, nie jest właściwa, jak zauważyłeś. Moim celem jest być w stanie zastosować grupy opcji do formatów i sparsować je w przestrzeń nazw. Byłoby miło, gdyby były powszechne, dlatego próbowałem użyć struktury macierzystej. – jdi

+0

Poszukujesz czegoś takiego jak 'pip',' git', itd., Gdzie oprócz globalnych opcji najwyższego poziomu i opcji specyficznych dla każdej podkomendy, dostępne są również opcje współużytkowane przez wiele różnych podkomend (np. opcje '--verbose',' --upgrade' i '--user' na' pip', odpowiednio) i mogą reprezentować to udostępnianie bezpośrednio, zamiast tworzyć je domyślnie (przez kopiowanie grup opcji do wielu akapitów) ? – abarnert

Odpowiedz

9

Jeśli fokus polega tylko na umieszczeniu wybranych argumentów we własnym numerze namespace, a użycie argumentów (i rodziców) ma charakter sporadyczny, to działanie niestandardowe może wystarczyć.

class GroupedAction(argparse.Action):  
    def __call__(self, parser, namespace, values, option_string=None): 
     group,dest = self.dest.split('.',2) 
     groupspace = getattr(namespace, group, argparse.Namespace()) 
     setattr(groupspace, dest, values) 
     setattr(namespace, group, groupspace) 

Istnieje kilka sposobów określania nazwy group. Mogłoby zostać przekazane jako argument przy definiowaniu akcji. Można go dodać jako parametr. Tutaj wybrałem przeanalizowanie go z poziomu dest (dzięki czemu namespace.filter.filter1 może uzyskać wartość filter.filter1.

# Main parser 
main_parser = argparse.ArgumentParser() 
main_parser.add_argument("-common") 

filter_parser = argparse.ArgumentParser(add_help=False) 
filter_parser.add_argument("--filter1", action=GroupedAction, dest='filter.filter1', default=argparse.SUPPRESS) 
filter_parser.add_argument("--filter2", action=GroupedAction, dest='filter.filter2', default=argparse.SUPPRESS) 

subparsers = main_parser.add_subparsers(help='sub-command help') 

parser_a = subparsers.add_parser('command_a', help="command_a help", parents=[filter_parser]) 
parser_a.add_argument("--foo") 
parser_a.add_argument("--bar") 
parser_a.add_argument("--bazers", action=GroupedAction, dest='anotherGroup.bazers', default=argparse.SUPPRESS) 
... 
namespace = main_parser.parse_args() 
print namespace 

musiałem dodać default=argparse.SUPPRESS więc wpis bazers=None nie pojawi się w głównej przestrzeni nazw.

Wynik:

>>> python PROG command_a --foo bar --filter1 val --bazers val 
Namespace(anotherGroup=Namespace(bazers='val'), 
    bar=None, common=None, 
    filter=Namespace(filter1='val'), 
    foo='bar') 

Jeśli potrzebujesz domyślne wpisy w zagnieżdżonych nazw, można określić nazw przed ręką:

filter_namespace = argparse.Namespace(filter1=None, filter2=None) 
namespace = argparse.Namespace(filter=filter_namespace) 
namespace = main_parser.parse_args(namespace=namespace) 

wynik jak poprzednio, z wyjątkiem:

filter=Namespace(filter1='val', filter2=None) 
+0

Tak. Mam zamiar zaakceptować tę zamiast wcześniej zaakceptowanej odpowiedzi, ponieważ to naprawdę rozwiązuje mój cel za pomocą funkcji argparse (akcja niestandardowa). I rzeczywiście ... kropka-notacja "dest" była dokładnie tym, na co początkowo miałem nadzieję. Dzięki! – jdi

+1

Zrobiłem kilka dodatków do twojej GroupedAction, aby wyczyścić oryginalny atrybut najwyższego poziomu, a także opcjonalnie wyprowadzić grupę/pole z opcji: http://pastebin.com/qgQBBuvP – jdi

+0

@jdi: Dokładnie to Miałem na myśli, kiedy powiedziałem, że prawdopodobnie byłoby lepiej rozszerzyć argparse z niestandardowym parsowaniem poprzez podklasę; Pokazałem tylko, jak to zrobić inaczej, ponieważ twoje pytanie sugerowało, że nie chcesz tego robić w ten sposób. Zgadzam się, że jest to świetna odpowiedź. – abarnert

5

Nie jestem do końca pewien, o co prosisz, ale myślę, że chcesz, aby jego argumenty znalazły się w pod-przestrzeni nazw.

O ile mi wiadomo, argparse nie robi tego po wyjęciu z pudełka. Ale naprawdę nie jest trudno to zrobić, przetwarzając wynik końcowy, o ile tylko chcesz zagłębić się w okładki. (Zgaduję jeszcze łatwiej to zrobić przez instacji ArgumentParser, ale wyraźnie powiedział, że nie chce tego robić, więc nie spróbować.)

parser = argparse.ArgumentParser() 
parser.add_argument('--foo') 
breakfast = parser.add_argument_group('breakfast') 
breakfast.add_argument('--spam') 
breakfast.add_argument('--eggs') 
args = parser.parse_args() 

Teraz listę wszystkich miejsc dla breakfast opcje to:

[action.dest for action in breakfast._group_actions] 

A par klucz-wartość w args jest:

args._get_kwargs() 

Tak, wszyscy musimy na to przenieść te, które pasują do siebie. Będzie trochę łatwiej, jeśli skonstruujemy słowniki, aby utworzyć przestrzenie nazw od:

breakfast_options = [action.dest for action in breakfast._group_actions] 
top_names = {name: value for (name, value) in args._get_kwargs() 
      if name not in breakfast_options} 
breakfast_names = {name: value for (name, value) in args._get_kwargs() 
        if name in breakfast_options} 
top_names['breakfast'] = argparse.Namespace(**breakfast_names) 
top_namespace = argparse.Namespace(**top_names) 

I to wszystko; top_namespace wygląda następująco:

Namespace(breakfast=Namespace(eggs=None, spam='7'), foo='bar') 

Oczywiście w tym przypadku mamy jedną grupę statyczną. A co jeśli potrzebujesz bardziej ogólnego rozwiązania? Łatwo. parser._action_groups to lista wszystkich grup, ale pierwsze dwa to globalne grupy pozycyjne i słowa kluczowe. Tak więc, po prostu wykonaj iterację ponad parser._action_groups[2:] i wykonaj to samo dla każdego, co zrobiłeś dla breakfast powyżej.


Co powiesz na polecenia podrzędne zamiast grup? Podobne, ale szczegóły są inne. Jeśli trzymałeś wokół każdego obiektu subparser, to po prostu cały inny ArgumentParser. Jeśli nie, ale zachowałeś obiekt subparsers, jest to specjalny typ Action, którego choices jest dyktando, którego kluczami są nazwy kropek i których wartościami są same akapity. Jeśli nie zatrzymałeś się ani ... zacznij od parser._subparsers i dowiedz się, jak to zrobić.

W każdym przypadku, gdy wiesz, jak znaleźć nazwy, które chcesz przenieść i gdzie chcesz je przenieść, są takie same, jak w grupach.


Jeśli masz, oprócz światowych args i/lub grup i args subparser specyficzne i/lub grup, niektóre grupy, które są wspólne dla wielu subparsers ... potem koncepcyjnie robi się trudne, ponieważ każdy subparser kończy się odniesieniami do tej samej grupy i nie można ich przenieść do wszystkich z nich. Ale na szczęście masz do czynienia tylko z jednym ciałem (lub bez), więc możesz po prostu zignorować pozostałe akapity i przenieść dowolną wspólną grupę pod wybranym parserem (i każda grupa, która nie istnieje w wybranym parserze, albo zostaw na górze, albo wyrzuć, albo wybierz jeden parser arbitrally).

+0

Tak, to prawie odpowiada na moje pytanie. Zacząłem od spojrzenia na grupy i wydaje się, że zgrupowano je tylko pod kątem pomocy, po wyjęciu z pudełka. Wyjaśniłeś więc, że to wymaga ręcznego przetwarzania, co jest w porządku. Wystarczy zobaczyć taki przykład pokazujący, że jest to wymagane podejście z argparse. Dzięki! – jdi

+0

@jdi: Jak już powiedziałem w odpowiedzi, myślę, że rozszerzenie argparse poprzez podklasy zamiast postprocessingu może być tu łatwiejsze. Obiekty grupowe to coś, na co można łatwo zbudować, aby zrobić więcej, z niewielkimi zmianami w obiekcie analizatora składni. I to też chyba bardziej idiomatyczne. Ale to, z czym czujesz się bardziej komfortowo, jest prawdopodobnie w porządku. – abarnert

0

W tym skrypcie zmodyfikowałem metodę argparse._SubParsersAction na __call__. Zamiast przekazywać do przedziału numer namespace, przechodzi on nowy. Następnie dodaje to do głównego namespace. Zmieniam tylko 3 linie __call__.

import argparse 

def mycall(self, parser, namespace, values, option_string=None): 
    parser_name = values[0] 
    arg_strings = values[1:] 

    # set the parser name if requested 
    if self.dest is not argparse.SUPPRESS: 
     setattr(namespace, self.dest, parser_name) 

    # select the parser 
    try: 
     parser = self._name_parser_map[parser_name] 
    except KeyError: 
     args = {'parser_name': parser_name, 
       'choices': ', '.join(self._name_parser_map)} 
     msg = _('unknown parser %(parser_name)r (choices: %(choices)s)') % args 
     raise argparse.ArgumentError(self, msg) 

    # CHANGES 
    # parse all the remaining options into a new namespace 
    # store any unrecognized options on the main namespace, so that the top 
    # level parser can decide what to do with them 
    newspace = argparse.Namespace() 
    newspace, arg_strings = parser.parse_known_args(arg_strings, newspace) 
    setattr(namespace, 'subspace', newspace) # is there a better 'dest'? 

    if arg_strings: 
     vars(namespace).setdefault(argparse._UNRECOGNIZED_ARGS_ATTR, []) 
     getattr(namespace, argparse._UNRECOGNIZED_ARGS_ATTR).extend(arg_strings) 

argparse._SubParsersAction.__call__ = mycall 

# Main parser 
main_parser = argparse.ArgumentParser() 
main_parser.add_argument("--common") 

# sub commands 
subparsers = main_parser.add_subparsers(dest='command') 

parser_a = subparsers.add_parser('command_a') 
parser_a.add_argument("--foo") 
parser_a.add_argument("--bar") 

parser_b = subparsers.add_parser('command_b') 
parser_b.add_argument("--biz") 
parser_b.add_argument("--baz") 

# parse 
input = 'command_a --foo bar --bar val --filter extra'.split() 
namespace = main_parser.parse_known_args(input) 
print namespace 

input = '--common test command_b --biz bar --baz val'.split() 
namespace = main_parser.parse_args(input) 
print namespace 

ta produkuje:

(Namespace(command='command_a', common=None, 
    subspace=Namespace(bar='val', foo='bar')), 
['--filter', 'extra']) 

Namespace(command='command_b', common='test', 
    subspace=Namespace(baz='val', biz='bar')) 

użyłem parse_known_args przetestować jak dodatkowy ciągi są przekazywane z powrotem do głównego parsera.

Upuściłem rzeczy parents, ponieważ nie dodają nic do zmiany tej przestrzeni nazw. jest to po prostu wygodna metoda zdefiniowania zestawu argumentów, z których korzysta wielu paru osób. argparse nie rejestruje, które argumenty zostały dodane przez parents i które zostały dodane bezpośrednio. Nie jest to narzędzie grupowania, które nie pomaga zbytnio. Są używane przez narzędzie do formatowania Pomocy, ale nie przez parse_args.

Mogę podklasę _SubParsersAction (zamiast ponownie przypisywać __call__), ale wtedy będę musiał zmienić main_parse.register.

+0

To jest świetny przykład, jak uzyskać to przetwarzanie przez łatanie małpy ... chociaż wadą jest to, że używa znanego/nieznanego podejścia args, co oznacza, że ​​filtry nie są dokumentowane lub zarządzane przez argparse. – jdi

4

Zagnieżdżanie z podklasami Action jest w porządku dla jednego typu akcji, ale jest uciążliwe, jeśli musisz podklasować kilka typów (store, store true, append, etc). Oto kolejny pomysł - przestrzeń nazw podklasy. Wykonuj ten sam rodzaj podziału i setattr, ale rób to w przestrzeni nazw, a nie w akcji. Następnie po prostu stwórz instancję nowej klasy i przekaż ją do parse_args.

class Nestedspace(argparse.Namespace): 
    def __setattr__(self, name, value): 
     if '.' in name: 
      group,name = name.split('.',1) 
      ns = getattr(self, group, Nestedspace()) 
      setattr(ns, name, value) 
      self.__dict__[group] = ns 
     else: 
      self.__dict__[name] = value 

p = argparse.ArgumentParser() 
p.add_argument('--foo') 
p.add_argument('--bar', dest='test.bar') 
print(p.parse_args('--foo test --bar baz'.split())) 

ns = Nestedspace() 
print(p.parse_args('--foo test --bar baz'.split(), ns)) 
p.add_argument('--deep', dest='test.doo.deep') 
args = p.parse_args('--foo test --bar baz --deep doodod'.split(), Nestedspace()) 
print(args) 
print(args.test.doo) 
print(args.test.doo.deep) 

produkcji:

Namespace(foo='test', test.bar='baz') 
Nestedspace(foo='test', test=Nestedspace(bar='baz')) 
Nestedspace(foo='test', test=Nestedspace(bar='baz', doo=Nestedspace(deep='doodod'))) 
Nestedspace(deep='doodod') 
doodod 

__getattr__ dla tej przestrzeni nazw (potrzebna do działania, takie jak liczby i dołącz) mogą być:

def __getattr__(self, name): 
    if '.' in name: 
     group,name = name.split('.',1) 
     try: 
      ns = self.__dict__[group] 
     except KeyError: 
      raise AttributeError 
     return getattr(ns, name) 
    else: 
     raise AttributeError 

jakie zaproponowano kilka innych opcji, ale jak to jest najlepsze. Umieszcza szczegóły przechowywania w miejscu, w którym się znajdują, w Przestrzeni nazw, a nie w parserze.

+0

Och, fajne. Nawet nie zastanawiałem się nad podklasowaniem Przestrzeni nazw. Ogólnie podoba mi się to, ale od czasu twojej ostatniej odpowiedzi znalazłem pewną korzyść w podklasowaniu rzeczy takich jak ArgumentGroup, aby ustawić domyślny metavar pasujący do pola, a także zarejestrować akcje niestandardowe. Jestem pewien, że ten niestandardowy obszar nazw ma sens w połączeniu. – jdi

0

Począwszy od odpowiedzi abarnerta, zestawię następujący MWE ++ ;-), który obsługuje wiele grup konfiguracji o podobnych nazwach opcji.

#!/usr/bin/env python2 
import argparse, re 

cmdl_skel = { 
    'description'  : 'An example of multi-level argparse usage.', 
    'opts'    : { 
     '--foo' : { 
      'type' : int, 
      'default' : 0, 
      'help' : 'foo help main', 
     }, 
     '--bar' : { 
      'type' : str, 
      'default' : 'quux', 
      'help' : 'bar help main', 
     }, 
    }, 
    # Assume your program uses sub-programs with their options. Argparse will 
    # first digest *all* defs, so opts with the same name across groups are 
    # forbidden. The trick is to use the module name (=> group.title) as 
    # pseudo namespace which is stripped off at group parsing 
    'groups' : [ 
     { 'module'  : 'mod1', 
      'description' : 'mod1 description', 
      'opts'   : { 
       '--mod1-foo, --mod1.foo' : { 
        'type' : int, 
        'default' : 0, 
        'help' : 'foo help for mod1' 
       }, 
      }, 
     }, 
     { 'module'  : 'mod2', 
      'description' : 'mod2 description', 
      'opts'   : { 
       '--mod2-foo, --mod2.foo' : { 
        'type' : int, 
        'default' : 1, 
        'help' : 'foo help for mod2' 
       }, 
      }, 
     }, 
    ], 
    'args'    : { 
     'arg1' : { 
      'type' : str, 
      'help' : 'arg1 help', 
     }, 
     'arg2' : { 
      'type' : str, 
      'help' : 'arg2 help', 
     }, 
    } 
} 


def parse_args(): 
    def _parse_group (parser, opt, **optd): 
     # digest variants 
     optv = re.split('\s*,\s*', opt) 
     # this may rise exceptions... 
     parser.add_argument(*optv, **optd) 

    errors = {} 
    parser = argparse.ArgumentParser(description=cmdl_skel['description'], 
       formatter_class=argparse.ArgumentDefaultsHelpFormatter) 

    # it'd be nice to loop in a single run over zipped lists, but they have 
    # different lenghts... 
    for opt in cmdl_skel['opts'].keys(): 
     _parse_group(parser, opt, **cmdl_skel['opts'][opt]) 

    for arg in cmdl_skel['args'].keys(): 
     _parse_group(parser, arg, **cmdl_skel['args'][arg]) 

    for grp in cmdl_skel['groups']: 
     group = parser.add_argument_group(grp['module'], grp['description']) 
     for mopt in grp['opts'].keys(): 
      _parse_group(group, mopt, **grp['opts'][mopt]) 

    args = parser.parse_args() 

    all_group_opts = [] 
    all_group_names = {} 
    for group in parser._action_groups[2:]: 
     gtitle = group.title 
     group_opts = [action.dest for action in group._group_actions] 
     all_group_opts += group_opts 
     group_names = { 
      # remove the leading pseudo-namespace 
      re.sub("^%s_" % gtitle, '', name) : value 
       for (name, value) in args._get_kwargs() 
        if name in group_opts 
     } 
     # build group namespace 
     all_group_names[gtitle] = argparse.Namespace(**group_names) 

    # rebuild top namespace 
    top_names = { 
     name: value for (name, value) in args._get_kwargs() 
      if name not in all_group_opts 
    } 
    top_names.update(**all_group_names) 
    top_namespace = argparse.Namespace(**top_names) 

    return top_namespace 


def main(): 
    args = parse_args() 

    print(str(args)) 
    print(args.bar) 
    print(args.mod1.foo) 


if __name__ == '__main__': 
    main() 

Następnie można nazwać tak (symbol: --mod1-... są opcje "MOD1", etc.):

$ ./argparse_example.py one two --bar=three --mod1-foo=11231 --mod2.foo=46546 
Namespace(arg1='one', arg2='two', bar='three', foo=0, mod1=Namespace(foo=11231), mod2=Namespace(foo=46546)) 
three 
11231 
Powiązane problemy