Skip to content

feat: use variable name as download filename in dataframe viewer#8227

Merged
mscolnick merged 9 commits intomarimo-team:mainfrom
AhmadYasser1:feat/download-filename-variable
Feb 10, 2026
Merged

feat: use variable name as download filename in dataframe viewer#8227
mscolnick merged 9 commits intomarimo-team:mainfrom
AhmadYasser1:feat/download-filename-variable

Conversation

@AhmadYasser1
Copy link
Contributor

Summary

Downloaded files from the dataframe viewer used a generic download.csv name. Now threads the variable name from the plugin through to the download action so the file is named after the variable (e.g. my_dataframe.csv).

Closes #8095

Test Plan

  • Added test for download filename propagation

AhmadYasser1 and others added 2 commits February 8, 2026 03:20
The rich dataframe viewer now names download files after the variable
(e.g., "my_df.csv" instead of "download.csv"). The backend already
extracts and sends the variable name; this change threads it through
the frontend components to the download action.

Closes marimo-team#8095
@vercel
Copy link

vercel bot commented Feb 8, 2026

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

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Feb 10, 2026 4:18pm

Request Review

- Add downloadFileName to the withData Zod schema so it isn't
  stripped during parsing
- Sanitize the filename: trim whitespace, strip existing extension
  to prevent double extensions, fall back to "download" if empty
Copy link
Collaborator

@Light2Dark Light2Dark left a comment

Choose a reason for hiding this comment

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

The donwloadFileName is not passed from the backend. Is that a follow-up?

Infer the variable name via inspect.currentframe() (same pattern as
DataFramePlugin) and pass it as download-file-name in component args
so the frontend can use it for the download filename.
@AhmadYasser1
Copy link
Contributor Author

Good catch — I've now added the backend side. The table's __init__ infers the variable name via inspect.currentframe() (same pattern as DataFramePlugin) and passes it as download-file-name in the component args. Also added a test for it.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Threads a dataframe/table variable name through the plugin/data-table stack so that downloads from the rich dataframe viewer (and tables) use a meaningful filename instead of a generic download.*.

Changes:

  • Added dataframeName support to the dataframe viewer plugin and passed it into the shared data-table download action.
  • Added downloadFileName support to the table plugin and propagated it through the data-table components.
  • Updated download action to derive ${baseName}.${ext} and added a Python unit test for table filename propagation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/_plugins/ui/_impl/test_table.py Adds a unit test asserting ui.table() sets the download filename argument.
marimo/_plugins/ui/_impl/table.py Infers a download filename via frame locals and emits it as a component arg.
frontend/src/plugins/impl/data-frames/DataFramePlugin.tsx Accepts dataframeName and passes it to the shared data-table as downloadFileName.
frontend/src/plugins/impl/DataTablePlugin.tsx Adds downloadFileName to plugin schema/props and passes it into the table component tree.
frontend/src/components/data-table/download-actions.tsx Uses downloadFileName to name downloaded files instead of download.*.
frontend/src/components/data-table/data-table.tsx Plumbs downloadFileName into TableActions.
frontend/src/components/data-table/TableActions.tsx Passes downloadFileName into DownloadAs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +506 to +523
if frame is not None and frame.f_back is not None:
for var_name, var_value in frame.f_back.f_locals.items():
if var_value is data:
download_file_name = var_name
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The variable-name inference compares locals against data after add_selection_column() may have converted/replaced the original input (notably for dataframe inputs). For ui.table(df) this will likely fail to match the caller’s df variable (identity check won’t match the new object), so downloads will still fall back to "download". Consider capturing the original argument before mutation (or compute the name before add_selection_column()), and use that for the identity match / filename propagation.

Copilot uses AI. Check for mistakes.
Comment on lines +518 to +526
try:
frame = inspect.currentframe()
if frame is not None and frame.f_back is not None:
for var_name, var_value in frame.f_back.f_locals.items():
if var_value is data:
download_file_name = var_name
break
except Exception:
pass
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

inspect.currentframe() can create reference cycles; without explicitly clearing the frame reference, this can delay GC in long-running sessions. Consider using a finally block to del frame (and any other frame refs) after the lookup to avoid leaking frames.

