diff --git a/changelog/65870.added.md b/changelog/65870.added.md new file mode 100644 index 00000000000..f6a2eeca99c --- /dev/null +++ b/changelog/65870.added.md @@ -0,0 +1 @@ +Added extra_args functionality to pip.uninstall module function diff --git a/salt/modules/pip.py b/salt/modules/pip.py index 208a9be5256..c4843658909 100644 --- a/salt/modules/pip.py +++ b/salt/modules/pip.py @@ -1058,6 +1058,7 @@ def uninstall( cwd=None, saltenv="base", use_vt=False, + extra_args=None, ): """ Uninstall packages individually or from a pip requirements file @@ -1100,6 +1101,26 @@ def uninstall( use_vt Use VT terminal emulation (see output while installing) + extra_args + pip keyword and positional arguments not yet implemented in salt + + .. code-block:: yaml + + salt '*' pip.install pandas extra_args="[{'--latest-pip-kwarg':'param'}, '--latest-pip-arg']" + + Will be translated into the following pip command: + + .. code-block:: bash + + pip install pandas --latest-pip-kwarg param --latest-pip-arg + + .. warning:: + + If unsupported options are passed here that are not supported in a + minion's version of pip, a `No such option error` will be thrown. + + .. versionadded:: 3008.0 + CLI Example: .. code-block:: bash @@ -1170,6 +1191,24 @@ def uninstall( pass cmd.extend(pkgs) + if extra_args: + # These are arguments from the latest version of pip that + # have not yet been implemented in salt + for arg in extra_args: + # It is a keyword argument + if isinstance(arg, dict): + # There will only ever be one item in this dictionary + key, val = arg.popitem() + # Don't allow any recursion into keyword arg definitions + # Don't allow multiple definitions of a keyword + if isinstance(val, (dict, list)): + raise TypeError(f"Too many levels in: {key}") + # This is a a normal one-to-one keyword argument + cmd.extend([key, val]) + # It is a positional argument, append it to the list + else: + cmd.append(arg) + cmd_kwargs = dict( python_shell=False, runas=user, cwd=cwd, saltenv=saltenv, use_vt=use_vt ) diff --git a/tests/pytests/unit/modules/test_pip.py b/tests/pytests/unit/modules/test_pip.py index 468b2983e1c..af53aa7b407 100644 --- a/tests/pytests/unit/modules/test_pip.py +++ b/tests/pytests/unit/modules/test_pip.py @@ -1355,6 +1355,52 @@ def test_uninstall_timeout_argument_in_resulting_command(python_binary): pytest.raises(ValueError, pip.uninstall, pkg, timeout="a") +def test_uninstall_extra_args_arguments_in_resulting_command(python_binary): + pkg = "pep8" + mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) + with patch.dict(pip.__salt__, {"cmd.run_all": mock}): + pip.uninstall( + pkg, extra_args=[{"--latest-pip-kwarg": "param"}, "--latest-pip-arg"] + ) + expected = [ + *python_binary, + "uninstall", + "-y", + pkg, + "--latest-pip-kwarg", + "param", + "--latest-pip-arg", + ] + mock.assert_called_with( + expected, + saltenv="base", + cwd=None, + runas=None, + use_vt=False, + python_shell=False, + ) + + +def test_uninstall_extra_args_arguments_recursion_error(): + pkg = "pep8" + mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) + with patch.dict(pip.__salt__, {"cmd.run_all": mock}): + + pytest.raises( + TypeError, + lambda: pip.uninstall( + pkg, extra_args=[{"--latest-pip-kwarg": ["param1", "param2"]}] + ), + ) + + pytest.raises( + TypeError, + lambda: pip.uninstall( + pkg, extra_args=[{"--latest-pip-kwarg": [{"--too-deep": dict()}]}] + ), + ) + + def test_freeze_command(python_binary): expected = [*python_binary, "freeze"] eggs = [