File: task.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 (482 lines) | stat: -rw-r--r-- 14,512 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
from mock import Mock
from pytest import raises, skip

from invoke import Context, Config, task, Task, Call, Collection
from invoke import FilesystemLoader as Loader

from _util import support


#
# NOTE: Most Task tests use @task as it's the primary interface and is a very
# thin wrapper around Task itself. This way we don't have to write 2x tests for
# both Task and @task. Meh :)
#


def _func(c):
    pass


class task_:
    "@task"

    def _load(self, name):
        mod, _ = self.loader.load(name)
        return Collection.from_module(mod)

    def setup(self):
        self.loader = Loader(start=support)
        self.vanilla = self._load("decorators")

    def allows_access_to_wrapped_object(self):
        def lolcats(c):
            pass

        assert task(lolcats).body == lolcats

    def allows_alias_specification(self):
        assert self.vanilla["foo"] == self.vanilla["bar"]

    def allows_multiple_aliases(self):
        assert self.vanilla["foo"] == self.vanilla["otherbar"]

    def allows_default_specification(self):
        assert self.vanilla[""] == self.vanilla["biz"]

    def has_autoprint_option(self):
        ap = self._load("autoprint")
        assert ap["nope"].autoprint is False
        assert ap["yup"].autoprint is True

    def raises_ValueError_on_multiple_defaults(self):
        with raises(ValueError):
            self._load("decorator_multi_default")

    def sets_arg_help(self):
        assert self.vanilla["punch"].help["why"] == "Motive"

    def sets_arg_kind(self):
        skip()

    def sets_which_args_are_optional(self):
        assert self.vanilla["optional_values"].optional == ("myopt",)

    def allows_annotating_args_as_positional(self):
        assert self.vanilla["one_positional"].positional == ["pos"]
        assert self.vanilla["two_positionals"].positional == ["pos1", "pos2"]

    def allows_annotating_args_as_iterable(self):
        assert self.vanilla["iterable_values"].iterable == ["mylist"]

    def allows_annotating_args_as_incrementable(self):
        arg = self.vanilla["incrementable_values"]
        assert arg.incrementable == ["verbose"]

    def when_positional_arg_missing_all_non_default_args_are_positional(self):
        arg = self.vanilla["implicit_positionals"]
        assert arg.positional == ["pos1", "pos2"]

    def context_arguments_should_not_appear_in_implicit_positional_list(self):
        @task
        def mytask(c):
            pass

        assert len(mytask.positional) == 0

    def pre_tasks_stored_directly(self):
        @task
        def whatever(c):
            pass

        @task(pre=[whatever])
        def func(c):
            pass

        assert func.pre == [whatever]

    def allows_star_args_as_shortcut_for_pre(self):
        @task
        def pre1(c):
            pass

        @task
        def pre2(c):
            pass

        @task(pre1, pre2)
        def func(c):
            pass

        assert func.pre == (pre1, pre2)

    def disallows_ambiguity_between_star_args_and_pre_kwarg(self):
        @task
        def pre1(c):
            pass

        @task
        def pre2(c):
            pass

        with raises(TypeError):

            @task(pre1, pre=[pre2])
            def func(c):
                pass

    def sets_name(self):
        @task(name="foo")
        def bar(c):
            pass

        assert bar.name == "foo"

    def returns_Task_instances_by_default(self):
        @task
        def mytask(c):
            pass

        assert isinstance(mytask, Task)

    def klass_kwarg_allows_overriding_class_used(self):
        class MyTask(Task):
            pass

        @task(klass=MyTask)
        def mytask(c):
            pass

        assert isinstance(mytask, MyTask)

    def klass_kwarg_works_for_subclassers_without_kwargs(self):
        # I.e. the previous test doesn't catch this particular use case
        class MyTask(Task):
            pass

        def uses_MyTask(*args, **kwargs):
            kwargs.setdefault("klass", MyTask)
            return task(*args, **kwargs)

        @uses_MyTask
        def mytask(c):
            pass

        assert isinstance(mytask, MyTask)

    def unknown_kwargs_get_mad_at_Task_level(self):
        # NOTE: this was previously untested behavior. We actually just
        # modified HOW TypeError gets raised (Task constructor, implicitly, vs
        # explicitly in @task itself) but the end result is the same for anyone
        # not trying to be stringly typed based on exception message.
        with raises(TypeError):

            @task(whatever="man")
            def mytask(c):
                pass


