run
— Task for generic command execution
This module defines a few useful functions and classes to embed generic
command execution in a depgraph
.
Spawning external processes
The RunTask
class is the basic building block to run tasks that
consist in spawning external processes and waiting for their completion. It
makes it possible to execute arbitrary commands. Consider:
>>> from valjean.cosette.run import RunTask
>>> task = RunTask.from_cli(name='say', cli=['echo', 'ni!'])
>>> env_update, status = task.do(env=dict(), config=config) # prints 'ni!'
>>> print(status)
TaskStatus.DONE
The task succeeded, but where is the output of our command?! Note that
RunTask
captures standard output and standard error and redirects them
to files. If you want to see what was printed, you have to look there:
>>> def print_stdout(env_up, name):
... """A small function to print the stdout of a task."""
... stdout = env_up[name]['stdout']
... with open(stdout) as stdout_f:
... print(stdout_f.read(), end='')
>>> print_stdout(env_update, 'say')
ni!
Note that command is not parsed by a shell. So the following may not do what you expect:
>>> task = RunTask.from_cli(name='want',
... cli=['echo', 'We want... ', '&&',
... 'echo', 'a shrubbery!'])
>>> env_update, status = task.do(env=dict(), config=config)
>>> print_stdout(env_update, 'want')
We want... && echo a shrubbery!
Indeed, RunTask
ran echo only once. If you need to execute
several commands, you can wrap them in a shell script and execute it.
Alternatively, you can directly invoke the RunTask.from_clis
class
method:
>>> task = RunTask.from_clis(name='want',
... clis=[['echo', '-n', 'We want... '],
... ['echo', 'a shrubbery!']])
>>> env_update, status = task.do(env=dict(), config=config)
>>> print_stdout(env_update, 'want')
We want... a shrubbery!
Creating tasks using a factory
When you want to create multiple RunTask
objects using the same
executable, it can be convenient to use RunTaskFactory
. This class can
be parametrized to create tasks by specifying the path to the executable once
and for all, for instance, and providing the missing arguments later.
The simplest way to create a RunTaskFactory
is to use one of the
RunTaskFactory.from_executable
or RunTaskFactory.from_task
class methods. For example, this will create a factory instance that generates
RunTask
objects for the echo executable:
>>> factory = RunTaskFactory.from_executable('echo', name='echo')
You can use it to generate tasks by invoking the RunTaskFactory.make
method:
>>> task = factory.make(name='task', extra_args=['spam'])
This creates an task.echo object (of type RunTask
) that executes
echo spam
when run:
>>> env_up, status = task.do(env={}, config=config)
>>> print_stdout(env_up, 'task.echo')
spam
You can also leave the name parameter out. If you do so,
RunTaskFactory
will generate a name for you:
>>> task_sausage = factory.make(extra_args=['sausage'])
>>> task_sausage.name
'....echo'
Of course you can generate multiple tasks using the same factory (this is the
whole point of RunTaskFactory
, really):
>>> task_spam = factory.make(name='task_spam', extra_args=['spam'])
>>> task_eggs = factory.make(name='task_eggs', extra_args=['eggs'])
>>> task_bacon = factory.make(name='task_bacon', extra_args=['bacon'])
You can also specify a few arguments beforehand and provide the rest later:
>>> factory = RunTaskFactory.from_executable('echo', name='echo',
... default_args=['spam'])
>>> task = factory.make(name='task', extra_args=['eggs'])
>>> env_up, status = task.do(env={}, config=config)
>>> print_stdout(env_up, 'task.echo')
spam eggs
Finally, you can parametrize your arguments on arbitrary keywords that will be provided when the task is created:
>>> args = ['{food}', 'with', '{side}']
>>> factory = RunTaskFactory.from_executable('echo', name='echo',
... default_args=args)
>>> task = factory.make(name='task', food='lobster', side='spam')
>>> env_up, status = task.do(env={}, config=config)
>>> print_stdout(env_up, 'task.echo')
lobster with spam
Default values for the parameters may be specified when creating the factory and can be overridden when the task is created:
>>> args = ['{food}', 'with', '{side}']
>>> factory = RunTaskFactory.from_executable('echo', default_args=args,
... side='spam', name='echo')
>>> beans = factory.make(name='baked beans', food='baked beans')
>>> eggs = factory.make(name='eggs', food='eggs',
... side='bacon and spam')
>>> env_up, status = beans.do(env={}, config=config)
>>> print_stdout(env_up, 'baked beans.echo')
baked beans with spam
>>> env_up, status = eggs.do(env={}, config=config)
>>> print_stdout(env_up, 'eggs.echo')
eggs with bacon and spam
Note also that you can refer to the environment or the configuration in your command-line arguments:
>>> args = ['{env[side]}']
>>> factory = RunTaskFactory.from_executable('echo', name='echo',
... default_args=args)
>>> task = factory.make(name='task')
>>> env_up, status = task.do(env={'side': 'spam'}, config=config)
>>> print_stdout(env_up, 'task.echo')
spam
Caching
The RunTaskFactory
class caches generated tasks under the hood.
Repeated calls to RunTaskFactory.make
from the same factory with the
same arguments will result in the same task:
>>> factory = RunTaskFactory.from_executable('echo', name='echo')
>>> task_sausage = factory.make(extra_args=['sausage'])
>>> task_sausage_again = factory.make(extra_args=['sausage'])
>>> task_sausage is task_sausage_again
True
If you instantiate another factory, the caching mechanism is defeated:
>>> other_factory = RunTaskFactory.from_executable('echo', name='echo')
>>> task_sausage_other = other_factory.make(extra_args=['sausage'])
>>> task_sausage is task_sausage_other
False
Module API
- valjean.cosette.run.run(clis, stdout, stderr, **subprocess_args)[source]
Run the given command lines and capture their stdout/stderr.
Execution stops at the first failure (result value != 0).
- Parameters:
clis (list) – The list of commands to execute.
stdout (file object) – File handle to capture the stdout stream.
stderr (file object) – File handle to capture the stderr stream.
subprocess_args (dict) – Parameters to be passed to
subprocess.call
.
- valjean.cosette.run.make_cap_paths(base_path)[source]
Construct filenames to capture stdout and stderr.
- class valjean.cosette.run.RunTask(name, clis_closure, *, deps=None, soft_deps=None, **subprocess_args)[source]
Task that executes the specified shell commands and waits for their completion.
- classmethod from_cli(name, cli, **kwargs)[source]
Create a
RunTask
from a single command line.Use the
RunTask.from_clis
method if you want to run several commands in a row.- Parameters:
name (str) – The name of this task.
cli (list) – The command line to be executed, as a list of strings. The first element of the list is the command and the following ones are its arguments.
kwargs – Any other keyword arguments will be passed on to the constructor (see
RunTask.__init__
).
- classmethod from_clis(name, clis, **kwargs)[source]
Create a
RunTask
from a list of command lines.- Parameters:
name (str) – The name of this task.
clis (list) – The command lines to be executed, as a list of lists of strings. The first element of each sub-list is the command and the following ones are its arguments.
kwargs – Any other keyword arguments will be passed on to the constructor (see
RunTask.__init__
).
- __init__(name, clis_closure, *, deps=None, soft_deps=None, **subprocess_args)[source]
Initialize this task from a list of command lines.
The clis_closure argument must be a closure. It will be invoked at execution time as:
clis_closure(env, config)
and it must return the command lines to be executed, as a list of lists of strings.
- Parameters:
name (str) – The name of this task.
clis_closure (list) – A closure to generate the command lines.
subprocess_args (dict) – Any remaining options will be passed to the
subprocess.Popen
constructor.deps (list(Task) or None) – The dependencies for this task (see
Task.__init__
for the format), or None.soft_deps (list(Task) or None) – The dependencies for this task (see
Task.__init__
for the format), or None.
- run_task(clis_closure, name, **subprocess_args)[source]
Execute the specified command and wait for its completion.
On completion, this method proposes the following updates to the environment:
env[task.name]['clis'] = clis env[task.name]['return_codes'] = return_codes env[task.name]['elapsed_time'] = wallclock_time env[task.name]['stdout'] = stdout env[task.name]['stderr'] = stderr
Here
clis
is the list of command lines that were executed,return_codes
is the list of return codes of the executed commands, andelapsed_time
is the time the whole list took. The keysstdout
andstderr
hold the paths to the files containing respectively the captured standard output and standard error streams.
- class valjean.cosette.run.RunTaskFactory(make_closure, *, deps=None, soft_deps=None, name, **kwargs)[source]
Create multiple tasks from the same executable without even breaking a sweat.
- classmethod from_task(task, *, relative_path, name=None, default_args=None, **kwargs)[source]
This class method creates a
RunTaskFactory
from aTask
. The command to be executed must appear in the output_dir directory of the given task; its relative location must be specified with the relative_path argument.This method can be used, among other things, to create a
RunTaskFactory
from aCheckoutTask
or aBuildTask
.- Parameters:
relative_path (str) – The path to the executable, relative to the the output directory (output_dir) of the task.
name (str or None) – a unique identifier for this factory. The factory name is used to produce unique names for the generated tasks. If None is given, the factory name will be constructed by hashing the other arguments.
default_args – see
RunTaskFactory.from_executable
.kwargs – Any remaining arguments will be passed to the
__init__
.
- classmethod from_executable(path, name=None, default_args=None, **kwargs)[source]
This class method creates a
RunTaskFactory
from the path to an existing executable.- Parameters:
path (str) – the path to the executable.
name (str or None) – a unique identifier for this factory. If None is given, the factory will use a hash of the other arguments.
default_args (list(str) or None) – The list of arguments that will be passed to the executed command by
RunTask
. It may contain expressions understood by Python’s format mini-language, such as{foo}
. These expressions can be filled when the task is generated by passing appropriate keyword arguments to themake
method.kwargs – Any remaining arguments will be passed to the
__init__
.
- make(*, name=None, extra_args=None, subprocess_args=None, deps=None, soft_deps=None, **kwargs)[source]
Create a
RunTask
object.- Parameters:
name (str or None) – the name of the task to be generated, as a string. If None is passed,
RunTaskFactory
will generate a name by hashing the contents of all the other arguments.extra_args (list or None) – A list of additional arguments that will be appended at the end of the command line.
subprocess_args (dict or None) – A dictionary of arguments for the call to the
subprocess.Popen
constructor.deps (list(Task) or None) – A list of dependencies for the generated task. Note that factories built with
from_task
automatically inject dependencies on the given task.soft_deps (list(Task) or None) – A list of soft dependencies for the generated task.
kwargs – Any remaining keyword arguments will be used to format the command line before execution. The environment and the configuration are available at formatting time as
env
andconfig
, respectively.