From 99c2ded0375c6c5e9a501c6466bb47e4098d3122 Mon Sep 17 00:00:00 2001 From: dylan madisetti Date: Thu, 20 Feb 2025 18:33:43 -0500 Subject: [PATCH 1/2] compat: Register webbrowser open hook for pyodide --- marimo/_runtime/marimo_browser.py | 91 +++++++++++++++++++++++++++++++ marimo/_runtime/patches.py | 21 ++++++- marimo/_runtime/runtime.py | 6 ++ tests/_runtime/test_patches.py | 57 +++++++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 marimo/_runtime/marimo_browser.py diff --git a/marimo/_runtime/marimo_browser.py b/marimo/_runtime/marimo_browser.py new file mode 100644 index 00000000000..a424beabe68 --- /dev/null +++ b/marimo/_runtime/marimo_browser.py @@ -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/static/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 wrote 20 short programs in ' + "Python yesterday." + " It was wonderful. " + "Perl, I'm leaving you.\"" + "
The technologies may have changed, but the " + "sentiment remains. We agree Randall;
Edited " + "from " + " XKCD/353" + " under CC-2.5;" + ), + ) + ) + else: + mo.output.append( + mo.Html( + f"", + ) + ) + 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 diff --git a/marimo/_runtime/patches.py b/marimo/_runtime/patches.py index e843d1ce707..877dd56d9e1 100644 --- a/marimo/_runtime/patches.py +++ b/marimo/_runtime/patches.py @@ -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: @@ -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 diff --git a/marimo/_runtime/runtime.py b/marimo/_runtime/runtime.py index a741af34b59..0020239ae9b 100644 --- a/marimo/_runtime/runtime.py +++ b/marimo/_runtime/runtime.py @@ -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: diff --git a/tests/_runtime/test_patches.py b/tests/_runtime/test_patches.py index c2ac7f53848..ca615474d48 100644 --- a/tests/_runtime/test_patches.py +++ b/tests/_runtime/test_patches.py @@ -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 " Date: Thu, 20 Feb 2025 19:40:06 -0500 Subject: [PATCH 2/2] Update marimo/_runtime/marimo_browser.py --- marimo/_runtime/marimo_browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/marimo/_runtime/marimo_browser.py b/marimo/_runtime/marimo_browser.py index a424beabe68..8cf2114bea9 100644 --- a/marimo/_runtime/marimo_browser.py +++ b/marimo/_runtime/marimo_browser.py @@ -46,7 +46,7 @@ def browser_open_fallback( ): mo.output.append( mo.image( - "https://marimo.app/static/antigravity.png", + "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 '