Skip to content

fix: preserve [[tool.uv.index]] semantics in marimo edit --sandbox - #10548

Open
jacobcbeaudin wants to merge 2 commits into
marimo-team:mainfrom
jacobcbeaudin:fix/sandbox-index-semantics
Open

fix: preserve [[tool.uv.index]] semantics in marimo edit --sandbox#10548
jacobcbeaudin wants to merge 2 commits into
marimo-team:mainfrom
jacobcbeaudin:fix/sandbox-index-semantics

Conversation

@jacobcbeaudin

@jacobcbeaudin jacobcbeaudin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

This pull request was authored by a coding agent. (Claude Code, driven and reviewed by @jacobcbeaudin)

📝 Summary

Closes #10547

marimo edit --sandbox changed each [[tool.uv.index]] entry in a notebook's PEP 723 metadata into a bare --index <url> flag. This removed the three meanings that uv gives to an index entry. This PR maps each entry to the flag that keeps its meaning.

Before / after — for this script metadata:

# /// script
# dependencies = ["internal-pkg"]
#
# [[tool.uv.index]]
# url = "https://pypi.org/simple/"
# default = true
#
# [[tool.uv.index]]
# name = "internal"
# url = "https://internal.example.com/simple/"
# ///
flags given to uv run
before --index https://pypi.org/simple/ --index https://internal.example.com/simple/
after --default-index https://pypi.org/simple/ --index internal=https://internal.example.com/simple/

The old flags broke three behaviors:

  • default = true — uv examines a default index last. A plain --index made it first and put it before every other index (in the example above, uv never examined internal). The entry now becomes --default-index. If more than one entry has default = true, marimo uses the first entry and logs a warning for the others. uv also uses only the first entry (behavior seen on uv 0.12; the uv docs do not specify this case).
  • name — the name selects the UV_INDEX_<NAME>_USERNAME / UV_INDEX_<NAME>_PASSWORD credentials. The old flags removed the name, which broke authenticated indexes in sandbox mode. A named entry now becomes --index <name>=<url>, and a named default becomes --default-index <name>=<url>.
  • explicit = true — uv has no flag for this key, so marimo cannot fully obey it. marimo puts explicit entries after the regular indexes. These entries can still supply packages that [tool.uv.sources] does not assign to them; the docs now say so.

marimo now skips a non-table entry in the index array (valid TOML that uv rejects with a schema error). Before, such an entry crashed flag construction.

Related fix: the code swallowed a stage-1 uv export --script failure (except CalledProcessError: pass with capture_output=True). Thus an error such as a 401 from an authenticated index appeared later as a confusing "package not found" error. The fallback now logs uv's stderr as a warning when the script contains a PEP 723 block (a new PyProjectReader.has_script_metadata property makes this check). The log redacts URL query strings: uv redacts userinfo credentials, but some registries put auth tokens in query strings. A plain .py file with no metadata block keeps its silent fallback.

The PR also types [[tool.uv.index]] entries as a TypedDict. The old annotation, list[dict[str, str]], was wrong for the boolean keys.

uv supports all flags used here since uv 0.4.23. The old bare --index form had the same minimum version.

📋 Pre-Review Checklist

  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • Video or media evidence is provided for any visual changes (optional).

✅ Merge Checklist

  • I have read the contributor guidelines.
  • Documentation has been updated where applicable, including docstrings for API changes.
  • Tests have been added for the changes made.

marimo edit --sandbox flattened every [[tool.uv.index]] entry to a bare
--index <url> flag, which dropped three semantics uv defines:

- default = true: uv consults a default index last, but the flattening
  promoted it to first priority, shadowing every other index. It now
  maps to --default-index (the <name>=<url> form is used when the
  default index is named, keeping its credential selector). Only the
  first default entry is honored, matching uv, which ignores the rest;
  a warning is logged for later ones.
- name: the credential selector for UV_INDEX_<NAME>_USERNAME/PASSWORD.
  Named entries now map to --index <name>=<url> so authenticated
  indexes work again.
- explicit = true: has no CLI equivalent, so it cannot be enforced;
  explicit entries are appended after regular indexes. They can still
  serve packages beyond their [tool.uv.sources] assignments, which the
  docs now note.

Non-table entries in the index array (valid TOML that uv rejects with
a schema error) are skipped instead of crashing flag construction.

