uvlazy 0.1.0__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.
- uvlazy-0.1.0/Cargo.lock +7 -0
- uvlazy-0.1.0/Cargo.toml +25 -0
- uvlazy-0.1.0/LICENSE +21 -0
- uvlazy-0.1.0/PKG-INFO +288 -0
- uvlazy-0.1.0/README.md +266 -0
- uvlazy-0.1.0/build.rs +53 -0
- uvlazy-0.1.0/pyproject.toml +39 -0
- uvlazy-0.1.0/scripts/launch.py +60 -0
- uvlazy-0.1.0/src/main.rs +74 -0
- uvlazy-0.1.0/src/uvlazy/__init__.py +12 -0
- uvlazy-0.1.0/src/uvlazy/__main__.py +3 -0
- uvlazy-0.1.0/src/uvlazy/cli.py +127 -0
- uvlazy-0.1.0/src/uvlazy/commands.py +60 -0
- uvlazy-0.1.0/src/uvlazy/config.py +206 -0
- uvlazy-0.1.0/src/uvlazy/installer.py +127 -0
- uvlazy-0.1.0/src/uvlazy/runtime.py +72 -0
- uvlazy-0.1.0/tests/test_uvlazy.py +654 -0
uvlazy-0.1.0/Cargo.lock
ADDED
uvlazy-0.1.0/Cargo.toml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "uvlazy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
rust-version = "1.90"
|
|
6
|
+
description = "Run project-locked tools and install Python dependencies only when used"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
repository = "https://github.com/rvben/uvlazy"
|
|
9
|
+
homepage = "https://github.com/rvben/uvlazy"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
keywords = ["uv", "python", "dependencies", "lazy", "cli"]
|
|
12
|
+
categories = ["command-line-utilities", "development-tools"]
|
|
13
|
+
include = [
|
|
14
|
+
"/Cargo.toml", "/Cargo.lock", "/build.rs", "/src/*.rs", "/src/uvlazy/*.py",
|
|
15
|
+
"/scripts/launch.py", "/pyproject.toml", "/README.md", "/LICENSE", "/tests/*.py",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[[bin]]
|
|
19
|
+
name = "uvlazy"
|
|
20
|
+
path = "src/main.rs"
|
|
21
|
+
|
|
22
|
+
[profile.release]
|
|
23
|
+
strip = true
|
|
24
|
+
lto = true
|
|
25
|
+
codegen-units = 1
|
uvlazy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ruben Jongejan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
uvlazy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uvlazy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Summary: Run project-locked tools and install Python dependencies only when used.
|
|
15
|
+
Home-Page: https://github.com/rvben/uvlazy
|
|
16
|
+
License: MIT
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
19
|
+
Project-URL: Homepage, https://github.com/rvben/uvlazy
|
|
20
|
+
Project-URL: Repository, https://github.com/rvben/uvlazy
|
|
21
|
+
|
|
22
|
+
# uvlazy
|
|
23
|
+
|
|
24
|
+
**Run the tool you need, at the project's locked version, without installing the
|
|
25
|
+
rest of the project.**
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
uvlazy run rumdl check .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
uvlazy finds the declared package providing `rumdl`, reads the version constraints
|
|
32
|
+
from `uv.lock`, installs that package and its dependency tree into a dedicated
|
|
33
|
+
environment, and runs its executable. Application dependencies and unrelated
|
|
34
|
+
tools stay uninstalled. Python console scripts and native executables both work.
|
|
35
|
+
|
|
36
|
+
This is experimental software for macOS and Linux. It requires Python 3.11+ and
|
|
37
|
+
`uv` 0.6.6+ on PATH. Cargo and PyPI install the same Rust executable, which
|
|
38
|
+
bundles a Python engine with no third-party runtime dependencies.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Install through either registry:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
cargo install uvlazy --locked
|
|
46
|
+
# or
|
|
47
|
+
uv tool install uvlazy
|
|
48
|
+
# or, in a Python environment
|
|
49
|
+
pip install uvlazy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or install from this checkout (both routes require Rust):
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
cargo install --path . --locked
|
|
56
|
+
# or
|
|
57
|
+
uv tool install .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or run directly from source without installing uvlazy:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
PYTHONPATH=/path/to/uvlazy/src python3 -m uvlazy run rumdl check .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The executable uses `uv python find --system --no-project` to locate an
|
|
67
|
+
installed Python 3.11+ without syncing a project. Set `UV_PYTHON` to choose a
|
|
68
|
+
specific interpreter, for example `UV_PYTHON=3.12 uvlazy run rumdl check .`.
|
|
69
|
+
If no compatible Python is installed, run `uv python install 3.11` first.
|
|
70
|
+
|
|
71
|
+
## Use in CI
|
|
72
|
+
|
|
73
|
+
Declare the tool in your project's `pyproject.toml`, alongside your existing
|
|
74
|
+
application dependencies:
|
|
75
|
+
|
|
76
|
+
```toml
|
|
77
|
+
[project]
|
|
78
|
+
name = "my-app"
|
|
79
|
+
version = "0.1.0"
|
|
80
|
+
requires-python = ">=3.11"
|
|
81
|
+
dependencies = ["rich>=13", "Pillow>=10", "rumdl"]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
No dependency groups or separate tool configuration are required when the
|
|
85
|
+
command and package names match.
|
|
86
|
+
|
|
87
|
+
Generate or update `uv.lock` during development with `uv lock`, and commit it
|
|
88
|
+
with your project. In CI, with uv and uvlazy available, run:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
uvlazy run rumdl check .
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
There is no preceding `uv sync` step. Tool commands require an existing, current
|
|
95
|
+
lockfile; a missing or stale lock fails without updating it. The selected tool's
|
|
96
|
+
exit status, signals, stdin, stdout, stderr, arguments, and working directory
|
|
97
|
+
are preserved, so lint failures fail the job normally. Installation diagnostics
|
|
98
|
+
go to stderr; `--quiet` suppresses installation progress.
|
|
99
|
+
|
|
100
|
+
The [CI example](examples/ci/) includes a lockfile and declares Rich, Pillow, and
|
|
101
|
+
rumdl. Running the following installs only rumdl into the example's environment:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
uvlazy run --project examples/ci rumdl check examples/ci
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`--project` selects the dependency configuration without changing the current
|
|
108
|
+
working directory. Every option for uvlazy goes before the tool name; everything
|
|
109
|
+
after it is passed to the tool unchanged.
|
|
110
|
+
|
|
111
|
+
## Package names and dependency groups
|
|
112
|
+
|
|
113
|
+
Matching command/package names such as `rumdl` need no configuration. For a
|
|
114
|
+
command whose package name differs, select its declared provider explicitly:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
uvlazy run --from httpie http --help
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or save that mapping:
|
|
121
|
+
|
|
122
|
+
```toml
|
|
123
|
+
[dependency-groups]
|
|
124
|
+
dev = ["httpie"]
|
|
125
|
+
|
|
126
|
+
[tool.uvlazy.commands]
|
|
127
|
+
http = "httpie"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then use `uvlazy run http --help`. Providers must be declared in
|
|
131
|
+
`[project].dependencies`, `[dependency-groups]`, or legacy
|
|
132
|
+
`[tool.uv].dev-dependencies`. Unknown commands fail without installing arbitrary
|
|
133
|
+
packages or falling back to executables from the system PATH. The executable must
|
|
134
|
+
belong to the selected package.
|
|
135
|
+
|
|
136
|
+
By default, a tool declared in project dependencies uses those declarations.
|
|
137
|
+
Otherwise, uvlazy automatically selects groups that directly declare the tool.
|
|
138
|
+
Only the requested package and its dependency tree are installed, including when
|
|
139
|
+
its group contains other tools. Nested `{include-group = "lint"}` declarations
|
|
140
|
+
are supported.
|
|
141
|
+
|
|
142
|
+
To select a particular group, use:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
uvlazy run --group lint rumdl check .
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`--group` can be repeated. This is also useful when the same tool has different
|
|
149
|
+
requirements in different groups: choose compatible groups explicitly. uv
|
|
150
|
+
validates the selected combination against the project's lockfile.
|
|
151
|
+
|
|
152
|
+
## Lazy Python imports
|
|
153
|
+
|
|
154
|
+
Scripts and modules retain the original import-based behavior:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
uvlazy run app.py --your-argument
|
|
158
|
+
uvlazy run -m your_module
|
|
159
|
+
uvlazy run --group dev app.py
|
|
160
|
+
uvlazy run --locked app.py
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A script starts in a separate environment and installs declared packages when
|
|
164
|
+
execution first reaches their missing imports. Each imported package brings its
|
|
165
|
+
normal dependency tree. The script is never restarted, and exceptions inside
|
|
166
|
+
imported packages propagate normally. Existing modules and your local code take
|
|
167
|
+
precedence.
|
|
168
|
+
|
|
169
|
+
By default, scripts select `[project].dependencies`; `--group` adds groups.
|
|
170
|
+
Mappings handle differing import and distribution names:
|
|
171
|
+
|
|
172
|
+
```toml
|
|
173
|
+
[project]
|
|
174
|
+
name = "my-app"
|
|
175
|
+
version = "0.1.0"
|
|
176
|
+
requires-python = ">=3.11"
|
|
177
|
+
dependencies = ["Pillow>=10"]
|
|
178
|
+
|
|
179
|
+
[tool.uvlazy.imports]
|
|
180
|
+
PIL = "Pillow"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`from PIL import Image` triggers Pillow's installation. An undeclared missing
|
|
184
|
+
import raises `ModuleNotFoundError` normally. Top-level imports count as use,
|
|
185
|
+
even when no functions from the package are subsequently called. Import
|
|
186
|
+
probes such as `importlib.util.find_spec()` can also trigger installation.
|
|
187
|
+
|
|
188
|
+
The [conditional example](examples/conditional/) has `plain`, `pretty`, and
|
|
189
|
+
`image` branches. From this checkout:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
uvlazy run --project examples/conditional examples/conditional/demo.py plain
|
|
193
|
+
uvlazy run --project examples/conditional examples/conditional/demo.py pretty
|
|
194
|
+
uvlazy run --project examples/conditional examples/conditional/demo.py image
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Unlike tool commands, scripts may run without a lockfile. At the first missing
|
|
198
|
+
import, `uv pip compile` resolves all selected requirements and caches the pins.
|
|
199
|
+
That resolution can fetch metadata or build artifacts for unused dependencies,
|
|
200
|
+
and an unsatisfiable unused dependency can prevent it. With `uv.lock`, the pins
|
|
201
|
+
come from `uv export --locked`. Add `--locked` to require and validate the lock
|
|
202
|
+
before any application code runs, even on paths that import nothing.
|
|
203
|
+
|
|
204
|
+
## Environments and caching
|
|
205
|
+
|
|
206
|
+
Environments and constraints live in `.uvlazy/<fingerprint>/`. Each tool package
|
|
207
|
+
and group selection has its own environment, separate from Python script runs.
|
|
208
|
+
Subsequent runs reuse installed packages. Concurrent runs serialize environment
|
|
209
|
+
creation and installation. A warm tool environment can run offline.
|
|
210
|
+
|
|
211
|
+
Changes to `pyproject.toml`, `uv.lock`, local `uv.toml`, the launcher interpreter,
|
|
212
|
+
selected groups, or checkout path select a fresh environment. This avoids
|
|
213
|
+
reusing virtualenv scripts whose absolute paths point to another checkout.
|
|
214
|
+
Existing `.venv` and `uv.lock` files are left alone.
|
|
215
|
+
|
|
216
|
+
For CI, cache uv's download cache to reuse package artifacts across jobs.
|
|
217
|
+
`.uvlazy` environments contain absolute paths and should only be reused at the
|
|
218
|
+
same checkout path with a compatible interpreter. Add `.uvlazy/` to the project's
|
|
219
|
+
`.gitignore`. Delete it while no uvlazy processes are running to start fresh.
|
|
220
|
+
|
|
221
|
+
The native executable also caches its bundled engine under
|
|
222
|
+
`$XDG_CACHE_HOME/uvlazy` (default: `~/Library/Caches/uvlazy` on macOS or
|
|
223
|
+
`~/.cache/uvlazy` on Linux). `UVLAZY_CACHE_DIR` overrides that location. The
|
|
224
|
+
archive is keyed by its contents and recreated locally when needed.
|
|
225
|
+
|
|
226
|
+
## Current boundaries
|
|
227
|
+
|
|
228
|
+
- Tool runs install the tool's normal dependency tree. They do not inject a lazy
|
|
229
|
+
import hook into the executable or automatically install undeclared plugins.
|
|
230
|
+
Tools that need the application installed, such as many pytest workflows,
|
|
231
|
+
still need a complete project environment.
|
|
232
|
+
- Lock export supplies version/source constraints without enforcing lockfile
|
|
233
|
+
artifact hashes. This is not a complete replacement for uv's locked sync.
|
|
234
|
+
- The project itself is not installed. Editable installs and uv sources/workspaces
|
|
235
|
+
remain unsupported; uv sources/workspaces are rejected explicitly.
|
|
236
|
+
- Project optional-dependency groups cannot be selected yet. Extras within a
|
|
237
|
+
selected requirement and environment markers are passed through to uv.
|
|
238
|
+
- For Python scripts, metadata queries, plugin discovery, namespace-package
|
|
239
|
+
mappings, and newly installed `.pth` hooks have no special lazy handling.
|
|
240
|
+
New Python subprocesses do not inherit the import hook.
|
|
241
|
+
- Set `UV_PYTHON` to an installed interpreter compatible with the project and
|
|
242
|
+
selected groups. The default selects an installed Python 3.11+; it does not
|
|
243
|
+
infer the project's Python requirement. Windows is not supported yet.
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
The integration suite generates local wheels and exercises real uv with network
|
|
248
|
+
access disabled, including command execution and selective installation.
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
cargo build --locked
|
|
252
|
+
UVLAZY_TEST_BINARY=target/debug/uvlazy PYTHONPATH=src \
|
|
253
|
+
python3 -m unittest discover -s tests -v
|
|
254
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
255
|
+
cargo fmt --check
|
|
256
|
+
cargo clippy --locked --all-targets -- -D warnings
|
|
257
|
+
ruff check src tests scripts examples
|
|
258
|
+
ruff format --check src tests scripts examples
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`Cargo.toml` is the version source for both registries. maturin uses
|
|
262
|
+
[`bindings = "bin"`](https://www.maturin.rs/bindings.html#bin) to put the native
|
|
263
|
+
executable into platform wheels. Cargo builds embed the Python sources without
|
|
264
|
+
requiring Python at build time; running the installed command needs no checkout
|
|
265
|
+
or separately installed `uvlazy` Python package.
|
|
266
|
+
|
|
267
|
+
Verify both source packages before a release:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
cargo package --locked
|
|
271
|
+
maturin build --release --sdist --locked --out dist
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
CI installs and tests both the Cargo executable and a wheel rebuilt from the
|
|
275
|
+
source distribution, with offline local-wheel fixtures. It covers Linux and
|
|
276
|
+
macOS, Python 3.11 and 3.14, and the minimum and latest uv versions.
|
|
277
|
+
|
|
278
|
+
The release workflow builds Linux and macOS wheels for x86_64 and aarch64, plus
|
|
279
|
+
a source distribution. A matching `v<version>` tag publishes to crates.io and
|
|
280
|
+
PyPI in independent jobs using `CARGO_REGISTRY_TOKEN` and `PYPI_API_TOKEN`
|
|
281
|
+
repository secrets. Manual runs default to `dry_run: true`; publishing requires
|
|
282
|
+
a matching version tag. After both registries succeed, the workflow creates a
|
|
283
|
+
GitHub release with the packages and SHA-256 checksums.
|
|
284
|
+
|
|
285
|
+
The implementation uses uv's [lockfile export](https://docs.astral.sh/uv/concepts/projects/export/)
|
|
286
|
+
and [constraints](https://docs.astral.sh/uv/pip/compile/), and Python's
|
|
287
|
+
[import finder API](https://docs.python.org/3/library/importlib.html).
|
|
288
|
+
|
uvlazy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# uvlazy
|
|
2
|
+
|
|
3
|
+
**Run the tool you need, at the project's locked version, without installing the
|
|
4
|
+
rest of the project.**
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
uvlazy run rumdl check .
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
uvlazy finds the declared package providing `rumdl`, reads the version constraints
|
|
11
|
+
from `uv.lock`, installs that package and its dependency tree into a dedicated
|
|
12
|
+
environment, and runs its executable. Application dependencies and unrelated
|
|
13
|
+
tools stay uninstalled. Python console scripts and native executables both work.
|
|
14
|
+
|
|
15
|
+
This is experimental software for macOS and Linux. It requires Python 3.11+ and
|
|
16
|
+
`uv` 0.6.6+ on PATH. Cargo and PyPI install the same Rust executable, which
|
|
17
|
+
bundles a Python engine with no third-party runtime dependencies.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Install through either registry:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
cargo install uvlazy --locked
|
|
25
|
+
# or
|
|
26
|
+
uv tool install uvlazy
|
|
27
|
+
# or, in a Python environment
|
|
28
|
+
pip install uvlazy
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install from this checkout (both routes require Rust):
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
cargo install --path . --locked
|
|
35
|
+
# or
|
|
36
|
+
uv tool install .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or run directly from source without installing uvlazy:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
PYTHONPATH=/path/to/uvlazy/src python3 -m uvlazy run rumdl check .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The executable uses `uv python find --system --no-project` to locate an
|
|
46
|
+
installed Python 3.11+ without syncing a project. Set `UV_PYTHON` to choose a
|
|
47
|
+
specific interpreter, for example `UV_PYTHON=3.12 uvlazy run rumdl check .`.
|
|
48
|
+
If no compatible Python is installed, run `uv python install 3.11` first.
|
|
49
|
+
|
|
50
|
+
## Use in CI
|
|
51
|
+
|
|
52
|
+
Declare the tool in your project's `pyproject.toml`, alongside your existing
|
|
53
|
+
application dependencies:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[project]
|
|
57
|
+
name = "my-app"
|
|
58
|
+
version = "0.1.0"
|
|
59
|
+
requires-python = ">=3.11"
|
|
60
|
+
dependencies = ["rich>=13", "Pillow>=10", "rumdl"]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
No dependency groups or separate tool configuration are required when the
|
|
64
|
+
command and package names match.
|
|
65
|
+
|
|
66
|
+
Generate or update `uv.lock` during development with `uv lock`, and commit it
|
|
67
|
+
with your project. In CI, with uv and uvlazy available, run:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
uvlazy run rumdl check .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
There is no preceding `uv sync` step. Tool commands require an existing, current
|
|
74
|
+
lockfile; a missing or stale lock fails without updating it. The selected tool's
|
|
75
|
+
exit status, signals, stdin, stdout, stderr, arguments, and working directory
|
|
76
|
+
are preserved, so lint failures fail the job normally. Installation diagnostics
|
|
77
|
+
go to stderr; `--quiet` suppresses installation progress.
|
|
78
|
+
|
|
79
|
+
The [CI example](examples/ci/) includes a lockfile and declares Rich, Pillow, and
|
|
80
|
+
rumdl. Running the following installs only rumdl into the example's environment:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
uvlazy run --project examples/ci rumdl check examples/ci
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`--project` selects the dependency configuration without changing the current
|
|
87
|
+
working directory. Every option for uvlazy goes before the tool name; everything
|
|
88
|
+
after it is passed to the tool unchanged.
|
|
89
|
+
|
|
90
|
+
## Package names and dependency groups
|
|
91
|
+
|
|
92
|
+
Matching command/package names such as `rumdl` need no configuration. For a
|
|
93
|
+
command whose package name differs, select its declared provider explicitly:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
uvlazy run --from httpie http --help
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or save that mapping:
|
|
100
|
+
|
|
101
|
+
```toml
|
|
102
|
+
[dependency-groups]
|
|
103
|
+
dev = ["httpie"]
|
|
104
|
+
|
|
105
|
+
[tool.uvlazy.commands]
|
|
106
|
+
http = "httpie"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then use `uvlazy run http --help`. Providers must be declared in
|
|
110
|
+
`[project].dependencies`, `[dependency-groups]`, or legacy
|
|
111
|
+
`[tool.uv].dev-dependencies`. Unknown commands fail without installing arbitrary
|
|
112
|
+
packages or falling back to executables from the system PATH. The executable must
|
|
113
|
+
belong to the selected package.
|
|
114
|
+
|
|
115
|
+
By default, a tool declared in project dependencies uses those declarations.
|
|
116
|
+
Otherwise, uvlazy automatically selects groups that directly declare the tool.
|
|
117
|
+
Only the requested package and its dependency tree are installed, including when
|
|
118
|
+
its group contains other tools. Nested `{include-group = "lint"}` declarations
|
|
119
|
+
are supported.
|
|
120
|
+
|
|
121
|
+
To select a particular group, use:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
uvlazy run --group lint rumdl check .
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--group` can be repeated. This is also useful when the same tool has different
|
|
128
|
+
requirements in different groups: choose compatible groups explicitly. uv
|
|
129
|
+
validates the selected combination against the project's lockfile.
|
|
130
|
+
|
|
131
|
+
## Lazy Python imports
|
|
132
|
+
|
|
133
|
+
Scripts and modules retain the original import-based behavior:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
uvlazy run app.py --your-argument
|
|
137
|
+
uvlazy run -m your_module
|
|
138
|
+
uvlazy run --group dev app.py
|
|
139
|
+
uvlazy run --locked app.py
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A script starts in a separate environment and installs declared packages when
|
|
143
|
+
execution first reaches their missing imports. Each imported package brings its
|
|
144
|
+
normal dependency tree. The script is never restarted, and exceptions inside
|
|
145
|
+
imported packages propagate normally. Existing modules and your local code take
|
|
146
|
+
precedence.
|
|
147
|
+
|
|
148
|
+
By default, scripts select `[project].dependencies`; `--group` adds groups.
|
|
149
|
+
Mappings handle differing import and distribution names:
|
|
150
|
+
|
|
151
|
+
```toml
|
|
152
|
+
[project]
|
|
153
|
+
name = "my-app"
|
|
154
|
+
version = "0.1.0"
|
|
155
|
+
requires-python = ">=3.11"
|
|
156
|
+
dependencies = ["Pillow>=10"]
|
|
157
|
+
|
|
158
|
+
[tool.uvlazy.imports]
|
|
159
|
+
PIL = "Pillow"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`from PIL import Image` triggers Pillow's installation. An undeclared missing
|
|
163
|
+
import raises `ModuleNotFoundError` normally. Top-level imports count as use,
|
|
164
|
+
even when no functions from the package are subsequently called. Import
|
|
165
|
+
probes such as `importlib.util.find_spec()` can also trigger installation.
|
|
166
|
+
|
|
167
|
+
The [conditional example](examples/conditional/) has `plain`, `pretty`, and
|
|
168
|
+
`image` branches. From this checkout:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
uvlazy run --project examples/conditional examples/conditional/demo.py plain
|
|
172
|
+
uvlazy run --project examples/conditional examples/conditional/demo.py pretty
|
|
173
|
+
uvlazy run --project examples/conditional examples/conditional/demo.py image
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Unlike tool commands, scripts may run without a lockfile. At the first missing
|
|
177
|
+
import, `uv pip compile` resolves all selected requirements and caches the pins.
|
|
178
|
+
That resolution can fetch metadata or build artifacts for unused dependencies,
|
|
179
|
+
and an unsatisfiable unused dependency can prevent it. With `uv.lock`, the pins
|
|
180
|
+
come from `uv export --locked`. Add `--locked` to require and validate the lock
|
|
181
|
+
before any application code runs, even on paths that import nothing.
|
|
182
|
+
|
|
183
|
+
## Environments and caching
|
|
184
|
+
|
|
185
|
+
Environments and constraints live in `.uvlazy/<fingerprint>/`. Each tool package
|
|
186
|
+
and group selection has its own environment, separate from Python script runs.
|
|
187
|
+
Subsequent runs reuse installed packages. Concurrent runs serialize environment
|
|
188
|
+
creation and installation. A warm tool environment can run offline.
|
|
189
|
+
|
|
190
|
+
Changes to `pyproject.toml`, `uv.lock`, local `uv.toml`, the launcher interpreter,
|
|
191
|
+
selected groups, or checkout path select a fresh environment. This avoids
|
|
192
|
+
reusing virtualenv scripts whose absolute paths point to another checkout.
|
|
193
|
+
Existing `.venv` and `uv.lock` files are left alone.
|
|
194
|
+
|
|
195
|
+
For CI, cache uv's download cache to reuse package artifacts across jobs.
|
|
196
|
+
`.uvlazy` environments contain absolute paths and should only be reused at the
|
|
197
|
+
same checkout path with a compatible interpreter. Add `.uvlazy/` to the project's
|
|
198
|
+
`.gitignore`. Delete it while no uvlazy processes are running to start fresh.
|
|
199
|
+
|
|
200
|
+
The native executable also caches its bundled engine under
|
|
201
|
+
`$XDG_CACHE_HOME/uvlazy` (default: `~/Library/Caches/uvlazy` on macOS or
|
|
202
|
+
`~/.cache/uvlazy` on Linux). `UVLAZY_CACHE_DIR` overrides that location. The
|
|
203
|
+
archive is keyed by its contents and recreated locally when needed.
|
|
204
|
+
|
|
205
|
+
## Current boundaries
|
|
206
|
+
|
|
207
|
+
- Tool runs install the tool's normal dependency tree. They do not inject a lazy
|
|
208
|
+
import hook into the executable or automatically install undeclared plugins.
|
|
209
|
+
Tools that need the application installed, such as many pytest workflows,
|
|
210
|
+
still need a complete project environment.
|
|
211
|
+
- Lock export supplies version/source constraints without enforcing lockfile
|
|
212
|
+
artifact hashes. This is not a complete replacement for uv's locked sync.
|
|
213
|
+
- The project itself is not installed. Editable installs and uv sources/workspaces
|
|
214
|
+
remain unsupported; uv sources/workspaces are rejected explicitly.
|
|
215
|
+
- Project optional-dependency groups cannot be selected yet. Extras within a
|
|
216
|
+
selected requirement and environment markers are passed through to uv.
|
|
217
|
+
- For Python scripts, metadata queries, plugin discovery, namespace-package
|
|
218
|
+
mappings, and newly installed `.pth` hooks have no special lazy handling.
|
|
219
|
+
New Python subprocesses do not inherit the import hook.
|
|
220
|
+
- Set `UV_PYTHON` to an installed interpreter compatible with the project and
|
|
221
|
+
selected groups. The default selects an installed Python 3.11+; it does not
|
|
222
|
+
infer the project's Python requirement. Windows is not supported yet.
|
|
223
|
+
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
The integration suite generates local wheels and exercises real uv with network
|
|
227
|
+
access disabled, including command execution and selective installation.
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
cargo build --locked
|
|
231
|
+
UVLAZY_TEST_BINARY=target/debug/uvlazy PYTHONPATH=src \
|
|
232
|
+
python3 -m unittest discover -s tests -v
|
|
233
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
234
|
+
cargo fmt --check
|
|
235
|
+
cargo clippy --locked --all-targets -- -D warnings
|
|
236
|
+
ruff check src tests scripts examples
|
|
237
|
+
ruff format --check src tests scripts examples
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`Cargo.toml` is the version source for both registries. maturin uses
|
|
241
|
+
[`bindings = "bin"`](https://www.maturin.rs/bindings.html#bin) to put the native
|
|
242
|
+
executable into platform wheels. Cargo builds embed the Python sources without
|
|
243
|
+
requiring Python at build time; running the installed command needs no checkout
|
|
244
|
+
or separately installed `uvlazy` Python package.
|
|
245
|
+
|
|
246
|
+
Verify both source packages before a release:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
cargo package --locked
|
|
250
|
+
maturin build --release --sdist --locked --out dist
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
CI installs and tests both the Cargo executable and a wheel rebuilt from the
|
|
254
|
+
source distribution, with offline local-wheel fixtures. It covers Linux and
|
|
255
|
+
macOS, Python 3.11 and 3.14, and the minimum and latest uv versions.
|
|
256
|
+
|
|
257
|
+
The release workflow builds Linux and macOS wheels for x86_64 and aarch64, plus
|
|
258
|
+
a source distribution. A matching `v<version>` tag publishes to crates.io and
|
|
259
|
+
PyPI in independent jobs using `CARGO_REGISTRY_TOKEN` and `PYPI_API_TOKEN`
|
|
260
|
+
repository secrets. Manual runs default to `dry_run: true`; publishing requires
|
|
261
|
+
a matching version tag. After both registries succeed, the workflow creates a
|
|
262
|
+
GitHub release with the packages and SHA-256 checksums.
|
|
263
|
+
|
|
264
|
+
The implementation uses uv's [lockfile export](https://docs.astral.sh/uv/concepts/projects/export/)
|
|
265
|
+
and [constraints](https://docs.astral.sh/uv/pip/compile/), and Python's
|
|
266
|
+
[import finder API](https://docs.python.org/3/library/importlib.html).
|
uvlazy-0.1.0/build.rs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
use std::{env, fmt::Write, fs, path::PathBuf};
|
|
2
|
+
|
|
3
|
+
// Embed the stdlib-only Python engine without requiring Python at build time.
|
|
4
|
+
// Python's own zipfile module materializes it when the executable first runs.
|
|
5
|
+
fn python_string(value: &str) -> String {
|
|
6
|
+
let mut result = String::from("'");
|
|
7
|
+
for character in value.chars() {
|
|
8
|
+
match character {
|
|
9
|
+
'\\' => result.push_str("\\\\"),
|
|
10
|
+
'\'' => result.push_str("\\'"),
|
|
11
|
+
'\n' => result.push_str("\\n"),
|
|
12
|
+
'\r' => result.push_str("\\r"),
|
|
13
|
+
'\t' => result.push_str("\\t"),
|
|
14
|
+
c if c.is_control() => write!(result, "\\U{:08x}", c as u32).unwrap(),
|
|
15
|
+
c => result.push(c),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
result.push('\'');
|
|
19
|
+
result
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn main() {
|
|
23
|
+
println!("cargo:rerun-if-changed=src/uvlazy");
|
|
24
|
+
println!("cargo:rerun-if-changed=scripts/launch.py");
|
|
25
|
+
println!("cargo:rerun-if-changed=Cargo.toml");
|
|
26
|
+
let mut paths: Vec<_> = fs::read_dir("src/uvlazy")
|
|
27
|
+
.unwrap()
|
|
28
|
+
.map(|entry| entry.unwrap().path())
|
|
29
|
+
.filter(|path| path.extension().is_some_and(|extension| extension == "py"))
|
|
30
|
+
.collect();
|
|
31
|
+
paths.sort();
|
|
32
|
+
let mut bootstrap = String::from("files = {\n");
|
|
33
|
+
for path in paths {
|
|
34
|
+
let name = format!("uvlazy/{}", path.file_name().unwrap().to_str().unwrap());
|
|
35
|
+
let source = fs::read_to_string(&path).unwrap();
|
|
36
|
+
writeln!(
|
|
37
|
+
bootstrap,
|
|
38
|
+
" {}: {},",
|
|
39
|
+
python_string(&name),
|
|
40
|
+
python_string(&source)
|
|
41
|
+
)
|
|
42
|
+
.unwrap();
|
|
43
|
+
}
|
|
44
|
+
writeln!(
|
|
45
|
+
bootstrap,
|
|
46
|
+
" 'uvlazy/_version': {},\n}}",
|
|
47
|
+
python_string(&env::var("CARGO_PKG_VERSION").unwrap())
|
|
48
|
+
)
|
|
49
|
+
.unwrap();
|
|
50
|
+
bootstrap.push_str(&fs::read_to_string("scripts/launch.py").unwrap());
|
|
51
|
+
let output = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
|
52
|
+
fs::write(output.join("bootstrap.py"), bootstrap).unwrap();
|
|
53
|
+
}
|