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