[PR #10031/2b4cb6da backport][stable-10] pipx: accept python version specs in parameter name (#10149)

pipx: accept python version specs in parameter `name` (#10031)

* pipx: accept python version specs in parameter "name"

* pipx_info: adjustment for backward compatibility

* remove unnecessary comprehension

* remove f-str

* no shebang for module utils

* remove f-str

* fix syntax error

* fix pipx_info

* rollback adjustments in existing tests

* docs & test update

* add debugging tasks to int test

* integration test checks for version of packaging

* move assertion to block

* fix idempotency when using version specifier

* add changelog frag

* fix docs

* dial down the version of tox used in tests

To accommodate old Pythons

* Update plugins/modules/pipx.py

* Apply suggestions from code review

* refactor/rename package requirements code

* fix filename in BOTMETA

* Update plugins/modules/pipx.py



* Update plugins/modules/pipx.py



* pipx mod utils: create make_process_dict and deprecate make_process_list

* pkg_req: make method private

* make_process_dict is simpler and more specialized

* ensure version specifiers are honored when state=install

* fix insanity

* pipx: reformat yaml blocks

* pipx: doc wordsmithing

---------


(cherry picked from commit 2b4cb6dabc)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
patchback[bot] 2025-05-17 08:24:12 +02:00 committed by GitHub
parent 8c8e755369
commit 9d245287b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 302 additions and 64 deletions

View file

@ -54,11 +54,17 @@ options:
name:
type: str
description:
- The name of the application. In C(pipx) documentation it is also referred to as the name of the virtual environment
where the application will be installed.
- The name of the application and also the name of the Python package being installed.
- In C(pipx) documentation it is also referred to as the name of the virtual environment where the application is installed.
- If O(name) is a simple package name without version specifiers, then that name is used as the Python package name
to be installed.
- Use O(source) for passing package specifications or installing from URLs or directories.
- Starting in community.general 10.7.0, you can use package specifiers when O(state=present) or O(state=install). For
example, O(name=tox<4.0.0) or O(name=tox>3.0.27).
- Please note that when you use O(state=present) and O(name) with version specifiers, contrary to the behavior of C(pipx),
this module honors the version specifier and installs a version of the application that satisfies it. If you want
to ensure the reinstallation of the application even when the version specifier is met, then you must use O(force=true),
or perhaps use O(state=upgrade) instead.
- Use O(source) for installing from URLs or directories.
source:
type: str
description:
@ -69,6 +75,7 @@ options:
- The value of this option is passed as-is to C(pipx).
- O(name) is still required when using O(source) to establish the application name without fetching the package from
a remote source.
- The module is not idempotent when using O(source).
install_apps:
description:
- Add apps from the injected packages.
@ -92,6 +99,7 @@ options:
description:
- Force modification of the application's virtual environment. See C(pipx) for details.
- Only used when O(state=install), O(state=upgrade), O(state=upgrade_all), O(state=latest), or O(state=inject).
- The module is not idempotent when O(force=true).
type: bool
default: false
include_injected:
@ -144,10 +152,10 @@ options:
with O(community.general.pipx_info#module:include_raw=true) and obtaining the content from the RV(community.general.pipx_info#module:raw_output).
type: path
version_added: 9.4.0
notes:
- 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.
requirements:
- When using O(name) with version specifiers, the Python package C(packaging) is required.
- If the package C(packaging) is at a version lesser than C(22.0.0), it will fail silently when processing invalid specifiers,
like C(tox<<<<4.0).
author:
- "Alexei Znamensky (@russoz)"
"""
@ -201,7 +209,8 @@ version:
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, make_process_list
from ansible_collections.community.general.plugins.module_utils.pipx import pipx_runner, pipx_common_argspec, make_process_dict
from ansible_collections.community.general.plugins.module_utils.pkg_req import PackageRequirement
from ansible.module_utils.facts.compat import ansible_facts
@ -258,18 +267,13 @@ class PipX(StateModuleHelper):
use_old_vardict = False
def _retrieve_installed(self):
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()
output_process = make_process_dict(include_injected=True)
installed, dummy = self.runner('_list global', output_process=output_process).run()
if name is not None:
app_list = [app for app in installed if app['name'] == name]
if app_list:
return {name: app_list[0]}
else:
return {}
if self.app_name is None:
return installed
return installed
return {k: v for k, v in installed.items() if k == self.app_name}
def __init_module__(self):
if self.vars.executable:
@ -279,6 +283,11 @@ class PipX(StateModuleHelper):
self.command = [facts['python']['executable'], '-m', 'pipx']
self.runner = pipx_runner(self.module, self.command)
pkg_req = PackageRequirement(self.module, self.vars.name)
self.parsed_name = pkg_req.parsed_name
self.parsed_req = pkg_req.requirement
self.app_name = _make_name(self.parsed_name, self.vars.suffix)
self.vars.set('application', self._retrieve_installed(), change=True, diff=True)
with self.runner("version") as ctx:
@ -295,12 +304,27 @@ class PipX(StateModuleHelper):
self.vars.set('run_info', ctx.run_info, verbosity=4)
def state_install(self):
if not self.vars.application or self.vars.force:
self.changed = True
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)
# If we have a version spec and no source, use the version spec as source
if self.parsed_req and not self.vars.source:
self.vars.source = self.vars.name
if self.vars.application.get(self.app_name):
is_installed = True
version_match = self.vars.application[self.app_name]['version'] in self.parsed_req.specifier if self.parsed_req else True
force = self.vars.force or (not version_match)
else:
is_installed = False
version_match = False
force = self.vars.force
if is_installed and version_match and not force:
return
self.changed = True
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.parsed_name, self.vars.source], force=force)
self._capture_results(ctx)
state_present = state_install