diff --git a/src/pyinfra/facts/util/packaging.py b/src/pyinfra/facts/util/packaging.py index 017d23cf2..b196e67ca 100644 --- a/src/pyinfra/facts/util/packaging.py +++ b/src/pyinfra/facts/util/packaging.py @@ -3,8 +3,10 @@ import re from typing import Iterable +PackageVersionDict = dict[str, set[str]] -def parse_packages(regex: str, output: Iterable[str]) -> dict[str, set[str]]: + +def parse_packages(regex: str, output: Iterable[str]) -> PackageVersionDict: packages: dict[str, set[str]] = {} for line in output: diff --git a/src/pyinfra/facts/uv.py b/src/pyinfra/facts/uv.py new file mode 100644 index 000000000..16af7613e --- /dev/null +++ b/src/pyinfra/facts/uv.py @@ -0,0 +1,294 @@ +"""uv Facts.""" + +import json +from collections.abc import Iterable +from operator import itemgetter +from typing import cast + +from typing_extensions import override + +from pyinfra import logger +from pyinfra.api import StringCommand +from pyinfra.api.facts import FactBase +from pyinfra.facts.util.packaging import PackageVersionDict + +# See https://docs.astral.sh/uv/ for full details on UV + +UV_CMD = "uv" +MANAGED_PYTHON = "--managed-python" +ONLY_INSTALLED = "--only-installed" + + +def process_json(command: str, output: Iterable[str], key: str, value: str) -> PackageVersionDict: + try: + info = json.loads("\n".join(output)) + except json.decoder.JSONDecodeError: + logger.warning(f"{command} json load failed, output was: '{output}") + info = [] + if not isinstance(info, list): + logger.warning(f"{command} produced unexpected output: '{output}") + info = [] + + return {v[key]: {v[value]} for v in sorted(info, key=itemgetter(key))} + + +class UvPipPackages(FactBase[PackageVersionDict]): + """ + Provides the installed python packages and their version. + + **Example:** + + .. code:: python + + { + "requests": ["2.32.5"], + } + """ + + @override + @staticmethod + def default() -> PackageVersionDict: + return cast("PackageVersionDict", dict) + + @override + def requires_command(self, *args, **kwargs) -> str | None: + return UV_CMD + + @override + def command(self) -> StringCommand | str: + return f"{UV_CMD} pip list --format json" + + @override + def process(self, output: Iterable[str]) -> PackageVersionDict: + return process_json(str(self.command()), output, "name", "version") + + +class UvAvailablePythonsByImplementation(FactBase[PackageVersionDict]): + """ + Provides the implementation(s) of python available for installation along with the versions(s) + of the implementation(s). + + + is_managed: if set, only list python implementations managed by `uv`. Default True + + **Example:** + + .. code:: python + + { + "cpython-3.13.4-macos-aarch64-none": ["3.13.4"] + } + """ + + are_installed = False + + @override + @staticmethod + def default() -> PackageVersionDict: + return cast("PackageVersionDict", dict) + + @override + def requires_command(self, *args, **kwargs) -> str: + return UV_CMD + + @override + def command(self, is_managed: bool = True) -> StringCommand | str: + managed = MANAGED_PYTHON if is_managed else "" + installed = ONLY_INSTALLED if self.are_installed else "" + self.cmd = f"{UV_CMD} python list {managed} {installed} --output-format=json" + return self.cmd + + @override + def process(self, output: Iterable[str]) -> PackageVersionDict: + return process_json(str(self.cmd), output, "key", "version") + + +class UvAvailablePythonsByVersion(UvAvailablePythonsByImplementation): + """ + Provides the version(s) of python available for installation along with the implementation(s) + of the version(s). + + + is_managed: if set, only list python implementations managed by `uv`. Default True + + **Example:** + + .. code:: python + + { + "3.13.4": ["cpython-3.13.4-macos-aarch64-none"] + } + """ + + @override + def process(self, output: Iterable[str]) -> PackageVersionDict: + return process_json(str(self.cmd), output, "version", "key") + + +class UvInstalledPythonsByImplementation(UvAvailablePythonsByImplementation): + """ + Provides the installed implementation(s) of python along with the versions(s) of + the implementation(s). + + + is_managed: if set, only list python implementations managed by `uv`. Default True + + **Example:** + + .. code:: python + + { + "cpython-3.13.4-macos-aarch64-none": ["3.13.4"] + } + """ + + are_installed = True + + @override + def process(self, output: Iterable[str]) -> PackageVersionDict: + return process_json(str(self.cmd), output, "key", "version") + + +class UvInstalledPythonsByVersion(UvInstalledPythonsByImplementation): + """ + Provides the installed versions of python along with the implementation(s) of + the version(s). + + + is_managed: if set, only list python versions managed by `uv`. Default True + + **Example:** + + .. code:: python + + { + "3.13.4": ["cpython-3.13.4-macos-aarch64-none"] + } + """ + + @override + def process(self, output: Iterable[str]) -> PackageVersionDict: + return process_json(str(self.cmd), output, "version", "key") + + +class UvPythonDir(FactBase[str]): + """ + Provides the directory in which uv installs tools + + **Example:** + + .. code:: python + + /home/someone/.local/share/uv/python + """ + + @override + @staticmethod + def default() -> str: + return "" + + @override + def requires_command(self, *args, **kwargs) -> str: + return UV_CMD + + @override + def command(self) -> StringCommand | str: + return f"{UV_CMD} python dir 2>/dev/null" + + @override + def process(self, output: Iterable[str]) -> str: + output_iter = iter(output) + if ((result := next(output_iter, None)) is not None) and (next(output_iter, None) is None): + return result + + logger.warning(f"ignoring unexpected output from {self.command()}: '{output}'") + return "" + + +class UvToolDir(UvPythonDir): + """ + Provides the directory in which uv installs tools + + **Example:** + + .. code:: python + + /home/someone/.local/share/uv/tools + """ + + @override + def command(self) -> StringCommand | str: + return f"{UV_CMD} tool dir 2>/dev/null" + + +class UvTools(FactBase[PackageVersionDict]): + """ + Provides the tool(s) currently installed along with their version. + + **Example:** + + .. code:: python + + { + "pyinfra": "3.4.1" + } + """ + + # the output is in line pairs: + # pyinfra v3.4.1 + # - pyinfra + + @override + @staticmethod + def default() -> PackageVersionDict: + return cast("PackageVersionDict", dict) + + @override + def requires_command(self, *args, **kwargs) -> str: + return UV_CMD + + @override + def command(self) -> StringCommand | str: + return f"{UV_CMD} tool list 2>/dev/null" + + # TODO - use JSON when available (https://github.com/astral-sh/uv/issues/10219) + @override + def process(self, output: Iterable[str]) -> PackageVersionDict: + result = {} + for line in output: + if line.startswith("- "): + continue # skip the names of the commands that have been added + if len(pieces := line.split(" ")) > 1: + result[pieces[0]] = {pieces[1]} + else: + logger.warning(f"ignoring unexpected output from {self.command()}: {line}") + return result + + +class UvVersion(FactBase[str]): + """ + Provides the version of uv itself. + + **Example:** + + .. code:: python + + uv 0.8.5 (Homebrew 2025-08-05) + """ + + @override + @staticmethod + def default() -> str: + return "" + + @override + def requires_command(self, *args, **kwargs) -> str: + return UV_CMD + + @override + def command(self) -> str | StringCommand: + return f"{UV_CMD} --version" + + @override + def process(self, output: Iterable[str]) -> str: + output_iter = iter(output) + if ((result := next(output_iter, None)) is not None) and (next(output_iter, None) is None): + return result + logger.warning(f"ignoring unexpected output from '{self.command()}': '{output}'") + return "" diff --git a/src/pyinfra/operations/uv.py b/src/pyinfra/operations/uv.py new file mode 100644 index 000000000..16bfebe71 --- /dev/null +++ b/src/pyinfra/operations/uv.py @@ -0,0 +1,286 @@ +"""Operations for uv.""" + +from __future__ import annotations + +from pyinfra import host +from pyinfra.api.command import QuoteString, StringCommand +from pyinfra.api.operation import operation +from pyinfra.facts import server +from pyinfra.facts.files import File +from pyinfra.facts.uv import ( + MANAGED_PYTHON, + UV_CMD, + UvInstalledPythonsByVersion, + UvPipPackages, + UvToolDir, + UvTools, +) +from pyinfra.operations import files +from pyinfra.operations.util.packaging import PkgInfo, ensure_packages + +# see (see https://docs.astral.sh/uv/) for full details on uv + +VENV = ".venv" +VENV_INDICATOR = f"{VENV}/bin/activate" + + +@operation() +def packages( + packages: str | list[str], + *, + requirements: str | None = None, + present: bool = True, + latest: bool = False, + extra_args: str | list[str] | None = None, +): + """ + Install/remove/update python packages using ``uv pip`` + + + packages: a package or list of packages to install/uninstall + + requirements: path to a requirements file to install/uninstall + + present: whether the packages should be installed or uninstalled + + latest: whether to upgrade packages for which a version was not specified + + extra_args: zero or more additional arguments to the ``uv pip`` command + + Python Package versions: + Package versions can be specified in the `usual manner`_, e.g. ``==``. + + .. _usual manner: https://pip.pypa.io/en/stable/reference/requirement-specifiers/ + + **Example:** + + .. code:: python + + uv.packages( + name="Install Requests", + packages=["requests"], + ) + """ + extras = " ".join(extra_args or []) if not isinstance(extra_args, str) else extra_args + install_command = f"{UV_CMD} pip install --no-progress {extras}" + uninstall_command = f"{UV_CMD} pip uninstall --no-progress {extras}" + upgrade_command = f"{UV_CMD} pip install --upgrade --no-progress {extras}" + + # (un)Install requirements + if requirements: + if present: + yield f"{upgrade_command if latest else install_command} -r {requirements}" + else: + yield f"{uninstall_command} -r {requirements}" + + if packages: + if isinstance(packages, str): + packages = [packages] + # PEP-0426 states that Python packages should be compared using lowercase, so lowercase the + # current packages. PkgInfo.from_pep508 takes care of the package name + current_packages = { + pkg.lower(): version for pkg, version in host.get_fact(UvPipPackages).items() + } + + yield from ensure_packages( + host, + list(filter(None, (PkgInfo.from_pep508(package) for package in packages))), + current_packages, + present, + install_command=install_command, + uninstall_command=uninstall_command, + upgrade_command=upgrade_command, + latest=latest, + ) + + if (not requirements) and (not packages): + host.noop("neither packages nor requirements requested to be (un)installed") + + +@operation() +def pythons( + versions: str | list[str], + *, + present: bool = True, + managed: bool = True, + extra_args: str | list[str] | None = None, +): + """ + Install/remove python version(s). + + + version: a version or list of python versions + + present: whether the versions should be installed or removed. Default True. + + managed: whether only managed or all available version should be considered. Default True. + + extra_args: zero or more additional arguments to be passed to ``uv``. Default None. + + **Example:** + + .. code:: python + + uv.tool( + name="Install Python 3.13 and 3.14", + versions=["3.14", "3.13"], + present=True + ) + """ + extras = " ".join(extra_args or []) if not isinstance(extra_args, str) else extra_args + mgd_str = MANAGED_PYTHON if managed else "" + install_command = f"{UV_CMD} python install --no-progress {mgd_str} {extras}" + uninstall_command = f"{UV_CMD} python uninstall --no-progress {mgd_str} {extras}" + current_versions = host.get_fact(UvInstalledPythonsByVersion, managed) + + if versions: + # uv supports only one python version at a time + for version in [versions] if isinstance(versions, str) else versions: + yield from ensure_packages( + host, + [version], + current_versions, + present, + install_command=install_command, + uninstall_command=uninstall_command, + ) + else: + host.noop("no python versions requested to be (un)installed") + + +@operation() +def tools( + tools: str | list[str], + *, + present: bool = True, + latest: bool = False, + extra_args: str | list[str] | None = None, +): + """ + Install/remove/update packages using ``uv tool``. + + + tools: a package or list of packages to install as tools + + present: whether the packages should be installed or uninstalled. Default True. + + latest: whether or not to upgrade packages without a specified version. Default False. + + extra_args: zero or more additional arguments to ``uv``. Default None. + + Versions: + Tool versions may be, but are not required to be, specified in the + `usual manner`_: ``==``. + + **Example:** + + .. code:: python + + uv.tool( + name="Install Pyinfra", + tools=["pyinfra"], + present=True + ) + """ + extras = " ".join(extra_args or []) if not isinstance(extra_args, str) else extra_args + install_command = f"{UV_CMD} tool install --no-progress {extras}" + uninstall_command = f"{UV_CMD} tool uninstall --no-progress {extras}" + upgrade_command = f"{install_command} --upgrade --no-progress {extras}" + + already_installed = {pkg.lower(): version for pkg, version in host.get_fact(UvTools).items()} + + # uv tool install supports only one package name at a time + if tools: + for tool in [tools] if isinstance(tools, str) else tools: + if (pkg_info := PkgInfo.from_pep508(tool)) is None: + continue # PkgInfo.from_pep508 logged a warning + yield from ensure_packages( + host, + [pkg_info], + already_installed, + present, + install_command=install_command, + uninstall_command=uninstall_command, + upgrade_command=upgrade_command, + latest=latest, + ) + else: + host.noop("no tools requested to be (un)installed") + + +@operation() +def tool_upgrade_all(): + """ + Upgrade all ``uv`` tools. + """ + yield f"{UV_CMD} tool --reinstall --upgrade" + + +@operation() +def tool_update_shell(): + """ + Ensure that the ``uv`` tool executable directory is on the `PATH`. + """ + if host.get_fact(UvToolDir) in host.get_fact(server.Path).split(":"): + host.noop(f"{UV_CMD} bin dir is already in the PATH") + else: + yield f"{UV_CMD} tool update-shell" + + +@operation() +def venv( + path: str, + *, + present: bool = True, + python: str | None = None, + allow_existing: bool = False, + clear_existing: bool = False, + link_mode: str | None = None, + seed: bool = False, + site_packages: bool = False, + extra_args: str | list[str] | None = None, +): + """ + Add/remove a Python venv in the specified location. + + + path: where to create the venv + + present: whether the virtualenv should exist. Default True + + python: python interpreter to use. Defaults to not specified. + + allow_existing: allow (and retain) existing files in the venv directory. Default False + + clear_existing: remove all existing files in the venv directory. Default False + + seed: whether to seed the venv with `pip`, and pre-3.12, `setuptools` and `wheel`. + Default False + + site_packages: give the venv access to the global site-packages. Default False + + link-mode: method to use when installing seed packages: clone, copy, hardlink, symlink. + Default is platform-dependent + + extra_args: zero or more additional arguments to the `uv tool` command + + **Example:** + + .. code:: python + + uv.venv( + name="Create a venv", + path="/my/projects/useful-project/.venv", + ) + """ + # https://docs.astral.sh/uv/reference/cli/#uv-venv + + exists = host.get_fact(File, path=f"{path}/{VENV_INDICATOR}") + + if present: + if allow_existing and clear_existing: + raise ValueError("allow_existing and clear_existing cannot both be set") + if len(str(path).strip()) == 0: + raise ValueError("must provide path in which .venv is to be created") + if exists: + host.noop(f"venv already exists at {path}") + else: + cmd_list: list[str | QuoteString] = [UV_CMD, "venv"] + if allow_existing: + cmd_list.append("--allow-existing") + if clear_existing: + cmd_list.append("--clear") + if seed: + cmd_list.append("--seed") + if link_mode: + cmd_list.extend(["--link-mode", link_mode]) + if site_packages: + cmd_list.append("--site-packages") + if python: + cmd_list.extend(["--python", python]) + cmd_list.extend(extra_args or [] if not isinstance(extra_args, str) else [extra_args]) + cmd_list.append(QuoteString(path)) + yield StringCommand(*cmd_list) + else: + if not exists: + host.noop(f"venv does not exist at {path}") + else: + yield from files.directory._inner(f"{path}/{VENV}", present=False) diff --git a/tests/facts/uv.UvAvailablePythonsByImplementation/bad_response.json b/tests/facts/uv.UvAvailablePythonsByImplementation/bad_response.json new file mode 100644 index 000000000..4ead0faa9 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByImplementation/bad_response.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "%$^#^%$^%$^" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByImplementation/empty_response.json b/tests/facts/uv.UvAvailablePythonsByImplementation/empty_response.json new file mode 100644 index 000000000..119273416 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByImplementation/empty_response.json @@ -0,0 +1,9 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByImplementation/not_list_output.json b/tests/facts/uv.UvAvailablePythonsByImplementation/not_list_output.json new file mode 100644 index 000000000..1cdc7e456 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByImplementation/not_list_output.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "{\"foo\":1,\"bar\":2}" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByImplementation/one_version.json b/tests/facts/uv.UvAvailablePythonsByImplementation/one_version.json new file mode 100644 index 000000000..570de9ca2 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByImplementation/one_version.json @@ -0,0 +1,13 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"}]" + ], + "fact": { + "cpython-3.14.0-macos-aarch64-none": [ + "3.14.0" + ] + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByImplementation/three_versions.json b/tests/facts/uv.UvAvailablePythonsByImplementation/three_versions.json new file mode 100644 index 000000000..cb2bfd65d --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByImplementation/three_versions.json @@ -0,0 +1,19 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"},{\"key\": \"cpython-3.15.0-macos-aarch64-none\",\"version\": \"3.15.0\"}, {\"key\": \"cpython-3.16.0-macos-aarch64-none\",\"version\": \"3.16.0\"}]" + ], + "fact": { + "cpython-3.14.0-macos-aarch64-none": [ + "3.14.0" + ], + "cpython-3.15.0-macos-aarch64-none": [ + "3.15.0" + ], + "cpython-3.16.0-macos-aarch64-none": [ + "3.16.0" + ] + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByVersion/bad_response.json b/tests/facts/uv.UvAvailablePythonsByVersion/bad_response.json new file mode 100644 index 000000000..4ead0faa9 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByVersion/bad_response.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "%$^#^%$^%$^" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByVersion/empty_response.json b/tests/facts/uv.UvAvailablePythonsByVersion/empty_response.json new file mode 100644 index 000000000..119273416 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByVersion/empty_response.json @@ -0,0 +1,9 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByVersion/not_list_output.json b/tests/facts/uv.UvAvailablePythonsByVersion/not_list_output.json new file mode 100644 index 000000000..1cdc7e456 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByVersion/not_list_output.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "{\"foo\":1,\"bar\":2}" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByVersion/one_version.json b/tests/facts/uv.UvAvailablePythonsByVersion/one_version.json new file mode 100644 index 000000000..df89a857d --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByVersion/one_version.json @@ -0,0 +1,11 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"}]" + ], + "fact": { + "3.14.0": ["cpython-3.14.0-macos-aarch64-none"] + } +} diff --git a/tests/facts/uv.UvAvailablePythonsByVersion/three_versions.json b/tests/facts/uv.UvAvailablePythonsByVersion/three_versions.json new file mode 100644 index 000000000..772ac44f1 --- /dev/null +++ b/tests/facts/uv.UvAvailablePythonsByVersion/three_versions.json @@ -0,0 +1,19 @@ +{ + "arg": true, + "command": "uv python list --managed-python --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"},{\"key\": \"cpython-3.11.1-macos-aarch64-none\",\"version\": \"3.11.1\"}, {\"key\": \"cpython-3.12.2-macos-aarch64-none\",\"version\": \"3.12.2\"}]" + ], + "fact": { + "3.11.1": [ + "cpython-3.11.1-macos-aarch64-none" + ], + "3.12.2": [ + "cpython-3.12.2-macos-aarch64-none" + ], + "3.14.0": [ + "cpython-3.14.0-macos-aarch64-none" + ] + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByImplementation/bad_response.json b/tests/facts/uv.UvInstalledPythonsByImplementation/bad_response.json new file mode 100644 index 000000000..3d31d6de5 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByImplementation/bad_response.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "%$^#^%$^%$^" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByImplementation/empty_response.json b/tests/facts/uv.UvInstalledPythonsByImplementation/empty_response.json new file mode 100644 index 000000000..6facbe074 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByImplementation/empty_response.json @@ -0,0 +1,9 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByImplementation/not_list_output.json b/tests/facts/uv.UvInstalledPythonsByImplementation/not_list_output.json new file mode 100644 index 000000000..030c7e021 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByImplementation/not_list_output.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "{\"foo\":1,\"bar\":2}" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByImplementation/one_version.json b/tests/facts/uv.UvInstalledPythonsByImplementation/one_version.json new file mode 100644 index 000000000..a2e67271b --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByImplementation/one_version.json @@ -0,0 +1,13 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"}]" + ], + "fact": { + "cpython-3.14.0-macos-aarch64-none": [ + "3.14.0" + ] + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByImplementation/three_versions.json b/tests/facts/uv.UvInstalledPythonsByImplementation/three_versions.json new file mode 100644 index 000000000..ef6f0a5d8 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByImplementation/three_versions.json @@ -0,0 +1,19 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"},{\"key\": \"cpython-3.15.0-macos-aarch64-none\",\"version\": \"3.15.0\"}, {\"key\": \"cpython-3.16.0-macos-aarch64-none\",\"version\": \"3.16.0\"}]" + ], + "fact": { + "cpython-3.14.0-macos-aarch64-none": [ + "3.14.0" + ], + "cpython-3.15.0-macos-aarch64-none": [ + "3.15.0" + ], + "cpython-3.16.0-macos-aarch64-none": [ + "3.16.0" + ] + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByVersion/bad_response.json b/tests/facts/uv.UvInstalledPythonsByVersion/bad_response.json new file mode 100644 index 000000000..3d31d6de5 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByVersion/bad_response.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "%$^#^%$^%$^" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByVersion/empty_response.json b/tests/facts/uv.UvInstalledPythonsByVersion/empty_response.json new file mode 100644 index 000000000..6facbe074 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByVersion/empty_response.json @@ -0,0 +1,9 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByVersion/not_list_output.json b/tests/facts/uv.UvInstalledPythonsByVersion/not_list_output.json new file mode 100644 index 000000000..030c7e021 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByVersion/not_list_output.json @@ -0,0 +1,10 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "{\"foo\":1,\"bar\":2}" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByVersion/one_version.json b/tests/facts/uv.UvInstalledPythonsByVersion/one_version.json new file mode 100644 index 000000000..1829464c2 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByVersion/one_version.json @@ -0,0 +1,11 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"}]" + ], + "fact": { + "3.14.0": ["cpython-3.14.0-macos-aarch64-none"] + } +} diff --git a/tests/facts/uv.UvInstalledPythonsByVersion/three_versions.json b/tests/facts/uv.UvInstalledPythonsByVersion/three_versions.json new file mode 100644 index 000000000..7d9293c62 --- /dev/null +++ b/tests/facts/uv.UvInstalledPythonsByVersion/three_versions.json @@ -0,0 +1,19 @@ +{ + "arg": true, + "command": "uv python list --managed-python --only-installed --output-format=json", + "requires_command": "uv", + "output": [ + "[{\"key\": \"cpython-3.14.0-macos-aarch64-none\",\"version\": \"3.14.0\"},{\"key\": \"cpython-3.11.1-macos-aarch64-none\",\"version\": \"3.11.1\"}, {\"key\": \"cpython-3.12.2-macos-aarch64-none\",\"version\": \"3.12.2\"}]" + ], + "fact": { + "3.11.1": [ + "cpython-3.11.1-macos-aarch64-none" + ], + "3.12.2": [ + "cpython-3.12.2-macos-aarch64-none" + ], + "3.14.0": [ + "cpython-3.14.0-macos-aarch64-none" + ] + } +} diff --git a/tests/facts/uv.UvPipPackages/no_packages.json b/tests/facts/uv.UvPipPackages/no_packages.json new file mode 100644 index 000000000..e58af97ee --- /dev/null +++ b/tests/facts/uv.UvPipPackages/no_packages.json @@ -0,0 +1,8 @@ +{ + "command": "uv pip list --format json", + "requires_command": "uv", + "output": [ + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvPipPackages/one_package.json b/tests/facts/uv.UvPipPackages/one_package.json new file mode 100644 index 000000000..65db9b90a --- /dev/null +++ b/tests/facts/uv.UvPipPackages/one_package.json @@ -0,0 +1,10 @@ +{ + "command": "uv pip list --format json", + "requires_command": "uv", + "output": [ + "[{\"name\": \"pydash\", \"version\": \"3.48\"}]" + ], + "fact": { + "pydash": ["3.48"] + } +} diff --git a/tests/facts/uv.UvPipPackages/three_packages.json b/tests/facts/uv.UvPipPackages/three_packages.json new file mode 100644 index 000000000..5fec7c2a5 --- /dev/null +++ b/tests/facts/uv.UvPipPackages/three_packages.json @@ -0,0 +1,13 @@ +{ + "command": "uv pip list --format json", + "requires_command": "uv", + "output": [ + "[{\"name\": \"pydash\", \"version\": \"3.48\"}, {\"name\": \"pluggy\", \"version\": \"1.60\"}, {\"name\": \"pip\", \"version\": \"25.3\"}]" + + ], + "fact": { + "pydash": ["3.48"], + "pluggy": ["1.60"], + "pip": ["25.3"] + } +} diff --git a/tests/facts/uv.UvPythonDir/extra_line.json b/tests/facts/uv.UvPythonDir/extra_line.json new file mode 100644 index 000000000..56521169b --- /dev/null +++ b/tests/facts/uv.UvPythonDir/extra_line.json @@ -0,0 +1,9 @@ +{ + "command": "uv python dir 2>/dev/null", + "requires_command": "uv", + "output": [ + "/home/guesswho/.local/share/uv/python", + "#%*#&$^$*^" + ], + "fact": "" +} diff --git a/tests/facts/uv.UvPythonDir/normal_response.json b/tests/facts/uv.UvPythonDir/normal_response.json new file mode 100644 index 000000000..ab0b3effe --- /dev/null +++ b/tests/facts/uv.UvPythonDir/normal_response.json @@ -0,0 +1,8 @@ +{ + "command": "uv python dir 2>/dev/null", + "requires_command": "uv", + "output": [ + "/home/guesswho/.local/share/uv/python" + ], + "fact": "/home/guesswho/.local/share/uv/python" +} diff --git a/tests/facts/uv.UvToolDir/extra_line.json b/tests/facts/uv.UvToolDir/extra_line.json new file mode 100644 index 000000000..ef3dbcbdd --- /dev/null +++ b/tests/facts/uv.UvToolDir/extra_line.json @@ -0,0 +1,9 @@ +{ + "command": "uv tool dir 2>/dev/null", + "requires_command": "uv", + "output": [ + "/home/guesswho/.local/share/uv/tools", + "#%*#&$^$*^" + ], + "fact": "" +} diff --git a/tests/facts/uv.UvToolDir/normal_response.json b/tests/facts/uv.UvToolDir/normal_response.json new file mode 100644 index 000000000..38e2414c5 --- /dev/null +++ b/tests/facts/uv.UvToolDir/normal_response.json @@ -0,0 +1,8 @@ +{ + "command": "uv tool dir 2>/dev/null", + "requires_command": "uv", + "output": [ + "/home/guesswho/.local/share/uv/tools" + ], + "fact": "/home/guesswho/.local/share/uv/tools" +} diff --git a/tests/facts/uv.UvTools/missing_version.json b/tests/facts/uv.UvTools/missing_version.json new file mode 100644 index 000000000..1f99d5a3e --- /dev/null +++ b/tests/facts/uv.UvTools/missing_version.json @@ -0,0 +1,9 @@ +{ + "command": "uv tool list 2>/dev/null", + "requires_command": "uv", + "output": [ + "pyinfra" + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvTools/no_tools.json b/tests/facts/uv.UvTools/no_tools.json new file mode 100644 index 000000000..9ae479b1c --- /dev/null +++ b/tests/facts/uv.UvTools/no_tools.json @@ -0,0 +1,8 @@ +{ + "command": "uv tool list 2>/dev/null", + "requires_command": "uv", + "output": [ + ], + "fact": { + } +} diff --git a/tests/facts/uv.UvTools/one_tool.json b/tests/facts/uv.UvTools/one_tool.json new file mode 100644 index 000000000..98fe637a0 --- /dev/null +++ b/tests/facts/uv.UvTools/one_tool.json @@ -0,0 +1,11 @@ +{ + "command": "uv tool list 2>/dev/null", + "requires_command": "uv", + "output": [ + "pyinfra v3.5.1", + "- pyinfra" + ], + "fact": { + "pyinfra": ["v3.5.1"] + } +} diff --git a/tests/facts/uv.UvTools/two_tools.json b/tests/facts/uv.UvTools/two_tools.json new file mode 100644 index 000000000..c42ecaa5c --- /dev/null +++ b/tests/facts/uv.UvTools/two_tools.json @@ -0,0 +1,19 @@ +{ + "command": "uv tool list 2>/dev/null", + "requires_command": "uv", + "output": [ + "colorpedia v1.2.2", + "- color", + "- colorpedia", + "pyinfra v3.5.1", + "- pyinfra" + ], + "fact": { + "colorpedia": [ + "v1.2.2" + ], + "pyinfra": [ + "v3.5.1" + ] + } +} diff --git a/tests/facts/uv.UvVersion/extra_line.json b/tests/facts/uv.UvVersion/extra_line.json new file mode 100644 index 000000000..76a58b252 --- /dev/null +++ b/tests/facts/uv.UvVersion/extra_line.json @@ -0,0 +1,9 @@ +{ + "command": "uv --version", + "requires_command": "uv", + "output": [ + "uv 0.9.8 (Homebrew 2025-11-07)", + "#%*#&$^$*^" + ], + "fact": "" +} diff --git a/tests/facts/uv.UvVersion/normal_response.json b/tests/facts/uv.UvVersion/normal_response.json new file mode 100644 index 000000000..5804f1be1 --- /dev/null +++ b/tests/facts/uv.UvVersion/normal_response.json @@ -0,0 +1,8 @@ +{ + "command": "uv --version", + "requires_command": "uv", + "output": [ + "uv 0.9.8 (Homebrew 2025-11-07)" + ], + "fact": "uv 0.9.8 (Homebrew 2025-11-07)" +} diff --git a/tests/operations/uv.packages/install_3_packages_with_1_existing.json b/tests/operations/uv.packages/install_3_packages_with_1_existing.json new file mode 100644 index 000000000..9b8e5cc72 --- /dev/null +++ b/tests/operations/uv.packages/install_3_packages_with_1_existing.json @@ -0,0 +1,12 @@ +{ + "args": [["ruff", "mypy", "pyrefly"]], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvPipPackages": {"ruff": ["1.2.3"], "mypy": ["4.5.6"]} + }, + "commands": [ + "uv pip install --no-progress pyrefly" + ] +} diff --git a/tests/operations/uv.packages/install_no_packages.json b/tests/operations/uv.packages/install_no_packages.json new file mode 100644 index 000000000..43d2724c9 --- /dev/null +++ b/tests/operations/uv.packages/install_no_packages.json @@ -0,0 +1,12 @@ +{ + "args": [[]], + "kwargs": { + "present": true + }, + "facts": { + }, + "commands": [ + ], + "noop_description": "neither packages nor requirements requested to be (un)installed" + +} diff --git a/tests/operations/uv.packages/install_no_packages_but_requirements.json b/tests/operations/uv.packages/install_no_packages_but_requirements.json new file mode 100644 index 000000000..0654c3cf6 --- /dev/null +++ b/tests/operations/uv.packages/install_no_packages_but_requirements.json @@ -0,0 +1,14 @@ +{ + "args": [null], + "kwargs": { + "requirements": "x.txt", + "present": true, + "latest": true, + "extra_args": ["--foo", "--bar"] + }, + "facts": { + }, + "commands": [ + "uv pip install --upgrade --no-progress --foo --bar -r x.txt" + ] +} diff --git a/tests/operations/uv.packages/install_one_already_existing_package.json b/tests/operations/uv.packages/install_one_already_existing_package.json new file mode 100644 index 000000000..090b1823b --- /dev/null +++ b/tests/operations/uv.packages/install_one_already_existing_package.json @@ -0,0 +1,12 @@ +{ + "args": ["pyinfra"], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvPipPackages": {"pyinfra": ["3.5.1"]} + }, + "commands": [ + ], + "noop_description": "package pyinfra is installed (3.5.1)" +} diff --git a/tests/operations/uv.packages/install_one_not_existing_package.json b/tests/operations/uv.packages/install_one_not_existing_package.json new file mode 100644 index 000000000..f9cfc3a15 --- /dev/null +++ b/tests/operations/uv.packages/install_one_not_existing_package.json @@ -0,0 +1,12 @@ +{ + "args": ["pyinfra"], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvPipPackages": {} + }, + "commands": [ + "uv pip install --no-progress pyinfra" + ] +} diff --git a/tests/operations/uv.packages/remove_3_packages_with_2_installed.json b/tests/operations/uv.packages/remove_3_packages_with_2_installed.json new file mode 100644 index 000000000..65e87f9d8 --- /dev/null +++ b/tests/operations/uv.packages/remove_3_packages_with_2_installed.json @@ -0,0 +1,12 @@ +{ + "args": [["ruff", "mypy", "pyrefly"]], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvPipPackages": {"ruff": ["1.2.3"], "mypy": ["4.5.6"]} + }, + "commands": [ + "uv pip uninstall --no-progress ruff mypy" + ] +} diff --git a/tests/operations/uv.packages/remove_no_packages.json b/tests/operations/uv.packages/remove_no_packages.json new file mode 100644 index 000000000..7f043565b --- /dev/null +++ b/tests/operations/uv.packages/remove_no_packages.json @@ -0,0 +1,11 @@ +{ + "args": [[]], + "kwargs": { + "present": false + }, + "facts": { + }, + "commands": [ + ], + "noop_description": "neither packages nor requirements requested to be (un)installed" +} diff --git a/tests/operations/uv.packages/remove_no_packages_but_requirements.json b/tests/operations/uv.packages/remove_no_packages_but_requirements.json new file mode 100644 index 000000000..f2497c5d0 --- /dev/null +++ b/tests/operations/uv.packages/remove_no_packages_but_requirements.json @@ -0,0 +1,12 @@ +{ + "args": [[]], + "kwargs": { + "present": false, + "requirements": "requirements.txt" + }, + "facts": { + }, + "commands": [ + "uv pip uninstall --no-progress -r requirements.txt" + ] +} diff --git a/tests/operations/uv.packages/remove_one_existing_package.json b/tests/operations/uv.packages/remove_one_existing_package.json new file mode 100644 index 000000000..c71cbc0d8 --- /dev/null +++ b/tests/operations/uv.packages/remove_one_existing_package.json @@ -0,0 +1,12 @@ +{ + "args": ["pyinfra"], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvPipPackages": {"pyinfra": ["3.5.1"]} + }, + "commands": [ + "uv pip uninstall --no-progress pyinfra" + ] +} diff --git a/tests/operations/uv.packages/remove_one_not_existing_package.json b/tests/operations/uv.packages/remove_one_not_existing_package.json new file mode 100644 index 000000000..166a2d261 --- /dev/null +++ b/tests/operations/uv.packages/remove_one_not_existing_package.json @@ -0,0 +1,12 @@ +{ + "args": ["pyinfra"], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvPipPackages": {} + }, + "commands": [ + ], + "noop_description": "package pyinfra is not installed" +} diff --git a/tests/operations/uv.packages/upgrade_no_packages.json b/tests/operations/uv.packages/upgrade_no_packages.json new file mode 100644 index 000000000..fc4e01b8a --- /dev/null +++ b/tests/operations/uv.packages/upgrade_no_packages.json @@ -0,0 +1,12 @@ +{ + "args": [[]], + "kwargs": { + "present": true, + "latest": true + }, + "facts": { + }, + "commands": [ + ], + "noop_description": "neither packages nor requirements requested to be (un)installed" +} diff --git a/tests/operations/uv.packages/upgrade_one_existing_package.json b/tests/operations/uv.packages/upgrade_one_existing_package.json new file mode 100644 index 000000000..73525d385 --- /dev/null +++ b/tests/operations/uv.packages/upgrade_one_existing_package.json @@ -0,0 +1,13 @@ +{ + "args": ["pyinfra"], + "kwargs": { + "present": true, + "latest": true + }, + "facts": { + "uv.UvPipPackages": {"pyinfra": ["3.5.1"]} + }, + "commands": [ + "uv pip install --upgrade --no-progress pyinfra" + ] +} diff --git a/tests/operations/uv.packages/upgrade_one_non_existing_package.json b/tests/operations/uv.packages/upgrade_one_non_existing_package.json new file mode 100644 index 000000000..8aae0b6b2 --- /dev/null +++ b/tests/operations/uv.packages/upgrade_one_non_existing_package.json @@ -0,0 +1,13 @@ +{ + "args": ["pyinfra"], + "kwargs": { + "present": true, + "latest": true + }, + "facts": { + "uv.UvPipPackages": {} + }, + "commands": [ + "uv pip install --no-progress pyinfra" + ] +} diff --git a/tests/operations/uv.pythons/install_3_pythons_with_1_existing.json b/tests/operations/uv.pythons/install_3_pythons_with_1_existing.json new file mode 100644 index 000000000..1d223ae82 --- /dev/null +++ b/tests/operations/uv.pythons/install_3_pythons_with_1_existing.json @@ -0,0 +1,18 @@ +{ + "args": [["3.11.14", "3.13.9", "3.14.0"]], + "kwargs": { + "present": true, + "extra_args": ["--no-cache"] + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": { + "3.11.14": ["cpython-3.11.14-macos-aarch64-none"], + "3.14.0": ["cpython-3.14.0-macos-aarch64-none"] + } + } + }, + "commands": [ + "uv python install --no-progress --managed-python --no-cache 3.13.9" + ] +} diff --git a/tests/operations/uv.pythons/install_no_pythons.json b/tests/operations/uv.pythons/install_no_pythons.json new file mode 100644 index 000000000..be01b373e --- /dev/null +++ b/tests/operations/uv.pythons/install_no_pythons.json @@ -0,0 +1,14 @@ +{ + "args": [[]], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": {} + } + }, + "commands": [ + ], + "noop_description": "no python versions requested to be (un)installed" +} diff --git a/tests/operations/uv.pythons/install_none_pythons.json b/tests/operations/uv.pythons/install_none_pythons.json new file mode 100644 index 000000000..dd02762d6 --- /dev/null +++ b/tests/operations/uv.pythons/install_none_pythons.json @@ -0,0 +1,15 @@ +{ + "args": [null], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": {} + } + }, + "commands": [ + ], + "noop_description": "no python versions requested to be (un)installed" + +} diff --git a/tests/operations/uv.pythons/install_one_already_existing_python.json b/tests/operations/uv.pythons/install_one_already_existing_python.json new file mode 100644 index 000000000..399a0e0f7 --- /dev/null +++ b/tests/operations/uv.pythons/install_one_already_existing_python.json @@ -0,0 +1,16 @@ +{ + "args": ["3.12.12"], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": { + "3.12.12": ["cpython-3.12.12-macos-aarch64-none"] + } + } + }, + "commands": [ + ], + "noop_description": "package 3.12.12 is installed (cpython-3.12.12-macos-aarch64-none)" +} diff --git a/tests/operations/uv.pythons/install_one_not_existing_python.json b/tests/operations/uv.pythons/install_one_not_existing_python.json new file mode 100644 index 000000000..b19486251 --- /dev/null +++ b/tests/operations/uv.pythons/install_one_not_existing_python.json @@ -0,0 +1,15 @@ +{ + "args": ["3.13.9"], + "kwargs": { + "present": true, + "extra_args": "--no-cache" + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": {} + } + }, + "commands": [ + "uv python install --no-progress --managed-python --no-cache 3.13.9" + ] +} diff --git a/tests/operations/uv.pythons/remove_3_pythons_with_2_installed.json b/tests/operations/uv.pythons/remove_3_pythons_with_2_installed.json new file mode 100644 index 000000000..71b4f5891 --- /dev/null +++ b/tests/operations/uv.pythons/remove_3_pythons_with_2_installed.json @@ -0,0 +1,18 @@ +{ + "args": [["3.11.14", "3.12.12", "3.13.9"]], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": { + "3.11.14": ["cpython-3.11.14-macos-aarch64-none"], + "3.13.9": ["cpython-3.13.9-macos-aarch64-none"] + } + } + }, + "commands": [ + "uv python uninstall --no-progress --managed-python 3.11.14", + "uv python uninstall --no-progress --managed-python 3.13.9" + ] +} diff --git a/tests/operations/uv.pythons/remove_no_pythons.json b/tests/operations/uv.pythons/remove_no_pythons.json new file mode 100644 index 000000000..68fce8822 --- /dev/null +++ b/tests/operations/uv.pythons/remove_no_pythons.json @@ -0,0 +1,14 @@ +{ + "args": [null], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": {} + } + }, + "commands": [ + ], + "noop_description": "no python versions requested to be (un)installed" +} diff --git a/tests/operations/uv.pythons/remove_none_pythons.json b/tests/operations/uv.pythons/remove_none_pythons.json new file mode 100644 index 000000000..dd02762d6 --- /dev/null +++ b/tests/operations/uv.pythons/remove_none_pythons.json @@ -0,0 +1,15 @@ +{ + "args": [null], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": {} + } + }, + "commands": [ + ], + "noop_description": "no python versions requested to be (un)installed" + +} diff --git a/tests/operations/uv.pythons/remove_one_existing_python.json b/tests/operations/uv.pythons/remove_one_existing_python.json new file mode 100644 index 000000000..842ed4b49 --- /dev/null +++ b/tests/operations/uv.pythons/remove_one_existing_python.json @@ -0,0 +1,18 @@ +{ + "args": ["3.10.19"], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": { + "3.10.19": [ + "cpython-3.10.19-macos-aarch64-none" + ] + } + } + }, + "commands": [ + "uv python uninstall --no-progress --managed-python 3.10.19" + ] +} diff --git a/tests/operations/uv.pythons/remove_one_not_existing_python.json b/tests/operations/uv.pythons/remove_one_not_existing_python.json new file mode 100644 index 000000000..5a42a4681 --- /dev/null +++ b/tests/operations/uv.pythons/remove_one_not_existing_python.json @@ -0,0 +1,14 @@ +{ + "args": ["3.15.1"], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvInstalledPythonsByVersion": { + "is_managed=True": {} + } + }, + "commands": [ + ], + "noop_description": "package 3.15.1 is not installed" +} diff --git a/tests/operations/uv.tool_update_shell/already_present.json b/tests/operations/uv.tool_update_shell/already_present.json new file mode 100644 index 000000000..f2e083fee --- /dev/null +++ b/tests/operations/uv.tool_update_shell/already_present.json @@ -0,0 +1,11 @@ +{ + "args": [], + "kwargs": {}, + "facts": { + "uv.UvToolDir": "/home/someone/.local/bin", + "server.Path": "/somewhere/special:/usr/local/sbin:/usr/local/bin" + }, + "commands": [ + "uv tool update-shell" + ] +} diff --git a/tests/operations/uv.tool_update_shell/not_present.json b/tests/operations/uv.tool_update_shell/not_present.json new file mode 100644 index 000000000..2d992f948 --- /dev/null +++ b/tests/operations/uv.tool_update_shell/not_present.json @@ -0,0 +1,10 @@ +{ + "args": [], + "kwargs": {}, + "facts": { + "uv.UvToolDir": "/home/someone/.local/bin", + "server.Path": "/somewhere/special:/home/someone/.local/bin:/usr/local/sbin:/usr/local/bin" + }, + "commands": [], + "noop_description": "uv bin dir is already in the PATH" +} diff --git a/tests/operations/uv.tool_upgrade_all/tool_upgrade_all.json b/tests/operations/uv.tool_upgrade_all/tool_upgrade_all.json new file mode 100644 index 000000000..4e1dbad80 --- /dev/null +++ b/tests/operations/uv.tool_upgrade_all/tool_upgrade_all.json @@ -0,0 +1,10 @@ +{ + "args": [], + "kwargs": { + }, + "facts": { + }, + "commands": [ + "uv tool --reinstall --upgrade" + ] +} diff --git a/tests/operations/uv.tools/install_3_tools_with_1_existing.json b/tests/operations/uv.tools/install_3_tools_with_1_existing.json new file mode 100644 index 000000000..d4875d325 --- /dev/null +++ b/tests/operations/uv.tools/install_3_tools_with_1_existing.json @@ -0,0 +1,16 @@ +{ + "args": [["ruff==0.14.4", "mypy", "sphinx >= 8.2"]], + "kwargs": { + "present": true, + "extra_args": ["--no-cache"] + }, + "facts": { + "uv.UvTools": { + "mypy": ["1.17.1"] + } + }, + "commands": [ + "uv tool install --no-progress --no-cache ruff==0.14.4", + "uv tool install --no-progress --no-cache 'sphinx>=8.2'" + ] +} diff --git a/tests/operations/uv.tools/install_no_tools.json b/tests/operations/uv.tools/install_no_tools.json new file mode 100644 index 000000000..46936adef --- /dev/null +++ b/tests/operations/uv.tools/install_no_tools.json @@ -0,0 +1,12 @@ +{ + "args": [[]], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvTools": {} + }, + "commands": [ + ], + "noop_description": "no tools requested to be (un)installed" +} diff --git a/tests/operations/uv.tools/install_none_tools.json b/tests/operations/uv.tools/install_none_tools.json new file mode 100644 index 000000000..9f113434f --- /dev/null +++ b/tests/operations/uv.tools/install_none_tools.json @@ -0,0 +1,13 @@ +{ + "args": [null], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvTools": {} + }, + "commands": [ + ], + "noop_description": "no tools requested to be (un)installed" + +} diff --git a/tests/operations/uv.tools/install_one_already_existing_tool.json b/tests/operations/uv.tools/install_one_already_existing_tool.json new file mode 100644 index 000000000..e7da8f3f7 --- /dev/null +++ b/tests/operations/uv.tools/install_one_already_existing_tool.json @@ -0,0 +1,11 @@ +{ + "args": ["ruff==0.14.4"], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvTools": {"ruff": ["0.14.4"]} + }, + "commands": [], + "noop_description": "package ruff is installed (0.14.4)" +} diff --git a/tests/operations/uv.tools/install_one_not_existing_tool.json b/tests/operations/uv.tools/install_one_not_existing_tool.json new file mode 100644 index 000000000..33ea15101 --- /dev/null +++ b/tests/operations/uv.tools/install_one_not_existing_tool.json @@ -0,0 +1,13 @@ +{ + "args": ["mypy==1.17.1"], + "kwargs": { + "present": true, + "extra_args": ["--no-cache"] + }, + "facts": { + "uv.UvTools": {} + }, + "commands": [ + "uv tool install --no-progress --no-cache mypy==1.17.1" + ] +} diff --git a/tests/operations/uv.tools/remove_3_tools_with_2_installed.json b/tests/operations/uv.tools/remove_3_tools_with_2_installed.json new file mode 100644 index 000000000..b4af51520 --- /dev/null +++ b/tests/operations/uv.tools/remove_3_tools_with_2_installed.json @@ -0,0 +1,17 @@ +{ + "args": [["ruff", "mypy", "pytest"]], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvTools": { + "ruff": ["0.12.1"], + "pytest": ["7.3.1"] + } + }, + "commands": [ + "uv tool uninstall --no-progress ruff", + "uv tool uninstall --no-progress pytest" + ], + "noop_description": "package mypy is not installed" +} diff --git a/tests/operations/uv.tools/remove_no_tools.json b/tests/operations/uv.tools/remove_no_tools.json new file mode 100644 index 000000000..7e73eb7da --- /dev/null +++ b/tests/operations/uv.tools/remove_no_tools.json @@ -0,0 +1,12 @@ +{ + "args": [null], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvTools": {} + }, + "commands": [ + ], + "noop_description": "no tools requested to be (un)installed" +} diff --git a/tests/operations/uv.tools/remove_none_tools.json b/tests/operations/uv.tools/remove_none_tools.json new file mode 100644 index 000000000..9f113434f --- /dev/null +++ b/tests/operations/uv.tools/remove_none_tools.json @@ -0,0 +1,13 @@ +{ + "args": [null], + "kwargs": { + "present": true + }, + "facts": { + "uv.UvTools": {} + }, + "commands": [ + ], + "noop_description": "no tools requested to be (un)installed" + +} diff --git a/tests/operations/uv.tools/remove_one_existing_tool.json b/tests/operations/uv.tools/remove_one_existing_tool.json new file mode 100644 index 000000000..41f8857d6 --- /dev/null +++ b/tests/operations/uv.tools/remove_one_existing_tool.json @@ -0,0 +1,12 @@ +{ + "args": ["ruff"], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvTools": {"ruff": ["0.13.5"]} + }, + "commands": [ + "uv tool uninstall --no-progress ruff" + ] +} diff --git a/tests/operations/uv.tools/remove_one_not_existing_tool.json b/tests/operations/uv.tools/remove_one_not_existing_tool.json new file mode 100644 index 000000000..8ae56b956 --- /dev/null +++ b/tests/operations/uv.tools/remove_one_not_existing_tool.json @@ -0,0 +1,12 @@ +{ + "args": ["ruff"], + "kwargs": { + "present": false + }, + "facts": { + "uv.UvTools": {} + }, + "commands": [ + ], + "noop_description": "package ruff is not installed" +} diff --git a/tests/operations/uv.venv/create_already_existing.json b/tests/operations/uv.venv/create_already_existing.json new file mode 100644 index 000000000..f78cda719 --- /dev/null +++ b/tests/operations/uv.venv/create_already_existing.json @@ -0,0 +1,17 @@ +{ + "args": ["/somewhere/useful"], + "kwargs": { + "present": true + }, + "facts": { + "files.File": { + "path=/somewhere/useful/.venv/bin/activate": true + }, + "files.Directory": { + "path=/somewhere/useful.venv": true + } + }, + "commands": [ + ], + "noop_description": "venv already exists at /somewhere/useful" +} diff --git a/tests/operations/uv.venv/create_but_both_clear_and_existing.json b/tests/operations/uv.venv/create_but_both_clear_and_existing.json new file mode 100644 index 000000000..135afb326 --- /dev/null +++ b/tests/operations/uv.venv/create_but_both_clear_and_existing.json @@ -0,0 +1,22 @@ +{ + "args": ["/somewhere/useful"], + "kwargs": { + "present": true, + "allow_existing": true, + "clear_existing": true + }, + "facts": { + "files.File": { + "path=/somewhere/useful/.venv/bin/activate": true + }, + "files.Directory": { + "path=/somewhere/useful.venv": true + } + }, + "commands": [ + ], + "exception": { + "names": ["ValueError"], + "message": "allow_existing and clear_existing cannot both be set" + } +} diff --git a/tests/operations/uv.venv/create_empty_path.json b/tests/operations/uv.venv/create_empty_path.json new file mode 100644 index 000000000..02cd2bbf8 --- /dev/null +++ b/tests/operations/uv.venv/create_empty_path.json @@ -0,0 +1,20 @@ +{ + "args": [" "], + "kwargs": { + "present": true + }, + "facts": { + "files.File": { + "path= /.venv/bin/activate": null + }, + "files.Directory": { + "path= /.venv": null + } + }, + "commands": [ + ], + "exception": { + "names": ["ValueError"], + "message": "must provide path in which .venv is to be created" + } +} diff --git a/tests/operations/uv.venv/create_not_existing_no_options.json b/tests/operations/uv.venv/create_not_existing_no_options.json new file mode 100644 index 000000000..6c3aae5b5 --- /dev/null +++ b/tests/operations/uv.venv/create_not_existing_no_options.json @@ -0,0 +1,15 @@ +{ + "args": ["/somewhere/useful"], + "kwargs": { + "present": true + }, + "facts": { + "files.File": { + "path=/somewhere/useful/.venv/bin/activate": null + }, + "files.Directory": { + "path=/somewhere/useful.venv": null + } + }, + "commands": ["uv venv /somewhere/useful"] +} diff --git a/tests/operations/uv.venv/create_not_existing_other_options.json b/tests/operations/uv.venv/create_not_existing_other_options.json new file mode 100644 index 000000000..3685de723 --- /dev/null +++ b/tests/operations/uv.venv/create_not_existing_other_options.json @@ -0,0 +1,18 @@ +{ + "args": ["/somewhere/useful"], + "kwargs": { + "present": true, + "clear_existing": true, + "link_mode": "symlink", + "site_packages": true + }, + "facts": { + "files.File": { + "path=/somewhere/useful/.venv/bin/activate": null + }, + "files.Directory": { + "path=/somewhere/useful.venv": null + } + }, + "commands": ["uv venv --clear --link-mode symlink --site-packages /somewhere/useful"] +} diff --git a/tests/operations/uv.venv/create_not_existing_some_options.json b/tests/operations/uv.venv/create_not_existing_some_options.json new file mode 100644 index 000000000..423482105 --- /dev/null +++ b/tests/operations/uv.venv/create_not_existing_some_options.json @@ -0,0 +1,18 @@ +{ + "args": ["/somewhere/useful"], + "kwargs": { + "present": true, + "python": "3.15", + "allow_existing": true, + "seed": true + }, + "facts": { + "files.File": { + "path=/somewhere/useful/.venv/bin/activate": null + }, + "files.Directory": { + "path=/somewhere/useful.venv": null + } + }, + "commands": ["uv venv --allow-existing --seed --python 3.15 /somewhere/useful"] +} diff --git a/tests/operations/uv.venv/remove_not_existing.json b/tests/operations/uv.venv/remove_not_existing.json new file mode 100644 index 000000000..d2eaae12c --- /dev/null +++ b/tests/operations/uv.venv/remove_not_existing.json @@ -0,0 +1,17 @@ +{ + "args": ["/somewhere/useful"], + "kwargs": { + "present": false + }, + "facts": { + "files.File": { + "path=/somewhere/useful/.venv/bin/activate": null + }, + "files.Directory": { + "path=/somewhere/useful/.venv": null + } + }, + "commands": [ + ], + "noop_description": "venv does not exist at /somewhere/useful" +} diff --git a/tests/operations/uv.venv/remove_that_exists.json b/tests/operations/uv.venv/remove_that_exists.json new file mode 100644 index 000000000..cfe3afdc1 --- /dev/null +++ b/tests/operations/uv.venv/remove_that_exists.json @@ -0,0 +1,17 @@ +{ + "args": ["/somewhere/useful"], + "kwargs": { + "present": false + }, + "facts": { + "files.File": { + "path=/somewhere/useful/.venv/bin/activate": true + }, + "files.Directory": { + "path=/somewhere/useful/.venv": true + } + }, + "commands": [ + "rm -rf /somewhere/useful/.venv" + ] +}