diff options
author | Richard Levasseur <rlevasseur@google.com> | 2018-08-13 11:06:08 -0700 |
---|---|---|
committer | Copybara-Service <copybara-piper@google.com> | 2018-08-13 11:06:32 -0700 |
commit | 6d9df2af09517656d83530d5841aeecfc41223cd (patch) | |
tree | 6a567f8d5842929be7b40102cebd5b0fb64357fd /absl/flags | |
parent | 183a37237f10785e342e6361e13e8a525fe24b54 (diff) | |
download | absl-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/BUILD | 37 | ||||
-rw-r--r-- | absl/flags/argparse_flags.py | 372 | ||||
-rw-r--r-- | absl/flags/tests/argparse_flags_test.py | 443 | ||||
-rw-r--r-- | absl/flags/tests/argparse_flags_test_helper.py | 89 |
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]) |