diff --git a/changelogs/fragments/8793-pipx-global.yml b/changelogs/fragments/8793-pipx-global.yml new file mode 100644 index 0000000000..c3d7f5157f --- /dev/null +++ b/changelogs/fragments/8793-pipx-global.yml @@ -0,0 +1,12 @@ +minor_changes: + - pipx - added parameter ``global`` to module (https://github.com/ansible-collections/community.general/pull/8793). + - pipx_info - added parameter ``global`` to module (https://github.com/ansible-collections/community.general/pull/8793). +deprecated_features: + - > + pipx - + support for versions of the command line tool ``pipx`` older than ``1.7.0`` is deprecated and will be removed in community.general 11.0.0 + (https://github.com/ansible-collections/community.general/pull/8793). + - > + pipx_info - + support for versions of the command line tool ``pipx`` older than ``1.7.0`` is deprecated and will be removed in community.general 11.0.0 + (https://github.com/ansible-collections/community.general/pull/8793). diff --git a/plugins/module_utils/pipx.py b/plugins/module_utils/pipx.py index 3f493545d5..054de886a4 100644 --- a/plugins/module_utils/pipx.py +++ b/plugins/module_utils/pipx.py @@ -24,26 +24,29 @@ _state_map = dict( def pipx_runner(module, command, **kwargs): + arg_formats = dict( + state=fmt.as_map(_state_map), + name=fmt.as_list(), + name_source=fmt.as_func(fmt.unpack_args(lambda n, s: [s] if s else [n])), + install_apps=fmt.as_bool("--include-apps"), + install_deps=fmt.as_bool("--include-deps"), + inject_packages=fmt.as_list(), + force=fmt.as_bool("--force"), + include_injected=fmt.as_bool("--include-injected"), + index_url=fmt.as_opt_val('--index-url'), + python=fmt.as_opt_val('--python'), + system_site_packages=fmt.as_bool("--system-site-packages"), + _list=fmt.as_fixed(['list', '--include-injected', '--json']), + editable=fmt.as_bool("--editable"), + pip_args=fmt.as_opt_eq_val('--pip-args'), + suffix=fmt.as_opt_val('--suffix'), + ) + arg_formats["global"] = fmt.as_bool("--global") + runner = CmdRunner( module, command=command, - arg_formats=dict( - state=fmt.as_map(_state_map), - name=fmt.as_list(), - name_source=fmt.as_func(fmt.unpack_args(lambda n, s: [s] if s else [n])), - install_apps=fmt.as_bool("--include-apps"), - install_deps=fmt.as_bool("--include-deps"), - inject_packages=fmt.as_list(), - force=fmt.as_bool("--force"), - include_injected=fmt.as_bool("--include-injected"), - index_url=fmt.as_opt_val('--index-url'), - python=fmt.as_opt_val('--python'), - system_site_packages=fmt.as_bool("--system-site-packages"), - _list=fmt.as_fixed(['list', '--include-injected', '--json']), - editable=fmt.as_bool("--editable"), - pip_args=fmt.as_opt_eq_val('--pip-args'), - suffix=fmt.as_opt_val('--suffix'), - ), + arg_formats=arg_formats, environ_update={'USE_EMOJI': '0'}, check_rc=True, **kwargs diff --git a/plugins/modules/pipx.py b/plugins/modules/pipx.py index 7f4954850f..1a73ae00bd 100644 --- a/plugins/modules/pipx.py +++ b/plugins/modules/pipx.py @@ -117,11 +117,19 @@ options: suffix: description: - Optional suffix for virtual environment and executable names. - - "B(Warning): C(pipx) documentation states this is an B(experimental) feature subject to change." + - "B(Warning:) C(pipx) documentation states this is an B(experimental) feature subject to change." type: str version_added: 9.3.0 + global: + description: + - The module will pass the C(--global) argument to C(pipx), to execute actions in global scope. + - The C(--global) is only available in C(pipx>=1.6.0), so make sure to have a compatible version when using this option. + Moreover, a nasty bug with C(--global) was fixed in C(pipx==1.7.0), so it is strongly recommended you used that version or newer. + type: bool + default: false + version_added: 9.4.0 notes: - - This module requires C(pipx) version 0.16.2.1 or above. + - This module requires C(pipx) version 0.16.2.1 or above. From community.general 11.0.0 onwards, the module will require C(pipx>=1.7.0). - Please note that C(pipx) requires Python 3.6 or above. - This module does not install the C(pipx) python package, however that can be easily done with the module M(ansible.builtin.pip). - This module does not require C(pipx) to be in the shell C(PATH), but it must be loadable by Python as a module. @@ -191,26 +199,29 @@ def _make_name(name, suffix): class PipX(StateModuleHelper): output_params = ['name', 'source', 'index_url', 'force', 'installdeps'] + argument_spec = dict( + state=dict(type='str', default='install', + choices=['present', 'absent', 'install', 'uninstall', 'uninstall_all', + 'inject', 'upgrade', 'upgrade_all', 'reinstall', 'reinstall_all', 'latest']), + name=dict(type='str'), + source=dict(type='str'), + install_apps=dict(type='bool', default=False), + install_deps=dict(type='bool', default=False), + inject_packages=dict(type='list', elements='str'), + force=dict(type='bool', default=False), + include_injected=dict(type='bool', default=False), + index_url=dict(type='str'), + python=dict(type='str'), + system_site_packages=dict(type='bool', default=False), + executable=dict(type='path'), + editable=dict(type='bool', default=False), + pip_args=dict(type='str'), + suffix=dict(type='str'), + ) + argument_spec["global"] = dict(type='bool', default=False) + module = dict( - argument_spec=dict( - state=dict(type='str', default='install', - choices=['present', 'absent', 'install', 'uninstall', 'uninstall_all', - 'inject', 'upgrade', 'upgrade_all', 'reinstall', 'reinstall_all', 'latest']), - name=dict(type='str'), - source=dict(type='str'), - install_apps=dict(type='bool', default=False), - install_deps=dict(type='bool', default=False), - inject_packages=dict(type='list', elements='str'), - force=dict(type='bool', default=False), - include_injected=dict(type='bool', default=False), - index_url=dict(type='str'), - python=dict(type='str'), - system_site_packages=dict(type='bool', default=False), - executable=dict(type='path'), - editable=dict(type='bool', default=False), - pip_args=dict(type='str'), - suffix=dict(type='str'), - ), + argument_spec=argument_spec, required_if=[ ('state', 'present', ['name']), ('state', 'install', ['name']), @@ -279,8 +290,8 @@ class PipX(StateModuleHelper): def state_install(self): if not self.vars.application or self.vars.force: self.changed = True - args = 'state index_url install_deps force python system_site_packages editable pip_args suffix name_source' - with self.runner(args, check_mode_skip=True) as ctx: + args_order = 'state global index_url install_deps force python system_site_packages editable pip_args suffix name_source' + with self.runner(args_order, check_mode_skip=True) as ctx: ctx.run(name_source=[self.vars.name, self.vars.source]) self._capture_results(ctx) @@ -293,14 +304,14 @@ class PipX(StateModuleHelper): if self.vars.force: self.changed = True - with self.runner('state include_injected index_url force editable pip_args name', check_mode_skip=True) as ctx: + with self.runner('state global include_injected index_url force editable pip_args name', check_mode_skip=True) as ctx: ctx.run(name=name) self._capture_results(ctx) def state_uninstall(self): if self.vars.application: name = _make_name(self.vars.name, self.vars.suffix) - with self.runner('state name', check_mode_skip=True) as ctx: + with self.runner('state global name', check_mode_skip=True) as ctx: ctx.run(name=name) self._capture_results(ctx) @@ -311,7 +322,7 @@ class PipX(StateModuleHelper): if not self.vars.application: self.do_raise("Trying to reinstall a non-existent application: {0}".format(name)) self.changed = True - with self.runner('state name python', check_mode_skip=True) as ctx: + with self.runner('state global name python', check_mode_skip=True) as ctx: ctx.run(name=name) self._capture_results(ctx) @@ -321,32 +332,32 @@ class PipX(StateModuleHelper): self.do_raise("Trying to inject packages into a non-existent application: {0}".format(name)) if self.vars.force: self.changed = True - with self.runner('state index_url install_apps install_deps force editable pip_args name inject_packages', check_mode_skip=True) as ctx: + with self.runner('state global index_url install_apps install_deps force editable pip_args name inject_packages', check_mode_skip=True) as ctx: ctx.run(name=name) self._capture_results(ctx) def state_uninstall_all(self): - with self.runner('state', check_mode_skip=True) as ctx: + with self.runner('state global', check_mode_skip=True) as ctx: ctx.run() self._capture_results(ctx) def state_reinstall_all(self): - with self.runner('state python', check_mode_skip=True) as ctx: + with self.runner('state global python', check_mode_skip=True) as ctx: ctx.run() self._capture_results(ctx) def state_upgrade_all(self): if self.vars.force: self.changed = True - with self.runner('state include_injected force', check_mode_skip=True) as ctx: + with self.runner('state global include_injected force', check_mode_skip=True) as ctx: ctx.run() self._capture_results(ctx) def state_latest(self): if not self.vars.application or self.vars.force: self.changed = True - args = 'state index_url install_deps force python system_site_packages editable pip_args suffix name_source' - with self.runner(args, check_mode_skip=True) as ctx: + args_order = 'state index_url install_deps force python system_site_packages editable pip_args suffix name_source' + with self.runner(args_order, check_mode_skip=True) as ctx: ctx.run(state='install', name_source=[self.vars.name, self.vars.source]) self._capture_results(ctx) diff --git a/plugins/modules/pipx_info.py b/plugins/modules/pipx_info.py index 992ca79419..dee3125da2 100644 --- a/plugins/modules/pipx_info.py +++ b/plugins/modules/pipx_info.py @@ -47,14 +47,22 @@ options: If not specified, the module will use C(python -m pipx) to run the tool, using the same Python interpreter as ansible itself. type: path + global: + description: + - The module will pass the C(--global) argument to C(pipx), to execute actions in global scope. + - The C(--global) is only available in C(pipx>=1.6.0), so make sure to have a compatible version when using this option. + Moreover, a nasty bug with C(--global) was fixed in C(pipx==1.7.0), so it is strongly recommended you used that version or newer. + type: bool + default: false + version_added: 9.3.0 notes: + - This module requires C(pipx) version 0.16.2.1 or above. From community.general 11.0.0 onwards, the module will require C(pipx>=1.7.0). + - Please note that C(pipx) requires Python 3.6 or above. - This module does not install the C(pipx) python package, however that can be easily done with the module M(ansible.builtin.pip). - This module does not require C(pipx) to be in the shell C(PATH), but it must be loadable by Python as a module. - > This module will honor C(pipx) environment variables such as but not limited to E(PIPX_HOME) and E(PIPX_BIN_DIR) passed using the R(environment Ansible keyword, playbooks_environment). - - This module requires C(pipx) version 0.16.2.1 or above. - - Please note that C(pipx) requires Python 3.6 or above. - See also the C(pipx) documentation at U(https://pypa.github.io/pipx/). author: - "Alexei Znamensky (@russoz)" @@ -140,14 +148,16 @@ from ansible.module_utils.facts.compat import ansible_facts class PipXInfo(ModuleHelper): output_params = ['name'] + argument_spec = dict( + name=dict(type='str'), + include_deps=dict(type='bool', default=False), + include_injected=dict(type='bool', default=False), + include_raw=dict(type='bool', default=False), + executable=dict(type='path'), + ) + argument_spec["global"] = dict(type='bool', default=False) module = dict( - argument_spec=dict( - name=dict(type='str'), - include_deps=dict(type='bool', default=False), - include_injected=dict(type='bool', default=False), - include_raw=dict(type='bool', default=False), - executable=dict(type='path'), - ), + argument_spec=argument_spec, supports_check_mode=True, ) use_old_vardict = False @@ -195,7 +205,7 @@ class PipXInfo(ModuleHelper): return results - with self.runner('_list', output_process=process_list) as ctx: + with self.runner('_list global', output_process=process_list) as ctx: self.vars.application = ctx.run(_list=1) self._capture_results(ctx) diff --git a/tests/integration/targets/pipx/tasks/main.yml b/tests/integration/targets/pipx/tasks/main.yml index aee8948b90..f1a993aa56 100644 --- a/tests/integration/targets/pipx/tasks/main.yml +++ b/tests/integration/targets/pipx/tasks/main.yml @@ -217,158 +217,33 @@ - "'tox' not in uninstall_tox_again.application" ############################################################################## -- name: ensure application pylint is uninstalled - community.general.pipx: - name: pylint - state: absent -- name: install application pylint - community.general.pipx: - name: pylint - register: install_pylint +- name: Include testcase for inject packages + ansible.builtin.include_tasks: testcase-injectpkg.yml -- name: inject packages - community.general.pipx: - state: inject - name: pylint - inject_packages: - - licenses - register: inject_pkgs_pylint +- name: Include testcase for jupyter + ansible.builtin.include_tasks: testcase-jupyter.yml -- name: inject packages with apps - community.general.pipx: - state: inject - name: pylint - inject_packages: - - black - install_apps: true - register: inject_pkgs_apps_pylint +- name: Include testcase for old site-wide + ansible.builtin.include_tasks: testcase-oldsitewide.yml -- name: cleanup pylint - community.general.pipx: - state: absent - name: pylint - register: uninstall_pylint +- name: Include testcase for issue 7497 + ansible.builtin.include_tasks: testcase-7497.yml -- name: check assertions inject_packages - assert: - that: - - install_pylint is changed - - inject_pkgs_pylint is changed - - '"pylint" in inject_pkgs_pylint.application' - - '"licenses" in inject_pkgs_pylint.application["pylint"]["injected"]' - - inject_pkgs_apps_pylint is changed - - '"pylint" in inject_pkgs_apps_pylint.application' - - '"black" in inject_pkgs_apps_pylint.application["pylint"]["injected"]' - - uninstall_pylint is changed +- name: Include testcase for issue 8656 + ansible.builtin.include_tasks: testcase-8656.yml -############################################################################## -- name: install jupyter - not working smoothly in freebsd - # when: ansible_system != 'FreeBSD' +- name: install pipx + pip: + name: pipx>=1.7.0 + extra_args: --user + ignore_errors: true + register: pipx170_install + +- name: Recent features + when: + - pipx170_install is not failed + - pipx170_install is changed block: - - name: ensure application mkdocs is uninstalled - community.general.pipx: - name: mkdocs - state: absent - - - name: install application mkdocs - community.general.pipx: - name: mkdocs - install_deps: true - register: install_mkdocs - - - name: cleanup mkdocs - community.general.pipx: - state: absent - name: mkdocs - - - name: check assertions - assert: - that: - - install_mkdocs is changed - - '"markdown_py" in install_mkdocs.stdout' - -############################################################################## -- name: ensure /opt/pipx - ansible.builtin.file: - path: /opt/pipx - state: directory - mode: 0755 - -- name: install tox site-wide - community.general.pipx: - name: tox - state: latest - register: install_tox_sitewide - environment: - PIPX_HOME: /opt/pipx - PIPX_BIN_DIR: /usr/local/bin - -- name: stat /usr/local/bin/tox - ansible.builtin.stat: - path: /usr/local/bin/tox - register: usrlocaltox - -- name: check assertions - ansible.builtin.assert: - that: - - install_tox_sitewide is changed - - usrlocaltox.stat.exists - -############################################################################## -# Test for issue 7497 -- name: ensure application pyinstaller is uninstalled - community.general.pipx: - name: pyinstaller - state: absent - -- name: Install Python Package pyinstaller - community.general.pipx: - name: pyinstaller - state: present - system_site_packages: true - pip_args: "--no-cache-dir" - register: install_pyinstaller - -- name: cleanup pyinstaller - community.general.pipx: - name: pyinstaller - state: absent - -- name: check assertions - assert: - that: - - install_pyinstaller is changed - -############################################################################## -# Test for issue 8656 -- name: ensure application conan2 is uninstalled - community.general.pipx: - name: conan2 - state: absent - -- name: Install Python Package conan with suffix 2 (conan2) - community.general.pipx: - name: conan - state: install - suffix: "2" - register: install_conan2 - -- name: Install Python Package conan with suffix 2 (conan2) again - community.general.pipx: - name: conan - state: install - suffix: "2" - register: install_conan2_again - -- name: cleanup conan2 - community.general.pipx: - name: conan2 - state: absent - -- name: check assertions - assert: - that: - - install_conan2 is changed - - "' - conan2' in install_conan2.stdout" - - install_conan2_again is not changed + - name: Include testcase for PR 8793 --global + ansible.builtin.include_tasks: testcase-8793-global.yml diff --git a/tests/integration/targets/pipx/tasks/testcase-7497.yml b/tests/integration/targets/pipx/tasks/testcase-7497.yml new file mode 100644 index 0000000000..938196ef59 --- /dev/null +++ b/tests/integration/targets/pipx/tasks/testcase-7497.yml @@ -0,0 +1,27 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: ensure application pyinstaller is uninstalled + community.general.pipx: + name: pyinstaller + state: absent + +- name: Install Python Package pyinstaller + community.general.pipx: + name: pyinstaller + state: present + system_site_packages: true + pip_args: "--no-cache-dir" + register: install_pyinstaller + +- name: cleanup pyinstaller + community.general.pipx: + name: pyinstaller + state: absent + +- name: check assertions + assert: + that: + - install_pyinstaller is changed diff --git a/tests/integration/targets/pipx/tasks/testcase-8656.yml b/tests/integration/targets/pipx/tasks/testcase-8656.yml new file mode 100644 index 0000000000..10e99e846e --- /dev/null +++ b/tests/integration/targets/pipx/tasks/testcase-8656.yml @@ -0,0 +1,35 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: ensure application conan2 is uninstalled + community.general.pipx: + name: conan2 + state: absent + +- name: Install Python Package conan with suffix 2 (conan2) + community.general.pipx: + name: conan + state: install + suffix: "2" + register: install_conan2 + +- name: Install Python Package conan with suffix 2 (conan2) again + community.general.pipx: + name: conan + state: install + suffix: "2" + register: install_conan2_again + +- name: cleanup conan2 + community.general.pipx: + name: conan2 + state: absent + +- name: check assertions + assert: + that: + - install_conan2 is changed + - "' - conan2' in install_conan2.stdout" + - install_conan2_again is not changed diff --git a/tests/integration/targets/pipx/tasks/testcase-8793-global.yml b/tests/integration/targets/pipx/tasks/testcase-8793-global.yml new file mode 100644 index 0000000000..7d3c871306 --- /dev/null +++ b/tests/integration/targets/pipx/tasks/testcase-8793-global.yml @@ -0,0 +1,58 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Set up environment + environment: + PATH: /usr/local/bin:{{ ansible_env.PATH }} + block: + - name: Remove global pipx dir + ansible.builtin.file: + path: /opt/pipx + state: absent + force: true + + - name: Create global pipx dir + ansible.builtin.file: + path: /opt/pipx + state: directory + mode: '0755' + + - name: Uninstall pycowsay + community.general.pipx: + state: uninstall + name: pycowsay + + - name: Uninstall pycowsay (global) + community.general.pipx: + state: uninstall + name: pycowsay + global: true + + - name: Run pycowsay (should fail) + ansible.builtin.command: pycowsay Moooooooo! + changed_when: false + ignore_errors: true + + - name: Install pycowsay (global) + community.general.pipx: + state: install + name: pycowsay + global: true + + - name: Run pycowsay (should succeed) + ansible.builtin.command: pycowsay Moooooooo! + changed_when: false + register: what_the_cow_said + + - name: Which cow? + ansible.builtin.command: which pycowsay + changed_when: false + register: which_cow + + - name: Assert Moooooooo + ansible.builtin.assert: + that: + - "'Moooooooo!' in what_the_cow_said.stdout" + - "'/usr/local/bin/pycowsay' in which_cow.stdout" diff --git a/tests/integration/targets/pipx/tasks/testcase-injectpkg.yml b/tests/integration/targets/pipx/tasks/testcase-injectpkg.yml new file mode 100644 index 0000000000..60296024e4 --- /dev/null +++ b/tests/integration/targets/pipx/tasks/testcase-injectpkg.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: ensure application pylint is uninstalled + community.general.pipx: + name: pylint + state: absent + +- name: install application pylint + community.general.pipx: + name: pylint + register: install_pylint + +- name: inject packages + community.general.pipx: + state: inject + name: pylint + inject_packages: + - licenses + register: inject_pkgs_pylint + +- name: inject packages with apps + community.general.pipx: + state: inject + name: pylint + inject_packages: + - black + install_apps: true + register: inject_pkgs_apps_pylint + +- name: cleanup pylint + community.general.pipx: + state: absent + name: pylint + register: uninstall_pylint + +- name: check assertions inject_packages + assert: + that: + - install_pylint is changed + - inject_pkgs_pylint is changed + - '"pylint" in inject_pkgs_pylint.application' + - '"licenses" in inject_pkgs_pylint.application["pylint"]["injected"]' + - inject_pkgs_apps_pylint is changed + - '"pylint" in inject_pkgs_apps_pylint.application' + - '"black" in inject_pkgs_apps_pylint.application["pylint"]["injected"]' + - uninstall_pylint is changed diff --git a/tests/integration/targets/pipx/tasks/testcase-jupyter.yml b/tests/integration/targets/pipx/tasks/testcase-jupyter.yml new file mode 100644 index 0000000000..e4b5d48dd5 --- /dev/null +++ b/tests/integration/targets/pipx/tasks/testcase-jupyter.yml @@ -0,0 +1,28 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: install jupyter + block: + - name: ensure application mkdocs is uninstalled + community.general.pipx: + name: mkdocs + state: absent + + - name: install application mkdocs + community.general.pipx: + name: mkdocs + install_deps: true + register: install_mkdocs + + - name: cleanup mkdocs + community.general.pipx: + state: absent + name: mkdocs + + - name: check assertions + assert: + that: + - install_mkdocs is changed + - '"markdown_py" in install_mkdocs.stdout' diff --git a/tests/integration/targets/pipx/tasks/testcase-oldsitewide.yml b/tests/integration/targets/pipx/tasks/testcase-oldsitewide.yml new file mode 100644 index 0000000000..1db3e60406 --- /dev/null +++ b/tests/integration/targets/pipx/tasks/testcase-oldsitewide.yml @@ -0,0 +1,40 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Ensure /opt/pipx + ansible.builtin.file: + path: /opt/pipx + state: directory + mode: 0755 + +- name: Install tox site-wide + community.general.pipx: + name: tox + state: latest + register: install_tox_sitewide + environment: + PIPX_HOME: /opt/pipx + PIPX_BIN_DIR: /usr/local/bin + +- name: stat /usr/local/bin/tox + ansible.builtin.stat: + path: /usr/local/bin/tox + register: usrlocaltox + +- name: Uninstall tox site-wide + community.general.pipx: + name: tox + state: uninstall + register: uninstall_tox_sitewide + environment: + PIPX_HOME: /opt/pipx + PIPX_BIN_DIR: /usr/local/bin + +- name: check assertions + ansible.builtin.assert: + that: + - install_tox_sitewide is changed + - usrlocaltox.stat.exists + - uninstall_tox_sitewide is changed