Composing task collections¶
Invoke's Collection groups tasks, aliases, defaults and configuration. Toolkit adds package discovery and local_tasks.py loading. Installed entry-point collections are a separate plugin mechanism.
Built-in collections¶
Enable Toolkit's built-in collections with -x and list their tasks with -l:
uv run intk -xl
These include config.show, config.get, config.set and config.unset;
create.add-shebang, create.package and create.script; plugin lifecycle tasks;
and shell completion installers. Use configuration,
package generation, plugins and
completion for their arguments and boundaries.
Explicit composition¶
For a small project, explicit imports and add_collection() are the most predictable choice. Save this complete example as tasks.py after installation:
from invoke_toolkit import Collection, Context, task
@task
def report(ctx: Context) -> None:
"""Print a non-destructive build report."""
print("Build configuration is ready")
@task(aliases=["check"])
def verify(ctx: Context) -> None:
"""Report that the task was reached."""
print("Verification task selected")
build = Collection("build", report, verify)
ns = Collection()
ns.add_collection(build)
intk --list
intk build.report
intk build.check
Task-name normalization, aliases, default tasks and pre/post tasks are inherited from Invoke collections. Toolkit warns and removes aliases that normalize to their own task name, rather than creating a recursive alias (#116).
Package discovery: flat or nested¶
Create a directory automation beside tasks.py. Its __init__.py can contain just a module docstring:
"""Project automation modules."""
Save automation/build.py:
from invoke_toolkit import Context, task
@task
def report(ctx: Context) -> None:
"""Describe the build module."""
print("Build module")
Save automation/checks.py:
from invoke_toolkit import Context, task
@task
def verify(ctx: Context) -> None:
"""Describe the checks module."""
print("Checks module")
For a flat namespace, replace tasks.py with:
from invoke_toolkit import Collection
ops = Collection("ops")
ops.add_flat_tasks_from_namespace("automation")
ns = Collection(ops)
intk ops.report
intk ops.verify
For module-qualified names, replace tasks.py with:
from invoke_toolkit import Collection
ops = Collection("ops")
ops.add_collections_from_namespace("automation")
ns = Collection(ops)
intk ops.build.report
intk ops.checks.verify
The package must be importable in the executing Python environment. These helpers import discoverable submodules, not task definitions in the package's own __init__.py. Imports execute top-level code: discovery is not a sandbox. Keep modules import-safe, and use explicit composition when you need tight control over what loads.
Flattening removes a naming boundary. Give tasks unique names across flattened modules; Toolkit does not provide a conflict-resolution contract for duplicate task names there. Nested composition keeps module names distinct.
Origin: package composition #8, flat discovery #31.
Module configuration with from_package¶
Collection.from_package(package_path, into) adds cleaned module subcollections into an existing collection and returns it. Unlike the two discovery helpers above, it collects module-level config dictionaries into root configuration keyed by the module name. It filters underscore-prefixed tasks and tasks imported from other modules. A module-level config_schema attribute referring to a ConfigSchema subclass is registered for validation; schema registration alone does not install schema defaults.
For the package above, a complete alternative tasks.py is:
from invoke_toolkit import Collection
ns = Collection.from_package("automation", into=Collection())
intk build.report
intk checks.verify
Use configuration schemas for structured values; choose configure(schema=..., mapping=...) when you want schema defaults applied explicitly. Origin: #90.
Local developer tasks¶
Toolkit automatically looks for local_tasks.py alongside the primary task collection and exposes its public tasks under local. Keep project-wide automation in tasks.py and machine-specific convenience tasks in a gitignored local_tasks.py.
Save local_tasks.py next to one of the examples above:
from pathlib import Path
from invoke_toolkit import Context, task
@task
def workspace(ctx: Context) -> None:
"""Print this developer's current directory."""
print(Path.cwd())
intk local.workspace
Rules and boundaries:
- If the main collection has a task or alias named
local, Toolkit warns and skipslocal_tasks.py; the main task/alias wins. - Existing subcollections use Invoke's collection merge behavior. Do not rely on a pre-existing
localsubcollection being preserved when local-file discovery adds one. - Local files in different directories use path-derived private module identities, so they do not share a single Python
local_tasksmodule. - A standalone local file can be used when no primary task file or installed plugin collection is found. When plugins alone supply the fallback collection, automatic local-file loading is not additionally performed.
load_local_tasks(search_path=None)is available for explicit library use; its default is the current directory.load_directory(directory)only adds an import search path—it does not itself discover tasks. Theload_plugins()method is currently a no-op; use namespace helpers or installed entry points instead.
Origins: #21, module identity repair #123, main namespace precedence #133.
Continue with plugin packaging or static API reference.