diff --git a/salt/modules/pip.py b/salt/modules/pip.py index a456b3b9c612..47d22cc3ead2 100644 --- a/salt/modules/pip.py +++ b/salt/modules/pip.py @@ -431,6 +431,7 @@ def install(pkgs=None, # pylint: disable=R0912,R0913,R0914 use_vt=False, trusted_host=None, no_cache_dir=False, + extra_args=None, cache_dir=None, no_binary=None, disable_version_check=False, @@ -605,6 +606,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 + disable_version_check Pip may periodically check PyPI to determine whether a new version of pip is available to download. Passing True for this option disables @@ -896,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 1134c059786b..a855b0c59c9e 100644 --- a/salt/states/pip_state.py +++ b/salt/states/pip_state.py @@ -404,6 +404,7 @@ def installed(name, no_cache_dir=False, cache_dir=None, no_binary=None, + extra_args=None, **kwargs): ''' Make sure the package is installed @@ -665,6 +666,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/ ''' @@ -901,6 +919,7 @@ def installed(name, use_vt=use_vt, trusted_host=trusted_host, no_cache_dir=no_cache_dir, + extra_args=extra_args, disable_version_check=True, **kwargs ) diff --git a/tests/unit/modules/test_pip.py b/tests/unit/modules/test_pip.py index 23e5f03e8e5d..c951ab6d0324 100644 --- a/tests/unit/modules/test_pip.py +++ b/tests/unit/modules/test_pip.py @@ -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 = [ @@ -1280,8 +1315,7 @@ def test_is_installed_false(self): def test_install_pre_argument_in_resulting_command(self): pkg = 'pep8' - # Lower than 1.4 versions don't end-up with `--pre` in the resulting - # output + # Lower than 1.4 versions don't end up with `--pre` in the resulting output mock = MagicMock(side_effect=[ {'retcode': 0, 'stdout': 'pip 1.2.0 /path/to/site-packages/pip'}, {'retcode': 0, 'stdout': ''}