From ec4a937b897caf73d0ac4e404e3d857635b788e5 Mon Sep 17 00:00:00 2001 From: Vishak Baddur Date: Sat, 4 Apr 2026 19:45:16 -0500 Subject: [PATCH 1/7] fix: resolve relative path sources in sandbox uv export --- marimo/_cli/sandbox.py | 18 +++++++++++++++++- tests/_cli/test_sandbox.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/marimo/_cli/sandbox.py b/marimo/_cli/sandbox.py index c34da872498..b6a88592f86 100644 --- a/marimo/_cli/sandbox.py +++ b/marimo/_cli/sandbox.py @@ -202,7 +202,23 @@ def _uv_export_script_requirements_txt( capture_output=True, text=True, ) - return result.stdout.split("\n") + lines = result.stdout.split("\n") + + # uv export returns local paths relative to the script file, but these + # lines get written to a temp requirements file consumed by + # `uv run --with-requirements`, which resolves relative paths from CWD. + # Convert relative paths to absolute using the script's directory as base. + # Applies to both editable (-e ../../) and non-editable (../../) sources. + script_dir = Path(name).resolve().parent + resolved = [] + for line in lines: + editable = line.startswith("-e ") + path = line[3:].strip() if editable else line.strip() + if path.startswith("."): + path = str((script_dir / path).resolve()) + prefix = "-e " if editable else "" + resolved.append(f"{prefix}{path}") + return resolved 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..f0ab47f0a41 100644 --- a/tests/_cli/test_sandbox.py +++ b/tests/_cli/test_sandbox.py @@ -845,3 +845,39 @@ 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_uv_export_script_requirements_txt_resolves_relative_paths( + tmp_path: Path, +) -> None: + """Test that relative paths in uv export output are resolved to absolute paths. + + Regression test for https://github.com/marimo-team/marimo/issues/8980. + uv export returns paths relative to the script file, but these get written + to a temp requirements file where uv resolves them relative to CWD instead. + """ + from unittest.mock import MagicMock, patch + from marimo._cli.sandbox import _uv_export_script_requirements_txt + + script_path = tmp_path / "subdir" / "notebook.py" + script_path.parent.mkdir(parents=True) + script_path.write_text("# placeholder") + + mock_result = MagicMock() + mock_result.stdout = "-e ../../\n../other_pkg\nnumpy==1.26.0\n/absolute/path\n\n" + + with patch("subprocess.run", return_value=mock_result): + lines = _uv_export_script_requirements_txt(str(script_path)) + + script_dir = script_path.resolve().parent + expected_editable = str((script_dir / "../../").resolve()) + expected_non_editable = str((script_dir / "../other_pkg").resolve()) + + # Editable relative path resolved to absolute + assert any(l.startswith("-e ") and expected_editable in l for l in lines) + # Non-editable relative path resolved to absolute + assert any(expected_non_editable in l and not l.startswith("-e ") for l in lines) + # Regular dep unchanged + assert any("numpy==1.26.0" in l for l in lines) + # Absolute path unchanged + assert any("/absolute/path" in l for l in lines) From 8f35d5bf98816f858d3c23dd02534f60a0dd5760 Mon Sep 17 00:00:00 2001 From: Vishak Baddur Date: Sat, 4 Apr 2026 19:58:53 -0500 Subject: [PATCH 2/7] fix: resolve relative path sources in sandbox uv export --- tests/_cli/test_sandbox.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/_cli/test_sandbox.py b/tests/_cli/test_sandbox.py index f0ab47f0a41..05bf090b3d7 100644 --- a/tests/_cli/test_sandbox.py +++ b/tests/_cli/test_sandbox.py @@ -857,6 +857,7 @@ def test_uv_export_script_requirements_txt_resolves_relative_paths( to a temp requirements file where uv resolves them relative to CWD instead. """ from unittest.mock import MagicMock, patch + from marimo._cli.sandbox import _uv_export_script_requirements_txt script_path = tmp_path / "subdir" / "notebook.py" @@ -864,7 +865,9 @@ def test_uv_export_script_requirements_txt_resolves_relative_paths( script_path.write_text("# placeholder") mock_result = MagicMock() - mock_result.stdout = "-e ../../\n../other_pkg\nnumpy==1.26.0\n/absolute/path\n\n" + mock_result.stdout = ( + "-e ../../\n../other_pkg\nnumpy==1.26.0\n/absolute/path\n\n" + ) with patch("subprocess.run", return_value=mock_result): lines = _uv_export_script_requirements_txt(str(script_path)) @@ -874,10 +877,15 @@ def test_uv_export_script_requirements_txt_resolves_relative_paths( expected_non_editable = str((script_dir / "../other_pkg").resolve()) # Editable relative path resolved to absolute - assert any(l.startswith("-e ") and expected_editable in l for l in lines) + assert any( + line.startswith("-e ") and expected_editable in line for line in lines + ) # Non-editable relative path resolved to absolute - assert any(expected_non_editable in l and not l.startswith("-e ") for l in lines) + assert any( + expected_non_editable in line and not line.startswith("-e ") + for line in lines + ) # Regular dep unchanged - assert any("numpy==1.26.0" in l for l in lines) + assert any("numpy==1.26.0" in line for line in lines) # Absolute path unchanged - assert any("/absolute/path" in l for l in lines) + assert any("/absolute/path" in line for line in lines) From 73fa8879d08c28a44a35c0afb0b3dd3f3076b7f1 Mon Sep 17 00:00:00 2001 From: Vishak Baddur Date: Mon, 6 Apr 2026 08:31:49 -0500 Subject: [PATCH 3/7] fix: handle env markers and comments in path resolution, fix subprocess patch --- marimo/_cli/sandbox.py | 17 +++++++++++++---- tests/_cli/test_sandbox.py | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/marimo/_cli/sandbox.py b/marimo/_cli/sandbox.py index b6a88592f86..d2dc8bcfb05 100644 --- a/marimo/_cli/sandbox.py +++ b/marimo/_cli/sandbox.py @@ -213,11 +213,20 @@ def _uv_export_script_requirements_txt( resolved = [] for line in lines: editable = line.startswith("-e ") - path = line[3:].strip() if editable else line.strip() - if path.startswith("."): - path = str((script_dir / path).resolve()) + rest = line[3:].strip() if editable else line.strip() + # Split off any environment markers ("; ...") or inline comments ("# ...") + # so we only resolve the path token itself. + for sep in (" ;", " #"): + if sep in rest: + path_token, remainder = rest.split(sep, 1) + remainder = sep.lstrip() + remainder + break + else: + path_token, remainder = rest, "" + if path_token.startswith("."): + path_token = str((script_dir / path_token).resolve()) prefix = "-e " if editable else "" - resolved.append(f"{prefix}{path}") + resolved.append(f"{prefix}{path_token}{remainder}") return resolved diff --git a/tests/_cli/test_sandbox.py b/tests/_cli/test_sandbox.py index 05bf090b3d7..da666b8c686 100644 --- a/tests/_cli/test_sandbox.py +++ b/tests/_cli/test_sandbox.py @@ -866,10 +866,15 @@ def test_uv_export_script_requirements_txt_resolves_relative_paths( mock_result = MagicMock() mock_result.stdout = ( - "-e ../../\n../other_pkg\nnumpy==1.26.0\n/absolute/path\n\n" + "-e ../../\n" + "../other_pkg\n" + "../pkg_with_marker ; python_version<'3.12'\n" + "numpy==1.26.0\n" + "/absolute/path\n" + "\n" ) - with patch("subprocess.run", return_value=mock_result): + with patch("marimo._cli.sandbox.subprocess.run", return_value=mock_result): lines = _uv_export_script_requirements_txt(str(script_path)) script_dir = script_path.resolve().parent @@ -885,6 +890,12 @@ def test_uv_export_script_requirements_txt_resolves_relative_paths( expected_non_editable in line and not line.startswith("-e ") for line in lines ) + # Relative path with environment marker: path resolved, marker preserved + expected_marker_path = str((script_dir / "../pkg_with_marker").resolve()) + assert any( + expected_marker_path in line and "python_version<'3.12'" in line + for line in lines + ) # Regular dep unchanged assert any("numpy==1.26.0" in line for line in lines) # Absolute path unchanged From e8045e8150b8499cdd999c8a34c5fbc98625480a Mon Sep 17 00:00:00 2001 From: Vishak Baddur Date: Thu, 9 Apr 2026 09:58:47 -0500 Subject: [PATCH 4/7] fix: preserve separator when splitting env markers and comments in path resolution --- marimo/_cli/sandbox.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/marimo/_cli/sandbox.py b/marimo/_cli/sandbox.py index d2dc8bcfb05..4b1e2f08112 100644 --- a/marimo/_cli/sandbox.py +++ b/marimo/_cli/sandbox.py @@ -216,11 +216,20 @@ def _uv_export_script_requirements_txt( rest = line[3:].strip() if editable else line.strip() # Split off any environment markers ("; ...") or inline comments ("# ...") # so we only resolve the path token itself. - for sep in (" ;", " #"): - if sep in rest: - path_token, remainder = rest.split(sep, 1) - remainder = sep.lstrip() + remainder + # Preserve the original separator exactly. Environment markers + # may appear without a leading space (e.g. ;python_version<"3.12"). + # Inline comments must be preceded by whitespace to be valid. + marker_idx = rest.find(";") + comment_idx = -1 + for i, char in enumerate(rest): + if char == "#" and i > 0 and rest[i - 1].isspace(): + comment_idx = i break + split_points = [idx for idx in (marker_idx, comment_idx) if idx != -1] + if split_points: + split_idx = min(split_points) + path_token = rest[:split_idx] + remainder = rest[split_idx:] else: path_token, remainder = rest, "" if path_token.startswith("."): From c3d0b351b97c6893e302ea7d33c99c248fa83bf4 Mon Sep 17 00:00:00 2001 From: dmadisetti Date: Tue, 21 Apr 2026 09:48:45 -0700 Subject: [PATCH 5/7] tidy: break out into a function --- marimo/_cli/sandbox.py | 54 +++++++++--------------- tests/_cli/test_sandbox.py | 86 +++++++++++++------------------------- 2 files changed, 49 insertions(+), 91 deletions(-) diff --git a/marimo/_cli/sandbox.py b/marimo/_cli/sandbox.py index 4b1e2f08112..68e4bed4e7d 100644 --- a/marimo/_cli/sandbox.py +++ b/marimo/_cli/sandbox.py @@ -182,6 +182,22 @@ 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[3:] if line.startswith("-e ") else line + 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,41 +218,11 @@ def _uv_export_script_requirements_txt( capture_output=True, text=True, ) - lines = result.stdout.split("\n") - - # uv export returns local paths relative to the script file, but these - # lines get written to a temp requirements file consumed by - # `uv run --with-requirements`, which resolves relative paths from CWD. - # Convert relative paths to absolute using the script's directory as base. - # Applies to both editable (-e ../../) and non-editable (../../) sources. script_dir = Path(name).resolve().parent - resolved = [] - for line in lines: - editable = line.startswith("-e ") - rest = line[3:].strip() if editable else line.strip() - # Split off any environment markers ("; ...") or inline comments ("# ...") - # so we only resolve the path token itself. - # Preserve the original separator exactly. Environment markers - # may appear without a leading space (e.g. ;python_version<"3.12"). - # Inline comments must be preceded by whitespace to be valid. - marker_idx = rest.find(";") - comment_idx = -1 - for i, char in enumerate(rest): - if char == "#" and i > 0 and rest[i - 1].isspace(): - comment_idx = i - break - split_points = [idx for idx in (marker_idx, comment_idx) if idx != -1] - if split_points: - split_idx = min(split_points) - path_token = rest[:split_idx] - remainder = rest[split_idx:] - else: - path_token, remainder = rest, "" - if path_token.startswith("."): - path_token = str((script_dir / path_token).resolve()) - prefix = "-e " if editable else "" - resolved.append(f"{prefix}{path_token}{remainder}") - return resolved + 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 da666b8c686..19fbcf53a42 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 ( @@ -847,56 +845,30 @@ def test_build_sandbox_venv_with_additional_deps(tmp_path: Path) -> None: cleanup_sandbox_dir(sandbox_dir) -def test_uv_export_script_requirements_txt_resolves_relative_paths( - tmp_path: Path, -) -> None: - """Test that relative paths in uv export output are resolved to absolute paths. - - Regression test for https://github.com/marimo-team/marimo/issues/8980. - uv export returns paths relative to the script file, but these get written - to a temp requirements file where uv resolves them relative to CWD instead. - """ - from unittest.mock import MagicMock, patch - - from marimo._cli.sandbox import _uv_export_script_requirements_txt - - script_path = tmp_path / "subdir" / "notebook.py" - script_path.parent.mkdir(parents=True) - script_path.write_text("# placeholder") - - mock_result = MagicMock() - mock_result.stdout = ( - "-e ../../\n" - "../other_pkg\n" - "../pkg_with_marker ; python_version<'3.12'\n" - "numpy==1.26.0\n" - "/absolute/path\n" - "\n" - ) - - with patch("marimo._cli.sandbox.subprocess.run", return_value=mock_result): - lines = _uv_export_script_requirements_txt(str(script_path)) - - script_dir = script_path.resolve().parent - expected_editable = str((script_dir / "../../").resolve()) - expected_non_editable = str((script_dir / "../other_pkg").resolve()) - - # Editable relative path resolved to absolute - assert any( - line.startswith("-e ") and expected_editable in line for line in lines - ) - # Non-editable relative path resolved to absolute - assert any( - expected_non_editable in line and not line.startswith("-e ") - for line in lines - ) - # Relative path with environment marker: path resolved, marker preserved - expected_marker_path = str((script_dir / "../pkg_with_marker").resolve()) - assert any( - expected_marker_path in line and "python_version<'3.12'" in line - for line in lines - ) - # Regular dep unchanged - assert any("numpy==1.26.0" in line for line in lines) - # Absolute path unchanged - assert any("/absolute/path" in line for line in lines) +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 and "py<'3.12'" in result + # Inline comment + result = _resolve_local_path_line("../pkg # via foo", d) + assert _r("../pkg") in result and "# 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) == "" From 02508550383ebca91b7178205706c020e5035134 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:49:37 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- marimo/_cli/sandbox.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/marimo/_cli/sandbox.py b/marimo/_cli/sandbox.py index 68e4bed4e7d..d7c7f9e57c3 100644 --- a/marimo/_cli/sandbox.py +++ b/marimo/_cli/sandbox.py @@ -185,10 +185,12 @@ def include_features(dep: str, features: list[DepFeatures]) -> str: 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")) + >>> _resolve_local_path_line( + ... "-e ../pkg ; py<'3.12' # via foo", Path("/a/b") + ... ) '-e /a/pkg ; py<\\'3.12\\' # via foo' """ - rest = line[3:] if line.startswith("-e ") else line + rest = line.removeprefix("-e ") path_and_comment, _, _ = rest.partition(";") path_token, _, _ = path_and_comment.partition(" #") path_token = path_token.rstrip() From bceb60f2a81f18da1bc21cc859e034edfdace774 Mon Sep 17 00:00:00 2001 From: dmadisetti Date: Tue, 21 Apr 2026 10:36:06 -0700 Subject: [PATCH 7/7] fix: ruff check --fix --- tests/_cli/test_sandbox.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/_cli/test_sandbox.py b/tests/_cli/test_sandbox.py index 19fbcf53a42..312ee22a21f 100644 --- a/tests/_cli/test_sandbox.py +++ b/tests/_cli/test_sandbox.py @@ -857,10 +857,12 @@ def test_resolve_local_path_line() -> None: 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 and "py<'3.12'" in result + 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 and "# via foo" in result + 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