From 4cf7876d6eeb82ece9bd92367f29d4c26a965de5 Mon Sep 17 00:00:00 2001 From: alejandro antillon Date: Fri, 27 Jun 2025 18:57:57 +0900 Subject: [PATCH] Draft Idea For Pre Tasks To Be Context Aware This commit introduces, in a draft state, the idea to allow tasks that are called as a pre task for another task, to keep state of this pre task assignment via the attribute `_is_pre_of`. This attribute is later used when a `Call` object is created from a `Task` instance that has said attribute set. A `Call` created from a pre task, will add a reference to the task that defines the current task as pre task, and said reference will be available during the pre task's execution in the context passed to the task. The approach here suggested uses Pythonic patterns to avoid changes to the class implementation as much as possible, but rather compose the new functionality from invidivual, signle purpose small components, such as a generic decorator, and a descriptor component. It can be very well the case that making a task more stateful by tracking this pre assignment is not aligned with the design if this library, in which case it would be better not to add functionality that is not part of the long term view for the library. I would be happy to contribute to this greaat library in other ways. --- invoke/tasks.py | 29 +++++++++++++++++++++++++++++ tests/task.py | 27 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/invoke/tasks.py b/invoke/tasks.py index cd3075e9b..03888f19f 100644 --- a/invoke/tasks.py +++ b/invoke/tasks.py @@ -32,6 +32,14 @@ T = TypeVar("T", bound=Callable) +class PreTaskDesc: + def __get__(self, obj, _type): + return obj._pre + + def __set__(self, obj, value): + for p in value: + p._is_pre_of = obj + obj._pre = value class Task(Generic[T]): """ @@ -48,6 +56,7 @@ class Task(Generic[T]): .. versionadded:: 1.0 """ + pre = PreTaskDesc() # TODO: store these kwarg defaults central, refer to those values both here # and in @task. @@ -395,6 +404,11 @@ def __init__( Keyword arguments to call with, if any. Default: ``None``. """ self.task = task + if hasattr(task, "_is_pre_of"): + self.make_context = _copy_attrs_to_return_val( + task, + "_is_pre_of" + )(self.make_context) self.called_as = called_as self.args = args or tuple() self.kwargs = kwargs or dict() @@ -517,3 +531,18 @@ def clean_build(c): .. versionadded:: 1.0 """ return Call(task, args=args, kwargs=kwargs) + +def _copy_attrs_to_return_val(source, *attrs): + """ + Copy attributes from a source to the return value of the decorated func + """ + def _wrapper(func): + def _inner(*args, **kwargs): + target = func(*args, **kwargs) + for name in attrs: + value = getattr(source, name) + if value: + setattr(target, name, value) + return target + return _inner + return _wrapper diff --git a/tests/task.py b/tests/task.py index d60d91230..8bff9bce4 100644 --- a/tests/task.py +++ b/tests/task.py @@ -92,6 +92,33 @@ def func(c): assert func.pre == [whatever] + def task_and_pre_tasks_binding(self): + + @task + def pre_task(c): + pass + + @task(pre=[pre_task]) + def my_task(c): + pass + + assert all([hasattr(p, "_is_pre_of") for p in my_task.pre]) + assert pre_task._is_pre_of == my_task + + def create_call_and_context_form_pre_task_has_access_to_parent_task(self): + + @task + def pre_task(c): + pass + + @task(pre=[pre_task]) + def my_task(c): + pass + + call = Call(pre_task) + c = call.make_context(Config(defaults={})) + assert getattr(c, "_is_pre_of") == my_task + def allows_star_args_as_shortcut_for_pre(self): @task def pre1(c):