@marimo-team/islands 0.23.17-dev3 → 0.23.17-dev30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-f9vLjQiF.js → ConnectedDataExplorerComponent-BEb8RZam.js} +13 -13
- package/dist/{ErrorBoundary-DhLqwSWA.js → ErrorBoundary-8ZHte0h9.js} +3 -3
- package/dist/{ImageComparisonComponent-C3y7L3eJ.js → ImageComparisonComponent-fdY0LUuf.js} +38 -27
- package/dist/{any-language-editor-DifE-fl7.js → any-language-editor-CioNaP-T.js} +5 -5
- package/dist/{button-BSm8IXRU.js → button-8aS1QcHr.js} +4 -2
- package/dist/{capabilities-C6OQ5YBO.js → capabilities-D23lgbH_.js} +1 -1
- package/dist/{chat-ui-DsZJj75A.js → chat-ui-CnjFz0bt.js} +16 -16
- package/dist/{check-DP7U0H0G.js → check-CCvJTB2E.js} +1 -1
- package/dist/{common-D747zlkP.js → common-BxnkCaMd.js} +21 -21
- package/dist/{copy-DNq8q3zw.js → copy-D45ZUY3j.js} +2 -2
- package/dist/{dist-DIlxLFXG.js → dist-DgzzIPS4.js} +2 -2
- package/dist/{error-banner-B_GQfxxD.js → error-banner-DHfPl1w3.js} +2 -2
- package/dist/{esm-DzZVaNH4.js → esm-DJsE6wi-.js} +1 -1
- package/dist/{extends-w2_S44bV.js → extends-xQ_2Cylt.js} +4 -4
- package/dist/{formats-Dwq1THhr.js → formats-D10vOEsT.js} +3 -3
- package/dist/{glide-data-editor-B9K6-MJe.js → glide-data-editor-FiwJJzrV.js} +9 -9
- package/dist/{html-to-image-CZ1kLKkq.js → html-to-image-B8KJvlou.js} +2111 -2105
- package/dist/{input-C5K4VBvB.js → input-DtTqU0xh.js} +6 -6
- package/dist/{label-DjoYwqmj.js → label-5IC840tS.js} +2 -2
- package/dist/{loader-DX-IDS7n.js → loader-DP2JkPth.js} +1 -1
- package/dist/main.js +47 -44
- package/dist/{mermaid-hv6IckVw.js → mermaid-BNhC-PrB.js} +5 -5
- package/dist/{process-output-bVfbcx5_.js → process-output-CjS9wS4l.js} +3 -3
- package/dist/{reveal-component-CcSMtt1K.js → reveal-component-D8ttQmaR.js} +11 -11
- package/dist/{spec-CQKL_D3i.js → spec-C7rMyZ_8.js} +4 -4
- package/dist/{strings-DIy9NAmg.js → strings-CITcoFNY.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-CEdAYjb7.js → swiper-component-dl1H9WIW.js} +2 -2
- package/dist/{toDate-CqhBWjvO.js → toDate-kxcuDOWd.js} +3 -3
- package/dist/{tooltip-f-qR0iUx.js → tooltip-BkSv4x8S.js} +3 -3
- package/dist/{types-DicH9Oi0.js → types-kNUyqkDD.js} +1 -1
- package/dist/{useAsyncData-CYOQb1EW.js → useAsyncData-Uo4VFafD.js} +1 -1
- package/dist/{useDateFormatter-Bt2qr_XS.js → useDateFormatter-ZCfHadYP.js} +2 -2
- package/dist/{useDeepCompareMemoize-CSHQeQEI.js → useDeepCompareMemoize-s3ZvmdK3.js} +1 -1
- package/dist/{useIframeCapabilities-kA09ykNq.js → useIframeCapabilities-pLC-ceDP.js} +1 -1
- package/dist/{useLifecycle-CMJGqBaY.js → useLifecycle-CLPBATCv.js} +3 -3
- package/dist/{useTheme-DYfby65G.js → useTheme-CEMsu1Js.js} +2 -2
- package/dist/{vega-component-C7nO5zcf.js → vega-component-DGkcoXu3.js} +10 -10
- package/dist/{zod-BUcyXSdR.js → zod-DdNi9290.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +5 -0
- package/src/components/databases/icons/huggingface.svg +8 -0
- package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +35 -0
- package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +61 -0
- package/src/components/editor/actions/export-dialog/export-dialog.tsx +27 -23
- package/src/components/editor/actions/export-dialog/format-notice.tsx +28 -0
- package/src/components/editor/actions/export-dialog/state.ts +23 -5
- package/src/components/editor/actions/export-dialog/use-export-dialog.ts +7 -15
- package/src/components/editor/chrome/panels/outline/__tests__/useActiveOutline.test.ts +26 -0
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +114 -2
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +14 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +20 -0
- package/src/components/editor/connections/storage/add-storage-form.tsx +10 -0
- package/src/components/editor/connections/storage/as-code.ts +19 -1
- package/src/components/editor/connections/storage/schemas.ts +19 -0
- package/src/components/storage/__tests__/storage-snippets.test.ts +88 -0
- package/src/components/storage/components.tsx +2 -0
- package/src/components/storage/storage-snippets.ts +58 -0
- package/src/core/__tests__/mode.test.ts +85 -0
- package/src/core/codemirror/code-lens/__tests__/extension.test.ts +2 -7
- package/src/core/codemirror/code-lens/extension.ts +0 -4
- package/src/core/codemirror/find-replace/search-highlight.ts +8 -3
- package/src/core/codemirror/theme/__tests__/light.test.ts +59 -123
- package/src/core/codemirror/theme/dark.ts +6 -5
- package/src/core/codemirror/theme/light.ts +6 -4
- package/src/core/config/feature-flag.tsx +0 -2
- package/src/core/dom/outline.ts +25 -2
- package/src/core/hotkeys/hotkeys.ts +2 -0
- package/src/core/mode.ts +11 -9
- package/src/core/network/__tests__/requests-lazy.test.ts +1 -0
- package/src/core/storage/types.ts +1 -0
- package/src/css/app/codemirror.css +11 -7
- package/src/css/globals.css +8 -0
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +20 -1
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +58 -0
- package/src/stories/theme.stories.tsx +71 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtom
|
|
3
|
+
import { useAtom } from "jotai";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { toast } from "@/components/ui/use-toast";
|
|
6
6
|
import {
|
|
7
7
|
updateCellOutputsWithScreenshots,
|
|
8
8
|
useEnrichCellOutputs,
|
|
9
9
|
} from "@/core/export/hooks";
|
|
10
|
-
import { runDuringPresentMode
|
|
10
|
+
import { runDuringPresentMode } from "@/core/mode";
|
|
11
11
|
import { useRequestClient } from "@/core/network/requests";
|
|
12
12
|
import type { ExportAvailabilityResponse } from "@/core/network/types";
|
|
13
13
|
import { useFilename } from "@/core/saving/filename";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
withLoadingToast,
|
|
22
22
|
} from "@/utils/download";
|
|
23
23
|
import { Filenames } from "@/utils/filenames";
|
|
24
|
+
import { Logger } from "@/utils/Logger";
|
|
24
25
|
import { Paths } from "@/utils/paths";
|
|
25
26
|
import { getExportCommand } from "./export-command";
|
|
26
27
|
import { exportNotebook } from "./export-notebook";
|
|
@@ -277,8 +278,6 @@ function useExportDialogAction({
|
|
|
277
278
|
}) {
|
|
278
279
|
const requests = useRequestClient();
|
|
279
280
|
const takeScreenshots = useEnrichCellOutputs();
|
|
280
|
-
const viewState = useAtomValue(viewStateAtom);
|
|
281
|
-
const setViewState = useSetAtom(viewStateAtom);
|
|
282
281
|
const [isExporting, setIsExporting] = useState(false);
|
|
283
282
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
284
283
|
const mountedRef = useRef(true);
|
|
@@ -302,15 +301,7 @@ function useExportDialogAction({
|
|
|
302
301
|
const capturePNG = async () => {
|
|
303
302
|
const capture = () =>
|
|
304
303
|
captureCurrentAppView(dialogRef.current?.parentElement ?? null);
|
|
305
|
-
|
|
306
|
-
await capture();
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
await runDuringPresentMode(capture);
|
|
311
|
-
} finally {
|
|
312
|
-
setViewState(viewState);
|
|
313
|
-
}
|
|
304
|
+
await runDuringPresentMode(capture);
|
|
314
305
|
};
|
|
315
306
|
|
|
316
307
|
const submit = async () => {
|
|
@@ -342,8 +333,9 @@ function useExportDialogAction({
|
|
|
342
333
|
if (mountedRef.current) {
|
|
343
334
|
onClose();
|
|
344
335
|
}
|
|
345
|
-
} catch {
|
|
346
|
-
//
|
|
336
|
+
} catch (error) {
|
|
337
|
+
// Most helpers toast actionable errors, but not all (e.g. updateCellOutputs).
|
|
338
|
+
Logger.error("Export failed", error);
|
|
347
339
|
} finally {
|
|
348
340
|
if (mountedRef.current) {
|
|
349
341
|
setIsExporting(false);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { parseOutline } from "@/core/dom/outline";
|
|
4
|
+
import { findOutlineElements } from "../useActiveOutline";
|
|
5
|
+
|
|
6
|
+
describe("findOutlineElements", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
"Design from the portfolio",
|
|
9
|
+
'Decorative Art of "Spanish California"',
|
|
10
|
+
`The painter's "Study"`,
|
|
11
|
+
])("finds an id-less heading named %s", (name) => {
|
|
12
|
+
const html = `<h3>${name}</h3>`;
|
|
13
|
+
document.body.innerHTML = html;
|
|
14
|
+
|
|
15
|
+
const outline = parseOutline({
|
|
16
|
+
mimetype: "text/html",
|
|
17
|
+
timestamp: 0,
|
|
18
|
+
channel: "output",
|
|
19
|
+
data: html,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(
|
|
23
|
+
findOutlineElements(outline?.items ?? []).map(([element]) => element),
|
|
24
|
+
).toEqual([document.querySelector("h3")]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -1,13 +1,61 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { EditorView } from "@codemirror/view";
|
|
4
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
beforeAll,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
it,
|
|
12
|
+
vi,
|
|
13
|
+
} from "vitest";
|
|
5
14
|
import { SetupMocks } from "@/__mocks__/common";
|
|
6
15
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
16
|
import { ReadonlyCode } from "../readonly-python-code";
|
|
8
17
|
|
|
18
|
+
const themeState = vi.hoisted(() => ({
|
|
19
|
+
value: "light" as "light" | "dark",
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
let originalRangeGetClientRects: PropertyDescriptor | undefined;
|
|
23
|
+
|
|
24
|
+
vi.mock("@/theme/useTheme", async (importOriginal) => {
|
|
25
|
+
const actual = await importOriginal<typeof import("@/theme/useTheme")>();
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
useTheme: () => ({ theme: themeState.value }),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
9
32
|
beforeAll(() => {
|
|
10
33
|
SetupMocks.resizeObserver();
|
|
34
|
+
originalRangeGetClientRects = Object.getOwnPropertyDescriptor(
|
|
35
|
+
Range.prototype,
|
|
36
|
+
"getClientRects",
|
|
37
|
+
);
|
|
38
|
+
Object.defineProperty(Range.prototype, "getClientRects", {
|
|
39
|
+
configurable: true,
|
|
40
|
+
value: () => [],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
// Keep the shared JSDOM prototype unchanged for other test files.
|
|
46
|
+
if (originalRangeGetClientRects) {
|
|
47
|
+
Object.defineProperty(
|
|
48
|
+
Range.prototype,
|
|
49
|
+
"getClientRects",
|
|
50
|
+
originalRangeGetClientRects,
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
Reflect.deleteProperty(Range.prototype, "getClientRects");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
themeState.value = "light";
|
|
11
59
|
});
|
|
12
60
|
|
|
13
61
|
/**
|
|
@@ -26,7 +74,71 @@ function isCollapsed(root: ParentNode) {
|
|
|
26
74
|
return root.querySelector(".cm")?.classList.contains("opacity-20") ?? false;
|
|
27
75
|
}
|
|
28
76
|
|
|
77
|
+
function getEditorIdentity(view: EditorView) {
|
|
78
|
+
return {
|
|
79
|
+
doc: view.state.doc,
|
|
80
|
+
dom: view.dom,
|
|
81
|
+
contentDOM: view.contentDOM,
|
|
82
|
+
scrollDOM: view.scrollDOM,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function expectEditorIdentity(
|
|
87
|
+
view: EditorView,
|
|
88
|
+
identity: ReturnType<typeof getEditorIdentity>,
|
|
89
|
+
) {
|
|
90
|
+
expect(view.state.doc).toBe(identity.doc);
|
|
91
|
+
expect(view.dom).toBe(identity.dom);
|
|
92
|
+
expect(view.contentDOM).toBe(identity.contentDOM);
|
|
93
|
+
expect(view.scrollDOM).toBe(identity.scrollDOM);
|
|
94
|
+
}
|
|
95
|
+
|
|
29
96
|
describe("ReadonlyCode", () => {
|
|
97
|
+
it("reconfigures the theme without replacing the editor", async () => {
|
|
98
|
+
let createdView: EditorView | undefined;
|
|
99
|
+
const onCreateEditor = (view: EditorView) => {
|
|
100
|
+
createdView = view;
|
|
101
|
+
};
|
|
102
|
+
const renderCode = (id: string) => (
|
|
103
|
+
<TooltipProvider>
|
|
104
|
+
<ReadonlyCode
|
|
105
|
+
id={id}
|
|
106
|
+
code={"line_1 = 1\nline_2 = 2\nline_3 = 3"}
|
|
107
|
+
showCopyCode={false}
|
|
108
|
+
onCreateEditor={onCreateEditor}
|
|
109
|
+
/>
|
|
110
|
+
</TooltipProvider>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const { rerender } = render(renderCode("light"));
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(createdView).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const view = createdView;
|
|
119
|
+
if (!view) {
|
|
120
|
+
throw new Error("ReadonlyCode did not create an editor view");
|
|
121
|
+
}
|
|
122
|
+
const identity = getEditorIdentity(view);
|
|
123
|
+
expect(view.state.facet(EditorView.darkTheme)).toBe(false);
|
|
124
|
+
|
|
125
|
+
themeState.value = "dark";
|
|
126
|
+
rerender(renderCode("dark"));
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(view.state.facet(EditorView.darkTheme)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
expect(createdView).toBe(view);
|
|
131
|
+
expectEditorIdentity(view, identity);
|
|
132
|
+
|
|
133
|
+
themeState.value = "light";
|
|
134
|
+
rerender(renderCode("light-again"));
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(view.state.facet(EditorView.darkTheme)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
expect(createdView).toBe(view);
|
|
139
|
+
expectEditorIdentity(view, identity);
|
|
140
|
+
});
|
|
141
|
+
|
|
30
142
|
it("starts collapsed when initiallyHideCode is true", () => {
|
|
31
143
|
const { container } = renderReadonly({ initiallyHideCode: true });
|
|
32
144
|
expect(isCollapsed(container)).toBe(true);
|
package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap
CHANGED
|
@@ -154,6 +154,20 @@ _creds = json.loads(_credentials_json)
|
|
|
154
154
|
fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
|
|
155
155
|
`;
|
|
156
156
|
|
|
157
|
+
exports[`generateStorageCode > Hugging Face > default connection 1`] = `
|
|
158
|
+
"from huggingface_hub import HfApi
|
|
159
|
+
|
|
160
|
+
hf = HfApi()"
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
exports[`generateStorageCode > Hugging Face > with token from secrets 1`] = `
|
|
164
|
+
"from huggingface_hub import HfApi
|
|
165
|
+
import os
|
|
166
|
+
|
|
167
|
+
_token = os.environ.get("HF_TOKEN")
|
|
168
|
+
hf = HfApi(token=_token)"
|
|
169
|
+
`;
|
|
170
|
+
|
|
157
171
|
exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `
|
|
158
172
|
"from obstore.store import S3Store
|
|
159
173
|
|
|
@@ -309,6 +309,26 @@ describe("generateStorageCode", () => {
|
|
|
309
309
|
});
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
+
describe("Hugging Face", () => {
|
|
313
|
+
it("default connection", () => {
|
|
314
|
+
expect(
|
|
315
|
+
generateStorageCode({ type: "huggingface" }, { library: "fsspec" }),
|
|
316
|
+
).toMatchSnapshot();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("with token from secrets", () => {
|
|
320
|
+
expect(
|
|
321
|
+
generateStorageCode(
|
|
322
|
+
{
|
|
323
|
+
type: "huggingface",
|
|
324
|
+
token: prefixSecret("HF_TOKEN"),
|
|
325
|
+
},
|
|
326
|
+
{ library: "fsspec" },
|
|
327
|
+
),
|
|
328
|
+
).toMatchSnapshot();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
312
332
|
describe("invalid cases", () => {
|
|
313
333
|
it("throws for empty S3 bucket", () => {
|
|
314
334
|
expect(() =>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CoreWeaveStorageSchema,
|
|
18
18
|
GCSStorageSchema,
|
|
19
19
|
GoogleDriveStorageSchema,
|
|
20
|
+
HuggingfaceStorageSchema,
|
|
20
21
|
S3StorageSchema,
|
|
21
22
|
type StorageConnection,
|
|
22
23
|
} from "./schemas";
|
|
@@ -79,6 +80,15 @@ const STORAGE_PROVIDERS = [
|
|
|
79
80
|
preferred: "fsspec",
|
|
80
81
|
},
|
|
81
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: "Hugging Face Hub",
|
|
85
|
+
schema: HuggingfaceStorageSchema,
|
|
86
|
+
protocol: "hf",
|
|
87
|
+
storageLibraries: {
|
|
88
|
+
libraries: ["huggingface_hub"],
|
|
89
|
+
preferred: "huggingface_hub",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
82
92
|
] satisfies StorageProviderSchema[];
|
|
83
93
|
|
|
84
94
|
const StorageProviderSelector: React.FC<{
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
StorageConnectionSchema,
|
|
11
11
|
} from "./schemas";
|
|
12
12
|
|
|
13
|
-
export type StorageLibrary = "obstore" | "fsspec";
|
|
13
|
+
export type StorageLibrary = "obstore" | "fsspec" | "huggingface_hub";
|
|
14
14
|
|
|
15
15
|
export interface StorageCodeOptions {
|
|
16
16
|
library: StorageLibrary;
|
|
@@ -20,6 +20,7 @@ export interface StorageCodeOptions {
|
|
|
20
20
|
export const StorageLibraryDisplayNames: Record<StorageLibrary, string> = {
|
|
21
21
|
obstore: "obstore",
|
|
22
22
|
fsspec: "fsspec",
|
|
23
|
+
huggingface_hub: "huggingface_hub",
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
class SecretContainer {
|
|
@@ -245,6 +246,20 @@ function generateGDriveCode(
|
|
|
245
246
|
return { imports, code };
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
function generateHuggingfaceCode(
|
|
250
|
+
connection: Extract<StorageConnection, { type: "huggingface" }>,
|
|
251
|
+
secrets: SecretContainer,
|
|
252
|
+
): { imports: Set<string>; code: string } {
|
|
253
|
+
const imports = new Set(["from huggingface_hub import HfApi"]);
|
|
254
|
+
|
|
255
|
+
if (!connection.token) {
|
|
256
|
+
return { imports, code: "hf = HfApi()" };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const token = secrets.print("token", connection.token);
|
|
260
|
+
return { imports, code: `hf = HfApi(token=${token})` };
|
|
261
|
+
}
|
|
262
|
+
|
|
248
263
|
export function generateStorageCode(
|
|
249
264
|
connection: StorageConnection,
|
|
250
265
|
options: StorageCodeOptions,
|
|
@@ -273,6 +288,9 @@ export function generateStorageCode(
|
|
|
273
288
|
isEmbedded: options.isEmbedded,
|
|
274
289
|
});
|
|
275
290
|
break;
|
|
291
|
+
case "huggingface":
|
|
292
|
+
result = generateHuggingfaceCode(connection, secrets);
|
|
293
|
+
break;
|
|
276
294
|
default:
|
|
277
295
|
assertNever(connection);
|
|
278
296
|
}
|
|
@@ -220,12 +220,31 @@ export const GoogleDriveStorageSchema = z
|
|
|
220
220
|
})
|
|
221
221
|
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
222
222
|
|
|
223
|
+
export const HuggingfaceStorageSchema = z
|
|
224
|
+
.object({
|
|
225
|
+
type: z.literal("huggingface"),
|
|
226
|
+
token: z
|
|
227
|
+
.string()
|
|
228
|
+
.optional()
|
|
229
|
+
.describe(
|
|
230
|
+
FieldOptions.of({
|
|
231
|
+
label: "Access Token",
|
|
232
|
+
description:
|
|
233
|
+
"Leave empty to use the HF_TOKEN environment variable or cached login",
|
|
234
|
+
inputType: "password",
|
|
235
|
+
optionRegex: "(hf.?token|hugging.?face.?token|hub.?token)",
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
238
|
+
})
|
|
239
|
+
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
240
|
+
|
|
223
241
|
export const StorageConnectionSchema = z.discriminatedUnion("type", [
|
|
224
242
|
S3StorageSchema,
|
|
225
243
|
GCSStorageSchema,
|
|
226
244
|
AzureStorageSchema,
|
|
227
245
|
CoreWeaveStorageSchema,
|
|
228
246
|
GoogleDriveStorageSchema,
|
|
247
|
+
HuggingfaceStorageSchema,
|
|
229
248
|
]);
|
|
230
249
|
|
|
231
250
|
export type StorageConnection = z.infer<typeof StorageConnectionSchema>;
|
|
@@ -44,6 +44,94 @@ describe("read-file snippet", () => {
|
|
|
44
44
|
`);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it("huggingface backend (dataset)", () => {
|
|
48
|
+
expect(
|
|
49
|
+
readSnippet.getCode(
|
|
50
|
+
makeCtx({
|
|
51
|
+
backendType: "huggingface",
|
|
52
|
+
entry: {
|
|
53
|
+
path: "datasets/scikit-learn/Fish/Fish.csv",
|
|
54
|
+
kind: "file",
|
|
55
|
+
size: 100,
|
|
56
|
+
lastModified: null,
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
).toMatchInlineSnapshot(`
|
|
61
|
+
"from huggingface_hub import hf_hub_download
|
|
62
|
+
|
|
63
|
+
local_path = hf_hub_download(
|
|
64
|
+
repo_id="scikit-learn/Fish",
|
|
65
|
+
filename="Fish.csv",
|
|
66
|
+
repo_type="dataset",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
with open(local_path, "rb") as f:
|
|
70
|
+
_data = f.read()
|
|
71
|
+
_data"
|
|
72
|
+
`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("huggingface backend (model)", () => {
|
|
76
|
+
expect(
|
|
77
|
+
readSnippet.getCode(
|
|
78
|
+
makeCtx({
|
|
79
|
+
backendType: "huggingface",
|
|
80
|
+
entry: {
|
|
81
|
+
path: "google-bert/bert-base-uncased/config.json",
|
|
82
|
+
kind: "file",
|
|
83
|
+
size: 100,
|
|
84
|
+
lastModified: null,
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
).toMatchInlineSnapshot(`
|
|
89
|
+
"from huggingface_hub import hf_hub_download
|
|
90
|
+
|
|
91
|
+
local_path = hf_hub_download(
|
|
92
|
+
repo_id="google-bert/bert-base-uncased",
|
|
93
|
+
filename="config.json",
|
|
94
|
+
repo_type="model",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with open(local_path, "rb") as f:
|
|
98
|
+
_data = f.read()
|
|
99
|
+
_data"
|
|
100
|
+
`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("huggingface backend returns null for unparsable paths", () => {
|
|
104
|
+
expect(
|
|
105
|
+
readSnippet.getCode(
|
|
106
|
+
makeCtx({
|
|
107
|
+
backendType: "huggingface",
|
|
108
|
+
entry: {
|
|
109
|
+
path: "buckets/my-bucket/file.csv",
|
|
110
|
+
kind: "file",
|
|
111
|
+
size: 100,
|
|
112
|
+
lastModified: null,
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("huggingface backend does not misparse a dataset/space root as a model repo", () => {
|
|
120
|
+
// These paths are missing a filename segment, so they aren't valid
|
|
121
|
+
// dataset/space repo files. They must not fall through and be treated
|
|
122
|
+
// as the model repo "datasets/scikit-learn" or "spaces/gradio".
|
|
123
|
+
for (const path of ["datasets/scikit-learn/Fish", "spaces/gradio/demo"]) {
|
|
124
|
+
expect(
|
|
125
|
+
readSnippet.getCode(
|
|
126
|
+
makeCtx({
|
|
127
|
+
backendType: "huggingface",
|
|
128
|
+
entry: { path, kind: "file", size: 100, lastModified: null },
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
).toBeNull();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
47
135
|
it("returns null for directories", () => {
|
|
48
136
|
expect(
|
|
49
137
|
readSnippet.getCode(
|
|
@@ -9,6 +9,7 @@ import CoreweaveDarkIcon from "@marimo-team/llm-info/icons/coreweave-dark.svg?in
|
|
|
9
9
|
import { DatabaseZapIcon, GlobeIcon, HardDriveIcon } from "lucide-react";
|
|
10
10
|
import GoogleCloudIcon from "@/components/databases/icons/google-cloud-storage.svg?inline";
|
|
11
11
|
import GoogleDriveIcon from "@/components/databases/icons/google-drive.svg?inline";
|
|
12
|
+
import HuggingfaceIcon from "@/components/databases/icons/huggingface.svg?inline";
|
|
12
13
|
import { GitHubIcon } from "@/components/icons/github";
|
|
13
14
|
import type { KnownStorageProtocol } from "@/core/storage/types";
|
|
14
15
|
import { useTheme } from "@/theme/useTheme";
|
|
@@ -28,6 +29,7 @@ const PROTOCOL_ICONS: Record<KnownStorageProtocol, IconEntry> = {
|
|
|
28
29
|
file: HardDriveIcon,
|
|
29
30
|
"in-memory": DatabaseZapIcon,
|
|
30
31
|
gdrive: { src: GoogleDriveIcon },
|
|
32
|
+
hf: { src: HuggingfaceIcon },
|
|
31
33
|
github: GitHubIcon,
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -27,6 +27,50 @@ function escapeForPythonString(value: string): string {
|
|
|
27
27
|
return JSON.stringify(value).slice(1, -1);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
interface ParsedHfRepoPath {
|
|
31
|
+
repoType: "model" | "dataset" | "space";
|
|
32
|
+
repoId: string;
|
|
33
|
+
filename: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Namespaced prefixes are reserved: they can never be the first segment of a
|
|
37
|
+
// model repo id, so a path like "datasets/org" that is missing a filename
|
|
38
|
+
// must not fall through and be misparsed as the model repo "datasets/org".
|
|
39
|
+
const RESERVED_PREFIXES = new Set(["datasets", "spaces", "buckets"]);
|
|
40
|
+
|
|
41
|
+
function parseHfRepoPath(path: string): ParsedHfRepoPath | null {
|
|
42
|
+
const parts = path.split("/").filter(Boolean);
|
|
43
|
+
if (parts[0] === "datasets" && parts.length >= 4) {
|
|
44
|
+
return {
|
|
45
|
+
repoType: "dataset",
|
|
46
|
+
repoId: `${parts[1]}/${parts[2]}`,
|
|
47
|
+
filename: parts.slice(3).join("/"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (parts[0] === "spaces" && parts.length >= 4) {
|
|
51
|
+
return {
|
|
52
|
+
repoType: "space",
|
|
53
|
+
repoId: `${parts[1]}/${parts[2]}`,
|
|
54
|
+
filename: parts.slice(3).join("/"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (parts.length >= 3 && !RESERVED_PREFIXES.has(parts[0])) {
|
|
58
|
+
return {
|
|
59
|
+
repoType: "model",
|
|
60
|
+
repoId: `${parts[0]}/${parts[1]}`,
|
|
61
|
+
filename: parts.slice(2).join("/"),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hfHubDownloadSnippet(parsed: ParsedHfRepoPath): string {
|
|
68
|
+
const repoId = escapeForPythonString(parsed.repoId);
|
|
69
|
+
const filename = escapeForPythonString(parsed.filename);
|
|
70
|
+
const repoType = escapeForPythonString(parsed.repoType);
|
|
71
|
+
return `from huggingface_hub import hf_hub_download\n\nlocal_path = hf_hub_download(\n repo_id="${repoId}",\n filename="${filename}",\n repo_type="${repoType}",\n)`;
|
|
72
|
+
}
|
|
73
|
+
|
|
30
74
|
export const STORAGE_SNIPPETS: StorageSnippet[] = [
|
|
31
75
|
{
|
|
32
76
|
id: "read-file",
|
|
@@ -37,6 +81,13 @@ export const STORAGE_SNIPPETS: StorageSnippet[] = [
|
|
|
37
81
|
return null;
|
|
38
82
|
}
|
|
39
83
|
const path = escapeForPythonString(ctx.entry.path);
|
|
84
|
+
if (ctx.backendType === "huggingface") {
|
|
85
|
+
const parsed = parseHfRepoPath(ctx.entry.path);
|
|
86
|
+
if (!parsed) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return `${hfHubDownloadSnippet(parsed)}\n\nwith open(local_path, "rb") as f:\n _data = f.read()\n_data`;
|
|
90
|
+
}
|
|
40
91
|
if (ctx.backendType === "obstore") {
|
|
41
92
|
return `_data = ${ctx.variableName}.get("${path}").bytes()\n_data`;
|
|
42
93
|
}
|
|
@@ -52,6 +103,13 @@ export const STORAGE_SNIPPETS: StorageSnippet[] = [
|
|
|
52
103
|
return null;
|
|
53
104
|
}
|
|
54
105
|
const path = escapeForPythonString(ctx.entry.path);
|
|
106
|
+
if (ctx.backendType === "huggingface") {
|
|
107
|
+
const parsed = parseHfRepoPath(ctx.entry.path);
|
|
108
|
+
if (!parsed) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return `${hfHubDownloadSnippet(parsed)}\nlocal_path`;
|
|
112
|
+
}
|
|
55
113
|
if (ctx.backendType === "obstore") {
|
|
56
114
|
if (NOT_SIGNABLE_PROTOCOLS.has(ctx.protocol)) {
|
|
57
115
|
return null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { CellId } from "@/core/cells/ids";
|
|
5
|
+
import { runDuringPresentMode, viewStateAtom } from "@/core/mode";
|
|
6
|
+
import { store } from "@/core/state/jotai";
|
|
7
|
+
|
|
8
|
+
const requestAnimationFrameMock = vi.fn((callback: FrameRequestCallback) => {
|
|
9
|
+
callback(0);
|
|
10
|
+
return 0;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function runAfterRender(fn: () => void | Promise<void>): Promise<void> {
|
|
14
|
+
const result = runDuringPresentMode(fn);
|
|
15
|
+
await vi.runAllTimersAsync();
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("runDuringPresentMode", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.useFakeTimers();
|
|
22
|
+
vi.stubGlobal("requestAnimationFrame", requestAnimationFrameMock);
|
|
23
|
+
requestAnimationFrameMock.mockClear();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.useRealTimers();
|
|
28
|
+
vi.unstubAllGlobals();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("runs in present mode and restores the captured view state", async () => {
|
|
32
|
+
const state = { mode: "edit" as const, cellAnchor: CellId.create() };
|
|
33
|
+
store.set(viewStateAtom, state);
|
|
34
|
+
|
|
35
|
+
await runAfterRender(() => {
|
|
36
|
+
expect(store.get(viewStateAtom)).toEqual({
|
|
37
|
+
...state,
|
|
38
|
+
mode: "present",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(store.get(viewStateAtom)).toEqual(state);
|
|
43
|
+
expect(requestAnimationFrameMock).toHaveBeenCalledTimes(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("restores the captured view state when the callback rejects", async () => {
|
|
47
|
+
const state = { mode: "edit" as const, cellAnchor: CellId.create() };
|
|
48
|
+
const error = new Error("capture failed");
|
|
49
|
+
store.set(viewStateAtom, state);
|
|
50
|
+
|
|
51
|
+
const result = runDuringPresentMode(() => Promise.reject(error));
|
|
52
|
+
const rejection = expect(result).rejects.toBe(error);
|
|
53
|
+
await vi.runAllTimersAsync();
|
|
54
|
+
await rejection;
|
|
55
|
+
|
|
56
|
+
expect(store.get(viewStateAtom)).toEqual(state);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("runs directly when already in present mode", async () => {
|
|
60
|
+
const state = { mode: "present" as const, cellAnchor: CellId.create() };
|
|
61
|
+
store.set(viewStateAtom, state);
|
|
62
|
+
|
|
63
|
+
await runDuringPresentMode(() => {
|
|
64
|
+
expect(store.get(viewStateAtom)).toEqual(state);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(store.get(viewStateAtom)).toEqual(state);
|
|
68
|
+
expect(requestAnimationFrameMock).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it.each(["read", "home", "gallery"] as const)(
|
|
72
|
+
"runs directly without changing %s mode",
|
|
73
|
+
async (mode) => {
|
|
74
|
+
const state = { mode, cellAnchor: CellId.create() };
|
|
75
|
+
store.set(viewStateAtom, state);
|
|
76
|
+
|
|
77
|
+
await runDuringPresentMode(() => {
|
|
78
|
+
expect(store.get(viewStateAtom)).toEqual(state);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(store.get(viewStateAtom)).toEqual(state);
|
|
82
|
+
expect(requestAnimationFrameMock).not.toHaveBeenCalled();
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
});
|
|
@@ -103,7 +103,7 @@ describe("codeLensBundle", () => {
|
|
|
103
103
|
|
|
104
104
|
beforeEach(() => {
|
|
105
105
|
vi.useFakeTimers();
|
|
106
|
-
mockFlags({
|
|
106
|
+
mockFlags({ cache_panel: true });
|
|
107
107
|
seedStore({});
|
|
108
108
|
});
|
|
109
109
|
|
|
@@ -141,11 +141,6 @@ describe("codeLensBundle", () => {
|
|
|
141
141
|
return [...v.dom.querySelectorAll<HTMLElement>(".mo-code-lens")];
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
it("returns an empty extension when the flag is off", () => {
|
|
145
|
-
mockFlags({ editor_code_lens: false });
|
|
146
|
-
expect(codeLensBundle(cellId("cell1"))).toEqual([]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
144
|
it("renders a lens at a dataframe declaration", async () => {
|
|
150
145
|
seedStore({ tables: [DF_TABLE] });
|
|
151
146
|
const v = await mount("df = load()\nx = 1");
|
|
@@ -293,7 +288,7 @@ describe("codeLensBundle", () => {
|
|
|
293
288
|
);
|
|
294
289
|
|
|
295
290
|
it("does not render cache lenses when the cache panel is disabled", async () => {
|
|
296
|
-
mockFlags({
|
|
291
|
+
mockFlags({ cache_panel: false });
|
|
297
292
|
const v = await mount("@mo.cache\ndef f():\n return 1");
|
|
298
293
|
|
|
299
294
|
expect(lenses(v)).toHaveLength(0);
|