Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user config for dataframe viewer #2067

Merged
merged 6 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 57 additions & 25 deletions frontend/src/components/app-config/user-config-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,46 +342,51 @@ export const UserConfigForm: React.FC = () => {
/>
<FormField
control={form.control}
name="display.code_editor_font_size"
name="display.cell_output"
render={({ field }) => (
<FormItem className={formItemClasses}>
<FormLabel>Code editor font size</FormLabel>
<FormControl>
<span className="inline-flex mr-2">
<NumberField
data-testid="code-editor-font-size-input"
className="m-0 w-24"
{...field}
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Cell output area</FormLabel>
<FormControl>
<NativeSelect
data-testid="cell-output-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
minValue={8}
maxValue={20}
onChange={(value) => {
field.onChange(value);
onSubmit(form.getValues());
}}
/>
</span>
</FormControl>
<FormMessage />
</FormItem>
disabled={field.disabled}
className="inline-flex mr-2"
>
{["above", "below"].map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
</FormItem>

<FormDescription>
Where to display cell's output.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="display.cell_output"
name="display.dataframes"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Cell output area</FormLabel>
<FormLabel>Dataframe viewer</FormLabel>
<FormControl>
<NativeSelect
data-testid="cell-output-select"
data-testid="display-dataframes-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{["above", "below"].map((option) => (
{["rich", "plain"].map((option) => (
<option value={option} key={option}>
{option}
</option>
Expand All @@ -392,11 +397,38 @@ export const UserConfigForm: React.FC = () => {
</FormItem>

<FormDescription>
Where to display cell's output.
Whether to use marimo's rich dataframe viewer or a plain HTML
table; requires notebook restart to take effect.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="display.code_editor_font_size"
render={({ field }) => (
<FormItem className={formItemClasses}>
<FormLabel>Code editor font size</FormLabel>
<FormControl>
<span className="inline-flex mr-2">
<NumberField
data-testid="code-editor-font-size-input"
className="m-0 w-24"
{...field}
value={field.value}
minValue={8}
maxValue={20}
onChange={(value) => {
field.onChange(value);
onSubmit(form.getValues());
}}
/>
</span>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingGroup>

<SettingGroup title="Package Management">
Expand Down
1 change: 1 addition & 0 deletions frontend/src/core/cells/__tests__/cells.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function createEditor(content: string) {
completionConfig: {
activate_on_typing: true,
copilot: false,
codeium_api_key: null,
},
hotkeys: new OverridingHotkeyProvider({}),
showPlaceholder: true,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/core/codemirror/__tests__/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function getOpts() {
completionConfig: {
activate_on_typing: false,
copilot: false,
codeium_api_key: null,
},
keymapConfig: {
preset: "default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function createState(content: string) {
completionConfig: {
copilot: false,
activate_on_typing: true,
codeium_api_key: null,
},
hotkeys: new OverridingHotkeyProvider({}),
enableAI: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function createEditor(doc: string) {
completionConfig: {
activate_on_typing: true,
copilot: false,
codeium_api_key: null,
},
hotkeys: new OverridingHotkeyProvider({}),
showPlaceholder: true,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/core/config/__tests__/config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test("default UserConfig - empty", () => {
"display": {
"cell_output": "above",
"code_editor_font_size": 14,
"dataframes": "rich",
"default_width": "medium",
"theme": "light",
},
Expand Down Expand Up @@ -85,6 +86,7 @@ test("default UserConfig - one level", () => {
"display": {
"cell_output": "above",
"code_editor_font_size": 14,
"dataframes": "rich",
"default_width": "medium",
"theme": "light",
},
Expand Down
15 changes: 7 additions & 8 deletions frontend/src/core/config/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { z } from "zod";
import { Logger } from "@/utils/Logger";
import { getMarimoAppConfig, getMarimoUserConfig } from "../dom/marimo-tag";
import { MarimoConfig } from "../network/types";
import type { MarimoConfig } from "../network/types";

// This has to be defined in the same file as the zod schema to satisfy zod
export const PackageManagerNames = [
Expand Down Expand Up @@ -75,6 +75,7 @@ export const UserConfigSchema = z
theme: z.enum(["light", "dark", "system"]).default("light"),
code_editor_font_size: z.number().nonnegative().default(14),
cell_output: z.enum(["above", "below"]).default("above"),
dataframes: z.enum(["rich", "plain"]).default("rich"),
default_width: z
.enum(VALID_APP_WIDTHS)
.default("medium")
Expand Down Expand Up @@ -129,11 +130,9 @@ export type SaveConfig = UserConfig["save"];
export type CompletionConfig = UserConfig["completion"];
export type KeymapConfig = UserConfig["keymap"];

export const AppTitleSchema = z
.string()
.regex(new RegExp("^[A-Za-z0-9-_' ]*$"), {
message: "Invalid application title",
});
export const AppTitleSchema = z.string().regex(/^[\w '-]*$/, {
message: "Invalid application title",
});
export const AppConfigSchema = z
.object({
width: z
Expand Down Expand Up @@ -169,7 +168,7 @@ export function parseUserConfig(): UserConfig {
Logger.log(`🧪 Experimental feature "${key}" is enabled.`);
}
}
return parsed as UserConfig;
return parsed as unknown as UserConfig;
} catch (error) {
Logger.error(
`Marimo got an unexpected value in the configuration file: ${error}`,
Expand All @@ -179,5 +178,5 @@ export function parseUserConfig(): UserConfig {
}

export function defaultUserConfig(): UserConfig {
return UserConfigSchema.parse({}) as UserConfig;
return UserConfigSchema.parse({}) as unknown as UserConfig;
}
12 changes: 7 additions & 5 deletions frontend/src/core/network/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ export const API = {
.then((response) => {
if (!response.ok) {
throw new Error(response.statusText);
} else if (
}
if (
response.headers.get("Content-Type")?.startsWith("application/json")
) {
return response.json() as RESP;
} else {
return null as RESP;
}
return null as RESP;
})
.catch((error) => {
// Catch and rethrow
Expand All @@ -95,19 +95,21 @@ export const API = {
},
handleResponse: <T>(response: {
data?: T | undefined;
error?: Error;
error?: Record<string, unknown>;
response: Response;
}): Promise<T> => {
if (response.error) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject(response.error);
}
return Promise.resolve(response.data as T);
},
handleResponseReturnNull: (response: {
error?: Error;
error?: Record<string, unknown>;
response: Response;
}): Promise<null> => {
if (response.error) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject(response.error);
}
return Promise.resolve(null);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/css/md.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
so we need to apply the same formatting
*/
display: block;
margin-block: 1rem 1rem;
margin-inline: 0 0;
margin-block: 1rem;
margin-inline: 0;
}

.markdown h1,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/stories/editor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const DefaultPython: Story = {
copilotBundle({
activate_on_typing: true,
copilot: false,
codeium_api_key: null,
}),
]}
/>
Expand Down
8 changes: 7 additions & 1 deletion marimo/_ast/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
MultipleDefinitionError,
UnparsableError,
)
from marimo._config.config import WidthType
from marimo._config.config import MarimoConfig, WidthType
from marimo._config.utils import load_config
from marimo._messaging.mimetypes import KnownMimeType
from marimo._output.hypertext import Html
from marimo._output.rich_help import mddoc
Expand Down Expand Up @@ -145,6 +146,7 @@ def __init__(self, **kwargs: Any) -> None:
# unrecognized settings will just be dropped, instead of raising
# a TypeError.
self._config = _AppConfig.from_untrusted_dict(kwargs)
self._user_config = load_config()

if runtime_context_installed():
# nested applications get a unique cell prefix to disambiguate
Expand Down Expand Up @@ -571,6 +573,10 @@ def __init__(self, app: App) -> None:
def config(self) -> _AppConfig:
return self._app._config

@property
def user_config(self) -> MarimoConfig:
return self._app._user_config

@property
def cell_manager(self) -> CellManager:
return self._app._cell_manager
Expand Down
3 changes: 3 additions & 0 deletions marimo/_config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,14 @@ class DisplayConfig(TypedDict):
- `theme`: `"light"`, `"dark"`, or `"system"`
- `code_editor_font_size`: font size for the code editor
- `cell_output`: `"above"` or `"below"`
- `dataframes`: `"rich"` or `"plain"`
"""

theme: Literal["light", "dark", "system"]
code_editor_font_size: int
cell_output: Literal["above", "below"]
default_width: WidthType
dataframes: Literal["rich", "plain"]


@mddoc
Expand Down Expand Up @@ -214,6 +216,7 @@ class MarimoConfig(TypedDict):
"code_editor_font_size": 14,
"cell_output": "above",
"default_width": "medium",
"dataframes": "rich",
},
"formatting": {"line_length": 79},
"keymap": {"preset": "default", "overrides": {}},
Expand Down
1 change: 1 addition & 0 deletions marimo/_config/settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Copyright 2024 Marimo. All rights reserved.
from __future__ import annotations

from dataclasses import dataclass
Expand Down
44 changes: 30 additions & 14 deletions marimo/_output/formatters/df_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
LOGGER = _loggers.marimo_logger()


def include_opinionated() -> bool:
from marimo._runtime.context import (
get_context,
runtime_context_installed,
)

if runtime_context_installed():
ctx = get_context()
return ctx.user_config["display"]["dataframes"] == "rich"
return True


class PolarsFormatter(FormatterFactory):
@staticmethod
def package_name() -> str:
Expand All @@ -19,15 +31,17 @@ def register(self) -> None:

from marimo._output import formatting

@formatting.opinionated_formatter(pl.DataFrame)
def _show_marimo_dataframe(
df: pl.DataFrame,
) -> tuple[KnownMimeType, str]:
try:
return table(df, selection=None, pagination=True)._mime_()
except Exception as e:
LOGGER.warning("Failed to format DataFrame: %s", e)
return ("text/html", df._repr_html_())
if include_opinionated():

@formatting.opinionated_formatter(pl.DataFrame)
def _show_marimo_dataframe(
df: pl.DataFrame,
) -> tuple[KnownMimeType, str]:
try:
return table(df, selection=None, pagination=True)._mime_()
except Exception as e:
LOGGER.warning("Failed to format DataFrame: %s", e)
return ("text/html", df._repr_html_())


class PyArrowFormatter(FormatterFactory):
Expand All @@ -40,8 +54,10 @@ def register(self) -> None:

from marimo._output import formatting

@formatting.opinionated_formatter(pa.Table)
def _show_marimo_dataframe(
df: pa.Table,
) -> tuple[KnownMimeType, str]:
return table(df, selection=None, pagination=True)._mime_()
if include_opinionated():

@formatting.opinionated_formatter(pa.Table)
def _show_marimo_dataframe(
df: pa.Table,
) -> tuple[KnownMimeType, str]:
return table(df, selection=None, pagination=True)._mime_()
Loading
Loading