1# coding: utf-8 2""" 3Integration Tests 4~~~~~~~~~~~~~~~~~ 5""" 6import sys 7import re 8import argparse 9 10import iocapture 11import mock 12import pytest 13 14import argh 15from argh.exceptions import AssemblingError 16 17from .base import DebugArghParser, get_usage_string, run, CmdResult as R 18 19 20@pytest.mark.xfail(reason='TODO') 21def test_guessing_integration(): 22 "guessing is used in dispatching" 23 assert 0 24 25 26def test_set_default_command_integration(): 27 def cmd(foo=1): 28 return foo 29 30 p = DebugArghParser() 31 p.set_default_command(cmd) 32 33 assert run(p, '') == R(out='1\n', err='') 34 assert run(p, '--foo 2') == R(out='2\n', err='') 35 assert run(p, '--help', exit=True) == None 36 37 38def test_set_default_command_integration_merging(): 39 @argh.arg('--foo', help='bar') 40 def cmd(foo=1): 41 return foo 42 43 p = DebugArghParser() 44 p.set_default_command(cmd) 45 46 assert run(p, '') == R(out='1\n', err='') 47 assert run(p, '--foo 2') == R(out='2\n', err='') 48 assert 'bar' in p.format_help() 49 50 51# 52# Function can be added to parser as is 53# 54 55 56def test_simple_function_no_args(): 57 def cmd(): 58 yield 1 59 60 p = DebugArghParser() 61 p.set_default_command(cmd) 62 63 assert run(p, '') == R(out='1\n', err='') 64 65 66def test_simple_function_positional(): 67 def cmd(x): 68 yield x 69 70 p = DebugArghParser() 71 p.set_default_command(cmd) 72 73 if sys.version_info < (3,3): 74 msg = 'too few arguments' 75 else: 76 msg = 'the following arguments are required: x' 77 assert run(p, '', exit=True) == msg 78 assert run(p, 'foo') == R(out='foo\n', err='') 79 80 81def test_simple_function_defaults(): 82 def cmd(x='foo'): 83 yield x 84 85 p = DebugArghParser() 86 p.set_default_command(cmd) 87 88 assert run(p, '') == R(out='foo\n', err='') 89 assert run(p, 'bar', exit=True) == 'unrecognized arguments: bar' 90 assert run(p, '--x bar') == R(out='bar\n', err='') 91 92 93def test_simple_function_varargs(): 94 95 def func(*file_paths): 96 # `paths` is the single positional argument with nargs='*' 97 yield ', '.join(file_paths) 98 99 p = DebugArghParser() 100 p.set_default_command(func) 101 102 assert run(p, '') == R(out='\n', err='') 103 assert run(p, 'foo') == R(out='foo\n', err='') 104 assert run(p, 'foo bar') == R(out='foo, bar\n', err='') 105 106 107def test_simple_function_kwargs(): 108 109 @argh.arg('foo') 110 @argh.arg('--bar') 111 def cmd(**kwargs): 112 # `kwargs` contain all arguments not fitting ArgSpec.args and .varargs. 113 # if ArgSpec.keywords in None, all @arg()'s will have to fit ArgSpec.args 114 for k in sorted(kwargs): 115 yield '{0}: {1}'.format(k, kwargs[k]) 116 117 p = DebugArghParser() 118 p.set_default_command(cmd) 119 120 if sys.version_info < (3,3): 121 msg = 'too few arguments' 122 else: 123 msg = 'the following arguments are required: foo' 124 assert run(p, '', exit=True) == msg 125 assert run(p, 'hello') == R(out='bar: None\nfoo: hello\n', err='') 126 assert run(p, '--bar 123', exit=True) == msg 127 assert run(p, 'hello --bar 123') == R(out='bar: 123\nfoo: hello\n', err='') 128 129 130@pytest.mark.xfail 131def test_simple_function_multiple(): 132 raise NotImplementedError 133 134 135@pytest.mark.xfail 136def test_simple_function_nested(): 137 raise NotImplementedError 138 139 140@pytest.mark.xfail 141def test_class_method_as_command(): 142 raise NotImplementedError 143 144 145def test_all_specs_in_one(): 146 147 @argh.arg('foo') 148 @argh.arg('--bar') 149 @argh.arg('fox') 150 @argh.arg('--baz') 151 def cmd(foo, bar=1, *args, **kwargs): 152 yield 'foo: {0}'.format(foo) 153 yield 'bar: {0}'.format(bar) 154 yield '*args: {0}'.format(args) 155 for k in sorted(kwargs): 156 yield '** {0}: {1}'.format(k, kwargs[k]) 157 158 p = DebugArghParser() 159 p.set_default_command(cmd) 160 161 # 1) bar=1 is treated as --bar so positionals from @arg that go **kwargs 162 # will still have higher priority than bar. 163 # 2) *args, a positional with nargs='*', sits between two required 164 # positionals (foo and fox), so it gets nothing. 165 assert run(p, 'one two') == R(out= 166 'foo: one\n' 167 'bar: 1\n' 168 '*args: ()\n' 169 '** baz: None\n' 170 '** fox: two\n', err='') 171 172 # two required positionals (foo and fox) get an argument each and one extra 173 # is left; therefore the middle one is given to *args. 174 assert run(p, 'one two three') == R(out= 175 'foo: one\n' 176 'bar: 1\n' 177 "*args: ('two',)\n" 178 '** baz: None\n' 179 '** fox: three\n', err='') 180 181 # two required positionals (foo and fox) get an argument each and two extra 182 # are left; both are given to *args (it's greedy). 183 assert run(p, 'one two three four') == R(out= 184 'foo: one\n' 185 'bar: 1\n' 186 "*args: ('two', 'three')\n" 187 '** baz: None\n' 188 '** fox: four\n', err='') 189 190 191def test_arg_merged(): 192 """ @arg merges into function signature. 193 """ 194 @argh.arg('my', help='a moose once bit my sister') 195 @argh.arg('-b', '--brain', help='i am made entirely of wood') 196 def gumby(my, brain=None): 197 return my, brain, 'hurts' 198 199 p = DebugArghParser('PROG') 200 p.set_default_command(gumby) 201 help_msg = p.format_help() 202 203 assert 'a moose once bit my sister' in help_msg 204 assert 'i am made entirely of wood' in help_msg 205 206 207def test_arg_mismatch_positional(): 208 """ An `@arg('positional')` must match function signature. 209 """ 210 @argh.arg('bogus-argument') 211 def confuse_a_cat(vet, funny_things=123): 212 return vet, funny_things 213 214 p = DebugArghParser('PROG') 215 with pytest.raises(AssemblingError) as excinfo: 216 p.set_default_command(confuse_a_cat) 217 218 msg = ("confuse_a_cat: argument bogus-argument does not fit " 219 "function signature: vet, -f/--funny-things") 220 assert msg in str(excinfo.value) 221 222 223def test_arg_mismatch_flag(): 224 """ An `@arg('--flag')` must match function signature. 225 """ 226 @argh.arg('--bogus-argument') 227 def confuse_a_cat(vet, funny_things=123): 228 return vet, funny_things 229 230 p = DebugArghParser('PROG') 231 with pytest.raises(AssemblingError) as excinfo: 232 p.set_default_command(confuse_a_cat) 233 234 msg = ("confuse_a_cat: argument --bogus-argument does not fit " 235 "function signature: vet, -f/--funny-things") 236 assert msg in str(excinfo.value) 237 238 239def test_arg_mismatch_positional_vs_flag(): 240 """ An `@arg('arg')` must match a positional arg in function signature. 241 """ 242 @argh.arg('foo') 243 def func(foo=123): 244 return foo 245 246 p = DebugArghParser('PROG') 247 with pytest.raises(AssemblingError) as excinfo: 248 p.set_default_command(func) 249 250 msg = ('func: argument "foo" declared as optional (in function signature)' 251 ' and positional (via decorator)') 252 assert msg in str(excinfo.value) 253 254 255def test_arg_mismatch_flag_vs_positional(): 256 """ An `@arg('--flag')` must match a keyword in function signature. 257 """ 258 @argh.arg('--foo') 259 def func(foo): 260 return foo 261 262 p = DebugArghParser('PROG') 263 with pytest.raises(AssemblingError) as excinfo: 264 p.set_default_command(func) 265 266 msg = ('func: argument "foo" declared as positional (in function signature)' 267 ' and optional (via decorator)') 268 assert msg in str(excinfo.value) 269 270 271class TestErrorWrapping: 272 273 def _get_parrot(self): 274 def parrot(dead=False): 275 if dead: 276 raise ValueError('this parrot is no more') 277 else: 278 return 'beautiful plumage' 279 280 return parrot 281 282 def test_error_raised(self): 283 parrot = self._get_parrot() 284 285 p = DebugArghParser() 286 p.set_default_command(parrot) 287 288 assert run(p, '') == R('beautiful plumage\n', '') 289 with pytest.raises(ValueError) as excinfo: 290 run(p, '--dead') 291 assert re.match('this parrot is no more', str(excinfo.value)) 292 293 def test_error_wrapped(self): 294 parrot = self._get_parrot() 295 wrapped_parrot = argh.wrap_errors([ValueError])(parrot) 296 297 p = DebugArghParser() 298 p.set_default_command(wrapped_parrot) 299 300 assert run(p, '') == R('beautiful plumage\n', '') 301 assert run(p, '--dead') == R('', 'ValueError: this parrot is no more\n') 302 303 def test_processor(self): 304 parrot = self._get_parrot() 305 wrapped_parrot = argh.wrap_errors([ValueError])(parrot) 306 307 def failure(err): 308 return 'ERR: ' + str(err) + '!' 309 processed_parrot = argh.wrap_errors(processor=failure)(wrapped_parrot) 310 311 p = argh.ArghParser() 312 p.set_default_command(processed_parrot) 313 314 assert run(p, '--dead') == R('', 'ERR: this parrot is no more!\n') 315 316 def test_stderr_vs_stdout(self): 317 318 @argh.wrap_errors([KeyError]) 319 def func(key): 320 db = {'a': 1} 321 return db[key] 322 323 p = argh.ArghParser() 324 p.set_default_command(func) 325 326 assert run(p, 'a') == R(out='1\n', err='') 327 assert run(p, 'b') == R(out='', err="KeyError: 'b'\n") 328 329 330def test_argv(): 331 332 def echo(text): 333 return 'you said {0}'.format(text) 334 335 p = DebugArghParser() 336 p.add_commands([echo]) 337 338 _argv = sys.argv 339 340 sys.argv = sys.argv[:1] + ['echo', 'hi there'] 341 assert run(p, None) == R('you said hi there\n', '') 342 343 sys.argv = _argv 344 345 346def test_commands_not_defined(): 347 p = DebugArghParser() 348 349 assert run(p, '', {'raw_output': True}).out == p.format_usage() 350 assert run(p, '').out == p.format_usage() + '\n' 351 352 assert 'unrecognized arguments' in run(p, 'foo', exit=True) 353 assert 'unrecognized arguments' in run(p, '--foo', exit=True) 354 355 356def test_command_not_chosen(): 357 def cmd(args): 358 return 1 359 360 p = DebugArghParser() 361 p.add_commands([cmd]) 362 363 if sys.version_info < (3,3): 364 # Python before 3.3 exits with an error 365 assert 'too few arguments' in run(p, '', exit=True) 366 else: 367 # Python since 3.3 returns a help message and doesn't exit 368 assert 'usage:' in run(p, '').out 369 370 371def test_invalid_choice(): 372 def cmd(args): 373 return 1 374 375 # root level command 376 377 p = DebugArghParser() 378 p.add_commands([cmd]) 379 380 assert run(p, 'bar', exit=True).startswith('invalid choice') 381 382 if sys.version_info < (3,3): 383 # Python before 3.3 exits with a less informative error 384 assert 'too few arguments' in run(p, '--bar', exit=True) 385 else: 386 # Python since 3.3 exits with a more informative error 387 assert run(p, '--bar', exit=True) == 'unrecognized arguments: --bar' 388 389 # nested command 390 391 p = DebugArghParser() 392 p.add_commands([cmd], namespace='nest') 393 394 assert run(p, 'nest bar', exit=True).startswith('invalid choice') 395 396 if sys.version_info < (3,3): 397 # Python before 3.3 exits with a less informative error 398 assert 'too few arguments' in run(p, 'nest --bar', exit=True) 399 else: 400 # Python since 3.3 exits with a more informative error 401 assert run(p, 'nest --bar', exit=True) == 'unrecognized arguments: --bar' 402 403 404def test_unrecognized_arguments(): 405 def cmd(): 406 return 1 407 408 # single-command parser 409 410 p = DebugArghParser() 411 p.set_default_command(cmd) 412 413 assert run(p, '--bar', exit=True) == 'unrecognized arguments: --bar' 414 assert run(p, 'bar', exit=True) == 'unrecognized arguments: bar' 415 416 # multi-command parser 417 418 p = DebugArghParser() 419 p.add_commands([cmd]) 420 421 assert run(p, 'cmd --bar', exit=True) == 'unrecognized arguments: --bar' 422 assert run(p, 'cmd bar', exit=True) == 'unrecognized arguments: bar' 423 424 425def test_echo(): 426 "A simple command is resolved to a function." 427 428 def echo(text): 429 return 'you said {0}'.format(text) 430 431 p = DebugArghParser() 432 p.add_commands([echo]) 433 434 assert run(p, 'echo foo') == R(out='you said foo\n', err='') 435 436 437def test_bool_action(): 438 "Action `store_true`/`store_false` is inferred from default value." 439 440 def parrot(dead=False): 441 return 'this parrot is no more' if dead else 'beautiful plumage' 442 443 p = DebugArghParser() 444 p.add_commands([parrot]) 445 446 assert run(p, 'parrot').out == 'beautiful plumage\n' 447 assert run(p, 'parrot --dead').out == 'this parrot is no more\n' 448 449 450def test_bare_namespace(): 451 "A command can be resolved to a function, not a namespace." 452 453 def hello(): 454 return 'hello world' 455 456 p = DebugArghParser() 457 p.add_commands([hello], namespace='greet') 458 459 # without arguments 460 461 if sys.version_info < (3,3): 462 # Python before 3.3 exits with an error 463 assert run(p, 'greet', exit=True) == 'too few arguments' 464 else: 465 # Python since 3.3 returns a help message and doesn't exit 466 assert 'usage:' in run(p, 'greet', exit=True).out 467 468 # with an argument 469 470 if sys.version_info < (3,3): 471 # Python before 3.3 exits with a less informative error 472 message = 'too few arguments' 473 else: 474 # Python since 3.3 exits with a more informative error 475 message = 'unrecognized arguments: --name=world' 476 assert run(p, 'greet --name=world', exit=True) == message 477 478 479def test_namespaced_function(): 480 "A subcommand is resolved to a function." 481 482 def hello(name='world'): 483 return 'Hello {0}!'.format(name or 'world') 484 485 def howdy(buddy): 486 return 'Howdy {0}?'.format(buddy) 487 488 p = DebugArghParser() 489 p.add_commands([hello, howdy], namespace='greet') 490 491 assert run(p, 'greet hello').out == 'Hello world!\n' 492 assert run(p, 'greet hello --name=John').out == 'Hello John!\n' 493 assert run(p, 'greet hello John', exit=True) == 'unrecognized arguments: John' 494 495 if sys.version_info < (3,3): 496 # Python before 3.3 exits with a less informative error 497 message = 'too few arguments' 498 else: 499 # Python since 3.3 exits with a more informative error 500 message = 'the following arguments are required: buddy' 501 502 assert message in run(p, 'greet howdy --name=John', exit=True) 503 assert run(p, 'greet howdy John').out == 'Howdy John?\n' 504 505 506def test_explicit_cmd_name(): 507 508 @argh.named('new-name') 509 def orig_name(): 510 return 'ok' 511 512 p = DebugArghParser() 513 p.add_commands([orig_name]) 514 assert run(p, 'orig-name', exit=True).startswith('invalid choice') 515 assert run(p, 'new-name').out == 'ok\n' 516 517 518def test_aliases(): 519 520 @argh.aliases('alias2', 'alias3') 521 def alias1(): 522 return 'ok' 523 524 p = DebugArghParser() 525 p.add_commands([alias1]) 526 527 if argh.assembling.SUPPORTS_ALIASES: 528 assert run(p, 'alias1').out == 'ok\n' 529 assert run(p, 'alias2').out == 'ok\n' 530 assert run(p, 'alias3').out == 'ok\n' 531 532 533def test_help_alias(): 534 p = DebugArghParser() 535 536 # assert the commands don't fail 537 538 assert None == run(p, '--help', exit=True) 539 assert None == run(p, 'greet --help', exit=True) 540 assert None == run(p, 'greet hello --help', exit=True) 541 542 assert None == run(p, 'help', exit=True) 543 assert None == run(p, 'help greet', exit=True) 544 assert None == run(p, 'help greet hello', exit=True) 545 546 547def test_arg_order(): 548 """Positional arguments are resolved in the order in which the @arg 549 decorators are defined. 550 """ 551 def cmd(foo, bar): 552 return foo, bar 553 554 p = DebugArghParser() 555 p.set_default_command(cmd) 556 assert run(p, 'foo bar').out == 'foo\nbar\n' 557 558 559def test_raw_output(): 560 "If the raw_output flag is set, no extra whitespace is added" 561 562 def cmd(foo, bar): 563 return foo, bar 564 565 p = DebugArghParser() 566 p.set_default_command(cmd) 567 568 assert run(p, 'foo bar').out == 'foo\nbar\n' 569 assert run(p, 'foo bar', {'raw_output': True}).out == 'foobar' 570 571 572def test_output_file(): 573 574 def cmd(): 575 return 'Hello world!' 576 577 p = DebugArghParser() 578 p.set_default_command(cmd) 579 580 assert run(p, '').out == 'Hello world!\n' 581 assert run(p, '', {'output_file': None}).out == 'Hello world!\n' 582 583 584def test_command_error(): 585 586 def whiner_plain(): 587 raise argh.CommandError('I feel depressed.') 588 589 def whiner_iterable(): 590 yield 'Hello...' 591 raise argh.CommandError('I feel depressed.') 592 593 p = DebugArghParser() 594 p.add_commands([whiner_plain, whiner_iterable]) 595 596 assert run(p, 'whiner-plain') == R( 597 out='', err='CommandError: I feel depressed.\n') 598 assert run(p, 'whiner-iterable') == R( 599 out='Hello...\n', err='CommandError: I feel depressed.\n') 600 601 602def test_custom_namespace(): 603 604 @argh.expects_obj 605 def cmd(args): 606 return args.custom_value 607 608 p = DebugArghParser() 609 p.set_default_command(cmd) 610 namespace = argparse.Namespace() 611 namespace.custom_value = 'foo' 612 613 assert run(p, '', {'namespace': namespace}).out == 'foo\n' 614 615 616def test_normalized_keys(): 617 """ Underscores in function args are converted to dashes and back. 618 """ 619 def cmd(a_b): 620 return a_b 621 622 p = DebugArghParser() 623 p.set_default_command(cmd) 624 625 assert run(p, 'hello').out == 'hello\n' 626 627 628@mock.patch('argh.assembling.COMPLETION_ENABLED', True) 629def test_custom_argument_completer(): 630 "Issue #33: Enable custom per-argument shell completion" 631 632 @argh.arg('foo', completer='STUB') 633 def func(foo): 634 pass 635 636 p = argh.ArghParser() 637 p.set_default_command(func) 638 639 assert p._actions[-1].completer == 'STUB' 640 641 642def test_class_members(): 643 "Issue #34: class members as commands" 644 645 class Controller: 646 var = 123 647 648 def instance_meth(self, value): 649 return value, self.var 650 651 @classmethod 652 def class_meth(cls, value): 653 return value, cls.var 654 655 @staticmethod 656 def static_meth(value): 657 return value, 'w00t?' 658 659 @staticmethod 660 def static_meth2(value): 661 return value, 'huh!' 662 663 controller = Controller() 664 665 p = DebugArghParser() 666 p.add_commands([ 667 controller.instance_meth, 668 controller.class_meth, 669 controller.static_meth, 670 Controller.static_meth2, 671 ]) 672 673 assert run(p, 'instance-meth foo').out == 'foo\n123\n' 674 assert run(p, 'class-meth foo').out == 'foo\n123\n' 675 assert run(p, 'static-meth foo').out == 'foo\nw00t?\n' 676 assert run(p, 'static-meth2 foo').out == 'foo\nhuh!\n' 677 678 679def test_kwonlyargs(): 680 "Correct dispatch in presence of keyword-only arguments" 681 if sys.version_info < (3,0): 682 pytest.skip('unsupported configuration') 683 684 ns = {} 685 686 exec("""def cmd(*args, foo='1', bar, baz='3', **kwargs): 687 return ' '.join(args), foo, bar, baz, len(kwargs) 688 """, None, ns) 689 cmd = ns['cmd'] 690 691 p = DebugArghParser() 692 p.set_default_command(cmd) 693 694 assert (run(p, '--baz=done test this --bar=do').out == 695 'test this\n1\ndo\ndone\n0\n') 696 if sys.version_info < (3,3): 697 message = 'argument --bar is required' 698 else: 699 message = 'the following arguments are required: --bar' 700 assert run(p, 'test --foo=do', exit=True) == message 701 702 703def test_default_arg_values_in_help(): 704 "Argument defaults should appear in the help message implicitly" 705 706 @argh.arg('name', default='Basil') 707 @argh.arg('--task', default='hang the Moose') 708 @argh.arg('--note', help='why is it a remarkable animal?') 709 def remind(name, task=None, reason='there are creatures living in it', 710 note='it can speak English'): 711 return "Oh what is it now, can't you leave me in peace..." 712 713 p = DebugArghParser() 714 p.set_default_command(remind) 715 716 assert 'Basil' in p.format_help() 717 assert 'Moose' in p.format_help() 718 assert 'creatures' in p.format_help() 719 720 # explicit help message is not obscured by the implicit one... 721 assert 'remarkable animal' in p.format_help() 722 # ...but is still present 723 assert 'it can speak' in p.format_help() 724 725 726def test_default_arg_values_in_help__regression(): 727 "Empty string as default value → empty help string → broken argparse" 728 729 def foo(bar=''): 730 return bar 731 732 p = DebugArghParser() 733 p.set_default_command(foo) 734 735 # doesn't break 736 p.format_help() 737 738 # now check details 739 assert "-b BAR, --bar BAR ''" in p.format_help() 740 # note the empty str repr ^^^ 741 742 743def test_help_formatting_is_preserved(): 744 "Formatting of docstrings should not be messed up in help messages" 745 746 def func(): 747 """ 748 Sample function. 749 750 Parameters: 751 foo: float 752 An example argument. 753 bar: bool 754 Another argument. 755 """ 756 return 'hello' 757 758 p = DebugArghParser() 759 p.set_default_command(func) 760 761 assert func.__doc__ in p.format_help() 762 763 764def test_prog(): 765 "Program name propagates from sys.argv[0]" 766 767 def cmd(foo=1): 768 return foo 769 770 p = DebugArghParser() 771 p.add_commands([cmd]) 772 773 usage = get_usage_string() 774 775 with iocapture.capture() as captured: 776 assert run(p, '-h', exit=True) == None 777 assert captured.stdout.startswith(usage) 778 779 780def test_unknown_args(): 781 782 def cmd(foo=1): 783 return foo 784 785 p = DebugArghParser() 786 p.set_default_command(cmd) 787 788 usage = get_usage_string('[-f FOO]') 789 790 assert run(p, '--foo 1') == R(out='1\n', err='') 791 assert run(p, '--bar 1', exit=True) == 'unrecognized arguments: --bar 1' 792 assert run(p, '--bar 1', exit=False, 793 kwargs={'skip_unknown_args': True}) == R(out=usage, err='') 794