class Task_:
    def has_useful_repr(self):
        i = repr(Task(_func))
        assert "_func" in i, "'func' not found in {!r}".format(i)
        e = repr(Task(_func, name="funky"))
        assert "funky" in e, "'funky' not found in {!r}".format(e)
        assert "_func" not in e, "'_func' unexpectedly seen in {!r}".format(e)

    def equality_testing(self):
        t1 = Task(_func, name="foo")
        t2 = Task(_func, name="foo")
        assert t1 == t2
        t3 = Task(_func, name="bar")
        assert t1 != t3

    class function_like_behavior:
        # Things that help them eg show up in autodoc easier
        def inherits_module_from_body(self):
            mytask = Task(_func, name="funky")
            assert mytask.__module__ is _func.__module__

    class attributes:
        def has_default_flag(self):
            assert Task(_func).is_default is False

        def name_defaults_to_body_name(self):
            assert Task(_func).name == "_func"

        def can_override_name(self):
            assert Task(_func, name="foo").name == "foo"

    class callability:
        def setup(self):
            @task
            def foo(c):
                "My docstring"
                return 5

            self.task = foo

        def dunder_call_wraps_body_call(self):
            context = Context()
            assert self.task(context) == 5

        def errors_if_first_arg_not_Context(self):
            @task
            def mytask(c):
                pass

            with raises(TypeError):
                mytask(5)

        def errors_if_no_first_arg_at_all(self):
            with raises(TypeError):

                @task
                def mytask():
                    pass

        def tracks_times_called(self):
            context = Context()
            assert self.task.called is False
            self.task(context)
            assert self.task.called is True
            assert self.task.times_called == 1
            self.task(context)
            assert self.task.times_called == 2

        def wraps_body_docstring(self):
            assert self.task.__doc__ == "My docstring"

        def wraps_body_name(self):
            assert self.task.__name__ == "foo"

    class get_arguments:
        def setup(self):
            @task(positional=["arg_3", "arg1"], optional=["arg1"])
            def mytask(c, arg1, arg2=False, arg_3=5):
                pass

            self.task = mytask
            self.args = self.task.get_arguments()
            self.argdict = self._arglist_to_dict(self.args)

        def _arglist_to_dict(self, arglist):
            # This kinda duplicates Context.add_arg(x) for x in arglist :(
            ret = {}
            for arg in arglist:
                for name in arg.names:
                    ret[name] = arg
            return ret

        def _task_to_dict(self, task):
            return self._arglist_to_dict(task.get_arguments())

        def positional_args_come_first(self):
            assert self.args[0].name == "arg_3"
            assert self.args[1].name == "arg1"
            assert self.args[2].name == "arg2"

        def kinds_are_preserved(self):
            # Remember that the default 'kind' is a string.
            assert [x.kind for x in self.args] == [int, str, bool]

        def positional_flag_is_preserved(self):
            assert [x.positional for x in self.args] == [True, True, False]

        def optional_flag_is_preserved(self):
            assert [x.optional for x in self.args] == [False, True, False]

        def optional_prevents_bool_defaults_from_affecting_kind(self):
            # Re #416. See notes in the function under test for rationale.
            @task(optional=["myarg"])
            def mytask(c, myarg=False):
                pass

            arg = mytask.get_arguments()[0]
            assert arg.kind is str  # not bool!

        def optional_plus_nonbool_default_does_not_override_kind(self):
            @task(optional=["myarg"])
            def mytask(c, myarg=17):
                pass

            arg = mytask.get_arguments()[0]
            assert arg.kind is int  # not str!

        def turns_function_signature_into_Arguments(self):
            assert len(self.args), 3 == str(self.args)
            assert "arg2" in self.argdict

        def shortflags_created_by_default(self):
            assert "a" in self.argdict
            assert self.argdict["a"] is self.argdict["arg1"]

        def shortflags_dont_care_about_positionals(self):
            "Positionalness doesn't impact whether shortflags are made"
            for short, long_ in (("a", "arg1"), ("r", "arg2"), ("g", "arg-3")):
                assert self.argdict[short] is self.argdict[long_]

        def autocreated_short_flags_can_be_disabled(self):
            @task(auto_shortflags=False)
            def mytask(c, arg):
                pass

            args = self._task_to_dict(mytask)
            assert "a" not in args
            assert "arg" in args

        def autocreated_shortflags_dont_collide(self):
            "auto-created short flags don't collide"

            @task
            def mytask(c, arg1, arg2, barg):
                pass

            args = self._task_to_dict(mytask)
            assert "a" in args
            assert args["a"] is args["arg1"]
            assert "r" in args
            assert args["r"] is args["arg2"]
            assert "b" in args
            assert args["b"] is args["barg"]

        def early_auto_shortflags_shouldnt_lock_out_real_shortflags(self):
            # I.e. "task --foo -f" => --foo should NOT get to pick '-f' for its
            # shortflag or '-f' is totally fucked.
            @task
            def mytask(c, longarg, l):
                pass

            args = self._task_to_dict(mytask)
            assert "longarg" in args
            assert "o" in args
            assert args["o"] is args["longarg"]
            assert "l" in args

        def context_arguments_are_not_returned(self):
            @task
            def mytask(c):
                pass

            assert len(mytask.get_arguments()) == 0

        def underscores_become_dashes(self):
            @task
            def mytask(c, longer_arg):
                pass

            arg = mytask.get_arguments()[0]
            assert arg.names == ("longer-arg", "l")
            assert arg.attr_name == "longer_arg"
            assert arg.name == "longer_arg"


