diff --git a/marimo/_cli/sandbox.py b/marimo/_cli/sandbox.py index c34da872498..d7c7f9e57c3 100644 --- a/marimo/_cli/sandbox.py +++ b/marimo/_cli/sandbox.py @@ -182,6 +182,24 @@ def include_features(dep: str, features: list[DepFeatures]) -> str: return filtered + [include_features(chosen, additional_features)] +def _resolve_local_path_line(line: str, script_dir: Path) -> str: + """Resolve a relative local-path requirement to an absolute path. + + >>> _resolve_local_path_line( + ... "-e ../pkg ; py<'3.12' # via foo", Path("/a/b") + ... ) + '-e /a/pkg ; py<\\'3.12\\' # via foo' + """ + rest = line.removeprefix("-e ") + path_and_comment, _, _ = rest.partition(";") + path_token, _, _ = path_and_comment.partition(" #") + path_token = path_token.rstrip() + if not path_token.startswith("."): + return line + resolved = str((script_dir / path_token).resolve()) + return line.replace(path_token, resolved, 1) + + def _uv_export_script_requirements_txt( name: str | None, ) -> list[str]: @@ -202,7 +220,11 @@ def _uv_export_script_requirements_txt( capture_output=True, text=True, ) - return result.stdout.split("\n") + script_dir = Path(name).resolve().parent + return [ + _resolve_local_path_line(line, script_dir) + for line in result.stdout.split("\n") + ] def _resolve_requirements_txt_lines(pyproject: PyProjectReader) -> list[str]: diff --git a/tests/_cli/test_sandbox.py b/tests/_cli/test_sandbox.py index 0d781eee753..312ee22a21f 100644 --- a/tests/_cli/test_sandbox.py +++ b/tests/_cli/test_sandbox.py @@ -1,12 +1,10 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Any +from pathlib import Path +from typing import Any from unittest.mock import patch -if TYPE_CHECKING: - from pathlib import Path - import pytest from marimo._cli.sandbox import ( @@ -845,3 +843,34 @@ def test_build_sandbox_venv_with_additional_deps(tmp_path: Path) -> None: assert os.path.exists(venv_python) finally: cleanup_sandbox_dir(sandbox_dir) + + +def test_resolve_local_path_line() -> None: + from marimo._cli.sandbox import _resolve_local_path_line + + d = Path("/project/notebooks") + _r = lambda p: str((d / p).resolve()) # noqa: E731 + + # Plain relative + assert _resolve_local_path_line("../../mylib", d) == _r("../../mylib") + # Editable + assert _resolve_local_path_line("-e ../pkg", d) == f"-e {_r('../pkg')}" + # Env marker + result = _resolve_local_path_line("../pkg ; py<'3.12'", d) + assert _r("../pkg") in result + assert "py<'3.12'" in result + # Inline comment + result = _resolve_local_path_line("../pkg # via foo", d) + assert _r("../pkg") in result + assert "# via foo" in result + # Both marker and comment + result = _resolve_local_path_line("../pkg ; py<'3.12' # via foo", d) + assert _r("../pkg") in result + assert "py<'3.12'" in result + assert "# via foo" in result + # Spaces in path + assert _r("../my lib") in _resolve_local_path_line("../my lib", d) + # Non-relative unchanged + assert _resolve_local_path_line("numpy==1.26.0", d) == "numpy==1.26.0" + assert _resolve_local_path_line("/absolute/path", d) == "/absolute/path" + assert _resolve_local_path_line("", d) == ""