Suggested change
try:
frame = inspect.currentframe()
if frame is not None and frame.f_back is not None:
for var_name, var_value in frame.f_back.f_locals.items():
if var_value is data:
download_file_name = var_name
break
except Exception:
pass
frame = None
frame_back = None
try:
frame = inspect.currentframe()
if frame is not None:
frame_back = frame.f_back
if frame_back is not None:
for var_name, var_value in frame_back.f_locals.items():
if var_value is data:
download_file_name = var_name
break
except Exception:
pass
finally:
del frame
del frame_back

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +164
const rawName = (props.downloadFileName ?? "").trim();
const baseName = rawName.replace(/\.[^/.]+$/, "") || "download";
downloadByURL(downloadUrl, `${baseName}.${ext}`);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

This duplicates filename-extension stripping logic that already exists in frontend/src/utils/filenames.ts (withoutExtension/replace). Using the shared utility here would reduce duplication and keep filename rules consistent across download entry points.

Copilot uses AI. Check for mistakes.
Comment on lines 159 to +164
onSelect={async () => {
const downloadUrl = await getDownloadUrl(option.format);
const ext = option.format;
downloadByURL(downloadUrl, `download.${ext}`);
const rawName = (props.downloadFileName ?? "").trim();
const baseName = rawName.replace(/\.[^/.]+$/, "") || "download";
downloadByURL(downloadUrl, `${baseName}.${ext}`);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The new filename propagation logic isn’t covered by a frontend test. Since the repo already has Vitest coverage for the data-table plugin, consider adding a unit test that asserts DownloadAs calls downloadByURL with the expected ${baseName}.${ext} when downloadFileName is provided (and falls back to download.* when it isn’t).

Copilot uses AI. Check for mistakes.
download_file_name = var_name
break
except Exception:
pass
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
pass
LOGGER.debug(
"Failed to infer download file name from caller frame.",
exc_info=True,
)

Copilot uses AI. Check for mistakes.
- move variable name inference before add_selection_column() so the
  identity check still matches the caller's original variable
- clean up inspect.currentframe() refs in a finally block to avoid
  reference cycles in long-running sessions
- log the exception instead of bare pass
- use shared Filenames.withoutExtension() instead of inline regex
@AhmadYasser1
Copy link
Contributor Author

pushed fixes for the review feedback:

  • moved the variable name inference before add_selection_column() so the identity check works correctly on the original data
  • added finally: del frame / del frame_back to avoid reference cycles
  • swapped the inline regex for the shared Filenames.withoutExtension() utility
  • added LOGGER.debug(...) instead of bare pass

Copy link
Collaborator

@Light2Dark Light2Dark left a comment

Choose a reason for hiding this comment

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

.

Comment on lines +509 to +528
download_file_name = "download"
frame = None
frame_back = None
try:
frame = inspect.currentframe()
if frame is not None:
frame_back = frame.f_back
if frame_back is not None:
for var_name, var_value in frame_back.f_locals.items():
if var_value is data:
download_file_name = var_name
break
except Exception:
LOGGER.debug(
"Failed to infer download file name from caller frame.",
exc_info=True,
)
finally:
del frame
del frame_back
Copy link
Collaborator

Choose a reason for hiding this comment

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

i would maybe refator this to a utility function, since dataframe.py uses it too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call, extracted it into marimo/_utils/variable_name.py — both table.py and dataframe.py now use infer_variable_name(value, fallback) instead of duplicating the frame-walking logic.

Move the `inspect.currentframe()` variable-name lookup used by both
`table.py` and `dataframe.py` into `marimo/_utils/variable_name.py`.
Both callers now use `infer_variable_name(value, fallback)` instead of
duplicating the frame-walking logic.
Copy link
Collaborator

@Light2Dark Light2Dark left a comment

Choose a reason for hiding this comment

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

lgtm, just a small change for correctness

Comment on lines +5 to +7
from typing import Any

LOGGER = logging.getLogger(__name__)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know why I can't suggest code anymore, sigh GitHub UI

from marimo import _loggers

LOGGER = _loggers.marimo_logger()

Removed unused logging import.
@mscolnick mscolnick merged commit af37b9b into marimo-team:main Feb 10, 2026
25 of 39 checks passed
@AhmadYasser1 AhmadYasser1 deleted the feat/download-filename-variable branch February 10, 2026 16:35
@github-actions
Copy link

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.19.10-dev38

@AhmadYasser1
Copy link
Contributor Author

Thanks @mscolnick and @Light2Dark ! Appreciate the help and feedback :)

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dataframe download from rich dataframe viewer automatically names download file by variable name

4 participants