Files
swift-mirror/utils/build_swift/argparse/parser.py
Ross Bayer 85abdcd62d Argparse "Overlay" Module (#12873)
* Implemented a wrapper module around the standard argparse package, exposing the same interface with some extras on top, including a new builder type with expressive DSL for constructing complex argument parsers.

* Fixed imports in build_swift/argparse/__init__.py to make flake8 happy.

* More re-formmating to meet the exacting standards of the python_lint script.

* Added doc-strings to all the modules in the build_swift argparse overlay.

* Implemented a new BoolType for the argparse module which handles boolean-like values and replaces the hard-coded boolean values in the _ToggleAction class.

* Fixed the mess of imports in the tests sub-package to favor relative imports, so now the unit-tests will actually run as expected. The README has also been updated with a better command for executing the unit-test suite.

* Updated the add_positional method on the ArgumentParser builder class to only take a single action or default to the store action.

* Cleaned up the set_defaults method.

* Added validation test to run the build_swift unit-tests.

* Updated validation-test for the build_swift unit-test suite to use %utils.

* Fixed hard-coded default values in the expected_options module used for generating argument parser tests.

* Updated the comment in the Python validation test to run the build_swift unit-tests.
2017-11-27 21:49:44 -08:00

228 lines
7.1 KiB
Python

# 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)