diff --git a/marimo/_dependencies/dependencies.py b/marimo/_dependencies/dependencies.py
index b6585225bb4..29e68d97747 100644
--- a/marimo/_dependencies/dependencies.py
+++ b/marimo/_dependencies/dependencies.py
@@ -121,3 +121,8 @@ def has_anywidget() -> bool:
def has_watchdog() -> bool:
"""Return True if watchdog is installed."""
return importlib.util.find_spec("watchdog") is not None
+
+ @staticmethod
+ def has_ipython() -> bool:
+ """Return True if IPython is installed."""
+ return importlib.util.find_spec("IPython") is not None
diff --git a/marimo/_output/builder.py b/marimo/_output/builder.py
index f765cdce382..786f635be9d 100644
--- a/marimo/_output/builder.py
+++ b/marimo/_output/builder.py
@@ -93,6 +93,7 @@ def iframe(
height: Optional[str] = None,
style: Optional[str] = None,
onload: Optional[str] = None,
+ **kwargs: str,
) -> str:
params: List[Tuple[str, Union[str, None]]] = []
if src:
@@ -107,6 +108,8 @@ def iframe(
params.append(("style", style))
if onload:
params.append(("onload", onload))
+ for key, value in kwargs.items():
+ params.append((key, value))
if len(params) == 0:
return ""
diff --git a/marimo/_output/formatters/formatter_factory.py b/marimo/_output/formatters/formatter_factory.py
index 75379d2fac3..9845ab286a6 100644
--- a/marimo/_output/formatters/formatter_factory.py
+++ b/marimo/_output/formatters/formatter_factory.py
@@ -2,7 +2,7 @@
from __future__ import annotations
import abc
-from typing import Optional
+from typing import Callable, Optional
# Abstract base class for formatters that are installed at runtime.
@@ -20,9 +20,12 @@ def package_name() -> Optional[str]:
raise NotImplementedError
@abc.abstractmethod
- def register(self) -> None:
+ def register(self) -> Callable[[], None] | None:
"""Registers formatters.
Formatters can be registered using the formatters.formatter decorator.
+
+ Optionally returns a handle to undo side-effects, such as module
+ patches.
"""
raise NotImplementedError
diff --git a/marimo/_output/formatters/formatters.py b/marimo/_output/formatters/formatters.py
index 8c135ae8640..9422d6f2a71 100644
--- a/marimo/_output/formatters/formatters.py
+++ b/marimo/_output/formatters/formatters.py
@@ -10,6 +10,7 @@
from marimo._output.formatters.cell import CellFormatter
from marimo._output.formatters.formatter_factory import FormatterFactory
from marimo._output.formatters.holoviews_formatters import HoloViewsFormatter
+from marimo._output.formatters.ipython_formatters import IPythonFormatter
from marimo._output.formatters.leafmap_formatters import LeafmapFormatter
from marimo._output.formatters.matplotlib_formatters import MatplotlibFormatter
from marimo._output.formatters.pandas_formatters import PandasFormatter
@@ -29,6 +30,7 @@
LeafmapFormatter.package_name(): LeafmapFormatter(),
BokehFormatter.package_name(): BokehFormatter(),
HoloViewsFormatter.package_name(): HoloViewsFormatter(),
+ IPythonFormatter.package_name(): IPythonFormatter(),
AnyWidgetFormatter.package_name(): AnyWidgetFormatter(),
}
diff --git a/marimo/_output/formatters/ipython_formatters.py b/marimo/_output/formatters/ipython_formatters.py
new file mode 100644
index 00000000000..d6346bd3776
--- /dev/null
+++ b/marimo/_output/formatters/ipython_formatters.py
@@ -0,0 +1,60 @@
+# Copyright 2024 Marimo. All rights reserved.
+from __future__ import annotations
+
+import functools
+from typing import Any, Callable
+
+from marimo._messaging.mimetypes import KnownMimeType
+from marimo._output import builder
+from marimo._output.formatters.formatter_factory import FormatterFactory
+
+
+class IPythonFormatter(FormatterFactory):
+ @staticmethod
+ def package_name() -> str:
+ return "IPython"
+
+ def register(self) -> Callable[[], None]:
+ import IPython.display # type:ignore
+
+ from marimo._output import formatting
+ from marimo._runtime.output import _output
+
+ old_display = IPython.display.display
+ # monkey patch IPython.display.display, which imperatively writes
+ # outputs to the frontend
+
+ @functools.wraps(old_display)
+ def display(*objs: Any, **kwargs: Any) -> None:
+ # IPython.display.display returns a DisplayHandle, which
+ # can be used to update the displayed object. We don't support
+ # that yet ...
+ if kwargs.pop("clear", False):
+ _output.clear()
+ for value in objs:
+ _output.append(value)
+
+ IPython.display.display = display
+
+ def unpatch() -> None:
+ IPython.display.display = old_display
+
+ @formatting.formatter(IPython.display.HTML)
+ def _format_html(
+ html: IPython.display.HTML,
+ ) -> tuple[KnownMimeType, str]:
+ if html.url is not None:
+ # TODO(akshayka): resize iframe not working
+ data = builder.h.iframe(
+ src=html.url,
+ width="100%",
+ onload="__resizeIframe(this)",
+ scrolling="auto",
+ frameborder="0",
+ )
+ else:
+ data = str(html._repr_html_()) # type: ignore
+
+ return ("text/html", data)
+
+ return unpatch
diff --git a/marimo/_plugins/ui/_impl/dataframes/handlers.py b/marimo/_plugins/ui/_impl/dataframes/handlers.py
index 3f3460ed630..ca7ae784049 100644
--- a/marimo/_plugins/ui/_impl/dataframes/handlers.py
+++ b/marimo/_plugins/ui/_impl/dataframes/handlers.py
@@ -182,7 +182,7 @@ def handle_aggregate(
# Pandas type-checking doesn't like the fact that the values
# are lists of strings (function names), even though the docs permit
# such a value
- return cast("pd.DataFrame", df.agg(dict_of_aggs)) # type: ignore[arg-type] # noqa: E501
+ return cast("pd.DataFrame", df.agg(dict_of_aggs)) # type: ignore # noqa: E501
@staticmethod
def handle_select_columns(
diff --git a/marimo/_smoke_tests/third_party/ipython_display.py b/marimo/_smoke_tests/third_party/ipython_display.py
new file mode 100644
index 00000000000..2ebe4cfe68c
--- /dev/null
+++ b/marimo/_smoke_tests/third_party/ipython_display.py
@@ -0,0 +1,58 @@
+# Copyright 2024 Marimo. All rights reserved.
+import marimo
+
+__generated_with = "0.3.1"
+app = marimo.App()
+
+
+@app.cell
+def __():
+ import IPython
+ import marimo as mo
+
+ url = IPython.display.HTML("https://marimo.io")
+ url
+ return IPython, mo, url
+
+
+@app.cell
+def __(IPython):
+ html = IPython.display.HTML("hello world")
+ html
+ return html,
+
+
+@app.cell
+def __(IPython, html, url):
+ IPython.display.display(html, url)
+ return
+
+
+@app.cell
+def __():
+ # not on PyPI
+ # installation instructions here https://github.com/allefeld/pytikz
+ import tikz
+ return tikz,
+
+
+@app.cell
+def __(tikz):
+ # define coordinates as a list of tuples
+ coords = [(0, 0), (0, 2), (1, 3.25), (2, 2), (2, 0), (0, 2), (2, 2), (0, 0), (2, 0)]
+
+ # create `Picture` object
+ pic = tikz.Picture()
+ # draw a line following the coordinates
+ pic.draw(tikz.line(coords), thick=True, rounded_corners='4pt')
+ return coords, pic
+
+
+@app.cell
+def __(pic):
+ pic.demo(dpi=300)
+ return
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/pyproject.toml b/pyproject.toml
index 9a40c6608cd..14672b9d7c0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -113,6 +113,7 @@ testoptional = [
# have the rust toolchain installed on CI
"polars==0.19.12",
"anywidget~=0.9.3",
+ "ipython~=8.12.3",
"openai~=1.12.0",
]
@@ -194,6 +195,7 @@ exclude = [
'marimo/_tutorials/',
'marimo/_smoke_tests/',
]
+warn_unused_ignores=false
# tutorials shouldn't be type-checked (should be excluded), but they
# get included anyway, maybe due to import following; this is coarse but works
diff --git a/tests/_output/formatters/test_formatters.py b/tests/_output/formatters/test_formatters.py
index 9609563d425..a68487a2cb1 100644
--- a/tests/_output/formatters/test_formatters.py
+++ b/tests/_output/formatters/test_formatters.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import importlib
import os.path
diff --git a/tests/conftest.py b/tests/conftest.py
index 8f2d4aac9cc..150b7c7d582 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -16,10 +16,7 @@
ThreadSafeStdout,
ThreadSafeStream,
)
-from marimo._runtime.context import (
- initialize_context,
- teardown_context,
-)
+from marimo._runtime.context import initialize_context, teardown_context
from marimo._runtime.requests import AppMetadata, ExecutionRequest
from marimo._runtime.runtime import Kernel