Mam aplikację w języku Python, która wymaga kilku (~ 30) parametrów konfiguracyjnych. Do tej pory używałem klasy OptionParser do definiowania wartości domyślnych w samej aplikacji, z możliwością zmiany poszczególnych parametrów w wierszu poleceń podczas wywoływania aplikacji.
Teraz chciałbym użyć „właściwych” plików konfiguracyjnych, na przykład z klasy ConfigParser. Jednocześnie użytkownicy powinni nadal mieć możliwość zmiany poszczególnych parametrów w wierszu poleceń.
Zastanawiałem się, czy istnieje sposób na połączenie tych dwóch kroków, np. Użycie optparse (lub nowszego argparse) do obsługi opcji wiersza poleceń, ale odczyt wartości domyślnych z pliku konfiguracyjnego w składni ConfigParse.
Jakieś pomysły, jak to zrobić w łatwy sposób? Naprawdę nie mam ochoty ręcznie wywoływać ConfigParse, a następnie ręcznie ustawiać wszystkie domyślne ustawienia wszystkich opcji na odpowiednie wartości ...
źródło
Odpowiedzi:
Właśnie odkryłem, że możesz to zrobić
argparse.ArgumentParser.parse_known_args()
. Zacznij od użycia,parse_known_args()
aby przeanalizować plik konfiguracyjny z wiersza poleceń, a następnie przeczytaj go za pomocą ConfigParser i ustaw wartości domyślne, a następnie przeanalizuj pozostałe opcje za pomocąparse_args()
. Umożliwi to ustawienie wartości domyślnej, zastąpienie jej plikiem konfiguracyjnym, a następnie zastąpienie tego opcją wiersza poleceń. Na przykład:Domyślnie bez wprowadzania danych przez użytkownika:
$ ./argparse-partial.py Option is "default"
Domyślnie z pliku konfiguracyjnego:
$ cat argparse-partial.config [Defaults] option=Hello world! $ ./argparse-partial.py -c argparse-partial.config Option is "Hello world!"
Domyślnie z pliku konfiguracyjnego, nadpisane przez linię poleceń:
$ ./argparse-partial.py -c argparse-partial.config --option override Option is "override"
argprase-partial.py następuje.
-h
Prawidłowe korzystanie z pomocy jest nieco skomplikowane .import argparse import ConfigParser import sys def main(argv=None): # Do argv default this way, as doing it in the functional # declaration sets it at compile time. if argv is None: argv = sys.argv # Parse any conf_file specification # We make this parser with add_help=False so that # it doesn't parse -h and print help. conf_parser = argparse.ArgumentParser( description=__doc__, # printed with -h/--help # Don't mess with format of description formatter_class=argparse.RawDescriptionHelpFormatter, # Turn off help, so we print all options in response to -h add_help=False ) conf_parser.add_argument("-c", "--conf_file", help="Specify config file", metavar="FILE") args, remaining_argv = conf_parser.parse_known_args() defaults = { "option":"default" } if args.conf_file: config = ConfigParser.SafeConfigParser() config.read([args.conf_file]) defaults.update(dict(config.items("Defaults"))) # Parse rest of arguments # Don't suppress add_help here so it will handle -h parser = argparse.ArgumentParser( # Inherit options from config_parser parents=[conf_parser] ) parser.set_defaults(**defaults) parser.add_argument("--option") args = parser.parse_args(remaining_argv) print "Option is \"{}\"".format(args.option) return(0) if __name__ == "__main__": sys.exit(main())
źródło
another=%(option)s you are cruel
toanother
zawsze zostanie rozwiązane,Hello world you are cruel
nawet jeślioption
zostanie nadpisane na coś innego w linii poleceń .. argghh-parser!--version
opcję do swojej aplikacji, lepiej jest dodać ją doconf_parser
niż doparser
i zamknąć aplikację po wydrukowaniu pomocy. Jeśli dodasz--version
doparser
i uruchomisz aplikację z--version
flagą, wówczas aplikacja będzie niepotrzebnie próbować otworzyć i przeanalizowaćargs.conf_file
plik konfiguracyjny (który może być źle sformułowany lub nawet nie istnieć, co prowadzi do wyjątku).Sprawdź ConfigArgParse - to nowy pakiet PyPI ( open source ), który zastępuje argparse z dodaną obsługą plików konfiguracyjnych i zmiennych środowiskowych.
źródło
Używam ConfigParser i argparse z podkomendami do obsługi takich zadań. Ważna linia w poniższym kodzie to:
Spowoduje to ustawienie wartości domyślnych podkomendy (z argparse) na wartości w sekcji pliku konfiguracyjnego.
Bardziej kompletny przykład znajduje się poniżej:
####### content of example.cfg: # [sub1] # verbosity=10 # gggg=3.5 # [sub2] # host=localhost import ConfigParser import argparse parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() parser_sub1 = subparsers.add_parser('sub1') parser_sub1.add_argument('-V','--verbosity', type=int, dest='verbosity') parser_sub1.add_argument('-G', type=float, dest='gggg') parser_sub2 = subparsers.add_parser('sub2') parser_sub2.add_argument('-H','--host', dest='host') conffile = ConfigParser.SafeConfigParser() conffile.read('example.cfg') for subp, subn in ((parser_sub1, "sub1"), (parser_sub2, "sub2")): subp.set_defaults(**dict(conffile.items(subn))) print parser.parse_args(['sub1',]) # Namespace(gggg=3.5, verbosity=10) print parser.parse_args(['sub1', '-V', '20']) # Namespace(gggg=3.5, verbosity=20) print parser.parse_args(['sub1', '-V', '20', '-G','42']) # Namespace(gggg=42.0, verbosity=20) print parser.parse_args(['sub2', '-H', 'www.example.com']) # Namespace(host='www.example.com') print parser.parse_args(['sub2',]) # Namespace(host='localhost')
źródło
Nie mogę powiedzieć, że to najlepszy sposób, ale mam klasę OptionParser, którą stworzyłem, która właśnie to robi - działa jak optparse.OptionParser z ustawieniami domyślnymi pochodzącymi z sekcji pliku konfiguracyjnego. Możesz to mieć ...
class OptionParser(optparse.OptionParser): def __init__(self, **kwargs): import sys import os config_file = kwargs.pop('config_file', os.path.splitext(os.path.basename(sys.argv[0]))[0] + '.config') self.config_section = kwargs.pop('config_section', 'OPTIONS') self.configParser = ConfigParser() self.configParser.read(config_file) optparse.OptionParser.__init__(self, **kwargs) def add_option(self, *args, **kwargs): option = optparse.OptionParser.add_option(self, *args, **kwargs) name = option.get_opt_string() if name.startswith('--'): name = name[2:] if self.configParser.has_option(self.config_section, name): self.set_default(name, self.configParser.get(self.config_section, name))
Zapraszam do przeglądania źródła . Testy znajdują się w katalogu siostrzanym.
źródło
Możesz użyć ChainMap
A ChainMap groups multiple dicts or other mappings together to create a single, updateable view. If no maps are specified, a single empty dictionary is provided so that a new chain always has at least one mapping.
Można łączyć wartości z wiersza poleceń, zmiennych środowiskowych, pliku konfiguracyjnego, aw przypadku braku wartości zdefiniować wartość domyślną.
import os from collections import ChainMap, defaultdict options = ChainMap(command_line_options, os.environ, config_file_options, defaultdict(lambda: 'default-value')) value = options['optname'] value2 = options['other-option'] print(value, value2) 'optvalue', 'default-value'
źródło
dicts
aktualizacji w żądanej kolejności pierwszeństwa? Zdefaultdict
tam ewentualnie zaletą jako nowe lub nieobsługiwane opcje można ustawić ale to oddzielić odChainMap
. Zakładam, że czegoś mi brakuje.Aktualizacja: ta odpowiedź nadal zawiera problemy; na przykład nie obsługuje
required
argumentów i wymaga niezręcznej składni konfiguracji. Zamiast tego ConfigArgParse wydaje się być dokładnie tym, o co chodzi w tym pytaniu, i jest przejrzystym zamiennikiem typu drop-in.Jednym z problemów związanych z prądem jest to, że nie wystąpi błąd, jeśli argumenty w pliku konfiguracyjnym są nieprawidłowe. Oto wersja z inną wadą: musisz umieścić prefiks
--
lub-
w kluczach.Oto kod w Pythonie ( link Gist z licencją MIT):
# Filename: main.py import argparse import configparser if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--config_file', help='config file') args, left_argv = parser.parse_known_args() if args.config_file: with open(args.config_file, 'r') as f: config = configparser.SafeConfigParser() config.read([args.config_file]) parser.add_argument('--arg1', help='argument 1') parser.add_argument('--arg2', type=int, help='argument 2') for k, v in config.items("Defaults"): parser.parse_args([str(k), str(v)], args) parser.parse_args(left_argv, args) print(args)
Oto przykład pliku konfiguracyjnego:
# Filename: config_correct.conf [Defaults] --arg1=Hello! --arg2=3
Teraz biegnę
> python main.py --config_file config_correct.conf --arg1 override Namespace(arg1='override', arg2=3, config_file='test_argparse.conf')
Jeśli jednak nasz plik konfiguracyjny zawiera błąd:
# config_invalid.conf --arg1=Hello! --arg2='not an integer!'
Uruchomienie skryptu spowoduje błąd, zgodnie z potrzebami:
> python main.py --config_file config_invalid.conf --arg1 override usage: test_argparse_conf.py [-h] [--config_file CONFIG_FILE] [--arg1 ARG1] [--arg2 ARG2] main.py: error: argument --arg2: invalid int value: 'not an integer!'
Główną wadą jest to, że jest to
parser.parse_args
nieco hakerskie w celu uzyskania sprawdzania błędów z ArgumentParser, ale nie znam żadnych alternatyw dla tego.źródło
Spróbuj w ten sposób
# encoding: utf-8 import imp import argparse class LoadConfigAction(argparse._StoreAction): NIL = object() def __init__(self, option_strings, dest, **kwargs): super(self.__class__, self).__init__(option_strings, dest) self.help = "Load configuration from file" def __call__(self, parser, namespace, values, option_string=None): super(LoadConfigAction, self).__call__(parser, namespace, values, option_string) config = imp.load_source('config', values) for key in (set(map(lambda x: x.dest, parser._actions)) & set(dir(config))): setattr(namespace, key, getattr(config, key))
Użyj tego:
parser.add_argument("-C", "--config", action=LoadConfigAction) parser.add_argument("-H", "--host", dest="host")
I stwórz przykładową konfigurację:
# Example config: /etc/myservice.conf import os host = os.getenv("HOST_NAME", "localhost")
źródło
fromfile_prefix_chars
Może nie jest to idealne API, ale warto o tym wiedzieć.
main.py
:#!/usr/bin/env python3 import argparse parser = argparse.ArgumentParser(fromfile_prefix_chars='@') parser.add_argument('-a', default=13) parser.add_argument('-b', default=42) print(parser.parse_args())
Następnie:
$ printf -- '-a\n1\n-b\n2\n' > opts.txt $ ./main.py Namespace(a=13, b=42) $ ./main.py @opts.txt Namespace(a='1', b='2') $ ./main.py @opts.txt -a 3 -b 4 Namespace(a='3', b='4') $ ./main.py -a 3 -b 4 @opts.txt Namespace(a='1', b='2')
Dokumentacja: https://docs.python.org/3.6/library/argparse.html#fromfile-prefix-chars
Testowano na Pythonie 3.6.5, Ubuntu 18.04.
źródło