Skip to content

--sandbox fails for scripts with relative path sources in [tool.uv.sources] #8980

@zpincus

Description

@zpincus

Describe the bug

marimo edit --sandbox fails when a script's inline metadata uses relative path sources in [tool.uv.sources]. The sandbox machinery calls uv export --script which returns paths relative to the script file, then writes them to a temp requirements file. When uv run --with-requirements <tempfile> processes that file, it resolves those paths as relative to the cwd, not the script's original directory. In the reasonably common scenario where you're not running marimo in the script's directory (e.g. marimo edit --sandbox path/to/notebook.py) then this will cause an error in path resolution.

FWIW, both editable and non-editable path sources are affected. uv export outputs paths like -e ../../ for editable sources and bare ../../ for non-editable ones.

Proposed fix

Convert relative paths to absolute paths in marimo/_cli/sandbox.py:_uv_export_script_requirements_txt(), using the script's parent directory as the base. This function has a single call site (_resolve_requirements_txt_lines) and AFAICT there's no scenario where it would be important to preserve relative paths.

 def _uv_export_script_requirements_txt(
     name: str | None,
 ) -> list[str]:
     if not name:
         return []
 
     result = subprocess.run(
         [
             find_uv_bin(),
             "export",
             "--no-hashes",
             "--no-annotate",
             "--no-header",
             "--script",
             name,
         ],
         check=True,
         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 will be written to a temp requirements file and consumed
+    # by `uv run --with-requirements`, which resolves relative paths
+    # from CWD. Convert to absolute paths to avoid mismatch.
+    # Applies to both editable (-e ../../) and non-editable (../../)
+    # path 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

I'm happy to submit a PR or someone can just implement the fix directly.

BTW, the above assumes all relative paths are prefixed with . (either ../[something] or ./[something]), which uv currently does. If that assumption feels dangerous, it could resolve any paths that don't start with /. Or it could even resolve everything, since pathlib will just return the second path when asked to join two absolute paths.

Will you submit a PR?

  • Yes

Environment

Details
{
       "marimo": "0.22.0",
       "editable": false,
       "OS": "Darwin",
       "OS Version": "24.6.0",
       "Processor": "arm",
       "Python Version": "3.13.7",
       "Locale": "C/en_US",
       "Binaries": {
         "Browser": "146.0.7680.179",
         "Node": "v25.6.1",
         "uv": "0.10.2 (Homebrew 2026-02-10)"
       },
       "Dependencies": {
         "click": "8.3.1",
         "docutils": "0.22.4",
         "itsdangerous": "2.2.0",
         "jedi": "0.19.2",
         "markdown": "3.10.2",
         "narwhals": "2.18.1",
         "packaging": "26.0",
         "psutil": "7.2.2",
         "pygments": "2.20.0",
         "pymdown-extensions": "10.21.2",
         "pyyaml": "6.0.3",
         "starlette": "1.0.0",
         "tomlkit": "0.14.0",
         "typing-extensions": "4.15.0",
         "uvicorn": "0.42.0",
         "websockets": "16.0"
       },
       "Optional Dependencies": {
         "loro": "1.10.3",
         "polars": "1.39.3",
         "pytest": "9.0.2",
         "python-lsp-ruff": "2.3.0",
         "python-lsp-server": "1.14.0",
         "ruff": "0.15.8"
       },
       "Experimental Flags": {}
     }

Code to reproduce

mkdir -p /tmp/repro/subdir/nested && cd /tmp/repro

cat > pyproject.toml << 'EOF'
[project]
name = "dummy-pkg"
version = "0.1.0"
requires-python = ">=3.13"
EOF

cat > subdir/nested/notebook.py << 'PYEOF'
# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "marimo>=0.22.0",
#     "dummy-pkg",
# ]
#
# [tool.uv.sources]
# dummy-pkg = { path = "../../", editable = true }
# ///

import marimo
__generated_with = "0.22.0"
app = marimo.App()

@app.cell
def _():
    print("hello")

if __name__ == "__main__":
    app.run()
PYEOF

marimo edit --sandbox subdir/nested/notebook.py

Expected: sandbox resolves ../../ from the script's location (subdir/nested/), finding the root pyproject.toml.

Actual:

× Failed to resolve `--with` requirement
╰─▶ /private does not appear to be a Python project, as neither
    `pyproject.toml` nor `setup.py` are present in the directory

../../ is resolved from CWD (/tmp/repro), landing at /private/tmp (macOS /tmp symlink) instead of the project root.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions