summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/modules/pipx.py
blob: dfa2f4300f8770148c5244f825107ac8f133d0b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2021, Alexei Znamensky <russoz@gmail.com>
# 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
short_description: Manages applications installed with pipx
version_added: 3.8.0
description:
    - Manage Python applications installed in isolated virtualenvs using pipx.
extends_documentation_fragment:
    - community.general.attributes
attributes:
    check_mode:
        support: full
    diff_mode:
        support: full
options:
    state:
        type: str
        choices: [present, absent, install, uninstall, uninstall_all, inject, upgrade, upgrade_all, reinstall, reinstall_all, latest]
        default: install
        description:
            - Desired state for the application.
            - The states C(present) and C(absent) are aliases to C(install) and C(uninstall), respectively.
            - The state C(latest) is equivalent to executing the task twice, with state C(install) and then C(upgrade).
              It was added in community.general 5.5.0.
    name:
        type: str
        description:
            - >
              The name of the application to be installed. It must to be a simple package name.
              For passing package specifications or installing from URLs or directories,
              please use the I(source) option.
    source:
        type: str
        description:
            - >
              If the application source, such as a package with version specifier, or an URL,
              directory or any other accepted specification. See C(pipx) documentation for more details.
            - When specified, the C(pipx) command will use I(source) instead of I(name).
    install_apps:
        description:
            - Add apps from the injected packages.
            - Only used when I(state=inject).
        type: bool
        default: false
        version_added: 6.5.0
    install_deps:
        description:
            - Include applications of dependent packages.
            - Only used when I(state=install), I(state=latest), or I(state=inject).
        type: bool
        default: false
    inject_packages:
        description:
            - Packages to be injected into an existing virtual environment.
            - Only used when I(state=inject).
        type: list
        elements: str
    force:
        description:
            - Force modification of the application's virtual environment. See C(pipx) for details.
            - Only used when I(state=install), I(state=upgrade), I(state=upgrade_all), I(state=latest), or I(state=inject).
        type: bool
        default: false
    include_injected:
        description:
            - Upgrade the injected packages along with the application.
            - Only used when I(state=upgrade), I(state=upgrade_all), or I(state=latest).
            - This is used with I(state=upgrade) and I(state=latest) since community.general 6.6.0.
        type: bool
        default: false
    index_url:
        description:
            - Base URL of Python Package Index.
            - Only used when I(state=install), I(state=upgrade), I(state=latest), or I(state=inject).
        type: str
    python:
        description:
            - Python version to be used when creating the application virtual environment. Must be 3.6+.
            - Only used when I(state=install), I(state=latest), I(state=reinstall), or I(state=reinstall_all).
        type: str
    system_site_packages:
        description:
            - Give application virtual environment access to the system site-packages directory.
            - Only used when I(state=install) or I(state=latest).
        type: bool
        default: false
        version_added: 6.6.0
    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
    editable:
        description:
            - Install the project in editable mode.
        type: bool
        default: false
        version_added: 4.6.0
    pip_args:
        description:
            - Arbitrary arguments to pass directly to C(pip).
        type: str
        version_added: 4.6.0
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.
    - >
      This module will honor C(pipx) environment variables such as but not limited to C(PIPX_HOME) and C(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.
    - >
      This first implementation does not verify whether a specified version constraint has been installed or not.
      Hence, when using version operators, C(pipx) module will always try to execute the operation,
      even when the application was previously installed.
      This feature will be added in the future.
    - See also the C(pipx) documentation at U(https://pypa.github.io/pipx/).
author:
    - "Alexei Znamensky (@russoz)"
'''

EXAMPLES = '''
- name: Install tox
  community.general.pipx:
    name: tox

- name: Install tox from git repository
  community.general.pipx:
    name: tox
    source: git+https://github.com/tox-dev/tox.git

- name: Upgrade tox
  community.general.pipx:
    name: tox
    state: upgrade

- name: Reinstall black with specific Python version
  community.general.pipx:
    name: black
    state: reinstall
    python: 3.7

- name: Uninstall pycowsay
  community.general.pipx:
    name: pycowsay
    state: absent
'''


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

from ansible.module_utils.facts.compat import ansible_facts


class PipX(StateModuleHelper):
    output_params = ['name', 'source', 'index_url', 'force', 'installdeps']
    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'),
        ),
        required_if=[
            ('state', 'present', ['name']),
            ('state', 'install', ['name']),
            ('state', 'absent', ['name']),
            ('state', 'uninstall', ['name']),
            ('state', 'upgrade', ['name']),
            ('state', 'reinstall', ['name']),
            ('state', 'latest', ['name']),
            ('state', 'inject', ['name', 'inject_packages']),
        ],
        supports_check_mode=True,
    )

    def _retrieve_installed(self):
        def process_list(rc, out, err):
            if not out:
                return {}

            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': dict(
                        (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:
            app_list = installed.get(self.vars.name)
            if app_list:
                return {self.vars.name: app_list}
            else:
                return {}

        return installed

    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 __quit_module__(self):
        self.vars.application = self._retrieve_installed()

    def _capture_results(self, ctx):
        self.vars.stdout = ctx.results_out
        self.vars.stderr = ctx.results_err
        self.vars.cmd = ctx.cmd
        if self.verbosity >= 4:
            self.vars.run_info = ctx.run_info

    def state_install(self):
        if not self.vars.application or self.vars.force:
            self.changed = True
            with self.runner('state index_url install_deps force python system_site_packages editable pip_args name_source', check_mode_skip=True) as ctx:
                ctx.run(name_source=[self.vars.name, self.vars.source])
                self._capture_results(ctx)

    state_present = state_install

    def state_upgrade(self):
        if not self.vars.application:
            self.do_raise("Trying to upgrade a non-existent application: {0}".format(self.vars.name))
        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:
            ctx.run()
            self._capture_results(ctx)

    def state_uninstall(self):
        if self.vars.application:
            with self.runner('state name', check_mode_skip=True) as ctx:
                ctx.run()
                self._capture_results(ctx)

    state_absent = state_uninstall

    def state_reinstall(self):
        if not self.vars.application:
            self.do_raise("Trying to reinstall a non-existent application: {0}".format(self.vars.name))
        self.changed = True
        with self.runner('state name python', check_mode_skip=True) as ctx:
            ctx.run()
            self._capture_results(ctx)

    def state_inject(self):
        if not self.vars.application:
            self.do_raise("Trying to inject packages into a non-existent application: {0}".format(self.vars.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:
            ctx.run()
            self._capture_results(ctx)

    def state_uninstall_all(self):
        with self.runner('state', 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:
            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:
            ctx.run()
            self._capture_results(ctx)

    def state_latest(self):
        if not self.vars.application or self.vars.force:
            self.changed = True
            with self.runner('state index_url install_deps force python system_site_packages editable pip_args name_source', check_mode_skip=True) as ctx:
                ctx.run(state='install', name_source=[self.vars.name, self.vars.source])
                self._capture_results(ctx)

        with self.runner('state include_injected index_url force editable pip_args name', check_mode_skip=True) as ctx:
            ctx.run(state='upgrade')
            self._capture_results(ctx)


def main():
    PipX.execute()


if __name__ == '__main__':
    main()