diff --git a/changelogs/fragments/9044-pipx-fixes.yml b/changelogs/fragments/9044-pipx-fixes.yml new file mode 100644 index 0000000000..dbf0e3c10d --- /dev/null +++ b/changelogs/fragments/9044-pipx-fixes.yml @@ -0,0 +1,7 @@ +minor_changes: + - pipx - refactor out parsing of ``pipx list`` output to module utils (https://github.com/ansible-collections/community.general/pull/9044). + - pipx_info - refactor out parsing of ``pipx list`` output to module utils (https://github.com/ansible-collections/community.general/pull/9044). + - pipx_info - add new return value ``pinned`` (https://github.com/ansible-collections/community.general/pull/9044). +bugfixes: + - pipx module utils - add missing command line formatter for argument ``spec_metadata`` (https://github.com/ansible-collections/community.general/pull/9044). + - pipx - it was ignoring ``global`` when listing existing applications (https://github.com/ansible-collections/community.general/pull/9044). diff --git a/plugins/module_utils/pipx.py b/plugins/module_utils/pipx.py index 513b9081f6..75b6621c1b 100644 --- a/plugins/module_utils/pipx.py +++ b/plugins/module_utils/pipx.py @@ -6,6 +6,10 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type + +import json + + from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt as fmt @@ -51,6 +55,7 @@ def pipx_runner(module, command, **kwargs): editable=fmt.as_bool("--editable"), pip_args=fmt.as_opt_eq_val('--pip-args'), suffix=fmt.as_opt_val('--suffix'), + spec_metadata=fmt.as_list(), ) arg_formats["global"] = fmt.as_bool("--global") @@ -63,3 +68,38 @@ def pipx_runner(module, command, **kwargs): **kwargs ) return runner + + +def make_process_list(mod_helper, **kwargs): + def process_list(rc, out, err): + if not out: + return [] + + results = [] + raw_data = json.loads(out) + if kwargs.get("include_raw"): + mod_helper.vars.raw_output = raw_data + + if kwargs["name"]: + if kwargs["name"] in raw_data['venvs']: + data = {kwargs["name"]: raw_data['venvs'][kwargs["name"]]} + else: + data = {} + else: + data = raw_data['venvs'] + + for venv_name, venv in data.items(): + entry = { + 'name': venv_name, + 'version': venv['metadata']['main_package']['package_version'], + 'pinned': venv['metadata']['main_package'].get('pinned'), + } + if kwargs.get("include_injected"): + entry['injected'] = {k: v['package_version'] for k, v in venv['metadata']['injected_packages'].items()} + if kwargs.get("include_deps"): + entry['dependencies'] = list(venv['metadata']['main_package']['app_paths_of_dependencies']) + results.append(entry) + + return results + + return process_list diff --git a/plugins/modules/pipx.py b/plugins/modules/pipx.py index c317ae8da8..9bde0f180c 100644 --- a/plugins/modules/pipx.py +++ b/plugins/modules/pipx.py @@ -191,10 +191,8 @@ EXAMPLES = """ """ -import json - from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper -from ansible_collections.community.general.plugins.module_utils.pipx import pipx_runner, pipx_common_argspec +from ansible_collections.community.general.plugins.module_utils.pipx import pipx_runner, pipx_common_argspec, make_process_list from ansible.module_utils.facts.compat import ansible_facts @@ -251,26 +249,14 @@ class PipX(StateModuleHelper): use_old_vardict = False def _retrieve_installed(self): - def process_list(rc, out, err): - if not out: - return {} + name = _make_name(self.vars.name, self.vars.suffix) + output_process = make_process_list(self, include_injected=True, name=name) + installed = self.runner('_list global', output_process=output_process).run() - results = {} - raw_data = json.loads(out) - for venv_name, venv in raw_data['venvs'].items(): - results[venv_name] = { - 'version': venv['metadata']['main_package']['package_version'], - 'injected': {k: v['package_version'] for k, v in venv['metadata']['injected_packages'].items()}, - } - return results - - installed = self.runner('_list', output_process=process_list).run(_list=1) - - if self.vars.name is not None: - name = _make_name(self.vars.name, self.vars.suffix) - app_list = installed.get(name) + if name is not None: + app_list = [app for app in installed if app['name'] == name] if app_list: - return {name: app_list} + return {name: app_list[0]} else: return {} diff --git a/plugins/modules/pipx_info.py b/plugins/modules/pipx_info.py index 65c0ba552e..33fbad0e5d 100644 --- a/plugins/modules/pipx_info.py +++ b/plugins/modules/pipx_info.py @@ -98,6 +98,15 @@ application: type: dict sample: licenses: "0.6.1" + pinned: + description: + - Whether the installed application is pinned or not. + - When using C(pipx<=1.6.0), this returns C(null). + returned: success + type: bool + sample: + pinned: true + version_added: 10.0.0 raw_output: description: The raw output of the C(pipx list) command, when O(include_raw=true). Used for debugging. @@ -112,10 +121,8 @@ cmd: sample: ["/usr/bin/python3.10", "-m", "pipx", "list", "--include-injected", "--json"] """ -import json - from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper -from ansible_collections.community.general.plugins.module_utils.pipx import pipx_runner, pipx_common_argspec +from ansible_collections.community.general.plugins.module_utils.pipx import pipx_runner, pipx_common_argspec, make_process_list from ansible.module_utils.facts.compat import ansible_facts @@ -143,41 +150,10 @@ class PipXInfo(ModuleHelper): self.command = [facts['python']['executable'], '-m', 'pipx'] self.runner = pipx_runner(self.module, self.command) - # self.vars.set('application', self._retrieve_installed(), change=True, diff=True) - def __run__(self): - def process_list(rc, out, err): - if not out: - return [] - - results = [] - raw_data = json.loads(out) - if self.vars.include_raw: - self.vars.raw_output = raw_data - - if self.vars.name: - if self.vars.name in raw_data['venvs']: - data = {self.vars.name: raw_data['venvs'][self.vars.name]} - else: - data = {} - else: - data = raw_data['venvs'] - - for venv_name, venv in data.items(): - entry = { - 'name': venv_name, - 'version': venv['metadata']['main_package']['package_version'] - } - if self.vars.include_injected: - entry['injected'] = {k: v['package_version'] for k, v in venv['metadata']['injected_packages'].items()} - if self.vars.include_deps: - entry['dependencies'] = list(venv['metadata']['main_package']['app_paths_of_dependencies']) - results.append(entry) - - return results - - with self.runner('_list global', output_process=process_list) as ctx: - self.vars.application = ctx.run(_list=1) + output_process = make_process_list(self, **self.vars.as_dict()) + with self.runner('_list global', output_process=output_process) as ctx: + self.vars.application = ctx.run() self._capture_results(ctx) def _capture_results(self, ctx): diff --git a/tests/integration/targets/pipx/files/spec.json b/tests/integration/targets/pipx/files/spec.json deleted file mode 100644 index 3c85125337..0000000000 --- a/tests/integration/targets/pipx/files/spec.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "pipx_spec_version": "0.1", - "venvs": { - "black": { - "metadata": { - "injected_packages": {}, - "main_package": { - "app_paths": [ - { - "__Path__": "/home/az/.local/pipx/venvs/black/bin/black", - "__type__": "Path" - }, - { - "__Path__": "/home/az/.local/pipx/venvs/black/bin/blackd", - "__type__": "Path" - } - ], - "app_paths_of_dependencies": {}, - "apps": [ - "black", - "blackd" - ], - "apps_of_dependencies": [], - "include_apps": true, - "include_dependencies": false, - "man_pages": [], - "man_pages_of_dependencies": [], - "man_paths": [], - "man_paths_of_dependencies": {}, - "package": "black", - "package_or_url": "black", - "package_version": "24.8.0", - "pinned": false, - "pip_args": [], - "suffix": "" - }, - "pipx_metadata_version": "0.5", - "python_version": "Python 3.11.9", - "source_interpreter": { - "__Path__": "/home/az/.pyenv/versions/3.11.9/bin/python3.11", - "__type__": "Path" - }, - "venv_args": [] - } - }, - "pycowsay": { - "metadata": { - "injected_packages": {}, - "main_package": { - "app_paths": [ - { - "__Path__": "/home/az/.local/pipx/venvs/pycowsay/bin/pycowsay", - "__type__": "Path" - } - ], - "app_paths_of_dependencies": {}, - "apps": [ - "pycowsay" - ], - "apps_of_dependencies": [], - "include_apps": true, - "include_dependencies": false, - "man_pages": [ - "man6/pycowsay.6" - ], - "man_pages_of_dependencies": [], - "man_paths": [ - { - "__Path__": "/home/az/.local/pipx/venvs/pycowsay/share/man/man6/pycowsay.6", - "__type__": "Path" - } - ], - "man_paths_of_dependencies": {}, - "package": "pycowsay", - "package_or_url": "pycowsay", - "package_version": "0.0.0.2", - "pinned": false, - "pip_args": [], - "suffix": "" - }, - "pipx_metadata_version": "0.5", - "python_version": "Python 3.11.9", - "source_interpreter": { - "__Path__": "/home/az/.pyenv/versions/3.11.9/bin/python3.11", - "__type__": "Path" - }, - "venv_args": [] - } - }, - } -} diff --git a/tests/integration/targets/pipx/files/spec.json.license b/tests/integration/targets/pipx/files/spec.json.license deleted file mode 100644 index a1390a69ed..0000000000 --- a/tests/integration/targets/pipx/files/spec.json.license +++ /dev/null @@ -1,3 +0,0 @@ -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 diff --git a/tests/integration/targets/pipx/meta/main.yml b/tests/integration/targets/pipx/meta/main.yml new file mode 100644 index 0000000000..982de6eb03 --- /dev/null +++ b/tests/integration/targets/pipx/meta/main.yml @@ -0,0 +1,7 @@ +--- +# 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 + +dependencies: + - setup_remote_tmp_dir diff --git a/tests/integration/targets/pipx/tasks/main.yml b/tests/integration/targets/pipx/tasks/main.yml index 30e96ef1bf..e764f17f68 100644 --- a/tests/integration/targets/pipx/tasks/main.yml +++ b/tests/integration/targets/pipx/tasks/main.yml @@ -3,10 +3,22 @@ # 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 pipx - pip: - name: pipx - extra_args: --user +- name: Determine pipx level + block: + - name: Install pipx>=1.7.0 + pip: + name: pipx>=1.7.0 + - name: Set has_pipx170 fact true + ansible.builtin.set_fact: + has_pipx170: true + rescue: + - name: Set has_pipx170 fact false + ansible.builtin.set_fact: + has_pipx170: false + - name: Install pipx (no version spec) + pip: + name: pipx + ############################################################################## - name: ensure application tox is uninstalled @@ -233,26 +245,21 @@ - name: Include testcase for issue 8656 ansible.builtin.include_tasks: testcase-8656.yml -- 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 + - has_pipx170 block: - name: Include testcase for PR 8793 --global ansible.builtin.include_tasks: testcase-8793-global.yml - name: Include testcase for PR 8809 install-all - ansible.builtin.include_tasks: testcase-8809-install-all.yml + ansible.builtin.include_tasks: testcase-8809-installall.yml - name: Include testcase for PR 8809 pin ansible.builtin.include_tasks: testcase-8809-pin.yml - name: Include testcase for PR 8809 injectpkg ansible.builtin.include_tasks: testcase-8809-uninjectpkg.yml + + - name: Include testcase for PR 9009 injectpkg --global + ansible.builtin.include_tasks: testcase-9009-fixglobal.yml diff --git a/tests/integration/targets/pipx/tasks/testcase-8809-installall.yml b/tests/integration/targets/pipx/tasks/testcase-8809-installall.yml index 37816247c0..9e770c1a98 100644 --- a/tests/integration/targets/pipx/tasks/testcase-8809-installall.yml +++ b/tests/integration/targets/pipx/tasks/testcase-8809-installall.yml @@ -24,10 +24,39 @@ - pycowsay register: uninstall_all_1 + - name: Install pycowsay and black + community.general.pipx: + state: install + name: "{{ item }}" + loop: + - black + - pycowsay + register: install_all_1 + + - name: Generate JSON spec + community.general.pipx_info: + include_raw: true + register: pipx_list + + - name: Copy content + ansible.builtin.copy: + content: "{{ pipx_list.raw_output }}" + dest: "{{ remote_tmp_dir }}/spec.json" + mode: "0644" + + - name: Uninstall pycowsay and black (again) + community.general.pipx: + state: uninstall + name: "{{ item }}" + loop: + - black + - pycowsay + register: uninstall_all_2 + - name: Use install-all community.general.pipx: - state: install-all - spec_metadata: spec.json + state: install_all + spec_metadata: "{{ remote_tmp_dir }}/spec.json" register: install_all - name: Run pycowsay (should succeed) @@ -47,13 +76,14 @@ loop: - black - pycowsay - register: uninstall_all_2 + register: uninstall_all_3 - name: Assert uninstall-all ansible.builtin.assert: that: - uninstall_all_1 is not changed + - install_all_1 is changed + - uninstall_all_2 is changed - install_all is changed - "'Moooooooo!' in what_the_cow_said.stdout" - - "'/usr/local/bin/pycowsay' in which_cow.stdout" - - uninstall_all_2 is changed + - uninstall_all_3 is changed diff --git a/tests/integration/targets/pipx/tasks/testcase-8809-pin.yml b/tests/integration/targets/pipx/tasks/testcase-8809-pin.yml index 89e4bb9dc6..c25073a719 100644 --- a/tests/integration/targets/pipx/tasks/testcase-8809-pin.yml +++ b/tests/integration/targets/pipx/tasks/testcase-8809-pin.yml @@ -60,10 +60,10 @@ - pycowsay register: uninstall_all_2 - - name: Assert uninstall-all + - name: Assert pin/unpin ansible.builtin.assert: that: - pin_cow is changed - - cow_info_1 == "0.0.0.1" + - cow_info_1.application.0.version == "0.0.0.1" - unpin_cow is changed - - cow_info_2 != "0.0.0.1" + - cow_info_2.application.0.version != "0.0.0.1" diff --git a/tests/integration/targets/pipx/tasks/testcase-8809-uninjectpkg.yml b/tests/integration/targets/pipx/tasks/testcase-8809-uninjectpkg.yml index 89e4bb9dc6..4092d6f244 100644 --- a/tests/integration/targets/pipx/tasks/testcase-8809-uninjectpkg.yml +++ b/tests/integration/targets/pipx/tasks/testcase-8809-uninjectpkg.yml @@ -64,6 +64,6 @@ ansible.builtin.assert: that: - pin_cow is changed - - cow_info_1 == "0.0.0.1" + - cow_info_1.application.0.version == "0.0.0.1" - unpin_cow is changed - - cow_info_2 != "0.0.0.1" + - cow_info_2.application.0.version != "0.0.0.1" diff --git a/tests/integration/targets/pipx/tasks/testcase-9009-fixglobal.yml b/tests/integration/targets/pipx/tasks/testcase-9009-fixglobal.yml new file mode 100644 index 0000000000..ffcd2651d0 --- /dev/null +++ b/tests/integration/targets/pipx/tasks/testcase-9009-fixglobal.yml @@ -0,0 +1,30 @@ +--- +# 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: 9009-Ensure application pylint is uninstalled + community.general.pipx: + name: pylint + state: absent + global: true + +- name: 9009-Install application pylint + community.general.pipx: + name: pylint + global: true + register: install_pylint + +- name: 9009-Inject packages + community.general.pipx: + state: inject + name: pylint + global: true + inject_packages: + - licenses + +- name: 9009-Ensure application pylint is uninstalled + community.general.pipx: + name: pylint + state: absent + global: true