File: parser_parser.py

package info (click to toggle)
python-invoke 1.4.1%2Bds-0.1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 1,704 kB
  • sloc: python: 11,377; makefile: 18; sh: 12
file content (540 lines) | stat: -rw-r--r-- 22,646 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
from pytest import raises

from invoke.parser import Parser, Context, Argument, ParseError


class Parser_:
    def can_take_initial_context(self):
        c = Context()
        p = Parser(initial=c)
        assert p.initial == c

    def can_take_initial_and_other_contexts(self):
        c1 = Context("foo")
        c2 = Context("bar")
        p = Parser(initial=Context(), contexts=[c1, c2])
        assert p.contexts["foo"] == c1
        assert p.contexts["bar"] == c2

    def can_take_just_other_contexts(self):
        c = Context("foo")
        p = Parser(contexts=[c])
        assert p.contexts["foo"] == c

    def can_take_just_contexts_as_non_keyword_arg(self):
        c = Context("foo")
        p = Parser([c])
        assert p.contexts["foo"] == c

    def raises_ValueError_for_unnamed_Contexts_in_contexts(self):
        with raises(ValueError):
            Parser(initial=Context(), contexts=[Context()])

    def raises_error_for_context_name_clashes(self):
        with raises(ValueError):
            Parser(contexts=(Context("foo"), Context("foo")))

    def raises_error_for_context_alias_and_name_clashes(self):
        with raises(ValueError):
            Parser((Context("foo", aliases=("bar",)), Context("bar")))

    def raises_error_for_context_name_and_alias_clashes(self):
        # I.e. inverse of the above, which is a different code path.
        with raises(ValueError):
            Parser((Context("foo"), Context("bar", aliases=("foo",))))

    def takes_ignore_unknown_kwarg(self):
        Parser(ignore_unknown=True)

    def ignore_unknown_defaults_to_False(self):
        assert Parser().ignore_unknown is False

    class parse_argv:
        def parses_sys_argv_style_list_of_strings(self):
            "parses sys.argv-style list of strings"
            # Doesn't-blow-up tests FTL
            mytask = Context(name="mytask")
            mytask.add_arg("arg")
            p = Parser(contexts=[mytask])
            p.parse_argv(["mytask", "--arg", "value"])

        def returns_only_contexts_mentioned(self):
            task1 = Context("mytask")
            task2 = Context("othertask")
            result = Parser((task1, task2)).parse_argv(["othertask"])
            assert len(result) == 1
            assert result[0].name == "othertask"

        def raises_error_if_unknown_contexts_found(self):
            with raises(ParseError):
                Parser().parse_argv(["foo", "bar"])

        def unparsed_does_not_share_state(self):
            r = Parser(ignore_unknown=True).parse_argv(["self"])
            assert r.unparsed == ["self"]
            r2 = Parser(ignore_unknown=True).parse_argv(["contained"])
            assert r.unparsed == ["self"]  # NOT ['self', 'contained']
            assert r2.unparsed == ["contained"]  # NOT ['self', 'contained']

        def ignore_unknown_returns_unparsed_argv_instead(self):
            r = Parser(ignore_unknown=True).parse_argv(["foo", "bar", "--baz"])
            assert r.unparsed == ["foo", "bar", "--baz"]

        def ignore_unknown_does_not_mutate_rest_of_argv(self):
            p = Parser([Context("ugh")], ignore_unknown=True)
            r = p.parse_argv(["ugh", "what", "-nowai"])
            # NOT: ['what', '-n', '-w', '-a', '-i']
            assert r.unparsed == ["what", "-nowai"]

        def always_includes_initial_context_if_one_was_given(self):
            # Even if no core/initial flags were seen
            t1 = Context("t1")
            init = Context()
            result = Parser((t1,), initial=init).parse_argv(["t1"])
            assert result[0].name is None
            assert result[1].name == "t1"

        def returned_contexts_are_in_order_given(self):
            t1, t2 = Context("t1"), Context("t2")
            r = Parser((t1, t2)).parse_argv(["t2", "t1"])
            assert [x.name for x in r] == ["t2", "t1"]

        def returned_context_member_arguments_contain_given_values(self):
            c = Context("mytask", args=(Argument("boolean", kind=bool),))
            result = Parser((c,)).parse_argv(["mytask", "--boolean"])
            assert result[0].args["boolean"].value is True

        def inverse_bools_get_set_correctly(self):
            arg = Argument("myarg", kind=bool, default=True)
            c = Context("mytask", args=(arg,))
            r = Parser((c,)).parse_argv(["mytask", "--no-myarg"])
            assert r[0].args["myarg"].value is False

        def arguments_which_take_values_get_defaults_overridden_correctly(
            self
        ):  # noqa
            args = (Argument("arg", kind=str), Argument("arg2", kind=int))
            c = Context("mytask", args=args)
            argv = ["mytask", "--arg", "myval", "--arg2", "25"]
            result = Parser((c,)).parse_argv(argv)
            assert result[0].args["arg"].value == "myval"
            assert result[0].args["arg2"].value == 25

        def returned_arguments_not_given_contain_default_values(self):
            # I.e. a Context with args A and B, invoked with no mention of B,
            # should result in B existing in the result, with its default value
            # intact, and not e.g. None, or the arg not existing.
            a = Argument("name", kind=str)
            b = Argument("age", default=7)
            c = Context("mytask", args=(a, b))
            Parser((c,)).parse_argv(["mytask", "--name", "blah"])
            assert c.args["age"].value == 7

        def returns_remainder(self):
            "returns -- style remainder string chunk"
            r = Parser((Context("foo"),)).parse_argv(
                ["foo", "--", "bar", "biz"]
            )
            assert r.remainder == "bar biz"

        def clones_initial_context(self):
            a = Argument("foo", kind=bool)
            assert a.value is None
            c = Context(args=(a,))
            p = Parser(initial=c)
            assert p.initial is c
            r = p.parse_argv(["--foo"])
            assert p.initial is c
            c2 = r[0]
            assert c2 is not c
            a2 = c2.args["foo"]
            assert a2 is not a
            assert a.value is None
            assert a2.value is True

        def clones_noninitial_contexts(self):
            a = Argument("foo")
            assert a.value is None
            c = Context(name="mytask", args=(a,))
            p = Parser(contexts=(c,))
            assert p.contexts["mytask"] is c
            r = p.parse_argv(["mytask", "--foo", "val"])
            assert p.contexts["mytask"] is c
            c2 = r[0]
            assert c2 is not c
            a2 = c2.args["foo"]
            assert a2 is not a
            assert a.value is None
            assert a2.value == "val"

        class parsing_errors:
            def setup(self):
                self.p = Parser([Context(name="foo", args=[Argument("bar")])])

            def missing_flag_values_raise_ParseError(self):
                with raises(ParseError):
                    self.p.parse_argv(["foo", "--bar"])

            def attaches_context_to_ParseErrors(self):
                try:
                    self.p.parse_argv(["foo", "--bar"])
                except ParseError as e:
                    assert e.context is not None

            def attached_context_is_None_outside_contexts(self):
                try:
                    Parser().parse_argv(["wat"])
                except ParseError as e:
                    assert e.context is None

        class positional_arguments:
            def _basic(self):
                arg = Argument("pos", positional=True)
                mytask = Context(name="mytask", args=[arg])
                return Parser(contexts=[mytask])

            def single_positional_arg(self):
                r = self._basic().parse_argv(["mytask", "posval"])
                assert r[0].args["pos"].value == "posval"

            def omitted_positional_arg_raises_ParseError(self):
                try:
                    self._basic().parse_argv(["mytask"])
                except ParseError as e:
                    expected = "'mytask' did not receive required positional arguments: 'pos'"  # noqa
                    assert str(e) == expected
                else:
                    assert False, "Did not raise ParseError!"

            def omitted_positional_args_raises_ParseError(self):
                try:
                    arg = Argument("pos", positional=True)
                    arg2 = Argument("morepos", positional=True)
                    mytask = Context(name="mytask", args=[arg, arg2])
                    Parser(contexts=[mytask]).parse_argv(["mytask"])
                except ParseError as e:
                    expected = "'mytask' did not receive required positional arguments: 'pos', 'morepos'"  # noqa
                    assert str(e) == expected
                else:
                    assert False, "Did not raise ParseError!"

            def positional_args_eat_otherwise_valid_context_names(self):
                mytask = Context(
                    "mytask",
                    args=[
                        Argument("pos", positional=True),
                        Argument("nonpos", default="default"),
                    ],
                )
                Context("lolwut")
                result = Parser([mytask]).parse_argv(["mytask", "lolwut"])
                r = result[0]
                assert r.args["pos"].value == "lolwut"
                assert r.args["nonpos"].value == "default"
                assert len(result) == 1  # Not 2

            def positional_args_can_still_be_given_as_flags(self):
                # AKA "positional args can come anywhere in the context"
                pos1 = Argument("pos1", positional=True)
                pos2 = Argument("pos2", positional=True)
                nonpos = Argument("nonpos", positional=False, default="lol")
                mytask = Context("mytask", args=[pos1, pos2, nonpos])
                assert mytask.positional_args == [pos1, pos2]
                r = Parser([mytask]).parse_argv(
                    [
                        "mytask",
                        "--nonpos",
                        "wut",
                        "--pos2",
                        "pos2val",
                        "pos1val",
                    ]
                )[0]
                assert r.args["pos1"].value == "pos1val"
                assert r.args["pos2"].value == "pos2val"
                assert r.args["nonpos"].value == "wut"

        class equals_signs:
            def _compare(self, argname, invoke, value):
                c = Context("mytask", args=(Argument(argname, kind=str),))
                r = Parser((c,)).parse_argv(["mytask", invoke])
                assert r[0].args[argname].value == value

            def handles_equals_style_long_flags(self):
                self._compare("foo", "--foo=bar", "bar")

            def handles_equals_style_short_flags(self):
                self._compare("f", "-f=bar", "bar")

            def does_not_require_escaping_equals_signs_in_value(self):
                self._compare("f", "-f=biz=baz", "biz=baz")

        def handles_multiple_boolean_flags_per_context(self):
            c = Context(
                "mytask",
                args=(Argument("foo", kind=bool), Argument("bar", kind=bool)),
            )
            r = Parser([c]).parse_argv(["mytask", "--foo", "--bar"])
            a = r[0].args
            assert a.foo.value is True
            assert a.bar.value is True

    class optional_arg_values:
        def setup(self):
            self.parser = self._parser()

        def _parser(self, arguments=None):
            if arguments is None:
                arguments = (
                    Argument(
                        names=("foo", "f"), optional=True, default="mydefault"
                    ),
                )
            self.context = Context("mytask", args=arguments)
            self.parser = Parser([self.context])
            return self.parser

        def _parse(self, argstr, parser=None):
            parser = parser or self.parser
            return parser.parse_argv(["mytask"] + argstr.split())

        def _expect(self, argstr, expected, parser=None):
            result = self._parse(argstr, parser)
            assert result[0].args.foo.value == expected

        def no_value_becomes_True_not_default_value(self):
            self._expect("--foo", True)
            self._expect("-f", True)

        def value_given_gets_preserved_normally(self):
            for argstr in (
                "--foo whatever",
                "--foo=whatever",
                "-f whatever",
                "-f=whatever",
            ):
                self._expect(argstr, "whatever")

        def not_given_at_all_uses_default_value(self):
            self._expect("", "mydefault")

        class ambiguity_sanity_checks:
            def _test_for_ambiguity(self, invoke, parser=None):
                msg = "is ambiguous"
                try:
                    self._parse(invoke, parser or self.parser)
                # Expected result
                except ParseError as e:
                    assert msg in str(e)
                # No exception occurred at all? Bollocks.
                else:
                    assert False
                # Any other exceptions will naturally cause failure here.

            def unfilled_posargs(self):
                p = self._parser(
                    (
                        Argument("foo", optional=True),
                        Argument("bar", positional=True),
                    )
                )
                self._test_for_ambiguity("--foo uhoh", p)

            def no_ambiguity_if_option_val_already_given(self):
                p = self._parser(
                    (
                        Argument("foo", optional=True),
                        Argument("bar", kind=bool),
                    )
                )
                # This should NOT raise a ParseError.
                result = self._parse("--foo hello --bar", p)
                assert result[0].args["foo"].value == "hello"
                assert result[0].args["bar"].value is True

            def valid_argument_is_NOT_ambiguous(self):
                # The one exception that proves the rule?
                self._parser((Argument("foo", optional=True), Argument("bar")))
                for form in ("--bar barval", "--bar=barval"):
                    result = self._parse("--foo {}".format(form))
                    assert len(result) == 1
                    args = result[0].args
                    assert args["foo"].value is True
                    assert args["bar"].value == "barval"

            def valid_flaglike_argument_is_NOT_ambiguous(self):
                # The OTHER exception that proves the rule?
                self._parser(
                    (
                        Argument("foo", optional=True),
                        Argument("bar", kind=bool),
                    )
                )
                result = self._parse("--foo --bar")
                assert len(result) == 1
                args = result[0].args
                assert args["foo"].value is True
                assert args["bar"].value is True

            def invalid_flaglike_value_is_stored_as_value(self):
                self._parser((Argument("foo", optional=True),))
                result = self._parse("--foo --bar")
                assert result[0].args["foo"].value == "--bar"

            def task_name(self):
                # mytask --foo myothertask
                c1 = Context("mytask", args=(Argument("foo", optional=True),))
                c2 = Context("othertask")
                p = Parser([c1, c2])
                self._test_for_ambiguity("--foo othertask", p)

    class list_type_arguments:
        "list-type (iterable) arguments"

        def _parse(self, *args):
            c = Context("mytask", args=(Argument("mylist", kind=list),))
            argv = ["mytask"] + list(args)
            return Parser([c]).parse_argv(argv)[0].args.mylist.value

        def can_be_given_no_times_resulting_in_default_empty_list(self):
            assert self._parse() == []

        def given_once_becomes_single_item_list(self):
            assert self._parse("--mylist", "foo") == ["foo"]

        def given_N_times_becomes_list_of_len_N(self):
            expected = ["foo", "bar", "biz"]
            got = self._parse(
                "--mylist", "foo", "--mylist", "bar", "--mylist", "biz"
            )
            assert got == expected

        def iterables_work_correctly_outside_a_vacuum(self):
            # Undetected bug where I was primarily focused on the -vvv use
            # case...'normal' incrementables never left 'waiting for value'
            # state in the parser! so _subsequent_ task names & such never got
            # parsed right, always got appended to the list.
            c = Context("mytask", args=[Argument("mylist", kind=list)])
            c2 = Context("othertask")
            argv = [
                "mytask",
                "--mylist",
                "val",
                "--mylist",
                "val2",
                "othertask",
            ]
            result = Parser([c, c2]).parse_argv(argv)
            # When bug present, result only has one context (for 'mytask') and
            # its 'mylist' consists of ['val', 'val2', 'othertask']. (the
            # middle '--mylist' was handled semi-correctly.)
            mylist = result[0].args.mylist.value
            assert mylist == ["val", "val2"]
            contexts = len(result)
            err = "Got {} parse context results instead of 2!".format(contexts)
            assert contexts == 2, err
            assert result[1].name == "othertask"

    class task_repetition:
        def is_happy_to_handle_same_task_multiple_times(self):
            task1 = Context("mytask")
            result = Parser((task1,)).parse_argv(["mytask", "mytask"])
            assert len(result) == 2
            for x in result:
                assert x.name == "mytask"

        def task_args_work_correctly(self):
            task1 = Context("mytask", args=(Argument("meh"),))
            result = Parser((task1,)).parse_argv(
                ["mytask", "--meh", "mehval1", "mytask", "--meh", "mehval2"]
            )
            assert result[0].args.meh.value == "mehval1"
            assert result[1].args.meh.value == "mehval2"

    class per_task_core_flags:
        class general:
            def _echo(self):
                return Argument("echo", kind=bool, default=False)

            def core_flags_work_normally_when_no_conflict(self):
                # Initial parse context with an --echo, plus a no-args task
                initial = Context(args=[self._echo()])
                task1 = Context("mytask")
                parser = Parser(initial=initial, contexts=[task1])
                # Call with --echo in the per-task context, expect the core
                # context got updated (vs an error)
                result = parser.parse_argv(["mytask", "--echo"])
                assert result[0].args.echo.value is True

            def when_conflict_per_task_args_win_out(self):
                # Initial parse context with an --echo, plus task w/ same
                initial = Context(args=[self._echo()])
                task1 = Context("mytask", args=[self._echo()])
                parser = Parser(initial=initial, contexts=[task1])
                # Call with --echo in the per-task context, expect the task
                # context got updated, and not core.
                result = parser.parse_argv(["mytask", "--echo"])
                assert result[0].args.echo.value is False
                assert result[1].args.echo.value is True

            def value_requiring_core_flags_also_work_correctly(self):
                "value-requiring core flags also work correctly"
                initial = Context(args=[Argument("hide")])
                task1 = Context("mytask")
                parser = Parser(initial=initial, contexts=[task1])
                result = parser.parse_argv(["mytask", "--hide", "both"])
                assert result[0].args.hide.value == "both"

        class edge_cases:
            def core_bool_but_per_task_string(self):
                # Initial parse context with bool --hide, and a task with a
                # regular (string) --hide
                initial = Context(
                    args=[Argument("hide", kind=bool, default=False)]
                )
                task1 = Context("mytask", args=[Argument("hide")])
                parser = Parser(initial=initial, contexts=[task1])
                # Expect that, because the task's version wins, we're able to
                # call it with a value. (If there were weird bugs where the
                # core flag informed the parsing, this would fail.)
                result = parser.parse_argv(["mytask", "--hide", "both"])
                assert result[0].args.hide.value is False
                assert result[1].args.hide.value == "both"

        class help_treats_context_name_as_its_value:
            def by_itself_base_case(self):
                task1 = Context("mytask")
                init = Context(args=[Argument("help", optional=True)])
                parser = Parser(initial=init, contexts=[task1])
                result = parser.parse_argv(["mytask", "--help"])
                assert len(result) == 2
                assert result[0].args.help.value == "mytask"
                assert "help" not in result[1].args

            def other_tokens_afterwards_raise_parse_errors(self):
                # NOTE: this is because of the special-casing where we supply
                # the task name as the value when the flag is literally named
                # "help".
                task1 = Context("mytask")
                init = Context(args=[Argument("help", optional=True)])
                parser = Parser(initial=init, contexts=[task1])
                with raises(ParseError, match=r".*foobar.*"):
                    parser.parse_argv(["mytask", "--help", "foobar"])


class ParseResult_:
    "ParseResult"

    def setup(self):
        self.context = Context(
            "mytask", args=(Argument("foo", kind=str), Argument("bar"))
        )
        argv = ["mytask", "--foo", "foo-val", "--", "my", "remainder"]
        self.result = Parser((self.context,)).parse_argv(argv)

    def acts_as_a_list_of_parsed_contexts(self):
        assert len(self.result) == 1
        assert self.result[0].name == "mytask"

    def exhibits_remainder_attribute(self):
        assert self.result.remainder == "my remainder"