pyinvoke / invoke

Pythonic task management & command execution.
http://pyinvoke.org
BSD 2-Clause "Simplified" License
4.39k stars 367 forks source link

Import magic for nested tasks.py #579

Open haydenflinner opened 6 years ago

haydenflinner commented 6 years ago

I'd like to be able to write a tasks.py in one folder, nest a module there, and then be able to run the inner commands right away, no Collection-additions required. Ex:

.
├── innermodule
│   ├── __init__.py
│   └── tasks.py
├── README
├── tasks.py

invoke --list

 main-level-task-1
 main-level-task-2
 innermodule

Where all I did in tasks.py was define a couple tasks and import innermodule. Does this sound too magic to be default behavior for this library? If not, how could I go about adding it?

haydenflinner commented 6 years ago

Found something close enough, guess I didn't go through the docs thoroughly enough -- http://docs.pyinvoke.org/en/1.2/concepts/namespaces.html#importing-modules-as-collections

haydenflinner commented 6 years ago

Actually, I was wrong, doesn't work like I thought it did.

What I'd like to do is import things but put them at the root level of the namespace, without explicitly naming which tasks I want to load, just whatever's in the module.

So in this case the issue is that I have a tasks.py, and I want to import a module that defines more tasks, and have them in the global namespace under the name of their module just as the doc shows how to do.

But I don't want to explicitly add all of the tasks in my current file after I do that (because I didn't have to do that before). Adding all of them explicitly seems to only be possibly by overriding the task decorator to add to namespace, or keeping a list of tasks, neither of which seem good. And I can't seem to add tasks from another file to the global namespace without again, either keeping a list or overriding the task decorator. Any suggestions besides don't do that? :smile:

haydenflinner commented 6 years ago

Along the lines of other magic, I wanted to be able to call another task from within a task and have it pull any named parameters that I didn't specify from the current runtime context (like how it does when you invoke it from cmdline). So here's that, just set it to klass on your task. Some stolen from runners._run_opts, some stolen from tasks.py.Task.call.

from invoke import task as _task
from invoke.tasks import Task, Context
from functools import wraps, partial
import inspect
import itertools

class get_params_from_config(Task):
    def __call__(self, *args, **kwargs):
        # This check is duplicated here because we assume context is first arg.
        # Guard against calling tasks with no context.
        if not isinstance(args[0], Context):
            err = "Task expected a Context as its first arg, got {} instead!"
            # TODO: raise a custom subclass _of_ TypeError instead
            raise TypeError(err.format(type(args[0])))

        ctx, args = args[0], args[1:]
        expecting = self.argspec(self.body)[0]
        name_to_arg = {
            name: arg for name, arg in zip(expecting, args)
        }
        args_passing = {}
        for param_name in expecting:
            passing = name_to_arg.get(param_name, None)
            passing = passing or kwargs.pop(param_name, None)
            passing = passing or ctx.config.get(self.name, {}).get(param_name, None)
            if passing == None:
                print("Warning! Couldn't find parameter {} for {}!".format(param_name, self.name))
            args_passing[param_name] = passing

        # Handle invalid kwarg keys (anything left in kwargs).
        # Act like a normal function would, i.e. TypeError
        if args_passing and kwargs:
            err = "run() got an unexpected keyword argument '{}'"
            raise TypeError(err.format(list(kwargs.keys())[0]))

        result = self.body(ctx, **args_passing)
        self.times_called += 1
        return result

task = partial(_task, klass=get_params_from_config)
haydenflinner commented 6 years ago

Update: #527 solves what I wanted to do perfectly and without magic

haydenflinner commented 6 years ago

Until #527 gets merged, there's also this to be pasted at the bottom of your file from the invoke rituals library:

# Register local tasks in root namespace
from invoke import Task
for _task in globals().values():
    if isinstance(_task, Task) and _task.body.__module__ == __name__:
        namespace.add_task(_task)