diff options
Diffstat (limited to 'lib/ansible/modules/pip.py')
-rw-r--r-- | lib/ansible/modules/pip.py | 129 |
1 files changed, 81 insertions, 48 deletions
diff --git a/lib/ansible/modules/pip.py b/lib/ansible/modules/pip.py index 95a5d0d3..3a073c85 100644 --- a/lib/ansible/modules/pip.py +++ b/lib/ansible/modules/pip.py @@ -12,8 +12,8 @@ DOCUMENTATION = ''' module: pip short_description: Manages Python library dependencies description: - - "Manage Python library dependencies. To use this module, one of the following keys is required: C(name) - or C(requirements)." + - "Manage Python library dependencies. To use this module, one of the following keys is required: O(name) + or O(requirements)." version_added: "0.7" options: name: @@ -24,7 +24,7 @@ options: elements: str version: description: - - The version number to install of the Python library specified in the I(name) parameter. + - The version number to install of the Python library specified in the O(name) parameter. type: str requirements: description: @@ -53,17 +53,17 @@ options: virtualenv_command: description: - The command or a pathname to the command to create the virtual - environment with. For example C(pyvenv), C(virtualenv), - C(virtualenv2), C(~/bin/virtualenv), C(/usr/local/bin/virtualenv). + environment with. For example V(pyvenv), V(virtualenv), + V(virtualenv2), V(~/bin/virtualenv), V(/usr/local/bin/virtualenv). type: path default: virtualenv version_added: "1.1" virtualenv_python: description: - The Python executable used for creating the virtual environment. - For example C(python3.5), C(python2.7). When not specified, the + For example V(python3.12), V(python2.7). When not specified, the Python version used to run the ansible module is used. This parameter - should not be used when C(virtualenv_command) is using C(pyvenv) or + should not be used when O(virtualenv_command) is using V(pyvenv) or the C(-m venv) module. type: str version_added: "2.0" @@ -94,9 +94,9 @@ options: description: - The explicit executable or pathname for the pip executable, if different from the Ansible Python interpreter. For - example C(pip3.3), if there are both Python 2.7 and 3.3 installations + example V(pip3.3), if there are both Python 2.7 and 3.3 installations in the system and you want to run pip for the Python 3.3 installation. - - Mutually exclusive with I(virtualenv) (added in 2.1). + - Mutually exclusive with O(virtualenv) (added in 2.1). - Does not affect the Ansible Python interpreter. - The setuptools package must be installed for both the Ansible Python interpreter and for the version of Python specified by this option. @@ -127,16 +127,16 @@ notes: installed on the remote host if the virtualenv parameter is specified and the virtualenv needs to be created. - Although it executes using the Ansible Python interpreter, the pip module shells out to - run the actual pip command, so it can use any pip version you specify with I(executable). + run the actual pip command, so it can use any pip version you specify with O(executable). By default, it uses the pip version for the Ansible Python interpreter. For example, pip3 on python 3, and pip2 or pip on python 2. - The interpreter used by Ansible (see R(ansible_python_interpreter, ansible_python_interpreter)) requires the setuptools package, regardless of the version of pip set with - the I(executable) option. + the O(executable) option. requirements: - pip - virtualenv -- setuptools +- setuptools or packaging author: - Matt Wright (@mattupstate) ''' @@ -266,6 +266,7 @@ virtualenv: sample: "/tmp/virtualenv" ''' +import argparse import os import re import sys @@ -273,20 +274,28 @@ import tempfile import operator import shlex import traceback -import types from ansible.module_utils.compat.version import LooseVersion -SETUPTOOLS_IMP_ERR = None +PACKAGING_IMP_ERR = None +HAS_PACKAGING = False +HAS_SETUPTOOLS = False try: - from pkg_resources import Requirement - - HAS_SETUPTOOLS = True -except ImportError: - HAS_SETUPTOOLS = False - SETUPTOOLS_IMP_ERR = traceback.format_exc() + from packaging.requirements import Requirement as parse_requirement + HAS_PACKAGING = True +except Exception: + # This is catching a generic Exception, due to packaging on EL7 raising a TypeError on import + HAS_PACKAGING = False + PACKAGING_IMP_ERR = traceback.format_exc() + try: + from pkg_resources import Requirement + parse_requirement = Requirement.parse # type: ignore[misc,assignment] + del Requirement + HAS_SETUPTOOLS = True + except ImportError: + pass -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.basic import AnsibleModule, is_executable, missing_required_lib from ansible.module_utils.common.locale import get_best_parsable_locale from ansible.module_utils.six import PY3 @@ -295,8 +304,16 @@ from ansible.module_utils.six import PY3 #: Python one-liners to be run at the command line that will determine the # installed version for these special libraries. These are libraries that # don't end up in the output of pip freeze. -_SPECIAL_PACKAGE_CHECKERS = {'setuptools': 'import setuptools; print(setuptools.__version__)', - 'pip': 'import pkg_resources; print(pkg_resources.get_distribution("pip").version)'} +_SPECIAL_PACKAGE_CHECKERS = { + 'importlib': { + 'setuptools': 'from importlib.metadata import version; print(version("setuptools"))', + 'pip': 'from importlib.metadata import version; print(version("pip"))', + }, + 'pkg_resources': { + 'setuptools': 'import setuptools; print(setuptools.__version__)', + 'pip': 'import pkg_resources; print(pkg_resources.get_distribution("pip").version)', + } +} _VCS_RE = re.compile(r'(svn|git|hg|bzr)\+') @@ -309,6 +326,18 @@ def _is_vcs_url(name): return re.match(_VCS_RE, name) +def _is_venv_command(command): + venv_parser = argparse.ArgumentParser() + venv_parser.add_argument('-m', type=str) + argv = shlex.split(command) + if argv[0] == 'pyvenv': + return True + args, dummy = venv_parser.parse_known_args(argv[1:]) + if args.m == 'venv': + return True + return False + + def _is_package_name(name): """Test whether the name is a package name or a version specifier.""" return not name.lstrip().startswith(tuple(op_dict.keys())) @@ -461,7 +490,7 @@ def _have_pip_module(): # type: () -> bool except ImportError: find_spec = None # type: ignore[assignment] # type: ignore[no-redef] - if find_spec: + if find_spec: # type: ignore[truthy-function] # noinspection PyBroadException try: # noinspection PyUnresolvedReferences @@ -493,7 +522,7 @@ def _fail(module, cmd, out, err): module.fail_json(cmd=cmd, msg=msg) -def _get_package_info(module, package, env=None): +def _get_package_info(module, package, python_bin=None): """This is only needed for special packages which do not show up in pip freeze pip and setuptools fall into this category. @@ -501,20 +530,19 @@ def _get_package_info(module, package, env=None): :returns: a string containing the version number if the package is installed. None if the package is not installed. """ - if env: - opt_dirs = ['%s/bin' % env] - else: - opt_dirs = [] - python_bin = module.get_bin_path('python', False, opt_dirs) - if python_bin is None: + return + + discovery_mechanism = 'pkg_resources' + importlib_rc = module.run_command([python_bin, '-c', 'import importlib.metadata'])[0] + if importlib_rc == 0: + discovery_mechanism = 'importlib' + + rc, out, err = module.run_command([python_bin, '-c', _SPECIAL_PACKAGE_CHECKERS[discovery_mechanism][package]]) + if rc: formatted_dep = None else: - rc, out, err = module.run_command([python_bin, '-c', _SPECIAL_PACKAGE_CHECKERS[package]]) - if rc: - formatted_dep = None - else: - formatted_dep = '%s==%s' % (package, out.strip()) + formatted_dep = '%s==%s' % (package, out.strip()) return formatted_dep @@ -543,7 +571,7 @@ def setup_virtualenv(module, env, chdir, out, err): virtualenv_python = module.params['virtualenv_python'] # -p is a virtualenv option, not compatible with pyenv or venv # this conditional validates if the command being used is not any of them - if not any(ex in module.params['virtualenv_command'] for ex in ('pyvenv', '-m venv')): + if not _is_venv_command(module.params['virtualenv_command']): if virtualenv_python: cmd.append('-p%s' % virtualenv_python) elif PY3: @@ -592,13 +620,15 @@ class Package: separator = '==' if version_string[0].isdigit() else ' ' name_string = separator.join((name_string, version_string)) try: - self._requirement = Requirement.parse(name_string) + self._requirement = parse_requirement(name_string) # old pkg_resource will replace 'setuptools' with 'distribute' when it's already installed - if self._requirement.project_name == "distribute" and "setuptools" in name_string: + project_name = Package.canonicalize_name( + getattr(self._requirement, 'name', None) or getattr(self._requirement, 'project_name', None) + ) + if project_name == "distribute" and "setuptools" in name_string: self.package_name = "setuptools" - self._requirement.project_name = "setuptools" else: - self.package_name = Package.canonicalize_name(self._requirement.project_name) + self.package_name = project_name self._plain_package = True except ValueError as e: pass @@ -606,7 +636,7 @@ class Package: @property def has_version_specifier(self): if self._plain_package: - return bool(self._requirement.specs) + return bool(getattr(self._requirement, 'specifier', None) or getattr(self._requirement, 'specs', None)) return False def is_satisfied_by(self, version_to_test): @@ -662,9 +692,9 @@ def main(): supports_check_mode=True, ) - if not HAS_SETUPTOOLS: - module.fail_json(msg=missing_required_lib("setuptools"), - exception=SETUPTOOLS_IMP_ERR) + if not HAS_SETUPTOOLS and not HAS_PACKAGING: + module.fail_json(msg=missing_required_lib("packaging"), + exception=PACKAGING_IMP_ERR) state = module.params['state'] name = module.params['name'] @@ -704,6 +734,9 @@ def main(): if not os.path.exists(os.path.join(env, 'bin', 'activate')): venv_created = True out, err = setup_virtualenv(module, env, chdir, out, err) + py_bin = os.path.join(env, 'bin', 'python') + else: + py_bin = module.params['executable'] or sys.executable pip = _get_pip(module, env, module.params['executable']) @@ -786,7 +819,7 @@ def main(): # So we need to get those via a specialcase for pkg in ('setuptools', 'pip'): if pkg in name: - formatted_dep = _get_package_info(module, pkg, env) + formatted_dep = _get_package_info(module, pkg, py_bin) if formatted_dep is not None: pkg_list.append(formatted_dep) out += '%s\n' % formatted_dep @@ -800,7 +833,7 @@ def main(): out_freeze_before = None if requirements or has_vcs: - _, out_freeze_before, _ = _get_packages(module, pip, chdir) + dummy, out_freeze_before, dummy = _get_packages(module, pip, chdir) rc, out_pip, err_pip = module.run_command(cmd, path_prefix=path_prefix, cwd=chdir) out += out_pip @@ -817,7 +850,7 @@ def main(): if out_freeze_before is None: changed = 'Successfully installed' in out_pip else: - _, out_freeze_after, _ = _get_packages(module, pip, chdir) + dummy, out_freeze_after, dummy = _get_packages(module, pip, chdir) changed = out_freeze_before != out_freeze_after changed = changed or venv_created |