Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/invoke/executor.py: 16%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
3from .config import Config
4from .parser import ParserContext
5from .util import debug
6from .tasks import Call, Task
8if TYPE_CHECKING:
9 from .collection import Collection
10 from .runners import Result
11 from .parser import ParseResult
14class Executor:
15 """
16 An execution strategy for Task objects.
18 Subclasses may override various extension points to change, add or remove
19 behavior.
21 .. versionadded:: 1.0
22 """
24 def __init__(
25 self,
26 collection: "Collection",
27 config: Optional["Config"] = None,
28 core: Optional["ParseResult"] = None,
29 ) -> None:
30 """
31 Initialize executor with handles to necessary data structures.
33 :param collection:
34 A `.Collection` used to look up requested tasks (and their default
35 config data, if any) by name during execution.
37 :param config:
38 An optional `.Config` holding configuration state. Defaults to an
39 empty `.Config` if not given.
41 :param core:
42 An optional `.ParseResult` holding parsed core program arguments.
43 Defaults to ``None``.
44 """
45 self.collection = collection
46 self.config = config if config is not None else Config()
47 self.core = core
49 def execute(
50 self, *tasks: Union[str, Tuple[str, Dict[str, Any]], ParserContext]
51 ) -> Dict["Task", "Result"]:
52 """
53 Execute one or more ``tasks`` in sequence.
55 :param tasks:
56 An all-purpose iterable of "tasks to execute", each member of which
57 may take one of the following forms:
59 **A string** naming a task from the Executor's `.Collection`. This
60 name may contain dotted syntax appropriate for calling namespaced
61 tasks, e.g. ``subcollection.taskname``. Such tasks are executed
62 without arguments.
64 **A two-tuple** whose first element is a task name string (as
65 above) and whose second element is a dict suitable for use as
66 ``**kwargs`` when calling the named task. E.g.::
68 [
69 ('task1', {}),
70 ('task2', {'arg1': 'val1'}),
71 ...
72 ]
74 is equivalent, roughly, to::
76 task1()
77 task2(arg1='val1')
79 **A `.ParserContext`** instance, whose ``.name`` attribute is used
80 as the task name and whose ``.as_kwargs`` attribute is used as the
81 task kwargs (again following the above specifications).
83 .. note::
84 When called without any arguments at all (i.e. when ``*tasks``
85 is empty), the default task from ``self.collection`` is used
86 instead, if defined.
88 :returns:
89 A dict mapping task objects to their return values.
91 This dict may include pre- and post-tasks if any were executed. For
92 example, in a collection with a ``build`` task depending on another
93 task named ``setup``, executing ``build`` will result in a dict
94 with two keys, one for ``build`` and one for ``setup``.
96 .. versionadded:: 1.0
97 """
98 # Normalize input
99 debug("Examining top level tasks {!r}".format([x for x in tasks]))
100 calls = self.normalize(tasks)
101 debug("Tasks (now Calls) with kwargs: {!r}".format(calls))
102 # Obtain copy of directly-given tasks since they should sometimes
103 # behave differently
104 direct = list(calls)
105 # Expand pre/post tasks
106 # TODO: may make sense to bundle expansion & deduping now eh?
107 expanded = self.expand_calls(calls)
108 # Get some good value for dedupe option, even if config doesn't have
109 # the tree we expect. (This is a concession to testing.)
110 try:
111 dedupe = self.config.tasks.dedupe
112 except AttributeError:
113 dedupe = True
114 # Dedupe across entire run now that we know about all calls in order
115 calls = self.dedupe(expanded) if dedupe else expanded
116 # Execute
117 results = {}
118 # TODO: maybe clone initial config here? Probably not necessary,
119 # especially given Executor is not designed to execute() >1 time at the
120 # moment...
121 for call in calls:
122 autoprint = call in direct and call.autoprint
123 debug("Executing {!r}".format(call))
124 # Hand in reference to our config, which will preserve user
125 # modifications across the lifetime of the session.
126 config = self.config
127 # But make sure we reset its task-sensitive levels each time
128 # (collection & shell env)
129 # TODO: load_collection needs to be skipped if task is anonymous
130 # (Fabric 2 or other subclassing libs only)
131 collection_config = self.collection.configuration(call.called_as)
132 config.load_collection(collection_config)
133 config.load_shell_env()
134 debug("Finished loading collection & shell env configs")
135 # Get final context from the Call (which will know how to generate
136 # an appropriate one; e.g. subclasses might use extra data from
137 # being parameterized), handing in this config for use there.
138 context = call.make_context(config)
139 args = (context, *call.args)
140 result = call.task(*args, **call.kwargs)
141 if autoprint:
142 print(result)
143 # TODO: handle the non-dedupe case / the same-task-different-args
144 # case, wherein one task obj maps to >1 result.
145 results[call.task] = result
146 return results
148 def normalize(
149 self,
150 tasks: Tuple[
151 Union[str, Tuple[str, Dict[str, Any]], ParserContext], ...
152 ],
153 ) -> List["Call"]:
154 """
155 Transform arbitrary task list w/ various types, into `.Call` objects.
157 See docstring for `~.Executor.execute` for details.
159 .. versionadded:: 1.0
160 """
161 calls = []
162 for task in tasks:
163 name: Optional[str]
164 if isinstance(task, str):
165 name = task
166 kwargs = {}
167 elif isinstance(task, ParserContext):
168 name = task.name
169 kwargs = task.as_kwargs
170 else:
171 name, kwargs = task
172 c = Call(self.collection[name], kwargs=kwargs, called_as=name)
173 calls.append(c)
174 if not tasks and self.collection.default is not None:
175 calls = [Call(self.collection[self.collection.default])]
176 return calls
178 def dedupe(self, calls: List["Call"]) -> List["Call"]:
179 """
180 Deduplicate a list of `tasks <.Call>`.
182 :param calls: An iterable of `.Call` objects representing tasks.
184 :returns: A list of `.Call` objects.
186 .. versionadded:: 1.0
187 """
188 deduped = []
189 debug("Deduplicating tasks...")
190 for call in calls:
191 if call not in deduped:
192 debug("{!r}: no duplicates found, ok".format(call))
193 deduped.append(call)
194 else:
195 debug("{!r}: found in list already, skipping".format(call))
196 return deduped
198 def expand_calls(self, calls: List["Call"]) -> List["Call"]:
199 """
200 Expand a list of `.Call` objects into a near-final list of same.
202 The default implementation of this method simply adds a task's
203 pre/post-task list before/after the task itself, as necessary.
205 Subclasses may wish to do other things in addition (or instead of) the
206 above, such as multiplying the `calls <.Call>` by argument vectors or
207 similar.
209 .. versionadded:: 1.0
210 """
211 ret = []
212 for call in calls:
213 # Normalize to Call (this method is sometimes called with pre/post
214 # task lists, which may contain 'raw' Task objects)
215 if isinstance(call, Task):
216 call = Call(call)
217 debug("Expanding task-call {!r}".format(call))
218 # TODO: this is where we _used_ to call Executor.config_for(call,
219 # config)...
220 # TODO: now we may need to preserve more info like where the call
221 # came from, etc, but I feel like that shit should go _on the call
222 # itself_ right???
223 # TODO: we _probably_ don't even want the config in here anymore,
224 # we want this to _just_ be about the recursion across pre/post
225 # tasks or parameterization...?
226 ret.extend(self.expand_calls(call.pre))
227 ret.append(call)
228 ret.extend(self.expand_calls(call.post))
229 return ret