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: run_button #1514

Merged
merged 4 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions docs/api/inputs/button.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Button

```{admonition} Looking for a submit/run button?
:class: tip

If you're looking for a button to trigger computation on click, consider
using [`mo.ui.run_button`](/api/inputs/run_button.md).
```

```{eval-rst}
.. marimo-embed::
@app.cell
Expand Down
31 changes: 31 additions & 0 deletions docs/api/inputs/run_button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Run Button

```{eval-rst}
.. marimo-embed::
@app.cell
def __():
b = mo.ui.run_button()
b
return

@app.cell
def __():
s = mo.ui.slider(1, 10)
s
return


@app.cell
def __():
mo.stop(b.value, "Click `run` to submit the slider's value")
Copy link
Contributor

@mscolnick mscolnick May 29, 2024

Choose a reason for hiding this comment

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

should this be mo.stop(not b.value).

not sure if this hides to much, but could put a stop on the button too?

b.stop(fallback=None)

# Run thing

or maybe b.gate(), b.clicked()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops yes it should.

Something like that could be nice — maybe b.gate() ... might add in a follow-up


s.value
return
```

```{eval-rst}
.. autoclass:: marimo.ui.run_button
:members:

.. autoclasstoc:: marimo._plugins.ui._impl.run_button.run_button
```
30 changes: 30 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,36 @@ form.value

## Working with buttons

### Create a button that triggers computation when clicked

**Use cases.** To trigger a computation on button click and only on button
click, use [`mo.ui.run_button()`](/api/inputs/run_button.md).

**Recipe.**

1. Import packages

```python
import marimo as mo
```

2. Create a run button

```python
button = mo.ui.run_button()
button
```

3. Run something only if the button has been clicked.

```python
mo.stop(not button.value, "Click 'run' to generate a random number")

import random
random.randint(0, 1000)
```


### Create a counter button

**Use cases.** A counter button, i.e. a button that counts the number of times
Expand Down
2 changes: 2 additions & 0 deletions marimo/_plugins/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"radio",
"range_slider",
"refresh",
"run_button",
"slider",
"switch",
"table",
Expand Down Expand Up @@ -62,6 +63,7 @@
from marimo._plugins.ui._impl.microphone import microphone
from marimo._plugins.ui._impl.plotly import plotly
from marimo._plugins.ui._impl.refresh import refresh
from marimo._plugins.ui._impl.run_button import run_button
from marimo._plugins.ui._impl.switch import switch
from marimo._plugins.ui._impl.table import table
from marimo._plugins.ui._impl.tabs import tabs
4 changes: 4 additions & 0 deletions marimo/_plugins/ui/_core/ui_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ def _update(self, value: S) -> None:
if self._on_change is not None:
self._on_change(self._value)

def _on_update_completion(self) -> None:
"""Callback to run after the kernel has processed a value update."""
return
Comment on lines +382 to +384
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method acts as a post execution hook; the runtime calls it after setting UI element values.


def _clone(self) -> UIElement[S, T]:
"""Clone a UIElement, returning one with a different id

Expand Down
1 change: 1 addition & 0 deletions marimo/_plugins/ui/_impl/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,7 @@ def __init__(
) -> None:
self._on_click = (lambda _: value) if on_click is None else on_click
self._initial_value = value
# This should be kept in sync with mo.ui.run_button()
super().__init__(
component_name=button._name,
# frontend's value is always a counter
Expand Down
117 changes: 117 additions & 0 deletions marimo/_plugins/ui/_impl/run_button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright 2024 Marimo. All rights reserved.
from __future__ import annotations

from typing import Any, Callable, Final, Literal, Optional

from marimo._output.rich_help import mddoc
from marimo._plugins.ui._core.ui_element import UIElement
from marimo._plugins.ui._impl.input import button
from marimo._runtime.context.types import ContextNotInitializedError


@mddoc
class run_button(UIElement[Any, Any]):
"""
A button that can be used to trigger computation.

**Example.**

```python
# a button that when clicked will have its value set to True;
# any cells referencing that button will automatically run.
button = mo.ui.button()
akshayka marked this conversation as resolved.
Show resolved Hide resolved
button
```

```python
slider = mo.ui.slider(1, 10)
slider
```

```python
# if the button hasn't been clicked, don't run.
mo.stop(not button.value)

slider.value
```

When clicked, `run_button`'s value is set to `True`, and any cells
referencing it are run. After those cells are run, `run_button`'s
value will automatically be set back to `False` as long as automatic
execution is enabled.

**Attributes.**

- `value`: the value of the button; `True` when clicked, and reset to
`False` after cells referencing the button finish running (when
automatic execution is enabled).

**Initialization Args.**

- `kind`: 'neutral', 'success', 'warn', or 'danger'
- `disabled`: whether the button is disabled
- `tooltip`: a tooltip to display for the button
- `label`: text label for the element
- `on_change`: optional callback to run when this element's value changes
- `full_width`: whether the input should take up the full width of its
container
"""

_name: Final[str] = "marimo-button"

def __init__(
self,
kind: Literal["neutral", "success", "warn", "danger"] = "neutral",
disabled: bool = False,
tooltip: Optional[str] = None,
*,
label: str = "click to run",
on_change: Optional[Callable[[Any], None]] = None,
full_width: bool = False,
) -> None:
self._initial_value = False
super().__init__(
# We reuse the button plugin on the frontend, since the UI
# is the same.
component_name=button._name,
# frontend's value is a counter
initial_value=0,
label=label,
args={
"kind": kind,
"disabled": disabled,
"tooltip": tooltip,
"full-width": full_width,
},
on_change=on_change,
)

def _convert_value(self, value: Any) -> Any:
if value == 0:
# frontend's value == 0 only during initialization; first value
# frontend will send is 1
return False
else:
return True

def _on_update_completion(self) -> None:
from marimo._runtime.context import get_context
from marimo._runtime.context.kernel_context import KernelRuntimeContext

try:
ctx = get_context()
except ContextNotInitializedError:
self._value = False
return

if isinstance(ctx, KernelRuntimeContext) and ctx.lazy:
# Resetting to False in lazy kernels makes the button pointless,
# since its value hasn't been read by downstream cells on update
# completion.
#
# The right thing to do would be to somehow set to False after
# all cells that were marked stale because of the update were run,
# but that's too complicated.
return
Comment on lines +106 to +114
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Special case for lazy kernels, since we don't when descendants will run

else:
self._value = False
4 changes: 4 additions & 0 deletions marimo/_runtime/context/kernel_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def globals(self) -> dict[str, Any]:
def execution_context(self) -> ExecutionContext | None:
return self._kernel.execution_context

@property
def lazy(self) -> bool:
return self._kernel.lazy()

@property
def cell_id(self) -> Optional[CellId_t]:
"""Get the cell id of the currently executing cell, if any."""
Expand Down
Loading
Loading