Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ jobs:
platforms: [linux/amd64]
steps:
- name: Checkout Git repository
uses: actions/checkout@v6
uses: actions/checkout@v6.0.3
with:
fetch-depth: 0

- name: Install uv and set the Python version
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0

- name: Install dependencies
shell: bash
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:
run: uv build

- name: Upload package to artifact registry
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: uvtask-${{ env.VERSION }}
path: dist/
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ jobs:
platforms: [linux/amd64, linux/arm64]
steps:
- name: Checkout Git repository
uses: actions/checkout@v6
uses: actions/checkout@v6.0.3
with:
fetch-depth: 0

- name: Install uv and set the Python version
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0

- name: Install dependencies
shell: bash
Expand All @@ -50,9 +50,14 @@ jobs:
run: uvx uvtask security-analysis:licenses
if: ${{ env.PIPELINE_TESTS == 'true' }}

- name: security-analysis-vulnerabilities
- name: security-analysis-vulnerabilities-code
shell: bash
run: uvx uvtask security-analysis:vulnerabilities
run: uvx uvtask security-analysis:vulnerabilities:code
if: ${{ env.PIPELINE_TESTS == 'true' }}

- name: security-analysis-vulnerabilities-pkgs
shell: bash
run: uvx uvtask security-analysis:vulnerabilities:pkgs
if: ${{ env.PIPELINE_TESTS == 'true' }}

