mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-06-05 07:47:12 +00:00
Add module to manage Python versions using uv (#11537)
* Add minimal uv_python module uv_python module: add integration tests * uv_python module: handle absent state uv_python module: add integration tests * uv_python module: restrict accepted version formats to X.Y and X.Y.Z uv_python module: add integration tests for version format * uv_python module: add _list_python and _get_latest_patch_release methods * uv_python module: add support for latest state uv_python module: add integration tests for latest state * uv_python module: add integration tests for check mode * uv_python module: improve latest state check mode to show version that will be installed * uv_python module: make latest state more deterministic by using install with explicite version * uv_python module: improve absent state check mode and add corresponding integration test * uv_python module: update latest state handling to sort versions without relying on uv behavior uv_python module: improve integration tests uv_python module: improve module return values * uv_python module: add integration test for when uv executable does not exist uv_python module: improve exception handling * uv_python module: add integration test for case when specified version does not exist * uv_python module: handle case when provided python version does not exist in latest state uv_python module: improve methods' return values and add docstrings uv_python module: improve integration tests * uv_python module: improve check mode for present state to fail when no patch version is available * uv_python module: return commands' stderr and return code as a variable of stdout * uv_python module: add python version to module return values for present state * uv_python module: add python version to module return values for absent state * uv_python module: add python version to module return values for latest state uv_python module: fix integration tests * uv_python module: add installation paths to return values for present state * uv_python module: add installation paths to return values for absent state * uv_python module: add installation paths to return values for latest state * uv_python module: update present, absent and latest state to only include versions managed by uv in return values uv_python module: improve integration tests uv_python module: update module documentation * uv_python module: use LooseVersion instead of StrictVersion to allow specifying threaded and major python versions * uv_python module: fail module if used uv version is less than the minimal supported version uv_python module: update documentation * uv_python module: add uv command options to executed commands to disable unneeded features * uv_python module: use packaging.version to only accept canonical python versions uv_python module: update integration tests uv_python module: improve error messages * uv_python module: pin uv version used in tests Improve module documentation Co-authored-by: Felix Fontein <felix@fontein.de> Add integration tests' aliases file for uv_python module * Use StrictVersion instead of packaging Version * make integration tests more deterministic Update attributes field in documentation Save uv bin path in an attribute Add another example in documentation Apply PR feedback and refactor code Fix typing to be compatible with python versions <= 3.8 Update example to use quotes for major.minor example and update documentation Update test aliases Use documentation fragment for uv_python attributes * Add aliases to skip running tests on freebsd and rhel Make uv_python tests more deterministic Clean uv_python documentation * Handle case when version given is an empty string in uv_python module * Apply linguistic guidelines for plugins/modules/uv_python.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Add Python version requirement in uv_python documentation * Update tests to install uv using pip and fix some tests Add typing to plugins/modules/uv_python.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Update plugins/modules/uv_python.py documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Add task to uv_python tests to add uv installation directory to PATH * Update uv_python to log unparsed versions in debug mode Refactor uv_python code Remove uv python label in .github/BOTMETA.yml Co-authored-by: Felix Fontein <felix@fontein.de> Add typing to plugins/modules/uv_python.py Co-authored-by: Felix Fontein <felix@fontein.de> Fix uv python documentation Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Allow testing using Python versions lower or equal to 3.8 skip running ci tests in macos Co-authored-by: Felix Fontein <felix@fontein.de> * Make uv version check more resilient to cli output format change in uv_python module Improve tests/integration/targets/uv_python/tasks/main.yaml Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> fix mypy union attr error * update uv_python tests to run on rhel and freebsd Update uv_python tests to use generic packge manager to install uv Install uv with curl for freebsd in uv python tests Install rust needed by uv in freebsd for uv python tests Update up_python tests to fix uv installation path in RHEL skip testing uv_python on freebsd as it only has tier 3 support by uv Fix fragment name in uv_python.py Co-authored-by: Felix Fontein <felix@fontein.de> Update version_added in plugins/modules/uv_python.py Co-authored-by: Felix Fontein <felix@fontein.de> Update tests/integration/targets/uv_python/tasks/main.yaml Co-authored-by: Felix Fontein <felix@fontein.de> Make version shown on debug message more precise in uv_python module * Handle case when uv python returns relative paths instead of absolute in uv_python module Add more typing to uv_python module * uv_python module: update debug message fo unsupported versions to be more clear --------- Co-authored-by: Mariam Ahhttouche <mariam.ahhttouche@etu.univ-grenoble-alpes.fr> Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
2d8e6cb851
commit
c1c389e684
4 changed files with 729 additions and 0 deletions
3
.github/BOTMETA.yml
vendored
3
.github/BOTMETA.yml
vendored
|
|
@ -1422,6 +1422,9 @@ files:
|
|||
$modules/utm_proxy_exception.py:
|
||||
keywords: sophos utm
|
||||
maintainers: $team_e_spirit RickS-C137
|
||||
$modules/uv_python.py:
|
||||
keywords: uv python
|
||||
maintainers: mriamah
|
||||
$modules/vdo.py:
|
||||
maintainers: rhawalsh bgurney-rh
|
||||
$modules/vertica_:
|
||||
|
|
|
|||
416
plugins/modules/uv_python.py
Normal file
416
plugins/modules/uv_python.py
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright (c) 2026 Mariam Ahhttouche <mariam.ahhttouche@gmail.com>
|
||||
# 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
|
||||
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: "13.0.0"
|
||||
requirements:
|
||||
- C(uv) must be installed and available in E(PATH) and must be at least 0.8.0.
|
||||
extends_documentation_fragment:
|
||||
- community.general._attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
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 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 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 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. 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).
|
||||
type: str
|
||||
choices: [present, absent, latest]
|
||||
default: present
|
||||
seealso:
|
||||
- name: C(uv) documentation
|
||||
description: Python versions management with C(uv).
|
||||
link: https://docs.astral.sh/uv/concepts/python-versions/
|
||||
- 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)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Install Python 3.14
|
||||
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
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
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
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.compat.version import LooseVersion, StrictVersion
|
||||
|
||||
MINIMUM_UV_VERSION = "0.8.0"
|
||||
|
||||
|
||||
class UV:
|
||||
"""
|
||||
Module for managing Python versions and installations using "uv python" command
|
||||
"""
|
||||
|
||||
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()
|
||||
try:
|
||||
self.python_version = StrictVersion(module.params["version"])
|
||||
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."
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
try:
|
||||
detected = re.search(r"\b\d+(?:\.\d+)+\b", out).group() # type: ignore[union-attr]
|
||||
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,
|
||||
)
|
||||
except AttributeError:
|
||||
self.module.warn("Could not get installed uv version, skipping uv version check")
|
||||
|
||||
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.
|
||||
Returns:
|
||||
- boolean to indicate if method changed state
|
||||
- command's stdout
|
||||
- command's stderr
|
||||
- command's return code
|
||||
- list of installed versions
|
||||
- list of installation paths for each installed version
|
||||
"""
|
||||
find_rc, existing_version, dummy_err = self._find_python("--show-version")
|
||||
if find_rc == 0:
|
||||
dummy_rc, version_path, dummy_err = self._find_python()
|
||||
return False, "", "", 0, [existing_version], [version_path]
|
||||
if self.module.check_mode:
|
||||
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."))
|
||||
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) -> 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.
|
||||
Returns:
|
||||
tuple [bool, str, str, int, list, list]
|
||||
- boolean to indicate if method changed state
|
||||
- command's stdout
|
||||
- command's stderr
|
||||
- command's return code
|
||||
- list of uninstalled versions
|
||||
- list of previous installation paths for each uninstalled version
|
||||
"""
|
||||
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) -> tuple[bool, str, str, int, list, list]:
|
||||
"""
|
||||
Runs command 'uv python install X.Y.Z' with latest patch version available.
|
||||
Returns:
|
||||
- boolean to indicate if method changed state
|
||||
- command's stdout
|
||||
- command's stderr
|
||||
- command's return code
|
||||
- list of installed versions
|
||||
- list of installation paths for each installed version
|
||||
"""
|
||||
rc, installed_version_str, dummy_err = self._find_python("--show-version")
|
||||
installed_version = self._parse_version(installed_version_str)
|
||||
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):
|
||||
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], []
|
||||
# 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: str, command: str, *args, check_rc: bool = False) -> tuple[int, str, str]:
|
||||
"""
|
||||
Execute a uv python subcommand.
|
||||
Args:
|
||||
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.
|
||||
"""
|
||||
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: bool = 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
|
||||
if inside a virtualenv otherwise it returns latest installed patch version.
|
||||
Args:
|
||||
*args: Additional positional arguments passed to _exec.
|
||||
check_rc (bool): Whether to fail if the command exits with 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: bool = 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
|
||||
Args:
|
||||
*args: Additional positional arguments passed to _exec.
|
||||
check_rc (bool): Whether to fail if the command exits with 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:
|
||||
pythons_installed = json.loads(out)
|
||||
# convert path to absolute path since in some recent uv releases "uv python list" returns relative install paths instead of absolute paths
|
||||
for result in pythons_installed:
|
||||
path = result.get("path", "")
|
||||
if path and not Path(path).is_absolute():
|
||||
result["path"] = str(Path(path).resolve())
|
||||
except json.decoder.JSONDecodeError:
|
||||
self.module.debug("No Python installation found.")
|
||||
return rc, pythons_installed, err
|
||||
|
||||
def _get_latest_patch_release(self, *args) -> tuple[str, str]:
|
||||
"""
|
||||
Returns latest available patch release for a given python version.
|
||||
Args:
|
||||
*args: Additional positional arguments passed to _list_python.
|
||||
Returns:
|
||||
- latest found patch version in format X.Y.Z
|
||||
- installation path of latest patch version if version exists
|
||||
"""
|
||||
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._filter_valid_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) -> tuple[list, list]:
|
||||
"""
|
||||
Returns installed patch releases for a given python version.
|
||||
Args:
|
||||
*args: Additional positional arguments passed to _list_python.
|
||||
Returns:
|
||||
- list of latest found patch versions
|
||||
- list of installation paths of installed versions
|
||||
"""
|
||||
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 [], []
|
||||
|
||||
def _filter_valid_versions(self, results: list):
|
||||
valid_results = []
|
||||
for result in results:
|
||||
version = result.get("version", "")
|
||||
try:
|
||||
result["parsed_version"] = StrictVersion(version)
|
||||
valid_results.append(result)
|
||||
except ValueError:
|
||||
self.module.debug(f"Filtering out version {version!r} since it's not supported by uv_python module.")
|
||||
return valid_results
|
||||
|
||||
@staticmethod
|
||||
def _parse_version(version_str: str):
|
||||
try:
|
||||
return StrictVersion(version_str)
|
||||
except ValueError:
|
||||
return StrictVersion("0")
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
version=dict(type="str", required=True),
|
||||
state=dict(type="str", default="present", choices=["present", "absent", "latest"]),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
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":
|
||||
exec_result = dict(
|
||||
zip(
|
||||
[
|
||||
"changed",
|
||||
"stdout",
|
||||
"stderr",
|
||||
"rc",
|
||||
"python_versions",
|
||||
"python_paths",
|
||||
],
|
||||
uv.install_python(),
|
||||
)
|
||||
)
|
||||
elif state == "absent":
|
||||
exec_result = dict(
|
||||
zip(
|
||||
[
|
||||
"changed",
|
||||
"stdout",
|
||||
"stderr",
|
||||
"rc",
|
||||
"python_versions",
|
||||
"python_paths",
|
||||
],
|
||||
uv.uninstall_python(),
|
||||
)
|
||||
)
|
||||
elif state == "latest":
|
||||
exec_result = dict(
|
||||
zip(
|
||||
[
|
||||
"changed",
|
||||
"stdout",
|
||||
"stderr",
|
||||
"rc",
|
||||
"python_versions",
|
||||
"python_paths",
|
||||
],
|
||||
uv.upgrade_python(),
|
||||
)
|
||||
)
|
||||
|
||||
result.update(exec_result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
tests/integration/targets/uv_python/aliases
Normal file
8
tests/integration/targets/uv_python/aliases
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
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/freebsd
|
||||
302
tests/integration/targets/uv_python/tasks/main.yaml
Normal file
302
tests/integration/targets/uv_python/tasks/main.yaml
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
---
|
||||
####################################################################
|
||||
# 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 python package
|
||||
ansible.builtin.pip:
|
||||
name: uv
|
||||
|
||||
- name: Install uv in RHEL
|
||||
when: ansible_facts['os_family'] == "RedHat"
|
||||
shell: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
environment:
|
||||
UV_INSTALL_DIR: "{{ ansible_env.HOME }}/ansible/bin"
|
||||
args:
|
||||
creates: "{{ ansible_env.HOME }}/ansible/bin/uv"
|
||||
|
||||
- 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:
|
||||
version: "3.14"
|
||||
state: present
|
||||
check_mode: true
|
||||
register: install_check_mode
|
||||
|
||||
- name: Verify Python 3.14 installation in check mode
|
||||
assert:
|
||||
that:
|
||||
- (install_check_mode is changed) == (check_python_314_exists is failed)
|
||||
- install_check_mode is not failed
|
||||
- install_check_mode.python_versions | length >= 1
|
||||
|
||||
- name: Install Python 3.14
|
||||
uv_python:
|
||||
version: "3.14"
|
||||
state: present
|
||||
register: install_python
|
||||
|
||||
- name: Verify Python 3.14 installation
|
||||
assert:
|
||||
that:
|
||||
- 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
|
||||
|
||||
- name: Get Python install path stats
|
||||
ansible.builtin.stat:
|
||||
path: "{{ install_python.python_paths[0] }}"
|
||||
follow: true
|
||||
register: python_path_stats
|
||||
|
||||
- name: Verify that Python 3.14 install path exists on host
|
||||
ansible.builtin.fail:
|
||||
msg: "Python install path {{ install_python.python_paths[0] }} is not correct"
|
||||
when: not python_path_stats.stat.exists
|
||||
|
||||
- name: Re-install Python 3.14
|
||||
uv_python:
|
||||
version: "3.14"
|
||||
state: present
|
||||
register: reinstall_python
|
||||
|
||||
- name: Verify Python 3.14 re-installation
|
||||
assert:
|
||||
that:
|
||||
- reinstall_python is not changed
|
||||
- reinstall_python is not failed
|
||||
- reinstall_python.python_versions | length >= 1
|
||||
- reinstall_python.python_paths | length >= 1
|
||||
|
||||
- name: Check if Python 3.13.5 exists already
|
||||
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:
|
||||
version: 3.13.5
|
||||
register: install_python_3135
|
||||
|
||||
- name: Verify Python 3.13.5 installation
|
||||
assert:
|
||||
that:
|
||||
- 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:
|
||||
version: 3.13.5
|
||||
state: present
|
||||
register: reinstall_python
|
||||
|
||||
- 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
|
||||
uv_python:
|
||||
version: "3.10"
|
||||
state: absent
|
||||
register: remove_python
|
||||
|
||||
- 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
|
||||
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
|
||||
assert:
|
||||
that:
|
||||
- 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
|
||||
|
||||
- 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 == 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'
|
||||
|
||||
- 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
|
||||
assert:
|
||||
that:
|
||||
- remove_python.changed is false
|
||||
- remove_python.failed is false
|
||||
- remove_python.python_versions | length == 0
|
||||
- remove_python.python_paths | length == 0
|
||||
|
||||
- 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
|
||||
- upgrade_python.python_versions | length >= 1
|
||||
- upgrade_python.python_paths | length >= 1
|
||||
|
||||
- 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
|
||||
assert:
|
||||
that:
|
||||
- upgrade_python.changed is false
|
||||
- upgrade_python.failed is false
|
||||
- upgrade_python.python_versions | length >= 1
|
||||
|
||||
- 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
|
||||
assert:
|
||||
that:
|
||||
- upgrade_python.changed is false
|
||||
- upgrade_python.failed is false
|
||||
- upgrade_python.python_versions | length >= 1
|
||||
|
||||
- name: Install unsupported Python version in check mode
|
||||
uv_python:
|
||||
version: 2.0
|
||||
state: present
|
||||
register: unsupported_python_check_mode
|
||||
ignore_errors: true
|
||||
check_mode: true
|
||||
|
||||
- name: Verify unsupported Python 2.0 install in check mode
|
||||
assert:
|
||||
that:
|
||||
- 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 unsupported Python version
|
||||
uv_python:
|
||||
state: present
|
||||
version: 2.0
|
||||
register: unsupported_python
|
||||
ignore_errors: true
|
||||
|
||||
- name: Verify unsupported Python 2.0 install
|
||||
assert:
|
||||
that:
|
||||
- unsupported_python.changed is false
|
||||
- unsupported_python.failed is true
|
||||
|
||||
- name: Install unsupported Python version with latest state
|
||||
uv_python:
|
||||
version: "2.0"
|
||||
state: latest
|
||||
register: unsupported_python
|
||||
ignore_errors: true
|
||||
|
||||
- name: Verify Python 2.0 install with latest state
|
||||
assert:
|
||||
that:
|
||||
- unsupported_python.changed is false
|
||||
- unsupported_python.failed is true
|
||||
|
||||
- name: Delete Python 3.13 version
|
||||
uv_python:
|
||||
version: "3.13"
|
||||
state: absent
|
||||
register: uninstall_result
|
||||
|
||||
- name: Verify Python 3.13 deletion
|
||||
assert:
|
||||
that:
|
||||
- uninstall_result.changed is true
|
||||
- uninstall_result.failed is false
|
||||
- uninstall_result.python_versions | length >= 1
|
||||
- '"3.13" in uninstall_result.python_versions[0]'
|
||||
|
||||
- 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
|
||||
- '"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'
|
||||
Loading…
Add table
Add a link
Reference in a new issue