From e1b75be738c91ce80e8fff1d7bed1e7469d86d17 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Feb 2026 17:20:30 +0100 Subject: [PATCH 001/131] Add minimal uv_python module --- plugins/modules/uv_python.py | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 plugins/modules/uv_python.py diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py new file mode 100644 index 0000000000..aece3f5c65 --- /dev/null +++ b/plugins/modules/uv_python.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# Copyright (c) 2026 Mariam Ahhttouche +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r''' +--- +module: uv_python +short_description: Manage Python installations using uv +description: + - Install or remove Python versions managed by uv. +# requirements: +# - uv must be installed and available on PATH +options: + version: + description: Python version to manage + type: str + required: true + state: + description: Desired state + type: str + choices: [present, absent] + default: present + force: + description: Force reinstall + type: bool + default: false + uv_path: + description: Path to uv binary + type: str + default: uv +author: + - Your Name +''' + +EXAMPLES = r''' +- name: Install Python 3.12 + uv_python: + version: "3.12" + +- name: Remove Python 3.11 + uv_python: + version: "3.11" + state: absent +''' + +RETURN = r''' +python: + description: Installed Python info + returned: when state=present + type: dict +''' + +from ansible.module_utils.basic import AnsibleModule + + +class UV: + def __init__(self, module, **kwargs): + self.module = module + + def install_python(self): + if self.find_python() == 0: + return False, "" + if self.module.check_mode: + return True, "" + + cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.module.params["version"]] + _, out, _ = self.module.run_command(cmd, check_rc=True) + return True, out + + def find_python(self): + cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.module.params["version"]] + rc, _, _ = self.module.run_command(cmd) + return rc + + +def main(): + module = AnsibleModule( + argument_spec=dict( + version=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + ), + supports_check_mode=True + ) + + result = dict( + changed=False, + msg="", + ) + state = module.params["state"] + uv = UV(module) + + if state == "present": + if uv.find_python() != 0: + result["changed"], result["msg"] = uv.install_python() + + module.exit_json(**result) + + +if __name__ == "__main__": + main() \ No newline at end of file From 731d7b571fc82ecce1c2b57749161ad737e58bea Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Feb 2026 17:20:48 +0100 Subject: [PATCH 002/131] uv_python module: add integration tests --- .../targets/uv_python/tasks/main.yaml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/integration/targets/uv_python/tasks/main.yaml diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml new file mode 100644 index 0000000000..e0d4e8c21d --- /dev/null +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -0,0 +1,31 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# 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 uv + shell: | + curl -LsSf https://astral.sh/uv/install.sh | sh + environment: + UV_INSTALL_DIR: /usr/local/bin +- name: Install python 3.14 + uv_python: + version: 3.14 + state: present +- name: Re-install python 3.14 + uv_python: + version: 3.14 + state: present +- name: Install python 3.13.5 + uv_python: + version: 3.13.5 + state: present +- name: Re-install python 3.13.5 + uv_python: + version: 3.13.5 + state: present \ No newline at end of file From 39b9661d594d1a39dc057c69156fb355ac997831 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 12 Feb 2026 09:09:28 +0100 Subject: [PATCH 003/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index aece3f5c65..0feae8b721 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -59,19 +59,20 @@ class UV: self.module = module def install_python(self): - if self.find_python() == 0: - return False, "" + rc, out = self._find_python() + if rc == 0: + return False, out if self.module.check_mode: - return True, "" + return True, "" cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.module.params["version"]] _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out - def find_python(self): + def _find_python(self): cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.module.params["version"]] - rc, _, _ = self.module.run_command(cmd) - return rc + rc, out, _ = self.module.run_command(cmd) + return rc, out def main(): @@ -91,7 +92,6 @@ def main(): uv = UV(module) if state == "present": - if uv.find_python() != 0: result["changed"], result["msg"] = uv.install_python() module.exit_json(**result) From 3117cba3a9c9c63613eb93d295c1ee39200014bc Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 12 Feb 2026 10:46:57 +0100 Subject: [PATCH 004/131] uv_python module: handle absent state --- plugins/modules/uv_python.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 0feae8b721..92f5a204e0 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -59,7 +59,7 @@ class UV: self.module = module def install_python(self): - rc, out = self._find_python() + rc, out, _ = self._find_python() if rc == 0: return False, out if self.module.check_mode: @@ -69,10 +69,21 @@ class UV: _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out + def uninstall_python(self): + rc, out, _ = self._find_python() + if rc != 0: + return False, out + if self.module.check_mode: + return True, "" + + cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.module.params["version"]] + _, out, _ = self.module.run_command(cmd, check_rc=True) + return True, out + def _find_python(self): cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.module.params["version"]] - rc, out, _ = self.module.run_command(cmd) - return rc, out + rc, out, err = self.module.run_command(cmd) + return rc, out, err def main(): @@ -92,7 +103,9 @@ def main(): uv = UV(module) if state == "present": - result["changed"], result["msg"] = uv.install_python() + result["changed"], result["msg"] = uv.install_python() + elif state == "absent": + result["changed"], result["msg"] = uv.uninstall_python() module.exit_json(**result) From 458cd11c421f669ae6f961cdec82d02569031f15 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 12 Feb 2026 10:47:21 +0100 Subject: [PATCH 005/131] uv_python module: add integration tests --- .../targets/uv_python/tasks/main.yaml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index e0d4e8c21d..02e3e1795a 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -28,4 +28,20 @@ - name: Re-install python 3.13.5 uv_python: version: 3.13.5 - state: present \ No newline at end of file + state: present +- name: Remove unexisting python 3.15 + uv_python: + version: 3.15 + state: absent +- name: Remove globally existing python 3.8 + uv_python: + version: 3.8 + state: absent +- name: Remove python 3.13.5 + uv_python: + version: 3.13.5 + state: absent +- name: Remove python 3.13.5 again + uv_python: + version: 3.13.5 + state: absent \ No newline at end of file From 21646442ce96d0180b69ee91a1fdb8007ad947b5 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 12 Feb 2026 16:31:37 +0100 Subject: [PATCH 006/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 92f5a204e0..99ae19440b 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -51,12 +51,17 @@ python: type: dict ''' +import re from ansible.module_utils.basic import AnsibleModule class UV: + """ + Documentation for uv python https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version + """ def __init__(self, module, **kwargs): self.module = module + self.python_version = module.params["version"] def install_python(self): rc, out, _ = self._find_python() @@ -65,7 +70,7 @@ class UV: if self.module.check_mode: return True, "" - cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.module.params["version"]] + cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.python_version] _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out @@ -76,12 +81,12 @@ class UV: if self.module.check_mode: return True, "" - cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.module.params["version"]] + cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.python_version] _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out def _find_python(self): - cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.module.params["version"]] + cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.python_version] rc, out, err = self.module.run_command(cmd) return rc, out, err From 21c6d762cc8ce272fc705ee83cc3d2284db3ff5d Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 12 Feb 2026 17:26:57 +0100 Subject: [PATCH 007/131] uv_python module: restrict accepted version formats to X.Y and X.Y.Z --- plugins/modules/uv_python.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 99ae19440b..a3f2602a44 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -52,24 +52,46 @@ python: ''' import re +from enum import Enum from ansible.module_utils.basic import AnsibleModule +MINOR_RELEASE = re.compile(r"^\d+\.\d+$") +PATCH_RELEASE = re.compile(r"^\d+\.\d+\.\d+$") + +class Release(Enum): + PATCH = 1 + MINOR = 2 + +def extract_python_version(version: str, module): + if PATCH_RELEASE.match(version): + return Release.PATCH, version + elif MINOR_RELEASE.match(version): + return Release.MINOR, version + else: + module.fail_json( + msg=( + f"Invalid version '{version}'. " + "Expected X.Y or X.Y.Z." + ) + ) + return None, None + class UV: """ Documentation for uv python https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version """ def __init__(self, module, **kwargs): self.module = module - self.python_version = module.params["version"] + self.python_version_type, self.python_version = extract_python_version(module.params["version"], module) def install_python(self): - rc, out, _ = self._find_python() + rc, out, _ = self._find_python() if rc == 0: return False, out if self.module.check_mode: return True, "" - + cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.python_version] _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out From 71d7538bdf7143e0df38783d55706d6a694459aa Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 12 Feb 2026 17:27:25 +0100 Subject: [PATCH 008/131] uv_python module: add integration tests for version format --- .../targets/uv_python/tasks/main.yaml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 02e3e1795a..ebf6b4a690 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -17,7 +17,7 @@ uv_python: version: 3.14 state: present -- name: Re-install python 3.14 +- name: Re-install python 3.14 # installs latest patch version for 3.14 uv_python: version: 3.14 state: present @@ -29,7 +29,7 @@ uv_python: version: 3.13.5 state: present -- name: Remove unexisting python 3.15 +- name: Remove unexisting python 3.15 # removes latest patch version for 3.15 if exists uv_python: version: 3.15 state: absent @@ -44,4 +44,15 @@ - name: Remove python 3.13.5 again uv_python: version: 3.13.5 - state: absent \ No newline at end of file + state: absent +- name: Install python 3 + uv_python: + version: 3 + state: present + register: result + ignore_errors: true +- name: Assert invalid version failed + ansible.builtin.assert: + that: + - result is failed + - "'Expected X.Y or X.Y.Z' in result.msg" \ No newline at end of file From e2f27a3a362e3b31a042d3ce89f88986a7392c10 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 13 Feb 2026 12:26:18 +0100 Subject: [PATCH 009/131] uv_python module: add _list_python and _get_latest_patch_release methods --- plugins/modules/uv_python.py | 53 +++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index a3f2602a44..0504c150d7 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -52,38 +52,29 @@ python: ''' import re +import json from enum import Enum from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.compat.version import StrictVersion -MINOR_RELEASE = re.compile(r"^\d+\.\d+$") -PATCH_RELEASE = re.compile(r"^\d+\.\d+\.\d+$") - -class Release(Enum): - PATCH = 1 - MINOR = 2 - -def extract_python_version(version: str, module): - if PATCH_RELEASE.match(version): - return Release.PATCH, version - elif MINOR_RELEASE.match(version): - return Release.MINOR, version - else: - module.fail_json( - msg=( - f"Invalid version '{version}'. " - "Expected X.Y or X.Y.Z." - ) - ) - return None, None - class UV: """ Documentation for uv python https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version """ def __init__(self, module, **kwargs): self.module = module - self.python_version_type, self.python_version = extract_python_version(module.params["version"], module) + python_version = module.params["version"] + try: + self.python_version = StrictVersion(python_version) + self.python_version_str = self.python_version.__str__() + except ValueError: + module.fail_json( + msg=( + f"Invalid version '{python_version}'. " + "Expected X.Y or X.Y.Z." + ) + ) def install_python(self): rc, out, _ = self._find_python() @@ -92,7 +83,7 @@ class UV: if self.module.check_mode: return True, "" - cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.python_version] + cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.python_version_str] _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out @@ -103,15 +94,27 @@ class UV: if self.module.check_mode: return True, "" - cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.python_version] + cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.python_version_str] _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out def _find_python(self): - cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.python_version] + cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.python_version_str] + rc, out, err = self.module.run_command(cmd) + return rc, out, err + + def _list_python(self): + # By default, only the latest patch version is shown for each minor version. + # https://docs.astral.sh/uv/reference/cli/#uv-python-list + cmd = [self.module.get_bin_path("uv", required=True), "python", "list", self.python_version_str, "--output-format", "json"] rc, out, err = self.module.run_command(cmd) return rc, out, err + def _get_latest_patch_release(self): + _, out, _ = self._list_python() + result = json.loads(out) + return result[-1]["version"] + def main(): module = AnsibleModule( From ac140457f1b41ccaffc47913b73dcd93b3f9e4da Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 13 Feb 2026 15:41:05 +0100 Subject: [PATCH 010/131] uv_python module: add support for latest state --- plugins/modules/uv_python.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 0504c150d7..cc68d443a4 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -72,7 +72,7 @@ class UV: module.fail_json( msg=( f"Invalid version '{python_version}'. " - "Expected X.Y or X.Y.Z." + "Expected formats are X.Y or X.Y.Z" ) ) @@ -97,14 +97,26 @@ class UV: cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.python_version_str] _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out + + def upgrade_python(self): + rc, out, _ = self._find_python("--show-version") + detected_version = out.split()[0] + if rc == 0 and detected_version == self._get_latest_patch_release(): + return False, out + if self.module.check_mode: + return True, "" + + cmd = [self.module.get_bin_path("uv", required=True), "python", "upgrade", self.python_version_str] + rc, out, _ = self.module.run_command(cmd, check_rc=True) + return True, out - def _find_python(self): - cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.python_version_str] + def _find_python(self, *args): + # if multiple similar minor versions exist, find returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version + cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.python_version_str, *args] rc, out, err = self.module.run_command(cmd) return rc, out, err def _list_python(self): - # By default, only the latest patch version is shown for each minor version. # https://docs.astral.sh/uv/reference/cli/#uv-python-list cmd = [self.module.get_bin_path("uv", required=True), "python", "list", self.python_version_str, "--output-format", "json"] rc, out, err = self.module.run_command(cmd) @@ -113,14 +125,14 @@ class UV: def _get_latest_patch_release(self): _, out, _ = self._list_python() result = json.loads(out) - return result[-1]["version"] + return result[0]["version"] # uv orders versions in descending order def main(): module = AnsibleModule( argument_spec=dict( version=dict(type='str', required=True), - state=dict(type='str', default='present', choices=['present', 'absent']), + state=dict(type='str', default='present', choices=['present', 'absent', 'latest']), ), supports_check_mode=True ) @@ -136,6 +148,8 @@ def main(): result["changed"], result["msg"] = uv.install_python() elif state == "absent": result["changed"], result["msg"] = uv.uninstall_python() + elif state == "latest": + result["changed"], result["msg"] = uv.upgrade_python() module.exit_json(**result) From 0f586328bcc02992526cbde658c2af7b3a6d4bfc Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 13 Feb 2026 15:42:09 +0100 Subject: [PATCH 011/131] uv_python module: add integration tests for latest state --- tests/integration/targets/uv_python/tasks/main.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index ebf6b4a690..2e93c92850 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -21,6 +21,10 @@ uv_python: version: 3.14 state: present +- name: Install latest python 3.14 # installs latest patch version for 3.14 + uv_python: + version: 3.14 + state: latest - name: Install python 3.13.5 uv_python: version: 3.13.5 @@ -55,4 +59,8 @@ ansible.builtin.assert: that: - result is failed - - "'Expected X.Y or X.Y.Z' in result.msg" \ No newline at end of file + - "'Expected formats are X.Y or X.Y.Z' in result.msg" +- name: Upgrade python 3.13 + uv_python: + version: 3.13 + state: latest \ No newline at end of file From 87ad81992d2afb03dac642fa7596eb142a85d51d Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 13 Feb 2026 15:49:15 +0100 Subject: [PATCH 012/131] uv_python module: add integration tests for check mode --- .../targets/uv_python/tasks/main.yaml | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 2e93c92850..6ea21f9cb4 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -13,54 +13,78 @@ curl -LsSf https://astral.sh/uv/install.sh | sh environment: UV_INSTALL_DIR: /usr/local/bin + +- name: Install python 3.14 in check mode + uv_python: + version: 3.14 + state: present + check_mode: yes + - name: Install python 3.14 uv_python: version: 3.14 state: present + - name: Re-install python 3.14 # installs latest patch version for 3.14 uv_python: version: 3.14 state: present + - name: Install latest python 3.14 # installs latest patch version for 3.14 uv_python: version: 3.14 state: latest + - name: Install python 3.13.5 uv_python: version: 3.13.5 state: present + - name: Re-install python 3.13.5 uv_python: version: 3.13.5 state: present + - name: Remove unexisting python 3.15 # removes latest patch version for 3.15 if exists uv_python: version: 3.15 state: absent + - name: Remove globally existing python 3.8 uv_python: version: 3.8 state: absent + - name: Remove python 3.13.5 uv_python: version: 3.13.5 state: absent + - name: Remove python 3.13.5 again uv_python: version: 3.13.5 state: absent + - name: Install python 3 uv_python: version: 3 state: present register: result ignore_errors: true + - name: Assert invalid version failed ansible.builtin.assert: that: - result is failed - "'Expected formats are X.Y or X.Y.Z' in result.msg" + - name: Upgrade python 3.13 uv_python: version: 3.13 - state: latest \ No newline at end of file + state: latest + +- name: Upgrade python 3.13 in check mode + uv_python: + version: 3.13 + state: latest + check_mode: yes \ No newline at end of file From 396fc6cbb24a977bd76018961a032b3a3c9cb688 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 13 Feb 2026 16:31:02 +0100 Subject: [PATCH 013/131] uv_python module: improve latest state check mode to show version that will be installed --- plugins/modules/uv_python.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index cc68d443a4..d8f29228d9 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -101,10 +101,11 @@ class UV: def upgrade_python(self): rc, out, _ = self._find_python("--show-version") detected_version = out.split()[0] - if rc == 0 and detected_version == self._get_latest_patch_release(): + latest_version = self._get_latest_patch_release() + if rc == 0 and detected_version == latest_version: return False, out if self.module.check_mode: - return True, "" + return True, latest_version cmd = [self.module.get_bin_path("uv", required=True), "python", "upgrade", self.python_version_str] rc, out, _ = self.module.run_command(cmd, check_rc=True) From b89775677639d2f2b143119baf8a4763c9b80df9 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 13 Feb 2026 17:04:24 +0100 Subject: [PATCH 014/131] uv_python module: make latest state more deterministic by using install with explicite version --- plugins/modules/uv_python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index d8f29228d9..43ba51a912 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -107,8 +107,8 @@ class UV: if self.module.check_mode: return True, latest_version - cmd = [self.module.get_bin_path("uv", required=True), "python", "upgrade", self.python_version_str] - rc, out, _ = self.module.run_command(cmd, check_rc=True) + cmd = [self.module.get_bin_path("uv", required=True), "python", "install", latest_version] + _, out, _ = self.module.run_command(cmd, check_rc=True) return True, out def _find_python(self, *args): From 473f758ec1c3ee89ce4ef21eef9e3be050fc38f2 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 13 Feb 2026 17:09:20 +0100 Subject: [PATCH 015/131] uv_python module: improve absent state check mode and add corresponding integration test --- plugins/modules/uv_python.py | 2 +- tests/integration/targets/uv_python/tasks/main.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 43ba51a912..67640fc1cf 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -92,7 +92,7 @@ class UV: if rc != 0: return False, out if self.module.check_mode: - return True, "" + return True, out cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.python_version_str] _, out, _ = self.module.run_command(cmd, check_rc=True) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 6ea21f9cb4..f27f4db619 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -50,6 +50,12 @@ version: 3.15 state: absent +- name: Remove globally existing python 3.8 in check mode + uv_python: + version: 3.8 + state: absent + check_mode: true + - name: Remove globally existing python 3.8 uv_python: version: 3.8 From d37009ae7b242a33871593b3b1091bf6c0c06551 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 10:04:38 +0100 Subject: [PATCH 016/131] uv_python module: update latest state handling to sort versions without relying on uv behavior --- plugins/modules/uv_python.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 67640fc1cf..cccb6f3d92 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -103,7 +103,7 @@ class UV: detected_version = out.split()[0] latest_version = self._get_latest_patch_release() if rc == 0 and detected_version == latest_version: - return False, out + return False, latest_version if self.module.check_mode: return True, latest_version @@ -124,9 +124,10 @@ class UV: return rc, out, err def _get_latest_patch_release(self): - _, out, _ = self._list_python() - result = json.loads(out) - return result[0]["version"] # uv orders versions in descending order + _, out, _ = self._list_python() # uv returns versions in descending order but we sort them just in case future uv behavior changes + results = json.loads(out) + versions = [StrictVersion(result["version"]) for result in results] + return max(versions).__str__() def main(): From c40054f3f62dc95302a752b76697ec70fd119a9b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 10:59:32 +0100 Subject: [PATCH 017/131] uv_python module: improve integration tests --- .../targets/uv_python/tasks/main.yaml | 122 +++++++++++++++--- 1 file changed, 107 insertions(+), 15 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index f27f4db619..3495a18984 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -19,78 +19,170 @@ version: 3.14 state: present check_mode: yes + register: install_check_mode + +- name: Verify python 3.14 installation in check mode + assert: + that: + - install_check_mode["changed"] is true + - install_check_mode["failed"] is false + - '"3.14" in install_check_mode["msg"]' - name: Install python 3.14 uv_python: version: 3.14 state: present + register: install_python -- name: Re-install python 3.14 # installs latest patch version for 3.14 +- name: Verify python 3.14 installation in check mode + assert: + that: + - install_python["changed"] is true + - install_python["failed"] is false + - '"3.14" in install_python["msg"]' + +- name: Re-install python 3.14 uv_python: version: 3.14 state: present + register: reinstall_python -- name: Install latest python 3.14 # installs latest patch version for 3.14 +- name: Verify python 3.14 re-installation + assert: + that: + - reinstall_python["changed"] is false + - reinstall_python["failed"] is false + - '"3.14" in reinstall_python["msg"]' + +- name: Install latest python 3.15 # installs latest patch version for 3.15 uv_python: - version: 3.14 + version: 3.15 state: latest + register: install_latest_python +- debug: + var: install_latest_python +- name: Verify python 3.15 installation + assert: + that: + - install_latest_python["changed"] is true + - install_latest_python["failed"] is false + - '"3.15" in install_latest_python["msg"]' - name: Install python 3.13.5 uv_python: version: 3.13.5 state: present + register: install_python + +- name: Verify python 3.13.5 installation + assert: + that: + - install_python["changed"] is true + - install_python["failed"] is false + - '"3.13.5" in install_python["msg"]' - name: Re-install python 3.13.5 uv_python: version: 3.13.5 state: present + register: reinstall_python -- name: Remove unexisting python 3.15 # removes latest patch version for 3.15 if exists +- name: Verify python 3.13.5 installation + assert: + that: + - reinstall_python["changed"] is false + - reinstall_python["failed"] is false + - '"3.13.5" in reinstall_python["msg"]' + +- name: Remove unexisting python 3.10 # removes latest patch version for 3.10 if exists uv_python: - version: 3.15 + version: 3.10 state: absent + register: remove_python -- name: Remove globally existing python 3.8 in check mode +- name: Verify python 3.10 deletion + assert: + that: + - remove_python["changed"] is false + - remove_python["failed"] is false + - 'remove_python["msg"] == ""' + +- name: Remove python 3.13.5 in check mode uv_python: - version: 3.8 + version: 3.13.5 state: absent check_mode: true + register: remove_python_in_check_mode -- name: Remove globally existing python 3.8 - uv_python: - version: 3.8 - state: absent +- name: Verify python 3.13.5 deletion in check mode + assert: + that: + - remove_python_in_check_mode["changed"] is true + - remove_python_in_check_mode["failed"] is false + - 'remove_python_in_check_mode["msg"] == ""' - name: Remove python 3.13.5 uv_python: version: 3.13.5 state: absent + register: remove_python + +- name: Verify python 3.13.5 deletion + assert: + that: + - remove_python["changed"] is true + - remove_python["failed"] is false + - 'remove_python["msg"] == ""' - name: Remove python 3.13.5 again uv_python: version: 3.13.5 state: absent + register: remove_python + +- name: Verify python 3.13.5 deletion + assert: + that: + - remove_python["changed"] is false + - remove_python["failed"] is false + - 'remove_python["msg"] == ""' - name: Install python 3 uv_python: version: 3 state: present - register: result + register: invalid_version ignore_errors: true - name: Assert invalid version failed ansible.builtin.assert: that: - - result is failed - - "'Expected formats are X.Y or X.Y.Z' in result.msg" + - invalid_version is failed + - "'Expected formats are X.Y or X.Y.Z' in invalid_version.msg" - name: Upgrade python 3.13 uv_python: version: 3.13 state: latest + register: upgrade_python + +- name: Verify python 3.13 upgrade + assert: + that: + - upgrade_python["changed"] is true + - upgrade_python["failed"] is false + - '"3.13" in upgrade_python["msg"]' - name: Upgrade python 3.13 in check mode uv_python: version: 3.13 state: latest - check_mode: yes \ No newline at end of file + check_mode: yes + register: upgrade_python + +- name: Verify python 3.13.5 deletion in check mode + assert: + that: + - upgrade_python["changed"] is false + - upgrade_python["failed"] is false + - '"3.13" in upgrade_python["msg"]' \ No newline at end of file From be9bad065ab51c14ade046107f31132d6d515c96 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 11:20:01 +0100 Subject: [PATCH 018/131] uv_python module: improve module return values --- plugins/modules/uv_python.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index cccb6f3d92..40227a01ce 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -77,39 +77,40 @@ class UV: ) def install_python(self): - rc, out, _ = self._find_python() + rc, out, _ = self._find_python("--show-version") if rc == 0: - return False, out + return False, out.split()[0] if self.module.check_mode: - return True, "" + return True, self.python_version_str cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.python_version_str] - _, out, _ = self.module.run_command(cmd, check_rc=True) - return True, out + self.module.run_command(cmd, check_rc=True) + return True, self.python_version_str def uninstall_python(self): rc, out, _ = self._find_python() if rc != 0: return False, out if self.module.check_mode: - return True, out + return True, "" cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.python_version_str] - _, out, _ = self.module.run_command(cmd, check_rc=True) - return True, out + self.module.run_command(cmd, check_rc=True) + return True, "" def upgrade_python(self): rc, out, _ = self._find_python("--show-version") - detected_version = out.split()[0] latest_version = self._get_latest_patch_release() - if rc == 0 and detected_version == latest_version: + if rc == 0: + detected_version = out.split()[0] + if detected_version == latest_version: return False, latest_version if self.module.check_mode: return True, latest_version cmd = [self.module.get_bin_path("uv", required=True), "python", "install", latest_version] - _, out, _ = self.module.run_command(cmd, check_rc=True) - return True, out + self.module.run_command(cmd, check_rc=True) + return True, latest_version def _find_python(self, *args): # if multiple similar minor versions exist, find returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version From ee79011e9c5f13339573902b81fc9a6636f0b77d Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 11:24:04 +0100 Subject: [PATCH 019/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 40227a01ce..88609c2c07 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -51,17 +51,18 @@ python: type: dict ''' -import re import json -from enum import Enum from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.compat.version import StrictVersion class UV: """ - Documentation for uv python https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version + Module for "uv python" command + Official documentation for uv python https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version """ + subcommand = "python" + def __init__(self, module, **kwargs): self.module = module python_version = module.params["version"] @@ -83,7 +84,7 @@ class UV: if self.module.check_mode: return True, self.python_version_str - cmd = [self.module.get_bin_path("uv", required=True), "python", "install", self.python_version_str] + cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] self.module.run_command(cmd, check_rc=True) return True, self.python_version_str @@ -94,7 +95,7 @@ class UV: if self.module.check_mode: return True, "" - cmd = [self.module.get_bin_path("uv", required=True), "python", "uninstall", self.python_version_str] + cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "uninstall", self.python_version_str] self.module.run_command(cmd, check_rc=True) return True, "" @@ -108,19 +109,19 @@ class UV: if self.module.check_mode: return True, latest_version - cmd = [self.module.get_bin_path("uv", required=True), "python", "install", latest_version] + cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", latest_version] self.module.run_command(cmd, check_rc=True) return True, latest_version def _find_python(self, *args): - # if multiple similar minor versions exist, find returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version - cmd = [self.module.get_bin_path("uv", required=True), "python", "find", self.python_version_str, *args] + # if multiple similar minor versions exist, "uv python find" returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version + cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "find", self.python_version_str, *args] rc, out, err = self.module.run_command(cmd) return rc, out, err def _list_python(self): # https://docs.astral.sh/uv/reference/cli/#uv-python-list - cmd = [self.module.get_bin_path("uv", required=True), "python", "list", self.python_version_str, "--output-format", "json"] + cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "list", self.python_version_str, "--output-format", "json"] rc, out, err = self.module.run_command(cmd) return rc, out, err From 7610b82e0030cf41454f57082ca3e9241549cd38 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 11:38:07 +0100 Subject: [PATCH 020/131] uv_python module: add integration test for when uv executable does not exist --- .../integration/targets/uv_python/tasks/main.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 3495a18984..2e58ea92de 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -8,6 +8,20 @@ # 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 python 3.14 when no uv executable exists + uv_python: + version: 3.14 + state: present + check_mode: yes + register: failed_install + ignore_errors: true + +- name: Verify python 3.14 installation failed + assert: + that: + - failed_install["failed"] is true + - '"Failed to find required executable" in failed_install["msg"]' + - name: Install uv shell: | curl -LsSf https://astral.sh/uv/install.sh | sh From 9bbf6efe28c35c94e4ed2a049e02acf3e2cdfd8e Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 11:42:49 +0100 Subject: [PATCH 021/131] uv_python module: improve exception handling --- plugins/modules/uv_python.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 88609c2c07..9b5bcd9a81 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -146,17 +146,19 @@ def main(): msg="", ) state = module.params["state"] - uv = UV(module) - if state == "present": - result["changed"], result["msg"] = uv.install_python() - elif state == "absent": - result["changed"], result["msg"] = uv.uninstall_python() - elif state == "latest": - result["changed"], result["msg"] = uv.upgrade_python() - - module.exit_json(**result) + try: + uv = UV(module) + if state == "present": + result["changed"], result["msg"] = uv.install_python() + elif state == "absent": + result["changed"], result["msg"] = uv.uninstall_python() + elif state == "latest": + result["changed"], result["msg"] = uv.upgrade_python() + module.exit_json(**result) + except Exception as e: + module.fail_json(msg=str(e)) if __name__ == "__main__": main() \ No newline at end of file From ce10eba5746d2a7abd4da2e776beababd357d114 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 14:28:57 +0100 Subject: [PATCH 022/131] uv_python module: add integration test for case when specified version does not exist --- .../targets/uv_python/tasks/main.yaml | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 2e58ea92de..7df14c8d0b 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -172,7 +172,7 @@ ansible.builtin.assert: that: - invalid_version is failed - - "'Expected formats are X.Y or X.Y.Z' in invalid_version.msg" + - '"Expected formats are X.Y or X.Y.Z" in invalid_version.msg' - name: Upgrade python 3.13 uv_python: @@ -199,4 +199,33 @@ that: - upgrade_python["changed"] is false - upgrade_python["failed"] is false - - '"3.13" in upgrade_python["msg"]' \ No newline at end of file + - '"3.13" in upgrade_python["msg"]' + +- name: Install unexisting python version + uv_python: + version: 3.12.13 + state: present + register: unexisting_python + ignore_errors: true + +- name: Verify python 3.13.5 deletion in check mode + assert: + that: + - unexisting_python["changed"] is false + - unexisting_python["failed"] is true + - '"No download found for request" in unexisting_python["msg"]' + +- name: Install unexisting python version + uv_python: + version: 3.12.13 + state: latest + register: unexisting_python + ignore_errors: true +- debug: + var: unexisting_python +- name: Verify python 3.13.5 deletion in check mode + assert: + that: + - unexisting_python["changed"] is false + - unexisting_python["failed"] is true + - '"Version 3.12.13 does not exist" in unexisting_python["msg"]' \ No newline at end of file From cfecbc6d7bae51b9b3bed9bb3c0aba5789b44a94 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 14:31:58 +0100 Subject: [PATCH 023/131] uv_python module: refactor code --- .../targets/uv_python/tasks/main.yaml | 91 +++++++++---------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 7df14c8d0b..5a2ba002ee 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -19,8 +19,8 @@ - name: Verify python 3.14 installation failed assert: that: - - failed_install["failed"] is true - - '"Failed to find required executable" in failed_install["msg"]' + - failed_install.failed is true + - '"Failed to find required executable" in failed_install.msg' - name: Install uv shell: | @@ -38,9 +38,9 @@ - name: Verify python 3.14 installation in check mode assert: that: - - install_check_mode["changed"] is true - - install_check_mode["failed"] is false - - '"3.14" in install_check_mode["msg"]' + - install_check_mode.changed is true + - install_check_mode.failed is false + - '"3.14" in install_check_mode.msg' - name: Install python 3.14 uv_python: @@ -51,9 +51,9 @@ - name: Verify python 3.14 installation in check mode assert: that: - - install_python["changed"] is true - - install_python["failed"] is false - - '"3.14" in install_python["msg"]' + - install_python.changed is true + - install_python.failed is false + - '"3.14" in install_python.msg' - name: Re-install python 3.14 uv_python: @@ -64,9 +64,9 @@ - name: Verify python 3.14 re-installation assert: that: - - reinstall_python["changed"] is false - - reinstall_python["failed"] is false - - '"3.14" in reinstall_python["msg"]' + - reinstall_python.changed is false + - reinstall_python.failed is false + - '"3.14" in reinstall_python.msg' - name: Install latest python 3.15 # installs latest patch version for 3.15 uv_python: @@ -78,9 +78,9 @@ - name: Verify python 3.15 installation assert: that: - - install_latest_python["changed"] is true - - install_latest_python["failed"] is false - - '"3.15" in install_latest_python["msg"]' + - install_latest_python.changed is true + - install_latest_python.failed is false + - '"3.15" in install_latest_python.msg' - name: Install python 3.13.5 uv_python: @@ -91,9 +91,9 @@ - name: Verify python 3.13.5 installation assert: that: - - install_python["changed"] is true - - install_python["failed"] is false - - '"3.13.5" in install_python["msg"]' + - install_python.changed is true + - install_python.failed is false + - '"3.13.5" in install_python.msg' - name: Re-install python 3.13.5 uv_python: @@ -104,9 +104,9 @@ - name: Verify python 3.13.5 installation assert: that: - - reinstall_python["changed"] is false - - reinstall_python["failed"] is false - - '"3.13.5" in reinstall_python["msg"]' + - reinstall_python.changed is false + - reinstall_python.failed is false + - '"3.13.5" in reinstall_python.msg' - name: Remove unexisting python 3.10 # removes latest patch version for 3.10 if exists uv_python: @@ -117,9 +117,9 @@ - name: Verify python 3.10 deletion assert: that: - - remove_python["changed"] is false - - remove_python["failed"] is false - - 'remove_python["msg"] == ""' + - remove_python.changed is false + - remove_python.failed is false + - 'remove_python.msg == ""' - name: Remove python 3.13.5 in check mode uv_python: @@ -131,9 +131,9 @@ - name: Verify python 3.13.5 deletion in check mode assert: that: - - remove_python_in_check_mode["changed"] is true - - remove_python_in_check_mode["failed"] is false - - 'remove_python_in_check_mode["msg"] == ""' + - remove_python_in_check_mode.changed is true + - remove_python_in_check_mode.failed is false + - 'remove_python_in_check_mode.msg == ""' - name: Remove python 3.13.5 uv_python: @@ -144,9 +144,9 @@ - name: Verify python 3.13.5 deletion assert: that: - - remove_python["changed"] is true - - remove_python["failed"] is false - - 'remove_python["msg"] == ""' + - remove_python.changed is true + - remove_python.failed is false + - 'remove_python.msg == ""' - name: Remove python 3.13.5 again uv_python: @@ -157,9 +157,9 @@ - name: Verify python 3.13.5 deletion assert: that: - - remove_python["changed"] is false - - remove_python["failed"] is false - - 'remove_python["msg"] == ""' + - remove_python.changed is false + - remove_python.failed is false + - 'remove_python.msg == ""' - name: Install python 3 uv_python: @@ -183,9 +183,9 @@ - name: Verify python 3.13 upgrade assert: that: - - upgrade_python["changed"] is true - - upgrade_python["failed"] is false - - '"3.13" in upgrade_python["msg"]' + - upgrade_python.changed is true + - upgrade_python.failed is false + - '"3.13" in upgrade_python.msg' - name: Upgrade python 3.13 in check mode uv_python: @@ -197,9 +197,9 @@ - name: Verify python 3.13.5 deletion in check mode assert: that: - - upgrade_python["changed"] is false - - upgrade_python["failed"] is false - - '"3.13" in upgrade_python["msg"]' + - upgrade_python.changed is false + - upgrade_python.failed is false + - '"3.13" in upgrade_python.msg' - name: Install unexisting python version uv_python: @@ -211,9 +211,9 @@ - name: Verify python 3.13.5 deletion in check mode assert: that: - - unexisting_python["changed"] is false - - unexisting_python["failed"] is true - - '"No download found for request" in unexisting_python["msg"]' + - unexisting_python.changed is false + - unexisting_python.failed is true + - '"No download found for request" in unexisting_python.msg' - name: Install unexisting python version uv_python: @@ -221,11 +221,10 @@ state: latest register: unexisting_python ignore_errors: true -- debug: - var: unexisting_python + - name: Verify python 3.13.5 deletion in check mode assert: that: - - unexisting_python["changed"] is false - - unexisting_python["failed"] is true - - '"Version 3.12.13 does not exist" in unexisting_python["msg"]' \ No newline at end of file + - unexisting_python.changed is false + - unexisting_python.failed is true + - '"Version 3.12.13 does not exist" in unexisting_python.msg' \ No newline at end of file From 5c842b805bff83f7a6fd3f0607d073db5420d9e4 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 14:33:33 +0100 Subject: [PATCH 024/131] uv_python module: handle case when provided python version does not exist in latest state --- plugins/modules/uv_python.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 9b5bcd9a81..7f58925610 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -70,9 +70,9 @@ class UV: self.python_version = StrictVersion(python_version) self.python_version_str = self.python_version.__str__() except ValueError: - module.fail_json( + self.module.fail_json( msg=( - f"Invalid version '{python_version}'. " + f"Invalid version {python_version}. " "Expected formats are X.Y or X.Y.Z" ) ) @@ -126,11 +126,23 @@ class UV: return rc, out, err def _get_latest_patch_release(self): + """ + Returns latest available patch release for a given python version + """ _, out, _ = self._list_python() # uv returns versions in descending order but we sort them just in case future uv behavior changes - results = json.loads(out) - versions = [StrictVersion(result["version"]) for result in results] - return max(versions).__str__() - + latest_version = "" + try: + results = json.loads(out) + versions = [StrictVersion(result["version"]) for result in results] + latest_version = max(versions).__str__() + except ValueError: + self.module.fail_json( + msg=( + f"Version {self.python_version_str} does not exist. " + ) + ) + return latest_version + def main(): module = AnsibleModule( From 92f7059686b3b35b9fe80b168e74f7c0324acaee Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 16:12:50 +0100 Subject: [PATCH 025/131] uv_python module: improve methods' return values and add docstrings --- plugins/modules/uv_python.py | 66 ++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 7f58925610..3b3c0f021e 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -78,28 +78,49 @@ class UV: ) def install_python(self): - rc, out, _ = self._find_python("--show-version") - if rc == 0: - return False, out.split()[0] - if self.module.check_mode: - return True, self.python_version_str - - cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] - self.module.run_command(cmd, check_rc=True) + """ + Runs command 'uv python install X.Y.Z' which installs specified python version. + If patch version is not specified uv installs latest available patch version. + Returns: tuple with following elements + - boolean to indicate if method changed state + - installed version + """ + rc, out, _ = self._find_python("--show-version") + if rc == 0: + return False, out.split()[0] + if self.module.check_mode: return True, self.python_version_str + + cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] + self.module.run_command(cmd, check_rc=True) + return True, self.python_version_str def uninstall_python(self): - rc, out, _ = self._find_python() + """ + Runs command 'uv python uninstall X.Y.Z' which removes specified python version from environment. + If patch version is not specified all correspending installed patch versions are removed. + Returns: tuple with following elements + - boolean to indicate if method changed state + - removed version + """ + rc, _, _ = self._find_python("--show-version") + # if "uv python find" fails, it means specified version does not exist if rc != 0: - return False, out + return False, "" if self.module.check_mode: - return True, "" + return True, self.python_version_str cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "uninstall", self.python_version_str] self.module.run_command(cmd, check_rc=True) - return True, "" + return True, self.python_version_str def upgrade_python(self): + """ + Runs command 'uv python install X.Y.Z' which installs specified python version. + Returns: tuple with following elements + - boolean to indicate if method changed state + - installed version + """ rc, out, _ = self._find_python("--show-version") latest_version = self._get_latest_patch_release() if rc == 0: @@ -114,12 +135,26 @@ class UV: return True, latest_version def _find_python(self, *args): - # if multiple similar minor versions exist, "uv python find" returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version + """ + Runs command 'uv python find' which returns path of installed patch releases for a given python version. + If multiple patch versions are installed, "uv python find" returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version. + Returns: tuple with following elements + - return code of executed command + - stdout of command + - stderr of command + """ cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "find", self.python_version_str, *args] rc, out, err = self.module.run_command(cmd) return rc, out, err def _list_python(self): + """ + Runs command 'uv python list' which returns list of installed patch releases for a given python version. + Returns: tuple with following elements + - return code of executed command + - stdout of command + - stderr of command + """ # https://docs.astral.sh/uv/reference/cli/#uv-python-list cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "list", self.python_version_str, "--output-format", "json"] rc, out, err = self.module.run_command(cmd) @@ -127,7 +162,8 @@ class UV: def _get_latest_patch_release(self): """ - Returns latest available patch release for a given python version + Returns latest available patch release for a given python version. + Fails when no available release exists for the specified version. """ _, out, _ = self._list_python() # uv returns versions in descending order but we sort them just in case future uv behavior changes latest_version = "" @@ -138,7 +174,7 @@ class UV: except ValueError: self.module.fail_json( msg=( - f"Version {self.python_version_str} does not exist. " + f"Version {self.python_version_str} is not available. " ) ) return latest_version From 571a5b51616b040fdbf65eca9a93c1b3bdddefd7 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 16:13:33 +0100 Subject: [PATCH 026/131] uv_python module: improve integration tests --- .../integration/targets/uv_python/tasks/main.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 5a2ba002ee..e2ffdced48 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -133,7 +133,7 @@ that: - remove_python_in_check_mode.changed is true - remove_python_in_check_mode.failed is false - - 'remove_python_in_check_mode.msg == ""' + - '"3.13.5" in remove_python_in_check_mode.msg' - name: Remove python 3.13.5 uv_python: @@ -146,7 +146,7 @@ that: - remove_python.changed is true - remove_python.failed is false - - 'remove_python.msg == ""' + - '"3.13.5" in remove_python_in_check_mode.msg' - name: Remove python 3.13.5 again uv_python: @@ -154,7 +154,7 @@ state: absent register: remove_python -- name: Verify python 3.13.5 deletion +- name: Verify python 3.13.5 deletion again assert: that: - remove_python.changed is false @@ -194,7 +194,7 @@ check_mode: yes register: upgrade_python -- name: Verify python 3.13.5 deletion in check mode +- name: Verify python 3.13 deletion in check mode assert: that: - upgrade_python.changed is false @@ -208,7 +208,7 @@ register: unexisting_python ignore_errors: true -- name: Verify python 3.13.5 deletion in check mode +- name: Verify python 3.12.13 deletion assert: that: - unexisting_python.changed is false @@ -222,9 +222,9 @@ register: unexisting_python ignore_errors: true -- name: Verify python 3.13.5 deletion in check mode +- name: Verify python 3.12.13 deletion assert: that: - unexisting_python.changed is false - unexisting_python.failed is true - - '"Version 3.12.13 does not exist" in unexisting_python.msg' \ No newline at end of file + - '"Version 3.12.13 is not available" in unexisting_python.msg' \ No newline at end of file From debdce35e7fec70051eb3f3ba2f20b2f6886d873 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 16 Feb 2026 16:16:29 +0100 Subject: [PATCH 027/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 3b3c0f021e..a88066c7d8 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -123,9 +123,7 @@ class UV: """ rc, out, _ = self._find_python("--show-version") latest_version = self._get_latest_patch_release() - if rc == 0: - detected_version = out.split()[0] - if detected_version == latest_version: + if rc == 0 and out.split()[0] == latest_version: return False, latest_version if self.module.check_mode: return True, latest_version From 5819c8eb7b72fe21e4a189f9efe1d474a6b20247 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 17 Feb 2026 14:42:17 +0100 Subject: [PATCH 028/131] uv_python module: improve check mode for present state to fail when no patch version is available --- plugins/modules/uv_python.py | 15 +++++++---- .../targets/uv_python/tasks/main.yaml | 27 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index a88066c7d8..f92a15e5e4 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -87,13 +87,17 @@ class UV: """ rc, out, _ = self._find_python("--show-version") if rc == 0: - return False, out.split()[0] + return False, out.split()[0], False if self.module.check_mode: - return True, self.python_version_str + _, versions_available, _ = self._list_python() + # when uv does not find any available patch version the install command will fail + if not json.loads(versions_available): + return False, "", True + return True, self.python_version_str, False cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] self.module.run_command(cmd, check_rc=True) - return True, self.python_version_str + return True, self.python_version_str, False def uninstall_python(self): """ @@ -172,7 +176,7 @@ class UV: except ValueError: self.module.fail_json( msg=( - f"Version {self.python_version_str} is not available. " + f"Version {self.python_version_str} is not available." ) ) return latest_version @@ -190,13 +194,14 @@ def main(): result = dict( changed=False, msg="", + failed=False ) state = module.params["state"] try: uv = UV(module) if state == "present": - result["changed"], result["msg"] = uv.install_python() + result["changed"], result["msg"], result["failed"] = uv.install_python() elif state == "absent": result["changed"], result["msg"] = uv.uninstall_python() elif state == "latest": diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index e2ffdced48..d009ab6446 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -20,7 +20,6 @@ assert: that: - failed_install.failed is true - - '"Failed to find required executable" in failed_install.msg' - name: Install uv shell: | @@ -73,8 +72,7 @@ version: 3.15 state: latest register: install_latest_python -- debug: - var: install_latest_python + - name: Verify python 3.15 installation assert: that: @@ -201,6 +199,21 @@ - upgrade_python.failed is false - '"3.13" in upgrade_python.msg' +- name: Install unexisting python version in check mode + uv_python: + version: 3.12.13 + state: present + register: unexisting_python_check_mode + ignore_errors: true + check_mode: true + +- name: Verify unexisting python 3.12.13 install in check mode + assert: + that: + - unexisting_python_check_mode.changed is false + - unexisting_python_check_mode.failed is true + - 'unexisting_python_check_mode.msg == ""' + - name: Install unexisting python version uv_python: version: 3.12.13 @@ -208,23 +221,21 @@ register: unexisting_python ignore_errors: true -- name: Verify python 3.12.13 deletion +- name: Verify unexisting python 3.12.13 install assert: that: - unexisting_python.changed is false - unexisting_python.failed is true - - '"No download found for request" in unexisting_python.msg' -- name: Install unexisting python version +- name: Install unexisting python version with latest state uv_python: version: 3.12.13 state: latest register: unexisting_python ignore_errors: true -- name: Verify python 3.12.13 deletion +- name: Verify python 3.12.13 deletion with latest state assert: that: - unexisting_python.changed is false - unexisting_python.failed is true - - '"Version 3.12.13 is not available" in unexisting_python.msg' \ No newline at end of file From 54e7a639efb8088e18b122adbe675091b2e51b04 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 17 Feb 2026 15:17:02 +0100 Subject: [PATCH 029/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 23 ++++++++++--------- .../targets/uv_python/tasks/main.yaml | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index f92a15e5e4..e1916d7f94 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -87,17 +87,20 @@ class UV: """ rc, out, _ = self._find_python("--show-version") if rc == 0: - return False, out.split()[0], False + return False, out.split()[0] if self.module.check_mode: _, versions_available, _ = self._list_python() # when uv does not find any available patch version the install command will fail - if not json.loads(versions_available): - return False, "", True - return True, self.python_version_str, False + try: + if not json.loads(versions_available): + self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) + except json.decoder.JSONDecodeError as e: + self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") + return True, self.python_version_str cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] self.module.run_command(cmd, check_rc=True) - return True, self.python_version_str, False + return True, self.python_version_str def uninstall_python(self): """ @@ -127,6 +130,8 @@ class UV: """ rc, out, _ = self._find_python("--show-version") latest_version = self._get_latest_patch_release() + if not latest_version: + self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) if rc == 0 and out.split()[0] == latest_version: return False, latest_version if self.module.check_mode: @@ -174,11 +179,7 @@ class UV: versions = [StrictVersion(result["version"]) for result in results] latest_version = max(versions).__str__() except ValueError: - self.module.fail_json( - msg=( - f"Version {self.python_version_str} is not available." - ) - ) + pass return latest_version @@ -201,7 +202,7 @@ def main(): try: uv = UV(module) if state == "present": - result["changed"], result["msg"], result["failed"] = uv.install_python() + result["changed"], result["msg"] = uv.install_python() elif state == "absent": result["changed"], result["msg"] = uv.uninstall_python() elif state == "latest": diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index d009ab6446..7b569e5f0f 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -212,7 +212,7 @@ that: - unexisting_python_check_mode.changed is false - unexisting_python_check_mode.failed is true - - 'unexisting_python_check_mode.msg == ""' + - '"Version 3.12.13 is not available." in unexisting_python_check_mode.msg' - name: Install unexisting python version uv_python: From 4659d3f54416122a5139bd5c83ad1633b87ac26e Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 17 Feb 2026 15:29:03 +0100 Subject: [PATCH 030/131] uv_python module: remove catch all exception --- plugins/modules/uv_python.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e1916d7f94..58720b3d63 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -199,18 +199,15 @@ def main(): ) state = module.params["state"] - try: - uv = UV(module) - if state == "present": - result["changed"], result["msg"] = uv.install_python() - elif state == "absent": - result["changed"], result["msg"] = uv.uninstall_python() - elif state == "latest": - result["changed"], result["msg"] = uv.upgrade_python() + uv = UV(module) + if state == "present": + result["changed"], result["msg"] = uv.install_python() + elif state == "absent": + result["changed"], result["msg"] = uv.uninstall_python() + elif state == "latest": + result["changed"], result["msg"] = uv.upgrade_python() - module.exit_json(**result) - except Exception as e: - module.fail_json(msg=str(e)) + module.exit_json(**result) if __name__ == "__main__": main() \ No newline at end of file From afbe833c02c111e9e9b08537748256f7d9ccec5b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 17 Feb 2026 16:46:42 +0100 Subject: [PATCH 031/131] uv_python module: return commands' stderr and return code as a variable of stdout --- plugins/modules/uv_python.py | 44 ++++++++++--------- .../targets/uv_python/tasks/main.yaml | 24 +++++----- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 58720b3d63..ff69f87e51 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -87,7 +87,7 @@ class UV: """ rc, out, _ = self._find_python("--show-version") if rc == 0: - return False, out.split()[0] + return False, out.split()[0], "", rc if self.module.check_mode: _, versions_available, _ = self._list_python() # when uv does not find any available patch version the install command will fail @@ -96,11 +96,11 @@ class UV: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) except json.decoder.JSONDecodeError as e: self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") - return True, self.python_version_str + return True, self.python_version_str, "", 0 cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] - self.module.run_command(cmd, check_rc=True) - return True, self.python_version_str + rc, _, err = self.module.run_command(cmd, check_rc=True) + return True, self.python_version_str, err, rc def uninstall_python(self): """ @@ -110,16 +110,18 @@ class UV: - boolean to indicate if method changed state - removed version """ - rc, _, _ = self._find_python("--show-version") - # if "uv python find" fails, it means specified version does not exist - if rc != 0: - return False, "" + rc, _, err = self._find_python("--show-version") + # if "uv python find" fails with return code 2, it means specified version is not installed or is not available + if rc == 2: + return False, "", "", 0 + elif rc != 0: + self.module.fail_json(msg=err) if self.module.check_mode: - return True, self.python_version_str + return True, self.python_version_str, "", 0 cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "uninstall", self.python_version_str] - self.module.run_command(cmd, check_rc=True) - return True, self.python_version_str + rc, _, err = self.module.run_command(cmd, check_rc=True) + return True, self.python_version_str, err, rc def upgrade_python(self): """ @@ -131,15 +133,15 @@ class UV: rc, out, _ = self._find_python("--show-version") latest_version = self._get_latest_patch_release() if not latest_version: - self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) + self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and out.split()[0] == latest_version: - return False, latest_version + return False, latest_version, "", rc if self.module.check_mode: - return True, latest_version + return True, latest_version, "", 0 cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", latest_version] - self.module.run_command(cmd, check_rc=True) - return True, latest_version + rc, _, err = self.module.run_command(cmd, check_rc=True) + return True, latest_version, err, rc def _find_python(self, *args): """ @@ -194,18 +196,20 @@ def main(): result = dict( changed=False, - msg="", + stdout="", + stderr="", + rc=0, failed=False ) state = module.params["state"] uv = UV(module) if state == "present": - result["changed"], result["msg"] = uv.install_python() + result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.install_python() elif state == "absent": - result["changed"], result["msg"] = uv.uninstall_python() + result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.uninstall_python() elif state == "latest": - result["changed"], result["msg"] = uv.upgrade_python() + result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.upgrade_python() module.exit_json(**result) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 7b569e5f0f..9d2574dfbd 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -39,7 +39,7 @@ that: - install_check_mode.changed is true - install_check_mode.failed is false - - '"3.14" in install_check_mode.msg' + - '"3.14" in install_check_mode.stdout' - name: Install python 3.14 uv_python: @@ -52,7 +52,7 @@ that: - install_python.changed is true - install_python.failed is false - - '"3.14" in install_python.msg' + - '"3.14" in install_python.stdout' - name: Re-install python 3.14 uv_python: @@ -65,7 +65,7 @@ that: - reinstall_python.changed is false - reinstall_python.failed is false - - '"3.14" in reinstall_python.msg' + - '"3.14" in reinstall_python.stdout' - name: Install latest python 3.15 # installs latest patch version for 3.15 uv_python: @@ -78,7 +78,7 @@ that: - install_latest_python.changed is true - install_latest_python.failed is false - - '"3.15" in install_latest_python.msg' + - '"3.15" in install_latest_python.stdout' - name: Install python 3.13.5 uv_python: @@ -91,7 +91,7 @@ that: - install_python.changed is true - install_python.failed is false - - '"3.13.5" in install_python.msg' + - '"3.13.5" in install_python.stdout' - name: Re-install python 3.13.5 uv_python: @@ -104,7 +104,7 @@ that: - reinstall_python.changed is false - reinstall_python.failed is false - - '"3.13.5" in reinstall_python.msg' + - '"3.13.5" in reinstall_python.stdout' - name: Remove unexisting python 3.10 # removes latest patch version for 3.10 if exists uv_python: @@ -117,7 +117,7 @@ that: - remove_python.changed is false - remove_python.failed is false - - 'remove_python.msg == ""' + - 'remove_python.stdout == ""' - name: Remove python 3.13.5 in check mode uv_python: @@ -131,7 +131,7 @@ that: - remove_python_in_check_mode.changed is true - remove_python_in_check_mode.failed is false - - '"3.13.5" in remove_python_in_check_mode.msg' + - '"3.13.5" in remove_python_in_check_mode.stdout' - name: Remove python 3.13.5 uv_python: @@ -144,7 +144,7 @@ that: - remove_python.changed is true - remove_python.failed is false - - '"3.13.5" in remove_python_in_check_mode.msg' + - '"3.13.5" in remove_python_in_check_mode.stdout' - name: Remove python 3.13.5 again uv_python: @@ -157,7 +157,7 @@ that: - remove_python.changed is false - remove_python.failed is false - - 'remove_python.msg == ""' + - 'remove_python.stdout == ""' - name: Install python 3 uv_python: @@ -183,7 +183,7 @@ that: - upgrade_python.changed is true - upgrade_python.failed is false - - '"3.13" in upgrade_python.msg' + - '"3.13" in upgrade_python.stdout' - name: Upgrade python 3.13 in check mode uv_python: @@ -197,7 +197,7 @@ that: - upgrade_python.changed is false - upgrade_python.failed is false - - '"3.13" in upgrade_python.msg' + - '"3.13" in upgrade_python.stdout' - name: Install unexisting python version in check mode uv_python: From 0b158b61530015e0b5b86ca46b9212afecd0b0e8 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 18 Feb 2026 11:18:14 +0100 Subject: [PATCH 032/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index ff69f87e51..76d9fbce09 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -87,7 +87,7 @@ class UV: """ rc, out, _ = self._find_python("--show-version") if rc == 0: - return False, out.split()[0], "", rc + return False, out.strip(), "", rc if self.module.check_mode: _, versions_available, _ = self._list_python() # when uv does not find any available patch version the install command will fail @@ -134,7 +134,7 @@ class UV: latest_version = self._get_latest_patch_release() if not latest_version: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") - if rc == 0 and out.split()[0] == latest_version: + if rc == 0 and out.strip() == latest_version: return False, latest_version, "", rc if self.module.check_mode: return True, latest_version, "", 0 From ae5eeaffe2f38dbe6512ca9053bed35d152b7bac Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 19 Feb 2026 20:30:22 +0100 Subject: [PATCH 033/131] uv_python module: add python version to module return values for present state --- plugins/modules/uv_python.py | 43 +++++++++++-------- .../targets/uv_python/tasks/main.yaml | 26 ++++++++--- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 76d9fbce09..2856bc8115 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -56,6 +56,14 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.compat.version import StrictVersion +def max_version(versions_list): + max_version = "" + if versions_list: + versions = [StrictVersion(result["version"]) for result in versions_list] + max_version = max(versions).__str__() + return max_version + + class UV: """ Module for "uv python" command @@ -85,22 +93,27 @@ class UV: - boolean to indicate if method changed state - installed version """ - rc, out, _ = self._find_python("--show-version") - if rc == 0: - return False, out.strip(), "", rc + find_rc, find_out, _ = self._find_python("--show-version") + if find_rc == 0: + existing_version = find_out.split()[0] + return False, "", "", 0, existing_version if self.module.check_mode: - _, versions_available, _ = self._list_python() + _, out_list, _ = self._list_python() + versions_available = json.loads(out_list) + version = max_version(versions_available) # when uv does not find any available patch version the install command will fail try: - if not json.loads(versions_available): + if not versions_available: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) except json.decoder.JSONDecodeError as e: self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") - return True, self.python_version_str, "", 0 + return True, self.python_version_str, "", 0, version cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] - rc, _, err = self.module.run_command(cmd, check_rc=True) - return True, self.python_version_str, err, rc + rc, out, err = self.module.run_command(cmd, check_rc=True, expand_user_and_vars=False) + _, find_out, _ = self._find_python("--show-version") + installed_version = find_out.split()[0] + return True, out, err, rc, installed_version def uninstall_python(self): """ @@ -153,7 +166,7 @@ class UV: - stderr of command """ cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "find", self.python_version_str, *args] - rc, out, err = self.module.run_command(cmd) + rc, out, err = self.module.run_command(cmd, expand_user_and_vars=False) return rc, out, err def _list_python(self): @@ -175,13 +188,8 @@ class UV: Fails when no available release exists for the specified version. """ _, out, _ = self._list_python() # uv returns versions in descending order but we sort them just in case future uv behavior changes - latest_version = "" - try: - results = json.loads(out) - versions = [StrictVersion(result["version"]) for result in results] - latest_version = max(versions).__str__() - except ValueError: - pass + results = json.loads(out) + latest_version = max_version(results) return latest_version @@ -199,13 +207,14 @@ def main(): stdout="", stderr="", rc=0, + python_version="", failed=False ) state = module.params["state"] uv = UV(module) if state == "present": - result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.install_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_version"] = uv.install_python() elif state == "absent": result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.uninstall_python() elif state == "latest": diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 9d2574dfbd..08b135b34b 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -39,7 +39,7 @@ that: - install_check_mode.changed is true - install_check_mode.failed is false - - '"3.14" in install_check_mode.stdout' + - '"3.14" in install_check_mode.python_version' - name: Install python 3.14 uv_python: @@ -52,7 +52,7 @@ that: - install_python.changed is true - install_python.failed is false - - '"3.14" in install_python.stdout' + - '"3.14" in install_python.python_version' - name: Re-install python 3.14 uv_python: @@ -65,7 +65,7 @@ that: - reinstall_python.changed is false - reinstall_python.failed is false - - '"3.14" in reinstall_python.stdout' + - '"3.14" in reinstall_python.python_version' - name: Install latest python 3.15 # installs latest patch version for 3.15 uv_python: @@ -91,7 +91,7 @@ that: - install_python.changed is true - install_python.failed is false - - '"3.13.5" in install_python.stdout' + - '"3.13.5" in install_python.python_version' - name: Re-install python 3.13.5 uv_python: @@ -104,7 +104,7 @@ that: - reinstall_python.changed is false - reinstall_python.failed is false - - '"3.13.5" in reinstall_python.stdout' + - '"3.13.5" in reinstall_python.python_version' - name: Remove unexisting python 3.10 # removes latest patch version for 3.10 if exists uv_python: @@ -192,7 +192,21 @@ check_mode: yes register: upgrade_python -- name: Verify python 3.13 deletion in check mode +- name: Verify python 3.13 upgrade in check mode + assert: + that: + - upgrade_python.changed is false + - upgrade_python.failed is false + - '"3.13" in upgrade_python.stdout' + +- name: Upgrade python 3.13 again + uv_python: + version: 3.13 + state: latest + check_mode: yes + register: upgrade_python + +- name: Verify python 3.13 upgrade again assert: that: - upgrade_python.changed is false From d8646b2db482675ab294f5e9c3d18e3a18b3b352 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 19 Feb 2026 21:32:54 +0100 Subject: [PATCH 034/131] uv_python module: add python version to module return values for absent state --- plugins/modules/uv_python.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 2856bc8115..ed5a3605d8 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -96,7 +96,7 @@ class UV: find_rc, find_out, _ = self._find_python("--show-version") if find_rc == 0: existing_version = find_out.split()[0] - return False, "", "", 0, existing_version + return False, "", "", 0, [existing_version] if self.module.check_mode: _, out_list, _ = self._list_python() versions_available = json.loads(out_list) @@ -107,13 +107,13 @@ class UV: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) except json.decoder.JSONDecodeError as e: self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") - return True, self.python_version_str, "", 0, version + return True, "", "", 0, [version] cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] rc, out, err = self.module.run_command(cmd, check_rc=True, expand_user_and_vars=False) _, find_out, _ = self._find_python("--show-version") installed_version = find_out.split()[0] - return True, out, err, rc, installed_version + return True, out, err, rc, [installed_version] def uninstall_python(self): """ @@ -126,15 +126,16 @@ class UV: rc, _, err = self._find_python("--show-version") # if "uv python find" fails with return code 2, it means specified version is not installed or is not available if rc == 2: - return False, "", "", 0 + return False, "", "", 0, [] elif rc != 0: self.module.fail_json(msg=err) + installed_versions = self._get_installed_versions() if self.module.check_mode: - return True, self.python_version_str, "", 0 + return True, "", "", 0, installed_versions cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "uninstall", self.python_version_str] - rc, _, err = self.module.run_command(cmd, check_rc=True) - return True, self.python_version_str, err, rc + rc, out, err = self.module.run_command(cmd, check_rc=True) + return True, out, err, rc, installed_versions def upgrade_python(self): """ @@ -169,7 +170,7 @@ class UV: rc, out, err = self.module.run_command(cmd, expand_user_and_vars=False) return rc, out, err - def _list_python(self): + def _list_python(self, *args): """ Runs command 'uv python list' which returns list of installed patch releases for a given python version. Returns: tuple with following elements @@ -178,7 +179,7 @@ class UV: - stderr of command """ # https://docs.astral.sh/uv/reference/cli/#uv-python-list - cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "list", self.python_version_str, "--output-format", "json"] + cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "list", self.python_version_str, "--output-format", "json", *args] rc, out, err = self.module.run_command(cmd) return rc, out, err @@ -191,6 +192,10 @@ class UV: results = json.loads(out) latest_version = max_version(results) return latest_version + + def _get_installed_versions(self): + _, out_list, _ = self._list_python("--only-installed") + return [result["version"] for result in json.loads(out_list)] def main(): @@ -207,7 +212,7 @@ def main(): stdout="", stderr="", rc=0, - python_version="", + python_version=[], failed=False ) state = module.params["state"] @@ -216,7 +221,7 @@ def main(): if state == "present": result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_version"] = uv.install_python() elif state == "absent": - result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.uninstall_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_version"] = uv.uninstall_python() elif state == "latest": result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.upgrade_python() From 594ff22aa2125997634d6f21ab9a7d678e018dbb Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 20 Feb 2026 17:05:23 +0100 Subject: [PATCH 035/131] uv_python module: add python version to module return values for latest state --- plugins/modules/uv_python.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index ed5a3605d8..af57631539 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -149,13 +149,13 @@ class UV: if not latest_version: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and out.strip() == latest_version: - return False, latest_version, "", rc + return False, "", "", rc, [latest_version] if self.module.check_mode: - return True, latest_version, "", 0 + return True, "", "", 0, [latest_version] cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", latest_version] - rc, _, err = self.module.run_command(cmd, check_rc=True) - return True, latest_version, err, rc + rc, out, err = self.module.run_command(cmd, check_rc=True) + return True, out, err, rc, [latest_version] def _find_python(self, *args): """ @@ -212,18 +212,18 @@ def main(): stdout="", stderr="", rc=0, - python_version=[], + python_versions=[], failed=False ) state = module.params["state"] uv = UV(module) if state == "present": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_version"] = uv.install_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.install_python() elif state == "absent": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_version"] = uv.uninstall_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.uninstall_python() elif state == "latest": - result["changed"], result["stdout"], result["stderr"], result["rc"] = uv.upgrade_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.upgrade_python() module.exit_json(**result) From 1371cf2fcb2ad3ead628c196e7db69c37171c71e Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 20 Feb 2026 17:06:08 +0100 Subject: [PATCH 036/131] uv_python module: fix integration tests --- .../targets/uv_python/tasks/main.yaml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 08b135b34b..fec27dd6f9 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -39,7 +39,7 @@ that: - install_check_mode.changed is true - install_check_mode.failed is false - - '"3.14" in install_check_mode.python_version' + - 'install_check_mode.python_versions != []' - name: Install python 3.14 uv_python: @@ -47,12 +47,12 @@ state: present register: install_python -- name: Verify python 3.14 installation in check mode +- name: Verify python 3.14 installation assert: that: - install_python.changed is true - install_python.failed is false - - '"3.14" in install_python.python_version' + - 'install_python.python_versions != []' - name: Re-install python 3.14 uv_python: @@ -65,7 +65,7 @@ that: - reinstall_python.changed is false - reinstall_python.failed is false - - '"3.14" in reinstall_python.python_version' + - 'reinstall_python.python_versions != []' - name: Install latest python 3.15 # installs latest patch version for 3.15 uv_python: @@ -78,7 +78,7 @@ that: - install_latest_python.changed is true - install_latest_python.failed is false - - '"3.15" in install_latest_python.stdout' + - 'install_latest_python.stdout != []' - name: Install python 3.13.5 uv_python: @@ -91,7 +91,7 @@ that: - install_python.changed is true - install_python.failed is false - - '"3.13.5" in install_python.python_version' + - 'install_python.python_versions != []' - name: Re-install python 3.13.5 uv_python: @@ -104,7 +104,7 @@ that: - reinstall_python.changed is false - reinstall_python.failed is false - - '"3.13.5" in reinstall_python.python_version' + - 'reinstall_python.python_versions != []' - name: Remove unexisting python 3.10 # removes latest patch version for 3.10 if exists uv_python: @@ -117,7 +117,7 @@ that: - remove_python.changed is false - remove_python.failed is false - - 'remove_python.stdout == ""' + - 'remove_python.python_versions == []' - name: Remove python 3.13.5 in check mode uv_python: @@ -131,7 +131,7 @@ that: - remove_python_in_check_mode.changed is true - remove_python_in_check_mode.failed is false - - '"3.13.5" in remove_python_in_check_mode.stdout' + - 'remove_python_in_check_mode.python_versions != []' - name: Remove python 3.13.5 uv_python: @@ -144,7 +144,7 @@ that: - remove_python.changed is true - remove_python.failed is false - - '"3.13.5" in remove_python_in_check_mode.stdout' + - 'remove_python_in_check_mode.python_versions != []' - name: Remove python 3.13.5 again uv_python: @@ -157,7 +157,7 @@ that: - remove_python.changed is false - remove_python.failed is false - - 'remove_python.stdout == ""' + - 'remove_python.python_versions == []' - name: Install python 3 uv_python: @@ -183,7 +183,7 @@ that: - upgrade_python.changed is true - upgrade_python.failed is false - - '"3.13" in upgrade_python.stdout' + - 'upgrade_python.python_versions != []' - name: Upgrade python 3.13 in check mode uv_python: @@ -197,7 +197,7 @@ that: - upgrade_python.changed is false - upgrade_python.failed is false - - '"3.13" in upgrade_python.stdout' + - 'upgrade_python.python_versions != []' - name: Upgrade python 3.13 again uv_python: @@ -211,7 +211,7 @@ that: - upgrade_python.changed is false - upgrade_python.failed is false - - '"3.13" in upgrade_python.stdout' + - 'upgrade_python.python_versions != []' - name: Install unexisting python version in check mode uv_python: From 7814c7b3299911a7f30854916ee9ac3e35a64379 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 20 Feb 2026 17:48:46 +0100 Subject: [PATCH 037/131] uv_python module: add installation paths to return values for present state --- plugins/modules/uv_python.py | 59 +++++++++++++++++------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index af57631539..5624795040 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -56,14 +56,6 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.compat.version import StrictVersion -def max_version(versions_list): - max_version = "" - if versions_list: - versions = [StrictVersion(result["version"]) for result in versions_list] - max_version = max(versions).__str__() - return max_version - - class UV: """ Module for "uv python" command @@ -93,27 +85,21 @@ class UV: - boolean to indicate if method changed state - installed version """ - find_rc, find_out, _ = self._find_python("--show-version") + find_rc, find_version, _ = self._find_python("--show-version") if find_rc == 0: - existing_version = find_out.split()[0] - return False, "", "", 0, [existing_version] + _, find_out, _ = self._find_python() + return False, "", "", 0, [find_version.split()[0]], [find_out.split()[0]] if self.module.check_mode: - _, out_list, _ = self._list_python() - versions_available = json.loads(out_list) - version = max_version(versions_available) + latest_version, _ = self._get_latest_patch_release() # when uv does not find any available patch version the install command will fail - try: - if not versions_available: - self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) - except json.decoder.JSONDecodeError as e: - self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") - return True, "", "", 0, [version] + if not latest_version: + self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) + return True, "", "", 0, [latest_version], [""] cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] rc, out, err = self.module.run_command(cmd, check_rc=True, expand_user_and_vars=False) - _, find_out, _ = self._find_python("--show-version") - installed_version = find_out.split()[0] - return True, out, err, rc, [installed_version] + latest_version, path = self._get_latest_patch_release("--only-installed") + return True, out, err, rc, [latest_version], [path] def uninstall_python(self): """ @@ -129,7 +115,7 @@ class UV: return False, "", "", 0, [] elif rc != 0: self.module.fail_json(msg=err) - installed_versions = self._get_installed_versions() + installed_versions, _ = self._get_installed_versions() if self.module.check_mode: return True, "", "", 0, installed_versions @@ -145,7 +131,7 @@ class UV: - installed version """ rc, out, _ = self._find_python("--show-version") - latest_version = self._get_latest_patch_release() + latest_version, _ = self._get_latest_patch_release() if not latest_version: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and out.strip() == latest_version: @@ -183,19 +169,27 @@ class UV: rc, out, err = self.module.run_command(cmd) return rc, out, err - def _get_latest_patch_release(self): + def _get_latest_patch_release(self, *args): """ Returns latest available patch release for a given python version. Fails when no available release exists for the specified version. """ - _, out, _ = self._list_python() # uv returns versions in descending order but we sort them just in case future uv behavior changes - results = json.loads(out) - latest_version = max_version(results) - return latest_version + latest_version = path = "" + try: + _, out, _ = self._list_python(*args) # uv returns versions in descending order but we sort them just in case future uv behavior changes + results = json.loads(out) + if results: + version = max(results, key=lambda item: (item["version_parts"]["major"], item["version_parts"]["minor"], item["version_parts"]["patch"])) + latest_version = version["version"] + path = version["path"] + except json.decoder.JSONDecodeError as e: + self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") + return latest_version, path def _get_installed_versions(self): _, out_list, _ = self._list_python("--only-installed") - return [result["version"] for result in json.loads(out_list)] + results = json.loads(out_list) + return [result["version"] for result in results], [result["path"] for result in results] def main(): @@ -213,13 +207,14 @@ def main(): stderr="", rc=0, python_versions=[], + python_paths=[], failed=False ) state = module.params["state"] uv = UV(module) if state == "present": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.install_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.install_python() elif state == "absent": result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.uninstall_python() elif state == "latest": From 09f7a79e63cf663f6b77f2a55771b8c5078b3269 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 21 Feb 2026 10:27:38 +0100 Subject: [PATCH 038/131] uv_python module: add installation paths to return values for absent state --- plugins/modules/uv_python.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 5624795040..2ad280e608 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -109,19 +109,15 @@ class UV: - boolean to indicate if method changed state - removed version """ - rc, _, err = self._find_python("--show-version") - # if "uv python find" fails with return code 2, it means specified version is not installed or is not available - if rc == 2: - return False, "", "", 0, [] - elif rc != 0: - self.module.fail_json(msg=err) - installed_versions, _ = self._get_installed_versions() + installed_versions, install_paths = self._get_installed_versions() + if not installed_versions: + return False, "", "", 0, [], [] if self.module.check_mode: - return True, "", "", 0, installed_versions + return True, "", "", 0, installed_versions, install_paths cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "uninstall", self.python_version_str] rc, out, err = self.module.run_command(cmd, check_rc=True) - return True, out, err, rc, installed_versions + return True, out, err, rc, installed_versions, install_paths def upgrade_python(self): """ @@ -188,9 +184,13 @@ class UV: def _get_installed_versions(self): _, out_list, _ = self._list_python("--only-installed") - results = json.loads(out_list) - return [result["version"] for result in results], [result["path"] for result in results] - + try: + if out_list: + results = json.loads(out_list) + return [result["version"] for result in results], [result["path"] for result in results] + except json.decoder.JSONDecodeError: + self.module.fail_json(msg=f"Failed to parse 'uv python list' output") + return [], [] def main(): module = AnsibleModule( @@ -216,7 +216,7 @@ def main(): if state == "present": result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.install_python() elif state == "absent": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.uninstall_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.uninstall_python() elif state == "latest": result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.upgrade_python() From ef85e2c7d80a9147698366ae728f34e186ecadb2 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 21 Feb 2026 11:20:04 +0100 Subject: [PATCH 039/131] uv_python module: add installation paths to return values for latest state --- plugins/modules/uv_python.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 2ad280e608..b50e14c062 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -127,17 +127,18 @@ class UV: - installed version """ rc, out, _ = self._find_python("--show-version") - latest_version, _ = self._get_latest_patch_release() + latest_version, latest_path = self._get_latest_patch_release() if not latest_version: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and out.strip() == latest_version: - return False, "", "", rc, [latest_version] + return False, "", "", rc, [latest_version], [latest_path] if self.module.check_mode: - return True, "", "", 0, [latest_version] - + return True, "", "", 0, [latest_version], [] + # it's possible to have latest version already installed but not used as default so in this case 'uv python install' will set latest version as default cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", latest_version] rc, out, err = self.module.run_command(cmd, check_rc=True) - return True, out, err, rc, [latest_version] + latest_version, latest_path = self._get_latest_patch_release("--only-installed") + return True, out, err, rc, [latest_version], [latest_path] def _find_python(self, *args): """ @@ -182,8 +183,8 @@ class UV: self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") return latest_version, path - def _get_installed_versions(self): - _, out_list, _ = self._list_python("--only-installed") + def _get_installed_versions(self, *args): + _, out_list, _ = self._list_python("--only-installed", *args) try: if out_list: results = json.loads(out_list) @@ -218,7 +219,7 @@ def main(): elif state == "absent": result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.uninstall_python() elif state == "latest": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"] = uv.upgrade_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.upgrade_python() module.exit_json(**result) From ae68f9bcbbbfb7d9c8ff3b7644a633fcd5449684 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 21 Feb 2026 12:27:46 +0100 Subject: [PATCH 040/131] uv_python module: update present, absent and latest state to only include versions managed by uv in return values --- plugins/modules/uv_python.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index b50e14c062..45faf40cf4 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -90,7 +90,7 @@ class UV: _, find_out, _ = self._find_python() return False, "", "", 0, [find_version.split()[0]], [find_out.split()[0]] if self.module.check_mode: - latest_version, _ = self._get_latest_patch_release() + latest_version, _ = self._get_latest_patch_release("--managed-python") # when uv does not find any available patch version the install command will fail if not latest_version: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) @@ -98,7 +98,7 @@ class UV: cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] rc, out, err = self.module.run_command(cmd, check_rc=True, expand_user_and_vars=False) - latest_version, path = self._get_latest_patch_release("--only-installed") + latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [path] def uninstall_python(self): @@ -109,7 +109,7 @@ class UV: - boolean to indicate if method changed state - removed version """ - installed_versions, install_paths = self._get_installed_versions() + installed_versions, install_paths = self._get_installed_versions("--managed-python") if not installed_versions: return False, "", "", 0, [], [] if self.module.check_mode: @@ -127,17 +127,19 @@ class UV: - installed version """ rc, out, _ = self._find_python("--show-version") - latest_version, latest_path = self._get_latest_patch_release() + installed_version = out.strip() + latest_version, latest_path = self._get_latest_patch_release("--managed-python") if not latest_version: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") - if rc == 0 and out.strip() == latest_version: - return False, "", "", rc, [latest_version], [latest_path] + if rc == 0 and StrictVersion(installed_version) >= StrictVersion(latest_version): + _, install_path, _ = self._find_python() + return False, "", "", rc, [installed_version], [install_path.strip()] if self.module.check_mode: return True, "", "", 0, [latest_version], [] # it's possible to have latest version already installed but not used as default so in this case 'uv python install' will set latest version as default cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", latest_version] rc, out, err = self.module.run_command(cmd, check_rc=True) - latest_version, latest_path = self._get_latest_patch_release("--only-installed") + latest_version, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [latest_path] def _find_python(self, *args): @@ -184,8 +186,8 @@ class UV: return latest_version, path def _get_installed_versions(self, *args): - _, out_list, _ = self._list_python("--only-installed", *args) try: + _, out_list, _ = self._list_python("--only-installed", *args) if out_list: results = json.loads(out_list) return [result["version"] for result in results], [result["path"] for result in results] From 5b59f830a260b2f9efdf399130b0dd6f1ad2e6d1 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 21 Feb 2026 13:04:01 +0100 Subject: [PATCH 041/131] uv_python module: improve integration tests --- .../targets/uv_python/tasks/main.yaml | 103 +++++++++++++----- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index fec27dd6f9..2352f50434 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -39,46 +39,52 @@ that: - install_check_mode.changed is true - install_check_mode.failed is false - - 'install_check_mode.python_versions != []' + - install_check_mode.python_versions | length >= 1 - name: Install python 3.14 uv_python: version: 3.14 state: present register: install_python - +- debug: + var: install_python - name: Verify python 3.14 installation assert: that: - install_python.changed is true - install_python.failed is false - - 'install_python.python_versions != []' + - install_python.python_versions | length >= 1 + - install_python.python_paths | length >= 1 - name: Re-install python 3.14 uv_python: version: 3.14 state: present register: reinstall_python - +- debug: + var: install_python - name: Verify python 3.14 re-installation assert: that: - reinstall_python.changed is false - reinstall_python.failed is false - - 'reinstall_python.python_versions != []' + - reinstall_python.python_versions | length >= 1 + - reinstall_python.python_paths | length >= 1 - name: Install latest python 3.15 # installs latest patch version for 3.15 uv_python: version: 3.15 state: latest register: install_latest_python - +- debug: + var: install_latest_python - name: Verify python 3.15 installation assert: that: - install_latest_python.changed is true - install_latest_python.failed is false - - 'install_latest_python.stdout != []' + - install_latest_python.python_versions | length >= 1 + - install_latest_python.python_paths | length >= 1 - name: Install python 3.13.5 uv_python: @@ -91,33 +97,37 @@ that: - install_python.changed is true - install_python.failed is false - - 'install_python.python_versions != []' + # - install_python.python_versions | length >= 1 + - '"3.13.5" in install_python.python_versions' + - install_python.python_paths | length >= 1 - name: Re-install python 3.13.5 uv_python: version: 3.13.5 state: present register: reinstall_python - +- debug: + var: reinstall_python - name: Verify python 3.13.5 installation assert: that: - reinstall_python.changed is false - reinstall_python.failed is false - - 'reinstall_python.python_versions != []' + - '"3.13.5" in reinstall_python.python_versions' -- name: Remove unexisting python 3.10 # removes latest patch version for 3.10 if exists +- name: Remove uninstalled python 3.10 # removes latest patch version for 3.10 if exists uv_python: version: 3.10 state: absent register: remove_python - +- debug: + var: remove_python - name: Verify python 3.10 deletion assert: that: - remove_python.changed is false - remove_python.failed is false - - 'remove_python.python_versions == []' + - remove_python.python_versions | length == 0 - name: Remove python 3.13.5 in check mode uv_python: @@ -125,39 +135,45 @@ state: absent check_mode: true register: remove_python_in_check_mode - +- debug: + var: remove_python_in_check_mode - name: Verify python 3.13.5 deletion in check mode assert: that: - remove_python_in_check_mode.changed is true - remove_python_in_check_mode.failed is false - - 'remove_python_in_check_mode.python_versions != []' + - '"3.13.5" in remove_python_in_check_mode.python_versions' + - remove_python_in_check_mode.python_paths | length >= 1 - name: Remove python 3.13.5 uv_python: version: 3.13.5 state: absent register: remove_python - +- debug: + var: remove_python - name: Verify python 3.13.5 deletion assert: that: - remove_python.changed is true - remove_python.failed is false - - 'remove_python_in_check_mode.python_versions != []' + - remove_python.python_paths | length >= 1 + - '"3.13.5" in remove_python.python_versions' - name: Remove python 3.13.5 again uv_python: version: 3.13.5 state: absent register: remove_python - +- debug: + var: remove_python - name: Verify python 3.13.5 deletion again assert: that: - remove_python.changed is false - remove_python.failed is false - - 'remove_python.python_versions == []' + - remove_python.python_versions | length == 0 + - remove_python.python_paths | length == 0 - name: Install python 3 uv_python: @@ -177,13 +193,15 @@ version: 3.13 state: latest register: upgrade_python - +- debug: + var: upgrade_python - name: Verify python 3.13 upgrade assert: that: - upgrade_python.changed is true - upgrade_python.failed is false - - 'upgrade_python.python_versions != []' + - upgrade_python.python_versions | length >= 1 + - upgrade_python.python_paths | length >= 1 - name: Upgrade python 3.13 in check mode uv_python: @@ -191,13 +209,14 @@ state: latest check_mode: yes register: upgrade_python - +- debug: + var: upgrade_python - name: Verify python 3.13 upgrade in check mode assert: that: - upgrade_python.changed is false - upgrade_python.failed is false - - 'upgrade_python.python_versions != []' + - upgrade_python.python_versions | length >= 1 - name: Upgrade python 3.13 again uv_python: @@ -205,13 +224,14 @@ state: latest check_mode: yes register: upgrade_python - +- debug: + var: upgrade_python - name: Verify python 3.13 upgrade again assert: that: - upgrade_python.changed is false - upgrade_python.failed is false - - 'upgrade_python.python_versions != []' + - upgrade_python.python_versions | length >= 1 - name: Install unexisting python version in check mode uv_python: @@ -220,7 +240,8 @@ register: unexisting_python_check_mode ignore_errors: true check_mode: true - +- debug: + var: unexisting_python_check_mode - name: Verify unexisting python 3.12.13 install in check mode assert: that: @@ -234,7 +255,8 @@ state: present register: unexisting_python ignore_errors: true - +- debug: + var: unexisting_python - name: Verify unexisting python 3.12.13 install assert: that: @@ -247,9 +269,32 @@ state: latest register: unexisting_python ignore_errors: true - -- name: Verify python 3.12.13 deletion with latest state +- debug: + var: unexisting_python +- name: Verify python 3.12.13 install with latest state assert: that: - unexisting_python.changed is false - unexisting_python.failed is true + +- name: Install python 3.13.11 version + uv_python: + version: 3.13.11 + state: present + +- name: Delete python 3.13 version + uv_python: + version: 3.13 + state: absent + register: uninstall_result + +- debug: + var: uninstall_result + +- name: Verify python 3.13 deletion + assert: + that: + - uninstall_result.changed is true + - uninstall_result.failed is false + - '"3.13.11" in uninstall_result.python_versions' + - uninstall_result.python_versions | length >= 2 \ No newline at end of file From a50fb91c4ab9c463e9e889217de6e381c0383ea8 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 21 Feb 2026 15:14:53 +0100 Subject: [PATCH 042/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 194 ++++++++++++++++++++++++----------- 1 file changed, 134 insertions(+), 60 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 45faf40cf4..1ae5403066 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -61,9 +61,8 @@ class UV: Module for "uv python" command Official documentation for uv python https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version """ - subcommand = "python" - def __init__(self, module, **kwargs): + def __init__(self, module): self.module = module python_version = module.params["version"] try: @@ -77,18 +76,28 @@ class UV: ) ) + def install_python(self): """ Runs command 'uv python install X.Y.Z' which installs specified python version. If patch version is not specified uv installs latest available patch version. - Returns: tuple with following elements + Returns: + tuple [bool, str, str, int, list, list] - boolean to indicate if method changed state - - installed version + - command's stdout + - command's stderr + - command's return code + - list of installed versions + - list of installation paths for each installed version + Raises: + AnsibleModuleFailJson: + If the install command exits with a non-zero return code. + If specified version is not available for download. """ - find_rc, find_version, _ = self._find_python("--show-version") + find_rc, existing_version, _ = self._find_python(self.python_version_str, "--show-version") if find_rc == 0: - _, find_out, _ = self._find_python() - return False, "", "", 0, [find_version.split()[0]], [find_out.split()[0]] + _, version_path, _ = self._find_python(self.python_version_str) + return False, "", "", 0, [existing_version], [version_path] if self.module.check_mode: latest_version, _ = self._get_latest_patch_release("--managed-python") # when uv does not find any available patch version the install command will fail @@ -96,18 +105,26 @@ class UV: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) return True, "", "", 0, [latest_version], [""] - cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", self.python_version_str] - rc, out, err = self.module.run_command(cmd, check_rc=True, expand_user_and_vars=False) + rc, out, err = self._exec(self.python_version_str, "install", check_rc=True) latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [path] - + + def uninstall_python(self): """ Runs command 'uv python uninstall X.Y.Z' which removes specified python version from environment. If patch version is not specified all correspending installed patch versions are removed. - Returns: tuple with following elements + Returns: + tuple [bool, str, str, int, list, list] - boolean to indicate if method changed state - - removed version + - command's stdout + - command's stderr + - command's return code + - list of uninstalled versions + - list of previous installation paths for each uninstalled version + Raises: + AnsibleModuleFailJson: + If the uninstall command exits with a non-zero return code. """ installed_versions, install_paths = self._get_installed_versions("--managed-python") if not installed_versions: @@ -115,86 +132,142 @@ class UV: if self.module.check_mode: return True, "", "", 0, installed_versions, install_paths - cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "uninstall", self.python_version_str] - rc, out, err = self.module.run_command(cmd, check_rc=True) + rc, out, err = self._exec(self.python_version_str, "uninstall", check_rc=True) return True, out, err, rc, installed_versions, install_paths + def upgrade_python(self): """ - Runs command 'uv python install X.Y.Z' which installs specified python version. - Returns: tuple with following elements + Runs command 'uv python install X.Y.Z' with latest patch version available. + Returns: + tuple [bool, str, str, int, list, list] - boolean to indicate if method changed state - - installed version + - command's stdout + - command's stderr + - command's return code + - list of installed versions + - list of installation paths for each installed version + Raises: + AnsibleModuleFailJson: + If the install command exits with a non-zero return code. + If resolved patch version is not available for download. """ - rc, out, _ = self._find_python("--show-version") - installed_version = out.strip() - latest_version, latest_path = self._get_latest_patch_release("--managed-python") + rc, installed_version, _ = self._find_python(self.python_version_str, "--show-version") + latest_version, _ = self._get_latest_patch_release("--managed-python") if not latest_version: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and StrictVersion(installed_version) >= StrictVersion(latest_version): - _, install_path, _ = self._find_python() - return False, "", "", rc, [installed_version], [install_path.strip()] + _, install_path, _ = self._find_python(self.python_version_str) + return False, "", "", rc, [installed_version], [install_path] if self.module.check_mode: return True, "", "", 0, [latest_version], [] # it's possible to have latest version already installed but not used as default so in this case 'uv python install' will set latest version as default - cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "install", latest_version] - rc, out, err = self.module.run_command(cmd, check_rc=True) + rc, out, err = self._exec(latest_version, "install", check_rc=True) latest_version, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [latest_path] - def _find_python(self, *args): + + def _exec(self, python_version, command, *args, check_rc=False): + """ + Execute a uv python subcommand. + Args: + python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). + command (str): uv python subcommand (e.g. "install", "uninstall", "find"). + *args: Additional positional arguments passed to the command. + check_rc (bool): Whether to fail if the command exits with non-zero return code. + Returns: + tuple[int, str, str]: + A tuple containing (rc, stdout, stderr). + Raises: + AnsibleModuleFailJson: + If check_rc is True and the command exits with a non-zero return code. + """ + cmd = [self.module.get_bin_path("uv", required=True), "python", command, python_version, *args] + rc, out, err = self.module.run_command(cmd, check_rc=check_rc) + return rc, out, err + + + def _find_python(self, python_version, *args, check_rc=False): """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. If multiple patch versions are installed, "uv python find" returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version. - Returns: tuple with following elements - - return code of executed command - - stdout of command - - stderr of command + Args: + python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). + *args: Additional positional arguments passed to _exec. + check_rc (bool): Whether to fail if the command exits with non-zero return code. + Returns: + tuple[int, str, str]: + A tuple containing (rc, stdout, stderr). + Raises: + AnsibleModuleFailJson: + If check_rc is True and the command exits with a non-zero return code. """ - cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "find", self.python_version_str, *args] - rc, out, err = self.module.run_command(cmd, expand_user_and_vars=False) + rc, out, err = self._exec(python_version, "find", *args, check_rc=check_rc) + if rc == 0: + out = out.strip() return rc, out, err - - def _list_python(self, *args): + + + def _list_python(self, python_version, *args, check_rc=False): """ - Runs command 'uv python list' which returns list of installed patch releases for a given python version. - Returns: tuple with following elements - - return code of executed command - - stdout of command - - stderr of command + Runs command 'uv python list' (which returns list of installed patch releases for a given python version). + Official documentation https://docs.astral.sh/uv/reference/cli/#uv-python-list + Args: + python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). + *args: Additional positional arguments passed to _exec. + check_rc (bool): Whether to fail if the command exits with non-zero return code. + Returns: + tuple[int, str, str]: + A tuple containing (rc, stdout, stderr). + Raises: + AnsibleModuleFailJson: + If check_rc is True and the command exits with a non-zero return code. """ - # https://docs.astral.sh/uv/reference/cli/#uv-python-list - cmd = [self.module.get_bin_path("uv", required=True), self.subcommand, "list", self.python_version_str, "--output-format", "json", *args] - rc, out, err = self.module.run_command(cmd) + rc, out, err = self._exec(python_version, "list", "--output-format", "json", *args, check_rc=check_rc) + try: + out = json.loads(out) + except json.decoder.JSONDecodeError: + # This happens when no version is found + pass return rc, out, err + def _get_latest_patch_release(self, *args): """ Returns latest available patch release for a given python version. - Fails when no available release exists for the specified version. + Args: + *args: Additional positional arguments passed to _list_python. + Returns: + tuple[str, str]: + - latest found patch version in format X.Y.Z + - installation path of latest patch version if version exists """ latest_version = path = "" - try: - _, out, _ = self._list_python(*args) # uv returns versions in descending order but we sort them just in case future uv behavior changes - results = json.loads(out) - if results: - version = max(results, key=lambda item: (item["version_parts"]["major"], item["version_parts"]["minor"], item["version_parts"]["patch"])) - latest_version = version["version"] - path = version["path"] - except json.decoder.JSONDecodeError as e: - self.module.fail_json(msg=f"Failed to parse 'uv python list' output with error {str(e)}") + _, results, _ = self._list_python(self.python_version_str, *args) # uv returns versions in descending order but we sort them just in case future uv behavior changes + if results: + version = max(results, key=lambda item: (item["version_parts"]["major"], item["version_parts"]["minor"], item["version_parts"]["patch"])) + latest_version = version["version"] + path = version["path"] if version["path"] else "" return latest_version, path - + + def _get_installed_versions(self, *args): - try: - _, out_list, _ = self._list_python("--only-installed", *args) - if out_list: - results = json.loads(out_list) - return [result["version"] for result in results], [result["path"] for result in results] - except json.decoder.JSONDecodeError: - self.module.fail_json(msg=f"Failed to parse 'uv python list' output") + """ + Returns installed patch releases for a given python version. + Args: + *args: Additional positional arguments passed to _list_python. + Returns: + tuple[list, list]: + - list of latest found patch versions + - list of installation paths of installed versions + """ + _, results, _ = self._list_python(self.python_version_str, "--only-installed", *args) + if results: + return [result["version"] for result in results], [result["path"] for result in results] return [], [] + + def main(): module = AnsibleModule( argument_spec=dict( @@ -219,11 +292,12 @@ def main(): if state == "present": result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.install_python() elif state == "absent": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.uninstall_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.uninstall_python() elif state == "latest": result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.upgrade_python() module.exit_json(**result) + if __name__ == "__main__": main() \ No newline at end of file From 4fe6c83d0fd5fcb81159476f6e1b5de5f93454f3 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 21 Feb 2026 17:54:28 +0100 Subject: [PATCH 043/131] uv_python module: update module documentation --- plugins/modules/uv_python.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 1ae5403066..35ee8bc691 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -6,31 +6,28 @@ DOCUMENTATION = r''' --- module: uv_python -short_description: Manage Python installations using uv +short_description: Manage Python installations using uv. description: - - Install or remove Python versions managed by uv. -# requirements: -# - uv must be installed and available on PATH + - Install, remove or upgrade Python versions managed by uv. +version_added: "X.Y" +requirements: + - uv must be installed and available on PATH + - uv >= 0.8.0 options: version: - description: Python version to manage + description: + - Python version to manage. + - Expected formats are "X.Y" or "X.Y.Z". type: str required: true state: - description: Desired state + description: Desired state of the specified Python version. type: str - choices: [present, absent] + choices: [present, absent, latest] default: present - force: - description: Force reinstall - type: bool - default: false - uv_path: - description: Path to uv binary - type: str - default: uv author: - - Your Name + - Mariam Ahhttouche (@mriamah) +deprecated: ''' EXAMPLES = r''' From 67d363a2ce839e823460ee432a219384974f072a Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 10:29:42 +0100 Subject: [PATCH 044/131] uv_python module: use LooseVersion instead of StrictVersion to allow specifying threaded and major python versions --- plugins/modules/uv_python.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 35ee8bc691..7792d364af 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -17,7 +17,7 @@ options: version: description: - Python version to manage. - - Expected formats are "X.Y" or "X.Y.Z". + - Expected formats are "X", "X.Y" or "X.Y.Z". type: str required: true state: @@ -50,9 +50,11 @@ python: import json from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.compat.version import StrictVersion +from ansible.module_utils.compat.version import LooseVersion +MINIMUM_UV_VERSION = "0.8.0" + class UV: """ Module for "uv python" command @@ -63,14 +65,11 @@ class UV: self.module = module python_version = module.params["version"] try: - self.python_version = StrictVersion(python_version) + self.python_version = LooseVersion(python_version) self.python_version_str = self.python_version.__str__() - except ValueError: + except ValueError as err: self.module.fail_json( - msg=( - f"Invalid version {python_version}. " - "Expected formats are X.Y or X.Y.Z" - ) + msg=err ) @@ -153,12 +152,13 @@ class UV: latest_version, _ = self._get_latest_patch_release("--managed-python") if not latest_version: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") - if rc == 0 and StrictVersion(installed_version) >= StrictVersion(latest_version): + if rc == 0 and LooseVersion(installed_version) >= LooseVersion(latest_version): _, install_path, _ = self._find_python(self.python_version_str) return False, "", "", rc, [installed_version], [install_path] if self.module.check_mode: return True, "", "", 0, [latest_version], [] - # it's possible to have latest version already installed but not used as default so in this case 'uv python install' will set latest version as default + # it's possible to have latest version already installed but not used as default + # so in this case 'uv python install' will set latest version as default rc, out, err = self._exec(latest_version, "install", check_rc=True) latest_version, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [latest_path] @@ -187,7 +187,8 @@ class UV: def _find_python(self, python_version, *args, check_rc=False): """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. - If multiple patch versions are installed, "uv python find" returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version. + If multiple patch versions are installed, "uv python find" returns the one used by default + if inside a virtualenv otherwise it returns latest installed patch version. Args: python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). *args: Additional positional arguments passed to _exec. From 5f0118e7925ea0e6f11b83b3109c07ff03db103f Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 10:32:21 +0100 Subject: [PATCH 045/131] uv_python module: fail module if used uv version is less than the minimal supported version --- plugins/modules/uv_python.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 7792d364af..4c6a12c2c3 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -63,6 +63,7 @@ class UV: def __init__(self, module): self.module = module + self._ensure_min_uv_version() python_version = module.params["version"] try: self.python_version = LooseVersion(python_version) @@ -73,6 +74,18 @@ class UV: ) + def _ensure_min_uv_version(self): + cmd = [self.module.get_bin_path("uv", required=True), "--version"] + _, out, _ = self.module.run_command(cmd, check_rc=True) + detected = out.strip().split()[-1] + if LooseVersion(detected) < LooseVersion(MINIMUM_UV_VERSION): + self.module.fail_json( + msg=f"uv_python module requires uv >= {MINIMUM_UV_VERSION}", + detected_version=detected, + required_version=MINIMUM_UV_VERSION, + ) + + def install_python(self): """ Runs command 'uv python install X.Y.Z' which installs specified python version. From 990cb6fa88ea320e63d2579bfb14ba9515feb63b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 11:24:23 +0100 Subject: [PATCH 046/131] uv_python module: update documentation --- plugins/modules/uv_python.py | 64 ++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 4c6a12c2c3..cb07609671 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -6,13 +6,15 @@ DOCUMENTATION = r''' --- module: uv_python -short_description: Manage Python installations using uv. +short_description: Manage Python versions and installations using uv. description: - - Install, remove or upgrade Python versions managed by uv. + - Install, remove or upgrade Python versions managed by uv. version_added: "X.Y" requirements: - uv must be installed and available on PATH - - uv >= 0.8.0 + - uv version should be at least 0.8.0 +deprecated: +author: Mariam Ahhttouche (@mriamah) options: version: description: @@ -25,27 +27,56 @@ options: type: str choices: [present, absent, latest] default: present -author: - - Mariam Ahhttouche (@mriamah) -deprecated: +seealso: + - https://docs.astral.sh/uv/concepts/python-versions/ + - https://docs.astral.sh/uv/reference/cli/#uv-python +attributes: + - check_mode: + description: Can run in check_mode and return changed status prediction without modifying target. + support: full + - diff_mode: + description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. + support: none + ''' EXAMPLES = r''' -- name: Install Python 3.12 - uv_python: - version: "3.12" +- name: Install Python 3.14 + mriamah.uv_python: + version: "3.14" -- name: Remove Python 3.11 - uv_python: - version: "3.11" +- name: Remove Python 3.13.5 + mriamah.uv_python: + version: "3.13.5" state: absent + +- name: Upgrade python 3 + mriamah.uv_python: + version: 3 + state: latest ''' RETURN = r''' -python: - description: Installed Python info - returned: when state=present - type: dict +python_versions: + description: List of Python versions changed. + returned: success + type: list +python_paths: + description: List of installation paths of Python versions changed. + returned: success + type: list +stdout: + description: Stdout of command run. + returned: success + type: str +stderr: + description: Stderr of command run. + returned: success + type: str +rc: + description: Return code of command run. + returned: success + type: int ''' import json @@ -58,7 +89,6 @@ MINIMUM_UV_VERSION = "0.8.0" class UV: """ Module for "uv python" command - Official documentation for uv python https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version """ def __init__(self, module): From 067784548d5ddcf1543183ff19da78c3b6e5610a Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 12:51:16 +0100 Subject: [PATCH 047/131] uv_python module: add uv command options to executed commands to disable unneeded features --- plugins/modules/uv_python.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index cb07609671..6e66076276 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -8,7 +8,7 @@ DOCUMENTATION = r''' module: uv_python short_description: Manage Python versions and installations using uv. description: - - Install, remove or upgrade Python versions managed by uv. + - Install, uninstall or upgrade Python versions managed by uv. version_added: "X.Y" requirements: - uv must be installed and available on PATH @@ -105,7 +105,7 @@ class UV: def _ensure_min_uv_version(self): - cmd = [self.module.get_bin_path("uv", required=True), "--version"] + cmd = [self.module.get_bin_path("uv", required=True), "--version", "--color", "never"] _, out, _ = self.module.run_command(cmd, check_rc=True) detected = out.strip().split()[-1] if LooseVersion(detected) < LooseVersion(MINIMUM_UV_VERSION): @@ -222,7 +222,7 @@ class UV: AnsibleModuleFailJson: If check_rc is True and the command exits with a non-zero return code. """ - cmd = [self.module.get_bin_path("uv", required=True), "python", command, python_version, *args] + cmd = [self.module.get_bin_path("uv", required=True), "python", command, python_version, "--color", "never", *args] rc, out, err = self.module.run_command(cmd, check_rc=check_rc) return rc, out, err From b8da4c6d6c39c63cf5160fcce81a757d22f42ec6 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 13:20:22 +0100 Subject: [PATCH 048/131] uv_python module: update documentation with version number --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 6e66076276..f533a0924b 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -9,7 +9,7 @@ module: uv_python short_description: Manage Python versions and installations using uv. description: - Install, uninstall or upgrade Python versions managed by uv. -version_added: "X.Y" +version_added: "1.0.0" requirements: - uv must be installed and available on PATH - uv version should be at least 0.8.0 From 6c1869847866bc77c699acdc1494dbf9266f52d3 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 16:00:15 +0100 Subject: [PATCH 049/131] uv_python module: use packaging.version to only accept canonical python versions --- plugins/modules/uv_python.py | 75 +++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index f533a0924b..08b41f35db 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -80,15 +80,26 @@ rc: ''' import json +import traceback from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.compat.version import LooseVersion +from ansible.module_utils.basic import missing_required_lib + +LIB_IMP_ERR = None +HAS_LIB = False +try: + from packaging.version import Version, InvalidVersion + HAS_LIB = True +except: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() MINIMUM_UV_VERSION = "0.8.0" class UV: """ - Module for "uv python" command + Module for managing Python versions and installations using "uv python" command """ def __init__(self, module): @@ -96,14 +107,13 @@ class UV: self._ensure_min_uv_version() python_version = module.params["version"] try: - self.python_version = LooseVersion(python_version) + self.python_version = Version(python_version) self.python_version_str = self.python_version.__str__() - except ValueError as err: + except InvalidVersion: self.module.fail_json( - msg=err + msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3) are supported in this release." ) - def _ensure_min_uv_version(self): cmd = [self.module.get_bin_path("uv", required=True), "--version", "--color", "never"] _, out, _ = self.module.run_command(cmd, check_rc=True) @@ -115,7 +125,6 @@ class UV: required_version=MINIMUM_UV_VERSION, ) - def install_python(self): """ Runs command 'uv python install X.Y.Z' which installs specified python version. @@ -143,12 +152,10 @@ class UV: if not latest_version: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) return True, "", "", 0, [latest_version], [""] - rc, out, err = self._exec(self.python_version_str, "install", check_rc=True) latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [path] - def uninstall_python(self): """ Runs command 'uv python uninstall X.Y.Z' which removes specified python version from environment. @@ -170,10 +177,8 @@ class UV: return False, "", "", 0, [], [] if self.module.check_mode: return True, "", "", 0, installed_versions, install_paths - rc, out, err = self._exec(self.python_version_str, "uninstall", check_rc=True) return True, out, err, rc, installed_versions, install_paths - def upgrade_python(self): """ @@ -191,21 +196,21 @@ class UV: If the install command exits with a non-zero return code. If resolved patch version is not available for download. """ - rc, installed_version, _ = self._find_python(self.python_version_str, "--show-version") - latest_version, _ = self._get_latest_patch_release("--managed-python") - if not latest_version: + rc, installed_version_str, _ = self._find_python(self.python_version_str, "--show-version") + installed_version = self._parse_version(installed_version_str) + latest_version_str, _ = self._get_latest_patch_release("--managed-python") + if not latest_version_str: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") - if rc == 0 and LooseVersion(installed_version) >= LooseVersion(latest_version): + if rc == 0 and installed_version >= Version(latest_version_str): _, install_path, _ = self._find_python(self.python_version_str) - return False, "", "", rc, [installed_version], [install_path] + return False, "", "", rc, [installed_version.__str__()], [install_path] if self.module.check_mode: - return True, "", "", 0, [latest_version], [] + return True, "", "", 0, [latest_version_str], [] # it's possible to have latest version already installed but not used as default # so in this case 'uv python install' will set latest version as default - rc, out, err = self._exec(latest_version, "install", check_rc=True) - latest_version, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") - return True, out, err, rc, [latest_version], [latest_path] - + rc, out, err = self._exec(latest_version_str, "install", check_rc=True) + latest_version_str, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") + return True, out, err, rc, [latest_version_str], [latest_path] def _exec(self, python_version, command, *args, check_rc=False): """ @@ -226,7 +231,6 @@ class UV: rc, out, err = self.module.run_command(cmd, check_rc=check_rc) return rc, out, err - def _find_python(self, python_version, *args, check_rc=False): """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. @@ -248,7 +252,6 @@ class UV: out = out.strip() return rc, out, err - def _list_python(self, python_version, *args, check_rc=False): """ Runs command 'uv python list' (which returns list of installed patch releases for a given python version). @@ -272,7 +275,6 @@ class UV: pass return rc, out, err - def _get_latest_patch_release(self, *args): """ Returns latest available patch release for a given python version. @@ -285,13 +287,13 @@ class UV: """ latest_version = path = "" _, results, _ = self._list_python(self.python_version_str, *args) # uv returns versions in descending order but we sort them just in case future uv behavior changes - if results: - version = max(results, key=lambda item: (item["version_parts"]["major"], item["version_parts"]["minor"], item["version_parts"]["patch"])) + valid_results = self._parse_versions(results) + if valid_results: + version = max(valid_results, key=lambda result: result["parsed_version"]) latest_version = version["version"] path = version["path"] if version["path"] else "" return latest_version, path - def _get_installed_versions(self, *args): """ Returns installed patch releases for a given python version. @@ -307,6 +309,23 @@ class UV: return [result["version"] for result in results], [result["path"] for result in results] return [], [] + @staticmethod + def _parse_versions(results): + valid_results =[] + for result in results: + try: + result["parsed_version"] = Version(result["version"]) + valid_results.append(result) + except InvalidVersion: + continue + return valid_results + + @staticmethod + def _parse_version(version_str): + try: + return Version(version_str) + except InvalidVersion: + return Version("0") def main(): @@ -318,6 +337,10 @@ def main(): supports_check_mode=True ) + if not HAS_LIB: + module.fail_json(msg=missing_required_lib("packaging"), + exception=LIB_IMP_ERR) + result = dict( changed=False, stdout="", From 680fcaf578046e2e7913c83c805fcf0d45181f7b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 17:33:30 +0100 Subject: [PATCH 050/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 08b41f35db..55af59c86d 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -142,9 +142,9 @@ class UV: If the install command exits with a non-zero return code. If specified version is not available for download. """ - find_rc, existing_version, _ = self._find_python(self.python_version_str, "--show-version") + find_rc, existing_version, _ = self._find_python("--show-version") if find_rc == 0: - _, version_path, _ = self._find_python(self.python_version_str) + _, version_path, _ = self._find_python() return False, "", "", 0, [existing_version], [version_path] if self.module.check_mode: latest_version, _ = self._get_latest_patch_release("--managed-python") @@ -196,13 +196,13 @@ class UV: If the install command exits with a non-zero return code. If resolved patch version is not available for download. """ - rc, installed_version_str, _ = self._find_python(self.python_version_str, "--show-version") + rc, installed_version_str, _ = self._find_python("--show-version") installed_version = self._parse_version(installed_version_str) latest_version_str, _ = self._get_latest_patch_release("--managed-python") if not latest_version_str: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and installed_version >= Version(latest_version_str): - _, install_path, _ = self._find_python(self.python_version_str) + _, install_path, _ = self._find_python() return False, "", "", rc, [installed_version.__str__()], [install_path] if self.module.check_mode: return True, "", "", 0, [latest_version_str], [] @@ -231,13 +231,12 @@ class UV: rc, out, err = self.module.run_command(cmd, check_rc=check_rc) return rc, out, err - def _find_python(self, python_version, *args, check_rc=False): + def _find_python(self, *args, check_rc=False): """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. If multiple patch versions are installed, "uv python find" returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version. Args: - python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). *args: Additional positional arguments passed to _exec. check_rc (bool): Whether to fail if the command exits with non-zero return code. Returns: @@ -247,17 +246,16 @@ class UV: AnsibleModuleFailJson: If check_rc is True and the command exits with a non-zero return code. """ - rc, out, err = self._exec(python_version, "find", *args, check_rc=check_rc) + rc, out, err = self._exec(self.python_version_str, "find", *args, check_rc=check_rc) if rc == 0: out = out.strip() return rc, out, err - def _list_python(self, python_version, *args, check_rc=False): + def _list_python(self, *args, check_rc=False): """ Runs command 'uv python list' (which returns list of installed patch releases for a given python version). Official documentation https://docs.astral.sh/uv/reference/cli/#uv-python-list Args: - python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). *args: Additional positional arguments passed to _exec. check_rc (bool): Whether to fail if the command exits with non-zero return code. Returns: @@ -267,7 +265,7 @@ class UV: AnsibleModuleFailJson: If check_rc is True and the command exits with a non-zero return code. """ - rc, out, err = self._exec(python_version, "list", "--output-format", "json", *args, check_rc=check_rc) + rc, out, err = self._exec(self.python_version_str, "list", "--output-format", "json", *args, check_rc=check_rc) try: out = json.loads(out) except json.decoder.JSONDecodeError: @@ -286,7 +284,7 @@ class UV: - installation path of latest patch version if version exists """ latest_version = path = "" - _, results, _ = self._list_python(self.python_version_str, *args) # uv returns versions in descending order but we sort them just in case future uv behavior changes + _, results, _ = self._list_python(*args) # uv returns versions in descending order but we sort them just in case future uv behavior changes valid_results = self._parse_versions(results) if valid_results: version = max(valid_results, key=lambda result: result["parsed_version"]) @@ -304,7 +302,7 @@ class UV: - list of latest found patch versions - list of installation paths of installed versions """ - _, results, _ = self._list_python(self.python_version_str, "--only-installed", *args) + _, results, _ = self._list_python("--only-installed", *args) if results: return [result["version"] for result in results], [result["path"] for result in results] return [], [] From 5e85b247434d513869328f91d1c2f7df4480b927 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 18:04:53 +0100 Subject: [PATCH 051/131] uv_python module: update documentation --- plugins/modules/uv_python.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 55af59c86d..98ea4fa0a8 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -6,24 +6,30 @@ DOCUMENTATION = r''' --- module: uv_python -short_description: Manage Python versions and installations using uv. +short_description: Manage Python versions and installations using C(uv) Python package manager. description: - - Install, uninstall or upgrade Python versions managed by uv. + - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "1.0.0" requirements: - - uv must be installed and available on PATH - - uv version should be at least 0.8.0 + - C(uv) must be installed and available on PATH. + - C(uv) version should be at least C(0.8.0). deprecated: author: Mariam Ahhttouche (@mriamah) options: version: description: - Python version to manage. - - Expected formats are "X", "X.Y" or "X.Y.Z". + - Only canonical Python versions are supported in this release such as C(3), C(3.12), or C(3.12.3). + - Advanced uv selectors such as C(>=3.12,<3.13) or C(cpython@3.12) are not supported in this release. + - When specifying only a major or major.minor version, behavior depends on state parameter. type: str required: true state: - description: Desired state of the specified Python version. + description: + - Desired state of the specified Python version. + - C(present) ensures the specified version is installed. + - C(absent) ensures the specified version is removed. + - C(latest) ensures the latest available patch version for the specified version is installed. type: str choices: [present, absent, latest] default: present @@ -37,7 +43,24 @@ attributes: - diff_mode: description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. support: none +notes: + - | + State behavior: + * O(state=present) + - If a full patch version is specified (for example 3.12.3), that exact version will be installed if not already present. + - If only a minor version is specified (for example 3.12), the latest available patch version for that minor release will be installed ONLY if no patch version for that minor release is currently installed including patch versions not managed by C(uv). + + * O(state=absent) + - If a full patch version is specified, only that exact patch version is removed. + - If only a minor version is specified (for example 3.12), all installed patch versions for that minor release are removed. + - If the specified version is not installed, no changes are made. + + * O(state=latest) + - If only a minor version is specified (for example 3.12), the latest available patch version for that minor release will always be installed. + - If another patch version is already installed but is not the latest, the latest patch version will be installed. + - This state does not use C(uv python upgrade). + - The latest patch installed depends on the uv version, since available Python versions are frozen per uv release. ''' EXAMPLES = r''' From 8181aa0343718eacb2690ef2f29778d850b9aad4 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sun, 22 Feb 2026 21:49:18 +0100 Subject: [PATCH 052/131] uv_python module: update integration tests --- .../targets/uv_python/tasks/main.yaml | 98 +++++++++++-------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 2352f50434..e4a93a2577 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -27,6 +27,51 @@ environment: UV_INSTALL_DIR: /usr/local/bin +- name: Install python 3 + uv_python: + version: 3 + state: present + register: python_install + +- name: Assert python 3 is installed + ansible.builtin.assert: + that: + - python_install.failed is false + - python_install.changed is false + - python_install.python_versions | length >= 1 + +- name: Upgrade python 3 + uv_python: + version: 3 + state: latest + register: python_install + +- debug: + var: python_install + +- name: Assert python 3 upgraded + ansible.builtin.assert: + that: + - python_install.failed is false + - python_install.changed is true + - python_install.python_versions | length >= 1 + +- name: Remove python 3 + uv_python: + version: 3 + state: absent + register: python_delete + +- debug: + var: python_delete + +- name: Assert python 3 deleted + ansible.builtin.assert: + that: + - python_delete.failed is false + - python_delete.changed is true + - python_delete.python_versions | length >= 1 + - name: Install python 3.14 in check mode uv_python: version: 3.14 @@ -62,7 +107,7 @@ state: present register: reinstall_python - debug: - var: install_python + var: reinstall_python - name: Verify python 3.14 re-installation assert: that: @@ -71,21 +116,6 @@ - reinstall_python.python_versions | length >= 1 - reinstall_python.python_paths | length >= 1 -- name: Install latest python 3.15 # installs latest patch version for 3.15 - uv_python: - version: 3.15 - state: latest - register: install_latest_python -- debug: - var: install_latest_python -- name: Verify python 3.15 installation - assert: - that: - - install_latest_python.changed is true - - install_latest_python.failed is false - - install_latest_python.python_versions | length >= 1 - - install_latest_python.python_paths | length >= 1 - - name: Install python 3.13.5 uv_python: version: 3.13.5 @@ -97,7 +127,6 @@ that: - install_python.changed is true - install_python.failed is false - # - install_python.python_versions | length >= 1 - '"3.13.5" in install_python.python_versions' - install_python.python_paths | length >= 1 @@ -117,7 +146,7 @@ - name: Remove uninstalled python 3.10 # removes latest patch version for 3.10 if exists uv_python: - version: 3.10 + version: "3.10" state: absent register: remove_python - debug: @@ -175,19 +204,6 @@ - remove_python.python_versions | length == 0 - remove_python.python_paths | length == 0 -- name: Install python 3 - uv_python: - version: 3 - state: present - register: invalid_version - ignore_errors: true - -- name: Assert invalid version failed - ansible.builtin.assert: - that: - - invalid_version is failed - - '"Expected formats are X.Y or X.Y.Z" in invalid_version.msg' - - name: Upgrade python 3.13 uv_python: version: 3.13 @@ -252,7 +268,6 @@ - name: Install unexisting python version uv_python: version: 3.12.13 - state: present register: unexisting_python ignore_errors: true - debug: @@ -277,11 +292,6 @@ - unexisting_python.changed is false - unexisting_python.failed is true -- name: Install python 3.13.11 version - uv_python: - version: 3.13.11 - state: present - - name: Delete python 3.13 version uv_python: version: 3.13 @@ -296,5 +306,15 @@ that: - uninstall_result.changed is true - uninstall_result.failed is false - - '"3.13.11" in uninstall_result.python_versions' - - uninstall_result.python_versions | length >= 2 \ No newline at end of file + - '"3.13.12" in uninstall_result.python_versions' + +- name: No specified version + uv_python: + state: latest + ignore_errors: true + register: no_version + +- name: Verify failure when no version is specified + assert: + that: + - no_version.failed is true \ No newline at end of file From 2f14908ba2cabbd57a335a1d51a4a2c9da43a86d Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 23 Feb 2026 06:54:08 +0100 Subject: [PATCH 053/131] uv_python module: update documentation --- plugins/modules/uv_python.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 98ea4fa0a8..9b4a05f1c8 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -11,17 +11,16 @@ description: - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "1.0.0" requirements: - - C(uv) must be installed and available on PATH. - - C(uv) version should be at least C(0.8.0). + - C(uv) must be installed and available in C(PATH). + - C(uv) version must be at least C(0.8.0). deprecated: -author: Mariam Ahhttouche (@mriamah) options: version: description: - Python version to manage. - - Only canonical Python versions are supported in this release such as C(3), C(3.12), or C(3.12.3). + - Only L(canonical Python versions, https://peps.python.org/pep-0440/) are supported in this release such as C(3), C(3.12), C(3.12.3), C(3.15.0a5). - Advanced uv selectors such as C(>=3.12,<3.13) or C(cpython@3.12) are not supported in this release. - - When specifying only a major or major.minor version, behavior depends on state parameter. + - When specifying only a major or major.minor version, behavior depends on the O(state) parameter. type: str required: true state: @@ -49,18 +48,24 @@ notes: * O(state=present) - If a full patch version is specified (for example 3.12.3), that exact version will be installed if not already present. - - If only a minor version is specified (for example 3.12), the latest available patch version for that minor release will be installed ONLY if no patch version for that minor release is currently installed including patch versions not managed by C(uv). + - If only a minor version is specified (for example 3.12), the latest available patch version for that minor release is installed + only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). + - RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. * O(state=absent) - If a full patch version is specified, only that exact patch version is removed. - - If only a minor version is specified (for example 3.12), all installed patch versions for that minor release are removed. + - If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. - If the specified version is not installed, no changes are made. + - RV(python_versions) and RV(python_paths) lengths can be higher or equal to one for this state. * O(state=latest) - - If only a minor version is specified (for example 3.12), the latest available patch version for that minor release will always be installed. - - If another patch version is already installed but is not the latest, the latest patch version will be installed. + - If only a minor version is specified (for example C(3.12)), the latest available patch version for that minor release is always installed. + - If another patch version is already installed but is not the latest, the latest patch version is installed. - This state does not use C(uv python upgrade). - - The latest patch installed depends on the uv version, since available Python versions are frozen per uv release. + - The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. + - RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. + +author: Mariam Ahhttouche (@mriamah) ''' EXAMPLES = r''' @@ -70,7 +75,7 @@ EXAMPLES = r''' - name: Remove Python 3.13.5 mriamah.uv_python: - version: "3.13.5" + version: 3.13.5 state: absent - name: Upgrade python 3 @@ -89,15 +94,15 @@ python_paths: returned: success type: list stdout: - description: Stdout of command run. + description: Stdout of the executed command. returned: success type: str stderr: - description: Stderr of command run. + description: Stderr of the executed command. returned: success type: str rc: - description: Return code of command run. + description: Return code of the executed command. returned: success type: int ''' From a35e1216f9ff52e78ac53609c6a18a3d66a185b5 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 23 Feb 2026 08:05:14 +0100 Subject: [PATCH 054/131] uv_python module: clean tests --- .../targets/uv_python/tasks/main.yaml | 48 +++++-------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index e4a93a2577..c0c2b173a8 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -46,9 +46,6 @@ state: latest register: python_install -- debug: - var: python_install - - name: Assert python 3 upgraded ansible.builtin.assert: that: @@ -62,9 +59,6 @@ state: absent register: python_delete -- debug: - var: python_delete - - name: Assert python 3 deleted ansible.builtin.assert: that: @@ -91,8 +85,7 @@ version: 3.14 state: present register: install_python -- debug: - var: install_python + - name: Verify python 3.14 installation assert: that: @@ -106,8 +99,7 @@ version: 3.14 state: present register: reinstall_python -- debug: - var: reinstall_python + - name: Verify python 3.14 re-installation assert: that: @@ -135,8 +127,7 @@ version: 3.13.5 state: present register: reinstall_python -- debug: - var: reinstall_python + - name: Verify python 3.13.5 installation assert: that: @@ -149,8 +140,7 @@ version: "3.10" state: absent register: remove_python -- debug: - var: remove_python + - name: Verify python 3.10 deletion assert: that: @@ -164,8 +154,7 @@ state: absent check_mode: true register: remove_python_in_check_mode -- debug: - var: remove_python_in_check_mode + - name: Verify python 3.13.5 deletion in check mode assert: that: @@ -179,8 +168,7 @@ version: 3.13.5 state: absent register: remove_python -- debug: - var: remove_python + - name: Verify python 3.13.5 deletion assert: that: @@ -194,8 +182,7 @@ version: 3.13.5 state: absent register: remove_python -- debug: - var: remove_python + - name: Verify python 3.13.5 deletion again assert: that: @@ -209,8 +196,7 @@ version: 3.13 state: latest register: upgrade_python -- debug: - var: upgrade_python + - name: Verify python 3.13 upgrade assert: that: @@ -225,8 +211,7 @@ state: latest check_mode: yes register: upgrade_python -- debug: - var: upgrade_python + - name: Verify python 3.13 upgrade in check mode assert: that: @@ -240,8 +225,7 @@ state: latest check_mode: yes register: upgrade_python -- debug: - var: upgrade_python + - name: Verify python 3.13 upgrade again assert: that: @@ -256,8 +240,7 @@ register: unexisting_python_check_mode ignore_errors: true check_mode: true -- debug: - var: unexisting_python_check_mode + - name: Verify unexisting python 3.12.13 install in check mode assert: that: @@ -270,8 +253,7 @@ version: 3.12.13 register: unexisting_python ignore_errors: true -- debug: - var: unexisting_python + - name: Verify unexisting python 3.12.13 install assert: that: @@ -284,8 +266,7 @@ state: latest register: unexisting_python ignore_errors: true -- debug: - var: unexisting_python + - name: Verify python 3.12.13 install with latest state assert: that: @@ -298,9 +279,6 @@ state: absent register: uninstall_result -- debug: - var: uninstall_result - - name: Verify python 3.13 deletion assert: that: From 77b04b488593926520a0197acc5f6d2b27573580 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 23 Feb 2026 14:06:59 +0100 Subject: [PATCH 055/131] uv_python module: improve tests --- tests/integration/targets/uv_python/tasks/main.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index c0c2b173a8..d1e301e3e5 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -37,7 +37,6 @@ ansible.builtin.assert: that: - python_install.failed is false - - python_install.changed is false - python_install.python_versions | length >= 1 - name: Upgrade python 3 @@ -50,7 +49,6 @@ ansible.builtin.assert: that: - python_install.failed is false - - python_install.changed is true - python_install.python_versions | length >= 1 - name: Remove python 3 @@ -63,8 +61,6 @@ ansible.builtin.assert: that: - python_delete.failed is false - - python_delete.changed is true - - python_delete.python_versions | length >= 1 - name: Install python 3.14 in check mode uv_python: @@ -111,7 +107,6 @@ - name: Install python 3.13.5 uv_python: version: 3.13.5 - state: present register: install_python - name: Verify python 3.13.5 installation @@ -250,6 +245,7 @@ - name: Install unexisting python version uv_python: + state: present version: 3.12.13 register: unexisting_python ignore_errors: true From 69e68090fdc2377ad45677f55a8a0771c17bcf32 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 23 Feb 2026 14:32:10 +0100 Subject: [PATCH 056/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 9b4a05f1c8..e2ff996cbb 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -64,7 +64,7 @@ notes: - This state does not use C(uv python upgrade). - The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. - RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. - + author: Mariam Ahhttouche (@mriamah) ''' @@ -316,8 +316,8 @@ class UV: valid_results = self._parse_versions(results) if valid_results: version = max(valid_results, key=lambda result: result["parsed_version"]) - latest_version = version["version"] - path = version["path"] if version["path"] else "" + latest_version = version.get("version", "") + path = version.get("path", "") return latest_version, path def _get_installed_versions(self, *args): @@ -340,7 +340,7 @@ class UV: valid_results =[] for result in results: try: - result["parsed_version"] = Version(result["version"]) + result["parsed_version"] = Version(result.get("version", "")) valid_results.append(result) except InvalidVersion: continue From 9aad044f2560f143034f31a85e60190408b64f64 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Mon, 23 Feb 2026 14:50:54 +0100 Subject: [PATCH 057/131] uv_python module: update module version --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e2ff996cbb..badcfd6702 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -9,7 +9,7 @@ module: uv_python short_description: Manage Python versions and installations using C(uv) Python package manager. description: - Install, uninstall or upgrade Python versions managed by C(uv). -version_added: "1.0.0" +version_added: "0.1.0" requirements: - C(uv) must be installed and available in C(PATH). - C(uv) version must be at least C(0.8.0). From 43f0eab99d3387daf4cd345aebc988a3887336ee Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 09:13:40 +0100 Subject: [PATCH 058/131] up_python module: updatedocumentation --- plugins/modules/uv_python.py | 53 +++++++++++------------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index badcfd6702..fefdf27473 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -5,36 +5,33 @@ DOCUMENTATION = r''' --- -module: uv_python -short_description: Manage Python versions and installations using C(uv) Python package manager. +module: uv.python +short_description: Manage Python versions and installations using uv Python package manager. description: - - Install, uninstall or upgrade Python versions managed by C(uv). -version_added: "0.1.0" + - Install, uninstall or upgrade Python versions managed by C(uv). +version_added: "0.1.5" requirements: - - C(uv) must be installed and available in C(PATH). - - C(uv) version must be at least C(0.8.0). + - uv must be installed and available in PATH. + - uv version must be at least 0.8.0. deprecated: options: version: - description: - - Python version to manage. + description: + - Python version to manage. - Only L(canonical Python versions, https://peps.python.org/pep-0440/) are supported in this release such as C(3), C(3.12), C(3.12.3), C(3.15.0a5). - Advanced uv selectors such as C(>=3.12,<3.13) or C(cpython@3.12) are not supported in this release. - When specifying only a major or major.minor version, behavior depends on the O(state) parameter. type: str required: true state: - description: + description: - Desired state of the specified Python version. - - C(present) ensures the specified version is installed. - - C(absent) ensures the specified version is removed. - - C(latest) ensures the latest available patch version for the specified version is installed. + - C(present) ensures the specified version is installed. If a full patch version is specified (for example C(3.12.3)), that exact version will be installed if not already present. If only a minor version is specified (for example 3.12), the latest available patch version for that minor release is installed only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. Uses C(uv python install) command. + - C(absent) ensures the specified version is removed. If a full patch version is specified, only that exact patch version is removed. If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. If the specified version is not installed, no changes are made. RV(python_versions) and RV(python_paths) lengths can be higher or equal to one for this state. Uses C(uv python uninstall) command. + - C(latest) ensures the latest available patch version for the specified version is installed. If only a minor version is specified (for example C(3.12)), the latest available patch version for that minor release is always installed. If another patch version is already installed but is not the latest, the latest patch version is installed. The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. This state does not use C(uv python upgrade). type: str choices: [present, absent, latest] default: present -seealso: - - https://docs.astral.sh/uv/concepts/python-versions/ - - https://docs.astral.sh/uv/reference/cli/#uv-python attributes: - check_mode: description: Can run in check_mode and return changed status prediction without modifying target. @@ -43,29 +40,11 @@ attributes: description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. support: none notes: - - | - State behavior: - - * O(state=present) - - If a full patch version is specified (for example 3.12.3), that exact version will be installed if not already present. - - If only a minor version is specified (for example 3.12), the latest available patch version for that minor release is installed - only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). - - RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. - - * O(state=absent) - - If a full patch version is specified, only that exact patch version is removed. - - If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. - - If the specified version is not installed, no changes are made. - - RV(python_versions) and RV(python_paths) lengths can be higher or equal to one for this state. - - * O(state=latest) - - If only a minor version is specified (for example C(3.12)), the latest available patch version for that minor release is always installed. - - If another patch version is already installed but is not the latest, the latest patch version is installed. - - This state does not use C(uv python upgrade). - - The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. - - RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. - +seealso: + - https://docs.astral.sh/uv/concepts/python-versions/ + - https://docs.astral.sh/uv/reference/cli/#uv-python author: Mariam Ahhttouche (@mriamah) + ''' EXAMPLES = r''' From fb55d4c6865c4b400f0867efb096e82de8719ec9 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 09:17:36 +0100 Subject: [PATCH 059/131] uv_python module: improve error messages --- plugins/modules/uv_python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index fefdf27473..1d36181221 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -5,7 +5,7 @@ DOCUMENTATION = r''' --- -module: uv.python +module: uv_python short_description: Manage Python versions and installations using uv Python package manager. description: - Install, uninstall or upgrade Python versions managed by C(uv). @@ -118,7 +118,7 @@ class UV: self.python_version_str = self.python_version.__str__() except InvalidVersion: self.module.fail_json( - msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3) are supported in this release." + msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3, 3.15.0a5) are supported in this release." ) def _ensure_min_uv_version(self): From 557e94420cb0fd036ae93386e6cc1bb3f152c3b3 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 10:39:34 +0100 Subject: [PATCH 060/131] uv_python module: fix documentation --- plugins/modules/uv_python.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 1d36181221..6447917ce2 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -9,11 +9,10 @@ module: uv_python short_description: Manage Python versions and installations using uv Python package manager. description: - Install, uninstall or upgrade Python versions managed by C(uv). -version_added: "0.1.5" +version_added: "0.1.7" requirements: - uv must be installed and available in PATH. - uv version must be at least 0.8.0. -deprecated: options: version: description: @@ -33,16 +32,20 @@ options: choices: [present, absent, latest] default: present attributes: - - check_mode: + check_mode: description: Can run in check_mode and return changed status prediction without modifying target. support: full - - diff_mode: + diff_mode: description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. support: none notes: seealso: - - https://docs.astral.sh/uv/concepts/python-versions/ - - https://docs.astral.sh/uv/reference/cli/#uv-python +- name: uv documentation + description: Python versions management with uv. + link: https://docs.astral.sh/uv/concepts/python-versions/ +- name: uv CLI documentation + description: uv CLI reference guide. + link: https://docs.astral.sh/uv/reference/cli/#uv-python author: Mariam Ahhttouche (@mriamah) ''' From 43b7b562ab7b6bec32ad0ca2e6a3bbbf21ca0eb1 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 10:43:35 +0100 Subject: [PATCH 061/131] uv_python module: refactor code --- plugins/modules/uv_python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 6447917ce2..8ecfbf346e 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -101,7 +101,6 @@ try: from packaging.version import Version, InvalidVersion HAS_LIB = True except: - HAS_LIB = False LIB_IMP_ERR = traceback.format_exc() From 16170301c09930a7cf64a86dfaba229f449c576b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 11:24:47 +0100 Subject: [PATCH 062/131] uv_python module: increment patch version --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 8ecfbf346e..4864f90f78 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -9,7 +9,7 @@ module: uv_python short_description: Manage Python versions and installations using uv Python package manager. description: - Install, uninstall or upgrade Python versions managed by C(uv). -version_added: "0.1.7" +version_added: "0.1.8" requirements: - uv must be installed and available in PATH. - uv version must be at least 0.8.0. From 14b3f86c454f51e08c11da130fac4d434bb1d5c7 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 11:32:55 +0100 Subject: [PATCH 063/131] uv_python module: pin uv version used in tests --- tests/integration/targets/uv_python/tasks/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index d1e301e3e5..708aa20ce0 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -23,7 +23,7 @@ - name: Install uv shell: | - curl -LsSf https://astral.sh/uv/install.sh | sh + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.10.5/uv-installer.sh | sh environment: UV_INSTALL_DIR: /usr/local/bin From e8020e5867e9daad3861a0d6b9ca0d166dc632be Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 12:05:11 +0100 Subject: [PATCH 064/131] uv_python module: format documentation --- plugins/modules/uv_python.py | 44 +++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 4864f90f78..d98f70f0c0 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -25,9 +25,26 @@ options: state: description: - Desired state of the specified Python version. - - C(present) ensures the specified version is installed. If a full patch version is specified (for example C(3.12.3)), that exact version will be installed if not already present. If only a minor version is specified (for example 3.12), the latest available patch version for that minor release is installed only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. Uses C(uv python install) command. - - C(absent) ensures the specified version is removed. If a full patch version is specified, only that exact patch version is removed. If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. If the specified version is not installed, no changes are made. RV(python_versions) and RV(python_paths) lengths can be higher or equal to one for this state. Uses C(uv python uninstall) command. - - C(latest) ensures the latest available patch version for the specified version is installed. If only a minor version is specified (for example C(3.12)), the latest available patch version for that minor release is always installed. If another patch version is already installed but is not the latest, the latest patch version is installed. The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. This state does not use C(uv python upgrade). + - | + C(present) ensures the specified version is installed. + If a full patch version is specified (for example C(3.12.3)), that exact version will be installed if not already present. + If only a minor version is specified (for example 3.12), the latest available patch version for that minor release is installed only + if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). + RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. + This state uses C(uv python install) command. + - | + C(absent) ensures the specified version is removed. If a full patch version is specified, only that exact patch version is removed. + If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. + If the specified version is not installed, no changes are made. RV(python_versions) and RV(python_paths) + lengths can be higher or equal to one for this state. + This state uses C(uv python uninstall) command. + - | + C(latest) ensures the latest available patch version for the specified version is installed. + If only a minor version is specified (for example C(3.12)), the latest available patch version for that minor release is always installed. + If another patch version is already installed but is not the latest, the latest patch version is installed. + The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. + RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. + This state does not use C(uv python upgrade). type: str choices: [present, absent, latest] default: present @@ -106,22 +123,23 @@ except: MINIMUM_UV_VERSION = "0.8.0" + class UV: """ Module for managing Python versions and installations using "uv python" command """ def __init__(self, module): - self.module = module - self._ensure_min_uv_version() - python_version = module.params["version"] - try: - self.python_version = Version(python_version) - self.python_version_str = self.python_version.__str__() - except InvalidVersion: - self.module.fail_json( - msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3, 3.15.0a5) are supported in this release." - ) + self.module = module + self._ensure_min_uv_version() + python_version = module.params["version"] + try: + self.python_version = Version(python_version) + self.python_version_str = self.python_version.__str__() + except InvalidVersion: + self.module.fail_json( + msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3, 3.15.0a5) are supported in this release." + ) def _ensure_min_uv_version(self): cmd = [self.module.get_bin_path("uv", required=True), "--version", "--color", "never"] From b678f5471b818f80039762d019f1766cd3936c29 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 12:17:23 +0100 Subject: [PATCH 065/131] uv_python module: format code using black --- plugins/modules/uv_python.py | 262 +++++++++++++++++------------------ 1 file changed, 127 insertions(+), 135 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index d98f70f0c0..20e723965e 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -116,6 +116,7 @@ LIB_IMP_ERR = None HAS_LIB = False try: from packaging.version import Version, InvalidVersion + HAS_LIB = True except: LIB_IMP_ERR = traceback.format_exc() @@ -126,38 +127,38 @@ MINIMUM_UV_VERSION = "0.8.0" class UV: """ - Module for managing Python versions and installations using "uv python" command + Module for managing Python versions and installations using "uv python" command """ def __init__(self, module): - self.module = module - self._ensure_min_uv_version() - python_version = module.params["version"] - try: - self.python_version = Version(python_version) - self.python_version_str = self.python_version.__str__() - except InvalidVersion: - self.module.fail_json( - msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3, 3.15.0a5) are supported in this release." - ) + self.module = module + self._ensure_min_uv_version() + python_version = module.params["version"] + try: + self.python_version = Version(python_version) + self.python_version_str = self.python_version.__str__() + except InvalidVersion: + self.module.fail_json( + msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3, 3.15.0a5) are supported in this release." + ) def _ensure_min_uv_version(self): - cmd = [self.module.get_bin_path("uv", required=True), "--version", "--color", "never"] - _, out, _ = self.module.run_command(cmd, check_rc=True) - detected = out.strip().split()[-1] - if LooseVersion(detected) < LooseVersion(MINIMUM_UV_VERSION): - self.module.fail_json( - msg=f"uv_python module requires uv >= {MINIMUM_UV_VERSION}", - detected_version=detected, - required_version=MINIMUM_UV_VERSION, - ) + cmd = [self.module.get_bin_path("uv", required=True), "--version", "--color", "never"] + _, out, _ = self.module.run_command(cmd, check_rc=True) + detected = out.strip().split()[-1] + if LooseVersion(detected) < LooseVersion(MINIMUM_UV_VERSION): + self.module.fail_json( + msg=f"uv_python module requires uv >= {MINIMUM_UV_VERSION}", + detected_version=detected, + required_version=MINIMUM_UV_VERSION, + ) def install_python(self): - """ + """ Runs command 'uv python install X.Y.Z' which installs specified python version. If patch version is not specified uv installs latest available patch version. - Returns: - tuple [bool, str, str, int, list, list] + Returns: + tuple [bool, str, str, int, list, list] - boolean to indicate if method changed state - command's stdout - command's stderr @@ -168,27 +169,27 @@ class UV: AnsibleModuleFailJson: If the install command exits with a non-zero return code. If specified version is not available for download. - """ - find_rc, existing_version, _ = self._find_python("--show-version") - if find_rc == 0: - _, version_path, _ = self._find_python() - return False, "", "", 0, [existing_version], [version_path] - if self.module.check_mode: - latest_version, _ = self._get_latest_patch_release("--managed-python") - # when uv does not find any available patch version the install command will fail - if not latest_version: - self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) - return True, "", "", 0, [latest_version], [""] - rc, out, err = self._exec(self.python_version_str, "install", check_rc=True) - latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") - return True, out, err, rc, [latest_version], [path] + """ + find_rc, existing_version, _ = self._find_python("--show-version") + if find_rc == 0: + _, version_path, _ = self._find_python() + return False, "", "", 0, [existing_version], [version_path] + if self.module.check_mode: + latest_version, _ = self._get_latest_patch_release("--managed-python") + # when uv does not find any available patch version the install command will fail + if not latest_version: + self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) + return True, "", "", 0, [latest_version], [""] + rc, out, err = self._exec(self.python_version_str, "install", check_rc=True) + latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") + return True, out, err, rc, [latest_version], [path] def uninstall_python(self): - """ + """ Runs command 'uv python uninstall X.Y.Z' which removes specified python version from environment. If patch version is not specified all correspending installed patch versions are removed. - Returns: - tuple [bool, str, str, int, list, list] + Returns: + tuple [bool, str, str, int, list, list] - boolean to indicate if method changed state - command's stdout - command's stderr @@ -198,20 +199,20 @@ class UV: Raises: AnsibleModuleFailJson: If the uninstall command exits with a non-zero return code. - """ - installed_versions, install_paths = self._get_installed_versions("--managed-python") - if not installed_versions: - return False, "", "", 0, [], [] - if self.module.check_mode: - return True, "", "", 0, installed_versions, install_paths - rc, out, err = self._exec(self.python_version_str, "uninstall", check_rc=True) - return True, out, err, rc, installed_versions, install_paths + """ + installed_versions, install_paths = self._get_installed_versions("--managed-python") + if not installed_versions: + return False, "", "", 0, [], [] + if self.module.check_mode: + return True, "", "", 0, installed_versions, install_paths + rc, out, err = self._exec(self.python_version_str, "uninstall", check_rc=True) + return True, out, err, rc, installed_versions, install_paths def upgrade_python(self): - """ + """ Runs command 'uv python install X.Y.Z' with latest patch version available. - Returns: - tuple [bool, str, str, int, list, list] + Returns: + tuple [bool, str, str, int, list, list] - boolean to indicate if method changed state - command's stdout - command's stderr @@ -222,25 +223,25 @@ class UV: AnsibleModuleFailJson: If the install command exits with a non-zero return code. If resolved patch version is not available for download. - """ - rc, installed_version_str, _ = self._find_python("--show-version") - installed_version = self._parse_version(installed_version_str) - latest_version_str, _ = self._get_latest_patch_release("--managed-python") - if not latest_version_str: - self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") - if rc == 0 and installed_version >= Version(latest_version_str): - _, install_path, _ = self._find_python() - return False, "", "", rc, [installed_version.__str__()], [install_path] - if self.module.check_mode: - return True, "", "", 0, [latest_version_str], [] - # it's possible to have latest version already installed but not used as default - # so in this case 'uv python install' will set latest version as default - rc, out, err = self._exec(latest_version_str, "install", check_rc=True) - latest_version_str, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") - return True, out, err, rc, [latest_version_str], [latest_path] + """ + rc, installed_version_str, _ = self._find_python("--show-version") + installed_version = self._parse_version(installed_version_str) + latest_version_str, _ = self._get_latest_patch_release("--managed-python") + if not latest_version_str: + self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") + if rc == 0 and installed_version >= Version(latest_version_str): + _, install_path, _ = self._find_python() + return False, "", "", rc, [installed_version.__str__()], [install_path] + if self.module.check_mode: + return True, "", "", 0, [latest_version_str], [] + # it's possible to have latest version already installed but not used as default + # so in this case 'uv python install' will set latest version as default + rc, out, err = self._exec(latest_version_str, "install", check_rc=True) + latest_version_str, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") + return True, out, err, rc, [latest_version_str], [latest_path] def _exec(self, python_version, command, *args, check_rc=False): - """ + """ Execute a uv python subcommand. Args: python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). @@ -253,15 +254,15 @@ class UV: Raises: AnsibleModuleFailJson: If check_rc is True and the command exits with a non-zero return code. - """ - cmd = [self.module.get_bin_path("uv", required=True), "python", command, python_version, "--color", "never", *args] - rc, out, err = self.module.run_command(cmd, check_rc=check_rc) - return rc, out, err + """ + cmd = [self.module.get_bin_path("uv", required=True), "python", command, python_version, "--color", "never", *args] + rc, out, err = self.module.run_command(cmd, check_rc=check_rc) + return rc, out, err def _find_python(self, *args, check_rc=False): - """ + """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. - If multiple patch versions are installed, "uv python find" returns the one used by default + If multiple patch versions are installed, "uv python find" returns the one used by default if inside a virtualenv otherwise it returns latest installed patch version. Args: *args: Additional positional arguments passed to _exec. @@ -272,14 +273,14 @@ class UV: Raises: AnsibleModuleFailJson: If check_rc is True and the command exits with a non-zero return code. - """ - rc, out, err = self._exec(self.python_version_str, "find", *args, check_rc=check_rc) - if rc == 0: - out = out.strip() - return rc, out, err + """ + rc, out, err = self._exec(self.python_version_str, "find", *args, check_rc=check_rc) + if rc == 0: + out = out.strip() + return rc, out, err def _list_python(self, *args, check_rc=False): - """ + """ Runs command 'uv python list' (which returns list of installed patch releases for a given python version). Official documentation https://docs.astral.sh/uv/reference/cli/#uv-python-list Args: @@ -291,17 +292,17 @@ class UV: Raises: AnsibleModuleFailJson: If check_rc is True and the command exits with a non-zero return code. - """ - rc, out, err = self._exec(self.python_version_str, "list", "--output-format", "json", *args, check_rc=check_rc) - try: - out = json.loads(out) - except json.decoder.JSONDecodeError: - # This happens when no version is found - pass - return rc, out, err + """ + rc, out, err = self._exec(self.python_version_str, "list", "--output-format", "json", *args, check_rc=check_rc) + try: + out = json.loads(out) + except json.decoder.JSONDecodeError: + # This happens when no version is found + pass + return rc, out, err def _get_latest_patch_release(self, *args): - """ + """ Returns latest available patch release for a given python version. Args: *args: Additional positional arguments passed to _list_python. @@ -309,18 +310,18 @@ class UV: tuple[str, str]: - latest found patch version in format X.Y.Z - installation path of latest patch version if version exists - """ - latest_version = path = "" - _, results, _ = self._list_python(*args) # uv returns versions in descending order but we sort them just in case future uv behavior changes - valid_results = self._parse_versions(results) - if valid_results: - version = max(valid_results, key=lambda result: result["parsed_version"]) - latest_version = version.get("version", "") - path = version.get("path", "") - return latest_version, path + """ + latest_version = path = "" + _, results, _ = self._list_python(*args) # uv returns versions in descending order but we sort them just in case future uv behavior changes + valid_results = self._parse_versions(results) + if valid_results: + version = max(valid_results, key=lambda result: result["parsed_version"]) + latest_version = version.get("version", "") + path = version.get("path", "") + return latest_version, path def _get_installed_versions(self, *args): - """ + """ Returns installed patch releases for a given python version. Args: *args: Additional positional arguments passed to _list_python. @@ -328,65 +329,56 @@ class UV: tuple[list, list]: - list of latest found patch versions - list of installation paths of installed versions - """ - _, results, _ = self._list_python("--only-installed", *args) - if results: - return [result["version"] for result in results], [result["path"] for result in results] - return [], [] + """ + _, results, _ = self._list_python("--only-installed", *args) + if results: + return [result["version"] for result in results], [result["path"] for result in results] + return [], [] @staticmethod def _parse_versions(results): - valid_results =[] - for result in results: - try: - result["parsed_version"] = Version(result.get("version", "")) - valid_results.append(result) - except InvalidVersion: - continue - return valid_results + valid_results = [] + for result in results: + try: + result["parsed_version"] = Version(result.get("version", "")) + valid_results.append(result) + except InvalidVersion: + continue + return valid_results @staticmethod def _parse_version(version_str): - try: - return Version(version_str) - except InvalidVersion: - return Version("0") + try: + return Version(version_str) + except InvalidVersion: + return Version("0") def main(): module = AnsibleModule( argument_spec=dict( - version=dict(type='str', required=True), - state=dict(type='str', default='present', choices=['present', 'absent', 'latest']), + version=dict(type="str", required=True), + state=dict(type="str", default="present", choices=["present", "absent", "latest"]), ), - supports_check_mode=True + supports_check_mode=True, ) if not HAS_LIB: - module.fail_json(msg=missing_required_lib("packaging"), - exception=LIB_IMP_ERR) - - result = dict( - changed=False, - stdout="", - stderr="", - rc=0, - python_versions=[], - python_paths=[], - failed=False - ) + module.fail_json(msg=missing_required_lib("packaging"), exception=LIB_IMP_ERR) + + result = dict(changed=False, stdout="", stderr="", rc=0, python_versions=[], python_paths=[], failed=False) state = module.params["state"] uv = UV(module) if state == "present": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.install_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.install_python() elif state == "absent": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.uninstall_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.uninstall_python() elif state == "latest": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.upgrade_python() + result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.upgrade_python() module.exit_json(**result) if __name__ == "__main__": - main() \ No newline at end of file + main() From 62f94de35c770b2a55233e18541f60f00a19b157 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 12:18:21 +0100 Subject: [PATCH 066/131] uv_python module: format documentation block --- plugins/modules/uv_python.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 20e723965e..ecd6b91fe9 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -26,20 +26,20 @@ options: description: - Desired state of the specified Python version. - | - C(present) ensures the specified version is installed. + V(present) ensures the specified version is installed. If a full patch version is specified (for example C(3.12.3)), that exact version will be installed if not already present. If only a minor version is specified (for example 3.12), the latest available patch version for that minor release is installed only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. This state uses C(uv python install) command. - | - C(absent) ensures the specified version is removed. If a full patch version is specified, only that exact patch version is removed. + V(absent) ensures the specified version is removed. If a full patch version is specified, only that exact patch version is removed. If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. If the specified version is not installed, no changes are made. RV(python_versions) and RV(python_paths) lengths can be higher or equal to one for this state. This state uses C(uv python uninstall) command. - | - C(latest) ensures the latest available patch version for the specified version is installed. + V(latest) ensures the latest available patch version for the specified version is installed. If only a minor version is specified (for example C(3.12)), the latest available patch version for that minor release is always installed. If another patch version is already installed but is not the latest, the latest patch version is installed. The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. @@ -50,19 +50,19 @@ options: default: present attributes: check_mode: - description: Can run in check_mode and return changed status prediction without modifying target. - support: full + description: Can run in check_mode and return changed status prediction without modifying target. + support: full diff_mode: - description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. - support: none + description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. + support: none notes: seealso: -- name: uv documentation - description: Python versions management with uv. - link: https://docs.astral.sh/uv/concepts/python-versions/ -- name: uv CLI documentation - description: uv CLI reference guide. - link: https://docs.astral.sh/uv/reference/cli/#uv-python + - name: uv documentation + description: Python versions management with uv. + link: https://docs.astral.sh/uv/concepts/python-versions/ + - name: uv CLI documentation + description: uv CLI reference guide. + link: https://docs.astral.sh/uv/reference/cli/#uv-python author: Mariam Ahhttouche (@mriamah) ''' From 295029774497be3e0839e92fd3435043f0838dd1 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 14:19:50 +0100 Subject: [PATCH 067/131] uv_python module: fix examples in documentation block --- plugins/modules/uv_python.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index ecd6b91fe9..b16a060651 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -9,7 +9,7 @@ module: uv_python short_description: Manage Python versions and installations using uv Python package manager. description: - Install, uninstall or upgrade Python versions managed by C(uv). -version_added: "0.1.8" +version_added: "0.1.9" requirements: - uv must be installed and available in PATH. - uv version must be at least 0.8.0. @@ -35,8 +35,8 @@ options: - | V(absent) ensures the specified version is removed. If a full patch version is specified, only that exact patch version is removed. If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. - If the specified version is not installed, no changes are made. RV(python_versions) and RV(python_paths) - lengths can be higher or equal to one for this state. + If the specified version is not installed, no changes are made. + RV(python_versions) and RV(python_paths) lengths can be higher or equal to one for this state. This state uses C(uv python uninstall) command. - | V(latest) ensures the latest available patch version for the specified version is installed. @@ -69,16 +69,16 @@ author: Mariam Ahhttouche (@mriamah) EXAMPLES = r''' - name: Install Python 3.14 - mriamah.uv_python: + community.general.uv_python: version: "3.14" - name: Remove Python 3.13.5 - mriamah.uv_python: + community.general.uv_python: version: 3.13.5 state: absent - name: Upgrade python 3 - mriamah.uv_python: + community.general.uv_python: version: 3 state: latest ''' From 4c4ab080c344a55d2bb316372b16b9ae5f623bcc Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 14:49:42 +0100 Subject: [PATCH 068/131] Format code with ruff --- plugins/modules/uv_python.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index b16a060651..045d21f671 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -3,7 +3,7 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uv_python short_description: Manage Python versions and installations using uv Python package manager. @@ -65,9 +65,9 @@ seealso: link: https://docs.astral.sh/uv/reference/cli/#uv-python author: Mariam Ahhttouche (@mriamah) -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: Install Python 3.14 community.general.uv_python: version: "3.14" @@ -81,9 +81,9 @@ EXAMPLES = r''' community.general.uv_python: version: 3 state: latest -''' +""" -RETURN = r''' +RETURN = r""" python_versions: description: List of Python versions changed. returned: success @@ -104,7 +104,7 @@ rc: description: Return code of the executed command. returned: success type: int -''' +""" import json import traceback From d3abd2234478e64e7035ca1547b24e01a0a72f3f Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 15:55:22 +0100 Subject: [PATCH 069/131] Use ansible tone in documentation --- plugins/modules/uv_python.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 045d21f671..8d21b58973 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -19,7 +19,7 @@ options: - Python version to manage. - Only L(canonical Python versions, https://peps.python.org/pep-0440/) are supported in this release such as C(3), C(3.12), C(3.12.3), C(3.15.0a5). - Advanced uv selectors such as C(>=3.12,<3.13) or C(cpython@3.12) are not supported in this release. - - When specifying only a major or major.minor version, behavior depends on the O(state) parameter. + - When you specify only a major or major.minor version, behavior depends on the O(state) parameter. type: str required: true state: @@ -27,23 +27,22 @@ options: - Desired state of the specified Python version. - | V(present) ensures the specified version is installed. - If a full patch version is specified (for example C(3.12.3)), that exact version will be installed if not already present. - If only a minor version is specified (for example 3.12), the latest available patch version for that minor release is installed only + If you specify a full patch version (for example C(3.12.3)), that exact version will be installed if not already present. + If you only specify a minor version (for example C(3.12)), the latest available patch version for that minor release is installed only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. - This state uses C(uv python install) command. - | - V(absent) ensures the specified version is removed. If a full patch version is specified, only that exact patch version is removed. - If only a minor version is specified (for example C(3.12)), all installed patch versions for that minor release are removed. - If the specified version is not installed, no changes are made. - RV(python_versions) and RV(python_paths) lengths can be higher or equal to one for this state. - This state uses C(uv python uninstall) command. + V(absent) ensures the specified version is removed. + If you specify a full patch version, only that exact patch version is removed. + If you only specify a minor version (for example C(3.12)), all installed patch versions for that minor release are removed. + If you specify a version that is not installed, no changes are made. + RV(python_versions) and RV(python_paths) lengths can be higher or equal to one in this state. - | V(latest) ensures the latest available patch version for the specified version is installed. - If only a minor version is specified (for example C(3.12)), the latest available patch version for that minor release is always installed. + If you only specify a minor version (for example C(3.12)), the latest available patch version for that minor release is always installed. If another patch version is already installed but is not the latest, the latest patch version is installed. The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. - RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. + RV(python_versions) and RV(python_paths) lengths are always equal to one in this state. This state does not use C(uv python upgrade). type: str choices: [present, absent, latest] @@ -118,7 +117,7 @@ try: from packaging.version import Version, InvalidVersion HAS_LIB = True -except: +except ImportError: LIB_IMP_ERR = traceback.format_exc() @@ -144,7 +143,7 @@ class UV: def _ensure_min_uv_version(self): cmd = [self.module.get_bin_path("uv", required=True), "--version", "--color", "never"] - _, out, _ = self.module.run_command(cmd, check_rc=True) + ignored_rc, out, ignored_err = self.module.run_command(cmd, check_rc=True) detected = out.strip().split()[-1] if LooseVersion(detected) < LooseVersion(MINIMUM_UV_VERSION): self.module.fail_json( @@ -170,12 +169,12 @@ class UV: If the install command exits with a non-zero return code. If specified version is not available for download. """ - find_rc, existing_version, _ = self._find_python("--show-version") + find_rc, existing_version, ignored_err = self._find_python("--show-version") if find_rc == 0: - _, version_path, _ = self._find_python() + ignored_rc, version_path, ignored_err = self._find_python() return False, "", "", 0, [existing_version], [version_path] if self.module.check_mode: - latest_version, _ = self._get_latest_patch_release("--managed-python") + latest_version, ignored_path = self._get_latest_patch_release("--managed-python") # when uv does not find any available patch version the install command will fail if not latest_version: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) @@ -224,13 +223,13 @@ class UV: If the install command exits with a non-zero return code. If resolved patch version is not available for download. """ - rc, installed_version_str, _ = self._find_python("--show-version") + rc, installed_version_str, ignored_err = self._find_python("--show-version") installed_version = self._parse_version(installed_version_str) - latest_version_str, _ = self._get_latest_patch_release("--managed-python") + latest_version_str, ignored_path = self._get_latest_patch_release("--managed-python") if not latest_version_str: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and installed_version >= Version(latest_version_str): - _, install_path, _ = self._find_python() + ignored_rc, install_path, ignored_err = self._find_python() return False, "", "", rc, [installed_version.__str__()], [install_path] if self.module.check_mode: return True, "", "", 0, [latest_version_str], [] @@ -312,7 +311,8 @@ class UV: - installation path of latest patch version if version exists """ latest_version = path = "" - _, results, _ = self._list_python(*args) # uv returns versions in descending order but we sort them just in case future uv behavior changes + # 'uv python list' returns versions in descending order but we sort them just in case future uv behavior changes + ignored_rc, results, ignored_err = self._list_python(*args) valid_results = self._parse_versions(results) if valid_results: version = max(valid_results, key=lambda result: result["parsed_version"]) @@ -330,7 +330,7 @@ class UV: - list of latest found patch versions - list of installation paths of installed versions """ - _, results, _ = self._list_python("--only-installed", *args) + ignored_rc, results, ignored_err = self._list_python("--only-installed", *args) if results: return [result["version"] for result in results], [result["path"] for result in results] return [], [] From 0a223b97b3bbd514a3c421c8e24851499d9bfdff Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 16:03:19 +0100 Subject: [PATCH 070/131] Add entry in .github/BOTMETA.yml for uv_python module --- .github/BOTMETA.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 4095986151..9be80a1133 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1425,6 +1425,10 @@ files: $modules/utm_proxy_exception.py: keywords: sophos utm maintainers: $team_e_spirit RickS-C137 + $modules/uv_python.py: + labels: uv python + keywords: uv python + maintainers: mriamah $modules/vdo.py: maintainers: rhawalsh bgurney-rh $modules/vertica_: From ee59148fb9603e7e97592131caca559c9e8bc407 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 24 Feb 2026 16:16:35 +0100 Subject: [PATCH 071/131] Upgrade patch version --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 8d21b58973..37fd6dc2c6 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -9,7 +9,7 @@ module: uv_python short_description: Manage Python versions and installations using uv Python package manager. description: - Install, uninstall or upgrade Python versions managed by C(uv). -version_added: "0.1.9" +version_added: "0.1.10" requirements: - uv must be installed and available in PATH. - uv version must be at least 0.8.0. From 19043541f11fc77aac1e1a6a7e714036f8ef20fd Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 08:33:22 +0100 Subject: [PATCH 072/131] Fix yamllint errors --- tests/integration/targets/uv_python/tasks/main.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 708aa20ce0..d6ee352526 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -12,7 +12,7 @@ uv_python: version: 3.14 state: present - check_mode: yes + check_mode: true register: failed_install ignore_errors: true @@ -66,7 +66,7 @@ uv_python: version: 3.14 state: present - check_mode: yes + check_mode: true register: install_check_mode - name: Verify python 3.14 installation in check mode @@ -204,7 +204,7 @@ uv_python: version: 3.13 state: latest - check_mode: yes + check_mode: true register: upgrade_python - name: Verify python 3.13 upgrade in check mode @@ -218,7 +218,7 @@ uv_python: version: 3.13 state: latest - check_mode: yes + check_mode: true register: upgrade_python - name: Verify python 3.13 upgrade again From d734dbbcf72495779dee2d2d57d719653ade0c87 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 08:41:44 +0100 Subject: [PATCH 073/131] Fix unsorted imports reported by ruff --- plugins/modules/uv_python.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 37fd6dc2c6..e8a8af6256 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -107,9 +107,6 @@ rc: import json import traceback -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.compat.version import LooseVersion -from ansible.module_utils.basic import missing_required_lib LIB_IMP_ERR = None HAS_LIB = False @@ -120,6 +117,9 @@ try: except ImportError: LIB_IMP_ERR = traceback.format_exc() +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.compat.version import LooseVersion + MINIMUM_UV_VERSION = "0.8.0" From d078c66f27545f7bb1ebde4cc04c102fbe444945 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 08:44:16 +0100 Subject: [PATCH 074/131] Add license information to uv_python module --- plugins/modules/uv_python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e8a8af6256..ac97910862 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -1,6 +1,7 @@ #!/usr/bin/python # Copyright (c) 2026 Mariam Ahhttouche # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later DOCUMENTATION = r""" From a4b658e613a2ca98792d922c108355f3ada26736 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 12:08:29 +0100 Subject: [PATCH 075/131] Fix typo --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index ac97910862..658f573025 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -231,7 +231,7 @@ class UV: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and installed_version >= Version(latest_version_str): ignored_rc, install_path, ignored_err = self._find_python() - return False, "", "", rc, [installed_version.__str__()], [install_path] + return False, "", "", rc, [installed_version_str], [install_path] if self.module.check_mode: return True, "", "", 0, [latest_version_str], [] # it's possible to have latest version already installed but not used as default From d53c0b92b652294fa43c8084a4281d042ff08485 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 20:02:36 +0100 Subject: [PATCH 076/131] Correct version_added field in module documentation Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 658f573025..9c1a97cbcb 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -10,7 +10,7 @@ module: uv_python short_description: Manage Python versions and installations using uv Python package manager. description: - Install, uninstall or upgrade Python versions managed by C(uv). -version_added: "0.1.10" +version_added: "12.5.0" requirements: - uv must be installed and available in PATH. - uv version must be at least 0.8.0. From 8cd809dfbfb7a2f56f9f8f4ae74ad52b5af4e832 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 20:03:26 +0100 Subject: [PATCH 077/131] Fix short_description field in module documentation Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 9c1a97cbcb..1c726bca4e 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -7,7 +7,7 @@ DOCUMENTATION = r""" --- module: uv_python -short_description: Manage Python versions and installations using uv Python package manager. +short_description: Manage Python versions and installations using uv Python package manager description: - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "12.5.0" From 062fd970412f1e2fd4ef6948a754e3ee92f54abd Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 20:04:04 +0100 Subject: [PATCH 078/131] Fix typo Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 1c726bca4e..f795add54a 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -77,7 +77,7 @@ EXAMPLES = r""" version: 3.13.5 state: absent -- name: Upgrade python 3 +- name: Upgrade Python 3 community.general.uv_python: version: 3 state: latest From 479b6333060ac7e11ab0e8219a67ffabbb0d192f Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 20:04:41 +0100 Subject: [PATCH 079/131] Improve module documentation Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index f795add54a..5eed4b448f 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -12,8 +12,8 @@ description: - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "12.5.0" requirements: - - uv must be installed and available in PATH. - - uv version must be at least 0.8.0. + - uv >= 0.8.0 must be installed and available in PATH + - packaging options: version: description: From 1858a586b437a1b348770958884f7cac0ec0db90 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 20:06:23 +0100 Subject: [PATCH 080/131] Fix typo Co-authored-by: Felix Fontein --- tests/integration/targets/uv_python/tasks/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index d6ee352526..891c775889 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -8,7 +8,7 @@ # 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 python 3.14 when no uv executable exists +- name: Install Python 3.14 when no uv executable exists uv_python: version: 3.14 state: present From ed0eb833f76e356fe3252b23c2bb206215406f89 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 25 Feb 2026 20:39:24 +0100 Subject: [PATCH 081/131] Add integration tests' aliases file for uv_python module --- tests/integration/targets/uv_python/aliases | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/integration/targets/uv_python/aliases diff --git a/tests/integration/targets/uv_python/aliases b/tests/integration/targets/uv_python/aliases new file mode 100644 index 0000000000..75a75a8786 --- /dev/null +++ b/tests/integration/targets/uv_python/aliases @@ -0,0 +1,9 @@ +# 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 + +uv_python +uv +python +skip/python2 +skip/windows \ No newline at end of file From 5033a4f92249c508b995bd528b97acde21ab9f7a Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 27 Feb 2026 17:18:48 +0100 Subject: [PATCH 082/131] Use StrictVersion instead of packaging Version --- plugins/modules/uv_python.py | 51 +++++++------------ .../targets/uv_python/tasks/main.yaml | 35 ------------- 2 files changed, 17 insertions(+), 69 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 5eed4b448f..94a1a9b898 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -12,15 +12,16 @@ description: - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "12.5.0" requirements: - - uv >= 0.8.0 must be installed and available in PATH - - packaging + - uv must be installed and available in PATH and uv version must be >= 0.8.0. options: version: description: - Python version to manage. - - Only L(canonical Python versions, https://peps.python.org/pep-0440/) are supported in this release such as C(3), C(3.12), C(3.12.3), C(3.15.0a5). + - | + Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated numeric components, + with an optional 'pre-release' tag on the end such as C(3.12), C(3.12.3), C(3.15.0a5). - Advanced uv selectors such as C(>=3.12,<3.13) or C(cpython@3.12) are not supported in this release. - - When you specify only a major or major.minor version, behavior depends on the O(state) parameter. + - When you specify only a major.minor version, behavior depends on the O(state) parameter. type: str required: true state: @@ -76,11 +77,6 @@ EXAMPLES = r""" community.general.uv_python: version: 3.13.5 state: absent - -- name: Upgrade Python 3 - community.general.uv_python: - version: 3 - state: latest """ RETURN = r""" @@ -107,19 +103,8 @@ rc: """ import json -import traceback - -LIB_IMP_ERR = None -HAS_LIB = False -try: - from packaging.version import Version, InvalidVersion - - HAS_LIB = True -except ImportError: - LIB_IMP_ERR = traceback.format_exc() - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.compat.version import LooseVersion +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.compat.version import LooseVersion, StrictVersion MINIMUM_UV_VERSION = "0.8.0" @@ -135,11 +120,12 @@ class UV: self._ensure_min_uv_version() python_version = module.params["version"] try: - self.python_version = Version(python_version) + self.python_version = StrictVersion(python_version) self.python_version_str = self.python_version.__str__() - except InvalidVersion: + except ValueError: self.module.fail_json( - msg="Unsupported version format. Only canonical Python versions (e.g. 3, 3.12, 3.12.3, 3.15.0a5) are supported in this release." + msg="Unsupported version format. Valid version numbers consist of two or three dot-separated numeric components, \ + with an optional 'pre-release' tag on the end (e.g. 3.12, 3.12.3, 3.15.0a5) are supported in this release." ) def _ensure_min_uv_version(self): @@ -229,7 +215,7 @@ class UV: latest_version_str, ignored_path = self._get_latest_patch_release("--managed-python") if not latest_version_str: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") - if rc == 0 and installed_version >= Version(latest_version_str): + if rc == 0 and installed_version >= StrictVersion(latest_version_str): ignored_rc, install_path, ignored_err = self._find_python() return False, "", "", rc, [installed_version_str], [install_path] if self.module.check_mode: @@ -341,18 +327,18 @@ class UV: valid_results = [] for result in results: try: - result["parsed_version"] = Version(result.get("version", "")) + result["parsed_version"] = StrictVersion(result.get("version", "")) valid_results.append(result) - except InvalidVersion: + except ValueError: continue return valid_results @staticmethod def _parse_version(version_str): try: - return Version(version_str) - except InvalidVersion: - return Version("0") + return StrictVersion(version_str) + except ValueError: + return StrictVersion("0") def main(): @@ -364,9 +350,6 @@ def main(): supports_check_mode=True, ) - if not HAS_LIB: - module.fail_json(msg=missing_required_lib("packaging"), exception=LIB_IMP_ERR) - result = dict(changed=False, stdout="", stderr="", rc=0, python_versions=[], python_paths=[], failed=False) state = module.params["state"] diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 891c775889..1a46174647 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -27,41 +27,6 @@ environment: UV_INSTALL_DIR: /usr/local/bin -- name: Install python 3 - uv_python: - version: 3 - state: present - register: python_install - -- name: Assert python 3 is installed - ansible.builtin.assert: - that: - - python_install.failed is false - - python_install.python_versions | length >= 1 - -- name: Upgrade python 3 - uv_python: - version: 3 - state: latest - register: python_install - -- name: Assert python 3 upgraded - ansible.builtin.assert: - that: - - python_install.failed is false - - python_install.python_versions | length >= 1 - -- name: Remove python 3 - uv_python: - version: 3 - state: absent - register: python_delete - -- name: Assert python 3 deleted - ansible.builtin.assert: - that: - - python_delete.failed is false - - name: Install python 3.14 in check mode uv_python: version: 3.14 From f3475ae0230f8f08cbbc2a52541b6472d506d107 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 12:17:51 +0100 Subject: [PATCH 083/131] make integration tests more deterministic --- tests/integration/targets/uv_python/tasks/main.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 1a46174647..7833fbcb6f 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -27,6 +27,11 @@ environment: UV_INSTALL_DIR: /usr/local/bin +- name: Check if Python 3.14 exists already + command: uv python find 3.14 + ignore_errors: true + register: check_python_314_exists + - name: Install python 3.14 in check mode uv_python: version: 3.14 @@ -37,7 +42,7 @@ - name: Verify python 3.14 installation in check mode assert: that: - - install_check_mode.changed is true + - install_check_mode.changed == check_python_314_exists.failed - install_check_mode.failed is false - install_check_mode.python_versions | length >= 1 @@ -50,7 +55,7 @@ - name: Verify python 3.14 installation assert: that: - - install_python.changed is true + - install_python.changed == check_python_314_exists.failed - install_python.failed is false - install_python.python_versions | length >= 1 - install_python.python_paths | length >= 1 From 90e8ad98a6e2c920692502098ec354613764d498 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 12:18:36 +0100 Subject: [PATCH 084/131] Update attributes field in documentation --- plugins/modules/uv_python.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 94a1a9b898..aa36198c0b 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -13,6 +13,15 @@ description: version_added: "12.5.0" requirements: - uv must be installed and available in PATH and uv version must be >= 0.8.0. +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + description: Can run in check_mode and return changed status prediction without modifying target. + support: full + diff_mode: + description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. + support: none options: version: description: @@ -49,13 +58,6 @@ options: type: str choices: [present, absent, latest] default: present -attributes: - check_mode: - description: Can run in check_mode and return changed status prediction without modifying target. - support: full - diff_mode: - description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. - support: none notes: seealso: - name: uv documentation From 9f81b6e9a0c65de50bc1488c8ac1686947be355f Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 12:25:58 +0100 Subject: [PATCH 085/131] Save uv bin path in an attribute --- plugins/modules/uv_python.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index aa36198c0b..b6d51aabba 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -119,9 +119,10 @@ class UV: def __init__(self, module): self.module = module + self.bin_path = self.module.get_bin_path("uv", required=True) self._ensure_min_uv_version() - python_version = module.params["version"] try: + python_version = module.params["version"] self.python_version = StrictVersion(python_version) self.python_version_str = self.python_version.__str__() except ValueError: @@ -131,7 +132,7 @@ class UV: ) def _ensure_min_uv_version(self): - cmd = [self.module.get_bin_path("uv", required=True), "--version", "--color", "never"] + cmd = [self.bin_path, "--version", "--color", "never"] ignored_rc, out, ignored_err = self.module.run_command(cmd, check_rc=True) detected = out.strip().split()[-1] if LooseVersion(detected) < LooseVersion(MINIMUM_UV_VERSION): @@ -243,7 +244,7 @@ class UV: AnsibleModuleFailJson: If check_rc is True and the command exits with a non-zero return code. """ - cmd = [self.module.get_bin_path("uv", required=True), "python", command, python_version, "--color", "never", *args] + cmd = [self.bin_path, "python", command, python_version, "--color", "never", *args] rc, out, err = self.module.run_command(cmd, check_rc=check_rc) return rc, out, err From 61e4cfb72d06a0560393d504bec3680ee851afd2 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 12:29:22 +0100 Subject: [PATCH 086/131] Use str instead of __str__ --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index b6d51aabba..7a00a173c5 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -124,7 +124,7 @@ class UV: try: python_version = module.params["version"] self.python_version = StrictVersion(python_version) - self.python_version_str = self.python_version.__str__() + self.python_version_str = str(self.python_version) except ValueError: self.module.fail_json( msg="Unsupported version format. Valid version numbers consist of two or three dot-separated numeric components, \ From 028a707e57897ef81321dd4c117c9ae00f437d60 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 12:37:25 +0100 Subject: [PATCH 087/131] Clean documentation --- plugins/modules/uv_python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 7a00a173c5..4337edbaf1 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -58,7 +58,6 @@ options: type: str choices: [present, absent, latest] default: present -notes: seealso: - name: uv documentation description: Python versions management with uv. From c910ddc7c39eb58f7f333936e2e8264f73113b39 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 13:44:19 +0100 Subject: [PATCH 088/131] Update plugins/modules/uv_python.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 4337edbaf1..a9ae4e0aa9 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -38,7 +38,7 @@ options: - Desired state of the specified Python version. - | V(present) ensures the specified version is installed. - If you specify a full patch version (for example C(3.12.3)), that exact version will be installed if not already present. + If you specify a full patch version (for example O(version=3.12.3)), that exact version will be installed if not already present. If you only specify a minor version (for example C(3.12)), the latest available patch version for that minor release is installed only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. From d44280e59144b0c7eb31ab898840f604098b3045 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 13:44:51 +0100 Subject: [PATCH 089/131] Update plugins/modules/uv_python.py documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index a9ae4e0aa9..e3464d9f62 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -28,7 +28,7 @@ options: - Python version to manage. - | Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated numeric components, - with an optional 'pre-release' tag on the end such as C(3.12), C(3.12.3), C(3.15.0a5). + with an optional 'pre-release' tag on the end such as V(3.12), V(3.12.3), V(3.15.0a5). - Advanced uv selectors such as C(>=3.12,<3.13) or C(cpython@3.12) are not supported in this release. - When you specify only a major.minor version, behavior depends on the O(state) parameter. type: str From 8c526794b7b9735fd2d00f90e8f8f4569728b32c Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 14:05:55 +0100 Subject: [PATCH 090/131] Add another example in documentation --- plugins/modules/uv_python.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e3464d9f62..4391202ace 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -74,6 +74,11 @@ EXAMPLES = r""" community.general.uv_python: version: "3.14" +- name: Upgrade Python 3.14 + community.general.uv_python: + version: 3.14 + state: latest + - name: Remove Python 3.13.5 community.general.uv_python: version: 3.13.5 From d52fdc4c83e70b8cdb9e9d34a39037bcd139360d Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 15:49:38 +0100 Subject: [PATCH 091/131] make integration tests more deterministic and fix typo --- .../targets/uv_python/tasks/main.yaml | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 7833fbcb6f..8ebfdbbea2 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -16,7 +16,7 @@ register: failed_install ignore_errors: true -- name: Verify python 3.14 installation failed +- name: Verify Python 3.14 installation failed assert: that: - failed_install.failed is true @@ -32,27 +32,27 @@ ignore_errors: true register: check_python_314_exists -- name: Install python 3.14 in check mode +- name: Install Python 3.14 in check mode uv_python: version: 3.14 state: present check_mode: true register: install_check_mode -- name: Verify python 3.14 installation in check mode +- name: Verify Python 3.14 installation in check mode assert: that: - install_check_mode.changed == check_python_314_exists.failed - install_check_mode.failed is false - install_check_mode.python_versions | length >= 1 -- name: Install python 3.14 +- name: Install Python 3.14 uv_python: version: 3.14 state: present register: install_python -- name: Verify python 3.14 installation +- name: Verify Python 3.14 installation assert: that: - install_python.changed == check_python_314_exists.failed @@ -60,13 +60,13 @@ - install_python.python_versions | length >= 1 - install_python.python_paths | length >= 1 -- name: Re-install python 3.14 +- name: Re-install Python 3.14 uv_python: version: 3.14 state: present register: reinstall_python -- name: Verify python 3.14 re-installation +- name: Verify Python 3.14 re-installation assert: that: - reinstall_python.changed is false @@ -74,53 +74,62 @@ - reinstall_python.python_versions | length >= 1 - reinstall_python.python_paths | length >= 1 -- name: Install python 3.13.5 +- name: Check if Python 3.13.5 exists already + command: uv python find 3.13.5 + ignore_errors: true + register: check_python_3135_exists + +- name: Check if Python + command: uv python list + ignore_errors: true + +- name: Install Python 3.13.5 uv_python: version: 3.13.5 register: install_python -- name: Verify python 3.13.5 installation +- name: Verify Python 3.13.5 installation assert: that: - - install_python.changed is true + - install_python.changed == check_python_3135_exists.failed - install_python.failed is false - '"3.13.5" in install_python.python_versions' - install_python.python_paths | length >= 1 -- name: Re-install python 3.13.5 +- name: Re-install Python 3.13.5 uv_python: version: 3.13.5 state: present register: reinstall_python -- name: Verify python 3.13.5 installation +- name: Verify Python 3.13.5 installation assert: that: - reinstall_python.changed is false - reinstall_python.failed is false - '"3.13.5" in reinstall_python.python_versions' -- name: Remove uninstalled python 3.10 # removes latest patch version for 3.10 if exists +- name: Remove uninstalled Python 3.10 # removes latest patch version for 3.10 if exists uv_python: version: "3.10" state: absent register: remove_python -- name: Verify python 3.10 deletion +- name: Verify Python 3.10 deletion assert: that: - remove_python.changed is false - remove_python.failed is false - remove_python.python_versions | length == 0 -- name: Remove python 3.13.5 in check mode +- name: Remove Python 3.13.5 in check mode uv_python: version: 3.13.5 state: absent check_mode: true register: remove_python_in_check_mode -- name: Verify python 3.13.5 deletion in check mode +- name: Verify Python 3.13.5 deletion in check mode assert: that: - remove_python_in_check_mode.changed is true @@ -128,13 +137,13 @@ - '"3.13.5" in remove_python_in_check_mode.python_versions' - remove_python_in_check_mode.python_paths | length >= 1 -- name: Remove python 3.13.5 +- name: Remove Python 3.13.5 uv_python: version: 3.13.5 state: absent register: remove_python -- name: Verify python 3.13.5 deletion +- name: Verify Python 3.13.5 deletion assert: that: - remove_python.changed is true @@ -142,13 +151,13 @@ - remove_python.python_paths | length >= 1 - '"3.13.5" in remove_python.python_versions' -- name: Remove python 3.13.5 again +- name: Remove Python 3.13.5 again uv_python: version: 3.13.5 state: absent register: remove_python -- name: Verify python 3.13.5 deletion again +- name: Verify Python 3.13.5 deletion again assert: that: - remove_python.changed is false @@ -156,13 +165,13 @@ - remove_python.python_versions | length == 0 - remove_python.python_paths | length == 0 -- name: Upgrade python 3.13 +- name: Upgrade Python 3.13 uv_python: version: 3.13 state: latest register: upgrade_python -- name: Verify python 3.13 upgrade +- name: Verify Python 3.13 upgrade assert: that: - upgrade_python.changed is true @@ -170,35 +179,35 @@ - upgrade_python.python_versions | length >= 1 - upgrade_python.python_paths | length >= 1 -- name: Upgrade python 3.13 in check mode +- name: Upgrade Python 3.13 in check mode uv_python: version: 3.13 state: latest check_mode: true register: upgrade_python -- name: Verify python 3.13 upgrade in check mode +- name: Verify Python 3.13 upgrade in check mode assert: that: - upgrade_python.changed is false - upgrade_python.failed is false - upgrade_python.python_versions | length >= 1 -- name: Upgrade python 3.13 again +- name: Upgrade Python 3.13 again uv_python: version: 3.13 state: latest check_mode: true register: upgrade_python -- name: Verify python 3.13 upgrade again +- name: Verify Python 3.13 upgrade again assert: that: - upgrade_python.changed is false - upgrade_python.failed is false - upgrade_python.python_versions | length >= 1 -- name: Install unexisting python version in check mode +- name: Install unexisting Python version in check mode uv_python: version: 3.12.13 state: present @@ -206,46 +215,46 @@ ignore_errors: true check_mode: true -- name: Verify unexisting python 3.12.13 install in check mode +- name: Verify unexisting Python 3.12.13 install in check mode assert: that: - unexisting_python_check_mode.changed is false - unexisting_python_check_mode.failed is true - '"Version 3.12.13 is not available." in unexisting_python_check_mode.msg' -- name: Install unexisting python version +- name: Install unexisting Python version uv_python: state: present version: 3.12.13 register: unexisting_python ignore_errors: true -- name: Verify unexisting python 3.12.13 install +- name: Verify unexisting Python 3.12.13 install assert: that: - unexisting_python.changed is false - unexisting_python.failed is true -- name: Install unexisting python version with latest state +- name: Install unexisting Python version with latest state uv_python: version: 3.12.13 state: latest register: unexisting_python ignore_errors: true -- name: Verify python 3.12.13 install with latest state +- name: Verify Python 3.12.13 install with latest state assert: that: - unexisting_python.changed is false - unexisting_python.failed is true -- name: Delete python 3.13 version +- name: Delete Python 3.13 version uv_python: version: 3.13 state: absent register: uninstall_result -- name: Verify python 3.13 deletion +- name: Verify Python 3.13 deletion assert: that: - uninstall_result.changed is true From f8bc6e84839f3a46e656a0993c845b827d2ceb03 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 16:49:54 +0100 Subject: [PATCH 092/131] Apply PR feedback and refactor code --- plugins/modules/uv_python.py | 53 ++++++++++++------------------------ 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 4391202ace..207c7d078c 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -29,7 +29,7 @@ options: - | Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated numeric components, with an optional 'pre-release' tag on the end such as V(3.12), V(3.12.3), V(3.15.0a5). - - Advanced uv selectors such as C(>=3.12,<3.13) or C(cpython@3.12) are not supported in this release. + - Advanced uv selectors such as V(>=3.12,<3.13) or V(cpython@3.12) are not supported in this release. - When you specify only a major.minor version, behavior depends on the O(state) parameter. type: str required: true @@ -39,18 +39,18 @@ options: - | V(present) ensures the specified version is installed. If you specify a full patch version (for example O(version=3.12.3)), that exact version will be installed if not already present. - If you only specify a minor version (for example C(3.12)), the latest available patch version for that minor release is installed only + If you only specify a minor version (for example V(3.12)), the latest available patch version for that minor release is installed only if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. - | V(absent) ensures the specified version is removed. If you specify a full patch version, only that exact patch version is removed. - If you only specify a minor version (for example C(3.12)), all installed patch versions for that minor release are removed. + If you only specify a minor version (for example V(3.12)), all installed patch versions for that minor release are removed. If you specify a version that is not installed, no changes are made. RV(python_versions) and RV(python_paths) lengths can be higher or equal to one in this state. - | V(latest) ensures the latest available patch version for the specified version is installed. - If you only specify a minor version (for example C(3.12)), the latest available patch version for that minor release is always installed. + If you only specify a minor version (for example V(3.12)), the latest available patch version for that minor release is always installed. If another patch version is already installed but is not the latest, the latest patch version is installed. The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. RV(python_versions) and RV(python_paths) lengths are always equal to one in this state. @@ -146,7 +146,7 @@ class UV: required_version=MINIMUM_UV_VERSION, ) - def install_python(self): + def install_python(self) -> tuple[bool, str, str, int, list, list]: """ Runs command 'uv python install X.Y.Z' which installs specified python version. If patch version is not specified uv installs latest available patch version. @@ -158,10 +158,6 @@ class UV: - command's return code - list of installed versions - list of installation paths for each installed version - Raises: - AnsibleModuleFailJson: - If the install command exits with a non-zero return code. - If specified version is not available for download. """ find_rc, existing_version, ignored_err = self._find_python("--show-version") if find_rc == 0: @@ -177,7 +173,7 @@ class UV: latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [path] - def uninstall_python(self): + def uninstall_python(self) -> tuple[bool, str, str, int, list, list]: """ Runs command 'uv python uninstall X.Y.Z' which removes specified python version from environment. If patch version is not specified all correspending installed patch versions are removed. @@ -189,9 +185,6 @@ class UV: - command's return code - list of uninstalled versions - list of previous installation paths for each uninstalled version - Raises: - AnsibleModuleFailJson: - If the uninstall command exits with a non-zero return code. """ installed_versions, install_paths = self._get_installed_versions("--managed-python") if not installed_versions: @@ -201,7 +194,7 @@ class UV: rc, out, err = self._exec(self.python_version_str, "uninstall", check_rc=True) return True, out, err, rc, installed_versions, install_paths - def upgrade_python(self): + def upgrade_python(self) -> tuple[bool, str, str, int, list, list]: """ Runs command 'uv python install X.Y.Z' with latest patch version available. Returns: @@ -212,10 +205,6 @@ class UV: - command's return code - list of installed versions - list of installation paths for each installed version - Raises: - AnsibleModuleFailJson: - If the install command exits with a non-zero return code. - If resolved patch version is not available for download. """ rc, installed_version_str, ignored_err = self._find_python("--show-version") installed_version = self._parse_version(installed_version_str) @@ -233,7 +222,7 @@ class UV: latest_version_str, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version_str], [latest_path] - def _exec(self, python_version, command, *args, check_rc=False): + def _exec(self, python_version, command, *args, check_rc=False) -> tuple[int, str, str]: """ Execute a uv python subcommand. Args: @@ -244,15 +233,12 @@ class UV: Returns: tuple[int, str, str]: A tuple containing (rc, stdout, stderr). - Raises: - AnsibleModuleFailJson: - If check_rc is True and the command exits with a non-zero return code. """ cmd = [self.bin_path, "python", command, python_version, "--color", "never", *args] rc, out, err = self.module.run_command(cmd, check_rc=check_rc) return rc, out, err - def _find_python(self, *args, check_rc=False): + def _find_python(self, *args, check_rc=False) -> tuple[int, str, str]: """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. If multiple patch versions are installed, "uv python find" returns the one used by default @@ -263,16 +249,13 @@ class UV: Returns: tuple[int, str, str]: A tuple containing (rc, stdout, stderr). - Raises: - AnsibleModuleFailJson: - If check_rc is True and the command exits with a non-zero return code. """ rc, out, err = self._exec(self.python_version_str, "find", *args, check_rc=check_rc) if rc == 0: out = out.strip() return rc, out, err - def _list_python(self, *args, check_rc=False): + def _list_python(self, *args, check_rc=False) -> tuple[int, list, str]: """ Runs command 'uv python list' (which returns list of installed patch releases for a given python version). Official documentation https://docs.astral.sh/uv/reference/cli/#uv-python-list @@ -280,21 +263,19 @@ class UV: *args: Additional positional arguments passed to _exec. check_rc (bool): Whether to fail if the command exits with non-zero return code. Returns: - tuple[int, str, str]: + tuple[int, list, str] A tuple containing (rc, stdout, stderr). - Raises: - AnsibleModuleFailJson: - If check_rc is True and the command exits with a non-zero return code. """ rc, out, err = self._exec(self.python_version_str, "list", "--output-format", "json", *args, check_rc=check_rc) + pythons_installed = [] try: - out = json.loads(out) + pythons_installed = json.loads(out) except json.decoder.JSONDecodeError: # This happens when no version is found pass - return rc, out, err + return rc, pythons_installed, err - def _get_latest_patch_release(self, *args): + def _get_latest_patch_release(self, *args) -> tuple[str, str]: """ Returns latest available patch release for a given python version. Args: @@ -314,7 +295,7 @@ class UV: path = version.get("path", "") return latest_version, path - def _get_installed_versions(self, *args): + def _get_installed_versions(self, *args) -> tuple[list, list]: """ Returns installed patch releases for a given python version. Args: @@ -326,7 +307,7 @@ class UV: """ ignored_rc, results, ignored_err = self._list_python("--only-installed", *args) if results: - return [result["version"] for result in results], [result["path"] for result in results] + return [result.get("version") for result in results], [result.get("path") for result in results] return [], [] @staticmethod From 05c100c66139510c4f3cd80bfc46892c2bdb86b0 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 17:51:45 +0100 Subject: [PATCH 093/131] Fix typing to be compatible with python versions <= 3.8 --- plugins/modules/uv_python.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 207c7d078c..1d1df1e1f7 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -109,6 +109,7 @@ rc: """ import json +from typing import Tuple from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.compat.version import LooseVersion, StrictVersion @@ -146,7 +147,7 @@ class UV: required_version=MINIMUM_UV_VERSION, ) - def install_python(self) -> tuple[bool, str, str, int, list, list]: + def install_python(self) -> Tuple[bool, str, str, int, list, list]: """ Runs command 'uv python install X.Y.Z' which installs specified python version. If patch version is not specified uv installs latest available patch version. @@ -173,7 +174,7 @@ class UV: latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [path] - def uninstall_python(self) -> tuple[bool, str, str, int, list, list]: + def uninstall_python(self) -> Tuple[bool, str, str, int, list, list]: """ Runs command 'uv python uninstall X.Y.Z' which removes specified python version from environment. If patch version is not specified all correspending installed patch versions are removed. @@ -194,7 +195,7 @@ class UV: rc, out, err = self._exec(self.python_version_str, "uninstall", check_rc=True) return True, out, err, rc, installed_versions, install_paths - def upgrade_python(self) -> tuple[bool, str, str, int, list, list]: + def upgrade_python(self) -> Tuple[bool, str, str, int, list, list]: """ Runs command 'uv python install X.Y.Z' with latest patch version available. Returns: @@ -222,7 +223,7 @@ class UV: latest_version_str, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version_str], [latest_path] - def _exec(self, python_version, command, *args, check_rc=False) -> tuple[int, str, str]: + def _exec(self, python_version, command, *args, check_rc=False) -> Tuple[int, str, str]: """ Execute a uv python subcommand. Args: @@ -238,7 +239,7 @@ class UV: rc, out, err = self.module.run_command(cmd, check_rc=check_rc) return rc, out, err - def _find_python(self, *args, check_rc=False) -> tuple[int, str, str]: + def _find_python(self, *args, check_rc=False) -> Tuple[int, str, str]: """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. If multiple patch versions are installed, "uv python find" returns the one used by default @@ -255,7 +256,7 @@ class UV: out = out.strip() return rc, out, err - def _list_python(self, *args, check_rc=False) -> tuple[int, list, str]: + def _list_python(self, *args, check_rc=False) -> Tuple[int, list, str]: """ Runs command 'uv python list' (which returns list of installed patch releases for a given python version). Official documentation https://docs.astral.sh/uv/reference/cli/#uv-python-list @@ -275,7 +276,7 @@ class UV: pass return rc, pythons_installed, err - def _get_latest_patch_release(self, *args) -> tuple[str, str]: + def _get_latest_patch_release(self, *args) -> Tuple[str, str]: """ Returns latest available patch release for a given python version. Args: @@ -295,7 +296,7 @@ class UV: path = version.get("path", "") return latest_version, path - def _get_installed_versions(self, *args) -> tuple[list, list]: + def _get_installed_versions(self, *args) -> Tuple[list, list]: """ Returns installed patch releases for a given python version. Args: From 0f5a46c98b7050d1c2f1edc771c9b8ed08fe92b8 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Thu, 5 Mar 2026 21:55:32 +0100 Subject: [PATCH 094/131] Update example to use quotes for major.minor example and update documentation --- plugins/modules/uv_python.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 1d1df1e1f7..b7d0f8c1e8 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -30,7 +30,9 @@ options: Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated numeric components, with an optional 'pre-release' tag on the end such as V(3.12), V(3.12.3), V(3.15.0a5). - Advanced uv selectors such as V(>=3.12,<3.13) or V(cpython@3.12) are not supported in this release. - - When you specify only a major.minor version, behavior depends on the O(state) parameter. + - | + When you specify only a major.minor version, make sure the number is enclosed in quotes so that it gets parsed correctly. + Note that in this case behavior depends on the O(state) parameter. type: str required: true state: @@ -76,7 +78,7 @@ EXAMPLES = r""" - name: Upgrade Python 3.14 community.general.uv_python: - version: 3.14 + version: "3.14" state: latest - name: Remove Python 3.13.5 From 4a4ec34145c51c2deff8e2031e14de1757e96002 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 12:34:20 +0000 Subject: [PATCH 095/131] Fix `typing.Tuple` is deprecated linting error --- plugins/modules/uv_python.py | 19 +++++++++---------- tests/integration/targets/uv_python/aliases | 1 + 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index b7d0f8c1e8..e5e6f85f36 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -13,6 +13,7 @@ description: version_added: "12.5.0" requirements: - uv must be installed and available in PATH and uv version must be >= 0.8.0. + - Python 3.9 or higher is required on the target system. extends_documentation_fragment: - community.general.attributes attributes: @@ -68,7 +69,6 @@ seealso: description: uv CLI reference guide. link: https://docs.astral.sh/uv/reference/cli/#uv-python author: Mariam Ahhttouche (@mriamah) - """ EXAMPLES = r""" @@ -111,7 +111,6 @@ rc: """ import json -from typing import Tuple from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.compat.version import LooseVersion, StrictVersion @@ -149,7 +148,7 @@ class UV: required_version=MINIMUM_UV_VERSION, ) - def install_python(self) -> Tuple[bool, str, str, int, list, list]: + def install_python(self) -> tuple[bool, str, str, int, list, list]: """ Runs command 'uv python install X.Y.Z' which installs specified python version. If patch version is not specified uv installs latest available patch version. @@ -176,7 +175,7 @@ class UV: latest_version, path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version], [path] - def uninstall_python(self) -> Tuple[bool, str, str, int, list, list]: + def uninstall_python(self) -> tuple[bool, str, str, int, list, list]: """ Runs command 'uv python uninstall X.Y.Z' which removes specified python version from environment. If patch version is not specified all correspending installed patch versions are removed. @@ -197,7 +196,7 @@ class UV: rc, out, err = self._exec(self.python_version_str, "uninstall", check_rc=True) return True, out, err, rc, installed_versions, install_paths - def upgrade_python(self) -> Tuple[bool, str, str, int, list, list]: + def upgrade_python(self) -> tuple[bool, str, str, int, list, list]: """ Runs command 'uv python install X.Y.Z' with latest patch version available. Returns: @@ -225,7 +224,7 @@ class UV: latest_version_str, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version_str], [latest_path] - def _exec(self, python_version, command, *args, check_rc=False) -> Tuple[int, str, str]: + def _exec(self, python_version, command, *args, check_rc=False) -> tuple[int, str, str]: """ Execute a uv python subcommand. Args: @@ -241,7 +240,7 @@ class UV: rc, out, err = self.module.run_command(cmd, check_rc=check_rc) return rc, out, err - def _find_python(self, *args, check_rc=False) -> Tuple[int, str, str]: + def _find_python(self, *args, check_rc=False) -> tuple[int, str, str]: """ Runs command 'uv python find' which returns path of installed patch releases for a given python version. If multiple patch versions are installed, "uv python find" returns the one used by default @@ -258,7 +257,7 @@ class UV: out = out.strip() return rc, out, err - def _list_python(self, *args, check_rc=False) -> Tuple[int, list, str]: + def _list_python(self, *args, check_rc=False) -> tuple[int, list, str]: """ Runs command 'uv python list' (which returns list of installed patch releases for a given python version). Official documentation https://docs.astral.sh/uv/reference/cli/#uv-python-list @@ -278,7 +277,7 @@ class UV: pass return rc, pythons_installed, err - def _get_latest_patch_release(self, *args) -> Tuple[str, str]: + def _get_latest_patch_release(self, *args) -> tuple[str, str]: """ Returns latest available patch release for a given python version. Args: @@ -298,7 +297,7 @@ class UV: path = version.get("path", "") return latest_version, path - def _get_installed_versions(self, *args) -> Tuple[list, list]: + def _get_installed_versions(self, *args) -> tuple[list, list]: """ Returns installed patch releases for a given python version. Args: diff --git a/tests/integration/targets/uv_python/aliases b/tests/integration/targets/uv_python/aliases index 75a75a8786..d63eaaca1d 100644 --- a/tests/integration/targets/uv_python/aliases +++ b/tests/integration/targets/uv_python/aliases @@ -6,4 +6,5 @@ uv_python uv python skip/python2 +skip/python3.8 skip/windows \ No newline at end of file From 22147bd91318d923ac85d8c602261b2e16daf796 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 13:30:53 +0000 Subject: [PATCH 096/131] Updates test aliases --- tests/integration/targets/uv_python/aliases | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/integration/targets/uv_python/aliases b/tests/integration/targets/uv_python/aliases index d63eaaca1d..b6fc5a1676 100644 --- a/tests/integration/targets/uv_python/aliases +++ b/tests/integration/targets/uv_python/aliases @@ -2,9 +2,8 @@ # 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 -uv_python -uv -python +azp/posix/2 +destructive skip/python2 skip/python3.8 skip/windows \ No newline at end of file From 2475c57cbffeddca1aad0245b95abf5acbcb672f Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 14:44:11 +0000 Subject: [PATCH 097/131] Format uv_python documentation using andebox --- plugins/modules/uv_python.py | 45 +++++++++++++++--------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e5e6f85f36..8db628b872 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -5,7 +5,6 @@ DOCUMENTATION = r""" ---- module: uv_python short_description: Manage Python versions and installations using uv Python package manager description: @@ -27,37 +26,31 @@ options: version: description: - Python version to manage. - - | - Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated numeric components, - with an optional 'pre-release' tag on the end such as V(3.12), V(3.12.3), V(3.15.0a5). + - "Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated + numeric components,\nwith an optional 'pre-release' tag at the end such as V(3.12), V(3.12.3), V(3.15.0a5)." - Advanced uv selectors such as V(>=3.12,<3.13) or V(cpython@3.12) are not supported in this release. - - | - When you specify only a major.minor version, make sure the number is enclosed in quotes so that it gets parsed correctly. - Note that in this case behavior depends on the O(state) parameter. + - "When you specify only a major.minor version, make sure the number is enclosed in quotes so that it gets parsed correctly.\n\ + Note that in case only a major.minor version are specified behavior depends on the O(state) parameter." type: str required: true state: description: - Desired state of the specified Python version. - - | - V(present) ensures the specified version is installed. - If you specify a full patch version (for example O(version=3.12.3)), that exact version will be installed if not already present. - If you only specify a minor version (for example V(3.12)), the latest available patch version for that minor release is installed only - if no patch version for that minor release is currently installed (including patch versions not managed by C(uv)). - RV(python_versions) and RV(python_paths) lengths are always equal to one for this state. - - | - V(absent) ensures the specified version is removed. - If you specify a full patch version, only that exact patch version is removed. - If you only specify a minor version (for example V(3.12)), all installed patch versions for that minor release are removed. - If you specify a version that is not installed, no changes are made. - RV(python_versions) and RV(python_paths) lengths can be higher or equal to one in this state. - - | - V(latest) ensures the latest available patch version for the specified version is installed. - If you only specify a minor version (for example V(3.12)), the latest available patch version for that minor release is always installed. - If another patch version is already installed but is not the latest, the latest patch version is installed. - The latest patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. - RV(python_versions) and RV(python_paths) lengths are always equal to one in this state. - This state does not use C(uv python upgrade). + - "V(present) ensures the specified version is installed.\nIf you specify a full patch version (for example O(version=3.12.3)), + that exact version is be installed if not already present.\nIf you only specify a minor version (for example V(3.12)), + the latest available patch version for that minor release is installed only\nif no patch version for that minor release + is currently installed (including patch versions not managed by C(uv)).\nRV(python_versions) and RV(python_paths) + lengths are always equal to one for this state." + - "V(absent) ensures the specified version is removed.\nIf you specify a full patch version, only that exact patch version + is removed.\nIf you only specify a minor version (for example V(3.12)), all installed patch versions for that minor + release are removed.\nIf you specify a version that is not installed, no changes are made.\nRV(python_versions) and + RV(python_paths) lengths can be higher or equal to one in this state." + - "V(latest) ensures the latest available patch version for the specified version is installed.\nIf you only specify + a minor version (for example V(3.12)), the latest available patch version for that minor release is always installed.\n\ + If another patch version is already installed but is not the latest, the latest patch version is installed.\nThe latest + patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release.\n\ + RV(python_versions) and RV(python_paths) lengths are always equal to one in this state.\nThis state does not use C(uv + python upgrade)." type: str choices: [present, absent, latest] default: present From ca02b7689950ffcc3e9271b116b1db9ba1697df9 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 15:03:23 +0000 Subject: [PATCH 098/131] Use documentation fragment for uv_python attributes --- plugins/modules/uv_python.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 8db628b872..a06b2db51e 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -17,10 +17,8 @@ extends_documentation_fragment: - community.general.attributes attributes: check_mode: - description: Can run in check_mode and return changed status prediction without modifying target. support: full diff_mode: - description: Returns details on what has changed (or possibly needs changing in check_mode), when in diff mode. support: none options: version: From 58616656cee565adb34e6cba3c70c019e7db3470 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 15:17:26 +0000 Subject: [PATCH 099/131] Remove unecessary integration test for uv_python --- tests/integration/targets/uv_python/tasks/main.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 8ebfdbbea2..1453b51a29 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -8,19 +8,6 @@ # 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 Python 3.14 when no uv executable exists - uv_python: - version: 3.14 - state: present - check_mode: true - register: failed_install - ignore_errors: true - -- name: Verify Python 3.14 installation failed - assert: - that: - - failed_install.failed is true - - name: Install uv shell: | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.10.5/uv-installer.sh | sh From 9697c31e8dc76345400d8e65e3258c24787afc0c Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 15:27:49 +0000 Subject: [PATCH 100/131] Add aliases to skip running tests on freebsd and rhel --- tests/integration/targets/uv_python/aliases | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/targets/uv_python/aliases b/tests/integration/targets/uv_python/aliases index b6fc5a1676..b216733e68 100644 --- a/tests/integration/targets/uv_python/aliases +++ b/tests/integration/targets/uv_python/aliases @@ -6,4 +6,6 @@ azp/posix/2 destructive skip/python2 skip/python3.8 -skip/windows \ No newline at end of file +skip/windows +skip/freebsd +skip/rhel From 97bda3fb5e8d3a17b2c5a990ba3f9a153e23f38f Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 17:04:20 +0000 Subject: [PATCH 101/131] Make uv_python tests more deterministic --- .../targets/uv_python/tasks/main.yaml | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 1453b51a29..53cef8c90d 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -66,22 +66,18 @@ ignore_errors: true register: check_python_3135_exists -- name: Check if Python - command: uv python list - ignore_errors: true - - name: Install Python 3.13.5 uv_python: version: 3.13.5 - register: install_python + register: install_python_3135 - name: Verify Python 3.13.5 installation assert: that: - - install_python.changed == check_python_3135_exists.failed - - install_python.failed is false - - '"3.13.5" in install_python.python_versions' - - install_python.python_paths | length >= 1 + - install_python_3135.changed == check_python_3135_exists.failed + - install_python_3135.failed is false + - '"3.13.5" in install_python_3135.python_versions' + - install_python_3135.python_paths | length >= 1 - name: Re-install Python 3.13.5 uv_python: @@ -119,8 +115,13 @@ - name: Verify Python 3.13.5 deletion in check mode assert: that: - - remove_python_in_check_mode.changed is true + - remove_python_in_check_mode.changed == install_python_3135.changed - remove_python_in_check_mode.failed is false + +- name: Additional check for Python 3.13.5 deletion in check mode + when: install_python_3135.changed is true + assert: + that: - '"3.13.5" in remove_python_in_check_mode.python_versions' - remove_python_in_check_mode.python_paths | length >= 1 @@ -133,8 +134,13 @@ - name: Verify Python 3.13.5 deletion assert: that: - - remove_python.changed is true + - remove_python.changed == install_python_3135.changed - remove_python.failed is false + +- name: Additional check for Python 3.13.5 deletion + when: install_python_3135.changed is true + assert: + that: - remove_python.python_paths | length >= 1 - '"3.13.5" in remove_python.python_versions' From 722c9441c5157d55d1c8465b3efeb6d2e26e309c Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 17:31:13 +0000 Subject: [PATCH 102/131] Clean uv_python documentation --- plugins/modules/uv_python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index a06b2db51e..e84448e651 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -12,7 +12,6 @@ description: version_added: "12.5.0" requirements: - uv must be installed and available in PATH and uv version must be >= 0.8.0. - - Python 3.9 or higher is required on the target system. extends_documentation_fragment: - community.general.attributes attributes: From a21646d3984accf2d47fdfa93d49ea006c7fd7e6 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 17:44:41 +0000 Subject: [PATCH 103/131] Handle case when version given is an empty string in uv_python module --- plugins/modules/uv_python.py | 5 ++--- .../targets/uv_python/tasks/main.yaml | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e84448e651..e67cd4a7f0 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -118,10 +118,9 @@ class UV: self.bin_path = self.module.get_bin_path("uv", required=True) self._ensure_min_uv_version() try: - python_version = module.params["version"] - self.python_version = StrictVersion(python_version) + self.python_version = StrictVersion(module.params["version"]) self.python_version_str = str(self.python_version) - except ValueError: + except (ValueError, AttributeError): self.module.fail_json( msg="Unsupported version format. Valid version numbers consist of two or three dot-separated numeric components, \ with an optional 'pre-release' tag on the end (e.g. 3.12, 3.12.3, 3.15.0a5) are supported in this release." diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 53cef8c90d..e7046b744a 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -257,10 +257,25 @@ - name: No specified version uv_python: state: latest + version: "" ignore_errors: true register: no_version - name: Verify failure when no version is specified assert: that: - - no_version.failed is true \ No newline at end of file + - no_version.failed is true + - '"Unsupported version format" in no_version.msg' + +- name: Unsupported version format given + uv_python: + state: latest + version: "3.8.post2" + ignore_errors: true + register: wrong_version + +- name: Verify failure when unsupported version format is specified + assert: + that: + - wrong_version.failed is true + - '"Unsupported version format" in wrong_version.msg' \ No newline at end of file From e42f9448a447891c97749a73ad006079b7407561 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Fri, 6 Mar 2026 22:04:27 +0000 Subject: [PATCH 104/131] Format code with ruff --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e67cd4a7f0..7cc3402420 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -101,10 +101,10 @@ rc: """ import json + from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.compat.version import LooseVersion, StrictVersion - MINIMUM_UV_VERSION = "0.8.0" From 1ad2630b308be7a3728e2939805f0e61b208c408 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 7 Mar 2026 15:28:02 +0000 Subject: [PATCH 105/131] Apply linguistic guidelines for plugins/modules/uv_python.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 7cc3402420..2ad344d658 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -123,7 +123,7 @@ class UV: except (ValueError, AttributeError): self.module.fail_json( msg="Unsupported version format. Valid version numbers consist of two or three dot-separated numeric components, \ - with an optional 'pre-release' tag on the end (e.g. 3.12, 3.12.3, 3.15.0a5) are supported in this release." + with an optional 'pre-release' tag on the end (for example 3.12, 3.12.3, 3.15.0a5) are supported in this release." ) def _ensure_min_uv_version(self): From 38200235ab12ac8cc7d4c85851c2ca26f358f801 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 7 Mar 2026 15:29:39 +0000 Subject: [PATCH 106/131] Remove escape character from documentation in Update plugins/modules/uv_python.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 2ad344d658..ca73814cce 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -24,7 +24,7 @@ options: description: - Python version to manage. - "Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated - numeric components,\nwith an optional 'pre-release' tag at the end such as V(3.12), V(3.12.3), V(3.15.0a5)." + numeric components, with an optional 'pre-release' tag at the end such as V(3.12), V(3.12.3), V(3.15.0a5)." - Advanced uv selectors such as V(>=3.12,<3.13) or V(cpython@3.12) are not supported in this release. - "When you specify only a major.minor version, make sure the number is enclosed in quotes so that it gets parsed correctly.\n\ Note that in case only a major.minor version are specified behavior depends on the O(state) parameter." From fec8c1d2153b41324a5c236804d9d252267c2e35 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 7 Mar 2026 15:34:40 +0000 Subject: [PATCH 107/131] Remove uneeded escape characters uv_python documentation --- plugins/modules/uv_python.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index ca73814cce..872b7caca9 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -33,20 +33,20 @@ options: state: description: - Desired state of the specified Python version. - - "V(present) ensures the specified version is installed.\nIf you specify a full patch version (for example O(version=3.12.3)), - that exact version is be installed if not already present.\nIf you only specify a minor version (for example V(3.12)), - the latest available patch version for that minor release is installed only\nif no patch version for that minor release - is currently installed (including patch versions not managed by C(uv)).\nRV(python_versions) and RV(python_paths) + - "V(present) ensures the specified version is installed. If you specify a full patch version (for example O(version=3.12.3)), + that exact version is be installed if not already present. If you only specify a minor version (for example V(3.12)), + the latest available patch version for that minor release is installed only if no patch version for that minor release + is currently installed (including patch versions not managed by C(uv)). RV(python_versions) and RV(python_paths) lengths are always equal to one for this state." - - "V(absent) ensures the specified version is removed.\nIf you specify a full patch version, only that exact patch version - is removed.\nIf you only specify a minor version (for example V(3.12)), all installed patch versions for that minor - release are removed.\nIf you specify a version that is not installed, no changes are made.\nRV(python_versions) and + - "V(absent) ensures the specified version is removed. If you specify a full patch version, only that exact patch version + is removed. If you only specify a minor version (for example V(3.12)), all installed patch versions for that minor + release are removed. If you specify a version that is not installed, no changes are made. RV(python_versions) and RV(python_paths) lengths can be higher or equal to one in this state." - - "V(latest) ensures the latest available patch version for the specified version is installed.\nIf you only specify - a minor version (for example V(3.12)), the latest available patch version for that minor release is always installed.\n\ - If another patch version is already installed but is not the latest, the latest patch version is installed.\nThe latest - patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release.\n\ - RV(python_versions) and RV(python_paths) lengths are always equal to one in this state.\nThis state does not use C(uv + - "V(latest) ensures the latest available patch version for the specified version is installed. If you only specify + a minor version (for example V(3.12)), the latest available patch version for that minor release is always installed. \ + If another patch version is already installed but is not the latest, the latest patch version is installed. The latest + patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. \ + RV(python_versions) and RV(python_paths) lengths are always equal to one in this state. This state does not use C(uv python upgrade)." type: str choices: [present, absent, latest] From 6521f08c09ff13ca4e9188ac5c1c1096811c7c5f Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 7 Mar 2026 15:36:59 +0000 Subject: [PATCH 108/131] Update dummy variable names in uv_python module --- plugins/modules/uv_python.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 872b7caca9..049ea21ad5 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -128,7 +128,7 @@ class UV: def _ensure_min_uv_version(self): cmd = [self.bin_path, "--version", "--color", "never"] - ignored_rc, out, ignored_err = self.module.run_command(cmd, check_rc=True) + dummy_rc, out, dummy_err = self.module.run_command(cmd, check_rc=True) detected = out.strip().split()[-1] if LooseVersion(detected) < LooseVersion(MINIMUM_UV_VERSION): self.module.fail_json( @@ -150,12 +150,12 @@ class UV: - list of installed versions - list of installation paths for each installed version """ - find_rc, existing_version, ignored_err = self._find_python("--show-version") + find_rc, existing_version, dummy_err = self._find_python("--show-version") if find_rc == 0: - ignored_rc, version_path, ignored_err = self._find_python() + dummy_rc, version_path, dummy_err = self._find_python() return False, "", "", 0, [existing_version], [version_path] if self.module.check_mode: - latest_version, ignored_path = self._get_latest_patch_release("--managed-python") + latest_version, dummy_path = self._get_latest_patch_release("--managed-python") # when uv does not find any available patch version the install command will fail if not latest_version: self.module.fail_json(msg=(f"Version {self.python_version_str} is not available.")) @@ -197,13 +197,13 @@ class UV: - list of installed versions - list of installation paths for each installed version """ - rc, installed_version_str, ignored_err = self._find_python("--show-version") + rc, installed_version_str, dummy_err = self._find_python("--show-version") installed_version = self._parse_version(installed_version_str) - latest_version_str, ignored_path = self._get_latest_patch_release("--managed-python") + latest_version_str, dummy_path = self._get_latest_patch_release("--managed-python") if not latest_version_str: self.module.fail_json(msg=f"Version {self.python_version_str} is not available.") if rc == 0 and installed_version >= StrictVersion(latest_version_str): - ignored_rc, install_path, ignored_err = self._find_python() + dummy_rc, install_path, dummy_err = self._find_python() return False, "", "", rc, [installed_version_str], [install_path] if self.module.check_mode: return True, "", "", 0, [latest_version_str], [] @@ -278,7 +278,7 @@ class UV: """ latest_version = path = "" # 'uv python list' returns versions in descending order but we sort them just in case future uv behavior changes - ignored_rc, results, ignored_err = self._list_python(*args) + dummy_rc, results, dummy_err = self._list_python(*args) valid_results = self._parse_versions(results) if valid_results: version = max(valid_results, key=lambda result: result["parsed_version"]) @@ -296,7 +296,7 @@ class UV: - list of latest found patch versions - list of installation paths of installed versions """ - ignored_rc, results, ignored_err = self._list_python("--only-installed", *args) + dummy_rc, results, dummy_err = self._list_python("--only-installed", *args) if results: return [result.get("version") for result in results], [result.get("path") for result in results] return [], [] From 55557c37a004a7d299875fae36d1dc6dd4821f59 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 7 Mar 2026 16:17:44 +0000 Subject: [PATCH 109/131] Add Python version requirement in uv_python documentation --- plugins/modules/uv_python.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 049ea21ad5..ae5367276d 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -11,7 +11,8 @@ description: - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "12.5.0" requirements: - - uv must be installed and available in PATH and uv version must be >= 0.8.0. + - uv must be installed and available in PATH and uv version must be at least 0.8.0. + - Python version must be at least 3.9 extends_documentation_fragment: - community.general.attributes attributes: From e221795cf854213d7624235ce94556a09e61d863 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 7 Mar 2026 17:35:00 +0000 Subject: [PATCH 110/131] Update tests to install uv using pip and fix some tests --- .../targets/uv_python/tasks/main.yaml | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index e7046b744a..7b3fee5a00 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -8,11 +8,9 @@ # 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 uv - shell: | - curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.10.5/uv-installer.sh | sh - environment: - UV_INSTALL_DIR: /usr/local/bin +- name: Install uv python package + ansible.builtin.pip: + name: uv - name: Check if Python 3.14 exists already command: uv python find 3.14 @@ -200,46 +198,46 @@ - upgrade_python.failed is false - upgrade_python.python_versions | length >= 1 -- name: Install unexisting Python version in check mode +- name: Install unsupported Python version in check mode uv_python: - version: 3.12.13 + version: 2.0 state: present - register: unexisting_python_check_mode + register: unsupported_python_check_mode ignore_errors: true check_mode: true -- name: Verify unexisting Python 3.12.13 install in check mode +- name: Verify unsupported Python 2.0 install in check mode assert: that: - - unexisting_python_check_mode.changed is false - - unexisting_python_check_mode.failed is true - - '"Version 3.12.13 is not available." in unexisting_python_check_mode.msg' + - unsupported_python_check_mode.changed is false + - unsupported_python_check_mode.failed is true + - '"Version 2.0 is not available." in unsupported_python_check_mode.msg' -- name: Install unexisting Python version +- name: Install unsupported Python version uv_python: state: present - version: 3.12.13 - register: unexisting_python + version: 2.0 + register: unsupported_python ignore_errors: true -- name: Verify unexisting Python 3.12.13 install +- name: Verify unsupported Python 2.0 install assert: that: - - unexisting_python.changed is false - - unexisting_python.failed is true + - unsupported_python.changed is false + - unsupported_python.failed is true -- name: Install unexisting Python version with latest state +- name: Install unsupported Python version with latest state uv_python: - version: 3.12.13 + version: 2.0 state: latest - register: unexisting_python + register: unsupported_python ignore_errors: true -- name: Verify Python 3.12.13 install with latest state +- name: Verify Python 2.0 install with latest state assert: that: - - unexisting_python.changed is false - - unexisting_python.failed is true + - unsupported_python.changed is false + - unsupported_python.failed is true - name: Delete Python 3.13 version uv_python: From dbfd881d7b64878e2b3695f5fc4aaa7252760030 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Sat, 7 Mar 2026 21:45:35 +0000 Subject: [PATCH 111/131] Add typing in Update plugins/modules/uv_python.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index ae5367276d..d1579472fd 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -214,11 +214,11 @@ class UV: latest_version_str, latest_path = self._get_latest_patch_release("--only-installed", "--managed-python") return True, out, err, rc, [latest_version_str], [latest_path] - def _exec(self, python_version, command, *args, check_rc=False) -> tuple[int, str, str]: + def _exec(self, python_version: str, command, *args, check_rc=False) -> tuple[int, str, str]: """ Execute a uv python subcommand. Args: - python_version (str): Python version specifier (e.g. "3.12", "3.12.3"). + python_version: Python version specifier (e.g. "3.12", "3.12.3"). command (str): uv python subcommand (e.g. "install", "uninstall", "find"). *args: Additional positional arguments passed to the command. check_rc (bool): Whether to fail if the command exits with non-zero return code. From 025cf018dc6b12ac346582fc7e1c71a972f06909 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 15:00:03 +0000 Subject: [PATCH 112/131] Update plugins/modules/uv_python.py documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index d1579472fd..c5245e868f 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -12,7 +12,7 @@ description: version_added: "12.5.0" requirements: - uv must be installed and available in PATH and uv version must be at least 0.8.0. - - Python version must be at least 3.9 + - Python version must be at least 3.9. extends_documentation_fragment: - community.general.attributes attributes: From a02743fd769d67e65a89f93e102add3b89b696da Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 15:01:47 +0000 Subject: [PATCH 113/131] Make code in plugins/modules/uv_python.py more pythonic Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index c5245e868f..960f0c9882 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -123,8 +123,10 @@ class UV: self.python_version_str = str(self.python_version) except (ValueError, AttributeError): self.module.fail_json( - msg="Unsupported version format. Valid version numbers consist of two or three dot-separated numeric components, \ - with an optional 'pre-release' tag on the end (for example 3.12, 3.12.3, 3.15.0a5) are supported in this release." + msg=( + "Unsupported version format. Valid version numbers consist of two or three dot-separated numeric components" + " with an optional 'pre-release' tag on the end (for example 3.12, 3.12.3, 3.15.0a5) are supported in this release." + ) ) def _ensure_min_uv_version(self): From cd3dc6a76f6147d22c1ac2e7a090577d70d2a2e9 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 15:02:35 +0000 Subject: [PATCH 114/131] Use ansible markup in plugins/modules/uv_python.py documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 960f0c9882..32864fa43d 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -53,11 +53,11 @@ options: choices: [present, absent, latest] default: present seealso: - - name: uv documentation - description: Python versions management with uv. + - name: C(uv) documentation + description: Python versions management with C(uv). link: https://docs.astral.sh/uv/concepts/python-versions/ - - name: uv CLI documentation - description: uv CLI reference guide. + - name: C(uv) CLI documentation + description: C(uv) CLI reference guide. link: https://docs.astral.sh/uv/reference/cli/#uv-python author: Mariam Ahhttouche (@mriamah) """ From 1e686895cf3fc65ae366ef3f5712b269f7e2107b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 15:05:24 +0000 Subject: [PATCH 115/131] Update plugins/modules/uv_python.py documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 32864fa43d..184276a741 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -43,12 +43,11 @@ options: is removed. If you only specify a minor version (for example V(3.12)), all installed patch versions for that minor release are removed. If you specify a version that is not installed, no changes are made. RV(python_versions) and RV(python_paths) lengths can be higher or equal to one in this state." - - "V(latest) ensures the latest available patch version for the specified version is installed. If you only specify - a minor version (for example V(3.12)), the latest available patch version for that minor release is always installed. \ + - V(latest) ensures the latest available patch version for the specified version is installed. If you only specify + a minor version (for example V(3.12)), the latest available patch version for that minor release is always installed. If another patch version is already installed but is not the latest, the latest patch version is installed. The latest - patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. \ - RV(python_versions) and RV(python_paths) lengths are always equal to one in this state. This state does not use C(uv - python upgrade)." + patch version installed depends on the C(uv) version, since available Python versions are frozen per C(uv) release. + RV(python_versions) and RV(python_paths) lengths are always equal to one in this state. This state does not use C(uv python upgrade). type: str choices: [present, absent, latest] default: present From 5341d23621d48b6de830e5e98f30041ba5e3115b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 15:06:17 +0000 Subject: [PATCH 116/131] Update plugins/modules/uv_python.py documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 184276a741..4240e54ede 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -27,8 +27,8 @@ options: - "Not all canonical Python versions are supported in this release. Valid version numbers consist of two or three dot-separated numeric components, with an optional 'pre-release' tag at the end such as V(3.12), V(3.12.3), V(3.15.0a5)." - Advanced uv selectors such as V(>=3.12,<3.13) or V(cpython@3.12) are not supported in this release. - - "When you specify only a major.minor version, make sure the number is enclosed in quotes so that it gets parsed correctly.\n\ - Note that in case only a major.minor version are specified behavior depends on the O(state) parameter." + - When you specify only a major.minor version, make sure the number is enclosed in quotes so that it gets parsed correctly. + Note that in case only a major.minor version are specified behavior depends on the O(state) parameter. type: str required: true state: From 51153a615c7851595bbffbc7164190ebed2b7dfd Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 15:07:00 +0000 Subject: [PATCH 117/131] Update plugins/modules/uv_python.py documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 4240e54ede..6fe2be68ce 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -6,7 +6,7 @@ DOCUMENTATION = r""" module: uv_python -short_description: Manage Python versions and installations using uv Python package manager +short_description: Manage Python versions and installations using the C(uv) Python package manager description: - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "12.5.0" From 9633e951d468c7aaec93814ec9774683a4aeceae Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 15:54:27 +0000 Subject: [PATCH 118/131] Add task to uv_python tests to add uv installation directory to PATH --- tests/integration/targets/uv_python/tasks/main.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 7b3fee5a00..9ae3c967ed 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -12,10 +12,15 @@ ansible.builtin.pip: name: uv +- name: Add uv installation directory to PATH in macOS + shell: export PATH="$(python3 -m site --user-base)/bin:$PATH" + when: ansible_facts['os_family'] == "Darwin" + - name: Check if Python 3.14 exists already command: uv python find 3.14 ignore_errors: true register: check_python_314_exists + changed_when: false - name: Install Python 3.14 in check mode uv_python: @@ -63,6 +68,7 @@ command: uv python find 3.13.5 ignore_errors: true register: check_python_3135_exists + changed_when: false - name: Install Python 3.13.5 uv_python: From f7c0bd4120270b94e2332ed17ebaa4793191c5bf Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 17:29:39 +0000 Subject: [PATCH 119/131] Update uv_python to log unparsed versions in debug mode --- plugins/modules/uv_python.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 6fe2be68ce..e4ea659d84 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -281,7 +281,7 @@ class UV: latest_version = path = "" # 'uv python list' returns versions in descending order but we sort them just in case future uv behavior changes dummy_rc, results, dummy_err = self._list_python(*args) - valid_results = self._parse_versions(results) + valid_results = self._filter_valid_versions(results) if valid_results: version = max(valid_results, key=lambda result: result["parsed_version"]) latest_version = version.get("version", "") @@ -303,15 +303,15 @@ class UV: return [result.get("version") for result in results], [result.get("path") for result in results] return [], [] - @staticmethod - def _parse_versions(results): + def _filter_valid_versions(self, results): valid_results = [] for result in results: + version = result.get("version", "") try: - result["parsed_version"] = StrictVersion(result.get("version", "")) + result["parsed_version"] = StrictVersion(version) valid_results.append(result) except ValueError: - continue + self.module.debug(f"Found {version} available, but it's not yet supported by uv_python module.") return valid_results @staticmethod From f7051feeaf727f56e62179ceebb6cd2f85f96037 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 17:40:57 +0000 Subject: [PATCH 120/131] Clean uv_python module tests --- tests/integration/targets/uv_python/tasks/main.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integration/targets/uv_python/tasks/main.yaml b/tests/integration/targets/uv_python/tasks/main.yaml index 9ae3c967ed..95531c9e58 100644 --- a/tests/integration/targets/uv_python/tasks/main.yaml +++ b/tests/integration/targets/uv_python/tasks/main.yaml @@ -12,10 +12,6 @@ ansible.builtin.pip: name: uv -- name: Add uv installation directory to PATH in macOS - shell: export PATH="$(python3 -m site --user-base)/bin:$PATH" - when: ansible_facts['os_family'] == "Darwin" - - name: Check if Python 3.14 exists already command: uv python find 3.14 ignore_errors: true From 4cb5b0e143e734199f22598ccb833a0865bfcf5b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Tue, 10 Mar 2026 18:18:31 +0000 Subject: [PATCH 121/131] Refactor uv_python code --- plugins/modules/uv_python.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e4ea659d84..e7c857883b 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -333,14 +333,17 @@ def main(): result = dict(changed=False, stdout="", stderr="", rc=0, python_versions=[], python_paths=[], failed=False) state = module.params["state"] - + exec_result = {} uv = UV(module) + if state == "present": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.install_python() + exec_result = dict(zip(["changed", "stdout", "stderr", "rc", "python_versions", "python_paths"], uv.install_python())) elif state == "absent": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.uninstall_python() + exec_result = dict(zip(["changed", "stdout", "stderr", "rc", "python_versions", "python_paths"], uv.uninstall_python())) elif state == "latest": - result["changed"], result["stdout"], result["stderr"], result["rc"], result["python_versions"], result["python_paths"] = uv.upgrade_python() + exec_result = dict(zip(["changed", "stdout", "stderr", "rc", "python_versions", "python_paths"], uv.upgrade_python())) + + result.update(exec_result) module.exit_json(**result) From ba49bc71c1e73c44df1c9b733a5e08dad4c0e2a3 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 13:53:12 +0000 Subject: [PATCH 122/131] Clean docstrings in Update plugins/modules/uv_python.py Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index e7c857883b..0a23a22773 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -144,7 +144,6 @@ class UV: Runs command 'uv python install X.Y.Z' which installs specified python version. If patch version is not specified uv installs latest available patch version. Returns: - tuple [bool, str, str, int, list, list] - boolean to indicate if method changed state - command's stdout - command's stderr From ec3be22b2383a7c37c81eeca2115faaa98a6bc16 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 13:55:01 +0000 Subject: [PATCH 123/131] Remove uv python label in .github/BOTMETA.yml Co-authored-by: Felix Fontein --- .github/BOTMETA.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 9be80a1133..d22b857334 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1426,7 +1426,6 @@ files: keywords: sophos utm maintainers: $team_e_spirit RickS-C137 $modules/uv_python.py: - labels: uv python keywords: uv python maintainers: mriamah $modules/vdo.py: From ae57b2b809aa9377f971141b28915fffb1cd00f0 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 13:58:30 +0000 Subject: [PATCH 124/131] Add typing to plugins/modules/uv_python.py Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 0a23a22773..404fbefb77 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -128,7 +128,7 @@ class UV: ) ) - def _ensure_min_uv_version(self): + def _ensure_min_uv_version(self) -> None: cmd = [self.bin_path, "--version", "--color", "never"] dummy_rc, out, dummy_err = self.module.run_command(cmd, check_rc=True) detected = out.strip().split()[-1] From 281b072aed9e7e9eee5e78250ea2e5161d645476 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 14:11:49 +0000 Subject: [PATCH 125/131] Add typing in plugins/modules/uv_python.py Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 404fbefb77..b79f23c3a9 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -113,7 +113,7 @@ class UV: Module for managing Python versions and installations using "uv python" command """ - def __init__(self, module): + def __init__(self, module: AnsibleModule) -> None: self.module = module self.bin_path = self.module.get_bin_path("uv", required=True) self._ensure_min_uv_version() From 532c92d760ac537e186aa8936c3d56f61a9dde0d Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 14:12:24 +0000 Subject: [PATCH 126/131] Add typing in plugins/modules/uv_python.py Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index b79f23c3a9..980856e917 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -139,7 +139,7 @@ class UV: required_version=MINIMUM_UV_VERSION, ) - def install_python(self) -> tuple[bool, str, str, int, list, list]: + def install_python(self) -> tuple[bool, str, str, int, list[str], list[str]]: """ Runs command 'uv python install X.Y.Z' which installs specified python version. If patch version is not specified uv installs latest available patch version. From b0555c9679a76f825eddf89ba1575aa17860135b Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 14:18:39 +0000 Subject: [PATCH 127/131] Fix uv python documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/uv_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 980856e917..46ac27dfcb 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -11,7 +11,7 @@ description: - Install, uninstall or upgrade Python versions managed by C(uv). version_added: "12.5.0" requirements: - - uv must be installed and available in PATH and uv version must be at least 0.8.0. + - C(uv) must be installed and available in E(PATH) and must be at least 0.8.0. - Python version must be at least 3.9. extends_documentation_fragment: - community.general.attributes From e774e5f0b3c3f2bc1320ffd689a9fa00839d8a51 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 14:22:59 +0000 Subject: [PATCH 128/131] Add future annotations Co-authored-by: Felix Fontein --- plugins/modules/uv_python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 46ac27dfcb..817b31f95b 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -3,6 +3,7 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import annotations DOCUMENTATION = r""" module: uv_python From 0a2eb0a6f8fc4fa7551b7523a75a49ab3929ed78 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 14:32:48 +0000 Subject: [PATCH 129/131] Fix uv_python module documentation --- plugins/modules/uv_python.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/modules/uv_python.py b/plugins/modules/uv_python.py index 817b31f95b..2def83cbdf 100644 --- a/plugins/modules/uv_python.py +++ b/plugins/modules/uv_python.py @@ -83,22 +83,31 @@ python_versions: description: List of Python versions changed. returned: success type: list + elements: str + sample: + - "3.13.5" python_paths: description: List of installation paths of Python versions changed. returned: success type: list + elements: str + sample: + - "/root/.local/share/uv/python/cpython-3.13.5-linux-x86_64-gnu/bin/python3.13" stdout: description: Stdout of the executed command. returned: success type: str + sample: "" stderr: description: Stderr of the executed command. returned: success type: str + sample: "" rc: description: Return code of the executed command. returned: success type: int + sample: 0 """ import json From 990c0a5eaf97c41de97de559a0314ec999212660 Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 11 Mar 2026 15:01:03 +0000 Subject: [PATCH 130/131] Allow testing using Python versions lower or equal to 3.8 --- tests/integration/targets/uv_python/aliases | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/targets/uv_python/aliases b/tests/integration/targets/uv_python/aliases index b216733e68..1e6d733593 100644 --- a/tests/integration/targets/uv_python/aliases +++ b/tests/integration/targets/uv_python/aliases @@ -4,8 +4,6 @@ azp/posix/2 destructive -skip/python2 -skip/python3.8 skip/windows skip/freebsd skip/rhel From 936854e9e358ca75f226bc670ce9df7a34ff0abb Mon Sep 17 00:00:00 2001 From: Mariam Ahhttouche Date: Wed, 18 Mar 2026 15:28:56 +0100 Subject: [PATCH 131/131] skip running ci tests in macos Co-authored-by: Felix Fontein --- tests/integration/targets/uv_python/aliases | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/targets/uv_python/aliases b/tests/integration/targets/uv_python/aliases index 1e6d733593..df78a209c4 100644 --- a/tests/integration/targets/uv_python/aliases +++ b/tests/integration/targets/uv_python/aliases @@ -4,6 +4,7 @@ azp/posix/2 destructive +skip/macos # TODO executables are installed in /Library/Frameworks/Python.framework/Versions/3.x/bin, which isn't part of $PATH skip/windows skip/freebsd skip/rhel