Skip to content

Commit 0dfa29a

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. - **Modularize CLI**: Split `cli.py` (847 lines) into focused modules: - `colors.py`: Color management and terminal output formatting - `commands.py`: Command execution and script loading - `config.py`: Configuration parsing and pyproject.toml handling - `executor.py`: Command execution with quiet/verbose support - `formatters.py`: Custom argparse formatters and help text processing - `hooks.py`: Pre/post hook discovery and validation - `parser.py`: Argument parsing and CLI builder - **Improved separation of concerns**: Each module has a single, well-defined responsibility, making the codebase more maintainable and testable - **Hooks support**: Added pre/post hook functionality supporting both Composer-style (`pre-{command}`, `post-{command}`) and NPM-style (`pre{command}`, `post{command}`) naming conventions - Automatic hook discovery and execution - Style consistency validation (prevents mixing styles) - `--no-hooks` and `--ignore-scripts` flags to skip hooks - **CI/CD pipelines**: Added GitHub Actions workflows - `ci.yml`: Continuous integration with linting, type checking, and tests - `cd.yml`: Continuous deployment with automated versioning and publishing - **Comprehensive unit tests**: Added test coverage for all new modules: - `test_cli.py`: CLI integration tests - `test_commands.py`: Command execution tests - `test_config.py`: Configuration parsing tests - `test_executor.py`: Command executor tests - `test_formatters.py`: Help formatter tests - `test_hooks.py`: Hook discovery and validation tests - `test_parser.py`: Argument parser tests - **Updated README**: Enhanced documentation with hooks examples and improved quick start guide - Updated `ty` package from 0.0.6 to 0.0.7 This refactoring maintains backward compatibility while significantly improving code quality, testability, and maintainability.
1 parent 2b9ed86 commit 0dfa29a

28 files changed

Lines changed: 2696 additions & 502 deletions

.github/workflows/cd.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
with:
45+
python-version: .python-version
46+
47+
- name: Install dependencies
48+
shell: bash
49+
run: uvx uvtask dev-install
50+
51+
- name: Set version
52+
shell: bash
53+
run: |
54+
if [ -z "${VERSION}" ] || [ "${VERSION}" = "None" ]; then
55+
uv version --bump minor
56+
VERSION=$(uv version --short)
57+
echo "VERSION=${VERSION}" >> "${GITHUB_ENV:-/dev/null}"
58+
else
59+
uv version ${VERSION}
60+
fi
61+
git config user.name "${{ github.actor }}"
62+
git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
63+
git tag -a "${VERSION}" -m "Release ${VERSION}"
64+
git push origin "${VERSION}"
65+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
66+
67+
- name: Build package
68+
shell: bash
69+
run: uv build
70+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
71+
72+
- name: Upload package to artifact registry
73+
uses: actions/upload-artifact@v6
74+
with:
75+
name: uvtask
76+
path: dist/
77+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
78+
79+
- name: Publish package
80+
shell: bash
81+
run: uv publish
82+
if: ${{ env.PIPELINE_TAG == 'true' && env.RELEASE_MODE == 'true' }}
83+
84+
- name: Clean
85+
shell: bash
86+
run: uvx uvtask clean

.github/workflows/ci.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
with:
44+
python-version: .python-version
45+
46+
- name: Install dependencies
47+
shell: bash
48+
run: uvx uvtask dev-install
49+
50+
- name: security-analysis-licenses
51+
shell: bash
52+
run: uvx uvtask security-analysis:licenses
53+
if: ${{ env.PIPELINE_TESTS == 'true' }}
54+
55+
- name: security-analysis-vulnerabilities
56+
shell: bash
57+
run: uvx uvtask security-analysis:vulnerabilities
58+
if: ${{ env.PIPELINE_TESTS == 'true' }}
59+
60+
- name: static-analysis-linter
61+
shell: bash
62+
run: uvx uvtask static-analysis:linter
63+
if: ${{ env.PIPELINE_TESTS == 'true' }}
64+
65+
- name: static-analysis-types
66+
shell: bash
67+
run: uvx uvtask static-analysis:types
68+
if: ${{ env.PIPELINE_TESTS == 'true' }}
69+
70+
- name: unit-tests
71+
shell: bash
72+
run: uvx uvtask unit-tests
73+
if: ${{ env.PIPELINE_TESTS == 'true' }}
74+
75+
- name: integration-tests
76+
shell: bash
77+
run: uvx uvtask integration-tests
78+
if: ${{ env.PIPELINE_TESTS == 'true' }}
79+
80+
- name: Clean
81+
shell: bash
82+
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)