diff --git a/poethepoet/config/config.py b/poethepoet/config/config.py index 56d9a346..acb444ad 100644 --- a/poethepoet/config/config.py +++ b/poethepoet/config/config.py @@ -125,6 +125,15 @@ def is_poetry_project(self) -> bool: and "poetry" in self._project_config.full_config.get("tool", {}) ) + @property + def is_uv_project(self) -> bool: + # Note: That it can happen that a uv managed project has no uv config + # In this case, this check would fail. + return ( + self._project_config.path.name == "pyproject.toml" + and "uv" in self._project_config.full_config.get("tool", {}) + ) + @property def project_dir(self) -> Path: return self._project_dir diff --git a/poethepoet/executor/__init__.py b/poethepoet/executor/__init__.py index 34c8a9a2..604a601c 100644 --- a/poethepoet/executor/__init__.py +++ b/poethepoet/executor/__init__.py @@ -1,6 +1,13 @@ from .base import PoeExecutor from .poetry import PoetryExecutor from .simple import SimpleExecutor +from .uv import UvExecutor from .virtualenv import VirtualenvExecutor -__all__ = ["PoeExecutor", "PoetryExecutor", "SimpleExecutor", "VirtualenvExecutor"] +__all__ = [ + "PoeExecutor", + "PoetryExecutor", + "SimpleExecutor", + "UvExecutor", + "VirtualenvExecutor", +] diff --git a/poethepoet/executor/base.py b/poethepoet/executor/base.py index a1402a01..541a5a88 100644 --- a/poethepoet/executor/base.py +++ b/poethepoet/executor/base.py @@ -103,6 +103,7 @@ def _resolve_implementation(cls, context: "RunContext", executor_type: str): if executor_type == "auto": for impl in [ cls.__executor_types["poetry"], + cls.__executor_types["uv"], cls.__executor_types["virtualenv"], ]: if impl.works_with_context(context): diff --git a/poethepoet/executor/uv.py b/poethepoet/executor/uv.py new file mode 100644 index 00000000..2807b80f --- /dev/null +++ b/poethepoet/executor/uv.py @@ -0,0 +1,54 @@ +from collections.abc import Sequence +from pathlib import Path +from typing import TYPE_CHECKING, Optional + +from .base import PoeExecutor + +if TYPE_CHECKING: + from ..context import RunContext + + +class UvExecutor(PoeExecutor): + """ + A poe task executor implementation that executes inside a uv managed dev + environment + """ + + __key__ = "uv" + __options__: dict[str, type] = {} + + @classmethod + def works_with_context(cls, context: "RunContext") -> bool: + if not context.config.is_uv_project: + return False + return bool(cls._uv_cmd_from_path()) + + def execute( + self, cmd: Sequence[str], input: Optional[bytes] = None, use_exec: bool = False + ) -> int: + """ + Execute the given cmd as a subprocess inside the uv managed dev environment. + + We simply use `uv run`, which handles the virtualenv and other setup for us. + """ + + # Run this task with `uv run` + return self._execute_cmd( + (self._uv_cmd(), "run", *cmd), + input=input, + use_exec=use_exec, + ) + + @classmethod + def _uv_cmd(cls): + from_path = cls._uv_cmd_from_path() + if from_path: + return str(Path(from_path).resolve()) + + return "uv" + + @classmethod + def _uv_cmd_from_path(cls): + import shutil + + return shutil.which("uv") diff --git a/tests/fixtures/uv_project/pyproject.toml b/tests/fixtures/uv_project/pyproject.toml new file mode 100644 index 00000000..4921c30d --- /dev/null +++ b/tests/fixtures/uv_project/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "uv-project" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[tool.uv] +# We need to have an empty table here to ensure that the tool is recognized + +[tool.poe.tasks] +show-version = "test_print_version" +test-package-version.script = "scripts:test_package_version" +test-package-exec-version.script = "scripts:test_package_exec_version" +show-env = "poe_test_env" + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/tests/fixtures/uv_project/scripts.py b/tests/fixtures/uv_project/scripts.py new file mode 100644 index 00000000..16701734 --- /dev/null +++ b/tests/fixtures/uv_project/scripts.py @@ -0,0 +1,10 @@ +def test_package_version(): + import poe_test_package + + print(poe_test_package.__version__) + + +def test_package_exec_version(): + from subprocess import Popen + + Popen(["test_print_version"]) diff --git a/tests/fixtures/uv_project/src/uv_project/__init__.py b/tests/fixtures/uv_project/src/uv_project/__init__.py new file mode 100644 index 00000000..94ebc9cf --- /dev/null +++ b/tests/fixtures/uv_project/src/uv_project/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from uv-project!"