Separately, a stage-1 'uv export --script' failure was silently
swallowed (except CalledProcessError: pass with capture_output=True),
so e.g. a 401 from an authenticated index surfaced later as a confusing
'package not found' resolution error. The fallback now logs uv's stderr
as a warning when the script contains a PEP 723 block, with URL query
strings redacted (uv redacts userinfo credentials but not query-string
tokens); plain files keep the silent fallback they rely on.

Also types [[tool.uv.index]] entries as a TypedDict (the previous
list[dict[str, str]] annotation was wrong for the boolean keys).

The flags emitted here are all supported since uv 0.4.23, the same
floor the previous bare --index form had.

Fixes marimo-team#10547

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview Aug 14, 2026 1:50am

Request Review

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

…larify prose

Review-driven polish; no behavior change except log-message wording:

- Add PyProjectReader.has_script_metadata and use it in the export
  fallback so the guard reads as what it means.
- Dedupe the [[tool.uv.index]] tests with _uv_cmd_for_indexes, and the
  export-failure tests with _captured_marimo_warnings and
  _failing_uv_export; assertions are unchanged.
- Assert that the multiple-default warning fires; it was the one
  behavior with no failing test on regression.
- Note that first-default-wins is observed uv behavior (uv 0.12), not
  documented, in the comment and the docs.
- Document that IndexConfig entries are unvalidated user TOML, and
  comment the url-less skip branch.
