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:

  1. Add a dev dependency on poethepoet-tasks

    with poetry
    poetry add poethepoet-tasks -G dev
    
    with uv
    uv add --dev poethepoet-tasks
    
  2. 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.

src/my_tasks/__init__.py
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:

pyproject.toml
[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.

tasks.py
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:

pyproject.toml
[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).