Loading tasks from a python package¶
Usually tasks are declared in a config file such as pyproject.toml, however you can also define tasks in python, and include them into your project config via the include_script
global option.
This is primarily intended to make it easier to reuse tasks across projects – by distributing them as a python package. However you can also use it to define tasks with a local script file.
The include_script
global option¶
The following config loads the tasks generated by running the get_tasks
function from the mypkg
package. The package must be accessible to the default executor, i.e. it must be within the current project or its dependencies.
[tool.poe]
include_script = "mypkg:get_tasks"
Just like for the include global option, you can also provide a multiple items like so:
[tool.poe]
include_script = ["mypkg:get_tasks", "something.else:more_tasks"]
Passing arguments to the script¶
The syntax for the function reference is identical to a script task, meaning that it is also possible to pass arguments to the function like so:
[tool.poe]
include_script = "mypkg:get_tasks(task_prefix='foo-', exclude_tasks='docs')"
Specifying a different executor¶
Normally Poe the Poet detects the most appropriate executor (i.e. project virtualenv) to use to find and execute the referenced package. However you can customize this behavior by specify an executor for the script like so:
[[tool.poe.include_script]]
script = "mypkg:get_tasks"
executor = { type = "virtualenv", location = "../parent.venv" }
Setting a working directory for included config¶
You can use the cwd
option on an include_script to make the included tasks run with a different working directory. This option behaves exactly like the similar option for include items.
[tool.poe]
include_script = [
{ script = "mypkg:get_tasks", cwd = "./subproject1" }
]
Out-of-the-box tasks from poethepoet-tasks¶
With include_scripts
you can reuse common tasks in each new project by depending directly on a private or public tasks package. As an example the poethepoet-tasks package provides an opinionated collection of tasks to get you started, with two simple steps:
Add a dev dependency on
poethepoet-tasks
with poetry¶poetry add poethepoet-tasks -G dev
with uv¶uv add --dev poethepoet-tasks
Configure your project to include tasks from
poethepoet-tasks
pyproject.toml¶[tool.poe] include_script = "poethepoet_tasks:tasks"
See the docs for poethepoet-tasks for details of how you can include just a specific subset of available tasks.
Tip
When you add a dev dependency on a tasks package like poethepoet-tasks you also automatically get all the other dev dependencies that it requires, e.g. ruff, pytest, etc. poethepoet-tasks also comes with config for ruff with opinionated defaults.
Create your own tasks package¶
To create your own tasks package, all you need is a python function that returns the required config structure (as a dict or json string), that can be imported from within your project, either from within the project itself or from a dependency.
Generating config directly¶
The following example illustrates defining a function that can be referenced by include_script
:
def generate_tasks():
return {
"tasks": {
"test": "pytest",
"build-proto": {
"cmd": """
protoc --proto_path=schemas --python_out=src/generated schemas/messages.proto
""",
"help": "Generate protobuf classes"
},
"build": {
"sequence": [
"build-proto",
"test",
{ "cmd": "poetry build" },
],
"help": "Build that code"
}
}
}
You may recognise the structure as identical to that supported when loading tasks from another file, which is a subset of the schema supported in the main project configuration, including tasks
and the env
and envfile
global options.
There is no restriction on the arguments accepted by the function, or the logic used to generate the tasks. However it is critical that the function does not write anything to stdout.
Warning
It is strongly recommended to minimize the code that must be imported or executed to generate the tasks configuration. This is because the poe CLI must create a python subprocess to import and execute the function every time it is used within the project; even when generating shell completions. This is also a good reason to ensure there are no side effects of this code or any of its imports.
Defining a TaskCollection¶
The recommended way to write tasks in python is to leverage the TaskCollection abstraction from poethepoet-tasks. It provides a convenient API for managing tasks, and allows users to select just the tasks they want via tags.
from poethepoet_tasks import TaskCollection
tasks = TaskCollection()
# Define a simple cmd task
tasks.add(
task_name="test",
task_config={
"help": "Run project tests",
"cmd": "pytest tests",
},
tags=["pytest"], # tags are optional and allow consumers to select only the desired tasks
)
The TaskCollection instance is callable such that it can be referenced directly like so:
[tool.poe]
include_script = "my_tasks:tasks"
See here for a real world example of creating a TaskCollection.
Decorating functions as script tasks¶
A TaskCollection can also be used to define inline script tasks. One nice pattern this enables is to have a tasks.py in the root of your project, similar to how one would work with invoke.
from poethepoet_tasks import TaskCollection
tasks = TaskCollection()
# Define an inline script task
@tasks.script(tags=["example"])
def hello(
foo: str | None = None,
*, # This means preceding args will be positional, instead of CLI options
bar: int = 1,
baz: bool = True,
):
"""
The first paragraph of the function docstring is picked up as the task help message!
Args:
foo: a positional argument for specifying foo
bar: a number of things
baz: to baz or not to baz
"""
pass
The above example is equivalent to the following toml based configuration:
[tool.poe.tasks.hello]
script = "tasks:hello(foo, bar=bar, baz=baz)"
help = "The first paragraph of the function docstring is picked up as the task help message!"
args = [
{ name = "foo", positional = true, help = "a positional argument for specifying foo" },
{ name = "bar", help = "a number of things" },
{ name = "baz", help = "to baz or not to baz" }
]
Tip
When creating an inline script task via the TaskCollection decorator most of task options are automatically infered from the code, including task name, args, and help text (given a sufficient docstring).