# Dummy task for Call tests
_ = object()


class Call_:
    def setup(self):
        self.task = Task(Mock(__name__="mytask"))

    class init:
        class task:
            def is_required(self):
                with raises(TypeError):
                    Call()

            def is_first_posarg(self):
                assert Call(_).task is _

        class called_as:
            def defaults_to_None(self):
                assert Call(_).called_as is None

            def may_be_given(self):
                assert Call(_, called_as="foo").called_as == "foo"

        class args:
            def defaults_to_empty_tuple(self):
                assert Call(_).args == tuple()

            def may_be_given(self):
                assert Call(_, args=(1, 2, 3)).args == (1, 2, 3)

        class kwargs:
            def defaults_to_empty_dict(self):
                assert Call(_).kwargs == dict()

            def may_be_given(self):
                assert Call(_, kwargs={"foo": "bar"}).kwargs == {"foo": "bar"}

    class stringrep:
        "__str__"

        def includes_task_name(self):
            call = Call(self.task)
            assert str(call) == "<Call 'mytask', args: (), kwargs: {}>"

        def works_for_subclasses(self):
            class MyCall(Call):
                pass

            call = MyCall(self.task)
            assert "<MyCall" in str(call)

        def includes_args_and_kwargs(self):
            call = Call(
                self.task,
                args=("posarg1", "posarg2"),
                # Single-key dict to avoid dict ordering issues
                kwargs={"kwarg1": "val1"},
            )
            expected = "<Call 'mytask', args: ('posarg1', 'posarg2'), kwargs: {'kwarg1': 'val1'}>"  # noqa
            assert str(call) == expected

        def includes_aka_if_explicit_name_given(self):
            call = Call(self.task, called_as="notmytask")
            expected = "<Call 'mytask' (called as: 'notmytask'), args: (), kwargs: {}>"  # noqa
            assert str(call) == expected

        def skips_aka_if_explicit_name_same_as_task_name(self):
            call = Call(self.task, called_as="mytask")
            assert str(call) == "<Call 'mytask', args: (), kwargs: {}>"

    class make_context:
        def requires_config_argument(self):
            with raises(TypeError):
                Call(_).make_context()

        def creates_a_new_Context_from_given_config(self):
            conf = Config(defaults={"foo": "bar"})
            c = Call(_).make_context(conf)
            assert isinstance(c, Context)
            assert c.foo == "bar"

    class clone:
        def returns_new_but_equivalent_object(self):
            orig = Call(self.task)
            clone = orig.clone()
            assert clone is not orig
            assert clone == orig

        def can_clone_into_a_subclass(self):
            orig = Call(self.task)

            class MyCall(Call):
                pass

            clone = orig.clone(into=MyCall)
            assert clone == orig
            assert isinstance(clone, MyCall)

        def can_be_given_extra_kwargs_to_clone_with(self):
            orig = Call(self.task)

            class MyCall(Call):
                def __init__(self, *args, **kwargs):
                    self.hooray = kwargs.pop("hooray")
                    super(MyCall, self).__init__(*args, **kwargs)

            clone = orig.clone(into=MyCall, with_={"hooray": "woo"})
            assert clone.hooray == "woo"