Skip to content

Instantly share code, notes, and snippets.

@robcowie
Created February 18, 2019 10:00
Show Gist options
  • Save robcowie/7c39e2afb140c905fdf2661b93ff3df9 to your computer and use it in GitHub Desktop.
Save robcowie/7c39e2afb140c905fdf2661b93ff3df9 to your computer and use it in GitHub Desktop.
ArgumentParser and argparse Action that can pull args from a yaml config file
# -*- coding: utf-8 -*-
"""
Part of the undertime app https://gitlab.com/anarcat/undertime by Antoine Beaupré.
AGPLv3 licence (https://gitlab.com/anarcat/undertime/blob/master/LICENSE)
"""
import argparse
import os
import yaml
class ConfigAction(argparse.Action):
"""Add configuration file to current defaults."""
def __init__(self, *args, **kwargs):
"""Config action is a search path, so a list, so one or more argument."""
super().__init__(*args, nargs='+', **kwargs)
def __call__(self, parser, ns, values, option):
"""Change defaults for the namespace.
still allows overriding from commandline options
"""
for path in values:
parser.set_defaults(**self.parse_config(path))
def parse_config(self, path):
"""Abstract implementation of config file parsing."""
raise NotImplementedError()
class YamlConfigAction(ConfigAction):
"""YAML config file parser action."""
def parse_config(self, path):
try:
with open(os.path.expanduser(path), 'r') as handle:
return yaml.safe_load(handle)
except (FileNotFoundError, yaml.parser.ParserError) as e:
raise argparse.ArgumentError(self, e)
class ConfigArgumentParser(argparse.ArgumentParser):
"""Argument parser which supports parsing extra config files.
Config files specified on the commandline through the
YamlConfigAction arguments modify the default values on the
spot. If a default is specified when adding an argument, it also
gets immediately loaded.
This will typically be used in a subclass, like this:
self.add_argument('--config', action=YamlConfigAction, default=self.default_config())
"""
def _add_action(self, action):
# this overrides the add_argument() routine, which is where
# actions get registered. it is done so we can properly load
# the default config file before the action actually gets
# fired. Ideally, we'd load the default config only if the
# action *never* gets fired (but still setting defaults for
# the namespace) but argparse doesn't give us that opportunity
# (and even if it would, it wouldn't retroactively change the
# Namespace object in parse_args() so it wouldn't work).
action = super()._add_action(action)
if isinstance(action, ConfigAction) and action.default is not None:
# fire the action, later calls can override defaults
try:
action(self, None, action.default, None)
except argparse.ArgumentError:
# ignore errors from missing default
pass
def default_config(self):
"""Shortcut to detect commonly used config paths."""
return [os.path.join(os.environ.get('XDG_CONFIG_HOME', '~/.config/'), self.prog + '.yml')]
if __name__ == "__main__":
parser = ConfigArgumentParser('test')
parser.add_argument('arg1')
parser.add_argument('--config', action=YamlConfigAction, default=parser.default_config())
args = parser.parse_args()
print(args)
@lucasc896
Copy link

Oh he's good...

@robcowie
Copy link
Author

🤣

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment