1# Copyright 2018 The Abseil Authors. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Tests for absl.flags.argparse_flags.""" 16 17import io 18import os 19import subprocess 20import sys 21import tempfile 22from unittest import mock 23 24from absl import flags 25from absl import logging 26from absl.flags import argparse_flags 27from absl.testing import _bazelize_command 28from absl.testing import absltest 29from absl.testing import parameterized 30 31 32class ArgparseFlagsTest(parameterized.TestCase): 33 34 def setUp(self): 35 super().setUp() 36 self._absl_flags = flags.FlagValues() 37 flags.DEFINE_bool( 38 'absl_bool', None, 'help for --absl_bool.', 39 short_name='b', flag_values=self._absl_flags) 40 # Add a boolean flag that starts with "no", to verify it can correctly 41 # handle the "no" prefixes in boolean flags. 42 flags.DEFINE_bool( 43 'notice', None, 'help for --notice.', 44 flag_values=self._absl_flags) 45 flags.DEFINE_string( 46 'absl_string', 'default', 'help for --absl_string=%.', 47 short_name='s', flag_values=self._absl_flags) 48 flags.DEFINE_integer( 49 'absl_integer', 1, 'help for --absl_integer.', 50 flag_values=self._absl_flags) 51 flags.DEFINE_float( 52 'absl_float', 1, 'help for --absl_integer.', 53 flag_values=self._absl_flags) 54 flags.DEFINE_enum( 55 'absl_enum', 'apple', ['apple', 'orange'], 'help for --absl_enum.', 56 flag_values=self._absl_flags) 57 58 def test_dash_as_prefix_char_only(self): 59 with self.assertRaises(ValueError): 60 argparse_flags.ArgumentParser(prefix_chars='/') 61 62 def test_default_inherited_absl_flags_value(self): 63 parser = argparse_flags.ArgumentParser() 64 self.assertIs(parser._inherited_absl_flags, flags.FLAGS) 65 66 def test_parse_absl_flags(self): 67 parser = argparse_flags.ArgumentParser( 68 inherited_absl_flags=self._absl_flags) 69 self.assertFalse(self._absl_flags.is_parsed()) 70 self.assertTrue(self._absl_flags['absl_string'].using_default_value) 71 self.assertTrue(self._absl_flags['absl_integer'].using_default_value) 72 self.assertTrue(self._absl_flags['absl_float'].using_default_value) 73 self.assertTrue(self._absl_flags['absl_enum'].using_default_value) 74 75 parser.parse_args( 76 ['--absl_string=new_string', '--absl_integer', '2']) 77 self.assertEqual(self._absl_flags.absl_string, 'new_string') 78 self.assertEqual(self._absl_flags.absl_integer, 2) 79 self.assertTrue(self._absl_flags.is_parsed()) 80 self.assertFalse(self._absl_flags['absl_string'].using_default_value) 81 self.assertFalse(self._absl_flags['absl_integer'].using_default_value) 82 self.assertTrue(self._absl_flags['absl_float'].using_default_value) 83 self.assertTrue(self._absl_flags['absl_enum'].using_default_value) 84 85 @parameterized.named_parameters( 86 ('true', ['--absl_bool'], True), 87 ('false', ['--noabsl_bool'], False), 88 ('does_not_accept_equal_value', ['--absl_bool=true'], SystemExit), 89 ('does_not_accept_space_value', ['--absl_bool', 'true'], SystemExit), 90 ('long_name_single_dash', ['-absl_bool'], SystemExit), 91 ('short_name', ['-b'], True), 92 ('short_name_false', ['-nob'], SystemExit), 93 ('short_name_double_dash', ['--b'], SystemExit), 94 ('short_name_double_dash_false', ['--nob'], SystemExit), 95 ) 96 def test_parse_boolean_flags(self, args, expected): 97 parser = argparse_flags.ArgumentParser( 98 inherited_absl_flags=self._absl_flags) 99 self.assertIsNone(self._absl_flags['absl_bool'].value) 100 self.assertIsNone(self._absl_flags['b'].value) 101 if isinstance(expected, bool): 102 parser.parse_args(args) 103 self.assertEqual(expected, self._absl_flags.absl_bool) 104 self.assertEqual(expected, self._absl_flags.b) 105 else: 106 with self.assertRaises(expected): 107 parser.parse_args(args) 108 109 @parameterized.named_parameters( 110 ('true', ['--notice'], True), 111 ('false', ['--nonotice'], False), 112 ) 113 def test_parse_boolean_existing_no_prefix(self, args, expected): 114 parser = argparse_flags.ArgumentParser( 115 inherited_absl_flags=self._absl_flags) 116 self.assertIsNone(self._absl_flags['notice'].value) 117 parser.parse_args(args) 118 self.assertEqual(expected, self._absl_flags.notice) 119 120 def test_unrecognized_flag(self): 121 parser = argparse_flags.ArgumentParser( 122 inherited_absl_flags=self._absl_flags) 123 with self.assertRaises(SystemExit): 124 parser.parse_args(['--unknown_flag=what']) 125 126 def test_absl_validators(self): 127 128 @flags.validator('absl_integer', flag_values=self._absl_flags) 129 def ensure_positive(value): 130 return value > 0 131 132 parser = argparse_flags.ArgumentParser( 133 inherited_absl_flags=self._absl_flags) 134 with self.assertRaises(SystemExit): 135 parser.parse_args(['--absl_integer', '-2']) 136 137 del ensure_positive 138 139 @parameterized.named_parameters( 140 ('regular_name_double_dash', '--absl_string=new_string', 'new_string'), 141 ('regular_name_single_dash', '-absl_string=new_string', SystemExit), 142 ('short_name_double_dash', '--s=new_string', SystemExit), 143 ('short_name_single_dash', '-s=new_string', 'new_string'), 144 ) 145 def test_dashes(self, argument, expected): 146 parser = argparse_flags.ArgumentParser( 147 inherited_absl_flags=self._absl_flags) 148 if isinstance(expected, str): 149 parser.parse_args([argument]) 150 self.assertEqual(self._absl_flags.absl_string, expected) 151 else: 152 with self.assertRaises(expected): 153 parser.parse_args([argument]) 154 155 def test_absl_flags_not_added_to_namespace(self): 156 parser = argparse_flags.ArgumentParser( 157 inherited_absl_flags=self._absl_flags) 158 args = parser.parse_args(['--absl_string=new_string']) 159 self.assertIsNone(getattr(args, 'absl_string', None)) 160 161 def test_mixed_flags_and_positional(self): 162 parser = argparse_flags.ArgumentParser( 163 inherited_absl_flags=self._absl_flags) 164 parser.add_argument('--header', help='Header message to print.') 165 parser.add_argument('integers', metavar='N', type=int, nargs='+', 166 help='an integer for the accumulator') 167 168 args = parser.parse_args( 169 ['--absl_string=new_string', '--header=HEADER', '--absl_integer', 170 '2', '3', '4']) 171 self.assertEqual(self._absl_flags.absl_string, 'new_string') 172 self.assertEqual(self._absl_flags.absl_integer, 2) 173 self.assertEqual(args.header, 'HEADER') 174 self.assertListEqual(args.integers, [3, 4]) 175 176 def test_subparsers(self): 177 parser = argparse_flags.ArgumentParser( 178 inherited_absl_flags=self._absl_flags) 179 parser.add_argument('--header', help='Header message to print.') 180 subparsers = parser.add_subparsers(help='The command to execute.') 181 182 sub_parser = subparsers.add_parser( 183 'sub_cmd', help='Sub command.', inherited_absl_flags=self._absl_flags) 184 sub_parser.add_argument('--sub_flag', help='Sub command flag.') 185 186 def sub_command_func(): 187 pass 188 189 sub_parser.set_defaults(command=sub_command_func) 190 191 args = parser.parse_args([ 192 '--header=HEADER', '--absl_string=new_value', 'sub_cmd', 193 '--absl_integer=2', '--sub_flag=new_sub_flag_value']) 194 195 self.assertEqual(args.header, 'HEADER') 196 self.assertEqual(self._absl_flags.absl_string, 'new_value') 197 self.assertEqual(args.command, sub_command_func) 198 self.assertEqual(self._absl_flags.absl_integer, 2) 199 self.assertEqual(args.sub_flag, 'new_sub_flag_value') 200 201 def test_subparsers_no_inherit_in_subparser(self): 202 parser = argparse_flags.ArgumentParser( 203 inherited_absl_flags=self._absl_flags) 204 subparsers = parser.add_subparsers(help='The command to execute.') 205 206 subparsers.add_parser( 207 'sub_cmd', help='Sub command.', 208 # Do not inherit absl flags in the subparser. 209 # This is the behavior that this test exercises. 210 inherited_absl_flags=None) 211 212 with self.assertRaises(SystemExit): 213 parser.parse_args(['sub_cmd', '--absl_string=new_value']) 214 215 def test_help_main_module_flags(self): 216 parser = argparse_flags.ArgumentParser( 217 inherited_absl_flags=self._absl_flags) 218 help_message = parser.format_help() 219 220 # Only the short name is shown in the usage string. 221 self.assertIn('[-s ABSL_STRING]', help_message) 222 # Both names are included in the options section. 223 self.assertIn('-s ABSL_STRING, --absl_string ABSL_STRING', help_message) 224 # Verify help messages. 225 self.assertIn('help for --absl_string=%.', help_message) 226 self.assertIn('<apple|orange>: help for --absl_enum.', help_message) 227 228 def test_help_non_main_module_flags(self): 229 flags.DEFINE_string( 230 'non_main_module_flag', 'default', 'help', 231 module_name='other.module', flag_values=self._absl_flags) 232 parser = argparse_flags.ArgumentParser( 233 inherited_absl_flags=self._absl_flags) 234 help_message = parser.format_help() 235 236 # Non main module key flags are not printed in the help message. 237 self.assertNotIn('non_main_module_flag', help_message) 238 239 def test_help_non_main_module_key_flags(self): 240 flags.DEFINE_string( 241 'non_main_module_flag', 'default', 'help', 242 module_name='other.module', flag_values=self._absl_flags) 243 flags.declare_key_flag('non_main_module_flag', flag_values=self._absl_flags) 244 parser = argparse_flags.ArgumentParser( 245 inherited_absl_flags=self._absl_flags) 246 help_message = parser.format_help() 247 248 # Main module key fags are printed in the help message, even if the flag 249 # is defined in another module. 250 self.assertIn('non_main_module_flag', help_message) 251 252 @parameterized.named_parameters( 253 ('h', ['-h']), 254 ('help', ['--help']), 255 ('helpshort', ['--helpshort']), 256 ('helpfull', ['--helpfull']), 257 ) 258 def test_help_flags(self, args): 259 parser = argparse_flags.ArgumentParser( 260 inherited_absl_flags=self._absl_flags) 261 with self.assertRaises(SystemExit): 262 parser.parse_args(args) 263 264 @parameterized.named_parameters( 265 ('h', ['-h']), 266 ('help', ['--help']), 267 ('helpshort', ['--helpshort']), 268 ('helpfull', ['--helpfull']), 269 ) 270 def test_no_help_flags(self, args): 271 parser = argparse_flags.ArgumentParser( 272 inherited_absl_flags=self._absl_flags, add_help=False) 273 with mock.patch.object(parser, 'print_help'): 274 with self.assertRaises(SystemExit): 275 parser.parse_args(args) 276 parser.print_help.assert_not_called() 277 278 def test_helpfull_message(self): 279 flags.DEFINE_string( 280 'non_main_module_flag', 'default', 'help', 281 module_name='other.module', flag_values=self._absl_flags) 282 parser = argparse_flags.ArgumentParser( 283 inherited_absl_flags=self._absl_flags) 284 with self.assertRaises(SystemExit),\ 285 mock.patch.object(sys, 'stdout', new=io.StringIO()) as mock_stdout: 286 parser.parse_args(['--helpfull']) 287 stdout_message = mock_stdout.getvalue() 288 logging.info('captured stdout message:\n%s', stdout_message) 289 self.assertIn('--non_main_module_flag', stdout_message) 290 self.assertIn('other.module', stdout_message) 291 # Make sure the main module is not included. 292 self.assertNotIn(sys.argv[0], stdout_message) 293 # Special flags defined in absl.flags. 294 self.assertIn('absl.flags:', stdout_message) 295 self.assertIn('--flagfile', stdout_message) 296 self.assertIn('--undefok', stdout_message) 297 298 @parameterized.named_parameters( 299 ('at_end', 300 ('1', '--absl_string=value_from_cmd', '--flagfile='), 301 'value_from_file'), 302 ('at_beginning', 303 ('--flagfile=', '1', '--absl_string=value_from_cmd'), 304 'value_from_cmd'), 305 ) 306 def test_flagfile(self, cmd_args, expected_absl_string_value): 307 # Set gnu_getopt to False, to verify it's ignored by argparse_flags. 308 self._absl_flags.set_gnu_getopt(False) 309 310 parser = argparse_flags.ArgumentParser( 311 inherited_absl_flags=self._absl_flags) 312 parser.add_argument('--header', help='Header message to print.') 313 parser.add_argument('integers', metavar='N', type=int, nargs='+', 314 help='an integer for the accumulator') 315 flagfile = tempfile.NamedTemporaryFile( 316 dir=absltest.TEST_TMPDIR.value, delete=False) 317 self.addCleanup(os.unlink, flagfile.name) 318 with flagfile: 319 flagfile.write(b''' 320# The flag file. 321--absl_string=value_from_file 322--absl_integer=1 323--header=header_from_file 324''') 325 326 expand_flagfile = lambda x: x + flagfile.name if x == '--flagfile=' else x 327 cmd_args = [expand_flagfile(x) for x in cmd_args] 328 args = parser.parse_args(cmd_args) 329 330 self.assertEqual([1], args.integers) 331 self.assertEqual('header_from_file', args.header) 332 self.assertEqual(expected_absl_string_value, self._absl_flags.absl_string) 333 334 @parameterized.parameters( 335 ('positional', {'positional'}, False), 336 ('--not_existed', {'existed'}, False), 337 ('--empty', set(), False), 338 ('-single_dash', {'single_dash'}, True), 339 ('--double_dash', {'double_dash'}, True), 340 ('--with_value=value', {'with_value'}, True), 341 ) 342 def test_is_undefok(self, arg, undefok_names, is_undefok): 343 self.assertEqual(is_undefok, argparse_flags._is_undefok(arg, undefok_names)) 344 345 @parameterized.named_parameters( 346 ('single', 'single', ['--single'], []), 347 ('multiple', 'first,second', ['--first', '--second'], []), 348 ('single_dash', 'dash', ['-dash'], []), 349 ('mixed_dash', 'mixed', ['-mixed', '--mixed'], []), 350 ('value', 'name', ['--name=value'], []), 351 ('boolean_positive', 'bool', ['--bool'], []), 352 ('boolean_negative', 'bool', ['--nobool'], []), 353 ('left_over', 'strip', ['--first', '--strip', '--last'], 354 ['--first', '--last']), 355 ) 356 def test_strip_undefok_args(self, undefok, args, expected_args): 357 actual_args = argparse_flags._strip_undefok_args(undefok, args) 358 self.assertListEqual(expected_args, actual_args) 359 360 @parameterized.named_parameters( 361 ('at_end', ['--unknown', '--undefok=unknown']), 362 ('at_beginning', ['--undefok=unknown', '--unknown']), 363 ('multiple', ['--unknown', '--undefok=unknown,another_unknown']), 364 ('with_value', ['--unknown=value', '--undefok=unknown']), 365 ('maybe_boolean', ['--nounknown', '--undefok=unknown']), 366 ('with_space', ['--unknown', '--undefok', 'unknown']), 367 ) 368 def test_undefok_flag_correct_use(self, cmd_args): 369 parser = argparse_flags.ArgumentParser( 370 inherited_absl_flags=self._absl_flags) 371 args = parser.parse_args(cmd_args) # Make sure it doesn't raise. 372 # Make sure `undefok` is not exposed in namespace. 373 sentinel = object() 374 self.assertIs(sentinel, getattr(args, 'undefok', sentinel)) 375 376 def test_undefok_flag_existing(self): 377 parser = argparse_flags.ArgumentParser( 378 inherited_absl_flags=self._absl_flags) 379 parser.parse_args( 380 ['--absl_string=new_value', '--undefok=absl_string']) 381 self.assertEqual('new_value', self._absl_flags.absl_string) 382 383 @parameterized.named_parameters( 384 ('no_equal', ['--unknown', 'value', '--undefok=unknown']), 385 ('single_dash', ['--unknown', '-undefok=unknown']), 386 ) 387 def test_undefok_flag_incorrect_use(self, cmd_args): 388 parser = argparse_flags.ArgumentParser( 389 inherited_absl_flags=self._absl_flags) 390 with self.assertRaises(SystemExit): 391 parser.parse_args(cmd_args) 392 393 def test_argument_default(self): 394 # Regression test for https://github.com/abseil/abseil-py/issues/171. 395 parser = argparse_flags.ArgumentParser( 396 inherited_absl_flags=self._absl_flags, argument_default=23) 397 parser.add_argument( 398 '--magic_number', type=int, help='The magic number to use.') 399 args = parser.parse_args([]) 400 self.assertEqual(args.magic_number, 23) 401 402 403class ArgparseWithAppRunTest(parameterized.TestCase): 404 405 @parameterized.named_parameters( 406 ('simple', 407 'main_simple', 'parse_flags_simple', 408 ['--argparse_echo=I am argparse.', '--absl_echo=I am absl.'], 409 ['I am argparse.', 'I am absl.']), 410 ('subcommand_roll_dice', 411 'main_subcommands', 'parse_flags_subcommands', 412 ['--argparse_echo=I am argparse.', '--absl_echo=I am absl.', 413 'roll_dice', '--num_faces=12'], 414 ['I am argparse.', 'I am absl.', 'Rolled a dice: ']), 415 ('subcommand_shuffle', 416 'main_subcommands', 'parse_flags_subcommands', 417 ['--argparse_echo=I am argparse.', '--absl_echo=I am absl.', 418 'shuffle', 'a', 'b', 'c'], 419 ['I am argparse.', 'I am absl.', 'Shuffled: ']), 420 ) 421 def test_argparse_with_app_run( 422 self, main_func_name, flags_parser_func_name, args, output_strings): 423 env = os.environ.copy() 424 env['MAIN_FUNC'] = main_func_name 425 env['FLAGS_PARSER_FUNC'] = flags_parser_func_name 426 helper = _bazelize_command.get_executable_path( 427 'absl/flags/tests/argparse_flags_test_helper') 428 try: 429 stdout = subprocess.check_output( 430 [helper] + args, env=env, universal_newlines=True) 431 except subprocess.CalledProcessError as e: 432 error_info = ('ERROR: argparse_helper failed\n' 433 'Command: {}\n' 434 'Exit code: {}\n' 435 '----- output -----\n{}' 436 '------------------') 437 error_info = error_info.format(e.cmd, e.returncode, 438 e.output + '\n' if e.output else '<empty>') 439 print(error_info, file=sys.stderr) 440 raise 441 442 for output_string in output_strings: 443 self.assertIn(output_string, stdout) 444 445 446if __name__ == '__main__': 447 absltest.main() 448