Initial commit (Clean history)
This commit is contained in:
229
path/to/venv/lib/python3.12/site-packages/invoke/executor.py
Normal file
229
path/to/venv/lib/python3.12/site-packages/invoke/executor.py
Normal file
@@ -0,0 +1,229 @@
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from .config import Config
|
||||
from .parser import ParserContext
|
||||
from .util import debug
|
||||
from .tasks import Call, Task
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .collection import Collection
|
||||
from .runners import Result
|
||||
from .parser import ParseResult
|
||||
|
||||
|
||||
class Executor:
|
||||
"""
|
||||
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: "Collection",
|
||||
config: Optional["Config"] = None,
|
||||
core: Optional["ParseResult"] = None,
|
||||
) -> 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: Union[str, Tuple[str, Dict[str, Any]], ParserContext]
|
||||
) -> Dict["Task", "Result"]:
|
||||
"""
|
||||
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
|
||||
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, *call.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: Tuple[
|
||||
Union[str, Tuple[str, Dict[str, Any]], ParserContext], ...
|
||||
],
|
||||
) -> List["Call"]:
|
||||
"""
|
||||
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: Optional[str]
|
||||
if isinstance(task, str):
|
||||
name = task
|
||||
kwargs = {}
|
||||
elif isinstance(task, ParserContext):
|
||||
name = task.name
|
||||
kwargs = task.as_kwargs
|
||||
else:
|
||||
name, kwargs = task
|
||||
c = Call(self.collection[name], kwargs=kwargs, called_as=name)
|
||||
calls.append(c)
|
||||
if not tasks and self.collection.default is not None:
|
||||
calls = [Call(self.collection[self.collection.default])]
|
||||
return calls
|
||||
|
||||
def dedupe(self, calls: List["Call"]) -> List["Call"]:
|
||||
"""
|
||||
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: List["Call"]) -> List["Call"]:
|
||||
"""
|
||||
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(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
|
||||
Reference in New Issue
Block a user