diff --git a/src/ansible_dev_environment/arg_parser.py b/src/ansible_dev_environment/arg_parser.py index bf36e38..97e413c 100644 --- a/src/ansible_dev_environment/arg_parser.py +++ b/src/ansible_dev_environment/arg_parser.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import contextlib import logging import os import sys @@ -12,12 +13,24 @@ from pathlib import Path from typing import TYPE_CHECKING +from .utils import str_to_bool + logger = logging.getLogger(__name__) if TYPE_CHECKING: from typing import Any + +ENVVAR_MAPPING: dict[str, str] = { + "ansi": "NO_COLOR", + "isolation_mode": "ADE_ISOLATION_MODE", + "seed": "ADE_SEED", + "uv": "ADE_UV", + "venv": "VIRTUAL_ENV", + "verbose": "ADE_VERBOSE", +} + try: from ._version import version as __version__ # type: ignore[unused-ignore,import-not-found] except ImportError: # pragma: no cover @@ -39,6 +52,14 @@ def common_args(parser: ArgumentParser) -> None: Args: parser: The parser to add the arguments to """ + parser.add_argument( + "--ansi", + action=argparse.BooleanOptionalAction, + default=True, + dest="ansi", + help="Enable or disable the use of ANSI codes for terminal hyperlink generation and color.", + ) + parser.add_argument( "--lf", "--log-file ", @@ -62,6 +83,13 @@ def common_args(parser: ArgumentParser) -> None: default="true", help="Append to log file.", ) + parser.add_argument( + "--uv", + dest="uv", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable or disable the use of uv as the python package manager if found.", + ) parser.add_argument( "-v", "--verbose", @@ -101,12 +129,9 @@ def parse() -> argparse.Namespace: level1 = ArgumentParser(add_help=False) - venv_path = os.environ.get("VIRTUAL_ENV", None) - if not venv_path: - warnings.warn("No virtualenv found active, we will assume .venv", stacklevel=1) level1.add_argument( "--venv ", - help="Target virtual environment.", + help="Target virtual environment. Created with 'ade install' if not found.", default=".venv", dest="venv", ) @@ -119,15 +144,6 @@ def parse() -> argparse.Namespace: action="store_true", ) - level1.add_argument( - "--na", - "--no-ansi", - action="store_true", - default=False, - dest="no_ansi", - help="Disable the use of ANSI codes for terminal hyperlink generation and color.", - ) - level1.add_argument( "--ssp", "--system-site-packages", @@ -195,7 +211,6 @@ def parse() -> argparse.Namespace: ) install.add_argument( - # "-adt", "--seed", action=argparse.BooleanOptionalAction, default=True, @@ -229,13 +244,106 @@ def parse() -> argparse.Namespace: _group_titles(subparser) args = sys.argv[1:] - for i, v in enumerate(args): - for old in ("-adt", "--ansible-dev-tools"): - if v == old: - msg = f"Replace the deprecated {old} argument with --seed to avoid future execution failure." - logger.warning(msg) - args[i] = "--seed" - return parser.parse_args(args) + for param in ("--adt", "--ansible-dev-tools"): + with contextlib.suppress(ValueError): + args[args.index(param)] = "--seed" + err = f"The parameter '{param}' is deprecated, use 'ADE_SEED or '--seed' instead." + warnings.warn(err, DeprecationWarning, stacklevel=2) + + if skip_uv := os.environ.get("SKIP_UV"): + os.environ["ADE_UV"] = skip_uv + err = "The environment variable 'SKIP_UV' is deprecated, use 'ADE_UV' or '--no-uv' instead." + warnings.warn(err, DeprecationWarning, stacklevel=2) + + return apply_envvars(args=args, parser=parser) + + +def _get_action(actions: list[argparse.Action], dest: str) -> argparse.Action | None: + """Get the action for a given destination. + + Args: + actions: The list of actions + dest: The destination to get the action for + + Returns: + The action for the given destination + + """ + for action in actions: + if action.dest == dest: + return action + if isinstance(action, argparse._SubParsersAction): # noqa: SLF001 + for subparser in action.choices.values(): + sub_action = _get_action(subparser._actions, dest) # noqa: SLF001 + if sub_action is not None: + return sub_action + + return None + + +def apply_envvars(args: list[str], parser: ArgumentParser) -> argparse.Namespace: + """Apply the environment variables to the arguments. + + Special handling exists NO_COLOR since it's value shouldn't matter. + Also account for a space in some option strings when checking for presence in sys.argv. + + Args: + args: The cli arguments + parser: The argument parser + + Returns: + The resulting namespace + + Raises: + NotImplementedError: If the action type is not implemented + """ + cli_result = parser.parse_args(args) + + for dest, envvar in ENVVAR_MAPPING.items(): + if (action := _get_action(parser._actions, dest)) is None: # noqa: SLF001 + err = f"Action for {dest} not found in parser" + raise NotImplementedError(err) + + present = any( + option + for option in action.option_strings + if any(arg for arg in args if arg.startswith(option.split()[0])) + ) + envvar_value = os.environ.get(envvar) + + if present or envvar_value is None: + continue + + final_value: bool | int | str | None = None + named_type = "not set" + + with contextlib.suppress(ValueError): + if envvar == "NO_COLOR" and envvar_value != "": + final_value = False + elif isinstance(action, (argparse.BooleanOptionalAction, argparse._StoreTrueAction)): # noqa: SLF001 + named_type = "boolean" + final_value = str_to_bool(envvar_value) + elif isinstance(action, argparse._CountAction) or action.type is int: # noqa: SLF001 + named_type = "int" + final_value = int(envvar_value) + elif action.type is str or action.type is None: + named_type = "str" + final_value = envvar_value + else: + err = f"Action type {action.type} not implemented for envvar {envvar}" + raise NotImplementedError(err) + + err = f"ade: error: environment variable {envvar}: invalid value: '{envvar_value}' " + if final_value is None: + err += f"could not convert to {named_type}\n" + parser.exit(1, err) + if action.choices and final_value not in action.choices: + err += f"(choose from {', '.join(map(repr, action.choices))})\n" + parser.exit(1, err) + + setattr(cli_result, dest, final_value) + + return cli_result def _group_titles(parser: ArgumentParser) -> None: @@ -266,6 +374,9 @@ def add_argument( # type: ignore[override] """ if "choices" in kwargs: kwargs["help"] += f" (choices: {', '.join(kwargs['choices'])})" + if kwargs.get("dest") in ENVVAR_MAPPING: + envvar = ENVVAR_MAPPING[kwargs["dest"]] + kwargs["help"] += f" (env: {envvar})" if "default" in kwargs and kwargs["default"] != "==SUPPRESS==": kwargs["help"] += f" (default: {kwargs['default']})" kwargs["help"] = kwargs["help"][0].upper() + kwargs["help"][1:] diff --git a/src/ansible_dev_environment/cli.py b/src/ansible_dev_environment/cli.py index 7761d58..20efa4d 100644 --- a/src/ansible_dev_environment/cli.py +++ b/src/ansible_dev_environment/cli.py @@ -51,8 +51,8 @@ def init_output(self) -> None: self.term_features = TermFeatures(color=False, links=False) else: self.term_features = TermFeatures( - color=False if os.environ.get("NO_COLOR") else not self.args.no_ansi, - links=not self.args.no_ansi, + color=self.args.ansi, + links=self.args.ansi, ) self.output = Output( @@ -98,6 +98,10 @@ def args_sanity(self) -> None: err = "Editable can not be used with a requirements file." self.output.critical(err) + self.output.debug("Arguments sanity check passed.") + for arg in vars(self.args): + self.output.debug(f"{arg}: {getattr(self.args, arg)}") + def isolation_check(self) -> bool: """Check the environment for isolation. diff --git a/src/ansible_dev_environment/config.py b/src/ansible_dev_environment/config.py index a23a117..c3b6190 100644 --- a/src/ansible_dev_environment/config.py +++ b/src/ansible_dev_environment/config.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import os import shutil import subprocess import sys @@ -49,7 +48,7 @@ def __init__( def init(self) -> None: """Initialize the configuration.""" - if self.args.venv: + if self.args.subcommand == "install": self._create_venv = True self._set_interpreter() @@ -65,19 +64,8 @@ def cache_dir(self) -> Path: @property def venv(self) -> Path: - """Return the virtual environment path. - - Raises: - SystemExit: If the virtual environment cannot be found. - """ - if self.args.venv: - return Path(self.args.venv).expanduser().resolve() - venv_str = os.environ.get("VIRTUAL_ENV") - if venv_str: - return Path(venv_str).expanduser().resolve() - err = "Failed to find a virtual environment." - self._output.critical(err) - raise SystemExit(1) # pragma: no cover # critical exits + """Return the virtual environment path.""" + return Path(self.args.venv).expanduser().resolve() @property def venv_cache_dir(self) -> Path: @@ -102,8 +90,8 @@ def uv_available(self) -> bool: Returns: True if uv is to be used. """ - if int(os.environ.get("SKIP_UV", "0")): - self._output.debug("uv is disabled by SKIP_UV=1 in the environment.") + if self.args.uv is False: + self._output.debug("uv is disabled.") return False if not (uv_path := shutil.which("uv")): @@ -112,7 +100,7 @@ def uv_available(self) -> bool: self._output.debug(f"uv is available at {uv_path}") self._output.info( - "uv is available and will be used instead of venv/pip. To disable that define SKIP_UV=1 in your environment.", + "uv is available and will be used instead of venv/pip. Disable with 'ADE_UV=0' or '--uv false'.", ) return True @@ -184,12 +172,11 @@ def _set_interpreter( if not self.venv.exists(): if self._create_venv: msg = f"Creating virtual environment: {self.venv}" - self._output.debug(msg) command = f"{venv_cmd} {self.venv}" - msg = f"Creating virtual environment: {self.venv}" if self.args.system_site_packages: command = f"{command} --system-site-packages" - msg = f"Creating virtual environment with system site packages: {self.venv}" + msg += " with system site packages" + self._output.debug(msg) try: subprocess_run( command=command, diff --git a/src/ansible_dev_environment/utils.py b/src/ansible_dev_environment/utils.py index 66616ce..0f42cb5 100644 --- a/src/ansible_dev_environment/utils.py +++ b/src/ansible_dev_environment/utils.py @@ -520,3 +520,24 @@ def __exit__( sys.stdout.write("\r") # show the cursor sys.stdout.write("\033[?25h") + + +def str_to_bool(value: str) -> bool | None: + """Converts a string to a boolean based on common truthy and falsy values. + + Args: + value: The string to convert. + + Returns: + True for truthy values, False for falsy values, None for invalid values. + """ + truthy_values = {"true", "1", "yes", "y", "on"} + falsy_values = {"false", "0", "no", "n", "off"} + + value_lower = value.strip().lower() + + if value_lower in truthy_values: + return True + if value_lower in falsy_values: + return False + return None diff --git a/tests/integration/test_basic.py b/tests/integration/test_basic.py index 97eb6ce..db5b078 100644 --- a/tests/integration/test_basic.py +++ b/tests/integration/test_basic.py @@ -213,3 +213,34 @@ def test_requirements( assert "ansible.netcommon" not in captured.out assert "ansible.scm" not in captured.out assert "ansible.utils" in captured.out + + +def test_system_site_packages( + capsys: pytest.CaptureFixture[str], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Install non-local collection. + + Args: + capsys: Capture stdout and stderr + tmp_path: Temporary directory + monkeypatch: Pytest monkeypatch + """ + monkeypatch.setattr( + "sys.argv", + [ + "ade", + "install", + "ansible.utils", + f"--venv={tmp_path / 'venv'}", + "--system-site-packages", + "--no-ansi", + "-vvv", + ], + ) + with pytest.raises(SystemExit): + main() + captured = capsys.readouterr() + assert "with system site packages" in captured.out + assert "Installed collections include: ansible.utils" in captured.out diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index b586731..c5247c0 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -7,6 +7,7 @@ import pytest +from ansible_dev_environment.arg_parser import ArgumentParser, apply_envvars from ansible_dev_environment.cli import Cli, main @@ -38,13 +39,13 @@ def test_tty(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch: Pytest fixture. """ monkeypatch.setattr("sys.stdout.isatty", (lambda: True)) - monkeypatch.setattr("os.environ", {"NO_COLOR": ""}) + monkeypatch.setattr("os.environ", {"NO_COLOR": "anything"}) monkeypatch.setattr("sys.argv", ["ansible-dev-environment", "install"]) cli = Cli() cli.parse_args() cli.init_output() - assert cli.output.term_features.color - assert cli.output.term_features.links + assert not cli.output.term_features.color + assert not cli.output.term_features.links @pytest.mark.usefixtures("_wide_console") @@ -239,12 +240,15 @@ def test_no_venv_specified( """ monkeypatch.setattr( "sys.argv", - ["ansible-dev-environment", "install"], + ["ansible-dev-environment", "install", "-vvv"], ) monkeypatch.delenv("VIRTUAL_ENV", raising=False) main(dry=True) captured = capsys.readouterr() - assert "No virtualenv found active, we will assume .venv" in captured.out + + found = [line for line in captured.out.splitlines() if "Debug: venv: " in line] + assert len(found) == 1 + assert found[0].endswith(".venv") def test_exit_code_one( @@ -297,3 +301,87 @@ def test_exit_code_two( assert excinfo.value.code == expected captured = capsys.readouterr() assert "Test warning" in captured.out + + +def test_envvar_mapping_error(monkeypatch: pytest.MonkeyPatch) -> None: + """Test environment mapping error. + + Args: + monkeypatch: Pytest fixture. + """ + monkeypatch.setattr( + "ansible_dev_environment.arg_parser.ENVVAR_MAPPING", + {"foo": "FOO"}, + ) + monkeypatch.setattr( + "sys.argv", + ["ansible-dev-environment", "install"], + ) + cli = Cli() + with pytest.raises(NotImplementedError): + cli.parse_args() + + +def test_apply_envvar_error(monkeypatch: pytest.MonkeyPatch) -> None: + """Test environment mapping error. + + Args: + monkeypatch: Pytest fixture. + """ + monkeypatch.setattr( + "ansible_dev_environment.arg_parser.ENVVAR_MAPPING", + {"foo": "FOO"}, + ) + monkeypatch.setenv("FOO", "42.0") + + parser = ArgumentParser() + parser.add_argument("--foo", type=float, help="helpless") + + with pytest.raises(NotImplementedError) as excinfo: + apply_envvars(args=[], parser=parser) + + assert "not implemented for envvar FOO" in str(excinfo.value) + + +def test_env_wrong_type( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test wrong type. + + Args: + monkeypatch: Pytest fixture. + capsys: Pytest stdout capture fixture. + """ + monkeypatch.setattr( + "sys.argv", + ["ansible-dev-environment", "install"], + ) + monkeypatch.setenv("ADE_VERBOSE", "not_an_int") + cli = Cli() + with pytest.raises(SystemExit): + cli.parse_args() + captured = capsys.readouterr() + assert "could not convert to int" in captured.err + + +def test_env_wrong_choice( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test wrong choice. + + Args: + monkeypatch: Pytest fixture. + capsys: Pytest stdout capture fixture. + """ + monkeypatch.setattr( + "sys.argv", + ["ansible-dev-environment", "install"], + ) + monkeypatch.setenv("ADE_ISOLATION_MODE", "wrong_choice") + cli = Cli() + with pytest.raises(SystemExit): + cli.parse_args() + captured = capsys.readouterr() + assert "choose from 'restrictive', 'cfg', 'none'" in captured.err diff --git a/tests/unit/test_cli_deprecated.py b/tests/unit/test_cli_deprecated.py new file mode 100644 index 0000000..04b47f2 --- /dev/null +++ b/tests/unit/test_cli_deprecated.py @@ -0,0 +1,46 @@ +"""Test some deprecated values in the CLI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ansible_dev_environment.cli import main + + +if TYPE_CHECKING: + import pytest + + +def test_adt(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: + """Test the seed option. + + Args: + capsys: Pytest stdout capture fixture. + monkeypatch: Pytest fixture. + """ + # Test the seed option + monkeypatch.setattr( + "sys.argv", + ["ansible-dev-environment", "install", "--adt"], + ) + main(dry=True) + captured = capsys.readouterr() + assert "'--adt' is deprecated" in captured.out + + +def test_skip_uv(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: + """Test the skip uv option. + + Args: + capsys: Pytest stdout capture fixture. + monkeypatch: Pytest fixture. + """ + # Test the skip uv option + monkeypatch.setattr( + "sys.argv", + ["ansible-dev-environment", "install"], + ) + monkeypatch.setenv("SKIP_UV", "1") + main(dry=True) + captured = capsys.readouterr() + assert "'SKIP_UV' is deprecated" in captured.out diff --git a/tests/unit/test_cli_precedence.py b/tests/unit/test_cli_precedence.py new file mode 100644 index 0000000..c21a8fa --- /dev/null +++ b/tests/unit/test_cli_precedence.py @@ -0,0 +1,75 @@ +"""Test for cli and environment variable precedence.""" + +from __future__ import annotations + +from typing import TypedDict + +import pytest + +from ansible_dev_environment.cli import Cli + + +class Data(TypedDict): + """Test data dictionary. + + Attributes: + attr: Attribute name. + args: Command line argument. + env: Environment variable name. + cli_expected: Expected value from command line argument. + env_expected: Expected value from environment variable. + + """ + + attr: str + args: str + env: str + cli_expected: bool | int | str + env_expected: bool | int | str + + +params: list[Data] = [ + { + "attr": "ansi", + "args": "--ansi", + "env": "NO_COLOR", + "env_expected": False, + "cli_expected": True, + }, + { + "attr": "seed", + "args": "--seed", + "env": "ADE_SEED", + "env_expected": False, + "cli_expected": True, + }, + {"attr": "verbose", "args": "-vvv", "env": "ADE_VERBOSE", "env_expected": 2, "cli_expected": 3}, + {"attr": "uv", "args": "--uv", "env": "ADE_UV", "env_expected": False, "cli_expected": True}, +] + + +@pytest.mark.parametrize("data", params, ids=lambda d: d["attr"]) +def test_cli_precedence_flag( + data: Data, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CLI precedence over environment variables. + + Args: + data: Test data dictionary. + monkeypatch: Pytest fixture. + + """ + cli = Cli() + + args = ["ade", "install"] + monkeypatch.setenv(data["env"], str(data["env_expected"])) + monkeypatch.setattr("sys.argv", args) + + cli.parse_args() + + assert getattr(cli.args, data["attr"]) == data["env_expected"] + + args.append(data["args"]) + cli.parse_args() + assert getattr(cli.args, data["attr"]) == data["cli_expected"] diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 0201f82..054a6fc 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -13,6 +13,8 @@ import pytest +from ansible_dev_environment.arg_parser import parse +from ansible_dev_environment.cli import Cli from ansible_dev_environment.config import Config from ansible_dev_environment.utils import subprocess_run @@ -22,40 +24,41 @@ def gen_args( - venv: str, + venv: str | None, system_site_packages: bool = False, # noqa: FBT001, FBT002 + subcommand: str = "check", ) -> argparse.Namespace: """Generate the arguments. Args: venv: The virtual environment. system_site_packages: Whether to include system site packages. + subcommand: The subcommand to run. Returns: The arguments. """ - return argparse.Namespace( + args = argparse.Namespace( verbose=0, - venv=venv, + subcommand=subcommand, system_site_packages=system_site_packages, + uv=True, ) + if venv is not None: + args.venv = venv + return args @pytest.mark.parametrize( - ("system_site_packages", "uv"), + ("system_site_packages"), ( - pytest.param(True, False, id="ssp1-uv0"), - pytest.param(False, False, id="ssp0-uv0"), - pytest.param(True, True, id="ssp1-uv1"), - pytest.param(False, True, id="ssp0-uv1"), + pytest.param(True, id="ssp1"), + pytest.param(False, id="ssp0"), ), ) def test_paths( - *, - tmpdir: Path, - system_site_packages: bool, - uv: bool, - monkeypatch: pytest.MonkeyPatch, + system_site_packages: bool, # noqa: FBT001 + session_venv: Config, output: Output, ) -> None: """Test the paths. @@ -63,14 +66,11 @@ def test_paths( Several of the found directories should have a parent of the tmpdir / test_venv Args: - tmpdir: A temporary directory. system_site_packages: Whether to include system site packages. - uv: Whether to use the uv module. - monkeypatch: A pytest fixture for monkey patching. + session_venv: The session venv fixture. output: The output fixture. """ - monkeypatch.setenv("SKIP_UV", str(int(not uv))) - venv = tmpdir / "test_venv" + venv = session_venv.venv args = gen_args( venv=str(venv), system_site_packages=system_site_packages, @@ -79,13 +79,6 @@ def test_paths( config = Config(args=args, output=output, term_features=output.term_features) config.init() - if uv: - assert config.uv_available - assert "uv pip install --python" in config.venv_pip_install_cmd - else: - assert not config.uv_available - assert "-m pip install" in config.venv_pip_install_cmd - assert config.venv == venv for attr in ( "site_pkg_collections_path", @@ -98,18 +91,18 @@ def test_paths( def test_galaxy_bin_venv( - tmpdir: Path, monkeypatch: pytest.MonkeyPatch, + session_venv: Config, output: Output, ) -> None: """Test the galaxy_bin property found in venv. Args: - tmpdir: A temporary directory. monkeypatch: A pytest fixture for monkey patching. + session_venv: The session venv fixture. output: The output fixture. """ - venv = tmpdir / "test_venv" + venv = session_venv.venv args = gen_args(venv=str(venv)) config = Config(args=args, output=output, term_features=output.term_features) @@ -134,18 +127,18 @@ def _exists(path: Path) -> bool: def test_galaxy_bin_site( - tmpdir: Path, monkeypatch: pytest.MonkeyPatch, + session_venv: Config, output: Output, ) -> None: """Test the galaxy_bin property found in site. Args: - tmpdir: A temporary directory. monkeypatch: A pytest fixture for monkey patching. + session_venv: The session venv fixture. output: The output fixture. """ - venv = tmpdir / "test_venv" + venv = session_venv.venv args = gen_args(venv=str(venv)) config = Config(args=args, output=output, term_features=output.term_features) @@ -170,18 +163,18 @@ def _exists(path: Path) -> bool: def test_galaxy_bin_path( - tmpdir: Path, monkeypatch: pytest.MonkeyPatch, + session_venv: Config, output: Output, ) -> None: """Test the galaxy_bin property found in path. Args: - tmpdir: A temporary directory. monkeypatch: A pytest fixture for monkey patching. + session_venv: The session venv fixture. output: The output fixture. """ - venv = tmpdir / "test_venv" + venv = session_venv.venv args = gen_args(venv=str(venv)) config = Config(args=args, output=output, term_features=output.term_features) @@ -217,18 +210,18 @@ def _which(name: str) -> str | None: def test_galaxy_bin_not_found( - tmpdir: Path, monkeypatch: pytest.MonkeyPatch, + session_venv: Config, output: Output, ) -> None: """Test the galaxy_bin property found in venv. Args: - tmpdir: A temporary directory. monkeypatch: A pytest fixture for monkey patching. + session_venv: The session venv fixture. output: The output fixture. """ - venv = tmpdir / "test_venv" + venv = session_venv.venv args = gen_args(venv=str(venv)) config = Config(args=args, output=output, term_features=output.term_features) @@ -282,8 +275,9 @@ def test_venv_from_env_var( """ venv = session_venv.venv - args = gen_args(venv="") + monkeypatch.setattr("sys.argv", ["ade", "install"]) monkeypatch.setenv("VIRTUAL_ENV", str(venv)) + args = parse() config = Config(args=args, output=output, term_features=output.term_features) config.init() @@ -295,6 +289,7 @@ def test_venv_not_found( output: Output, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, ) -> None: """Test the venv not found. @@ -302,8 +297,9 @@ def test_venv_not_found( output: The output fixture. capsys: A pytest fixture for capturing stdout and stderr. monkeypatch: A pytest fixture for patching. + tmp_path: A temporary directory. """ - args = gen_args(venv="") + args = gen_args(venv=str(tmp_path / "test_venv")) config = Config(args=args, output=output, term_features=output.term_features) monkeypatch.delenv("VIRTUAL_ENV", raising=False) @@ -311,7 +307,7 @@ def test_venv_not_found( config.init() assert exc.value.code == 1 - assert "Failed to find a virtual environment." in capsys.readouterr().err + assert "Critical: Cannot find virtual environment:" in capsys.readouterr().err def test_venv_creation_failed( @@ -328,7 +324,7 @@ def test_venv_creation_failed( monkeypatch: A pytest fixture for patching. capsys: A pytest fixture for capturing stdout and stderr. """ - args = gen_args(venv=str(tmp_path / "test_venv")) + args = gen_args(venv=str(tmp_path / "test_venv"), subcommand="install") orig_subprocess_run = subprocess_run @@ -376,9 +372,10 @@ def test_venv_env_var_wrong( monkeypatch: A pytest fixture for patching. tmp_path: A temporary directory """ - args = gen_args(venv="") - config = Config(args=args, output=output, term_features=output.term_features) + monkeypatch.setattr("sys.argv", ["ade", "check"]) monkeypatch.setenv("VIRTUAL_ENV", str(tmp_path / "test_venv")) + args = parse() + config = Config(args=args, output=output, term_features=output.term_features) with pytest.raises(SystemExit) as exc: config.init() @@ -611,3 +608,64 @@ def mock_subprocess_run( assert exc.value.code == 1 assert "Failed to find purelib in sysconfig paths" in capsys.readouterr().err + + +def test_uv_not_found(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + """Test uv disabled and not available. + + Args: + monkeypatch: Pytest fixture. + capsys: Pytest fixture for capturing output. + """ + monkeypatch.setattr("sys.argv", ["ansible-dev-environment", "install", "-vvv"]) + + orig_which = shutil.which + + def _which(name: str) -> str | None: + if name != "uv": + return orig_which(name) + return None + + monkeypatch.setattr(shutil, "which", _which) + + cli = Cli() + cli.parse_args() + cli.init_output() + cli.config = Config( + args=cli.args, + output=cli.output, + term_features=cli.term_features, + ) + cli.config.init() + + assert cli.config.uv_available is False + + output = capsys.readouterr() + assert "uv is not available" in output.out + assert cli.config.venv_pip_install_cmd.endswith("/bin/python -m pip install") + + +def test_uv_disabled(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + """Test uv disabled and not available. + + Args: + monkeypatch: Pytest fixture. + capsys: Pytest fixture for capturing output. + """ + monkeypatch.setattr("sys.argv", ["ansible-dev-environment", "install", "--no-uv", "-vvv"]) + + cli = Cli() + cli.parse_args() + cli.init_output() + cli.config = Config( + args=cli.args, + output=cli.output, + term_features=cli.term_features, + ) + cli.config.init() + + assert cli.config.uv_available is False + + output = capsys.readouterr() + assert "uv is disabled" in output.out + assert cli.config.venv_pip_install_cmd.endswith("/bin/python -m pip install") diff --git a/tests/unit/test_installer.py b/tests/unit/test_installer.py index 9f74afd..4144f8d 100644 --- a/tests/unit/test_installer.py +++ b/tests/unit/test_installer.py @@ -276,6 +276,8 @@ def test_no_adt_install( collection_specifier=None, requirement=None, cpi=None, + subcommand="install", + uv=True, ) config = Config(args=args, output=output, term_features=output.term_features) @@ -310,6 +312,8 @@ def test_adt_install( collection_specifier=None, requirement=None, cpi=None, + subcommand="install", + uv=True, ) config = Config(args=args, output=output, term_features=output.term_features) diff --git a/tests/unit/test_treemaker.py b/tests/unit/test_treemaker.py index e50f83d..c6329dc 100644 --- a/tests/unit/test_treemaker.py +++ b/tests/unit/test_treemaker.py @@ -65,6 +65,8 @@ def test_tree_empty( args = Namespace( venv=venv_path, verbose=0, + subcommand="tree", + uv=True, ) output._verbosity = 0 config = Config(args=args, output=output, term_features=output.term_features) @@ -96,6 +98,8 @@ def test_tree_malformed_info( args = Namespace( venv=venv_path, verbose=0, + subcommand="tree", + uv=True, ) def collect_manifests( @@ -151,6 +155,8 @@ def test_tree_malformed_deps( args = Namespace( venv=venv_path, verbose=0, + subcommand="tree", + uv=True, ) def collect_manifests( @@ -208,6 +214,8 @@ def test_tree_malformed_deps_not_string( args = Namespace( venv=venv_path, verbose=0, + subcommand="tree", + uv=True, ) def collect_manifests( @@ -262,10 +270,7 @@ def test_tree_malformed_repo_not_string( venv_path = tmp_path / "venv" SafeEnvBuilder().create(venv_path) - args = Namespace( - venv=venv_path, - verbose=0, - ) + args = Namespace(venv=venv_path, verbose=0, subcommand="tree", uv=True) def collect_manifests( target: Path, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 0494e07..2329855 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -13,7 +13,7 @@ ) from ansible_dev_environment.config import Config from ansible_dev_environment.output import Output -from ansible_dev_environment.utils import TermFeatures, builder_introspect +from ansible_dev_environment.utils import TermFeatures, builder_introspect, str_to_bool term_features = TermFeatures(color=False, links=False) @@ -125,12 +125,17 @@ def test_parse_collection_request(scenario: tuple[str, Collection | None]) -> No assert parse_collection_request(string=string, config=config, output=output) == spec -def test_builder_found(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_builder_found( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + session_venv: Config, +) -> None: """Test that builder is found. Args: tmp_path: A temporary path monkeypatch: The pytest Monkeypatch fixture + session_venv: The session venv Raises: AssertionError: if either file is not found @@ -150,7 +155,13 @@ def cache_dir(_self: Config) -> Path: monkeypatch.setattr(Config, "cache_dir", cache_dir) - args = Namespace(venv=str(tmp_path / ".venv"), system_site_packages=False, verbose=0) + args = Namespace( + venv=session_venv.venv, + system_site_packages=False, + verbose=0, + subcommand="check", + uv=True, + ) cfg = Config( args=args, @@ -163,3 +174,25 @@ def cache_dir(_self: Config) -> Path: assert cfg.discovered_bindep_reqs.exists() is True assert cfg.discovered_python_reqs.exists() is True + + +def test_str_to_bool() -> None: + """Test the str_to_bool function. + + This function tests the conversion of string values to boolean values. + """ + assert str_to_bool("true") is True + assert str_to_bool("True") is True + assert str_to_bool("1") is True + assert str_to_bool("yes") is True + assert str_to_bool("y") is True + assert str_to_bool("on") is True + + assert str_to_bool("false") is False + assert str_to_bool("False") is False + assert str_to_bool("0") is False + assert str_to_bool("no") is False + assert str_to_bool("n") is False + assert str_to_bool("off") is False + + assert str_to_bool("anything else") is None