|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""This module provides argparse integration with absl.flags. |
|
|
|
``argparse_flags.ArgumentParser`` is a drop-in replacement for |
|
:class:`argparse.ArgumentParser`. It takes care of collecting and defining absl |
|
flags in :mod:`argparse`. |
|
|
|
Here is a simple example:: |
|
|
|
# Assume the following absl.flags is defined in another module: |
|
# |
|
# from absl import flags |
|
# flags.DEFINE_string('echo', None, 'The echo message.') |
|
# |
|
parser = argparse_flags.ArgumentParser( |
|
description='A demo of absl.flags and argparse integration.') |
|
parser.add_argument('--header', help='Header message to print.') |
|
|
|
# The parser will also accept the absl flag `--echo`. |
|
# The `header` value is available as `args.header` just like a regular |
|
# argparse flag. The absl flag `--echo` continues to be available via |
|
# `absl.flags.FLAGS` if you want to access it. |
|
args = parser.parse_args() |
|
|
|
# Example usages: |
|
# ./program --echo='A message.' --header='A header' |
|
# ./program --header 'A header' --echo 'A message.' |
|
|
|
|
|
Here is another example demonstrates subparsers:: |
|
|
|
parser = argparse_flags.ArgumentParser(description='A subcommands demo.') |
|
parser.add_argument('--header', help='The header message to print.') |
|
|
|
subparsers = parser.add_subparsers(help='The command to execute.') |
|
|
|
roll_dice_parser = subparsers.add_parser( |
|
'roll_dice', help='Roll a dice.', |
|
# By default, absl flags can also be specified after the sub-command. |
|
# To only allow them before sub-command, pass |
|
# `inherited_absl_flags=None`. |
|
inherited_absl_flags=None) |
|
roll_dice_parser.add_argument('--num_faces', type=int, default=6) |
|
roll_dice_parser.set_defaults(command=roll_dice) |
|
|
|
shuffle_parser = subparsers.add_parser('shuffle', help='Shuffle inputs.') |
|
shuffle_parser.add_argument( |
|
'inputs', metavar='I', nargs='+', help='Inputs to shuffle.') |
|
shuffle_parser.set_defaults(command=shuffle) |
|
|
|
args = parser.parse_args(argv[1:]) |
|
args.command(args) |
|
|
|
# Example usages: |
|
# ./program --echo='A message.' roll_dice --num_faces=6 |
|
# ./program shuffle --echo='A message.' 1 2 3 4 |
|
|
|
|
|
There are several differences between :mod:`absl.flags` and |
|
:mod:`~absl.flags.argparse_flags`: |
|
|
|
1. Flags defined with absl.flags are parsed differently when using the |
|
argparse parser. Notably: |
|
|
|
1) absl.flags allows both single-dash and double-dash for any flag, and |
|
doesn't distinguish them; argparse_flags only allows double-dash for |
|
flag's regular name, and single-dash for flag's ``short_name``. |
|
2) Boolean flags in absl.flags can be specified with ``--bool``, |
|
``--nobool``, as well as ``--bool=true/false`` (though not recommended); |
|
in argparse_flags, it only allows ``--bool``, ``--nobool``. |
|
|
|
2. Help related flag differences: |
|
|
|
1) absl.flags does not define help flags, absl.app does that; argparse_flags |
|
defines help flags unless passed with ``add_help=False``. |
|
2) absl.app supports ``--helpxml``; argparse_flags does not. |
|
3) argparse_flags supports ``-h``; absl.app does not. |
|
""" |
|
|
|
import argparse |
|
import sys |
|
|
|
from absl import flags |
|
|
|
|
|
_BUILT_IN_FLAGS = frozenset({ |
|
'help', |
|
'helpshort', |
|
'helpfull', |
|
'helpxml', |
|
'flagfile', |
|
'undefok', |
|
}) |
|
|
|
|
|
class ArgumentParser(argparse.ArgumentParser): |
|
"""Custom ArgumentParser class to support special absl flags.""" |
|
|
|
def __init__(self, **kwargs): |
|
"""Initializes ArgumentParser. |
|
|
|
Args: |
|
**kwargs: same as argparse.ArgumentParser, except: |
|
1. It also accepts `inherited_absl_flags`: the absl flags to inherit. |
|
The default is the global absl.flags.FLAGS instance. Pass None to |
|
ignore absl flags. |
|
2. The `prefix_chars` argument must be the default value '-'. |
|
|
|
Raises: |
|
ValueError: Raised when prefix_chars is not '-'. |
|
""" |
|
prefix_chars = kwargs.get('prefix_chars', '-') |
|
if prefix_chars != '-': |
|
raise ValueError( |
|
'argparse_flags.ArgumentParser only supports "-" as the prefix ' |
|
'character, found "{}".'.format(prefix_chars)) |
|
|
|
|
|
self._inherited_absl_flags = kwargs.pop('inherited_absl_flags', flags.FLAGS) |
|
|
|
|
|
super(ArgumentParser, self).__init__(**kwargs) |
|
|
|
if self.add_help: |
|
|
|
|
|
self.add_argument( |
|
|
|
'--helpshort', action='help', |
|
default=argparse.SUPPRESS, help=argparse.SUPPRESS) |
|
self.add_argument( |
|
'--helpfull', action=_HelpFullAction, |
|
default=argparse.SUPPRESS, help='show full help message and exit') |
|
|
|
if self._inherited_absl_flags: |
|
self.add_argument( |
|
'--undefok', default=argparse.SUPPRESS, help=argparse.SUPPRESS) |
|
self._define_absl_flags(self._inherited_absl_flags) |
|
|
|
def parse_known_args(self, args=None, namespace=None): |
|
if args is None: |
|
args = sys.argv[1:] |
|
if self._inherited_absl_flags: |
|
|
|
|
|
|
|
args = self._inherited_absl_flags.read_flags_from_files( |
|
args, force_gnu=True) |
|
|
|
undefok_missing = object() |
|
undefok = getattr(namespace, 'undefok', undefok_missing) |
|
|
|
namespace, args = super(ArgumentParser, self).parse_known_args( |
|
args, namespace) |
|
|
|
|
|
|
|
|
|
if undefok is not undefok_missing: |
|
namespace.undefok = undefok |
|
|
|
if self._inherited_absl_flags: |
|
|
|
|
|
|
|
|
|
|
|
if hasattr(namespace, 'undefok'): |
|
args = _strip_undefok_args(namespace.undefok, args) |
|
|
|
|
|
del namespace.undefok |
|
self._inherited_absl_flags.mark_as_parsed() |
|
try: |
|
self._inherited_absl_flags.validate_all_flags() |
|
except flags.IllegalFlagValueError as e: |
|
self.error(str(e)) |
|
|
|
return namespace, args |
|
|
|
def _define_absl_flags(self, absl_flags): |
|
"""Defines flags from absl_flags.""" |
|
key_flags = set(absl_flags.get_key_flags_for_module(sys.argv[0])) |
|
for name in absl_flags: |
|
if name in _BUILT_IN_FLAGS: |
|
|
|
continue |
|
flag_instance = absl_flags[name] |
|
|
|
|
|
if name == flag_instance.name: |
|
|
|
|
|
suppress = flag_instance not in key_flags |
|
self._define_absl_flag(flag_instance, suppress) |
|
|
|
def _define_absl_flag(self, flag_instance, suppress): |
|
"""Defines a flag from the flag_instance.""" |
|
flag_name = flag_instance.name |
|
short_name = flag_instance.short_name |
|
argument_names = ['--' + flag_name] |
|
if short_name: |
|
argument_names.insert(0, '-' + short_name) |
|
if suppress: |
|
helptext = argparse.SUPPRESS |
|
else: |
|
|
|
helptext = flag_instance.help.replace('%', '%%') |
|
if flag_instance.boolean: |
|
|
|
argument_names.append('--no' + flag_name) |
|
self.add_argument( |
|
*argument_names, action=_BooleanFlagAction, help=helptext, |
|
metavar=flag_instance.name.upper(), |
|
flag_instance=flag_instance) |
|
else: |
|
self.add_argument( |
|
*argument_names, action=_FlagAction, help=helptext, |
|
metavar=flag_instance.name.upper(), |
|
flag_instance=flag_instance) |
|
|
|
|
|
class _FlagAction(argparse.Action): |
|
"""Action class for Abseil non-boolean flags.""" |
|
|
|
def __init__( |
|
self, |
|
option_strings, |
|
dest, |
|
help, |
|
metavar, |
|
flag_instance, |
|
default=argparse.SUPPRESS): |
|
"""Initializes _FlagAction. |
|
|
|
Args: |
|
option_strings: See argparse.Action. |
|
dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS. |
|
help: See argparse.Action. |
|
metavar: See argparse.Action. |
|
flag_instance: absl.flags.Flag, the absl flag instance. |
|
default: Ignored. The flag always uses dest=argparse.SUPPRESS so it |
|
doesn't affect the parsing result. |
|
""" |
|
del dest |
|
self._flag_instance = flag_instance |
|
super(_FlagAction, self).__init__( |
|
option_strings=option_strings, |
|
dest=argparse.SUPPRESS, |
|
help=help, |
|
metavar=metavar) |
|
|
|
def __call__(self, parser, namespace, values, option_string=None): |
|
"""See https://docs.python.org/3/library/argparse.html#action-classes.""" |
|
self._flag_instance.parse(values) |
|
self._flag_instance.using_default_value = False |
|
|
|
|
|
class _BooleanFlagAction(argparse.Action): |
|
"""Action class for Abseil boolean flags.""" |
|
|
|
def __init__( |
|
self, |
|
option_strings, |
|
dest, |
|
help, |
|
metavar, |
|
flag_instance, |
|
default=argparse.SUPPRESS): |
|
"""Initializes _BooleanFlagAction. |
|
|
|
Args: |
|
option_strings: See argparse.Action. |
|
dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS. |
|
help: See argparse.Action. |
|
metavar: See argparse.Action. |
|
flag_instance: absl.flags.Flag, the absl flag instance. |
|
default: Ignored. The flag always uses dest=argparse.SUPPRESS so it |
|
doesn't affect the parsing result. |
|
""" |
|
del dest, default |
|
self._flag_instance = flag_instance |
|
flag_names = [self._flag_instance.name] |
|
if self._flag_instance.short_name: |
|
flag_names.append(self._flag_instance.short_name) |
|
self._flag_names = frozenset(flag_names) |
|
super(_BooleanFlagAction, self).__init__( |
|
option_strings=option_strings, |
|
dest=argparse.SUPPRESS, |
|
nargs=0, |
|
help=help, |
|
metavar=metavar) |
|
|
|
def __call__(self, parser, namespace, values, option_string=None): |
|
"""See https://docs.python.org/3/library/argparse.html#action-classes.""" |
|
if not isinstance(values, list) or values: |
|
raise ValueError('values must be an empty list.') |
|
if option_string.startswith('--'): |
|
option = option_string[2:] |
|
else: |
|
option = option_string[1:] |
|
if option in self._flag_names: |
|
self._flag_instance.parse('true') |
|
else: |
|
if not option.startswith('no') or option[2:] not in self._flag_names: |
|
raise ValueError('invalid option_string: ' + option_string) |
|
self._flag_instance.parse('false') |
|
self._flag_instance.using_default_value = False |
|
|
|
|
|
class _HelpFullAction(argparse.Action): |
|
"""Action class for --helpfull flag.""" |
|
|
|
def __init__(self, option_strings, dest, default, help): |
|
"""Initializes _HelpFullAction. |
|
|
|
Args: |
|
option_strings: See argparse.Action. |
|
dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS. |
|
default: Ignored. |
|
help: See argparse.Action. |
|
""" |
|
del dest, default |
|
super(_HelpFullAction, self).__init__( |
|
option_strings=option_strings, |
|
dest=argparse.SUPPRESS, |
|
default=argparse.SUPPRESS, |
|
nargs=0, |
|
help=help) |
|
|
|
def __call__(self, parser, namespace, values, option_string=None): |
|
"""See https://docs.python.org/3/library/argparse.html#action-classes.""" |
|
|
|
|
|
|
|
|
|
parser.print_help() |
|
|
|
absl_flags = parser._inherited_absl_flags |
|
if absl_flags: |
|
modules = sorted(absl_flags.flags_by_module_dict()) |
|
main_module = sys.argv[0] |
|
if main_module in modules: |
|
|
|
modules.remove(main_module) |
|
print(absl_flags._get_help_for_modules( |
|
modules, prefix='', include_special_flags=True)) |
|
parser.exit() |
|
|
|
|
|
def _strip_undefok_args(undefok, args): |
|
"""Returns a new list of args after removing flags in --undefok.""" |
|
if undefok: |
|
undefok_names = set(name.strip() for name in undefok.split(',')) |
|
undefok_names |= set('no' + name for name in undefok_names) |
|
|
|
args = [arg for arg in args if not _is_undefok(arg, undefok_names)] |
|
return args |
|
|
|
|
|
def _is_undefok(arg, undefok_names): |
|
"""Returns whether we can ignore arg based on a set of undefok flag names.""" |
|
if not arg.startswith('-'): |
|
return False |
|
if arg.startswith('--'): |
|
arg_without_dash = arg[2:] |
|
else: |
|
arg_without_dash = arg[1:] |
|
if '=' in arg_without_dash: |
|
name, _ = arg_without_dash.split('=', 1) |
|
else: |
|
name = arg_without_dash |
|
if name in undefok_names: |
|
return True |
|
return False |
|
|