aboutsummaryrefslogtreecommitdiff
path: root/absl/flags
diff options
context:
space:
mode:
authorRichard Levasseur <rlevasseur@google.com>2018-08-13 11:06:08 -0700
committerCopybara-Service <copybara-piper@google.com>2018-08-13 11:06:32 -0700
commit6d9df2af09517656d83530d5841aeecfc41223cd (patch)
tree6a567f8d5842929be7b40102cebd5b0fb64357fd /absl/flags
parent183a37237f10785e342e6361e13e8a525fe24b54 (diff)
downloadabsl-py-6d9df2af09517656d83530d5841aeecfc41223cd.tar.gz
Make absl flags integrate with argparse.
Argparse support is provided by two pieces: an argparse-compatible parser that understands absl flags, and app.run accepting a custom flag parser. argparse_flags.ArgumentParser is a custom argparse.ArgumentParser that makes absl defined flags available via the argparse APIs. The argparse_flags module contains more documentation. app.run accepts a flags_parser argument, which is responsible for parsing command line args and returning the value to pass onto main. The app.run function contains more documentation. Resolves issue #27 PiperOrigin-RevId: 208509632
Diffstat (limited to 'absl/flags')
-rw-r--r--absl/flags/BUILD37
-rw-r--r--absl/flags/argparse_flags.py372
-rw-r--r--absl/flags/tests/argparse_flags_test.py443
-rw-r--r--absl/flags/tests/argparse_flags_test_helper.py89
4 files changed, 941 insertions, 0 deletions
diff --git a/absl/flags/BUILD b/absl/flags/BUILD
index 662b17d..fa2b271 100644
--- a/absl/flags/BUILD
+++ b/absl/flags/BUILD
@@ -20,6 +20,13 @@ py_library(
)
py_library(
+ name = "argparse_flags",
+ srcs = ["argparse_flags.py"],
+ srcs_version = "PY2AND3",
+ deps = [":flags"],
+)
+
+py_library(
name = "_argument_parser",
srcs = ["_argument_parser.py"],
srcs_version = "PY2AND3",
@@ -164,6 +171,36 @@ py_test(
)
py_test(
+ name = "tests/argparse_flags_test",
+ size = "small",
+ srcs = ["tests/argparse_flags_test.py"],
+ data = [":tests/argparse_flags_test_helper"],
+ srcs_version = "PY2AND3",
+ deps = [
+ ":argparse_flags",
+ ":flags",
+ "//absl/logging",
+ "//absl/testing:_bazelize_command",
+ "//absl/testing:absltest",
+ "//absl/testing:parameterized",
+ "@mock_archive//:mock",
+ "@six_archive//:six",
+ ],
+)
+
+py_binary(
+ name = "tests/argparse_flags_test_helper",
+ testonly = 1,
+ srcs = ["tests/argparse_flags_test_helper.py"],
+ srcs_version = "PY2AND3",
+ deps = [
+ ":argparse_flags",
+ ":flags",
+ "//absl:app",
+ ],
+)
+
+py_test(
name = "tests/flags_formatting_test",
size = "small",
srcs = ["tests/flags_formatting_test.py"],
diff --git a/absl/flags/argparse_flags.py b/absl/flags/argparse_flags.py
new file mode 100644
index 0000000..a9b7cba
--- /dev/null
+++ b/absl/flags/argparse_flags.py
@@ -0,0 +1,372 @@
+# 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
+argparse.ArgumentParser. It takes care of collecting and defining absl flags
+in 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 absl.flags and 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.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+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', 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._assert_all_validators() # pylint: disable=protected-access
+ 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, metavar, flag_instance): # pylint: disable=redefined-builtin
+ """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.
+ """
+ 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): # pylint: disable=redefined-builtin
+ """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.
+ """
+ del dest
+ 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
diff --git a/absl/flags/tests/argparse_flags_test.py b/absl/flags/tests/argparse_flags_test.py
new file mode 100644
index 0000000..244f977
--- /dev/null
+++ b/absl/flags/tests/argparse_flags_test.py
@@ -0,0 +1,443 @@
+# 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.
+
+"""Tests for absl.flags.argparse_flags."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import subprocess
+import sys
+import tempfile
+
+from absl import flags
+from absl import logging
+from absl.flags import argparse_flags
+from absl.testing import _bazelize_command
+from absl.testing import absltest
+from absl.testing import parameterized
+import mock
+import six
+
+
+FLAGS = flags.FLAGS
+
+
+class ArgparseFlagsTest(parameterized.TestCase):
+
+ def setUp(self):
+ self._absl_flags = flags.FlagValues()
+ flags.DEFINE_bool(
+ 'absl_bool', None, 'help for --absl_bool.',
+ short_name='b', flag_values=self._absl_flags)
+ # Add a boolean flag that starts with "no", to verify it can correctly
+ # handle the "no" prefixes in boolean flags.
+ flags.DEFINE_bool(
+ 'notice', None, 'help for --notice.',
+ flag_values=self._absl_flags)
+ flags.DEFINE_string(
+ 'absl_string', 'default', 'help for --absl_string=%.',
+ short_name='s', flag_values=self._absl_flags)
+ flags.DEFINE_integer(
+ 'absl_integer', 1, 'help for --absl_integer.',
+ flag_values=self._absl_flags)
+ flags.DEFINE_float(
+ 'absl_float', 1, 'help for --absl_integer.',
+ flag_values=self._absl_flags)
+ flags.DEFINE_enum(
+ 'absl_enum', 'apple', ['apple', 'orange'], 'help for --absl_enum.',
+ flag_values=self._absl_flags)
+
+ def test_dash_as_prefix_char_only(self):
+ with self.assertRaises(ValueError):
+ argparse_flags.ArgumentParser(prefix_chars='/')
+
+ def test_default_inherited_absl_flags_value(self):
+ parser = argparse_flags.ArgumentParser()
+ self.assertIs(parser._inherited_absl_flags, flags.FLAGS)
+
+ def test_parse_absl_flags(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ self.assertFalse(self._absl_flags.is_parsed())
+ self.assertTrue(self._absl_flags['absl_string'].using_default_value)
+ self.assertTrue(self._absl_flags['absl_integer'].using_default_value)
+ self.assertTrue(self._absl_flags['absl_float'].using_default_value)
+ self.assertTrue(self._absl_flags['absl_enum'].using_default_value)
+
+ parser.parse_args(
+ ['--absl_string=new_string', '--absl_integer', '2'])
+ self.assertEqual(self._absl_flags.absl_string, 'new_string')
+ self.assertEqual(self._absl_flags.absl_integer, 2)
+ self.assertTrue(self._absl_flags.is_parsed())
+ self.assertFalse(self._absl_flags['absl_string'].using_default_value)
+ self.assertFalse(self._absl_flags['absl_integer'].using_default_value)
+ self.assertTrue(self._absl_flags['absl_float'].using_default_value)
+ self.assertTrue(self._absl_flags['absl_enum'].using_default_value)
+
+ @parameterized.named_parameters(
+ ('true', ['--absl_bool'], True),
+ ('false', ['--noabsl_bool'], False),
+ ('does_not_accept_equal_value', ['--absl_bool=true'], SystemExit),
+ ('does_not_accept_space_value', ['--absl_bool', 'true'], SystemExit),
+ ('long_name_single_dash', ['-absl_bool'], SystemExit),
+ ('short_name', ['-b'], True),
+ ('short_name_false', ['-nob'], SystemExit),
+ ('short_name_double_dash', ['--b'], SystemExit),
+ ('short_name_double_dash_false', ['--nob'], SystemExit),
+ )
+ def test_parse_boolean_flags(self, args, expected):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ self.assertIsNone(self._absl_flags['absl_bool'].value)
+ self.assertIsNone(self._absl_flags['b'].value)
+ if isinstance(expected, bool):
+ parser.parse_args(args)
+ self.assertEqual(expected, self._absl_flags.absl_bool)
+ self.assertEqual(expected, self._absl_flags.b)
+ else:
+ with self.assertRaises(expected):
+ parser.parse_args(args)
+
+ @parameterized.named_parameters(
+ ('true', ['--notice'], True),
+ ('false', ['--nonotice'], False),
+ )
+ def test_parse_boolean_existing_no_prefix(self, args, expected):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ self.assertIsNone(self._absl_flags['notice'].value)
+ parser.parse_args(args)
+ self.assertEqual(expected, self._absl_flags.notice)
+
+ def test_unrecognized_flag(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ with self.assertRaises(SystemExit):
+ parser.parse_args(['--unknown_flag=what'])
+
+ def test_absl_validators(self):
+
+ @flags.validator('absl_integer', flag_values=self._absl_flags)
+ def ensure_positive(value):
+ return value > 0
+
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ with self.assertRaises(SystemExit):
+ parser.parse_args(['--absl_integer', '-2'])
+
+ del ensure_positive
+
+ @parameterized.named_parameters(
+ ('regular_name_double_dash', '--absl_string=new_string', 'new_string'),
+ ('regular_name_single_dash', '-absl_string=new_string', SystemExit),
+ ('short_name_double_dash', '--s=new_string', SystemExit),
+ ('short_name_single_dash', '-s=new_string', 'new_string'),
+ )
+ def test_dashes(self, argument, expected):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ if isinstance(expected, six.string_types):
+ parser.parse_args([argument])
+ self.assertEqual(self._absl_flags.absl_string, expected)
+ else:
+ with self.assertRaises(expected):
+ parser.parse_args([argument])
+
+ def test_absl_flags_not_added_to_namespace(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ args = parser.parse_args(['--absl_string=new_string'])
+ self.assertIsNone(getattr(args, 'absl_string', None))
+
+ def test_mixed_flags_and_positional(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ parser.add_argument('--header', help='Header message to print.')
+ parser.add_argument('integers', metavar='N', type=int, nargs='+',
+ help='an integer for the accumulator')
+
+ args = parser.parse_args(
+ ['--absl_string=new_string', '--header=HEADER', '--absl_integer',
+ '2', '3', '4'])
+ self.assertEqual(self._absl_flags.absl_string, 'new_string')
+ self.assertEqual(self._absl_flags.absl_integer, 2)
+ self.assertEqual(args.header, 'HEADER')
+ self.assertListEqual(args.integers, [3, 4])
+
+ def test_subparsers(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ parser.add_argument('--header', help='Header message to print.')
+ subparsers = parser.add_subparsers(help='The command to execute.')
+
+ sub_parser = subparsers.add_parser(
+ 'sub_cmd', help='Sub command.', inherited_absl_flags=self._absl_flags)
+ sub_parser.add_argument('--sub_flag', help='Sub command flag.')
+
+ def sub_command_func():
+ pass
+
+ sub_parser.set_defaults(command=sub_command_func)
+
+ args = parser.parse_args([
+ '--header=HEADER', '--absl_string=new_value', 'sub_cmd',
+ '--absl_integer=2', '--sub_flag=new_sub_flag_value'])
+
+ self.assertEqual(args.header, 'HEADER')
+ self.assertEqual(self._absl_flags.absl_string, 'new_value')
+ self.assertEqual(args.command, sub_command_func)
+ self.assertEqual(self._absl_flags.absl_integer, 2)
+ self.assertEqual(args.sub_flag, 'new_sub_flag_value')
+
+ def test_subparsers_no_inherit_in_subparser(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ subparsers = parser.add_subparsers(help='The command to execute.')
+
+ subparsers.add_parser(
+ 'sub_cmd', help='Sub command.',
+ # Do not inherit absl flags in the subparser.
+ # This is the behavior that this test exercises.
+ inherited_absl_flags=None)
+
+ with self.assertRaises(SystemExit):
+ parser.parse_args(['sub_cmd', '--absl_string=new_value'])
+
+ def test_help_main_module_flags(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ help_message = parser.format_help()
+
+ # Only the short name is shown in the usage string.
+ self.assertIn('[-s ABSL_STRING]', help_message)
+ # Both names are included in the options section.
+ self.assertIn('-s ABSL_STRING, --absl_string ABSL_STRING', help_message)
+ # Verify help messages.
+ self.assertIn('help for --absl_string=%.', help_message)
+ self.assertIn('<apple|orange>: help for --absl_enum.', help_message)
+
+ def test_help_non_main_module_flags(self):
+ flags.DEFINE_string(
+ 'non_main_module_flag', 'default', 'help',
+ module_name='other.module', flag_values=self._absl_flags)
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ help_message = parser.format_help()
+
+ # Non main module key flags are not printed in the help message.
+ self.assertNotIn('non_main_module_flag', help_message)
+
+ def test_help_non_main_module_key_flags(self):
+ flags.DEFINE_string(
+ 'non_main_module_flag', 'default', 'help',
+ module_name='other.module', flag_values=self._absl_flags)
+ flags.declare_key_flag('non_main_module_flag', flag_values=self._absl_flags)
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ help_message = parser.format_help()
+
+ # Main module key fags are printed in the help message, even if the flag
+ # is defined in another module.
+ self.assertIn('non_main_module_flag', help_message)
+
+ @parameterized.named_parameters(
+ ('h', ['-h']),
+ ('help', ['--help']),
+ ('helpshort', ['--helpshort']),
+ ('helpfull', ['--helpfull']),
+ )
+ def test_help_flags(self, args):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ with self.assertRaises(SystemExit):
+ parser.parse_args(args)
+
+ @parameterized.named_parameters(
+ ('h', ['-h']),
+ ('help', ['--help']),
+ ('helpshort', ['--helpshort']),
+ ('helpfull', ['--helpfull']),
+ )
+ def test_no_help_flags(self, args):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags, add_help=False)
+ with mock.patch.object(parser, 'print_help'):
+ with self.assertRaises(SystemExit):
+ parser.parse_args(args)
+ parser.print_help.assert_not_called()
+
+ def test_helpfull_message(self):
+ flags.DEFINE_string(
+ 'non_main_module_flag', 'default', 'help',
+ module_name='other.module', flag_values=self._absl_flags)
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ with self.assertRaises(SystemExit),\
+ mock.patch.object(sys, 'stdout', new=six.StringIO()) as mock_stdout:
+ parser.parse_args(['--helpfull'])
+ stdout_message = mock_stdout.getvalue()
+ logging.info('captured stdout message:\n%s', stdout_message)
+ self.assertIn('--non_main_module_flag', stdout_message)
+ self.assertIn('other.module', stdout_message)
+ # Make sure the main module is not included.
+ self.assertNotIn(sys.argv[0], stdout_message)
+ # Special flags defined in absl.flags.
+ self.assertIn('absl.flags:', stdout_message)
+ self.assertIn('--flagfile', stdout_message)
+ self.assertIn('--undefok', stdout_message)
+
+ @parameterized.named_parameters(
+ ('at_end',
+ ('1', '--absl_string=value_from_cmd', '--flagfile='),
+ 'value_from_file'),
+ ('at_beginning',
+ ('--flagfile=', '1', '--absl_string=value_from_cmd'),
+ 'value_from_cmd'),
+ )
+ def test_flagfile(self, cmd_args, expected_absl_string_value):
+ # Set gnu_getopt to False, to verify it's ignored by argparse_flags.
+ self._absl_flags.set_gnu_getopt(False)
+
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ parser.add_argument('--header', help='Header message to print.')
+ parser.add_argument('integers', metavar='N', type=int, nargs='+',
+ help='an integer for the accumulator')
+ flagfile = tempfile.NamedTemporaryFile(dir=FLAGS.test_tmpdir, delete=False)
+ self.addCleanup(os.unlink, flagfile.name)
+ with flagfile:
+ flagfile.write(b'''
+# The flag file.
+--absl_string=value_from_file
+--absl_integer=1
+--header=header_from_file
+''')
+
+ expand_flagfile = lambda x: x + flagfile.name if x == '--flagfile=' else x
+ cmd_args = [expand_flagfile(x) for x in cmd_args]
+ args = parser.parse_args(cmd_args)
+
+ self.assertEqual([1], args.integers)
+ self.assertEqual('header_from_file', args.header)
+ self.assertEqual(expected_absl_string_value, self._absl_flags.absl_string)
+
+ @parameterized.parameters(
+ ('positional', {'positional'}, False),
+ ('--not_existed', {'existed'}, False),
+ ('--empty', set(), False),
+ ('-single_dash', {'single_dash'}, True),
+ ('--double_dash', {'double_dash'}, True),
+ ('--with_value=value', {'with_value'}, True),
+ )
+ def test_is_undefok(self, arg, undefok_names, is_undefok):
+ self.assertEqual(is_undefok, argparse_flags._is_undefok(arg, undefok_names))
+
+ @parameterized.named_parameters(
+ ('single', 'single', ['--single'], []),
+ ('multiple', 'first,second', ['--first', '--second'], []),
+ ('single_dash', 'dash', ['-dash'], []),
+ ('mixed_dash', 'mixed', ['-mixed', '--mixed'], []),
+ ('value', 'name', ['--name=value'], []),
+ ('boolean_positive', 'bool', ['--bool'], []),
+ ('boolean_negative', 'bool', ['--nobool'], []),
+ ('left_over', 'strip', ['--first', '--strip', '--last'],
+ ['--first', '--last']),
+ )
+ def test_strip_undefok_args(self, undefok, args, expected_args):
+ actual_args = argparse_flags._strip_undefok_args(undefok, args)
+ self.assertListEqual(expected_args, actual_args)
+
+ @parameterized.named_parameters(
+ ('at_end', ['--unknown', '--undefok=unknown']),
+ ('at_beginning', ['--undefok=unknown', '--unknown']),
+ ('multiple', ['--unknown', '--undefok=unknown,another_unknown']),
+ ('with_value', ['--unknown=value', '--undefok=unknown']),
+ ('maybe_boolean', ['--nounknown', '--undefok=unknown']),
+ ('with_space', ['--unknown', '--undefok', 'unknown']),
+ )
+ def test_undefok_flag_correct_use(self, cmd_args):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ args = parser.parse_args(cmd_args) # Make sure it doesn't raise.
+ # Make sure `undefok` is not exposed in namespace.
+ sentinel = object()
+ self.assertIs(sentinel, getattr(args, 'undefok', sentinel))
+
+ def test_undefok_flag_existing(self):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ parser.parse_args(
+ ['--absl_string=new_value', '--undefok=absl_string'])
+ self.assertEqual('new_value', self._absl_flags.absl_string)
+
+ @parameterized.named_parameters(
+ ('no_equal', ['--unknown', 'value', '--undefok=unknown']),
+ ('single_dash', ['--unknown', '-undefok=unknown']),
+ )
+ def test_undefok_flag_incorrect_use(self, cmd_args):
+ parser = argparse_flags.ArgumentParser(
+ inherited_absl_flags=self._absl_flags)
+ with self.assertRaises(SystemExit):
+ parser.parse_args(cmd_args)
+
+
+class ArgparseWithAppRunTest(parameterized.TestCase):
+
+ @parameterized.named_parameters(
+ ('simple',
+ 'main_simple', 'parse_flags_simple',
+ ['--argparse_echo=I am argparse.', '--absl_echo=I am absl.'],
+ ['I am argparse.', 'I am absl.']),
+ ('subcommand_roll_dice',
+ 'main_subcommands', 'parse_flags_subcommands',
+ ['--argparse_echo=I am argparse.', '--absl_echo=I am absl.',
+ 'roll_dice', '--num_faces=12'],
+ ['I am argparse.', 'I am absl.', 'Rolled a dice: ']),
+ ('subcommand_shuffle',
+ 'main_subcommands', 'parse_flags_subcommands',
+ ['--argparse_echo=I am argparse.', '--absl_echo=I am absl.',
+ 'shuffle', 'a', 'b', 'c'],
+ ['I am argparse.', 'I am absl.', 'Shuffled: ']),
+ )
+ def test_argparse_with_app_run(
+ self, main_func_name, flags_parser_func_name, args, output_strings):
+ env = os.environ.copy()
+ env['MAIN_FUNC'] = main_func_name
+ env['FLAGS_PARSER_FUNC'] = flags_parser_func_name
+ helper = 'absl/flags/tests/argparse_flags_test_helper'
+ try:
+ stdout = subprocess.check_output(
+ [_bazelize_command.get_executable_path(helper)] + args, env=env,
+ universal_newlines=True)
+ except subprocess.CalledProcessError as e:
+ error_info = ('ERROR: argparse_helper failed\n'
+ 'Command: {}\n'
+ 'Exit code: {}\n'
+ '----- output -----\n{}'
+ '------------------')
+ error_info = error_info.format(e.cmd, e.returncode,
+ e.output + '\n' if e.output else '<empty>')
+ print(error_info, file=sys.stderr)
+ raise
+
+ for output_string in output_strings:
+ self.assertIn(output_string, stdout)
+
+
+if __name__ == '__main__':
+ absltest.main()
diff --git a/absl/flags/tests/argparse_flags_test_helper.py b/absl/flags/tests/argparse_flags_test_helper.py
new file mode 100644
index 0000000..8cf42e6
--- /dev/null
+++ b/absl/flags/tests/argparse_flags_test_helper.py
@@ -0,0 +1,89 @@
+# 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.
+
+"""Test helper for argparse_flags_test."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import random
+
+from absl import app
+from absl import flags
+from absl.flags import argparse_flags
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_string('absl_echo', None, 'The echo message from absl.flags.')
+
+
+def parse_flags_simple(argv):
+ """Simple example for absl.flags + argparse."""
+ parser = argparse_flags.ArgumentParser(
+ description='A simple example of argparse_flags.')
+ parser.add_argument(
+ '--argparse_echo', help='The echo message from argparse_flags')
+ return parser.parse_args(argv[1:])
+
+
+def main_simple(args):
+ print('--absl_echo is', FLAGS.absl_echo)
+ print('--argparse_echo is', args.argparse_echo)
+
+
+def roll_dice(args):
+ print('Rolled a dice:', random.randint(1, args.num_faces))
+
+
+def shuffle(args):
+ inputs = list(args.inputs)
+ random.shuffle(inputs)
+ print('Shuffled:', ' '.join(inputs))
+
+
+def parse_flags_subcommands(argv):
+ """Subcommands example for absl.flags + argparse."""
+ parser = argparse_flags.ArgumentParser(
+ description='A subcommands example of argparse_flags.')
+ parser.add_argument('--argparse_echo',
+ help='The echo message from argparse_flags')
+
+ subparsers = parser.add_subparsers(help='The command to execute.')
+
+ roll_dice_parser = subparsers.add_parser(
+ 'roll_dice', help='Roll a dice.')
+ 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)
+
+ return parser.parse_args(argv[1:])
+
+
+def main_subcommands(args):
+ main_simple(args)
+ args.command(args)
+
+
+if __name__ == '__main__':
+ main_func_name = os.environ['MAIN_FUNC']
+ flags_parser_func_name = os.environ['FLAGS_PARSER_FUNC']
+ app.run(main=globals()[main_func_name],
+ flags_parser=globals()[flags_parser_func_name])