-
Notifications
You must be signed in to change notification settings - Fork 370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Easy execution of tasks from within Python itself #170
Comments
Not exactly sure if this is 100% related, but - I have a handful of task collections bundled by functionality. I prefer not to do It would be nice to have a way of doing this this without the hackery. |
@sophacles Is there a reason the namespacing features don't cut it for that use case? The entire point of that API is to make it pretty easy to throw around Python modules/Collection objects, import them from wherever, etc; then make them into a task tree. There's more exploration to do in that space yet (eg selecting a specific subtree at runtime - which would also help your use case if your problem is you don't want to expose the whole tree at once) but it works reasonably well already. |
Really it comes down to environment building. The exact gist i linked is slightly truncated. I have like 10 utilities built out of invoke, each of which has something like 5-20 targets (some overlapping at the deepest layers of the tree, but each utility has a specific cognitive task focus -- build, test, deploy generic, deploy specific environments, debug tools, demo setup, and so on). I could probably train myself always to do:
But I kind of like having them:
Also noteworthy, several of my targets are most commonly called from places in the system that are not the directory where invoke lives. For example it's nice having the build scripts called from the cwd when I'm debugging. Another example is the deploy scripts callable from wherever I happen to be in the fs when I figure out my error. (In the later case, I'm often developing my tasks in an editor on my mac, with the system I'm testing deploys on in a virtualbox that has the source tree mounted, or on a remote vm with the source tree mounted in both places via NFS). |
Somehow I neglected to click your gist link before. Durr. I see what you're doing there. I think that's closely related to but not 100% the same as this ticket, insofar as I was intending this to be "I have some stuff at the Python level and want to call a task as a function or similar" and yours is "I want to hijack or duplicate the normal parser -> load collection -> execute task(s), process". Definitely agree that your use case should be supported (making the CLI guts reusable has been a huge weak spot in Fab 1 and I want to avoid that mistake here). Reckon we could make a spinoff ticket, link your gist, and then I can shit out some thoughts based on existing API and what needed changes might be. For now I'll just make sure it's a separate use case up top. |
OK - made a ticket for the funcitonality I was describing at #173 |
As for the rest of this ticket - I know invoke is intended to be part of fab2, so perhaps (unless they exist and I missed them) and explanation of how it's intended for fab2 to utilize invoke would be a good starting point. That way there are practical considerations and actual use cases to generalize from. It certainly helps avoid "architecture astronauting". It also could help avoiding some bad cases of fab2 relying on invoke relying on fab2. |
Yes, one of the reasons I have been poking Invoke lately is I'm attempting to get an actual Fab 2 ready for people to hammer on. And as you implied, every time I need to take one step forwards with Fab 2 work, it results in me needing to come back here and add or change stuff :) so it's a very good way to force this sort of reusability! |
Been having another discussion with @sophacles on IRC about this (or at least, strongly related things). His core use case in that discussion is (sanitized a tiny bit):
Originally, I was assuming the best approach here is the "pure" approach: if task A calls task B, task B gets a context 100% identical to if it had been called as a top level task. If task A wants to change B's behavior it should just use regular function arguments. Erich's use case seems to be that he wants to call task B with a "hybrid" context containing elements from task A's namespace, so that the A namespace can change some of those default settings affecting B's behavior. I.e. say the B module defines I'm not entirely for or against this at the moment but it is not what I'd originally assumed to be the sensible default. |
I deleted my previous two comments as I was still not explaining it sufficiently for my brain to shut up about it. I think this actually explains it correctly: First off, I view the context as similar to the OS's environment[1]. In an OS, it is up to the spawning process (the caller) to set up the environment in which the child process (callee) runs. In the OS, if the callee manipulates the environment, and in turn spawns another child (callee_1), then callee_1 is beholden to whatever manipulations callee set up, even if they differ from caller's settings. Each child process should do something like this:
By analogy, the context resulting from a task doing manipulation, is the context a called task (a callee) should hold. It will need to do (or it's logical equivalent):
Second, is the role of Collection in all this. There is all the stuff about collections building containers, including setting container defaults. Using the OS analogy, I view it as similar to creating a wrapper script[2]:
When called from the command line, None of this is of course the "controversial" stuff... wrappers in wrappers, or collections in collections, are the same, and everyone agrees that is good :). So, going on to the thing discussed in IRC, and outlined quite well by @bitprophet above, I need to set up a couple definitions:
In code that is:
In the OS/shell analogy, let's say we have
If I just call Now say I choose to build another tool, taking advantage of
If i don't define
and make use of the tools and defaults I've already built. This second N.B - just because my examples above are in shell, they are really operating system semantics Finally: there is an argument that stuff above should just be The tl;dr of this, is that:
should act like:
[1] I am deriving this influence from the fact that invoke is fundamentally a way to write better shell scripts. Better being subjective of course, but the goal is to use a real programming language to handle all the stuff that is icky in shell - such as building parameter lists, path manipulation, having command arguments, and making decisions (if, loop, et al) - before actually spawning some other command. [2] or using run-parts, or using functions or sourcing other scripts, but they work out very similarly |
In a similar vein, using terminology from above:
|
Some semi disjointed replies after rereading most of the above:
|
NOT having read the long thread, I'm still throwing #223 in the ring. 😄 |
Just in case somebody needs a workaround to execute tasks from another tasks right now, I implemented a really simple way to do so in my tasks by passing a root namespace and a helper function into from invoke import ctask, Collection
from invoke.executor import Executor
# Define tasks
# ============
@ctask
def test_task1(context, arg1):
print("TASK1")
@ctask
def test_task2(context):
print("TASK2 BEGIN")
context.invoke_execute(
context,
'test_task1',
arg1='value1'
)
print("TASK2 END")
# Define and configure root namespace
# ===================================
# NOTE: `namespace` or `ns` name is required!
namespace = Collection(
test_task,
# put tasks or nested collections here
)
def invoke_execute(context, command_name, **kwargs):
"""
Helper function to make invoke-tasks execution easier.
"""
results = Executor(namespace, config=context.config).execute((command_name, kwargs))
target_task = context.root_namespace[command_name]
return results[target_task]
namespace.configure({
'root_namespace': namespace,
'invoke_execute': invoke_execute,
}) |
Sorry for bothering. This issue is 3 years old, I was wondering if we've reached better ways now to call tasks from other tasks. Thanks! |
No, but it remains at or near the top of the priority list; it's definitely something that needs solving before 1.0. |
Any updates on this ? |
@bitprophet I see that 1.0 was released :) |
With the release of 1.0, is the ability to call tasks from within tasks (similar to v1's EDIT: If the answer to the original question is yes, please also advise if the current task context object can be passed through to other called tasks. |
Re: @muhammedabad's 2nd question, see #261 - it needs work still! Re: this ticket and 1.0: hey, running these projects isn't a cakewalk! ;) I judged it was better to get this & related projects above-board and on semver, than to continually punt until it was perfect. My gut says we can definitely implement this in a backwards compatible manner, since it's likely to end up implemented as a new method on |
Any updates? I've written a set of configuration management tools around Fabric (and Apache Libcloud). But can't upgrade it to be Python 2.7-3+ compatible until I upgrade Fabric. But that requires Roadmap? |
I'm in the same boat. I need execute to migrate from python 2.7 to 3+ |
To get this to work with fabric, and honor the executed tasks host list, I had to from invoke import Collection
from fabric import task
from fabric.executor import Executor
from fabric.main import program
# Define tasks
# ============
@task(hosts=["localhost"])
def test_task1(context, arg1):
print("TASK1", context)
@task
def test_task2(context):
print("TASK2 BEGIN", context)
context.invoke_execute(
context,
'test_task1',
arg1='value1'
)
print("TASK2 END")
# Define and configure root namespace
# ===================================
# NOTE: `namespace` or `ns` name is required!
namespace = Collection(
test_task1,
test_task2,
# put tasks or nested collections here
)
def invoke_execute(context, command_name, **kwargs):
"""
Helper function to make invoke-tasks execution easier.
"""
results = Executor(namespace, config=context.config, core=program.core).execute((command_name, kwargs))
target_task = context.root_namespace[command_name]
return results[target_task]
namespace.configure({
'root_namespace': namespace,
'invoke_execute': invoke_execute,
}) $ fab test-task2
('TASK2 BEGIN', <Context: <Config: {'root_namespace': <Collection None: test-task1, test-task2>, 'tasks': {'search_root': None, 'collection_name': 'fabfile', 'dedupe': True, 'auto_dash_names': True}, 'run': {'shell': '/bin/bash', 'hide': None, 'pty': False, 'encoding': None, 'in_stream': None, 'replace_env': True, 'echo': False, 'warn': False, 'echo_stdin': None, 'watchers': [], 'env': {}, 'out_stream': None, 'err_stream': None, 'fallback': True}, 'timeouts': {'connect': None}, 'sudo': {'password': None, 'prompt': '[sudo] password: ', 'user': None}, 'inline_ssh_env': False, 'port': 22, 'load_ssh_configs': True, 'user': 'cureatr', 'ssh_config_path': None, 'invoke_execute': <function invoke_execute at 0x7f1993b41f50>, 'connect_kwargs': {}, 'forward_agent': False, 'gateway': None, 'runners': {'remote': <class 'fabric.runners.Remote'>, 'local': <class 'invoke.runners.Local'>}}>>)
('TASK1', <Connection host=localhost>)
TASK2 END |
…o fabric1 `execute` Fixes pyinvoke#170
…o fabric1 `execute` Fixes pyinvoke#170
Any update on this? |
I've switched to the Python 3 compatible fab-classic in the meantime. |
Another year has gone by… any update? |
Also greatly missing this feature. I have some subtasks that I would like to either call directly from the command line or from other tasks. |
Would this feature also help with the following scenario I'm after?
So the question is whether invoke can be made to repeatedly re-launch a task. |
@jcw - I think you want https://github.com/gorakhargosh/watchdog |
Another year has gone by… any update? |
Until now the focus has been mostly on CLI execution, but calling programmatically has always been on the table - couldn't find a good ticket so this must be it :) (#112 is sort of a feature request for this but it's more of a support thread, I'll roll it into here instead.)
Currently there are no great options for calling a task from another task:
Executor(<Collection obj>).execute('name')
, which requires one to have access to the rightCollection
instance.Collection
is the one the task is itself a part of, this is basically impossible with the current API.that_collection['taskname'](args)
, however:Rambling thoughts about how to smooth over the above problems:
Collection
when executing is still a must at some point - Fabric'sexecute('name')
can't work here because it relies on implicit global state.Context
handed in (or some other argument, but a single forced argument is plenty...).Context
andCollection
might help here.Collection
orContext
.Context
- which is doable). You can always just treat everything like a regular module-of-functions and call other nearby task functions straight up.Brainstorm of use cases/scenarios:
Call a top level task from a subtask(this doesn't actually make any sense, by definition a subtask has no idea that it IS a subtask, at best it can be expected to know about its siblings)The text was updated successfully, but these errors were encountered: