diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index b448836f44..a75397852f 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -810,6 +810,8 @@ files: maintainers: bcoca matburt maxamillion $modules/packaging/language/pipx.py: maintainers: russoz + $modules/packaging/language/pipx_info.py: + maintainers: russoz $modules/packaging/language/yarn.py: maintainers: chrishoffman verkaufer $modules/packaging/os/apk.py: diff --git a/meta/runtime.yml b/meta/runtime.yml index cbb131346d..a7578eea22 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1131,6 +1131,8 @@ plugin_routing: redirect: community.general.packaging.language.pip_package_info pipx: redirect: community.general.packaging.language.pipx + pipx_info: + redirect: community.general.packaging.language.pipx_info pkg5: redirect: community.general.packaging.os.pkg5 pkg5_publisher: diff --git a/plugins/modules/packaging/language/pipx_info.py b/plugins/modules/packaging/language/pipx_info.py new file mode 100644 index 0000000000..05b9db1b2d --- /dev/null +++ b/plugins/modules/packaging/language/pipx_info.py @@ -0,0 +1,205 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Alexei Znamensky +# 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 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: pipx_info +short_description: Rretrieves information about applications installed with pipx +version_added: 5.6.0 +description: + - Retrieve details about Python applications installed in isolated virtualenvs using pipx. +options: + name: + description: + - Name of an application installed with C(pipx). + type: str + include_deps: + description: + - Include dependent packages in the output. + type: bool + default: false + include_injected: + description: + - Include injected packages in the output. + type: bool + default: false + include_raw: + description: + - Returns the raw output of C(pipx list --json). + - The raw output is not affected by I(include_deps) or I(include_injected). + type: bool + default: false + executable: + description: + - Path to the C(pipx) installed in the system. + - > + 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 +notes: + - 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. + - 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)" +''' + +EXAMPLES = ''' +- name: retrieve all installed applications + community.general.pipx_info: {} + +- name: retrieve all installed applications, include dependencies and injected packages + community.general.pipx_info: + include_deps: true + include_injected: true + +- name: retrieve application tox + community.general.pipx_info: + name: tox + include_deps: true + +- name: retrieve application ansible-lint, include dependencies + community.general.pipx_info: + name: ansible-lint + include_deps: true +''' + +RETURN = ''' +application: + description: The list of installed applications + returned: success + type: list + elements: dict + contains: + name: + description: The name of the installed application. + returned: success + type: str + sample: "tox" + version: + description: The version of the installed application. + returned: success + type: str + sample: "3.24.0" + dependencies: + description: The dependencies of the installed application, when I(include_deps=true). + returned: success + type: list + elements: str + sample: ["virtualenv"] + injected: + description: The injected packages for the installed application, when I(include_injected=true). + returned: success + type: dict + sample: + licenses: "0.6.1" + +raw_output: + description: The raw output of the C(pipx list) command, when I(include_raw=true). Used for debugging. + returned: success + type: dict + +cmd: + description: Command executed to obtain the list of installed applications. + returned: success + type: list + elements: str + 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 + +from ansible.module_utils.facts.compat import ansible_facts + + +class PipXInfo(ModuleHelper): + output_params = ['name'] + 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'), + ), + supports_check_mode=True, + ) + + def __init_module__(self): + if self.vars.executable: + self.command = [self.vars.executable] + else: + facts = ansible_facts(self.module, gather_subset=['python']) + 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'] = dict( + (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', output_process=process_list) as ctx: + self.vars.application = ctx.run(_list=1) + self._capture_results(ctx) + + def _capture_results(self, ctx): + self.vars.cmd = ctx.cmd + if self.verbosity >= 4: + self.vars.run_info = ctx.run_info + + +def main(): + PipXInfo.execute() + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/pipx_info/aliases b/tests/integration/targets/pipx_info/aliases new file mode 100644 index 0000000000..c42790aedf --- /dev/null +++ b/tests/integration/targets/pipx_info/aliases @@ -0,0 +1,8 @@ +# 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 + +destructive +shippable/posix/group3 +skip/python2 +skip/python3.5 diff --git a/tests/integration/targets/pipx_info/tasks/main.yml b/tests/integration/targets/pipx_info/tasks/main.yml new file mode 100644 index 0000000000..61163afd08 --- /dev/null +++ b/tests/integration/targets/pipx_info/tasks/main.yml @@ -0,0 +1,139 @@ +--- +# 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 pipx + pip: + name: pipx + extra_args: --user + +############################################################################## +- name: ensure application tox is uninstalled + community.general.pipx: + state: absent + name: tox + +- name: retrieve applications (empty) + community.general.pipx_info: {} + register: info_empty + +- name: install application tox + community.general.pipx: + name: tox + +- name: retrieve applications + community.general.pipx_info: {} + register: info_all + +- name: retrieve applications (include_deps=true) + community.general.pipx_info: + include_deps: true + register: info_all_deps + +- name: retrieve application tox + community.general.pipx_info: + name: tox + include_deps: true + register: info_tox + +- name: uninstall application tox + community.general.pipx: + state: absent + name: tox + +- name: check assertions tox + assert: + that: + - info_empty.application|length == 0 + + - info_all.application|length == 1 + - info_all.application[0].name == "tox" + - "'version' in info_all.application[0]" + - "'dependencies' not in info_all.application[0]" + - "'injected' not in info_all.application[0]" + + - info_all_deps.application|length == 1 + - info_all_deps.application[0].name == "tox" + - "'version' in info_all_deps.application[0]" + - info_all_deps.application[0].dependencies == ["virtualenv"] + - "'injected' not in info_all.application[0]" + + - info_tox.application == info_all_deps.application + +############################################################################## +- name: set test applications + set_fact: + apps: + - name: tox + source: tox==3.24.0 + - name: ansible-lint + inject_packages: + - licenses + +- name: ensure applications are uninstalled + community.general.pipx: + name: "{{ item.name }}" + state: absent + loop: "{{ apps }}" + +- name: install applications + community.general.pipx: + name: "{{ item.name }}" + source: "{{ item.source|default(omit) }}" + loop: "{{ apps }}" + +- name: inject packages + community.general.pipx: + state: inject + name: "{{ item.name }}" + inject_packages: "{{ item.inject_packages }}" + when: "'inject_packages' in item" + loop: "{{ apps }}" + +- name: retrieve applications + community.general.pipx_info: {} + register: info2_all + +- name: retrieve applications (include_deps=true) + community.general.pipx_info: + include_deps: true + include_injected: true + register: info2_all_deps + +- name: retrieve application ansible-lint + community.general.pipx_info: + name: ansible-lint + include_deps: true + include_injected: true + register: info2_lint + +- name: ensure applications are uninstalled + community.general.pipx: + name: "{{ item.name }}" + state: absent + loop: "{{ apps }}" + +- name: check assertions multiple apps + assert: + that: + - all_apps|length == 2 + - all_apps[1].name == "tox" + - all_apps[1].version == "3.24.0" + - "'dependencies' not in all_apps[1]" + - "'injected' not in all_apps[1]" + + - all_apps_deps|length == 2 + - all_apps_deps[1].name == "tox" + - all_apps_deps[1].version == "3.24.0" + - all_apps_deps[1].dependencies == ["virtualenv"] + - "'injected' in all_apps_deps[0]" + - "'licenses' in all_apps_deps[0].injected" + + - lint|length == 1 + - all_apps_deps|length == 2 + - lint[0] == all_apps_deps[0] + vars: + all_apps: "{{ info2_all.application|sort(attribute='name') }}" + all_apps_deps: "{{ info2_all_deps.application|sort(attribute='name') }}" + lint: "{{ info2_lint.application|sort(attribute='name') }}"