Repository: bluss/pyproject-local-kernel

Issue:
Support uv run
The following configuration almost works right now:

```toml
[tool.pyproject-local-kernel]
python-cmd = ["uv", "run", "--with", "ipykernel", "python"]
```

(installing ipykernel if missing is a great feature.)

The following problems remain

- With no configuration change, interrupting computation does not work correctly
  - https://github.com/astral-sh/uv/issues/3095 or other solution(?) required to make interrupt_mode="signal" work
- papermill execution does not work (Linux), also because of interrupt_mode
- Kernel setting interrupt_mode="message" seems to be a working workaround on Linux, but Github issues says it will not work with ipykernel on Windows (and an experiment confirmed this, #14)

Discussion:

Patch:
diff --git a/README.md b/README.md
index 596469f..b7d5f1e 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ figure out which kind of project it is:
 - Use venv at path (for other setups)
 
 Designed to be used with JupyterLab. Can also work with Jupyter Notebook and
-papermill. Doesn't seem to be compatible with Codium or VS Code.
+papermill. Doesn't seem to be compatible with VSCodium or VSCode.
 
 ## Quick Start
 
@@ -85,8 +85,6 @@ relative to the pyproject.toml file, which should be used.
 use-venv = ".venv"
 ```
 
-This is what's used automatically when Uv is detected (at the moment).
-
 ### Custom Command
 
 By default python from the local pyproject is run (using rye run, poetry run,
@@ -102,6 +100,32 @@ python in the virtual environment you want to use for the project.
 python-cmd = ["my", "custom", "python"]
 ```
 
+## About Particular Project Managers
+
+The package manager command, be it rye, uv, pdm, etc needs to be
+available on the path where jupyterlab runs. Either install the project
+manager in the jupyterlab environment, or install the project manager
+user-wide (using something like pipx, rye tools, uv tool, brew, or
+other method to install it.)
+
+### Rye
+
+- Rye is detected if the pyproject.toml contains `tool.rye.managed = true`
+  which Rye sets by default for its new projects.
+
+### Uv
+
+- Uv is detected if the pyproject.toml contains `tool.uv`
+
+- pyproject-local-kernel requires uv 0.2.29 or later
+
+- Uses `uv run` which is a preview feature (could break on future uv changes)
+
+- The command used is `uv run --with ipykernel python` which means that it ensures
+  `ipykernel` is used even if it's not already in the project(!). However, note that
+  it uses an ephemeral virtual environment for ipykernel in that case. Add
+  ipykernel to the project to avoid this.
+
 ## Project Status
 
 Status: Working proof of concept, published to PyPI. Additional interest and
@@ -109,6 +133,7 @@ maintainer help is welcomed.
 
 See also:
 
-* Rye: https://github.com/mitsuhiko/rye
+* https://github.com/mitsuhiko/rye
+* https://github.com/astral-sh/uv
 * https://github.com/goerz/python-localvenv-kernel
-* Poetry-kernel: https://github.com/pathbird/poetry-kernel
+* https://github.com/pathbird/poetry-kernel
diff --git a/example/my-jupyterlab/pyproject.toml b/example/my-jupyterlab/pyproject.toml
index 11190ed..522a26d 100644
--- a/example/my-jupyterlab/pyproject.toml
+++ b/example/my-jupyterlab/pyproject.toml
@@ -14,7 +14,7 @@ classifiers = [
 dependencies = [
     "jupyterlab",
     "jupytext>=1.16.1",
-    "pyproject-local-kernel",
+    "pyproject-local-kernel>=0.6.0",
 ]
 
 [tool.rye]
diff --git a/src/pyproject_local_kernel/__init__.py b/src/pyproject_local_kernel/__init__.py
index 726519e..7202ec8 100644
--- a/src/pyproject_local_kernel/__init__.py
+++ b/src/pyproject_local_kernel/__init__.py
@@ -61,8 +61,7 @@ class ProjectKind(enum.Enum):
         if self == ProjectKind.Hatch:
             return ['hatch', 'run', 'python']
         if self == ProjectKind.Uv:
-            # uv - can't use uv run right now
-            return [get_venv_bin_python(Path(".venv"))]
+            return ['uv', 'run', '--with', 'ipykernel', 'python']
         return None
 
 
@@ -130,12 +129,13 @@ def is_custom(data: dict):
     return python_cmd is not None, {'python_cmd': python_cmd}
 
 
+def is_uv(data: dict):
+    return get_dotkey(data, 'tool.uv', None) is not None
+
 def is_use_venv(data: dict):
     use_venv = get_dotkey(data, f'tool.{MY_TOOL_NAME}.use-venv', None)
     if use_venv is not None and not isinstance(use_venv, str):
         use_venv = None
-    if use_venv is None and get_dotkey(data, 'tool.uv', None) is not None:
-        use_venv = ".venv"
     return use_venv is not None, {'use_venv': use_venv}
 
 
@@ -144,6 +144,7 @@ IDENTIFY_FUNCTIONS = {
     ProjectKind.Pdm: is_pdm,
     ProjectKind.Poetry: is_poetry,
     ProjectKind.Hatch: is_hatch,
+    ProjectKind.Uv: is_uv,
 }
 
 
diff --git a/src/pyproject_local_kernel/__main__.py b/src/pyproject_local_kernel/__main__.py
index 3e25cba..9d701eb 100644
--- a/src/pyproject_local_kernel/__main__.py
+++ b/src/pyproject_local_kernel/__main__.py
@@ -40,6 +40,7 @@ def main():
     python_cmd = find_project.get_python_cmd()
 
     launched = False
+    failure_to_start_msg = ""
 
     if python_cmd is not None:
         cmd = python_cmd + [
@@ -57,6 +58,7 @@ def main():
                 forward_signals = set(signal.Signals) - {signal.SIGKILL, signal.SIGSTOP}
 
             def handle_signal(sig, _frame):
+                _logger.debug("Forwarding signal to kernel: %r", sig)
                 proc.send_signal(sig)
 
             for sig in forward_signals:
@@ -68,14 +70,16 @@ def main():
             else:
                 launched = True
         except (IOError, OSError) as exc:
-            _logger.error("kernel could not be started: %s", exc)
+            failure_to_start_msg = str(exc)
+            _logger.error("kernel could not be started: %s", failure_to_start_msg)
         except Exception as exc:
-            _logger.exception("kernel could not be started: %s", exc)
+            failure_to_start_msg = str(exc)
+            _logger.exception("kernel could not be started: %s", failure_to_start_msg)
     if not launched:
-        start_fallback_kernel(find_project.kind)
+        start_fallback_kernel(find_project.kind, failure_to_start_msg)
 
 
-def start_fallback_kernel(project_kind: ProjectKind):
+def start_fallback_kernel(project_kind: ProjectKind, failure_to_start_msg: str = ""):
     """
     Start a fallback kernel. Its purpose is
 
@@ -101,6 +105,10 @@ def start_fallback_kernel(project_kind: ProjectKind):
     if not has_pyproject:
         help_messages += rye_init_messages
 
+    if failure_to_start_msg:
+        help_messages += ["Error: " + failure_to_start_msg]
+
+
     sync_kernel_env_messages = [
         f"Failed to start kernel! The detected project type is: {project_kind.name}",
         "Is the virtual environment created, and does it have ipykernel in the project?",


Test patch:
diff --git a/tests/test_identify.py b/tests/test_identify.py
index c48a87a..3c38ce8 100644
--- a/tests/test_identify.py
+++ b/tests/test_identify.py
@@ -12,6 +12,7 @@ from pyproject_local_kernel import identify
     ("tests/identify/poetry", ProjectKind.Poetry),
     ("tests/identify/pdm", ProjectKind.Pdm),
     ("tests/identify/hatch", ProjectKind.Hatch),
+    ("tests/identify/uv", ProjectKind.Uv),
     ("tests/identify/broken", ProjectKind.InvalidData),
     ("tests/identify/unknown", ProjectKind.Unknown),
 ])
@@ -32,7 +33,6 @@ def test_custom(path, cmd):
 
 @pytest.mark.parametrize("path,unix_cmd,win_cmd", [
     ("tests/identify/use-venv", ".myvenv/bin/python", r".myvenv\Scripts\python.exe"),
-    ("tests/identify/uv", ".venv/bin/python", r".venv\Scripts\python.exe"),
 ])
 def test_use_venv(path, unix_cmd, win_cmd):
     # venv resolves to absolute path