diff --git a/salt/modules/pip.py b/salt/modules/pip.py index 64b2f7f9bbc0..7d0616502bdd 100644 --- a/salt/modules/pip.py +++ b/salt/modules/pip.py @@ -79,6 +79,7 @@ # Import python libs import logging import os + try: import pkg_resources except ImportError: @@ -441,6 +442,7 @@ def install(pkgs=None, # pylint: disable=R0912,R0913,R0914 no_cache_dir=False, cache_dir=None, no_binary=None, + extra_args=None, **kwargs): ''' Install packages with pip @@ -612,6 +614,24 @@ def install(pkgs=None, # pylint: disable=R0912,R0913,R0914 no_cache_dir Disable the cache. + 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']" + + .. 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. + + Will be translated into the following pip command: + + .. code-block:: bash + + pip install pandas --latest-pip-kwarg param --latest-pip-arg + CLI Example: .. code-block:: bash @@ -895,6 +915,24 @@ def install(pkgs=None, # pylint: disable=R0912,R0913,R0914 if trusted_host: cmd.extend(['--trusted-host', trusted_host]) + 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("Too many levels in: {}".format(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(saltenv=saltenv, use_vt=use_vt, runas=user) if kwargs: diff --git a/salt/states/pip_state.py b/salt/states/pip_state.py index edc79762f31b..6d80e4a9d5b0 100644 --- a/salt/states/pip_state.py +++ b/salt/states/pip_state.py @@ -21,8 +21,10 @@ # Import python libs from __future__ import absolute_import, print_function, unicode_literals -import re + import logging +import re + try: import pkg_resources HAS_PKG_RESOURCES = True @@ -341,6 +343,7 @@ def installed(name, no_cache_dir=False, cache_dir=None, no_binary=None, + extra_args=None, **kwargs): ''' Make sure the package is installed @@ -602,6 +605,23 @@ def installed(name, - reload_modules: True - exists_action: i + extra_args + pip keyword and positional arguments not yet implemented in salt + + .. code-block:: yaml + + pandas: + pip.installed: + - name: pandas + - extra_args: + - --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. + .. _`virtualenv`: http://www.virtualenv.org/en/latest/ ''' @@ -838,6 +858,7 @@ def installed(name, use_vt=use_vt, trusted_host=trusted_host, no_cache_dir=no_cache_dir, + extra_args=extra_args, **kwargs ) diff --git a/tests/unit/modules/test_pip.py b/tests/unit/modules/test_pip.py index 23e5f03e8e5d..394a98cd2161 100644 --- a/tests/unit/modules/test_pip.py +++ b/tests/unit/modules/test_pip.py @@ -2,18 +2,18 @@ # Import python libs from __future__ import absolute_import + import os import sys -# Import Salt Testing libs -from tests.support.mixins import LoaderModuleMockMixin -from tests.support.unit import skipIf, TestCase -from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch - +import salt.modules.pip as pip # Import salt libs import salt.utils.platform -import salt.modules.pip as pip from salt.exceptions import CommandExecutionError +# Import Salt Testing libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch +from tests.support.unit import skipIf, TestCase @skipIf(NO_MOCK, NO_MOCK_REASON) @@ -803,6 +803,41 @@ def test_install_multiple_requirements_arguments_in_resulting_command(self): python_shell=False, ) + def test_install_extra_args_arguments_in_resulting_command(self): + pkg = 'pep8' + mock = MagicMock(return_value={'retcode': 0, 'stdout': ''}) + with patch.dict(pip.__salt__, {'cmd.run_all': mock}): + pip.install(pkg, extra_args=[ + {"--latest-pip-kwarg": "param"}, + "--latest-pip-arg" + ]) + expected = [ + sys.executable, '-m', 'pip', 'install', pkg, + "--latest-pip-kwarg", "param", "--latest-pip-arg" + ] + mock.assert_called_with( + expected, + saltenv='base', + runas=None, + use_vt=False, + python_shell=False, + ) + + def test_install_extra_args_arguments_recursion_error(self): + pkg = 'pep8' + mock = MagicMock(return_value={'retcode': 0, 'stdout': ''}) + with patch.dict(pip.__salt__, {'cmd.run_all': mock}): + + self.assertRaises(TypeError, lambda: pip.install( + pkg, extra_args=[ + {"--latest-pip-kwarg": ["param1", "param2"]}, + ])) + + self.assertRaises(TypeError, lambda: pip.install( + pkg, extra_args=[ + {"--latest-pip-kwarg": [{"--too-deep": dict()}]}, + ])) + def test_uninstall_multiple_requirements_arguments_in_resulting_command(self): with patch('salt.modules.pip._get_cached_requirements') as get_cached_requirements: cached_reqs = [