env-module-manager 0.6.0__tar.gz → 0.7.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.
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/PKG-INFO +7 -1
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/README.md +6 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/python-tools.md +16 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/reference.md +17 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/module_manager/__init__.py +1 -1
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/module_manager/cli.py +86 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/module_manager/deploy.py +148 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/pyproject.toml +1 -1
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/tests/test_cli.py +52 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/tests/test_integration.py +3 -3
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/tests/test_modulefile.py +93 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/uv.lock +1 -1
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/.github/workflows/publish.yml +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/.github/workflows/tag.yml +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/.github/workflows/test.yml +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/.gitignore +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/.python-version +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/.readthedocs.yaml +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/AGENTS.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/CONTRIBUTING.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/LICENSE.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/collective-environments.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/configuration.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/development.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/environment-modules.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/index.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/installation.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/rust-tools.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/shell-tools.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/stylesheets/extra.css +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/docs/uninstall.md +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/example.bash +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/mkdocs.yml +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/module_manager/config.py +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/module_manager/modulefile.py +0 -0
- {env_module_manager-0.6.0 → env_module_manager-0.7.0}/tests/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: env-module-manager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Deploy Python, Rust, shell, and collective CLI environments behind GNU environment modulefiles
|
|
5
5
|
Project-URL: Homepage, https://github.com/ejb90/module-manager
|
|
6
6
|
Project-URL: Documentation, https://module-manager.readthedocs.io/
|
|
@@ -88,6 +88,12 @@ module-manager deploy-rust ripgrep 14.1.1 \
|
|
|
88
88
|
--module-root /prod/modulefiles
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
Generate a constraints file for a Python CLI:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
module-manager auto-constraints ruff==0.8.0 --output constraints.txt
|
|
95
|
+
```
|
|
96
|
+
|
|
91
97
|
Deploy a shell script:
|
|
92
98
|
|
|
93
99
|
```sh
|
|
@@ -56,6 +56,12 @@ module-manager deploy-rust ripgrep 14.1.1 \
|
|
|
56
56
|
--module-root /prod/modulefiles
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Generate a constraints file for a Python CLI:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
module-manager auto-constraints ruff==0.8.0 --output constraints.txt
|
|
63
|
+
```
|
|
64
|
+
|
|
59
65
|
Deploy a shell script:
|
|
60
66
|
|
|
61
67
|
```sh
|
|
@@ -70,6 +70,22 @@ module-manager deploy-python gitconductor 0.1.0 \
|
|
|
70
70
|
|
|
71
71
|
`--constraints` may be used more than once.
|
|
72
72
|
|
|
73
|
+
## Generate Constraint Files
|
|
74
|
+
|
|
75
|
+
Use `auto-constraints` to compile a package and its dependencies into a
|
|
76
|
+
constraints file:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
module-manager auto-constraints gitconductor==0.7.0 \
|
|
80
|
+
--index https://packages.example/simple \
|
|
81
|
+
--output constraints.txt
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The command runs `uv pip compile` and writes `constraints.txt` by default. If uv
|
|
85
|
+
reports a transitive URL dependency, `module-manager` adds the suggested
|
|
86
|
+
`name @ URL` requirement and retries until all URL dependencies have been
|
|
87
|
+
included or uv reports an unrecoverable error.
|
|
88
|
+
|
|
73
89
|
## Use a uv Config File
|
|
74
90
|
|
|
75
91
|
Pass a specific uv configuration file to `uv tool install`:
|
|
@@ -30,6 +30,12 @@ Show collective environment deployment options:
|
|
|
30
30
|
module-manager deploy-env --help
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Show constraint generation options:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
module-manager auto-constraints --help
|
|
37
|
+
```
|
|
38
|
+
|
|
33
39
|
Show uninstall options:
|
|
34
40
|
|
|
35
41
|
```sh
|
|
@@ -52,6 +58,17 @@ module-manager deploy-python NAME VERSION \
|
|
|
52
58
|
[--execute-install]
|
|
53
59
|
```
|
|
54
60
|
|
|
61
|
+
## Auto-Constraints Command
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
module-manager auto-constraints PACKAGE \
|
|
65
|
+
[--output PATH] \
|
|
66
|
+
[--python PYTHON] \
|
|
67
|
+
[--index URL] \
|
|
68
|
+
[--find-links PATH_OR_URL] \
|
|
69
|
+
[--uv-config-file PATH]
|
|
70
|
+
```
|
|
71
|
+
|
|
55
72
|
## Rust Command
|
|
56
73
|
|
|
57
74
|
```sh
|
|
@@ -12,12 +12,14 @@ from click import Command
|
|
|
12
12
|
from . import __version__
|
|
13
13
|
from .config import AppConfig, load_config
|
|
14
14
|
from .deploy import (
|
|
15
|
+
ConstraintGenerationError,
|
|
15
16
|
EnvironmentDeploymentResult,
|
|
16
17
|
UninstallResult,
|
|
17
18
|
deploy_environment,
|
|
18
19
|
deploy_python_tool,
|
|
19
20
|
deploy_rust_tool,
|
|
20
21
|
deploy_script_tool,
|
|
22
|
+
generate_constraints,
|
|
21
23
|
load_environment_spec,
|
|
22
24
|
uninstall_tool,
|
|
23
25
|
)
|
|
@@ -41,6 +43,7 @@ TOP_LEVEL_EXAMPLES = (
|
|
|
41
43
|
"--prefix /prod/tools --module-root /prod/modulefiles[/cyan]\n\n"
|
|
42
44
|
"[cyan]module-manager deploy-script my-tool 1.0.0 --script ./scripts/my-tool "
|
|
43
45
|
"--prefix /prod/tools --module-root /prod/modulefiles[/cyan]\n\n"
|
|
46
|
+
"[cyan]module-manager auto-constraints ruff==0.8.0 --output constraints.txt[/cyan]\n\n"
|
|
44
47
|
"[cyan]module-manager deploy-env --file dev-tools.toml[/cyan]\n\n"
|
|
45
48
|
"[cyan]module-manager uninstall ruff 0.8.0 --prefix /prod/tools --module-root /prod/modulefiles[/cyan]"
|
|
46
49
|
)
|
|
@@ -76,6 +79,13 @@ DEPLOY_ENV_EXAMPLES = (
|
|
|
76
79
|
"--module-root /scratch/modulefiles --dry-run[/cyan]"
|
|
77
80
|
)
|
|
78
81
|
|
|
82
|
+
AUTO_CONSTRAINTS_EXAMPLES = (
|
|
83
|
+
"Examples:\n\n"
|
|
84
|
+
"[cyan]module-manager auto-constraints gitconductor==0.7.0[/cyan]\n\n"
|
|
85
|
+
"[cyan]module-manager auto-constraints internal-tool==1.2.3 --index https://packages.example/simple "
|
|
86
|
+
"--output /prod/constraints/internal-tool.txt[/cyan]"
|
|
87
|
+
)
|
|
88
|
+
|
|
79
89
|
UNINSTALL_EXAMPLES = (
|
|
80
90
|
"Examples:\n\n"
|
|
81
91
|
"[cyan]module-manager uninstall ruff 0.8.0 --prefix /prod/tools "
|
|
@@ -283,6 +293,82 @@ def deploy_python(
|
|
|
283
293
|
)
|
|
284
294
|
|
|
285
295
|
|
|
296
|
+
@main.command(
|
|
297
|
+
"auto-constraints",
|
|
298
|
+
help="Generate constraints for a Python package with uv.",
|
|
299
|
+
epilog=AUTO_CONSTRAINTS_EXAMPLES,
|
|
300
|
+
)
|
|
301
|
+
@click.argument("package")
|
|
302
|
+
@click.option(
|
|
303
|
+
"--output",
|
|
304
|
+
"output_file",
|
|
305
|
+
type=PATH,
|
|
306
|
+
default=Path("constraints.txt"),
|
|
307
|
+
show_default=True,
|
|
308
|
+
help="Constraints file to write.",
|
|
309
|
+
)
|
|
310
|
+
@click.option("--python", "python", help="Python interpreter/version passed to uv.")
|
|
311
|
+
@click.option(
|
|
312
|
+
"--index",
|
|
313
|
+
"indexes",
|
|
314
|
+
multiple=True,
|
|
315
|
+
help="Additional package index URL passed to uv. May be used more than once.",
|
|
316
|
+
)
|
|
317
|
+
@click.option(
|
|
318
|
+
"--find-links",
|
|
319
|
+
"find_links",
|
|
320
|
+
multiple=True,
|
|
321
|
+
help="Directory or HTML page of packages passed to uv. May be used more than once.",
|
|
322
|
+
)
|
|
323
|
+
@click.option(
|
|
324
|
+
"--uv-config-file",
|
|
325
|
+
type=PATH,
|
|
326
|
+
help="uv.toml file passed to uv with --config-file.",
|
|
327
|
+
)
|
|
328
|
+
@click.pass_obj
|
|
329
|
+
def auto_constraints(
|
|
330
|
+
config: AppConfig,
|
|
331
|
+
package: str,
|
|
332
|
+
output_file: Path,
|
|
333
|
+
python: str | None,
|
|
334
|
+
indexes: tuple[str, ...],
|
|
335
|
+
find_links: tuple[str, ...],
|
|
336
|
+
uv_config_file: Path | None,
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Generate a constraints file for a Python package.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
config: Resolved application configuration from the Click context.
|
|
342
|
+
package: Package requirement to compile.
|
|
343
|
+
output_file: Constraints file to write.
|
|
344
|
+
python: Optional Python interpreter or version passed to uv.
|
|
345
|
+
indexes: Additional package index URLs passed to uv.
|
|
346
|
+
find_links: Wheelhouse directories or HTML package pages passed to uv.
|
|
347
|
+
uv_config_file: Optional uv configuration file passed to uv.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
click.ClickException: If uv cannot generate constraints.
|
|
351
|
+
MissingExecutableError: If uv is not on `PATH`.
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
result = generate_constraints(
|
|
355
|
+
package=package,
|
|
356
|
+
output_file=output_file.expanduser(),
|
|
357
|
+
python=python,
|
|
358
|
+
indexes=indexes or config.indexes,
|
|
359
|
+
find_links=find_links or config.find_links,
|
|
360
|
+
uv_config_file=(uv_config_file.expanduser() if uv_config_file else config.uv_config_file),
|
|
361
|
+
)
|
|
362
|
+
except ConstraintGenerationError as error:
|
|
363
|
+
raise click.ClickException(str(error)) from error
|
|
364
|
+
|
|
365
|
+
click.echo(f"constraints: {result.output_file}")
|
|
366
|
+
if result.discovered_url_dependencies:
|
|
367
|
+
click.echo(f"discovered URL dependencies: {len(result.discovered_url_dependencies)}")
|
|
368
|
+
for requirement in result.discovered_url_dependencies:
|
|
369
|
+
click.echo(f" {requirement}")
|
|
370
|
+
|
|
371
|
+
|
|
286
372
|
@main.command(
|
|
287
373
|
"deploy-rust",
|
|
288
374
|
help="Copy a Rust CLI binary and write a modulefile for it.",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import shlex
|
|
7
8
|
import shutil
|
|
8
9
|
import stat
|
|
@@ -20,6 +21,10 @@ class MissingExecutableError(RuntimeError):
|
|
|
20
21
|
"""Raised when a required external executable is unavailable."""
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
class ConstraintGenerationError(RuntimeError):
|
|
25
|
+
"""Raised when uv cannot generate constraints after URL discovery."""
|
|
26
|
+
|
|
27
|
+
|
|
23
28
|
@dataclass(frozen=True)
|
|
24
29
|
class DeploymentPaths:
|
|
25
30
|
"""Filesystem paths created or targeted by a deployment.
|
|
@@ -126,6 +131,26 @@ class EnvironmentDeploymentResult:
|
|
|
126
131
|
actions: tuple[str, ...]
|
|
127
132
|
|
|
128
133
|
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class ConstraintGenerationResult:
|
|
136
|
+
"""Result of generating a Python constraints file.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
output_file: Constraints file written by uv.
|
|
140
|
+
requirements: Initial and discovered direct requirements compiled.
|
|
141
|
+
discovered_url_dependencies: URL requirements discovered from uv errors.
|
|
142
|
+
iterations: Number of uv compile attempts.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
output_file: Path
|
|
146
|
+
requirements: tuple[str, ...]
|
|
147
|
+
discovered_url_dependencies: tuple[str, ...]
|
|
148
|
+
iterations: int
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
URL_DEPENDENCY_RE = re.compile(r"`([^`]+?\s@\s[^`]+?)`")
|
|
152
|
+
|
|
153
|
+
|
|
129
154
|
def deployment_paths(module_root: Path, prefix: Path, name: str, version: str) -> DeploymentPaths:
|
|
130
155
|
"""Compute versioned installation and modulefile paths for a tool.
|
|
131
156
|
|
|
@@ -184,6 +209,129 @@ def uv_install_command(
|
|
|
184
209
|
return command
|
|
185
210
|
|
|
186
211
|
|
|
212
|
+
def uv_compile_command(
|
|
213
|
+
output_file: Path,
|
|
214
|
+
python: str | None = None,
|
|
215
|
+
indexes: tuple[str, ...] = (),
|
|
216
|
+
find_links: tuple[str, ...] = (),
|
|
217
|
+
uv_config_file: Path | None = None,
|
|
218
|
+
) -> list[str]:
|
|
219
|
+
"""Build the uv command used to compile Python constraints.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
output_file: Destination constraints file.
|
|
223
|
+
python: Optional Python interpreter or version passed to uv.
|
|
224
|
+
indexes: Additional package index URLs.
|
|
225
|
+
find_links: Wheelhouse directories or HTML package pages.
|
|
226
|
+
uv_config_file: Optional uv configuration file passed to `uv`.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tokenized uv command suitable for `subprocess.run`.
|
|
230
|
+
"""
|
|
231
|
+
command = ["uv"]
|
|
232
|
+
if uv_config_file:
|
|
233
|
+
command.extend(["--config-file", str(uv_config_file)])
|
|
234
|
+
command.extend(
|
|
235
|
+
[
|
|
236
|
+
"pip",
|
|
237
|
+
"compile",
|
|
238
|
+
"-",
|
|
239
|
+
"--output-file",
|
|
240
|
+
str(output_file),
|
|
241
|
+
"--no-header",
|
|
242
|
+
"--no-annotate",
|
|
243
|
+
]
|
|
244
|
+
)
|
|
245
|
+
if python:
|
|
246
|
+
command.extend(["--python", python])
|
|
247
|
+
for index in indexes:
|
|
248
|
+
command.extend(["--index", index])
|
|
249
|
+
for link in find_links:
|
|
250
|
+
command.extend(["--find-links", link])
|
|
251
|
+
return command
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def discover_url_requirements(output: str) -> tuple[str, ...]:
|
|
255
|
+
"""Extract uv-suggested direct URL requirements from compiler output.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
output: Combined stdout and stderr from `uv pip compile`.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Direct URL requirements in discovery order.
|
|
262
|
+
"""
|
|
263
|
+
requirements: list[str] = []
|
|
264
|
+
for match in URL_DEPENDENCY_RE.findall(output):
|
|
265
|
+
if match not in requirements:
|
|
266
|
+
requirements.append(match)
|
|
267
|
+
return tuple(requirements)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def generate_constraints(
|
|
271
|
+
*,
|
|
272
|
+
package: str,
|
|
273
|
+
output_file: Path,
|
|
274
|
+
python: str | None = None,
|
|
275
|
+
indexes: tuple[str, ...] = (),
|
|
276
|
+
find_links: tuple[str, ...] = (),
|
|
277
|
+
uv_config_file: Path | None = None,
|
|
278
|
+
max_iterations: int = 20,
|
|
279
|
+
) -> ConstraintGenerationResult:
|
|
280
|
+
"""Generate a constraints file, discovering transitive URL dependencies.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
package: Initial package requirement to compile.
|
|
284
|
+
output_file: Destination constraints file.
|
|
285
|
+
python: Optional Python interpreter or version passed to uv.
|
|
286
|
+
indexes: Additional package index URLs passed to uv.
|
|
287
|
+
find_links: Wheelhouse directories or HTML package pages passed to uv.
|
|
288
|
+
uv_config_file: Optional uv configuration file passed to `uv`.
|
|
289
|
+
max_iterations: Maximum uv compile attempts before failing.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Constraints generation result.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
MissingExecutableError: If uv is not on `PATH`.
|
|
296
|
+
ConstraintGenerationError: If uv fails without a new URL dependency to
|
|
297
|
+
add, or if the iteration limit is reached.
|
|
298
|
+
"""
|
|
299
|
+
require_executable("uv")
|
|
300
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
requirements = [package]
|
|
302
|
+
discovered: list[str] = []
|
|
303
|
+
command = uv_compile_command(output_file, python, indexes, find_links, uv_config_file)
|
|
304
|
+
|
|
305
|
+
for iteration in range(1, max_iterations + 1):
|
|
306
|
+
completed = subprocess.run(
|
|
307
|
+
command,
|
|
308
|
+
check=False,
|
|
309
|
+
input="\n".join(requirements) + "\n",
|
|
310
|
+
capture_output=True,
|
|
311
|
+
text=True,
|
|
312
|
+
)
|
|
313
|
+
if completed.returncode == 0:
|
|
314
|
+
return ConstraintGenerationResult(
|
|
315
|
+
output_file=output_file,
|
|
316
|
+
requirements=tuple(requirements),
|
|
317
|
+
discovered_url_dependencies=tuple(discovered),
|
|
318
|
+
iterations=iteration,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
combined_output = f"{completed.stdout}\n{completed.stderr}"
|
|
322
|
+
new_requirements = [
|
|
323
|
+
requirement for requirement in discover_url_requirements(combined_output) if requirement not in requirements
|
|
324
|
+
]
|
|
325
|
+
if not new_requirements:
|
|
326
|
+
msg = combined_output.strip() or f"uv pip compile failed with exit code {completed.returncode}"
|
|
327
|
+
raise ConstraintGenerationError(msg)
|
|
328
|
+
requirements.extend(new_requirements)
|
|
329
|
+
discovered.extend(new_requirements)
|
|
330
|
+
|
|
331
|
+
msg = f"uv pip compile did not converge after {max_iterations} attempts"
|
|
332
|
+
raise ConstraintGenerationError(msg)
|
|
333
|
+
|
|
334
|
+
|
|
187
335
|
def uv_install_environment(tool_dir: Path, bin_dir: Path) -> dict[str, str]:
|
|
188
336
|
"""Build an isolated environment for a managed uv tool installation.
|
|
189
337
|
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
import pytest
|
|
6
7
|
from click.testing import CliRunner
|
|
7
8
|
|
|
8
9
|
from module_manager.cli import main
|
|
10
|
+
from module_manager.deploy import ConstraintGenerationResult
|
|
9
11
|
|
|
10
12
|
ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
11
13
|
|
|
@@ -135,6 +137,56 @@ def test_deploy_command_rejects_matching_prefix_and_module_root() -> None:
|
|
|
135
137
|
assert not collision_path_exists
|
|
136
138
|
|
|
137
139
|
|
|
140
|
+
def test_auto_constraints_command_writes_output(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
141
|
+
"""The auto-constraints command should report the generated file.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
monkeypatch: Pytest helper used to replace constraint generation.
|
|
145
|
+
"""
|
|
146
|
+
runner = CliRunner()
|
|
147
|
+
calls: list[dict[str, object]] = []
|
|
148
|
+
|
|
149
|
+
def fake_generate_constraints(**kwargs: object) -> ConstraintGenerationResult:
|
|
150
|
+
"""Record generation options."""
|
|
151
|
+
calls.append(kwargs)
|
|
152
|
+
return ConstraintGenerationResult(
|
|
153
|
+
output_file=Path("constraints.txt"),
|
|
154
|
+
requirements=("url-tool", "url-dep @ file:///tmp/url-dep"),
|
|
155
|
+
discovered_url_dependencies=("url-dep @ file:///tmp/url-dep",),
|
|
156
|
+
iterations=2,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
monkeypatch.setattr("module_manager.cli.generate_constraints", fake_generate_constraints)
|
|
160
|
+
|
|
161
|
+
with runner.isolated_filesystem():
|
|
162
|
+
result = runner.invoke(
|
|
163
|
+
main,
|
|
164
|
+
[
|
|
165
|
+
"auto-constraints",
|
|
166
|
+
"url-tool",
|
|
167
|
+
"--index",
|
|
168
|
+
"https://packages.example/simple",
|
|
169
|
+
"--output",
|
|
170
|
+
"constraints.txt",
|
|
171
|
+
],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert result.exit_code == 0
|
|
175
|
+
assert calls == [
|
|
176
|
+
{
|
|
177
|
+
"package": "url-tool",
|
|
178
|
+
"output_file": Path("constraints.txt"),
|
|
179
|
+
"python": None,
|
|
180
|
+
"indexes": ("https://packages.example/simple",),
|
|
181
|
+
"find_links": (),
|
|
182
|
+
"uv_config_file": None,
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
assert "constraints: constraints.txt" in result.output
|
|
186
|
+
assert "discovered URL dependencies: 1" in result.output
|
|
187
|
+
assert "url-dep @ file:///tmp/url-dep" in result.output
|
|
188
|
+
|
|
189
|
+
|
|
138
190
|
def test_cli_options_override_config_defaults() -> None:
|
|
139
191
|
"""CLI options should take precedence over configured defaults."""
|
|
140
192
|
runner = CliRunner()
|
|
@@ -30,7 +30,7 @@ def test_deploy_python_real_uv_install_uses_cli_roots(tmp_path: Path, monkeypatc
|
|
|
30
30
|
[
|
|
31
31
|
"deploy-python",
|
|
32
32
|
"module-manager",
|
|
33
|
-
"0.
|
|
33
|
+
"0.7.0",
|
|
34
34
|
"--package",
|
|
35
35
|
str(project_root),
|
|
36
36
|
"--prefix",
|
|
@@ -41,11 +41,11 @@ def test_deploy_python_real_uv_install_uses_cli_roots(tmp_path: Path, monkeypatc
|
|
|
41
41
|
],
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
install_root = prefix / "module-manager" / "0.
|
|
44
|
+
install_root = prefix / "module-manager" / "0.7.0"
|
|
45
45
|
assert result.exit_code == 0, result.output
|
|
46
46
|
assert (install_root / "bin" / "module-manager").exists()
|
|
47
47
|
assert (install_root / "uv-tools" / "env-module-manager" / "uv-receipt.toml").exists()
|
|
48
|
-
assert (module_root / "module-manager" / "0.
|
|
48
|
+
assert (module_root / "module-manager" / "0.7.0").is_file()
|
|
49
49
|
assert not inherited_tool_dir.exists()
|
|
50
50
|
assert not inherited_bin_dir.exists()
|
|
51
51
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Modulefile rendering and deployment helper tests."""
|
|
2
2
|
|
|
3
|
+
import subprocess
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from shlex import quote as shlex_quote
|
|
5
6
|
|
|
@@ -11,9 +12,12 @@ from module_manager.deploy import (
|
|
|
11
12
|
deploy_python_tool,
|
|
12
13
|
deploy_rust_tool,
|
|
13
14
|
deploy_script_tool,
|
|
15
|
+
discover_url_requirements,
|
|
16
|
+
generate_constraints,
|
|
14
17
|
load_environment_spec,
|
|
15
18
|
require_executable,
|
|
16
19
|
uninstall_tool,
|
|
20
|
+
uv_compile_command,
|
|
17
21
|
uv_install_command,
|
|
18
22
|
uv_install_environment,
|
|
19
23
|
)
|
|
@@ -126,6 +130,95 @@ def test_python_tool_install_command_accepts_uv_config_file() -> None:
|
|
|
126
130
|
]
|
|
127
131
|
|
|
128
132
|
|
|
133
|
+
def test_uv_compile_command_accepts_sources_and_config() -> None:
|
|
134
|
+
"""Uv compile commands should include resolver and config options."""
|
|
135
|
+
assert uv_compile_command(
|
|
136
|
+
Path("/prod/constraints.txt"),
|
|
137
|
+
python="3.12",
|
|
138
|
+
indexes=("https://packages.example/simple",),
|
|
139
|
+
find_links=("/prod/wheels",),
|
|
140
|
+
uv_config_file=Path("/prod/uv.toml"),
|
|
141
|
+
) == [
|
|
142
|
+
"uv",
|
|
143
|
+
"--config-file",
|
|
144
|
+
"/prod/uv.toml",
|
|
145
|
+
"pip",
|
|
146
|
+
"compile",
|
|
147
|
+
"-",
|
|
148
|
+
"--output-file",
|
|
149
|
+
"/prod/constraints.txt",
|
|
150
|
+
"--no-header",
|
|
151
|
+
"--no-annotate",
|
|
152
|
+
"--python",
|
|
153
|
+
"3.12",
|
|
154
|
+
"--index",
|
|
155
|
+
"https://packages.example/simple",
|
|
156
|
+
"--find-links",
|
|
157
|
+
"/prod/wheels",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_discover_url_requirements_extracts_uv_suggestions() -> None:
|
|
162
|
+
"""Uv URL dependency suggestions should be parsed from errors."""
|
|
163
|
+
output = """
|
|
164
|
+
Package `url-dep` was included as a URL dependency. URL dependencies
|
|
165
|
+
must be expressed as direct requirements or constraints. Consider adding
|
|
166
|
+
`url-dep @ file:///tmp/url-dep` to your dependencies or constraints file.
|
|
167
|
+
""".strip()
|
|
168
|
+
|
|
169
|
+
assert discover_url_requirements(output) == ("url-dep @ file:///tmp/url-dep",)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_generate_constraints_retries_discovered_url_dependencies(
|
|
173
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Constraint generation should retry with uv-suggested URL requirements.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
tmp_path: Temporary deployment root.
|
|
179
|
+
monkeypatch: Pytest helper used to replace process execution.
|
|
180
|
+
"""
|
|
181
|
+
calls: list[tuple[list[str], str]] = []
|
|
182
|
+
|
|
183
|
+
def fake_run(
|
|
184
|
+
command: list[str],
|
|
185
|
+
check: bool,
|
|
186
|
+
input: str,
|
|
187
|
+
capture_output: bool,
|
|
188
|
+
text: bool,
|
|
189
|
+
) -> subprocess.CompletedProcess[str]:
|
|
190
|
+
"""Fail once with a URL suggestion, then write constraints."""
|
|
191
|
+
assert not check
|
|
192
|
+
assert capture_output
|
|
193
|
+
assert text
|
|
194
|
+
calls.append((command, input))
|
|
195
|
+
if len(calls) == 1:
|
|
196
|
+
return subprocess.CompletedProcess(
|
|
197
|
+
command,
|
|
198
|
+
1,
|
|
199
|
+
stdout="",
|
|
200
|
+
stderr=("Consider adding `url-dep @ file:///tmp/url-dep` to your dependencies or constraints file."),
|
|
201
|
+
)
|
|
202
|
+
output_file = tmp_path / "constraints.txt"
|
|
203
|
+
output_file.write_text("url-dep @ file:///tmp/url-dep\nurl-tool==1.0.0\n", encoding="utf-8")
|
|
204
|
+
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
|
205
|
+
|
|
206
|
+
monkeypatch.setattr("module_manager.deploy.require_executable", lambda _name: "uv")
|
|
207
|
+
monkeypatch.setattr("module_manager.deploy.subprocess.run", fake_run)
|
|
208
|
+
|
|
209
|
+
result = generate_constraints(package="url-tool", output_file=tmp_path / "constraints.txt")
|
|
210
|
+
|
|
211
|
+
assert result.output_file == tmp_path / "constraints.txt"
|
|
212
|
+
assert result.requirements == ("url-tool", "url-dep @ file:///tmp/url-dep")
|
|
213
|
+
assert result.discovered_url_dependencies == ("url-dep @ file:///tmp/url-dep",)
|
|
214
|
+
assert result.iterations == 2
|
|
215
|
+
assert calls[0][1] == "url-tool\n"
|
|
216
|
+
assert calls[1][1] == "url-tool\nurl-dep @ file:///tmp/url-dep\n"
|
|
217
|
+
assert (tmp_path / "constraints.txt").read_text(encoding="utf-8") == (
|
|
218
|
+
"url-dep @ file:///tmp/url-dep\nurl-tool==1.0.0\n"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
129
222
|
def test_python_tool_install_command_accepts_url_dependency() -> None:
|
|
130
223
|
"""Uv install commands should pass direct URL dependencies unchanged."""
|
|
131
224
|
package = "mytool @ https://packages.example/files/mytool-1.0.0-py3-none-any.whl"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|