File: executor.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 (212 lines) | stat: -rw-r--r-- 8,321 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
from .util import six

from .config import Config
from .parser import ParserContext
from .util import debug
from .tasks import Call, Task


class Executor(object):
    """
    An execution strategy for Task objects.

    Subclasses may override various extension points to change, add or remove
    behavior.

    .. versionadded:: 1.0
    """

    def __init__(self, collection, config=None, core=None):
        """
        Initialize executor with handles to necessary data structures.

        :param collection:
            A `.Collection` used to look up requested tasks (and their default
            config data, if any) by name during execution.

        :param config:
            An optional `.Config` holding configuration state. Defaults to an
            empty `.Config` if not given.

        :param core:
            An optional `.ParseResult` holding parsed core program arguments.
            Defaults to ``None``.
        """
        self.collection = collection
        self.config = config if config is not None else Config()
        self.core = core

    def execute(self, *tasks):
        """
        Execute one or more ``tasks`` in sequence.

        :param tasks:
            An all-purpose iterable of "tasks to execute", each member of which
            may take one of the following forms:

            **A string** naming a task from the Executor's `.Collection`. This
            name may contain dotted syntax appropriate for calling namespaced
            tasks, e.g. ``subcollection.taskname``. Such tasks are executed
            without arguments.

            **A two-tuple** whose first element is a task name string (as
            above) and whose second element is a dict suitable for use as
            ``**kwargs`` when calling the named task. E.g.::

                [
                    ('task1', {}),
                    ('task2', {'arg1': 'val1'}),
                    ...
                ]

            is equivalent, roughly, to::

                task1()
                task2(arg1='val1')

            **A `.ParserContext`** instance, whose ``.name`` attribute is used
            as the task name and whose ``.as_kwargs`` attribute is used as the
            task kwargs (again following the above specifications).

            .. note::
                When called without any arguments at all (i.e. when ``*tasks``
                is empty), the default task from ``self.collection`` is used
                instead, if defined.

        :returns:
            A dict mapping task objects to their return values.

            This dict may include pre- and post-tasks if any were executed. For
            example, in a collection with a ``build`` task depending on another
            task named ``setup``, executing ``build`` will result in a dict
            with two keys, one for ``build`` and one for ``setup``.

        .. versionadded:: 1.0
        """
        # Normalize input
        debug("Examining top level tasks {!r}".format([x for x in tasks]))
        calls = self.normalize(tasks)
        debug("Tasks (now Calls) with kwargs: {!r}".format(calls))
        # Obtain copy of directly-given tasks since they should sometimes
        # behave differently
        direct = list(calls)
        # Expand pre/post tasks
        # TODO: may make sense to bundle expansion & deduping now eh?
        expanded = self.expand_calls(calls)
        # Get some good value for dedupe option, even if config doesn't have
        # the tree we expect. (This is a concession to testing.)
        try:
            dedupe = self.config.tasks.dedupe
        except AttributeError:
            dedupe = True
        # Dedupe across entire run now that we know about all calls in order
        calls = self.dedupe(expanded) if dedupe else expanded
        # Execute
        results = {}
        # TODO: maybe clone initial config here? Probably not necessary,
        # especially given Executor is not designed to execute() >1 time at the
        # moment...
        for call in calls:
            autoprint = call in direct and call.autoprint
            args = call.args
            debug("Executing {!r}".format(call))
            # Hand in reference to our config, which will preserve user
            # modifications across the lifetime of the session.
            config = self.config
            # But make sure we reset its task-sensitive levels each time
            # (collection & shell env)
            # TODO: load_collection needs to be skipped if task is anonymous
            # (Fabric 2 or other subclassing libs only)
            collection_config = self.collection.configuration(call.called_as)
            config.load_collection(collection_config)
            config.load_shell_env()
            debug("Finished loading collection & shell env configs")
            # Get final context from the Call (which will know how to generate
            # an appropriate one; e.g. subclasses might use extra data from
            # being parameterized), handing in this config for use there.
            context = call.make_context(config)
            args = (context,) + args
            result = call.task(*args, **call.kwargs)
            if autoprint:
                print(result)
            # TODO: handle the non-dedupe case / the same-task-different-args
            # case, wherein one task obj maps to >1 result.
            results[call.task] = result
        return results

    def normalize(self, tasks):
        """
        Transform arbitrary task list w/ various types, into `.Call` objects.

        See docstring for `~.Executor.execute` for details.

        .. versionadded:: 1.0
        """
        calls = []
        for task in tasks:
            name, kwargs = None, {}
            if isinstance(task, six.string_types):
                name = task
            elif isinstance(task, ParserContext):
                name = task.name
                kwargs = task.as_kwargs
            else:
                name, kwargs = task
            c = Call(task=self.collection[name], kwargs=kwargs, called_as=name)
            calls.append(c)
        if not tasks and self.collection.default is not None:
            calls = [Call(task=self.collection[self.collection.default])]
        return calls

    def dedupe(self, calls):
        """
        Deduplicate a list of `tasks <.Call>`.

        :param calls: An iterable of `.Call` objects representing tasks.

        :returns: A list of `.Call` objects.

        .. versionadded:: 1.0
        """
        deduped = []
        debug("Deduplicating tasks...")
        for call in calls:
            if call not in deduped:
                debug("{!r}: no duplicates found, ok".format(call))
                deduped.append(call)
            else:
                debug("{!r}: found in list already, skipping".format(call))
        return deduped

    def expand_calls(self, calls):
        """
        Expand a list of `.Call` objects into a near-final list of same.

        The default implementation of this method simply adds a task's
        pre/post-task list before/after the task itself, as necessary.

        Subclasses may wish to do other things in addition (or instead of) the
        above, such as multiplying the `calls <.Call>` by argument vectors or
        similar.

        .. versionadded:: 1.0
        """
        ret = []
        for call in calls:
            # Normalize to Call (this method is sometimes called with pre/post
            # task lists, which may contain 'raw' Task objects)
            if isinstance(call, Task):
                call = Call(task=call)
            debug("Expanding task-call {!r}".format(call))
            # TODO: this is where we _used_ to call Executor.config_for(call,
            # config)...
            # TODO: now we may need to preserve more info like where the call
            # came from, etc, but I feel like that shit should go _on the call
            # itself_ right???
            # TODO: we _probably_ don't even want the config in here anymore,
            # we want this to _just_ be about the recursion across pre/post
            # tasks or parameterization...?
            ret.extend(self.expand_calls(call.pre))
            ret.append(call)
            ret.extend(self.expand_calls(call.post))
        return ret