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