uv-script 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uv_script-0.1.5/PKG-INFO +162 -0
- uv_script-0.1.5/README.md +152 -0
- uv_script-0.1.5/pyproject.toml +40 -0
- uv_script-0.1.5/src/uv_script/__init__.py +3 -0
- uv_script-0.1.5/src/uv_script/__main__.py +3 -0
- uv_script-0.1.5/src/uv_script/cli.py +102 -0
- uv_script-0.1.5/src/uv_script/config.py +80 -0
- uv_script-0.1.5/src/uv_script/runner.py +78 -0
uv_script-0.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uv-script
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: A lightweight script runner for uv — define and run project scripts from pyproject.toml
|
|
5
|
+
Author: Felix Simkovic
|
|
6
|
+
Author-email: Felix Simkovic <felixsimkovic@me.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# uv-script
|
|
12
|
+
|
|
13
|
+
[](https://pypi.org/project/uv-script/)
|
|
14
|
+
|
|
15
|
+
A lightweight, zero-dependency script runner for [uv](https://docs.astral.sh/uv/). Define project scripts in `pyproject.toml` and run them through `uv run`.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
$ uvs --list
|
|
19
|
+
check lint -> test
|
|
20
|
+
format ruff format src/
|
|
21
|
+
lint ruff check src/
|
|
22
|
+
test pytest tests/ -v
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why?
|
|
26
|
+
|
|
27
|
+
uv is a fantastic package manager but has no built-in task runner. If you've been using [Hatch](https://hatch.pypa.io/) just for its scripts, or reaching for [Poe the Poet](https://poethepoet.natn.io/) / [Taskipy](https://github.com/taskipy/taskipy) to fill the gap, `uvs` offers a simpler alternative:
|
|
28
|
+
|
|
29
|
+
- **Zero runtime dependencies** — stdlib only (`tomllib`, `argparse`, `subprocess`)
|
|
30
|
+
- **uv-native** — every command runs through `uv run`, so your venv, lockfile, and Python version are always in sync
|
|
31
|
+
- **Familiar** — if you've used npm scripts or Hatch scripts, you already know how this works
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# As a dev dependency in your project
|
|
37
|
+
uv add --dev uv-script
|
|
38
|
+
|
|
39
|
+
# Or run without installing
|
|
40
|
+
uvx uv-script --list
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
Add a `[tool.uvs.scripts]` section to your `pyproject.toml`:
|
|
46
|
+
|
|
47
|
+
```toml
|
|
48
|
+
[tool.uvs.scripts]
|
|
49
|
+
test = "pytest tests/ -v"
|
|
50
|
+
lint = "ruff check src/"
|
|
51
|
+
format = "ruff format src/"
|
|
52
|
+
check = ["lint", "test"]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uvs test # runs: uv run pytest tests/ -v
|
|
59
|
+
uvs check # runs lint, then test (stops on first failure)
|
|
60
|
+
uvs --list # shows all available scripts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Scripts are defined under `[tool.uvs.scripts]` in `pyproject.toml`. Three formats are supported:
|
|
66
|
+
|
|
67
|
+
### Simple command
|
|
68
|
+
|
|
69
|
+
A string value runs a single command through `uv run`:
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
[tool.uvs.scripts]
|
|
73
|
+
test = "pytest tests/ -v"
|
|
74
|
+
lint = "ruff check ."
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Composite script
|
|
78
|
+
|
|
79
|
+
An array of strings runs multiple steps sequentially. Items can reference other script names or be raw commands. Execution stops on the first non-zero exit code.
|
|
80
|
+
|
|
81
|
+
```toml
|
|
82
|
+
[tool.uvs.scripts]
|
|
83
|
+
lint = "ruff check ."
|
|
84
|
+
test = "pytest tests/"
|
|
85
|
+
check = ["lint", "test"]
|
|
86
|
+
full = ["ruff format --check .", "lint", "test"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Table with options
|
|
90
|
+
|
|
91
|
+
A table gives you control over environment variables and help text:
|
|
92
|
+
|
|
93
|
+
```toml
|
|
94
|
+
[tool.uvs.scripts.serve]
|
|
95
|
+
cmd = "python -m flask run"
|
|
96
|
+
env = { FLASK_DEBUG = "1", FLASK_APP = "app.py" }
|
|
97
|
+
help = "Start the development server"
|
|
98
|
+
|
|
99
|
+
[tool.uvs.scripts.deploy]
|
|
100
|
+
cmd = "python scripts/deploy.py"
|
|
101
|
+
env = { ENV = "production" }
|
|
102
|
+
help = "Deploy to production"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Key | Type | Required | Description |
|
|
106
|
+
|--------|-------------------|----------|------------------------------------------|
|
|
107
|
+
| `cmd` | string | yes | The command to run |
|
|
108
|
+
| `env` | table of strings | no | Environment variables (overlays current) |
|
|
109
|
+
| `help` | string | no | Description shown in `--list` output |
|
|
110
|
+
|
|
111
|
+
## Usage
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
uvs [options] <script> [-- extra-args...]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Flag | Description |
|
|
118
|
+
|-------------------|-----------------------------------|
|
|
119
|
+
| `-l`, `--list` | List all available scripts |
|
|
120
|
+
| `-v`, `--verbose` | Print each command before running |
|
|
121
|
+
| `-V`, `--version` | Show version and exit |
|
|
122
|
+
|
|
123
|
+
### Passing extra arguments
|
|
124
|
+
|
|
125
|
+
Use `--` to forward arguments to the underlying command:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uvs test -- -k "test_parse" --no-header
|
|
129
|
+
# runs: uv run pytest tests/ -v -k test_parse --no-header
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For composite scripts, extra arguments are appended to the last command in the chain.
|
|
133
|
+
|
|
134
|
+
### Running from subdirectories
|
|
135
|
+
|
|
136
|
+
`uvs` walks up from your current directory to find the nearest `pyproject.toml`, just like `uv` does. You can run `uvs test` from anywhere inside your project.
|
|
137
|
+
|
|
138
|
+
## How it works
|
|
139
|
+
|
|
140
|
+
`uvs` is a thin orchestration layer. Every command is prefixed with `uv run`, which means:
|
|
141
|
+
|
|
142
|
+
1. Your virtual environment is automatically activated
|
|
143
|
+
2. Dependencies are synced from the lockfile if needed
|
|
144
|
+
3. The correct Python version is used
|
|
145
|
+
|
|
146
|
+
There is no magic — `uvs test` is equivalent to typing `uv run pytest tests/ -v` yourself.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
This project uses `uvs` to manage its own scripts:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git clone <repo-url> && cd uv-script
|
|
154
|
+
uv sync
|
|
155
|
+
uvs test # run tests
|
|
156
|
+
uvs lint # run linter
|
|
157
|
+
uvs check # lint + test
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# uv-script
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/uv-script/)
|
|
4
|
+
|
|
5
|
+
A lightweight, zero-dependency script runner for [uv](https://docs.astral.sh/uv/). Define project scripts in `pyproject.toml` and run them through `uv run`.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
$ uvs --list
|
|
9
|
+
check lint -> test
|
|
10
|
+
format ruff format src/
|
|
11
|
+
lint ruff check src/
|
|
12
|
+
test pytest tests/ -v
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why?
|
|
16
|
+
|
|
17
|
+
uv is a fantastic package manager but has no built-in task runner. If you've been using [Hatch](https://hatch.pypa.io/) just for its scripts, or reaching for [Poe the Poet](https://poethepoet.natn.io/) / [Taskipy](https://github.com/taskipy/taskipy) to fill the gap, `uvs` offers a simpler alternative:
|
|
18
|
+
|
|
19
|
+
- **Zero runtime dependencies** — stdlib only (`tomllib`, `argparse`, `subprocess`)
|
|
20
|
+
- **uv-native** — every command runs through `uv run`, so your venv, lockfile, and Python version are always in sync
|
|
21
|
+
- **Familiar** — if you've used npm scripts or Hatch scripts, you already know how this works
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# As a dev dependency in your project
|
|
27
|
+
uv add --dev uv-script
|
|
28
|
+
|
|
29
|
+
# Or run without installing
|
|
30
|
+
uvx uv-script --list
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
Add a `[tool.uvs.scripts]` section to your `pyproject.toml`:
|
|
36
|
+
|
|
37
|
+
```toml
|
|
38
|
+
[tool.uvs.scripts]
|
|
39
|
+
test = "pytest tests/ -v"
|
|
40
|
+
lint = "ruff check src/"
|
|
41
|
+
format = "ruff format src/"
|
|
42
|
+
check = ["lint", "test"]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then run:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uvs test # runs: uv run pytest tests/ -v
|
|
49
|
+
uvs check # runs lint, then test (stops on first failure)
|
|
50
|
+
uvs --list # shows all available scripts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
Scripts are defined under `[tool.uvs.scripts]` in `pyproject.toml`. Three formats are supported:
|
|
56
|
+
|
|
57
|
+
### Simple command
|
|
58
|
+
|
|
59
|
+
A string value runs a single command through `uv run`:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[tool.uvs.scripts]
|
|
63
|
+
test = "pytest tests/ -v"
|
|
64
|
+
lint = "ruff check ."
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Composite script
|
|
68
|
+
|
|
69
|
+
An array of strings runs multiple steps sequentially. Items can reference other script names or be raw commands. Execution stops on the first non-zero exit code.
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
[tool.uvs.scripts]
|
|
73
|
+
lint = "ruff check ."
|
|
74
|
+
test = "pytest tests/"
|
|
75
|
+
check = ["lint", "test"]
|
|
76
|
+
full = ["ruff format --check .", "lint", "test"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Table with options
|
|
80
|
+
|
|
81
|
+
A table gives you control over environment variables and help text:
|
|
82
|
+
|
|
83
|
+
```toml
|
|
84
|
+
[tool.uvs.scripts.serve]
|
|
85
|
+
cmd = "python -m flask run"
|
|
86
|
+
env = { FLASK_DEBUG = "1", FLASK_APP = "app.py" }
|
|
87
|
+
help = "Start the development server"
|
|
88
|
+
|
|
89
|
+
[tool.uvs.scripts.deploy]
|
|
90
|
+
cmd = "python scripts/deploy.py"
|
|
91
|
+
env = { ENV = "production" }
|
|
92
|
+
help = "Deploy to production"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Key | Type | Required | Description |
|
|
96
|
+
|--------|-------------------|----------|------------------------------------------|
|
|
97
|
+
| `cmd` | string | yes | The command to run |
|
|
98
|
+
| `env` | table of strings | no | Environment variables (overlays current) |
|
|
99
|
+
| `help` | string | no | Description shown in `--list` output |
|
|
100
|
+
|
|
101
|
+
## Usage
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
uvs [options] <script> [-- extra-args...]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Flag | Description |
|
|
108
|
+
|-------------------|-----------------------------------|
|
|
109
|
+
| `-l`, `--list` | List all available scripts |
|
|
110
|
+
| `-v`, `--verbose` | Print each command before running |
|
|
111
|
+
| `-V`, `--version` | Show version and exit |
|
|
112
|
+
|
|
113
|
+
### Passing extra arguments
|
|
114
|
+
|
|
115
|
+
Use `--` to forward arguments to the underlying command:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
uvs test -- -k "test_parse" --no-header
|
|
119
|
+
# runs: uv run pytest tests/ -v -k test_parse --no-header
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For composite scripts, extra arguments are appended to the last command in the chain.
|
|
123
|
+
|
|
124
|
+
### Running from subdirectories
|
|
125
|
+
|
|
126
|
+
`uvs` walks up from your current directory to find the nearest `pyproject.toml`, just like `uv` does. You can run `uvs test` from anywhere inside your project.
|
|
127
|
+
|
|
128
|
+
## How it works
|
|
129
|
+
|
|
130
|
+
`uvs` is a thin orchestration layer. Every command is prefixed with `uv run`, which means:
|
|
131
|
+
|
|
132
|
+
1. Your virtual environment is automatically activated
|
|
133
|
+
2. Dependencies are synced from the lockfile if needed
|
|
134
|
+
3. The correct Python version is used
|
|
135
|
+
|
|
136
|
+
There is no magic — `uvs test` is equivalent to typing `uv run pytest tests/ -v` yourself.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
This project uses `uvs` to manage its own scripts:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git clone <repo-url> && cd uv-script
|
|
144
|
+
uv sync
|
|
145
|
+
uvs test # run tests
|
|
146
|
+
uvs lint # run linter
|
|
147
|
+
uvs check # lint + test
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uv-script"
|
|
3
|
+
version = "0.1.5"
|
|
4
|
+
description = "A lightweight script runner for uv — define and run project scripts from pyproject.toml"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Felix Simkovic", email = "felixsimkovic@me.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
uvs = "uv_script.cli:main"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.8.7,<0.9.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8.0",
|
|
23
|
+
"pytest-cov>=5.0",
|
|
24
|
+
"ruff>=0.9.0",
|
|
25
|
+
"ty>=0.0.15",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
target-version = "py312"
|
|
30
|
+
line-length = 99
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
34
|
+
|
|
35
|
+
[tool.uvs.scripts]
|
|
36
|
+
test = "pytest tests/ -v"
|
|
37
|
+
lint = "ruff check src/"
|
|
38
|
+
format = "ruff format src/"
|
|
39
|
+
typecheck = "ty check src/"
|
|
40
|
+
check = ["lint", "typecheck", "test"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""CLI entry point for uvs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from uv_script import __version__
|
|
9
|
+
from uv_script.config import ConfigError, ScriptDef, load_scripts
|
|
10
|
+
from uv_script.runner import run_script
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> None:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="uvs",
|
|
16
|
+
description="Run scripts defined in [tool.uvs.scripts] via uv run",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"-V",
|
|
20
|
+
"--version",
|
|
21
|
+
action="version",
|
|
22
|
+
version=f"%(prog)s {__version__}",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-l",
|
|
26
|
+
"--list",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="List available scripts",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"-v",
|
|
32
|
+
"--verbose",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="Print commands before executing",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"script",
|
|
38
|
+
nargs="?",
|
|
39
|
+
metavar="SCRIPT",
|
|
40
|
+
help="Name of the script to run",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"args",
|
|
44
|
+
nargs=argparse.REMAINDER,
|
|
45
|
+
metavar="...",
|
|
46
|
+
help="Additional arguments passed to the script",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
parsed = parser.parse_args(argv)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
scripts = load_scripts()
|
|
53
|
+
except ConfigError as e:
|
|
54
|
+
print(f"uvs: {e}", file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
if parsed.list:
|
|
58
|
+
_print_list(scripts)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if not parsed.script:
|
|
62
|
+
parser.print_help()
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
if parsed.script not in scripts:
|
|
66
|
+
print(f"uvs: unknown script '{parsed.script}'", file=sys.stderr)
|
|
67
|
+
print(f"Available scripts: {', '.join(sorted(scripts))}", file=sys.stderr)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
script = scripts[parsed.script]
|
|
71
|
+
|
|
72
|
+
# Strip leading '--' from extra args if present
|
|
73
|
+
extra_args = parsed.args
|
|
74
|
+
if extra_args and extra_args[0] == "--":
|
|
75
|
+
extra_args = extra_args[1:]
|
|
76
|
+
|
|
77
|
+
exit_code = run_script(
|
|
78
|
+
script,
|
|
79
|
+
all_scripts=scripts,
|
|
80
|
+
extra_args=extra_args or None,
|
|
81
|
+
verbose=parsed.verbose,
|
|
82
|
+
)
|
|
83
|
+
sys.exit(exit_code)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _print_list(scripts: dict[str, ScriptDef]) -> None:
|
|
87
|
+
"""Print a formatted list of available scripts."""
|
|
88
|
+
if not scripts:
|
|
89
|
+
print("No scripts defined.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
max_name = max(len(name) for name in scripts)
|
|
93
|
+
|
|
94
|
+
for name in sorted(scripts):
|
|
95
|
+
script = scripts[name]
|
|
96
|
+
help_text = script.help_text
|
|
97
|
+
if not help_text:
|
|
98
|
+
help_text = (
|
|
99
|
+
" -> ".join(script.commands) if script.is_composite else script.commands[0]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
print(f" {name:<{max_name}} {help_text}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Load and parse script definitions from pyproject.toml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ScriptDef:
|
|
12
|
+
"""Normalized representation of a script definition."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
commands: list[str]
|
|
16
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
17
|
+
help_text: str = ""
|
|
18
|
+
is_composite: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigError(Exception):
|
|
22
|
+
"""Raised when pyproject.toml is missing or malformed."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_pyproject(start: Path | None = None) -> Path:
|
|
26
|
+
"""Walk up from start (default: cwd) to find pyproject.toml."""
|
|
27
|
+
current = start or Path.cwd()
|
|
28
|
+
for directory in [current, *current.parents]:
|
|
29
|
+
candidate = directory / "pyproject.toml"
|
|
30
|
+
if candidate.is_file():
|
|
31
|
+
return candidate
|
|
32
|
+
raise ConfigError("No pyproject.toml found in current directory or any parent")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_scripts(pyproject_path: Path | None = None) -> dict[str, ScriptDef]:
|
|
36
|
+
"""Load and normalize all script definitions.
|
|
37
|
+
|
|
38
|
+
Returns dict mapping script name -> ScriptDef.
|
|
39
|
+
"""
|
|
40
|
+
path = pyproject_path or find_pyproject()
|
|
41
|
+
|
|
42
|
+
with open(path, "rb") as f:
|
|
43
|
+
data = tomllib.load(f)
|
|
44
|
+
|
|
45
|
+
scripts_table = data.get("tool", {}).get("uvs", {}).get("scripts", {})
|
|
46
|
+
if not scripts_table:
|
|
47
|
+
raise ConfigError(f"No [tool.uvs.scripts] section found in {path}")
|
|
48
|
+
|
|
49
|
+
result: dict[str, ScriptDef] = {}
|
|
50
|
+
for name, value in scripts_table.items():
|
|
51
|
+
result[name] = _parse_script(name, value)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_script(name: str, value: str | list | dict) -> ScriptDef:
|
|
56
|
+
"""Parse a single script value into a ScriptDef."""
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
return ScriptDef(name=name, commands=[value])
|
|
59
|
+
|
|
60
|
+
if isinstance(value, list):
|
|
61
|
+
if not all(isinstance(v, str) for v in value):
|
|
62
|
+
raise ConfigError(f"Script '{name}': array items must all be strings")
|
|
63
|
+
return ScriptDef(name=name, commands=value, is_composite=True)
|
|
64
|
+
|
|
65
|
+
if isinstance(value, dict):
|
|
66
|
+
cmd = value.get("cmd")
|
|
67
|
+
if not cmd or not isinstance(cmd, str):
|
|
68
|
+
raise ConfigError(f"Script '{name}': table form requires a 'cmd' string")
|
|
69
|
+
env = value.get("env", {})
|
|
70
|
+
if not isinstance(env, dict):
|
|
71
|
+
raise ConfigError(f"Script '{name}': 'env' must be a table of strings")
|
|
72
|
+
help_text = value.get("help", "")
|
|
73
|
+
return ScriptDef(
|
|
74
|
+
name=name,
|
|
75
|
+
commands=[cmd],
|
|
76
|
+
env={str(k): str(v) for k, v in env.items()},
|
|
77
|
+
help_text=help_text,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
raise ConfigError(f"Script '{name}': value must be a string, array, or table")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Execute scripts by delegating to uv run."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from uv_script.config import ConfigError, ScriptDef
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_script(
|
|
14
|
+
script: ScriptDef,
|
|
15
|
+
all_scripts: dict[str, ScriptDef],
|
|
16
|
+
extra_args: list[str] | None = None,
|
|
17
|
+
verbose: bool = False,
|
|
18
|
+
) -> int:
|
|
19
|
+
"""Execute a script definition. Returns exit code (0 = success)."""
|
|
20
|
+
steps = resolve_steps(script, all_scripts)
|
|
21
|
+
|
|
22
|
+
for i, (cmd_str, env) in enumerate(steps):
|
|
23
|
+
if extra_args and i == len(steps) - 1:
|
|
24
|
+
cmd_str = cmd_str + " " + " ".join(shlex.quote(a) for a in extra_args)
|
|
25
|
+
|
|
26
|
+
exit_code = _exec_one(cmd_str, env, verbose)
|
|
27
|
+
if exit_code != 0:
|
|
28
|
+
return exit_code
|
|
29
|
+
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_steps(
|
|
34
|
+
script: ScriptDef,
|
|
35
|
+
all_scripts: dict[str, ScriptDef],
|
|
36
|
+
_seen: set[str] | None = None,
|
|
37
|
+
) -> list[tuple[str, dict[str, str]]]:
|
|
38
|
+
"""Resolve a script into a flat list of (command, env) pairs.
|
|
39
|
+
|
|
40
|
+
Handles references to other scripts and detects cycles.
|
|
41
|
+
"""
|
|
42
|
+
if _seen is None:
|
|
43
|
+
_seen = set()
|
|
44
|
+
|
|
45
|
+
if script.name in _seen:
|
|
46
|
+
raise ConfigError(f"Circular reference detected: {script.name}")
|
|
47
|
+
_seen.add(script.name)
|
|
48
|
+
|
|
49
|
+
if not script.is_composite:
|
|
50
|
+
return [(cmd, script.env) for cmd in script.commands]
|
|
51
|
+
|
|
52
|
+
result: list[tuple[str, dict[str, str]]] = []
|
|
53
|
+
for item in script.commands:
|
|
54
|
+
if item in all_scripts:
|
|
55
|
+
referenced = all_scripts[item]
|
|
56
|
+
result.extend(resolve_steps(referenced, all_scripts, _seen.copy()))
|
|
57
|
+
else:
|
|
58
|
+
result.append((item, script.env))
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _exec_one(cmd_str: str, env: dict[str, str], verbose: bool) -> int:
|
|
64
|
+
"""Execute a single command string via uv run."""
|
|
65
|
+
parts = shlex.split(cmd_str)
|
|
66
|
+
full_cmd = ["uv", "run"] + parts
|
|
67
|
+
|
|
68
|
+
if verbose:
|
|
69
|
+
env_prefix = " ".join(f"{k}={shlex.quote(v)}" for k, v in env.items())
|
|
70
|
+
display = f"{env_prefix} {' '.join(full_cmd)}".strip()
|
|
71
|
+
print(f"$ {display}", file=sys.stderr)
|
|
72
|
+
|
|
73
|
+
run_env = None
|
|
74
|
+
if env:
|
|
75
|
+
run_env = {**os.environ, **env}
|
|
76
|
+
|
|
77
|
+
result = subprocess.run(full_cmd, env=run_env)
|
|
78
|
+
return result.returncode
|