Skip to content

Commit 6db8cd3

Browse files
authored
Fix for galaxy with system site packages (#147)
1 parent 57b47a0 commit 6db8cd3

File tree

3 files changed

+230
-20
lines changed

3 files changed

+230
-20
lines changed

src/ansible_dev_environment/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import os
7+
import shutil
78
import subprocess
89
import sys
910

@@ -101,6 +102,31 @@ def interpreter(self: Config) -> Path:
101102
"""Return the current interpreter."""
102103
return Path(sys.executable)
103104

105+
@property
106+
def galaxy_bin(self: Config) -> Path | None:
107+
"""Find the ansible galaxy command.
108+
109+
Prefer the venv over the system package over the PATH.
110+
"""
111+
within_venv = self.venv_bindir / "ansible-galaxy"
112+
if within_venv.exists():
113+
msg = f"Found ansible-galaxy in virtual environment: {within_venv}"
114+
self._output.debug(msg)
115+
return within_venv
116+
system_pkg = self.site_pkg_path / "bin" / "ansible-galaxy"
117+
if system_pkg.exists():
118+
msg = f"Found ansible-galaxy in system packages: {system_pkg}"
119+
self._output.debug(msg)
120+
return system_pkg
121+
last_resort = shutil.which("ansible-galaxy")
122+
if last_resort:
123+
msg = f"Found ansible-galaxy in PATH: {last_resort}"
124+
self._output.debug(msg)
125+
return Path(last_resort)
126+
msg = "Failed to find ansible-galaxy."
127+
self._output.critical(msg)
128+
return None
129+
104130
def _set_interpreter(
105131
self: Config,
106132
) -> None:

src/ansible_dev_environment/subcommands/installer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def _install_galaxy_collections(
137137
shutil.rmtree(collection.site_pkg_path)
138138

139139
command = (
140-
f"{self._config.venv_bindir / 'ansible-galaxy'} collection"
140+
f"{self._config.galaxy_bin} collection"
141141
f" install {collections_str}"
142142
f" -p {self._config.site_pkg_path}"
143143
" --force"
@@ -191,7 +191,7 @@ def _install_galaxy_requirements(self: Installer) -> None:
191191
shutil.rmtree(cpath)
192192

193193
command = (
194-
f"{self._config.venv_bindir / 'ansible-galaxy'} collection"
194+
f"{self._config.galaxy_bin} collection"
195195
f" install -r {self._config.args.requirement}"
196196
f" -p {self._config.site_pkg_path}"
197197
" --force"
@@ -359,7 +359,7 @@ def _install_local_collection(
359359

360360
command = (
361361
f"cd {collection.build_dir} &&"
362-
f" {self._config.venv_bindir / 'ansible-galaxy'} collection build"
362+
f" {self._config.galaxy_bin} collection build"
363363
f" --output-path {collection.build_dir}"
364364
" --force"
365365
)
@@ -412,7 +412,7 @@ def _install_local_collection(
412412
shutil.rmtree(info_dir)
413413

414414
command = (
415-
f"{self._config.venv_bindir / 'ansible-galaxy'} collection"
415+
f"{self._config.galaxy_bin} collection"
416416
f" install {tarball} -p {self._config.site_pkg_path}"
417417
" --force"
418418
)

tests/unit/test_config.py

Lines changed: 200 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,66 @@
33
from __future__ import annotations
44

55
import argparse
6+
import shutil
67

8+
from pathlib import Path
79
from typing import TYPE_CHECKING
810

911
import pytest
1012

1113
from ansible_dev_environment.config import Config
12-
from ansible_dev_environment.output import Output
13-
from ansible_dev_environment.utils import TermFeatures
1414

1515

1616
if TYPE_CHECKING:
17-
from pathlib import Path
17+
from ansible_dev_environment.output import Output
18+
19+
20+
def gen_args(
21+
venv: str,
22+
system_site_packages: bool = False, # noqa: FBT001, FBT002
23+
) -> argparse.Namespace:
24+
"""Generate the arguments.
25+
26+
Args:
27+
venv: The virtual environment.
28+
system_site_packages: Whether to include system site packages.
29+
30+
Returns:
31+
The arguments.
32+
"""
33+
return argparse.Namespace(
34+
verbose=0,
35+
venv=venv,
36+
system_site_packages=system_site_packages,
37+
)
1838

1939

2040
@pytest.mark.parametrize(
2141
"system_site_packages",
2242
((True, False)),
2343
ids=["ssp_true", "ssp_false"],
2444
)
25-
def test_paths(tmpdir: Path, system_site_packages: bool) -> None: # noqa: FBT001
45+
def test_paths(
46+
tmpdir: Path,
47+
system_site_packages: bool, # noqa: FBT001
48+
output: Output,
49+
) -> None:
2650
"""Test the paths.
2751
2852
Several of the found directories should have a parent of the tmpdir / test_venv
2953
3054
Args:
3155
tmpdir: A temporary directory.
3256
system_site_packages: Whether to include system site packages.
57+
output: The output fixture.
3358
"""
3459
venv = tmpdir / "test_venv"
35-
args = argparse.Namespace(
60+
args = gen_args(
3661
venv=str(venv),
3762
system_site_packages=system_site_packages,
38-
verbose=0,
39-
)
40-
term_features = TermFeatures(color=False, links=False)
41-
42-
output = Output(
43-
log_file=str(tmpdir / "test_log.log"),
44-
log_level="debug",
45-
log_append="false",
46-
term_features=term_features,
47-
verbosity=0,
4863
)
4964

50-
config = Config(args=args, output=output, term_features=term_features)
65+
config = Config(args=args, output=output, term_features=output.term_features)
5166
config.init()
5267

5368
assert config.venv == venv
@@ -59,3 +74,172 @@ def test_paths(tmpdir: Path, system_site_packages: bool) -> None: # noqa: FBT00
5974
"venv_interpreter",
6075
):
6176
assert venv in getattr(config, attr).parents
77+
78+
79+
def test_galaxy_bin_venv(
80+
tmpdir: Path,
81+
monkeypatch: pytest.MonkeyPatch,
82+
output: Output,
83+
) -> None:
84+
"""Test the galaxy_bin property found in venv.
85+
86+
Args:
87+
tmpdir: A temporary directory.
88+
monkeypatch: A pytest fixture for monkey patching.
89+
output: The output fixture.
90+
"""
91+
venv = tmpdir / "test_venv"
92+
args = gen_args(venv=str(venv))
93+
94+
config = Config(args=args, output=output, term_features=output.term_features)
95+
config.init()
96+
97+
orig_exists = Path.exists
98+
exists_called = False
99+
100+
def _exists(path: Path) -> bool:
101+
if path.name != "ansible-galaxy":
102+
return orig_exists(path)
103+
if path.parent == config.venv_bindir:
104+
nonlocal exists_called
105+
exists_called = True
106+
return True
107+
return False
108+
109+
monkeypatch.setattr(Path, "exists", _exists)
110+
111+
assert config.galaxy_bin == venv / "bin" / "ansible-galaxy"
112+
assert exists_called
113+
114+
115+
def test_galaxy_bin_site(
116+
tmpdir: Path,
117+
monkeypatch: pytest.MonkeyPatch,
118+
output: Output,
119+
) -> None:
120+
"""Test the galaxy_bin property found in site.
121+
122+
Args:
123+
tmpdir: A temporary directory.
124+
monkeypatch: A pytest fixture for monkey patching.
125+
output: The output fixture.
126+
"""
127+
venv = tmpdir / "test_venv"
128+
args = gen_args(venv=str(venv))
129+
130+
config = Config(args=args, output=output, term_features=output.term_features)
131+
config.init()
132+
133+
orig_exists = Path.exists
134+
exists_called = False
135+
136+
def _exists(path: Path) -> bool:
137+
if path.name != "ansible-galaxy":
138+
return orig_exists(path)
139+
if path.parent == config.site_pkg_path / "bin":
140+
nonlocal exists_called
141+
exists_called = True
142+
return True
143+
return False
144+
145+
monkeypatch.setattr(Path, "exists", _exists)
146+
147+
assert config.galaxy_bin == config.site_pkg_path / "bin" / "ansible-galaxy"
148+
assert exists_called
149+
150+
151+
def test_galaxy_bin_path(
152+
tmpdir: Path,
153+
monkeypatch: pytest.MonkeyPatch,
154+
output: Output,
155+
) -> None:
156+
"""Test the galaxy_bin property found in path.
157+
158+
Args:
159+
tmpdir: A temporary directory.
160+
monkeypatch: A pytest fixture for monkey patching.
161+
output: The output fixture.
162+
"""
163+
venv = tmpdir / "test_venv"
164+
args = gen_args(venv=str(venv))
165+
166+
config = Config(args=args, output=output, term_features=output.term_features)
167+
config.init()
168+
169+
orig_exists = Path.exists
170+
exists_called = False
171+
172+
def _exists(path: Path) -> bool:
173+
if path.name != "ansible-galaxy":
174+
return orig_exists(path)
175+
nonlocal exists_called
176+
exists_called = True
177+
return False
178+
179+
monkeypatch.setattr(Path, "exists", _exists)
180+
181+
orig_which = shutil.which
182+
which_called = False
183+
184+
def _which(name: str) -> str | None:
185+
if not name.endswith("ansible-galaxy"):
186+
return orig_which(name)
187+
nonlocal which_called
188+
which_called = True
189+
return "patched"
190+
191+
monkeypatch.setattr(shutil, "which", _which)
192+
193+
assert config.galaxy_bin == Path("patched")
194+
assert exists_called
195+
assert which_called
196+
197+
198+
def test_galaxy_bin_not_found(
199+
tmpdir: Path,
200+
monkeypatch: pytest.MonkeyPatch,
201+
output: Output,
202+
) -> None:
203+
"""Test the galaxy_bin property found in venv.
204+
205+
Args:
206+
tmpdir: A temporary directory.
207+
monkeypatch: A pytest fixture for monkey patching.
208+
output: The output fixture.
209+
"""
210+
venv = tmpdir / "test_venv"
211+
args = gen_args(venv=str(venv))
212+
213+
config = Config(args=args, output=output, term_features=output.term_features)
214+
config.init()
215+
216+
orig_exists = Path.exists
217+
exist_called = False
218+
219+
def _exists(path: Path) -> bool:
220+
if path.name == "ansible-galaxy":
221+
nonlocal exist_called
222+
exist_called = True
223+
return False
224+
return orig_exists(path)
225+
226+
monkeypatch.setattr(Path, "exists", _exists)
227+
228+
orig_which = shutil.which
229+
which_called = False
230+
231+
def _which(name: str) -> str | None:
232+
if name.endswith("ansible-galaxy"):
233+
nonlocal which_called
234+
which_called = True
235+
return None
236+
return orig_which(name)
237+
238+
monkeypatch.setattr(shutil, "which", _which)
239+
240+
with pytest.raises(SystemExit) as exc:
241+
assert config.galaxy_bin is None
242+
243+
assert exc.value.code == 1
244+
assert exist_called
245+
assert which_called

0 commit comments

Comments
 (0)