blob: c915e25b0d806b92df0f948ab17cdb6b7b7cefba [file] [log] [blame]
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
"""
Extensions to the standard argparse ArgumentParer class to support multiple
destination actions as well as a new builder DSL for declaratively
constructing complex parsers.
"""
import argparse
from contextlib import contextmanager
from . import Namespace, SUPPRESS, actions
from .actions import Action
__all__ = [
'ArgumentParser',
]
# -----------------------------------------------------------------------------
class _ActionContainer(object):
"""Container object holding partially applied actions used as a part of the
builder DSL.
"""
def __init__(self):
self.append = _PartialAction(actions.AppendAction)
self.custom_call = _PartialAction(actions.CustomCallAction)
self.store = _PartialAction(actions.StoreAction)
self.store_int = _PartialAction(actions.StoreIntAction)
self.store_true = _PartialAction(actions.StoreTrueAction)
self.store_false = _PartialAction(actions.StoreFalseAction)
self.store_path = _PartialAction(actions.StorePathAction)
self.toggle_true = _PartialAction(actions.ToggleTrueAction)
self.toggle_false = _PartialAction(actions.ToggleFalseAction)
self.unsupported = _PartialAction(actions.UnsupportedAction)
class _CompoundAction(Action):
"""Action composed of multiple actions. Default attributes are derived
from the first action.
"""
def __init__(self, actions, **kwargs):
_actions = []
for action in actions:
_actions.append(action(**kwargs))
kwargs.setdefault('nargs', kwargs[0].nargs)
kwargs.setdefault('metavar', kwargs[0].metavar)
kwargs.setdefault('choices', kwargs[0].choices)
super(_CompoundAction, self).__init__(**kwargs)
self.actions = _actions
def __call__(self, *args):
for action in self.actions:
action(*args)
class _PartialAction(Action):
"""Action that is partially applied, creating a factory closure used to
defer initialization of acitons in the builder DSL.
"""
def __init__(self, action_class):
self.action_class = action_class
def __call__(self, dests=None, *call_args, **call_kwargs):
def factory(**kwargs):
kwargs.update(call_kwargs)
if dests is not None:
return self.action_class(dests=dests, *call_args, **kwargs)
return self.action_class(*call_args, **kwargs)
return factory
# -----------------------------------------------------------------------------
class _Builder(object):
"""Builder object for constructing complex ArgumentParser instances with
a more friendly and descriptive DSL.
"""
def __init__(self, parser, **kwargs):
assert isinstance(parser, ArgumentParser)
self._parser = parser
self._current_group = self._parser
self._defaults = dict()
self.actions = _ActionContainer()
def build(self):
self._parser.set_defaults(**self._defaults)
return self._parser
def _add_argument(self, names, *actions, **kwargs):
# Unwrap partial actions
_actions = []
for action in actions:
if isinstance(action, _PartialAction):
action = action()
_actions.append(action)
if len(_actions) == 0:
# Default to store action
action = actions.StoreAction
elif len(_actions) == 1:
action = _actions[0]
else:
def thunk(**kwargs):
return _CompoundAction(_actions, **kwargs)
action = thunk
return self._current_group.add_argument(
*names, action=action, **kwargs)
def add_positional(self, dests, action=None, **kwargs):
if isinstance(dests, str):
dests = [dests]
if any(dest.startswith('-') for dest in dests):
raise ValueError("add_positional can't add optional arguments")
if action is None:
action = actions.StoreAction
return self._add_argument(dests, action, **kwargs)
def add_option(self, option_strings, *actions, **kwargs):
if isinstance(option_strings, str):
option_strings = [option_strings]
if not all(opt.startswith('-') for opt in option_strings):
raise ValueError("add_option can't add positional arguments")
return self._add_argument(option_strings, *actions, **kwargs)
def set_defaults(self, *args, **kwargs):
if len(args) == 1:
raise TypeError('set_defaults takes at least 2 arguments')
if len(args) >= 2:
dests, value = args[:-1], args[-1]
for dest in dests:
kwargs[dest] = value
self._defaults.update(**kwargs)
def in_group(self, description):
self._current_group = self._parser.add_argument_group(description)
return self._current_group
def reset_group(self):
self._current_group = self._parser
@contextmanager
def argument_group(self, description):
previous_group = self._current_group
self._current_group = self._parser.add_argument_group(description)
yield self._current_group
self._current_group = previous_group
@contextmanager
def mutually_exclusive_group(self, **kwargs):
previous_group = self._current_group
self._current_group = previous_group \
.add_mutually_exclusive_group(**kwargs)
yield self._current_group
self._current_group = previous_group
# -----------------------------------------------------------------------------
class ArgumentParser(argparse.ArgumentParser):
"""A thin extension class to the standard ArgumentParser which incluldes
methods to interact with a builder instance.
"""
@classmethod
def builder(cls, **kwargs):
"""Create a new builder instance using this parser class.
"""
return _Builder(parser=cls(**kwargs))
def to_builder(self):
"""Construct and return a builder instance with this parser.
"""
return _Builder(parser=self)
def parse_known_args(self, args=None, namespace=None):
"""Thin wrapper around parse_known_args which shims-in support for
actions with multiple destinations.
"""
if namespace is None:
namespace = Namespace()
# Add action defaults not present in namespace
for action in self._actions:
if not hasattr(action, 'dests'):
continue
for dest in action.dests:
if hasattr(namespace, dest):
continue
if action.default is SUPPRESS:
continue
setattr(namespace, dest, action.default)
return super(ArgumentParser, self).parse_known_args(args, namespace)