PyInvoke is a Python task runner. pip install invoke. Task: from invoke import task. @task def test(c): c.run("pytest"). Run: invoke test. Context: c.run("cmd") — runs shell command. c.run("cmd", echo=True) — print command. c.run("cmd", warn=True) — don’t raise on non-zero exit. c.run("cmd", hide=True) — suppress output. c.run("cmd", pty=True) — allocate pseudo-terminal (color output). Result: result = c.run("cmd"). result.stdout, result.stderr, result.exited, result.ok. Dependencies: @task(pre=[clean]). @task(pre=[build], post=[notify]). @task(pre=[call(setup, env="prod")]) — pass args. Namespace: from invoke import task, Collection. ns = Collection("deploy"). ns.add_task(build). ns.add_task(push). Module: from invoke import Collection; ns = Collection.from_module(deploy_module). Help: @task(help={"env": "target environment (prod/staging)"}). Default task: @task(default=True). List tasks: invoke --list. Prompt: from invoke.watchers import Responder; responder = Responder(pattern=r"Password:", response="secret\n"). c.run("ssh-add", watchers=[responder]). Config: invoke.yaml or .invoke.yaml — set defaults. c.config.run.echo = True. Parallel: c.run("cmd &") or use concurrent.futures inside task. Claude Code generates invoke task files, pre/post dependency chains, and collection-based task modules.
CLAUDE.md for PyInvoke
## PyInvoke Stack
- Version: invoke >= 2.2 | pip install invoke
- Task: @task def name(c): c.run("shell command")
- Options: @task def fn(c, env="prod"): ... → invoke fn --env staging
- Deps: @task(pre=[clean, build]) — run clean, build before this task
- Namespace: Collection("group").add_task(fn) → invoke group.fn
- Result: result = c.run("cmd"); result.ok | result.stdout | result.exited
- Config: .invoke.yaml for project defaults | c.config.run.echo = True
PyInvoke Task Runner Pipeline
# tasks.py — PyInvoke task definitions for test, lint, build, and deploy
from __future__ import annotations
import os
import sys
from pathlib import Path
from invoke import Collection, task
from invoke.watchers import Responder
# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────
PROJECT_ROOT = Path(__file__).parent
SRC_DIR = PROJECT_ROOT / "src"
DIST_DIR = PROJECT_ROOT / "dist"
def _python(c) -> str:
"""Return the Python binary path — respects virtual environments."""
return sys.executable
# ─────────────────────────────────────────────────────────────────────────────
# 1. Setup and environment
# ─────────────────────────────────────────────────────────────────────────────
@task(
help={"extras": "comma-separated pip extras (e.g. dev,test)"},
default=True,
)
def install(c, extras="dev"):
"""Install project dependencies."""
extras_flag = f"[{extras}]" if extras else ""
c.run(f"{_python(c)} -m pip install -e '.{extras_flag}'", echo=True)
@task
def clean(c):
"""Remove build artefacts and cache directories."""
dirs_to_clean = ["dist", "build", ".mypy_cache", ".pytest_cache", "htmlcov"]
for d in dirs_to_clean:
c.run(f"rm -rf {d}", warn=True, hide=True)
c.run("find . -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null", warn=True, hide=True)
c.run("find . -name '*.pyc' -delete 2>/dev/null", warn=True, hide=True)
print("Clean: done")
# ─────────────────────────────────────────────────────────────────────────────
# 2. Code quality
# ─────────────────────────────────────────────────────────────────────────────
@task(
help={
"fix": "auto-fix issues where possible (default: False)",
"path": "path to lint (default: src/)",
}
)
def lint(c, fix=False, path="src/"):
"""Run ruff linter. Pass --fix to auto-correct."""
fix_flag = "--fix" if fix else ""
c.run(f"ruff check {fix_flag} {path}", echo=True)
@task(help={"path": "path to format (default: .)"})
def fmt(c, path="."):
"""Format code with ruff formatter."""
c.run(f"ruff format {path}", echo=True)
@task(help={"strict": "enable strict mode (default: False)"})
def typecheck(c, strict=False):
"""Run mypy type checker."""
strict_flag = "--strict" if strict else ""
c.run(f"mypy {strict_flag} {SRC_DIR}", echo=True)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Testing
# ─────────────────────────────────────────────────────────────────────────────
@task(
help={
"cov": "enable coverage (default: True)",
"verbose": "verbose output (default: False)",
"k": "pytest -k expression to filter tests",
"fast": "skip slow tests (default: False)",
}
)
def test(c, cov=True, verbose=False, k="", fast=False):
"""Run pytest test suite with optional coverage."""
flags: list[str] = []
if cov:
flags += ["--cov=src", "--cov-report=term-missing", "--cov-report=html"]
if verbose:
flags.append("-v")
if k:
flags.append(f"-k '{k}'")
if fast:
flags.append("-m 'not slow'")
flags_str = " ".join(flags)
c.run(f"{_python(c)} -m pytest {flags_str}", echo=True, pty=True)
@task(
help={"module": "module to benchmark (default: all)"},
)
def bench(c, module=""):
"""Run pytest-benchmark benchmarks."""
pattern = f"tests/test_{module}.py" if module else "tests/"
c.run(
f"{_python(c)} -m pytest {pattern} --benchmark-only --benchmark-sort=mean",
echo=True,
pty=True,
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Build
# ─────────────────────────────────────────────────────────────────────────────
@task(pre=[clean])
def build(c):
"""Build source and wheel distributions."""
c.run(f"{_python(c)} -m build", echo=True)
DIST_DIR.mkdir(exist_ok=True)
result = c.run("ls -lh dist/", hide=True)
print(result.stdout.strip())
@task(pre=[lint, typecheck, test, build])
def release_check(c):
"""Run full quality gate before release: lint → typecheck → test → build."""
print("Release check passed.")
# ─────────────────────────────────────────────────────────────────────────────
# 5. Docker
# ─────────────────────────────────────────────────────────────────────────────
@task(
help={
"tag": "image tag (default: latest)",
"push": "push to registry after build (default: False)",
}
)
def docker_build(c, tag="latest", push=False):
"""Build Docker image."""
image = f"myapp:{tag}"
c.run(f"docker build -t {image} .", echo=True)
if push:
c.run(f"docker push {image}", echo=True)
@task(help={"env": "environment: dev or prod (default: dev)"})
def docker_up(c, env="dev"):
"""Start Docker Compose services."""
file_flag = f"-f docker-compose.{env}.yml" if env != "dev" else ""
c.run(f"docker compose {file_flag} up -d", echo=True, pty=True)
@task
def docker_down(c):
"""Stop Docker Compose services."""
c.run("docker compose down", echo=True)
# ─────────────────────────────────────────────────────────────────────────────
# 6. Deployment
# ─────────────────────────────────────────────────────────────────────────────
@task(
help={
"env": "target: staging or production (default: staging)",
"branch": "git branch to deploy (default: HEAD)",
}
)
def deploy(c, env="staging", branch="HEAD"):
"""Deploy application to target environment."""
if env == "production":
# Confirm before production deploy
answer = input("Deploy to PRODUCTION? [y/N] ").strip().lower()
if answer != "y":
print("Aborted.")
return
sha = c.run(f"git rev-parse --short {branch}", hide=True).stdout.strip()
print(f"Deploying {sha} to {env}…")
steps = [
f"./scripts/deploy.sh {env} {sha}",
f"./scripts/smoke_test.sh {env}",
]
for step in steps:
result = c.run(step, echo=True, warn=True)
if not result.ok:
raise SystemExit(f"Deploy step failed: {step}")
print(f"Deployed {sha} to {env} successfully.")
# ─────────────────────────────────────────────────────────────────────────────
# 7. Database
# ─────────────────────────────────────────────────────────────────────────────
@task(help={"env": "environment (default: development)"})
def migrate(c, env="development"):
"""Run database migrations."""
c.run(f"APP_ENV={env} alembic upgrade head", echo=True)
@task
def db_shell(c):
"""Open an interactive psql session (allocates a PTY for the REPL)."""
db_url = os.environ.get("DATABASE_URL", "postgresql://localhost/myapp_dev")
c.run(f"psql {db_url}", pty=True) # pty=True needed for interactive apps
# ─────────────────────────────────────────────────────────────────────────────
# 8. Watchers — auto-respond to interactive prompts
# ─────────────────────────────────────────────────────────────────────────────
@task
def add_ssh_key(c):
"""Add SSH key to agent — auto-responds to passphrase prompt."""
passphrase = os.environ.get("SSH_PASSPHRASE", "")
responder = Responder(
pattern=r"Enter passphrase",
response=f"{passphrase}\n",
)
c.run("ssh-add ~/.ssh/id_ed25519", watchers=[responder], hide=True)
print("SSH key added.")
# ─────────────────────────────────────────────────────────────────────────────
# 9. Collection — organize tasks into namespaces
# ─────────────────────────────────────────────────────────────────────────────
# Top-level namespace groups
ci_ns = Collection("ci")
ci_ns.add_task(lint)
ci_ns.add_task(typecheck)
ci_ns.add_task(test)
ci_ns.add_task(bench)
docker_ns = Collection("docker")
docker_ns.add_task(docker_build, name="build")
docker_ns.add_task(docker_up, name="up")
docker_ns.add_task(docker_down, name="down")
db_ns = Collection("db")
db_ns.add_task(migrate)
db_ns.add_task(db_shell, name="shell")
# Root namespace
ns = Collection()
ns.add_task(install)
ns.add_task(clean)
ns.add_task(fmt)
ns.add_task(build)
ns.add_task(release_check)
ns.add_task(deploy)
ns.add_task(add_ssh_key, name="ssh-key")
ns.add_collection(ci_ns)
ns.add_collection(docker_ns)
ns.add_collection(db_ns)
For the Makefile alternative — a Makefile runs shell commands directly without Python’s import system, cannot easily pass typed arguments (everything is a string), requires GNU Make to be installed, mixes shell syntax with build rules in a single non-Python file, and calls Python scripts as subprocesses rather than importing them, while PyInvoke tasks are plain Python functions that can import your application code directly, receive typed CLI arguments with invoke test --k auth, show rich --help including argument descriptions, and compose with pre=[clean, build] dependency chains without shell variable escaping. For the nox / tox alternative — nox and tox are optimized for running test suites in isolated virtual environments with matrix-parametrized Python versions, while PyInvoke is a general task runner for the full dev lifecycle: linting, building, Docker, deployment, database migrations, and interactive prompts via Responder watchers — use nox for multi-Python CI test matrices and invoke for everything else in your project workflow. The Claude Skills 360 bundle includes PyInvoke skill sets covering @task with help strings and typed CLI options, c.run echo/warn/hide/pty flags, result.ok and result.stdout assertion, pre/post dependency chains, Collection namespace grouping, add_collection for modular task libraries, Responder watchers for interactive prompt automation, pty=True for REPL and colored output, invoke —list discovery, and compose tasks into a full lint-test-build-deploy pipeline. Start with the free tier to try Python task runner code generation.