- Rewrite the PR's comments, docstrings, log messages, and docs section
  in short, active sentences (ASD-STE100-informed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mscolnick

Copy link
Copy Markdown
Contributor

@cubic-dev-ai

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai

@mscolnick I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="marimo/_utils/inline_script_metadata.py">

<violation number="1" location="marimo/_utils/inline_script_metadata.py:77">
P2: When a PEP 723 block contains valid empty TOML, `has_script_metadata` returns false and stage-1 `uv export` failures stay silent. Preserve metadata presence separately from the parsed mapping instead of deriving it from truthiness.</violation>

<violation number="2" location="marimo/_utils/inline_script_metadata.py:89">
P3: `index_configs` claims every element is an `IndexConfig` even though it deliberately exposes non-table TOML entries. Return a type that includes unvalidated values, or validate and filter entries before returning so typed callers cannot skip the required narrowing.

(Based on your team's feedback about strict typing over `Any` and unchecked assertions.)</violation>
</file>
Architecture diagram
sequenceDiagram
    participant CLI as CLI Entry
    participant Sandbox as Sandbox Builder
    participant Reader as PyProjectReader
    participant UV as uv Process
    participant Logger as Logger

    Note over CLI,Sandbox: marimo edit --sandbox flow

    CLI->>Sandbox: construct_uv_command()
    Sandbox->>Reader: read notebook metadata
    
    alt PEP 723 metadata present (has_script_metadata)
        Sandbox->>UV: uv export --script (try)
        alt Export succeeds
            UV-->>Sandbox: requirement lines
        else Export fails (CalledProcessError)
            UV-->>Sandbox: stderr (401, etc.)
            Sandbox->>Logger: CHANGED: log warning with redacted URLs
            Logger-->>Sandbox: warning logged
            Sandbox->>Sandbox: fallback to raw dependency list
        end
    else No metadata block
        Sandbox->>Sandbox: silent fallback (existing behavior)
    end

    Note over Sandbox,UV: Index flag construction (CHANGED logic)

    Sandbox->>Reader: get index_configs (typed entries)
    Reader-->>Sandbox: list of IndexConfig
    
    loop Each [[tool.uv.index]] entry
        alt Entry is not a dict (valid TOML, invalid uv)
            Sandbox->>Sandbox: skip entry (no crash)
        else Entry missing url
            Sandbox->>Sandbox: skip entry (let uv report)
        else Entry has default=true (first one)
            Sandbox->>Sandbox: map to --default-index [name=]url
        else Entry has default=true (duplicate)
            Sandbox->>Logger: warn about duplicate default
            Sandbox->>Sandbox: skip entry
        else Entry has explicit=true
            Sandbox->>Sandbox: collect as --index [name=]url (deferred)
        else Regular entry
            Sandbox->>Sandbox: map to --index [name=]url (in order)
        end
    end
    
    Sandbox->>Sandbox: append explicit entries after regular ones
    
    Sandbox->>UV: uv run with constructed flags
    Note over UV: --default-index for default entries<br/>--index name=url for named entries<br/>bare --index for unnamed entries
Loading

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

header of a markdown notebook). It is empty when the file has
no metadata.
"""
return bool(self.project)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a PEP 723 block contains valid empty TOML, has_script_metadata returns false and stage-1 uv export failures stay silent. Preserve metadata presence separately from the parsed mapping instead of deriving it from truthiness.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_utils/inline_script_metadata.py, line 77:

<comment>When a PEP 723 block contains valid empty TOML, `has_script_metadata` returns false and stage-1 `uv export` failures stay silent. Preserve metadata presence separately from the parsed mapping instead of deriving it from truthiness.</comment>

<file context>
@@ -48,6 +66,16 @@ def from_script(script: str) -> PyProjectReader:
+        header of a markdown notebook). It is empty when the file has
+        no metadata.
+        """
+        return bool(self.project)
+
     @property
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first part is correct: a block that contains only comments or empty comment lines parses to {}, so has_script_metadata returns False. (A block with zero lines between the markers does not match the PEP 723 regular expression. This block parses to None, the same as no block.)

But the failure in your comment cannot occur. I did a test with uv 0.12.3:

  1. uv export --script gives exit code 0 for all empty-block types (comments-only, empty-comment, and zero-line blocks). An empty dependency set resolves with no index and no network. Thus the except branch does not operate for these files.
  2. If the uv export operation does not complete, the fallback result is not worse. For an empty block, the fallback gives [], and a correct export operation gives no packages (one empty line). The two results give the same set of packages: none. Thus a warning gives the user no information.

The warning is for a block that has content. One example is a 401 from an authenticated index. Content makes the dict not empty, and the warning shows. I did a test of this path also. uv export gives exit code 2 for a block with an incorrect [tool.uv] table. The new warning shows uv's error text.

A new flag that shows if a block was found will add data to the constructor and to both factory methods. A user will see no change. Because of this, we keep the bool(self.project) check.


@property
def index_configs(self) -> list[dict[str, str]]:
def index_configs(self) -> list[IndexConfig]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: index_configs claims every element is an IndexConfig even though it deliberately exposes non-table TOML entries. Return a type that includes unvalidated values, or validate and filter entries before returning so typed callers cannot skip the required narrowing.

(Based on your team's feedback about strict typing over Any and unchecked assertions.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_utils/inline_script_metadata.py, line 89:

<comment>`index_configs` claims every element is an `IndexConfig` even though it deliberately exposes non-table TOML entries. Return a type that includes unvalidated values, or validate and filter entries before returning so typed callers cannot skip the required narrowing.

(Based on your team's feedback about strict typing over `Any` and unchecked assertions.) </comment>

<file context>
@@ -58,7 +86,7 @@ def extra_index_urls(self) -> list[str]:
 
     @property
-    def index_configs(self) -> list[dict[str, str]]:
+    def index_configs(self) -> list[IndexConfig]:
         # See https://docs.astral.sh/uv/reference/settings/#index
         return self.project.get("tool", {}).get("uv", {}).get("index", [])  # type: ignore[no-any-return]
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your comment is correct: the annotation shows the correct structure, but marimo does not make sure that the data agrees with the annotation. But we selected this pattern, and the full file uses it:

  • Each PyProjectReader property shows the correct structure of TOML that is not validated (dependencies -> list[str], extra_index_urls -> list[str]). The type: ignore identifies the trust boundary.
  • This PR made the annotation more accurate, not less. The old annotation was list[dict[str, str]], which was incorrect for the boolean keys.
  • The docstring tells you that the type is "a goal, not a guarantee".
  • construct_uv_flags is the only consumer. It examines each entry with isinstance, and the check has a comment and a test. marimo does not remove incorrect entries, because uv reads the script itself and rejects them. My test on uv 0.12.3 gave invalid type: string "...", expected struct IndexWire and exit code 2. The new fallback warning shows that error to the user.

If we add one more consumer with type annotations, we can move the validation into the property. That change will make the property the only accessor in the file that validates its value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--sandbox drops [[tool.uv.index]] semantics (default, name, explicit, sources) and silently discards uv export --script failures

2 participants