Skip to content

Commit 8d726e4

Browse files
committed
refactor: modularize CLI architecture and add hooks support
This commit represents a major architectural refactoring of uvtask, transforming the monolithic CLI implementation into a modular, testable architecture while adding new features and infrastructure improvements.
1 parent 2b9ed86 commit 8d726e4

28 files changed

Lines changed: 2692 additions & 502 deletions

.github/workflows/cd.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: CD
2+
3+
run-name: CD - ${{ github.event_name == 'workflow_run' && format('{0} (from CI)', github.event.workflow_run.head_commit.message) || format('{0}{1}', github.event.inputs.environment != '' && github.event.inputs.environment || 'dev', github.event.inputs.version != '' && format(' (v{0})', github.event.inputs.version) || '') }}
4+
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: "Release version"
10+
default: "None"
11+
required: false
12+
workflow_run:
13+
workflows: ["CI"]
14+
branches: [main]
15+
types:
16+
- completed
17+
18+
env:
19+
CD: ${{ vars.CONTINUOUS_DEPLOYMENT }}
20+
DEPLOY_MODE: "true"
21+
VERSION: ${{ github.event.inputs.version != '' && github.event.inputs.version || 'None' }}
22+
23+
jobs:
24+
# Continuous Deployment (CD) pipeline
25+
cd:
26+
if: ${{ vars.CONTINUOUS_DEPLOYMENT == 'true' && github.ref_name == 'main' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')) }}
27+
permissions:
28+
contents: write
29+
id-token: write
30+
timeout-minutes: 15
31+
runs-on: ${{ matrix.os }}
32+
strategy:
33+
matrix:
34+
os: [ubuntu-latest]
35+
platforms: [linux/amd64]
36+
steps:
37+
- name: Checkout Git repository
38+
uses: actions/checkout@v6
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Install uv and set the Python version
43+
uses: astral-sh/setup-uv@v7
44+
45+
- name: Install dependencies
46+
shell: bash
47+
run: uvx uvtask dev-install
48+
49+
- name: Set version
50+
shell: bash
51+
run: |
52+
if [ -z "${VERSION}" ] || [ "${VERSION}" = "None" ]; then
53+
uv version --bump minor
54+
VERSION=$(uv version --short)
55+
echo "VERSION=${VERSION}" >> "${GITHUB_ENV:-/dev/null}"
56+
else
57+
uv version ${VERSION}
58+
fi
59+
git config user.name "${{ github.actor }}"
60+
git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
61+
git tag -a "${VERSION}" -m "Release ${VERSION}"
62+
git push origin "${VERSION}"
63+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
64+
65+
- name: Build package
66+
shell: bash
67+
run: uv build
68+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
69+
70+
- name: Upload package to artifact registry
71+
uses: actions/upload-artifact@v6
72+
with:
73+
name: uvtask
74+
path: dist/
75+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
76+
77+
- name: Publish package
78+
shell: bash
79+
run: uv publish
80+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
81+
82+
- name: Clean
83+
shell: bash
84+
run: uvx uvtask clean

.github/workflows/ci.yml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: CI
2+
3+
run-name: CI - ${{ github.event_name == 'pull_request' && github.event.pull_request.title || github.event_name == 'push' && github.event.head_commit.message || github.ref_name }} ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' && format('(v{0})', github.event.inputs.version) || '' }}
4+
5+
on:
6+
push:
7+
branches: [main]
8+
pull_request:
9+
workflow_dispatch:
10+
inputs:
11+
version:
12+
description: "Release version"
13+
default: "None"
14+
required: false
15+
16+
env:
17+
CI: "true"
18+
19+
jobs:
20+
# Continuous Integration (CI) pipeline
21+
ci:
22+
if: ${{ vars.CONTINUOUS_INTEGRATION == 'true' }}
23+
permissions:
24+
contents: read
25+
env:
26+
PIPELINE_TESTS: ${{ github.event_name != 'workflow_dispatch' && github.event.inputs.version == '' && startsWith(github.ref, 'refs/tags/') == false && github.ref != 'refs/heads/main' && 'true' || 'false' }}
27+
RELEASE_MODE: "false"
28+
VERSION: ${{ github.run_id }}
29+
timeout-minutes: 15
30+
runs-on: ${{ matrix.os }}
31+
strategy:
32+
matrix:
33+
os: [ubuntu-latest]
34+
platforms: [linux/amd64, linux/arm64]
35+
steps:
36+
- name: Checkout Git repository
37+
uses: actions/checkout@v6
38+
with:
39+
fetch-depth: 0
40+
41+
- name: Install uv and set the Python version
42+
uses: astral-sh/setup-uv@v7
43+
44+
- name: Install dependencies
45+
shell: bash
46+
run: uvx uvtask dev-install
47+
48+
- name: security-analysis-licenses
49+
shell: bash
50+
run: uvx uvtask security-analysis:licenses
51+
if: ${{ env.PIPELINE_TESTS == 'true' }}
52+
53+
- name: security-analysis-vulnerabilities
54+
shell: bash
55+
run: uvx uvtask security-analysis:vulnerabilities
56+
if: ${{ env.PIPELINE_TESTS == 'true' }}
57+
58+
- name: static-analysis-linter
59+
shell: bash
60+
run: uvx uvtask static-analysis:linter
61+
if: ${{ env.PIPELINE_TESTS == 'true' }}
62+
63+
- name: static-analysis-types
64+
shell: bash
65+
run: uvx uvtask static-analysis:types
66+
if: ${{ env.PIPELINE_TESTS == 'true' }}
67+
68+
- name: unit-tests
69+
shell: bash
70+
run: uvx uvtask unit-tests
71+
if: ${{ env.PIPELINE_TESTS == 'true' }}
72+
73+
- name: integration-tests
74+
shell: bash
75+
run: uvx uvtask integration-tests
76+
if: ${{ env.PIPELINE_TESTS == 'true' }}
77+
78+
- name: Clean
79+
shell: bash
80+
run: uvx uvtask clean

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*.egg-info/
99
*.pyc
1010
__pycache__/
11+
.coverage
1112
build/
1213
dist/
1314
requirements-dev.txt

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.14

README.md

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,67 @@
1-
# 🚀 uvtask
1+
# uvtask
22

3-
[![PyPI version](https://badge.fury.io/py/uvtask.svg)](https://badge.fury.io/py/uvtask)
3+
[![image](https://img.shields.io/pypi/v/uvtask.svg)](https://pypi.python.org/pypi/uvtask)
4+
[![image](https://img.shields.io/pypi/l/uvtask.svg)](https://pypi.python.org/pypi/uvtask)
5+
[![image](https://img.shields.io/pypi/pyversions/uvtask.svg)](https://pypi.python.org/pypi/uvtask)
6+
[![Actions status](https://github.com/aiopy/python-uvtask/actions/workflows/ci.yml/badge.svg)](https://github.com/aiopy/python-uvtask/actions)
47
[![PyPIDownloads](https://static.pepy.tech/badge/uvtask)](https://pepy.tech/project/uvtask)
58

6-
**uvtask** is a modern, fast, and flexible Python task runner and test automation tool designed to simplify development workflows. It supports running, organizing, and managing tasks or tests in Python projects with an emphasis on ease of use and speed. ⚡
9+
An extremely fast Python task runner.
10+
11+
## Highlights
12+
13+
-**Extremely fast** - Built for speed with zero installation overhead
14+
- 📝 **Simple configuration** - Define scripts in `pyproject.toml`
15+
- 🔗 **Pre/post hooks** - Automatically run hooks before and after commands
16+
- 🎨 **Beautiful output** - Colorful, `uv`-inspired CLI
717

818
## 🎯 Quick Start
919

10-
Run tasks defined in your `pyproject.toml`:
20+
Run `uvtask` directly with `uvx` (no installation required):
1121

1222
```shell
13-
uvx uvtask <task_name>
23+
uvx uvtask <OPTIONS> [COMMAND]
24+
```
25+
26+
Or install it and use it directly:
27+
28+
```shell
29+
uv add --dev uvtask
30+
uvtask <OPTIONS> [COMMAND]
1431
```
1532

1633
## 📝 Configuration
1734

18-
Define your tasks in `pyproject.toml` under the `[tool.run-script]` section:
35+
Define your scripts in `pyproject.toml` under the `[tool.run-script]` section:
1936

2037
```toml
2138
[tool.run-script]
22-
hello-world = "echo 'hello world'"
39+
install = "uv sync --dev --all-extras"
40+
format = "ruff format ."
41+
lint = { command = "ruff check .", description = "Check code quality" }
42+
check = ["ty check .", "mypy ."]
43+
pre-test = "echo 'Running tests...'"
44+
test = "pytest"
45+
post-test = "echo 'Tests completed!'"
46+
deploy = [
47+
"echo 'Building...'",
48+
"uv build",
49+
"echo 'Deploying...'",
50+
"uv deploy"
51+
]
2352
```
2453

2554
## 🛠️ Development
2655

2756
To run the development version:
2857

2958
```shell
30-
uvx --no-cache --from $PWD run --help
59+
uvx -q --no-cache --from $PWD uvtask
3160
```
3261

33-
## 📋 Requirements
34-
35-
- 🐍 Python >= 3.13
36-
3762
## 🤝 Contributing
3863

39-
Contributions are welcome! 🎉
40-
41-
- For major changes, please open an issue first to discuss what you would like to change
42-
- Make sure to update tests as appropriate
43-
- Follow the existing code style and conventions
64+
Contributions are welcome! Please feel free to submit a Pull Request.
4465

4566
## 📄 License
4667

pyproject.toml

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
[build-system]
2-
build-backend = "setuptools.build_meta"
3-
requires = ["setuptools"]
2+
requires = ["uv_build"]
3+
build-backend = "uv_build"
4+
5+
[tool.uv.build-backend]
6+
module-name = "uvtask"
7+
module-root = ""
48

59
[project]
610
authors = [
@@ -27,8 +31,8 @@ classifiers = [
2731
"Programming Language :: Python :: 3.14",
2832
]
2933
dependencies = []
30-
description = "uvtask is a modern, fast, and flexible Python task runner and test automation tool designed to simplify development workflows. It supports running, organizing, and managing tasks or tests in Python projects with an emphasis on ease of use and speed."
31-
dynamic = ["version"]
34+
description = "An extremely fast Python task runner."
35+
version = "0.0.0"
3236
keywords = [
3337
"uv",
3438
"uvx",
@@ -51,25 +55,13 @@ dev = [
5155
"pytest-cov>=7.0.0", # test, coverage
5256
"pytest-xdist>=3.8.0", # test
5357
"ruff>=0.14.10", # code-formatter, static-analysis
54-
"ty>=0.0.4", # static-analysis
58+
"ty>=0.0.6", # static-analysis
5559
]
5660

5761
[project.urls]
5862
"documentation" = "https://aiopy.github.io/python-uvtask/"
5963
"repository" = "https://github.com/aiopy/python-uvtask"
6064

61-
[tool.setuptools.dynamic]
62-
version = { attr = "uvtask.__version__" }
63-
64-
[tool.setuptools.packages.find]
65-
include = ["uvtask*"]
66-
67-
[tool.setuptools.package-data]
68-
"uvtask" = ["py.typed"]
69-
70-
[[tool.uv.index]]
71-
url = "https://pypi.org/simple"
72-
7365
[tool.bandit]
7466
exclude_dirs = ["tests"]
7567
skips = ["B404", "B602"]
@@ -103,6 +95,8 @@ lint.select = [
10395
]
10496
lint.ignore = [
10597
"PLC0415",
98+
"PLR0912",
99+
"PLR0913",
106100
"PLR2004",
107101
]
108102

@@ -118,7 +112,7 @@ force-single-line = false
118112
known-first-party = ["uvtask"]
119113

120114
[tool.ruff.lint.mccabe]
121-
max-complexity = 15
115+
max-complexity = 20
122116

123117
[tool.ty.environment]
124118
python-version = "3.13"
@@ -127,35 +121,37 @@ python-version = "3.13"
127121
exclude = [
128122
"tests/fixtures/**",
129123
"var",
124+
".venv",
130125
]
131126

132127
[tool.run-script]
133-
install = "uv sync --frozen --no-dev"
134-
upgrade-install = "uv sync --frozen --no-dev --upgrade --refresh"
135-
dev-install = "uv sync --dev --all-extras"
136-
upgrade-dev-install = "uv sync --dev --all-extras --upgrade --refresh"
137-
deploy = "uv build && uv publish"
138-
docs = "python3 -m mkdocs build -f docs_src/config/en/mkdocs.yml && python3 -m mkdocs build -f docs_src/config/es/mkdocs.yml"
139-
dev-docs = "python3 -m mkdocs serve -f docs_src/config/en/mkdocs.yml"
140-
code-formatter = "uv run ruff format uvtask tests $@"
141-
"security-analysis:licenses" = "uv run pip-licenses"
142-
"security-analysis:vulnerabilities" = "uv run bandit -r -c pyproject.toml uvtask tests"
143-
"static-analysis:linter" = "uv run ruff check uvtask tests"
144-
"static-analysis:types" = "uv run ty check uvtask tests"
145-
test = "uv run pytest"
146-
unit-tests = "uv run pytest tests/unit"
147-
integration-tests = "uv run pytest tests/integration"
148-
functional-tests = "uv run pytest -n1 tests/functional"
149-
coverage = "uv run pytest -n1 --cov --cov-report=html"
150-
clean = """python3 -c \"
128+
install = { command = "uv sync --frozen --no-dev", description = "Install dependencies as specified in lockfile, excluding dev dependencies" }
129+
upgrade-install = { command = "uv sync --frozen --no-dev --upgrade --refresh", description = "Upgrade and refresh installation of non-dev dependencies" }
130+
dev-install = { command = "uv sync --dev --all-extras", description = "Install all dependencies including dev and extras" }
131+
upgrade-dev-install = { command = "uv sync --dev --all-extras --upgrade --refresh", description = "Upgrade and refresh installation of all dependencies including dev and extras" }
132+
code-formatter = { command = "ruff format uvtask tests", description = "Format code with ruff" }
133+
"security-analysis" = { command = ["security-analysis:licenses", "security-analysis:vulnerabilities"], description = "Run all security analysis checks" }
134+
"security-analysis:licenses" = { command = "pip-licenses", description = "Check third-party dependencies licenses using pip-licenses" }
135+
"security-analysis:vulnerabilities" = { command = "bandit -r -c pyproject.toml uvtask tests", description = "Scan code for security vulnerabilities using bandit" }
136+
"static-analysis" = { command = ["static-analysis:linter", "static-analysis:types"], description = "Run all static analysis checks" }
137+
"static-analysis:linter" = { command = "ruff check uvtask tests", description = "Run linter checks using ruff" }
138+
"static-analysis:types" = { command = "ty check uvtask tests", description = "Run type checks using ty" }
139+
test = { command = ["unit-tests", "integration-tests", "functional-tests"], description = "Run all tests with pytest" }
140+
unit-tests = { command = "pytest tests/unit", description = "Run unit tests with pytest" }
141+
integration-tests = { command = "pytest tests/integration", description = "Run integration tests with pytest" }
142+
functional-tests = { command = "pytest -n1 tests/functional", description = "Run functional tests with pytest" }
143+
coverage = { command = "pytest -n1 --cov --cov-report=html", description = "Run tests with coverage report in HTML using pytest" }
144+
clean = { command = """python3 -c "
151145
from glob import iglob
152146
from shutil import rmtree
153147
154148
for pathname in ['./build', './*.egg-info', './dist', './var', '**/__pycache__']:
155149
for path in iglob(pathname, recursive=True):
156150
rmtree(path, ignore_errors=True)
157-
\""""
151+
"
152+
""", description = "Clean build artifacts" }
158153

159154
[project.scripts]
160155
uvtask = "uvtask.cli:main"
156+
run = "uvtask.cli:main"
161157
run-script = "uvtask.cli:main"

tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""Test package for uvtask."""
1+

0 commit comments

Comments
 (0)