env-module-manager 0.5.0__tar.gz → 0.6.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.
Files changed (37) hide show
  1. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/PKG-INFO +2 -1
  2. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/README.md +1 -0
  3. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/collective-environments.md +2 -0
  4. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/python-tools.md +15 -0
  5. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/reference.md +1 -0
  6. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/module_manager/__init__.py +1 -1
  7. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/module_manager/cli.py +9 -0
  8. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/module_manager/deploy.py +35 -1
  9. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/pyproject.toml +1 -1
  10. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/tests/test_cli.py +5 -0
  11. env_module_manager-0.6.0/tests/test_integration.py +138 -0
  12. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/tests/test_modulefile.py +46 -0
  13. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/uv.lock +1 -1
  14. env_module_manager-0.5.0/tests/test_integration.py +0 -49
  15. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/.github/workflows/publish.yml +0 -0
  16. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/.github/workflows/tag.yml +0 -0
  17. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/.github/workflows/test.yml +0 -0
  18. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/.gitignore +0 -0
  19. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/.python-version +0 -0
  20. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/.readthedocs.yaml +0 -0
  21. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/AGENTS.md +0 -0
  22. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/CONTRIBUTING.md +0 -0
  23. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/LICENSE.md +0 -0
  24. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/configuration.md +0 -0
  25. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/development.md +0 -0
  26. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/environment-modules.md +0 -0
  27. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/index.md +0 -0
  28. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/installation.md +0 -0
  29. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/rust-tools.md +0 -0
  30. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/shell-tools.md +0 -0
  31. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/stylesheets/extra.css +0 -0
  32. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/docs/uninstall.md +0 -0
  33. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/example.bash +0 -0
  34. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/mkdocs.yml +0 -0
  35. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/module_manager/config.py +0 -0
  36. {env_module_manager-0.5.0 → env_module_manager-0.6.0}/module_manager/modulefile.py +0 -0
  37. {env_module_manager-0.5.0 → env_module_manager-0.6.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.5.0
3
+ Version: 0.6.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/
@@ -73,6 +73,7 @@ Deploy a Python CLI with `uv tool install`:
73
73
  ```sh
74
74
  module-manager deploy-python ruff 0.8.0 \
75
75
  --package 'ruff==0.8.0' \
76
+ --constraints /prod/constraints/runtime.txt \
76
77
  --prefix /prod/tools \
77
78
  --module-root /prod/modulefiles \
78
79
  --execute-install
@@ -41,6 +41,7 @@ Deploy a Python CLI with `uv tool install`:
41
41
  ```sh
42
42
  module-manager deploy-python ruff 0.8.0 \
43
43
  --package 'ruff==0.8.0' \
44
+ --constraints /prod/constraints/runtime.txt \
44
45
  --prefix /prod/tools \
45
46
  --module-root /prod/modulefiles \
46
47
  --execute-install
@@ -22,6 +22,7 @@ package = "ruff==0.8.0"
22
22
  python = "3.12"
23
23
  indexes = ["https://packages.example/simple"]
24
24
  find_links = ["/prod/wheels"]
25
+ constraints = ["/prod/constraints/runtime.txt"]
25
26
  uv_config_file = "/prod/config/uv.toml"
26
27
 
27
28
  [[tools]]
@@ -59,6 +60,7 @@ module load dev-tools
59
60
  ```
60
61
 
61
62
  Relative `binary` and `script` paths are resolved relative to the manifest file.
63
+ Relative Python `constraints` paths are resolved the same way.
62
64
 
63
65
  ## Preview
64
66
 
@@ -55,6 +55,21 @@ module-manager deploy-python gitconductor 0.1.0 \
55
55
 
56
56
  `--find-links` may be used more than once.
57
57
 
58
+ ## Use Constraint Files
59
+
60
+ Pass requirement constraint files to `uv tool install`:
61
+
62
+ ```sh
63
+ module-manager deploy-python gitconductor 0.1.0 \
64
+ --package gitconductor==0.1.0 \
65
+ --constraints /prod/constraints/runtime.txt \
66
+ --prefix /prod/tools \
67
+ --module-root /prod/modulefiles \
68
+ --execute-install
69
+ ```
70
+
71
+ `--constraints` may be used more than once.
72
+
58
73
  ## Use a uv Config File
59
74
 
60
75
  Pass a specific uv configuration file to `uv tool install`:
@@ -44,6 +44,7 @@ module-manager deploy-python NAME VERSION \
44
44
  [--python PYTHON] \
45
45
  [--index URL] \
46
46
  [--find-links PATH_OR_URL] \
47
+ [--constraints PATH] \
47
48
  [--uv-config-file PATH] \
48
49
  [--prefix PATH] \
49
50
  [--module-root PATH] \
@@ -1,3 +1,3 @@
1
1
  """Deployment helpers for HPC environment module files."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -199,6 +199,12 @@ def main(ctx: click.Context, config: Path | None) -> None:
199
199
  multiple=True,
200
200
  help="Directory or HTML page of packages passed to uv. May be used more than once.",
201
201
  )
202
+ @click.option(
203
+ "--constraints",
204
+ "constraints",
205
+ multiple=True,
206
+ help="Requirements constraint file passed to uv. May be used more than once.",
207
+ )
202
208
  @click.option(
203
209
  "--uv-config-file",
204
210
  type=PATH,
@@ -223,6 +229,7 @@ def deploy_python(
223
229
  python: str | None,
224
230
  indexes: tuple[str, ...],
225
231
  find_links: tuple[str, ...],
232
+ constraints: tuple[str, ...],
226
233
  uv_config_file: Path | None,
227
234
  execute_install: bool,
228
235
  ) -> None:
@@ -241,6 +248,7 @@ def deploy_python(
241
248
  python: Optional Python interpreter or version passed to uv.
242
249
  indexes: Additional package index URLs passed to uv.
243
250
  find_links: Wheelhouse directories or HTML package pages passed to uv.
251
+ constraints: Requirements constraint files passed to uv.
244
252
  uv_config_file: Optional uv configuration file passed to `uv tool`.
245
253
  execute_install: Whether to run `uv tool install` immediately.
246
254
 
@@ -265,6 +273,7 @@ def deploy_python(
265
273
  python=python,
266
274
  indexes=indexes or config.indexes,
267
275
  find_links=find_links or config.find_links,
276
+ constraints=constraints,
268
277
  uv_config_file=(uv_config_file.expanduser() if uv_config_file else config.uv_config_file),
269
278
  execute_install=execute_install,
270
279
  make_default=make_default,
@@ -68,6 +68,7 @@ class EnvironmentToolSpec:
68
68
  python: Optional Python interpreter or version passed to uv.
69
69
  indexes: Additional package index URLs passed to uv.
70
70
  find_links: Wheelhouse directories or HTML package pages passed to uv.
71
+ constraints: Constraint requirement files passed to uv.
71
72
  description: Optional tool description.
72
73
  homepage: Optional upstream homepage.
73
74
  """
@@ -82,6 +83,7 @@ class EnvironmentToolSpec:
82
83
  python: str | None = None
83
84
  indexes: tuple[str, ...] = ()
84
85
  find_links: tuple[str, ...] = ()
86
+ constraints: tuple[str, ...] = ()
85
87
  description: str | None = None
86
88
  homepage: str | None = None
87
89
 
@@ -150,6 +152,7 @@ def uv_install_command(
150
152
  python: str | None = None,
151
153
  indexes: tuple[str, ...] = (),
152
154
  find_links: tuple[str, ...] = (),
155
+ constraints: tuple[str, ...] = (),
153
156
  uv_config_file: Path | None = None,
154
157
  ) -> list[str]:
155
158
  """Build the uv command used to install a Python CLI tool.
@@ -159,6 +162,7 @@ def uv_install_command(
159
162
  python: Optional Python interpreter or version passed to uv.
160
163
  indexes: Additional package index URLs.
161
164
  find_links: Wheelhouse directories or HTML package pages.
165
+ constraints: Constraint requirement files.
162
166
  uv_config_file: Optional uv configuration file passed to `uv tool`.
163
167
 
164
168
  Returns:
@@ -174,6 +178,8 @@ def uv_install_command(
174
178
  command.extend(["--index", index])
175
179
  for link in find_links:
176
180
  command.extend(["--find-links", link])
181
+ for constraint in constraints:
182
+ command.extend(["--constraints", constraint])
177
183
  command.append(package)
178
184
  return command
179
185
 
@@ -400,6 +406,29 @@ def optional_manifest_path(data: dict[str, Any], key: str, base_dir: Path | None
400
406
  return path
401
407
 
402
408
 
409
+ def optional_manifest_paths(data: dict[str, Any], key: str, base_dir: Path | None = None) -> tuple[Path, ...]:
410
+ """Read optional paths from manifest data.
411
+
412
+ Args:
413
+ data: Manifest table.
414
+ key: Optional path-list key.
415
+ base_dir: Directory used to resolve relative paths.
416
+
417
+ Returns:
418
+ Expanded path values, or an empty tuple.
419
+
420
+ Raises:
421
+ TypeError: If the value is not a list of strings.
422
+ """
423
+ paths = []
424
+ for value in optional_string_tuple(data, key):
425
+ path = Path(value).expanduser()
426
+ if base_dir and not path.is_absolute():
427
+ path = base_dir / path
428
+ paths.append(path)
429
+ return tuple(paths)
430
+
431
+
403
432
  def optional_string_tuple(data: dict[str, Any], key: str) -> tuple[str, ...]:
404
433
  """Read an optional list of strings from manifest data.
405
434
 
@@ -512,6 +541,7 @@ def parse_environment_tool(data: object, base_dir: Path, index: int) -> Environm
512
541
  python=optional_string(data, "python"),
513
542
  indexes=optional_string_tuple(data, "indexes"),
514
543
  find_links=optional_string_tuple(data, "find_links"),
544
+ constraints=tuple(str(path) for path in optional_manifest_paths(data, "constraints", base_dir)),
515
545
  description=optional_string(data, "description"),
516
546
  homepage=optional_string(data, "homepage"),
517
547
  )
@@ -570,6 +600,7 @@ def environment_actions(spec: EnvironmentSpec, paths: DeploymentPaths) -> tuple[
570
600
  tool.python,
571
601
  tool.indexes,
572
602
  tool.find_links,
603
+ tool.constraints,
573
604
  tool.uv_config_file,
574
605
  )
575
606
  actions.append(
@@ -628,6 +659,7 @@ def deploy_environment(
628
659
  tool.python,
629
660
  tool.indexes,
630
661
  tool.find_links,
662
+ tool.constraints,
631
663
  tool.uv_config_file,
632
664
  ),
633
665
  check=True,
@@ -668,6 +700,7 @@ def deploy_python_tool(
668
700
  python: str | None = None,
669
701
  indexes: tuple[str, ...] = (),
670
702
  find_links: tuple[str, ...] = (),
703
+ constraints: tuple[str, ...] = (),
671
704
  uv_config_file: Path | None = None,
672
705
  execute_install: bool = False,
673
706
  make_default: bool = True,
@@ -685,6 +718,7 @@ def deploy_python_tool(
685
718
  python: Optional Python interpreter or version passed to uv.
686
719
  indexes: Additional package index URLs passed to uv.
687
720
  find_links: Wheelhouse directories or HTML package pages passed to uv.
721
+ constraints: Constraint requirement files passed to uv.
688
722
  uv_config_file: Optional uv configuration file passed to `uv tool`.
689
723
  execute_install: Whether to run `uv tool install` immediately.
690
724
  make_default: Whether to make this version the module default.
@@ -700,7 +734,7 @@ def deploy_python_tool(
700
734
  paths = deployment_paths(module_root, prefix, name, version)
701
735
  paths.bin_dir.mkdir(parents=True, exist_ok=True)
702
736
  tool_dir = paths.install_root / "uv-tools"
703
- command = uv_install_command(package, python, indexes, find_links, uv_config_file)
737
+ command = uv_install_command(package, python, indexes, find_links, constraints, uv_config_file)
704
738
 
705
739
  if execute_install:
706
740
  require_executable("uv")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "env-module-manager"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Deploy Python, Rust, shell, and collective CLI environments behind GNU environment modulefiles"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -56,6 +56,8 @@ def test_deploy_python_command_writes_modulefile() -> None:
56
56
  "https://packages.example/simple",
57
57
  "--find-links",
58
58
  "/prod/wheels",
59
+ "--constraints",
60
+ "/prod/constraints.txt",
59
61
  "--uv-config-file",
60
62
  "/prod/uv.toml",
61
63
  ],
@@ -66,6 +68,7 @@ def test_deploy_python_command_writes_modulefile() -> None:
66
68
  assert "modulefile: modules/ruff/0.8.0" in result.output
67
69
  assert "default version: modules/ruff/.version" in result.output
68
70
  assert "--config-file /prod/uv.toml" in modulefile
71
+ assert "--constraints /prod/constraints.txt" in modulefile
69
72
 
70
73
 
71
74
  def test_deploy_python_command_uses_config_defaults() -> None:
@@ -408,6 +411,7 @@ name = "ruff"
408
411
  version = "0.8.0"
409
412
  package = "ruff==0.8.0"
410
413
  indexes = ["https://packages.example/simple"]
414
+ constraints = ["constraints.txt"]
411
415
  """.strip(),
412
416
  encoding="utf-8",
413
417
  )
@@ -418,6 +422,7 @@ indexes = ["https://packages.example/simple"]
418
422
  assert "would create install root: tools/dev-tools/2026.05" in result.output
419
423
  assert "would install python ruff:" in result.output
420
424
  assert "--index https://packages.example/simple" in result.output
425
+ assert "--constraints constraints.txt" in result.output
421
426
  assert not install_root_exists
422
427
 
423
428
 
@@ -0,0 +1,138 @@
1
+ """Integration tests that execute external deployment tools."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ from click.testing import CliRunner
8
+
9
+ from module_manager.cli import main
10
+
11
+
12
+ @pytest.mark.integration
13
+ def test_deploy_python_real_uv_install_uses_cli_roots(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
14
+ """A real uv install should use the CLI prefix and module root.
15
+
16
+ Args:
17
+ tmp_path: Temporary deployment root.
18
+ monkeypatch: Pytest helper used to control inherited uv destinations.
19
+ """
20
+ project_root = Path(__file__).parent.parent
21
+ prefix = tmp_path / "tools"
22
+ module_root = tmp_path / "modules"
23
+ inherited_tool_dir = tmp_path / "inherited-tools"
24
+ inherited_bin_dir = tmp_path / "inherited-bin"
25
+ monkeypatch.setenv("UV_TOOL_DIR", str(inherited_tool_dir))
26
+ monkeypatch.setenv("UV_TOOL_BIN_DIR", str(inherited_bin_dir))
27
+
28
+ result = CliRunner().invoke(
29
+ main,
30
+ [
31
+ "deploy-python",
32
+ "module-manager",
33
+ "0.6.0",
34
+ "--package",
35
+ str(project_root),
36
+ "--prefix",
37
+ str(prefix),
38
+ "--module-root",
39
+ str(module_root),
40
+ "--execute-install",
41
+ ],
42
+ )
43
+
44
+ install_root = prefix / "module-manager" / "0.6.0"
45
+ assert result.exit_code == 0, result.output
46
+ assert (install_root / "bin" / "module-manager").exists()
47
+ assert (install_root / "uv-tools" / "env-module-manager" / "uv-receipt.toml").exists()
48
+ assert (module_root / "module-manager" / "0.6.0").is_file()
49
+ assert not inherited_tool_dir.exists()
50
+ assert not inherited_bin_dir.exists()
51
+
52
+
53
+ @pytest.mark.integration
54
+ def test_deploy_python_real_uv_install_handles_url_dependency(tmp_path: Path) -> None:
55
+ """A real uv install should resolve and run direct URL dependencies.
56
+
57
+ Args:
58
+ tmp_path: Temporary deployment root.
59
+ """
60
+ dependency = tmp_path / "url-dependency"
61
+ dependency.mkdir()
62
+ (dependency / "pyproject.toml").write_text(
63
+ """
64
+ [project]
65
+ name = "url-dependency"
66
+ version = "1.0.0"
67
+ requires-python = ">=3.11"
68
+ """.strip(),
69
+ encoding="utf-8",
70
+ )
71
+ dependency_package = dependency / "url_dependency"
72
+ dependency_package.mkdir()
73
+ (dependency_package / "__init__.py").write_text(
74
+ """
75
+ def message() -> str:
76
+ return "url dependency works"
77
+ """.strip(),
78
+ encoding="utf-8",
79
+ )
80
+
81
+ tool = tmp_path / "url-tool"
82
+ tool.mkdir()
83
+ (tool / "pyproject.toml").write_text(
84
+ f"""
85
+ [project]
86
+ name = "url-tool"
87
+ version = "1.0.0"
88
+ requires-python = ">=3.11"
89
+ dependencies = [
90
+ "url-dependency @ {dependency.as_uri()}",
91
+ ]
92
+
93
+ [project.scripts]
94
+ url-tool = "url_tool:main"
95
+ """.strip(),
96
+ encoding="utf-8",
97
+ )
98
+ tool_package = tool / "url_tool"
99
+ tool_package.mkdir()
100
+ (tool_package / "__init__.py").write_text(
101
+ """
102
+ from url_dependency import message
103
+
104
+
105
+ def main() -> None:
106
+ print(message())
107
+ """.strip(),
108
+ encoding="utf-8",
109
+ )
110
+
111
+ prefix = tmp_path / "tools"
112
+ module_root = tmp_path / "modules"
113
+ result = CliRunner().invoke(
114
+ main,
115
+ [
116
+ "deploy-python",
117
+ "url-tool",
118
+ "1.0.0",
119
+ "--package",
120
+ str(tool),
121
+ "--prefix",
122
+ str(prefix),
123
+ "--module-root",
124
+ str(module_root),
125
+ "--execute-install",
126
+ ],
127
+ )
128
+
129
+ executable = prefix / "url-tool" / "1.0.0" / "bin" / "url-tool"
130
+ assert result.exit_code == 0, result.output
131
+ assert executable.exists()
132
+ completed = subprocess.run(
133
+ [str(executable)],
134
+ check=True,
135
+ capture_output=True,
136
+ text=True,
137
+ )
138
+ assert completed.stdout.strip() == "url dependency works"
@@ -1,6 +1,7 @@
1
1
  """Modulefile rendering and deployment helper tests."""
2
2
 
3
3
  from pathlib import Path
4
+ from shlex import quote as shlex_quote
4
5
 
5
6
  import pytest
6
7
 
@@ -84,6 +85,7 @@ def test_python_tool_install_command_accepts_indexes() -> None:
84
85
  "internal-tool==1.2.3",
85
86
  indexes=("https://packages.example/simple", "https://mirror.example/simple"),
86
87
  find_links=("/prod/wheels",),
88
+ constraints=("/prod/constraints.txt",),
87
89
  ) == [
88
90
  "uv",
89
91
  "tool",
@@ -94,6 +96,8 @@ def test_python_tool_install_command_accepts_indexes() -> None:
94
96
  "https://mirror.example/simple",
95
97
  "--find-links",
96
98
  "/prod/wheels",
99
+ "--constraints",
100
+ "/prod/constraints.txt",
97
101
  "internal-tool==1.2.3",
98
102
  ]
99
103
 
@@ -122,6 +126,20 @@ def test_python_tool_install_command_accepts_uv_config_file() -> None:
122
126
  ]
123
127
 
124
128
 
129
+ def test_python_tool_install_command_accepts_url_dependency() -> None:
130
+ """Uv install commands should pass direct URL dependencies unchanged."""
131
+ package = "mytool @ https://packages.example/files/mytool-1.0.0-py3-none-any.whl"
132
+
133
+ assert uv_install_command(package, constraints=("/prod/constraints.txt",)) == [
134
+ "uv",
135
+ "tool",
136
+ "install",
137
+ "--constraints",
138
+ "/prod/constraints.txt",
139
+ package,
140
+ ]
141
+
142
+
125
143
  def test_uv_install_environment_ignores_inherited_uv_tool_settings(
126
144
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
127
145
  ) -> None:
@@ -158,6 +176,7 @@ def test_deploy_python_tool_writes_package_sources_to_install_hint(tmp_path: Pat
158
176
  prefix=tmp_path / "tools",
159
177
  indexes=("https://packages.example/simple",),
160
178
  find_links=("/prod/wheels",),
179
+ constraints=("/prod/constraints.txt",),
161
180
  uv_config_file=Path("/prod/uv.toml"),
162
181
  )
163
182
 
@@ -165,6 +184,28 @@ def test_deploy_python_tool_writes_package_sources_to_install_hint(tmp_path: Pat
165
184
  assert "--config-file /prod/uv.toml" in modulefile
166
185
  assert "--index https://packages.example/simple" in modulefile
167
186
  assert "--find-links /prod/wheels" in modulefile
187
+ assert "--constraints /prod/constraints.txt" in modulefile
188
+
189
+
190
+ def test_deploy_python_tool_writes_url_dependency_to_install_hint(tmp_path: Path) -> None:
191
+ """Generated module help should preserve direct URL package specs.
192
+
193
+ Args:
194
+ tmp_path: Temporary deployment root.
195
+ """
196
+ package = "mytool @ https://packages.example/files/mytool-1.0.0-py3-none-any.whl"
197
+ paths = deploy_python_tool(
198
+ name="mytool",
199
+ version="1.0.0",
200
+ package=package,
201
+ module_root=tmp_path / "modules",
202
+ prefix=tmp_path / "tools",
203
+ constraints=("/prod/constraints.txt",),
204
+ )
205
+
206
+ modulefile = paths.modulefile.read_text(encoding="utf-8")
207
+ assert shlex_quote(package) in modulefile
208
+ assert "--constraints /prod/constraints.txt" in modulefile
168
209
 
169
210
 
170
211
  def test_require_executable_reports_missing_command(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -403,6 +444,7 @@ package = "ruff==0.8.0"
403
444
  python = "3.12"
404
445
  indexes = ["https://packages.example/simple"]
405
446
  find_links = ["/prod/wheels"]
447
+ constraints = ["constraints.txt", "/prod/global-constraints.txt"]
406
448
  uv_config_file = "uv.toml"
407
449
 
408
450
  [[tools]]
@@ -422,6 +464,7 @@ script = "scripts/helper"
422
464
  assert not spec.make_default
423
465
  assert spec.tools[0].package == "ruff==0.8.0"
424
466
  assert spec.tools[0].indexes == ("https://packages.example/simple",)
467
+ assert spec.tools[0].constraints == (str(tmp_path / "constraints.txt"), "/prod/global-constraints.txt")
425
468
  assert spec.tools[0].uv_config_file == tmp_path / "uv.toml"
426
469
  assert spec.tools[1].script == tmp_path / "scripts/helper"
427
470
 
@@ -530,6 +573,7 @@ type = "python"
530
573
  name = "ruff"
531
574
  package = "ruff==0.8.0"
532
575
  python = "3.12"
576
+ constraints = ["constraints.txt"]
533
577
  uv_config_file = "uv.toml"
534
578
  """.strip(),
535
579
  encoding="utf-8",
@@ -550,6 +594,8 @@ uv_config_file = "uv.toml"
550
594
  "install",
551
595
  "--python",
552
596
  "3.12",
597
+ "--constraints",
598
+ str(tmp_path / "constraints.txt"),
553
599
  "ruff==0.8.0",
554
600
  ]
555
601
  assert calls[0][1]["UV_TOOL_DIR"] == str(result.paths.install_root / "uv-tools")
@@ -344,7 +344,7 @@ wheels = [
344
344
 
345
345
  [[package]]
346
346
  name = "env-module-manager"
347
- version = "0.5.0"
347
+ version = "0.6.0"
348
348
  source = { editable = "." }
349
349
  dependencies = [
350
350
  { name = "click" },
@@ -1,49 +0,0 @@
1
- """Integration tests that execute external deployment tools."""
2
-
3
- from pathlib import Path
4
-
5
- import pytest
6
- from click.testing import CliRunner
7
-
8
- from module_manager.cli import main
9
-
10
-
11
- @pytest.mark.integration
12
- def test_deploy_python_real_uv_install_uses_cli_roots(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
13
- """A real uv install should use the CLI prefix and module root.
14
-
15
- Args:
16
- tmp_path: Temporary deployment root.
17
- monkeypatch: Pytest helper used to control inherited uv destinations.
18
- """
19
- project_root = Path(__file__).parent.parent
20
- prefix = tmp_path / "tools"
21
- module_root = tmp_path / "modules"
22
- inherited_tool_dir = tmp_path / "inherited-tools"
23
- inherited_bin_dir = tmp_path / "inherited-bin"
24
- monkeypatch.setenv("UV_TOOL_DIR", str(inherited_tool_dir))
25
- monkeypatch.setenv("UV_TOOL_BIN_DIR", str(inherited_bin_dir))
26
-
27
- result = CliRunner().invoke(
28
- main,
29
- [
30
- "deploy-python",
31
- "module-manager",
32
- "0.5.0",
33
- "--package",
34
- str(project_root),
35
- "--prefix",
36
- str(prefix),
37
- "--module-root",
38
- str(module_root),
39
- "--execute-install",
40
- ],
41
- )
42
-
43
- install_root = prefix / "module-manager" / "0.5.0"
44
- assert result.exit_code == 0, result.output
45
- assert (install_root / "bin" / "module-manager").exists()
46
- assert (install_root / "uv-tools" / "env-module-manager" / "uv-receipt.toml").exists()
47
- assert (module_root / "module-manager" / "0.5.0").is_file()
48
- assert not inherited_tool_dir.exists()
49
- assert not inherited_bin_dir.exists()