diff --git a/.changelog/_unreleased.toml b/.changelog/_unreleased.toml index 64fdbd2e..d8d615d1 100644 --- a/.changelog/_unreleased.toml +++ b/.changelog/_unreleased.toml @@ -3,3 +3,21 @@ id = "e3dd07f8-7520-45b9-9407-9f650e4d333c" type = "feature" description = "Extend pytest task to accept a sequence of paths that contain test files." author = "@sebimarkgraf" + +[[entries]] +id = "eca4a4bb-fef4-4692-8799-402d874a5807" +type = "feature" +description = "Add Python build-system support for Uv (https://docs.astral.sh/uv/guides/projects/)" +author = "@NiklasRosenstein" + +[[entries]] +id = "2d36cc17-ab77-476d-9187-9f509e51735d" +type = "fix" +description = "Use `fsdecode()` on result from `find_uv_bin()` for enhanced compatbility" +author = "@NiklasRosenstein" + +[[entries]] +id = "6262db32-1e87-471b-b4fb-b99ef55eea56" +type = "fix" +description = "Fix `inject_url_credentials()` to not drop port" +author = "@NiklasRosenstein" diff --git a/examples/uv-project-consumer/.kraken.py b/examples/uv-project-consumer/.kraken.py new file mode 100644 index 00000000..c2889725 --- /dev/null +++ b/examples/uv-project-consumer/.kraken.py @@ -0,0 +1,16 @@ +import os + +from kraken.std import python + +python.python_settings(always_use_managed_env=True).add_package_index( + alias="local", + index_url=os.environ["LOCAL_PACKAGE_INDEX"], + credentials=(os.environ["LOCAL_USER"], os.environ["LOCAL_PASSWORD"]), +) +python.install() +python.mypy(version_spec="==1.10.0") +python.flake8(version_spec="==7.0.0") +python.black(version_spec="==24.4.2") +python.isort(version_spec="==5.13.2") +python.pytest() +python.publish(package_index="local", distributions=python.build(as_version="0.1.0").output_files) diff --git a/examples/uv-project-consumer/pyproject.toml b/examples/uv-project-consumer/pyproject.toml new file mode 100644 index 00000000..d6689c22 --- /dev/null +++ b/examples/uv-project-consumer/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "uv-project-consumer" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.10" +dependencies = [ + "tqdm>=4.66.5", + "uv-project==0.1.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/uv-project-consumer/src/uv_project_consumer/__init__.py b/examples/uv-project-consumer/src/uv_project_consumer/__init__.py new file mode 100644 index 00000000..b52abc8b --- /dev/null +++ b/examples/uv-project-consumer/src/uv_project_consumer/__init__.py @@ -0,0 +1,5 @@ +from uv_project import hello + + +def main() -> None: + hello() diff --git a/examples/uv-project-consumer/uv.lock b/examples/uv-project-consumer/uv.lock new file mode 100644 index 00000000..d6342adf --- /dev/null +++ b/examples/uv-project-consumer/uv.lock @@ -0,0 +1,34 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "tqdm" +version = "4.66.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", size = 78351 }, +] + +[[package]] +name = "uv-project" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "tqdm" }, +] + +[package.metadata] +requires-dist = [{ name = "tqdm" }] diff --git a/examples/uv-project/.kraken.py b/examples/uv-project/.kraken.py new file mode 100644 index 00000000..c2889725 --- /dev/null +++ b/examples/uv-project/.kraken.py @@ -0,0 +1,16 @@ +import os + +from kraken.std import python + +python.python_settings(always_use_managed_env=True).add_package_index( + alias="local", + index_url=os.environ["LOCAL_PACKAGE_INDEX"], + credentials=(os.environ["LOCAL_USER"], os.environ["LOCAL_PASSWORD"]), +) +python.install() +python.mypy(version_spec="==1.10.0") +python.flake8(version_spec="==7.0.0") +python.black(version_spec="==24.4.2") +python.isort(version_spec="==5.13.2") +python.pytest() +python.publish(package_index="local", distributions=python.build(as_version="0.1.0").output_files) diff --git a/examples/uv-project/pyproject.toml b/examples/uv-project/pyproject.toml new file mode 100644 index 00000000..f66fcdc4 --- /dev/null +++ b/examples/uv-project/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "uv-project" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.10" +dependencies = [ + "tqdm>=4.66.5", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv] +dev-dependencies = [ + "types-tqdm>=4.66.0.20240417", +] diff --git a/examples/uv-project/src/uv_project/__init__.py b/examples/uv-project/src/uv_project/__init__.py new file mode 100644 index 00000000..c3666020 --- /dev/null +++ b/examples/uv-project/src/uv_project/__init__.py @@ -0,0 +1,9 @@ +from time import sleep + +from tqdm import tqdm + + +def hello() -> None: + for _ in tqdm(range(10)): + print("Hello from uv-project!") + sleep(1) diff --git a/examples/uv-project/uv.lock b/examples/uv-project/uv.lock new file mode 100644 index 00000000..798eb5d8 --- /dev/null +++ b/examples/uv-project/uv.lock @@ -0,0 +1,51 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "tqdm" +version = "4.66.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", size = 78351 }, +] + +[[package]] +name = "types-tqdm" +version = "4.66.0.20240417" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/65/f14716c07d40f51be63cb46d89a71c4c5314bcf501506930b7fa5201ece0/types-tqdm-4.66.0.20240417.tar.gz", hash = "sha256:16dce9ef522ea8d40e4f5b8d84dd8a1166eefc13ceee7a7e158bf0f1a1421a31", size = 11916 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/dd/39a411628bfdeeac54587aa013a83a446a2ecf8e7e324744b9ba3bf076f3/types_tqdm-4.66.0.20240417-py3-none-any.whl", hash = "sha256:248aef1f9986b7b8c2c12b3cb4399fc17dba0a29e7e3f3f9cd704babb879383d", size = 19163 }, +] + +[[package]] +name = "uv-project" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "tqdm" }, +] + +[package.dev-dependencies] +dev = [ + { name = "types-tqdm" }, +] + +[package.metadata] +requires-dist = [{ name = "tqdm", specifier = ">=4.66.5" }] + +[package.metadata.requires-dev] +dev = [{ name = "types-tqdm" }] diff --git a/kraken-build/src/kraken/std/python/buildsystem/__init__.py b/kraken-build/src/kraken/std/python/buildsystem/__init__.py index e5b792b1..6d31ad44 100644 --- a/kraken-build/src/kraken/std/python/buildsystem/__init__.py +++ b/kraken-build/src/kraken/std/python/buildsystem/__init__.py @@ -184,4 +184,12 @@ def detect_build_system(project_directory: Path) -> PythonBuildSystem | None: return PDMPythonBuildSystem(project_directory) - return None + if "[tool.uv]" not in pyproject_content: + logger.info( + "Got no hint as to the Python build system used in the project '%s', falling back to UV (experimental)", + project_directory, + ) + + from kraken.std.python.buildsystem.uv import UVPythonBuildSystem + + return UVPythonBuildSystem(project_directory) diff --git a/kraken-build/src/kraken/std/python/buildsystem/pdm.py b/kraken-build/src/kraken/std/python/buildsystem/pdm.py index 35734bc7..c36d58f9 100644 --- a/kraken-build/src/kraken/std/python/buildsystem/pdm.py +++ b/kraken-build/src/kraken/std/python/buildsystem/pdm.py @@ -33,9 +33,6 @@ class PdmPyprojectHandler(PyprojectHandler): Implements the PyprojectHandler interface for PDM projects. """ - def __init__(self, pyproj: TomlFile) -> None: - super().__init__(pyproj) - # PyprojectHandler def get_package_indexes(self) -> list[PackageIndex]: diff --git a/kraken-build/src/kraken/std/python/buildsystem/slap.py b/kraken-build/src/kraken/std/python/buildsystem/slap.py index e98baf25..a297c7ab 100644 --- a/kraken-build/src/kraken/std/python/buildsystem/slap.py +++ b/kraken-build/src/kraken/std/python/buildsystem/slap.py @@ -72,6 +72,7 @@ def bump_version(self, version: str) -> Iterator[None]: def build(self, output_directory: Path) -> list[Path]: with tempfile.TemporaryDirectory() as tempdir: command = ["slap", "publish", "--dry", "-b", tempdir] + logger.info("Running %s in '%s'", command, self.project_directory) sp.check_call(command, cwd=self.project_directory) src_files = list(Path(tempdir).iterdir()) dst_files = [output_directory / path.name for path in src_files] diff --git a/kraken-build/src/kraken/std/python/buildsystem/uv.py b/kraken-build/src/kraken/std/python/buildsystem/uv.py new file mode 100644 index 00000000..6542caf6 --- /dev/null +++ b/kraken-build/src/kraken/std/python/buildsystem/uv.py @@ -0,0 +1,284 @@ +""" +Experimental. + +Support for Python projects managed by [UV](https://docs.astral.sh/uv/guides/projects/). +""" + +from __future__ import annotations + +from hashlib import md5 +import logging +from os import fsdecode +import os +import shutil +import subprocess as sp +from collections.abc import Sequence +from pathlib import Path +import tempfile +from typing import TYPE_CHECKING, Annotated, Any, Iterable, MutableMapping, TypeVar +from urllib.parse import urlparse + +from attr import dataclass + +from kraken.common.toml import TomlFile +from kraken.core import TaskStatus +from kraken.std.python.pyproject import PackageIndex, PyprojectHandler +from kraken.std.python.settings import PythonSettings +from kraken.std.util.url import inject_url_credentials + +from . import ManagedEnvironment, PythonBuildSystem + +# "uv" is a dependency of Kraken, so we can use it's packaged version. +if TYPE_CHECKING: + + def find_uv_bin() -> str: ... + +else: + from uv.__main__ import find_uv_bin + + +logger = logging.getLogger(__name__) +T = TypeVar("T") +T_PackageIndex = TypeVar("T_PackageIndex", bound=PackageIndex) +Safe = Annotated[T, "safe"] +Unsafe = Annotated[T, "unsafe"] + + +@dataclass +class PipIndex: + url: str + credentials: tuple[str, str] | None = None + + @property + def safe_url(self) -> str: + if self.credentials: + return inject_url_credentials(self.url, self.credentials[0], "[MASKED]") + return self.url + + @property + def unsafe_url(self) -> str: + if self.credentials: + return inject_url_credentials(self.url, self.credentials[0], self.credentials[1]) + return self.url + + @staticmethod + def of(index: PackageIndex) -> "PipIndex": + credentials = index.credentials if isinstance(index, PythonSettings._PackageIndex) else None + return PipIndex(index.index_url, credentials) + + +@dataclass +class PipIndexes: + primary: PipIndex | None + supplemental: list[PipIndex] + + @staticmethod + def from_package_indexes(indexes: Iterable[T_PackageIndex]) -> "PipIndexes": + default_index = next((idx for idx in indexes if idx.priority == PackageIndex.Priority.default), None) + primary_index = next((idx for idx in indexes if idx.priority == PackageIndex.Priority.primary), None) + remainder = [idx for idx in indexes if idx not in (default_index, primary_index)] + + if default_index and primary_index: + logger.warning( + "Cannot have 'default' and 'primary' index for a UV project. The 'primary' index (%s) will be used " + "as the first of the 'supplemental' indexes instead.", + primary_index.alias, + ) + remainder.insert(0, primary_index) + primary_index = None + + elif primary_index and not default_index: + default_index, primary_index = primary_index, None + + return PipIndexes( + primary=PipIndex.of(default_index) if default_index is not None else None, + supplemental=[PipIndex.of(idx) for idx in remainder], + ) + + def to_safe_args(self) -> list[str]: + """Create a list of arguments for UV with sensitive information masked.""" + + args = [] + if self.primary is not None: + args += ["--index-url", self.primary.safe_url] + for index in self.supplemental: + args += ["--extra-index-url", index.safe_url] + return args + + def to_unsafe_args(self) -> list[str]: + """Create a list of arguments for UV with sensitive information in plaintext.""" + + args = [] + if self.primary is not None: + args += ["--index-url", self.primary.unsafe_url] + for index in self.supplemental: + args += ["--extra-index-url", index.unsafe_url] + return args + + def to_config(self, config: MutableMapping[str, Any]) -> None: + """Inject UV configuration for indexes into a configuration.""" + + if self.primary is None: + config.pop("index-url", None) + else: + config["index-url"] = self.primary.url + + if not self.supplemental: + config.pop("extra-index-url", None) + else: + config["extra-index-url"] = [idx.url for idx in self.supplemental] + + +class UvPyprojectHandler(PyprojectHandler): + """Implements the PyprojectHandler interface for UV projects.""" + + # TODO: Support `uv.toml` configuration file? + + # PyprojectHandler + + def get_package_indexes(self) -> list[PackageIndex]: + """Maps the UV [`index-url`][1] and [`extra-index-url`][2] options to Kraken's concept of package indices. + Note that UV does not support the concept of "aliases" for package indices, so instead the package index alias + is ignored and generated automatically based on the hostname and URL hash. + + [1]: https://docs.astral.sh/uv/reference/settings/#index-url + [2]: https://docs.astral.sh/uv/reference/settings/#extra-index-url + """ + + def gen_alias(url: str) -> str: + hostname = urlparse(url).hostname + assert hostname is not None, "expected hostname in package index URL" + return f"hostname-{md5(url.encode()).hexdigest()[:5]}" + + indexes: list[PackageIndex] = [] + config: dict[str, Any] = self.raw.get("tool", {}).get("uv", {}) + + if index_url := config.get("index-url"): + indexes.append( + PackageIndex( + alias=gen_alias(index_url), + index_url=index_url, + priority=PackageIndex.Priority.default, + verify_ssl=True, + ) + ) + + for index_url in config.get("extra-index-url", []): + indexes.append( + PackageIndex( + alias=gen_alias(index_url), + index_url=index_url, + priority=PackageIndex.Priority.supplemental, + verify_ssl=True, + ) + ) + + return indexes + + def set_package_indexes(self, indexes: Sequence[PackageIndex]) -> None: + """Counterpart to [`get_package_indexes()`], check there.""" + + config: dict[str, Any] = self.raw.setdefault("tool", {}).setdefault("uv", {}) + PipIndexes.from_package_indexes(indexes).to_config(config) + + def get_packages(self) -> list[PyprojectHandler.Package]: + # TODO: Detect packages in the project. + return [] + + +class UVPythonBuildSystem(PythonBuildSystem): + """ + Implements Python build-system capabilities for [UV]. + + [UV]: https://docs.astral.sh/uv/guides/projects/ + """ + + name = "UV" + + def __init__(self, project_directory: Path, uv_bin: Path | None = None) -> None: + self.project_directory = project_directory + self.uv_bin = str(uv_bin or Path(fsdecode(find_uv_bin())).absolute()) + + def get_pyproject_reader(self, pyproject: TomlFile) -> UvPyprojectHandler: + return UvPyprojectHandler(pyproject) + + def supports_managed_environments(self) -> bool: + return True + + def get_managed_environment(self) -> ManagedEnvironment: + return UVManagedEnvironment(self.project_directory, self.uv_bin) + + def update_lockfile(self, settings: PythonSettings, pyproject: TomlFile) -> TaskStatus: + indexes = PipIndexes.from_package_indexes(settings.package_indexes.values()) + safe_command = [self.uv_bin, "lock"] + indexes.to_safe_args() + unsafe_command = [self.uv_bin, "lock"] + indexes.to_unsafe_args() + logger.info("Running %s in '%s'", safe_command, self.project_directory) + sp.check_call(unsafe_command, cwd=self.project_directory) + return TaskStatus.succeeded() + + def requires_login(self) -> bool: + return False + + # TODO: Implement bump_version() + + def build(self, output_directory: Path) -> list[Path]: + """ + Uses [build] `>=1.0.0,<2.0.0` to build a distribution of the Python project. + + [build]: https://pypi.org/project/build/ + """ + + with tempfile.TemporaryDirectory() as tempdir: + env = os.environ.copy() + + # Make sure that UV is on the path for `pyproject-build` to find it. + assert Path(self.uv_bin).name == "uv" + if shutil.which("uv") != self.uv_bin: + env["PATH"] = str(Path(self.uv_bin).parent) + os.pathsep + env["PATH"] + + command = [ + self.uv_bin, + "tool", + "run", + "--from", + "build>=1.0.0,<2.0.0", + "pyproject-build", + "--outdir", + tempdir, + "--installer", + "uv", + ] + sp.check_call(command, cwd=self.project_directory, env=env) + logger.info("Running %s in '%s'", command, self.project_directory) + + src_files = list(Path(tempdir).iterdir()) + dst_files = [output_directory / path.name for path in src_files] + for src, dst in zip(src_files, dst_files): + shutil.move(str(src), dst) + + return dst_files + + def get_lockfile(self) -> Path | None: + return self.project_directory / "uv.lock" + + +class UVManagedEnvironment(ManagedEnvironment): + def __init__(self, project_directory: Path, uv_bin: str) -> None: + self.project_directory = project_directory + self.uv_bin = uv_bin + self.env_path = project_directory / ".venv" + + # ManagedEnvironment + + def exists(self) -> bool: + return self.env_path.is_dir() + + def get_path(self) -> Path: + return self.env_path + + def install(self, settings: PythonSettings) -> None: + indexes = PipIndexes.from_package_indexes(settings.package_indexes.values()) + safe_command = [self.uv_bin, "sync"] + indexes.to_safe_args() + unsafe_command = [self.uv_bin, "sync"] + indexes.to_unsafe_args() + logger.info("Running %s in '%s'", safe_command, self.project_directory) + sp.check_call(unsafe_command, cwd=self.project_directory) diff --git a/kraken-build/src/kraken/std/util/url.py b/kraken-build/src/kraken/std/util/url.py index 429bee4a..d4f7010a 100644 --- a/kraken-build/src/kraken/std/util/url.py +++ b/kraken-build/src/kraken/std/util/url.py @@ -14,5 +14,5 @@ def inject_url_credentials(url: str, username: str, password: str) -> str: """Injects a username and password into a URL.""" parsed = urlparse(url) - replaced = parsed._replace(netloc=f"{username}:{password}@{parsed.hostname}") + replaced = parsed._replace(netloc=f"{username}:{password}@{parsed.netloc}") return replaced.geturl() diff --git a/kraken-build/src/kraken/std/util/url_test.py b/kraken-build/src/kraken/std/util/url_test.py new file mode 100644 index 00000000..bb6cdc1d --- /dev/null +++ b/kraken-build/src/kraken/std/util/url_test.py @@ -0,0 +1,7 @@ +from kraken.std.util.url import inject_url_credentials + + +def test_inject_url_credentials() -> None: + assert ( + inject_url_credentials("http://localhost:8000/simple/", "foo", "bar") == "http://foo:bar@localhost:8000/simple/" + ) diff --git a/kraken-build/tests/kraken_std/integration/python/test_python.py b/kraken-build/tests/kraken_std/integration/python/test_python.py index dd57dcb4..daa1ba7c 100644 --- a/kraken-build/tests/kraken_std/integration/python/test_python.py +++ b/kraken-build/tests/kraken_std/integration/python/test_python.py @@ -20,6 +20,7 @@ from kraken.std.python.buildsystem.maturin import MaturinPoetryPyprojectHandler from kraken.std.python.buildsystem.pdm import PdmPyprojectHandler from kraken.std.python.buildsystem.poetry import PoetryPyprojectHandler +from kraken.std.python.buildsystem.uv import UvPyprojectHandler from kraken.std.util.http import http_probe from tests.kraken_std.util.docker import DockerServiceManager from tests.resources import example_dir @@ -80,7 +81,7 @@ def pypiserver(docker_service_manager: DockerServiceManager) -> str: @pytest.mark.parametrize( "project_dir", - ["poetry-project", "slap-project", "pdm-project", "rust-poetry-project", "rust-pdm-project"], + ["poetry-project", "slap-project", "pdm-project", "rust-poetry-project", "rust-pdm-project", "uv-project"], ) @unittest.mock.patch.dict(os.environ, {}) def test__python_project_install_lint_and_publish( @@ -178,6 +179,7 @@ def test__python_project_upgrade_python_version_string( ("slap-project", PoetryPyprojectHandler, "^3.6"), ("pdm-project", PdmPyprojectHandler, ">=3.9"), ("rust-poetry-project", MaturinPoetryPyprojectHandler, "^3.7"), + ("uv-project", UvPyprojectHandler, ">=3.10"), ], ) @unittest.mock.patch.dict(os.environ, {}) diff --git a/kraken-wrapper/src/kraken/wrapper/_buildenv_uv.py b/kraken-wrapper/src/kraken/wrapper/_buildenv_uv.py index db034632..8212a7ef 100644 --- a/kraken-wrapper/src/kraken/wrapper/_buildenv_uv.py +++ b/kraken-wrapper/src/kraken/wrapper/_buildenv_uv.py @@ -1,3 +1,4 @@ +from os import fsdecode from pathlib import Path from typing import TYPE_CHECKING, Any @@ -24,7 +25,7 @@ class UvBuildEnv(_VenvBuildEnv): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self._uv_bin = find_uv_bin() + self._uv_bin = fsdecode(find_uv_bin()) def _get_create_venv_command(self, python_bin: Path, path: Path) -> list[str]: return [self._uv_bin, "venv", str(path)]