- name: static-analysis-linter
Expand Down
169 changes: 139 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,174 @@
[![Actions status](https://github.com/aiopy/python-uvtask/actions/workflows/ci.yml/badge.svg)](https://github.com/aiopy/python-uvtask/actions)
[![PyPIDownloads](https://static.pepy.tech/badge/uvtask)](https://pepy.tech/project/uvtask)

An extremely fast Python task runner.
A task runner for `pyproject.toml` scripts, optimized for the uv workflow.

> **Note:** This is an **independent, third-party project**, not an official Astral tool. It is highly inspired by and designed to work seamlessly with Astral's excellent tools (such as `uv`/`uvx`, `ruff`, and `ty`). We're grateful for the amazing work the Astral team does for the Python ecosystem!
Define commands once in TOML, run them with `uvx uvtask` from any machine — zero runtime dependencies in your project.

## Highlights
## Why uvtask

- ⚡ **Extremely fast** - Built for speed with zero installation overhead
- 📝 **Simple configuration** - Define scripts in `pyproject.toml`
- 🔗 **Pre/post hooks** - Automatically run hooks before and after commands
- 🎨 **Consistent UX** - Maintains visual and stylistic continuity with `uv`'s design language
- **Run anywhere with `uvx`** — no install required; zero runtime dependencies
- **Scripts live in `pyproject.toml`** — under `[tool.run-script]` or `[tool.uvtask.run-script]`
- **Compose pipelines without shell glue** — chain commands by referencing other script names
- **Pre/post hooks** — Composer-style (`pre-test` / `post-test`) or NPM-style (`pretest` / `posttest`)
- **uv-like CLI** — colored output, structured help, and typo suggestions for unknown commands
- **Forward arguments safely** — extra CLI args pass through to the underlying command, including JSON and values with spaces

## 🎯 Quick Start
Pick uvtask when you want npm/composer-style project scripts in Python, with a CLI that feels at home next to `uv`, `ruff`, and `ty`.

Run `uvtask` directly with `uvx` (no installation required):
## Quick Start

```shell
uvx uvtask <OPTIONS> [COMMAND]
**1. Add scripts to `pyproject.toml`:**

```toml
[tool.run-script]
lint = "uv run ruff check ."
test = "uv run pytest"
check = ["lint", "test"]
```

Or install it and use it directly:
**2. Run them:**

```shell
uv add --dev uvtask
uv run uvtask <OPTIONS> [COMMAND]
uvx uvtask check
uvx uvtask test -k integration # args forwarded to pytest
```

## 📝 Configuration
For daily use, install once with `uv tool install uvtask`, then run `uvtask` directly.

## Features

### Configuration formats

Define your scripts in `pyproject.toml` under the `[tool.run-script]` (or `[tool.uvtask.run-script]`) section:
Scripts support several TOML shapes:

```toml
[tool.run-script]
install = "uv sync --dev --all-extras"
# Simple string
format = "uv run ruff format ."

# With description (shown in help)
lint = { command = "uv run ruff check .", description = "Check code quality" }
check = ["uv run ty check .", "uv run mypy ."]
pre-test = "echo 'Running tests...'"

# Multiple commands run in sequence
check = ["lint", "test"]

# Multiline commands
deploy = """
echo 'Building...'
uv build
echo 'Done!'
"""
```

You can also nest scripts under `[tool.uvtask.run-script]` if you prefer a namespaced layout.

See this repository's [pyproject.toml](pyproject.toml) for a full real-world script catalog.

### Command composition

Reference other script names to build pipelines without repeating shell commands:

```toml
[tool.run-script]
lint = "uv run ruff check ."
test = "uv run pytest"
post-test = "echo 'Tests completed!'"
deploy = [
"echo 'Building...'",
"uv build",
"echo 'Deploying...'",
"uv deploy"
]
static = ["lint", "test"]
all = ["static"]
```

Running `uvx uvtask all` executes `lint` then `test`.

### Hooks

Define hooks that run automatically before and after a command:

```toml
[tool.run-script]
pre-test = "echo 'Setting up...'"
test = "uv run pytest"
post-test = "echo 'Cleaning up...'"
```

Both naming styles are supported:

| Style | Pre-hook | Post-hook |
|----------|------------|-------------|
| Composer | `pre-test` | `post-test` |
| NPM | `pretest` | `posttest` |

Skip hooks when needed:

```shell
uvx uvtask --no-hooks test
```

## 🛠️ Development
### Arguments

To run the development version:
Extra arguments after the command name are forwarded to the underlying script:

```shell
uvx uvtask test -k integration -x
uvx uvtask celery-call example -k '{"kwarg": "value"}'
```

Values with spaces and JSON are quoted correctly for the shell on both Unix and Windows.

### Namespaced commands

Use colons to group related commands:

```toml
[tool.run-script]
static-analysis = { command = ["static-analysis:linter", "static-analysis:types"], description = "Run all static analysis checks" }
"static-analysis:linter" = "uv run ruff check ."
"static-analysis:types" = "uv run ty check ."
```

## CLI reference

| Flag | Purpose |
|------|---------|
| `-q` / `--quiet` | Suppress stdout (stackable) |
| `-v` / `--verbose` | Show command and exit codes (stackable) |
| `--no-hooks` / `--ignore-scripts` | Skip pre/post hooks |
| `--color` | Control color output: `auto`, `always`, or `never` |
| `help [command]` | Show per-command documentation from `description` |
| `-V` / `--version` | Print version |
| `-h` / `--help` | Print general help |

## Comparison

| Tool | Best for | uvtask difference |
|------|----------|-------------------|
| `uv run` / `uvx` | One-off tool invocations | Named, documented project commands with hooks and composition |
| [Poe the Poet](https://github.com/nat-n/poethepoet) | Rich task runner with templating | Zero deps, `uvx`-native, uv-styled CLI |
| [Hatch scripts](https://hatch.pypa.io/) | Hatch-managed projects | Tool-agnostic `pyproject.toml` config, works with any uv project |
| npm / Composer scripts | JS / PHP ecosystems | Same mental model, Python-native |

## Development

Run the development version from a local checkout:

```shell
uvx -q --no-cache --from $PWD uvtask
```

## 🤝 Contributing
Common project tasks:

```shell
uvx uvtask static-analysis
uvx uvtask test
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## 📄 License
## License

[MIT](https://github.com/aiopy/python-uvtask/blob/master/LICENSE) © uvtask contributors

---

**Note:** uvtask is an independent, third-party project — not an official Astral tool. It is inspired by and designed to work seamlessly with Astral's excellent tools (`uv`, `ruff`, `ty`). We're grateful for the work the Astral team does for the Python ecosystem.
22 changes: 14 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["uv_build"]
requires = ["uv_build>=0.11.17,<0.11.18"]
build-backend = "uv_build"

[tool.uv.build-backend]
Expand Down Expand Up @@ -27,8 +27,12 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
]
dependencies = []
description = "An extremely fast Python task runner."
Expand All @@ -45,18 +49,18 @@ keywords = [
license = { text = "MIT" }
name = "uvtask"
readme = "README.md"
requires-python = ">=3.13"
requires-python = ">=3.10"

[dependency-groups]
dev = [
"bandit>=1.9.4", # security-analysis:vulnerabilities
"pip-licenses>=5.5.1", # security-analysis:licenses
"pytest>=9.0.2", # test
"pytest-cov>=7.0.0", # test, coverage
"pip-licenses>=5.5.5", # security-analysis:licenses
"pytest>=9.1.0", # test
"pytest-cov>=7.1.0", # test, coverage
"pytest-xdist>=3.8.0", # test
"radon>=6.0.1", # complexity:visibility
"ruff>=0.15.4", # code-formatter, static-analysis:linter
"ty>=0.0.20", # static-analysis:types
"ruff>=0.15.17", # code-formatter, static-analysis:linter
"ty>=0.0.49", # static-analysis:types
"xenon>=0.9.3", # complexity:enforcement
]

Expand Down Expand Up @@ -142,7 +146,9 @@ upgrade-dev-install = { command = "uv sync --dev --all-extras --upgrade --refres
code-formatter = { command = "uv run ruff format uvtask tests", description = "Format code with ruff" }
security-analysis = { command = ["security-analysis:licenses", "security-analysis:vulnerabilities"], description = "Run all security analysis checks" }
"security-analysis:licenses" = { command = "uv run pip-licenses", description = "Check third-party dependencies licenses using pip-licenses" }
"security-analysis:vulnerabilities" = { command = "uv run bandit -r -c pyproject.toml uvtask tests", description = "Scan code for security vulnerabilities using bandit" }
"security-analysis:vulnerabilities" = { command = ["security-analysis:vulnerabilities:code", "security-analysis:vulnerabilities:pkgs"], description = "Scan code for security vulnerabilities using bandit and audit" }
"security-analysis:vulnerabilities:code" = { command = "uv run bandit -r -c pyproject.toml uvtask tests", description = "Scan code for security vulnerabilities using bandit" }
"security-analysis:vulnerabilities:pkgs" = { command = "uv audit", description = "Scan code for security vulnerabilities using audit" }
static-analysis = { command = ["static-analysis:linter", "static-analysis:types"], description = "Run all static analysis checks" }
"static-analysis:linter" = { command = "uv run ruff check uvtask tests", description = "Run linter checks using ruff" }
"static-analysis:types" = { command = "uv run ty check uvtask tests", description = "Run type checks using ty" }
Expand Down
25 changes: 24 additions & 1 deletion tests/unit/test_commands.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import sys
from shlex import join as shlex_join
from subprocess import list2cmdline
from unittest.mock import MagicMock, patch

import pytest
Expand All @@ -11,6 +14,12 @@
)


def _expected_script_args_str(script_args: list[str]) -> str:
if sys.platform == "win32":
return list2cmdline(script_args)
return shlex_join(script_args)


class TestCommandBuilder:
def test_build_string_command(self) -> None:
builder = CommandBuilder()
Expand Down Expand Up @@ -55,7 +64,21 @@ def test_build_circular_reference_detection(self) -> None:
def test_build_invalid_type(self) -> None:
builder = CommandBuilder()
with pytest.raises(ValueError, match="Invalid script format"):
builder.build_commands(123, []) # type: ignore[arg-type]
builder.build_commands(123, []) # ty: ignore[invalid-argument-type]

def test_build_command_quotes_json_kwargs(self) -> None:
builder = CommandBuilder()
script_args = ["example", "-k", '{"kwarg": "value"}']
commands = builder.build_commands("celery -A app call", script_args)
expected = f"celery -A app call {_expected_script_args_str(script_args)}"
assert commands == [expected]

def test_build_command_quotes_args_with_spaces(self) -> None:
builder = CommandBuilder()
script_args = ["hello world"]
commands = builder.build_commands("echo", script_args)
expected = f"echo {_expected_script_args_str(script_args)}"
assert commands == [expected]


class TestCommandValidator:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def test_parse_dict_without_command(self) -> None:

def test_parse_invalid_type(self) -> None:
with pytest.raises(ValueError, match="Invalid script value"):
ScriptValueParser.parse("test", 123) # type: ignore[arg-type]
ScriptValueParser.parse("test", 123) # ty: ignore[invalid-argument-type]


class TestRunScriptSectionReader:
Expand Down
Loading
Loading