blob: dd8b505f7ddd470ba4c4e9fd5a533f5f63306a9b [file] [log] [blame]
# Copyright 2018 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""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))
# Remove inherited_absl_flags before calling super.
self._inherited_absl_flags = kwargs.pop('inherited_absl_flags', flags.FLAGS)
# Now call super to initialize argparse.ArgumentParser before calling
# add_argument in _define_absl_flags.
super(ArgumentParser, self).__init__(**kwargs)
if self.add_help:
# -h and --help are defined in super.
# Also add the --helpshort and --helpfull flags.
self.add_argument(
# Action 'help' defines a similar flag to -h/--help.
'--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:
# Handle --flagfile.
# Explicitly specify force_gnu=True, since argparse behaves like
# gnu_getopt: flags can be specified after positional arguments.
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)
# For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where
# sub-parsers don't preserve existing namespace attributes.
# Restore the undefok attribute if a sub-parser dropped it.
if undefok is not undefok_missing:
namespace.undefok = undefok
if self._inherited_absl_flags:
# Handle --undefok. At this point, `args` only contains unknown flags,
# so it won't strip defined flags that are also specified with --undefok.
# For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where
# sub-parsers don't preserve existing namespace attributes. The undefok
# attribute might not exist because a subparser dropped it.
if hasattr(namespace, 'undefok'):
args = _strip_undefok_args(namespace.undefok, args)
# absl flags are not exposed in the Namespace object. See Namespace:
# https://docs.python.org/3/library/argparse.html#argparse.Namespace.
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:
# Do not inherit built-in flags.
continue
flag_instance = absl_flags[name]
# Each flags with short_name appears in FLAGS twice, so only define
# when the dictionary key is equal to the regular name.
if name == flag_instance.name:
# Suppress the flag in the help short message if it's not a main
# module's key flag.
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:
# argparse help string uses %-formatting. Escape the literal %'s.
helptext = flag_instance.help.replace('%', '%%')
if flag_instance.boolean:
# Only add the `no` form to the long name.
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, # pylint: disable=redefined-builtin
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, # pylint: disable=redefined-builtin
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, # Does not accept values, only `--bool` or `--nobool`.
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): # pylint: disable=redefined-builtin
"""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."""
# This only prints flags when help is not argparse.SUPPRESS.
# It includes user defined argparse flags, as well as main module's
# key absl flags. Other absl flags use argparse.SUPPRESS, so they aren't
# printed here.
parser.print_help()
absl_flags = parser._inherited_absl_flags # pylint: disable=protected-access
if absl_flags:
modules = sorted(absl_flags.flags_by_module_dict())
main_module = sys.argv[0]
if main_module in modules:
# The main module flags are already printed in parser.print_help().
modules.remove(main_module)
print(absl_flags._get_help_for_modules( # pylint: disable=protected-access
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)
# Remove undefok flags.
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