Skip to content

Commit

Permalink
compat: Register webbrowser.open hook for pyodide (#3864)
Browse files Browse the repository at this point in the history
## 📝 Summary

`import webbrowser.open` seems to be broken in WASM (which is fine, I
think opening pages is potentially dangerous)- so this loads in a hook
to insert an iframe into output if webbrowser is called instead of
failing. As a result, the following code now works

```python
import antigravity
```

but with some marimo tweaks that might make for good social engagement
on 25/04/01


![image](https://github.com/user-attachments/assets/8721a668-516b-40c8-9ae1-86056d58151a)

(PS: this needs a static upload)

Had a spare hour and noticed it when looking through cpython/Lib
Let me know if it's too irreverent and I can walk back the feature- but
seems enabling this seems like full python support to me

## 📜 Reviewers

<!--
Tag potential reviewers from the community or maintainers who might be
interested in reviewing this pull request.

Your PR will be reviewed more quickly if you can figure out the right
person to tag with @ -->

@akshayka OR @mscolnick

---------

Co-authored-by: Myles Scolnick <[email protected]>
  • Loading branch information
dmadisetti and mscolnick authored Feb 21, 2025
1 parent 1443d60 commit 39a607a
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 1 deletion.
91 changes: 91 additions & 0 deletions marimo/_runtime/marimo_browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright 2025 Marimo. All rights reserved.
from __future__ import annotations

import webbrowser

from marimo._runtime.context import get_context
from marimo._runtime.context.types import ContextNotInitializedError


def browser_open_fallback(
url: str, new: int = 0, autoraise: bool = False
) -> bool:
"""
Inserts an iframe with the given URL into the output.
NB Returns false on failure.
"""
import inspect

import marimo as mo

del new, autoraise # unused

try:
ctx = get_context()
except ContextNotInitializedError:
return False

if ctx.execution_context is None:
return False

# import antigravity is a real module in python. see:
# github.com/python/cpython/blob/main/Lib/antigravity.py
# which automatically triggers a webbrowser.open call to the relevant
# comic. We patch webbrowser.open due to an incompatible stub:
# https://pyodide.org/en/stable/usage/wasm-constraints.html
# so may as well hook in to customize the easter egg. Especially since
# iframe constraints actually block this from loading on marimo.app.
#
# For other python lore, try:
# import this
stack = inspect.stack()
if len(stack) > 3 and (
stack[2].filename.endswith("antigravity.py")
or (stack[1].filename.endswith("antigravity.py"))
):
mo.output.append(
mo.image(
"https://marimo.app/images/antigravity.png",
alt=(
"The image shows 2 stick figures in XKCD style. The one "
'on the left says:"You\'re Flying! How?", a floating '
'stick figure on the right responds "marimo!"'
),
caption=(
'Original alt text: "<i>I wrote 20 short programs in '
"Python yesterday."
"<b> It was wonderful. </b>"
"Perl, <u>I'm leaving you.</u></i>\""
"<br/> The technologies may have changed, but the "
"sentiment remains. We agree Randall;<br /> Edited "
"from <u><a "
" href='https://xkcd.com/353'>"
" XKCD/353"
" </a></u> under CC-2.5;"
),
)
)
else:
mo.output.append(
mo.Html(
f"<iframe src='{url}' style='width:100%;height:500px'></iframe>",
)
)
return True


def build_browser_fallback() -> "type[webbrowser.BaseBrowser]":
"""
Dynamically create the class since BaseBrowser does not exist in
pyodide.
"""

# Construct like this to limit stack frames.
MarimoBrowser = type(
"MarimoBrowser",
(webbrowser.BaseBrowser,),
{"open": staticmethod(browser_open_fallback)},
)

return MarimoBrowser
21 changes: 20 additions & 1 deletion marimo/_runtime/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import types
from typing import Any, Callable, Iterator

from marimo._runtime import marimo_pdb
from marimo._runtime import marimo_browser, marimo_pdb


def patch_pdb(debugger: marimo_pdb.MarimoPdb) -> None:
Expand All @@ -19,6 +19,25 @@ def patch_pdb(debugger: marimo_pdb.MarimoPdb) -> None:
pdb.set_trace = functools.partial(marimo_pdb.set_trace, debugger=debugger)


def patch_webbrowser() -> None:
import webbrowser

try:
_ = webbrowser.get()
# pyodide doesn't have a webbrowser.get() method
# (nor a webbrowser.Error, so careful)
except AttributeError:
webbrowser.open = marimo_browser.browser_open_fallback
except webbrowser.Error:
MarimoBrowser = marimo_browser.build_browser_fallback()
webbrowser.register(
"marimo-output",
None,
MarimoBrowser(),
preferred=True,
)


def patch_sys_module(module: types.ModuleType) -> None:
sys.modules[module.__name__] = module

Expand Down
6 changes: 6 additions & 0 deletions marimo/_runtime/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,8 +537,14 @@ def __init__(
# was invoked. New state updates evict older ones.
self.state_updates: dict[State[Any], CellId_t] = {}

# Webbrowser may not be set (e.g. docker container) or stubbed/broken
# (e.g. in pyodide). Set default to just inject an iframe of the
# expected page to output.
patches.patch_webbrowser()
# micropip only patched in non-pyodide environments.
if not is_pyodide():
patches.patch_micropip(self.globals)

exec("import marimo as __marimo__", self.globals)

def teardown(self) -> None:
Expand Down
57 changes: 57 additions & 0 deletions tests/_runtime/test_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,60 @@ async def test_micropip_set_index_urls(
]
)
TestMicropip._assert_micropip_warning_printed(buf.getvalue())


async def test_webbrowser_injection(
mocked_kernel: Kernel, exec_req: ExecReqProvider
):
await mocked_kernel.k.run(
[
exec_req.get("""
import webbrowser
MarimoBrowser = __marimo__._runtime.marimo_browser.build_browser_fallback()
webbrowser.register(
"marimo-output", None, MarimoBrowser(), preferred=True
)
"""),
]
)
await mocked_kernel.k.run(
[
cell := exec_req.get("webbrowser.open('https://marimo.io');"),
]
)
assert "webbrowser" in mocked_kernel.k.globals
outputs: list[str] = []
for msg in mocked_kernel.stream.messages:
if msg[0] == "cell-op" and msg[1]["output"] is not None:
outputs.append(msg[1]["output"]["data"])

assert "<iframe" in outputs[-1]


async def test_webbrowser_easter_egg(
mocked_kernel: Kernel, exec_req: ExecReqProvider
):
await mocked_kernel.k.run(
[
exec_req.get("""
import webbrowser
MarimoBrowser = __marimo__._runtime.marimo_browser.build_browser_fallback()
webbrowser.register(
"marimo-output", None, MarimoBrowser(), preferred=True
)
"""),
]
)
await mocked_kernel.k.run(
[
cell := exec_req.get("import antigravity;"),
]
)
assert "antigravity" in mocked_kernel.k.globals
outputs: list[str] = []
for msg in mocked_kernel.stream.messages:
if msg[0] == "cell-op" and msg[1]["output"] is not None:
outputs.append(msg[1]["output"]["data"])

assert "<iframe" not in outputs[-1]
assert "<img" in outputs[-1]

0 comments on commit 39a607a

Please sign in to comment.