diff --git a/changelog/68525.fixed.md b/changelog/68525.fixed.md new file mode 100644 index 000000000000..f21f7fe82e6a --- /dev/null +++ b/changelog/68525.fixed.md @@ -0,0 +1 @@ +Fix `pip.installed` state managed pre-release upgrades diff --git a/requirements/pytest.txt b/requirements/pytest.txt index 376464b0a0d7..81a65e448b2d 100644 --- a/requirements/pytest.txt +++ b/requirements/pytest.txt @@ -1,4 +1,5 @@ mock >= 3.0.0 +packaging # PyTest docker >= 7.1.0; python_version >= '3.8' docker < 7.1.0; python_version < '3.8' diff --git a/salt/modules/pip.py b/salt/modules/pip.py index d108d0815ad8..320dbdf1a40c 100644 --- a/salt/modules/pip.py +++ b/salt/modules/pip.py @@ -1654,7 +1654,11 @@ def list_all_versions( pip_version = version(bin_env=bin_env, cwd=cwd, user=user) if salt.utils.versions.compare(ver1=pip_version, oper=">=", ver2="21.2"): regex = re.compile(r"\s*Available versions: (.*)") - cmd.extend(["index", "versions", pkg]) + # pre-release versions are not included by default + if any([include_alpha, include_beta, include_rc]): + cmd.extend(["index", "versions", "--pre", pkg]) + else: + cmd.extend(["index", "versions", pkg]) else: if salt.utils.versions.compare(ver1=pip_version, oper=">=", ver2="20.3"): cmd.append("--use-deprecated=legacy-resolver") diff --git a/tests/pytests/functional/modules/test_pip.py b/tests/pytests/functional/modules/test_pip.py index fa2b1a6573dc..8c0bd8b0c5f7 100644 --- a/tests/pytests/functional/modules/test_pip.py +++ b/tests/pytests/functional/modules/test_pip.py @@ -6,11 +6,15 @@ from contextlib import contextmanager import pytest +from packaging.version import parse as parse_version import salt.utils.platform from salt.exceptions import CommandNotFoundError +from salt.modules import cmdmod +from salt.modules import pip as pipmod from salt.modules.virtualenv_mod import KNOWN_BINARY_NAMES from tests.support.helpers import VirtualEnv, patched_environ +from tests.support.mock import MagicMock pytestmark = [ pytest.mark.slow_test, @@ -20,6 +24,55 @@ ] +def mock_pip_versions_cmds(*args, **kwargs): + """Function to mock cmd.run_all for pip commands that get package versions but pass-thru other invocations. + + Used by `test_list_available_packages_with_pre_releases_flags` because otherwise we must rely on the presence of + pre-release versions of a package in an uncontrolled package index. + + This can be removed if there were an index that we could guarantee had pre-release versions of a package for any + caller. + """ + + def _is_pip_versions_cmd(cmd): + pip_args = None + for i, arg in enumerate(cmd): + if arg == "pip" or arg.endswith("salt-pip"): + pip_args = cmd[i + 1 :] + if pip_args is None: + return False + if pip_args[0] == "--use-deprecated=legacy-resolver": + pip_args = pip_args[1:] + if ( + pip_args[:2] == ["index", "versions"] + or pip_args[0] == "install" + and pip_args[1].endswith("==versions") + ): + return True + return False + + if _is_pip_versions_cmd(args[0]): + versions = "1.0.0, 1.0.1rc1, 1.0.2b1, 1.0.3.a1" + return { + "stdout": "\n".join( + [ + f"Available versions: {versions}", + f"Could not find a version (from versions: {versions})", + ] + ) + } + return cmdmod.run_all(*args, **kwargs) + + +@pytest.fixture +def configure_loader_modules(): + return { + pipmod: { + "__salt__": {"cmd.run_all": MagicMock(side_effect=mock_pip_versions_cmds)} + } + } + + @pytest.fixture def venv(tmp_path): with VirtualEnv(venv_dir=tmp_path / "the-venv") as venv: @@ -113,6 +166,47 @@ def test_list_available_packages_with_index_url(pip, pip_version, tmp_path): assert available_versions +@pytest.mark.parametrize( + "pip_version", + ( + pytest.param( + "pip==9.0.3", + marks=pytest.mark.skipif( + sys.version_info >= (3, 10), + reason="'pip==9.0.3' is not available on Py >= 3.10", + ), + ), + "pip<20.0", + "pip<21.0", + "pip>=21.0", + ), +) +@pytest.mark.parametrize("include_alpha", (True, False)) +@pytest.mark.parametrize("include_beta", (True, False)) +@pytest.mark.parametrize("include_rc", (True, False)) +def test_list_available_packages_with_pre_releases_flags( + venv, pip, pip_version, include_alpha, include_beta, include_rc +): + """Tests that pre-release versions are returned when flags enable them. + + Note: relies on the `configure_loader_modules` fixture to mock the versions we test with. + """ + venv.install("-U", pip_version) + versions = pipmod.list_all_versions( + "foo", + bin_env=str(venv.venv_bin_dir), + include_alpha=include_alpha, + include_beta=include_beta, + include_rc=include_rc, + ) + + has_prerelease = any(map(lambda v: v.is_prerelease, map(parse_version, versions))) + if any([include_alpha, include_beta, include_rc]): + assert has_prerelease + else: + assert not has_prerelease + + def test_issue_2087_missing_pip(venv, pip, modules): # Let's remove the pip binary pip_bin = venv.venv_bin_dir / "pip"