1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

pipx - new module (#3507)

* pipx - new module

* using python instead of python3

* removed ensure_path as it is unused

* ensuring we are running the same python as Ansible

* changed the last solution to adding a pipx_path parameter to the module, with a sensible default

* added docs for the new parameter

* changed param name to executable, and customized it for Darwin

* use executable if passed, otherwise use python -m pipx

* minor update

* added examples

* Update plugins/modules/packaging/language/pipx.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/packaging/language/pipx.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/packaging/language/pipx.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* tests names 324 -> 3.24.0

* ensure tox is uninstalled by the beginning of the test

* Renamed option+suggestions from PR

* improved idempotency

* fixed sanity

* fixed test

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Alexei Znamensky 2021-10-06 18:01:54 +13:00 committed by GitHub
parent 0bc4518f3b
commit f1807d3323
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 381 additions and 0 deletions

2
.github/BOTMETA.yml vendored
View file

@ -758,6 +758,8 @@ files:
ignore: jle64 ignore: jle64
$modules/packaging/language/pip_package_info.py: $modules/packaging/language/pip_package_info.py:
maintainers: bcoca matburt maxamillion maintainers: bcoca matburt maxamillion
$modules/packaging/language/pipx.py:
maintainers: russoz
$modules/packaging/language/yarn.py: $modules/packaging/language/yarn.py:
maintainers: chrishoffman verkaufer maintainers: chrishoffman verkaufer
$modules/packaging/os/apk.py: $modules/packaging/os/apk.py:

View file

@ -0,0 +1,282 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2021, Alexei Znamensky <russoz@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
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.
options:
state:
type: str
choices: [present, absent, install, uninstall, uninstall_all, inject, upgrade, upgrade_all, reinstall, reinstall_all]
default: install
description:
- Desired state for the application.
- The states C(present) and C(absent) are aliases to C(install) and C(uninstall), respectively.
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_deps:
description:
- Include applications of dependent packages.
- Only used when I(state=install) or I(state=upgrade).
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), 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) or I(state=upgrade_all).
type: bool
default: false
index_url:
description:
- Base URL of Python Package Index.
- Only used when I(state=install), I(state=upgrade), 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=reinstall), or I(state=reinstall_all).
type: str
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.
- >
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 (
CmdStateModuleHelper, ArgFormat, ModuleHelperException
)
from ansible.module_utils.facts.compat import ansible_facts
_state_map = dict(
present='install',
absent='uninstall',
uninstall_all='uninstall-all',
upgrade_all='upgrade-all',
reinstall_all='reinstall-all',
)
class PipX(CmdStateModuleHelper):
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']),
name=dict(type='str'),
source=dict(type='str'),
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'),
executable=dict(type='path')
),
required_if=[
('state', 'present', ['name']),
('state', 'install', ['name']),
('state', 'absent', ['name']),
('state', 'uninstall', ['name']),
('state', 'inject', ['name', 'inject_packages']),
],
supports_check_mode=True,
)
command_args_formats = dict(
state=dict(fmt=lambda v: [_state_map.get(v, v)]),
name_source=dict(fmt=lambda n, s: [s] if s else [n], stars=1),
install_deps=dict(fmt="--install-deps", style=ArgFormat.BOOLEAN),
inject_packages=dict(fmt=lambda v: v),
force=dict(fmt="--force", style=ArgFormat.BOOLEAN),
include_injected=dict(fmt="--include-injected", style=ArgFormat.BOOLEAN),
index_url=dict(fmt=('--index-url', '{0}'),),
python=dict(fmt=('--python', '{0}'),),
_list=dict(fmt=('list', '--include-injected', '--json'), style=ArgFormat.BOOLEAN),
)
check_rc = 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']
),
}
return results
installed = self.run_command(params=[{'_list': True}], process_output=process_list,
publish_rc=False, publish_out=False, publish_err=False)
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.vars.set('will_change', False, output=False, change=True)
self.vars.set('application', self._retrieve_installed(), change=True, diff=True)
def __quit_module__(self):
self.vars.application = self._retrieve_installed()
def state_install(self):
if not self.vars.application or self.vars.force:
self.vars.will_change = True
if not self.module.check_mode:
self.run_command(params=['state', 'index_url', 'install_deps', 'force', 'python',
{'name_source': [self.vars.name, self.vars.source]}])
state_present = state_install
def state_upgrade(self):
if not self.vars.application:
raise ModuleHelperException(
"Trying to upgrade a non-existent application: {0}".format(self.vars.name))
if self.vars.force:
self.vars.will_change = True
if not self.module.check_mode:
self.run_command(params=['state', 'index_url', 'install_deps', 'force', 'name'])
def state_uninstall(self):
if self.vars.application and not self.module.check_mode:
self.run_command(params=['state', 'name'])
state_absent = state_uninstall
def state_reinstall(self):
if not self.vars.application:
raise ModuleHelperException(
"Trying to reinstall a non-existent application: {0}".format(self.vars.name))
self.vars.will_change = True
if not self.module.check_mode:
self.run_command(params=['state', 'name', 'python'])
def state_inject(self):
if not self.vars.application:
raise ModuleHelperException(
"Trying to inject packages into a non-existent application: {0}".format(self.vars.name))
if self.vars.force:
self.vars.will_change = True
if not self.module.check_mode:
self.run_command(params=['state', 'index_url', 'force', 'name', 'inject_packages'])
def state_uninstall_all(self):
if not self.module.check_mode:
self.run_command(params=['state'])
def state_reinstall_all(self):
if not self.module.check_mode:
self.run_command(params=['state', 'python'])
def state_upgrade_all(self):
if self.vars.force:
self.vars.will_change = True
if not self.module.check_mode:
self.run_command(params=['state', 'include_injected', 'force'])
def main():
PipX.execute()
if __name__ == '__main__':
main()

1
plugins/modules/pipx.py Symbolic link
View file

@ -0,0 +1 @@
./packaging/language/pipx.py

View file

@ -0,0 +1,4 @@
destructive
shippable/posix/group2
skip/python2
skip/python3.5

View file

@ -0,0 +1,92 @@
---
- name: install pipx
pip:
name: pipx
extra_args: --user
##############################################################################
- name: ensure application tox is uninstalled
community.general.pipx:
state: absent
name: tox
register: uninstall_tox
- name: install application tox
community.general.pipx:
name: tox
register: install_tox
- name: install application tox again
community.general.pipx:
name: tox
register: install_tox_again
ignore_errors: yes
- name: install application tox again force
community.general.pipx:
name: tox
force: yes
register: install_tox_again_force
- name: uninstall application tox
community.general.pipx:
state: absent
name: tox
register: uninstall_tox
- name: check assertions tox
assert:
that:
- install_tox is changed
- "'tox' in install_tox.application"
- install_tox_again is not changed
- install_tox_again_force is changed
- uninstall_tox is changed
- "'tox' not in uninstall_tox.application"
##############################################################################
- name: install application tox 3.24.0
community.general.pipx:
name: tox
source: tox==3.24.0
register: install_tox_324
- name: reinstall tox 3.24.0
community.general.pipx:
name: tox
state: reinstall
register: reinstall_tox_324
- name: upgrade tox 3.24.0
community.general.pipx:
name: tox
state: upgrade
register: upgrade_tox_324
- name: downgrade tox 3.24.0
community.general.pipx:
name: tox
source: tox==3.24.0
force: yes
register: downgrade_tox_324
- name: cleanup tox 3.24.0
community.general.pipx:
state: absent
name: tox
register: uninstall_tox_324
- name: check assertions tox 3.24.0
assert:
that:
- install_tox_324 is changed
- "'tox' in install_tox_324.application"
- install_tox_324.application.tox.version == '3.24.0'
- reinstall_tox_324 is changed
- reinstall_tox_324.application.tox.version == '3.24.0'
- upgrade_tox_324 is changed
- upgrade_tox_324.application.tox.version != '3.24.0'
- downgrade_tox_324 is changed
- downgrade_tox_324.application.tox.version == '3.24.0'
- uninstall_tox_324 is changed
- "'tox' not in uninstall_tox_324.application"