diff --git a/skore/src/skore/persistence/item/altair_chart_item.py b/skore/src/skore/persistence/item/altair_chart_item.py index b36d4afd3..cd4ddbae1 100644 --- a/skore/src/skore/persistence/item/altair_chart_item.py +++ b/skore/src/skore/persistence/item/altair_chart_item.py @@ -9,7 +9,6 @@ from skore.persistence.item.item import Item, ItemTypeError from skore.persistence.item.media_item import lazy_is_instance -from skore.utils import bytes_to_b64_str if TYPE_CHECKING: from altair.vegalite.v5.schema.core import TopLevelSpec as AltairChart @@ -69,13 +68,3 @@ def chart(self) -> AltairChart: import altair return altair.Chart.from_json(self.chart_str) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - chart_bytes = self.chart_str.encode("utf-8") - chart_b64_str = bytes_to_b64_str(chart_bytes) - - return super().as_serializable_dict() | { - "media_type": "application/vnd.vega.v5+json;base64", - "value": chart_b64_str, - } diff --git a/skore/src/skore/persistence/item/item.py b/skore/src/skore/persistence/item/item.py index bd3cd9de0..82a4748b1 100644 --- a/skore/src/skore/persistence/item/item.py +++ b/skore/src/skore/persistence/item/item.py @@ -84,15 +84,3 @@ def __parameters__(self) -> dict[str, Any]: def __repr__(self) -> str: """Represent the item.""" return f"{self.__class__.__name__}(...)" - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend. - - Derived class must call their super implementation and merge the result with - their output. - """ - return { - "updated_at": self.updated_at, - "created_at": self.created_at, - "note": self.note, - } diff --git a/skore/src/skore/persistence/item/matplotlib_figure_item.py b/skore/src/skore/persistence/item/matplotlib_figure_item.py index 3ff0a2b79..ea1d004b1 100644 --- a/skore/src/skore/persistence/item/matplotlib_figure_item.py +++ b/skore/src/skore/persistence/item/matplotlib_figure_item.py @@ -97,16 +97,3 @@ def figure(self) -> Figure: mpl_backend(backend="agg"), ): return joblib.load(stream) - - def as_serializable_dict(self) -> dict: - """Convert item to a JSON-serializable dict to used by frontend.""" - with BytesIO() as stream: - self.figure.savefig(stream, format="svg", bbox_inches="tight") - - figure_bytes = stream.getvalue() - figure_b64_str = bytes_to_b64_str(figure_bytes) - - return super().as_serializable_dict() | { - "media_type": "image/svg+xml;base64", - "value": figure_b64_str, - } diff --git a/skore/src/skore/persistence/item/media_item.py b/skore/src/skore/persistence/item/media_item.py index 1c800f820..d3039be0d 100644 --- a/skore/src/skore/persistence/item/media_item.py +++ b/skore/src/skore/persistence/item/media_item.py @@ -92,10 +92,3 @@ def factory( raise ValueError(f"MIME type '{media_type}' is not supported.") return cls(media, media_type, **kwargs) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "media_type": self.media_type, - "value": self.media, - } diff --git a/skore/src/skore/persistence/item/numpy_array_item.py b/skore/src/skore/persistence/item/numpy_array_item.py index b8c5f13b6..f5ac86cb3 100644 --- a/skore/src/skore/persistence/item/numpy_array_item.py +++ b/skore/src/skore/persistence/item/numpy_array_item.py @@ -91,10 +91,3 @@ def factory(cls, array: numpy.ndarray, /, **kwargs) -> NumpyArrayItem: array_bytes = stream.getvalue() array_b64_str = bytes_to_b64_str(array_bytes) return cls(array_b64_str=array_b64_str, **kwargs) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "media_type": "text/markdown", - "value": self.array.tolist(), - } diff --git a/skore/src/skore/persistence/item/pandas_dataframe_item.py b/skore/src/skore/persistence/item/pandas_dataframe_item.py index 1e8e3dda6..b25c8798b 100644 --- a/skore/src/skore/persistence/item/pandas_dataframe_item.py +++ b/skore/src/skore/persistence/item/pandas_dataframe_item.py @@ -131,10 +131,3 @@ def factory(cls, dataframe: pandas.DataFrame, /, **kwargs) -> PandasDataFrameIte dataframe_json=dataframe.to_json(orient=PandasDataFrameItem.ORIENT), **kwargs, ) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "media_type": "application/vnd.dataframe", - "value": self.dataframe.fillna("NaN").to_dict(orient="tight"), - } diff --git a/skore/src/skore/persistence/item/pandas_series_item.py b/skore/src/skore/persistence/item/pandas_series_item.py index 8b943281d..f2cfcda7f 100644 --- a/skore/src/skore/persistence/item/pandas_series_item.py +++ b/skore/src/skore/persistence/item/pandas_series_item.py @@ -123,10 +123,3 @@ def factory(cls, series: pandas.Series, /, **kwargs) -> PandasSeriesItem: series_json=series.to_json(orient=PandasSeriesItem.ORIENT), **kwargs, ) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "value": self.series.fillna("NaN").to_list(), - "media_type": "text/markdown", - } diff --git a/skore/src/skore/persistence/item/pickle_item.py b/skore/src/skore/persistence/item/pickle_item.py index 0881da925..a61749da9 100644 --- a/skore/src/skore/persistence/item/pickle_item.py +++ b/skore/src/skore/persistence/item/pickle_item.py @@ -7,7 +7,6 @@ from __future__ import annotations from io import BytesIO -from traceback import format_exception from typing import Any, Optional import joblib @@ -79,29 +78,3 @@ def object(self) -> Any: with BytesIO(pickle_bytes) as stream: return joblib.load(stream) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - try: - object = self.object - except Exception as e: - value = "Item cannot be displayed" - traceback = "".join(format_exception(None, e, e.__traceback__)) - note = "".join( - ( - (self.note or ""), - "\n\n", - "UnpicklingError with complete traceback:", - "\n\n", - traceback, - ) - ) - else: - value = f"```python\n{repr(object)}\n```" - note = self.note - - return super().as_serializable_dict() | { - "media_type": "text/markdown", - "value": value, - "note": note, - } diff --git a/skore/src/skore/persistence/item/pillow_image_item.py b/skore/src/skore/persistence/item/pillow_image_item.py index e0e5199d9..417ab6af1 100644 --- a/skore/src/skore/persistence/item/pillow_image_item.py +++ b/skore/src/skore/persistence/item/pillow_image_item.py @@ -5,7 +5,6 @@ from __future__ import annotations -from io import BytesIO from typing import TYPE_CHECKING, Optional from skore.persistence.item.item import Item, ItemTypeError @@ -92,16 +91,3 @@ def image(self) -> PIL.Image.Image: size=self.image_size, data=image_bytes, ) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - with BytesIO() as stream: - self.image.save(stream, format="png") - - png_bytes = stream.getvalue() - png_b64_str = bytes_to_b64_str(png_bytes) - - return super().as_serializable_dict() | { - "media_type": "image/png;base64", - "value": png_b64_str, - } diff --git a/skore/src/skore/persistence/item/plotly_figure_item.py b/skore/src/skore/persistence/item/plotly_figure_item.py index 332e7582b..268d2dfd7 100644 --- a/skore/src/skore/persistence/item/plotly_figure_item.py +++ b/skore/src/skore/persistence/item/plotly_figure_item.py @@ -9,7 +9,6 @@ from skore.persistence.item.item import Item, ItemTypeError from skore.persistence.item.media_item import lazy_is_instance -from skore.utils import bytes_to_b64_str if TYPE_CHECKING: import plotly.basedatatypes @@ -76,13 +75,3 @@ def figure(self) -> plotly.basedatatypes.BaseFigure: import plotly.io return plotly.io.from_json(self.figure_str) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - figure_bytes = self.figure_str.encode("utf-8") - figure_b64_str = bytes_to_b64_str(figure_bytes) - - return super().as_serializable_dict() | { - "media_type": "application/vnd.plotly.v1+json;base64", - "value": figure_b64_str, - } diff --git a/skore/src/skore/persistence/item/polars_dataframe_item.py b/skore/src/skore/persistence/item/polars_dataframe_item.py index baa84723c..7fa4e7431 100644 --- a/skore/src/skore/persistence/item/polars_dataframe_item.py +++ b/skore/src/skore/persistence/item/polars_dataframe_item.py @@ -98,10 +98,3 @@ def factory(cls, dataframe: polars.DataFrame, /, **kwargs) -> PolarsDataFrameIte raise PolarsToJSONError("Conversion to JSON failed") from e return cls(dataframe_json=dataframe_json, **kwargs) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "value": self.dataframe.to_pandas().fillna("NaN").to_dict(orient="tight"), - "media_type": "application/vnd.dataframe", - } diff --git a/skore/src/skore/persistence/item/polars_series_item.py b/skore/src/skore/persistence/item/polars_series_item.py index 172565d4a..02d69c3f9 100644 --- a/skore/src/skore/persistence/item/polars_series_item.py +++ b/skore/src/skore/persistence/item/polars_series_item.py @@ -86,10 +86,3 @@ def factory(cls, series: polars.Series, /, **kwargs) -> PolarsSeriesItem: raise ItemTypeError(f"Type '{series.__class__}' is not supported.") return cls(series_json=series.to_frame().write_json(), **kwargs) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "value": self.series.to_list(), - "media_type": "text/markdown", - } diff --git a/skore/src/skore/persistence/item/primitive_item.py b/skore/src/skore/persistence/item/primitive_item.py index a7d341c72..8752a8778 100644 --- a/skore/src/skore/persistence/item/primitive_item.py +++ b/skore/src/skore/persistence/item/primitive_item.py @@ -89,10 +89,3 @@ def factory(cls, primitive: Primitive, /, **kwargs) -> PrimitiveItem: raise ItemTypeError(f"Type '{primitive.__class__}' is not supported.") return cls(primitive=primitive, **kwargs) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "media_type": "text/markdown", - "value": self.primitive, - } diff --git a/skore/src/skore/persistence/item/sklearn_base_estimator_item.py b/skore/src/skore/persistence/item/sklearn_base_estimator_item.py index d359d8b07..ae034b028 100644 --- a/skore/src/skore/persistence/item/sklearn_base_estimator_item.py +++ b/skore/src/skore/persistence/item/sklearn_base_estimator_item.py @@ -118,10 +118,3 @@ def factory( estimator_skops_untrusted_types=estimator_skops_untrusted_types, **kwargs, ) - - def as_serializable_dict(self): - """Convert item to a JSON-serializable dict to used by frontend.""" - return super().as_serializable_dict() | { - "value": self.estimator_html_repr, - "media_type": "application/vnd.sklearn.estimator+html", - } diff --git a/skore/src/skore/ui/project_routes.py b/skore/src/skore/ui/project_routes.py index 5178ca320..40b2c2e32 100644 --- a/skore/src/skore/ui/project_routes.py +++ b/skore/src/skore/ui/project_routes.py @@ -12,6 +12,7 @@ from skore.persistence.item import Item from skore.project.project import Project +from skore.ui.serializers import item_as_serializable router = APIRouter(prefix="/project") @@ -30,7 +31,7 @@ class SerializableItem: def __item_as_serializable(name: str, item: Item, version: int) -> SerializableItem: - d = item.as_serializable_dict() + d = item_as_serializable(item) return SerializableItem( name=name, media_type=d.get("media_type"), diff --git a/skore/src/skore/ui/serializers.py b/skore/src/skore/ui/serializers.py new file mode 100644 index 000000000..473293793 --- /dev/null +++ b/skore/src/skore/ui/serializers.py @@ -0,0 +1,197 @@ +"""Centralize all item serialization functions.""" + +from io import BytesIO +from traceback import format_exception + +from skore.persistence.item import ( + AltairChartItem, + Item, + MatplotlibFigureItem, + MediaItem, + NumpyArrayItem, + PandasDataFrameItem, + PandasSeriesItem, + PickleItem, + PillowImageItem, + PlotlyFigureItem, + PolarsDataFrameItem, + PolarsSeriesItem, + PrimitiveItem, + SklearnBaseEstimatorItem, +) +from skore.utils import bytes_to_b64_str + + +def item_as_serializable(item: Item): + """Convert item to a JSON-serializable dict.""" + subclass = {} + if isinstance(item, PrimitiveItem): + subclass = _primitive_item_as_serializable(item) + elif isinstance(item, NumpyArrayItem): + subclass = _numpy_array_item_as_serializable_dict(item) + elif isinstance(item, PandasDataFrameItem): + subclass = _pandas_data_frame_item_as_serializable_dict(item) + elif isinstance(item, PolarsDataFrameItem): + subclass = _polars_data_frame_item_as_serializable_dict(item) + elif isinstance(item, PandasSeriesItem): + subclass = _pandas_series_item_as_serializable_dict(item) + elif isinstance(item, PolarsSeriesItem): + subclass = _polars_series_item_as_serializable(item) + elif isinstance(item, SklearnBaseEstimatorItem): + subclass = _sklearn_base_estimator_item_as_serializable(item) + elif isinstance(item, MediaItem): + subclass = _media_item_as_serializable_dict(item) + elif isinstance(item, PillowImageItem): + subclass = _pillow_image_item_as_serializable_dict(item) + elif isinstance(item, PlotlyFigureItem): + subclass = _plotly_figure_item_as_serializable_dict(item) + elif isinstance(item, AltairChartItem): + subclass = _altair_chart_item_as_serializable_dict(item) + elif isinstance(item, MatplotlibFigureItem): + subclass = _matplotlib_figure_item_as_serializable_dict(item) + elif isinstance(item, PickleItem): + subclass = _pickle_item_as_serializable_dict(item) + return { + "updated_at": item.updated_at, + "created_at": item.created_at, + "note": item.note, + } | subclass + + +def _altair_chart_item_as_serializable_dict(item: AltairChartItem): + """Convert a AltairChartItem to a JSON-serializable dict to used by frontend.""" + chart_bytes = item.chart_str.encode("utf-8") + chart_b64_str = bytes_to_b64_str(chart_bytes) + + return { + "media_type": "application/vnd.vega.v5+json;base64", + "value": chart_b64_str, + } + + +def _matplotlib_figure_item_as_serializable_dict(item: MatplotlibFigureItem) -> dict: + """Convert a MatplotlibFigureItem to a JSON-serializable dict.""" + with BytesIO() as stream: + item.figure.savefig(stream, format="svg", bbox_inches="tight") + + figure_bytes = stream.getvalue() + figure_b64_str = bytes_to_b64_str(figure_bytes) + + return { + "media_type": "image/svg+xml;base64", + "value": figure_b64_str, + } + + +def _media_item_as_serializable_dict(item: MediaItem): + """Convert a MediaItem to a JSON-serializable dict.""" + return { + "media_type": item.media_type, + "value": item.media, + } + + +def _numpy_array_item_as_serializable_dict(item: NumpyArrayItem): + """Convert a NumpyArrayItem to a JSON-serializable dict.""" + return { + "media_type": "text/markdown", + "value": item.array.tolist(), + } + + +def _pandas_data_frame_item_as_serializable_dict(item: PandasDataFrameItem): + """Convert a PandasDataFrameItem to a JSON-serializable dict.""" + return { + "media_type": "application/vnd.dataframe", + "value": item.dataframe.fillna("NaN").to_dict(orient="tight"), + } + + +def _pandas_series_item_as_serializable_dict(item: PandasSeriesItem): + """Convert a PandasSeriesItem to a JSON-serializable dict.""" + return { + "value": item.series.fillna("NaN").to_list(), + "media_type": "text/markdown", + } + + +def _pickle_item_as_serializable_dict(item: PickleItem): + """Convert a PickleItem to a JSON-serializable dict.""" + try: + object = item.object + except Exception as e: + traceback = "".join(format_exception(None, e, e.__traceback__)) + value = "".join( + ( + "Item cannot be displayed", + "\n\n", + "UnpicklingError with complete traceback:", + "\n\n```pytb\n", + traceback, + "```", + ) + ) + else: + value = f"```python\n{repr(object)}\n```" + + return { + "media_type": "text/markdown", + "value": value, + } + + +def _pillow_image_item_as_serializable_dict(item: PillowImageItem): + """Convert a PillowImageItem to a JSON-serializable dict.""" + with BytesIO() as stream: + item.image.save(stream, format="png") + + png_bytes = stream.getvalue() + png_b64_str = bytes_to_b64_str(png_bytes) + + return { + "media_type": "image/png;base64", + "value": png_b64_str, + } + + +def _plotly_figure_item_as_serializable_dict(item: PlotlyFigureItem): + """Convert a PlotlyFigureItem to a JSON-serializable dict.""" + figure_bytes = item.figure_str.encode("utf-8") + figure_b64_str = bytes_to_b64_str(figure_bytes) + + return { + "media_type": "application/vnd.plotly.v1+json;base64", + "value": figure_b64_str, + } + + +def _polars_data_frame_item_as_serializable_dict(item: PolarsDataFrameItem): + """Convert a PolarsDataFrameItem to a JSON-serializable dict.""" + return { + "value": item.dataframe.to_pandas().fillna("NaN").to_dict(orient="tight"), + "media_type": "application/vnd.dataframe", + } + + +def _polars_series_item_as_serializable(item: PolarsSeriesItem): + """Convert a PolarsSeriesItem to a JSON-serializable dict.""" + return { + "value": item.series.to_list(), + "media_type": "text/markdown", + } + + +def _primitive_item_as_serializable(item: PrimitiveItem): + """Convert a PrimitiveItem to a JSON-serializable dict.""" + return { + "media_type": "text/markdown", + "value": item.primitive, + } + + +def _sklearn_base_estimator_item_as_serializable(item: SklearnBaseEstimatorItem): + """Convert a SklearnBaseEstimatorItem to a JSON-serializable dict.""" + return { + "value": item.estimator_html_repr, + "media_type": "application/vnd.sklearn.estimator+html", + } diff --git a/skore/tests/integration/ui/test_serializers.py b/skore/tests/integration/ui/test_serializers.py new file mode 100644 index 000000000..72730c0c0 --- /dev/null +++ b/skore/tests/integration/ui/test_serializers.py @@ -0,0 +1,230 @@ +import base64 +import io + +import altair +import numpy +import pandas +import PIL +import plotly +import polars +import pytest +import sklearn +from matplotlib.figure import Figure +from skore.persistence.item.altair_chart_item import AltairChartItem +from skore.persistence.item.matplotlib_figure_item import MatplotlibFigureItem +from skore.persistence.item.media_item import MediaItem, MediaType +from skore.persistence.item.numpy_array_item import NumpyArrayItem +from skore.persistence.item.pandas_dataframe_item import PandasDataFrameItem +from skore.persistence.item.pandas_series_item import PandasSeriesItem +from skore.persistence.item.pickle_item import PickleItem +from skore.persistence.item.pillow_image_item import PillowImageItem +from skore.persistence.item.plotly_figure_item import PlotlyFigureItem +from skore.persistence.item.polars_dataframe_item import PolarsDataFrameItem +from skore.persistence.item.polars_series_item import PolarsSeriesItem +from skore.persistence.item.primitive_item import PrimitiveItem +from skore.persistence.item.sklearn_base_estimator_item import SklearnBaseEstimatorItem +from skore.ui.serializers import ( + _altair_chart_item_as_serializable_dict, + _matplotlib_figure_item_as_serializable_dict, + _media_item_as_serializable_dict, + _numpy_array_item_as_serializable_dict, + _pandas_data_frame_item_as_serializable_dict, + _pandas_series_item_as_serializable_dict, + _pickle_item_as_serializable_dict, + _pillow_image_item_as_serializable_dict, + _plotly_figure_item_as_serializable_dict, + _polars_data_frame_item_as_serializable_dict, + _polars_series_item_as_serializable, + _primitive_item_as_serializable, + _sklearn_base_estimator_item_as_serializable, + item_as_serializable, +) +from skore.utils import bytes_to_b64_str + + +def test_item_as_serializable_dict(monkeypatch, mock_nowstr, MockDatetime): + monkeypatch.setattr("skore.persistence.item.item.datetime", MockDatetime) + + item = PrimitiveItem.factory(2) + assert item_as_serializable(item) == { + "updated_at": mock_nowstr, + "created_at": mock_nowstr, + "note": None, + "media_type": "text/markdown", + "value": 2, + } + + +def test_altair_chart_item_as_serializable_dict(): + chart = altair.Chart().mark_point() + chart_str = chart.to_json() + chart_bytes = chart_str.encode("utf-8") + chart_b64_str = base64.b64encode(chart_bytes).decode() + + item = AltairChartItem.factory(chart) + + assert _altair_chart_item_as_serializable_dict(item) == { + "media_type": "application/vnd.vega.v5+json;base64", + "value": chart_b64_str, + } + + +class FakeFigure(Figure): + def savefig(self, stream, *args, **kwargs): + stream.write(b"
") + + +def test_matplotlib_figure_item_as_serializable_dict(): + figure = FakeFigure() + + with io.BytesIO() as stream: + figure.savefig(stream, format="svg", bbox_inches="tight") + + figure_bytes = stream.getvalue() + figure_b64_str = base64.b64encode(figure_bytes).decode() + + item = MatplotlibFigureItem.factory(figure) + + assert _matplotlib_figure_item_as_serializable_dict(item) == { + "media_type": "image/svg+xml;base64", + "value": figure_b64_str, + } + + +@pytest.mark.parametrize("media_type", [enum.value for enum in MediaType]) +def test_media_item_as_serializable_dict(media_type): + item = MediaItem.factory("", media_type) + + assert _media_item_as_serializable_dict(item) == { + "media_type": media_type, + "value": "", + } + + +def test_numpy_array_item_as_serializable_dict(): + array = numpy.array([1, 2, 3]) + + item = NumpyArrayItem.factory(array) + serializable = _numpy_array_item_as_serializable_dict(item) + assert serializable == { + "media_type": "text/markdown", + "value": array.tolist(), + } + + +def test_pandas_data_frame_item_as_serializable_dict(): + dataframe = pandas.DataFrame( + [{"key": numpy.array([1])}], pandas.Index([0], name="myIndex") + ) + item = PandasDataFrameItem.factory(dataframe) + serializable = _pandas_data_frame_item_as_serializable_dict(item) + assert serializable == { + "media_type": "application/vnd.dataframe", + "value": dataframe.fillna("NaN").to_dict(orient="tight"), + } + + +def test_pandas_series_item_as_serializable_dict(): + series = pandas.Series([numpy.array([1])], pandas.Index([0], name="myIndex")) + item = PandasSeriesItem.factory(series) + serializable = _pandas_series_item_as_serializable_dict(item) + assert serializable == { + "media_type": "text/markdown", + "value": series.to_list(), + } + + +def test_pickle_item_as_serializable_dict(): + item = PickleItem.factory(int) + serializable = _pickle_item_as_serializable_dict(item) + assert serializable == { + "media_type": "text/markdown", + "value": "```python\n\n```", + } + + +def test_pillow_image_item_as_serializable_dict(): + image = PIL.Image.new("RGB", (100, 100), color="red") + item = PillowImageItem.factory(image) + + with io.BytesIO() as stream: + image.save(stream, format="png") + + png_bytes = stream.getvalue() + png_b64_str = bytes_to_b64_str(png_bytes) + + assert _pillow_image_item_as_serializable_dict(item) == { + "media_type": "image/png;base64", + "value": png_b64_str, + } + + +def test_plotly_figure_item_as_serializable_dict(): + bar = plotly.graph_objects.Bar(x=[1, 2, 3], y=[1, 3, 2]) + figure = plotly.graph_objects.Figure(data=[bar]) + figure_str = plotly.io.to_json(figure, engine="json") + figure_bytes = figure_str.encode("utf-8") + figure_b64_str = base64.b64encode(figure_bytes).decode() + + item = PlotlyFigureItem.factory(figure) + + assert _plotly_figure_item_as_serializable_dict(item) == { + "media_type": "application/vnd.plotly.v1+json;base64", + "value": figure_b64_str, + } + + +def test_polars_data_frame_item_as_serializable_dict(): + dataframe = polars.DataFrame([{"key": "value"}]) + item = PolarsDataFrameItem.factory(dataframe) + serializable = _polars_data_frame_item_as_serializable_dict(item) + assert serializable == { + "media_type": "application/vnd.dataframe", + "value": dataframe.to_pandas().fillna("NaN").to_dict(orient="tight"), + } + + +def test_polars_series_item_as_serializable(): + series = polars.Series([numpy.array([1, 2])]) + item = PolarsSeriesItem.factory(series) + serializable = _polars_series_item_as_serializable(item) + assert serializable == { + "media_type": "text/markdown", + "value": series.to_list(), + } + + +@pytest.mark.parametrize( + "primitive", + [ + 0, + 1.1, + True, + [0, 1, 2], + (0, 1, 2), + {"a": 0}, + ], +) +def test_primitive_item_as_serializable(monkeypatch, MockDatetime, primitive): + monkeypatch.setattr("skore.persistence.item.item.datetime", MockDatetime) + + item = PrimitiveItem.factory(primitive) + serializable = _primitive_item_as_serializable(item) + assert serializable == { + "media_type": "text/markdown", + "value": primitive, + } + + +def test_sklearn_base_estimator_item_as_serializable(): + class Estimator(sklearn.svm.SVC): + pass + + estimator = Estimator() + item = SklearnBaseEstimatorItem.factory(estimator) + serializable = _sklearn_base_estimator_item_as_serializable(item) + + assert serializable == { + "media_type": "application/vnd.sklearn.estimator+html", + "value": item.estimator_html_repr, + } diff --git a/skore/tests/integration/ui/test_ui.py b/skore/tests/integration/ui/test_ui.py index 052e4480e..067759fbc 100644 --- a/skore/tests/integration/ui/test_ui.py +++ b/skore/tests/integration/ui/test_ui.py @@ -362,7 +362,7 @@ def test_get_items_with_pickle_item_and_unpickling_error( monkeypatch.delattr("skore.persistence.item.numpy_array_item.NumpyArrayItem") monkeypatch.setattr( - "skore.persistence.item.pickle_item.format_exception", + "skore.ui.serializers.format_exception", lambda *args, **kwargs: "", ) @@ -375,8 +375,8 @@ def test_get_items_with_pickle_item_and_unpickling_error( "updated_at": mock_nowstr, "name": "pickle", "media_type": "text/markdown", - "value": "Item cannot be displayed", - "note": ("\n\nUnpicklingError with complete traceback:\n\n"), + "value": "Item cannot be displayed\n\nUnpicklingError with complete traceback:\n\n```pytb\n```", # noqa: E501 + "note": None, "version": 0, }, ] diff --git a/skore/tests/unit/item/test_altair_chart_item.py b/skore/tests/unit/item/test_altair_chart_item.py index 10b26d7fd..6cb08b455 100644 --- a/skore/tests/unit/item/test_altair_chart_item.py +++ b/skore/tests/unit/item/test_altair_chart_item.py @@ -1,4 +1,3 @@ -import base64 import json import altair @@ -40,19 +39,3 @@ def test_chart(self): # Altair strict equality doesn't work assert item1.chart.to_json() == chart_str assert item2.chart.to_json() == chart_str - - def test_as_serializable_dict(self, mock_nowstr): - chart = altair.Chart().mark_point() - chart_str = chart.to_json() - chart_bytes = chart_str.encode("utf-8") - chart_b64_str = base64.b64encode(chart_bytes).decode() - - item = AltairChartItem.factory(chart) - - assert item.as_serializable_dict() == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "application/vnd.vega.v5+json;base64", - "value": chart_b64_str, - } diff --git a/skore/tests/unit/item/test_matplotlib_figure_item.py b/skore/tests/unit/item/test_matplotlib_figure_item.py index 7da35aa50..19046783a 100644 --- a/skore/tests/unit/item/test_matplotlib_figure_item.py +++ b/skore/tests/unit/item/test_matplotlib_figure_item.py @@ -1,4 +1,3 @@ -import base64 import io import json @@ -13,11 +12,6 @@ from skore.utils import b64_str_to_bytes, bytes_to_b64_str -class FakeFigure(Figure): - def savefig(self, stream, *args, **kwargs): - stream.write(b"
") - - class TestMatplotlibFigureItem: @pytest.fixture(autouse=True) def monkeypatch_datetime(self, monkeypatch, MockDatetime): @@ -77,25 +71,6 @@ def test_figure(self, tmp_path): compare_images(tmp_path / "figure.png", tmp_path / "item2.png", 0) is None ) - def test_as_serializable_dict(self, mock_nowstr): - figure = FakeFigure() - - with io.BytesIO() as stream: - figure.savefig(stream, format="svg", bbox_inches="tight") - - figure_bytes = stream.getvalue() - figure_b64_str = base64.b64encode(figure_bytes).decode() - - item = MatplotlibFigureItem.factory(figure) - - assert item.as_serializable_dict() == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "image/svg+xml;base64", - "value": figure_b64_str, - } - def test_backend_switch(self): backend = get_backend() # hoppefuly PostScript is never the default in tests ^^ diff --git a/skore/tests/unit/item/test_media_item.py b/skore/tests/unit/item/test_media_item.py index c787d0d32..364943315 100644 --- a/skore/tests/unit/item/test_media_item.py +++ b/skore/tests/unit/item/test_media_item.py @@ -22,15 +22,3 @@ def test_factory(self, mock_nowstr, media_type): assert item.media_type == media_type assert item.created_at == mock_nowstr assert item.updated_at == mock_nowstr - - @pytest.mark.parametrize("media_type", [enum.value for enum in MediaType]) - def test_as_serializable_dict(self, mock_nowstr, media_type): - item = MediaItem.factory("", media_type) - - assert item.as_serializable_dict() == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": media_type, - "value": "", - } diff --git a/skore/tests/unit/item/test_numpy_array_item.py b/skore/tests/unit/item/test_numpy_array_item.py index a7ae6c315..686c69a36 100644 --- a/skore/tests/unit/item/test_numpy_array_item.py +++ b/skore/tests/unit/item/test_numpy_array_item.py @@ -65,16 +65,3 @@ def test_array_with_complex_object(self, mock_nowstr): match="Object arrays cannot be saved when allow_pickle=False", ): NumpyArrayItem.factory(array) - - def test_get_serializable_dict(self, mock_nowstr): - array = numpy.array([1, 2, 3]) - - item = NumpyArrayItem.factory(array) - serializable = item.as_serializable_dict() - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "text/markdown", - "value": array.tolist(), - } diff --git a/skore/tests/unit/item/test_pandas_dataframe_item.py b/skore/tests/unit/item/test_pandas_dataframe_item.py index a9b85162d..b41344e73 100644 --- a/skore/tests/unit/item/test_pandas_dataframe_item.py +++ b/skore/tests/unit/item/test_pandas_dataframe_item.py @@ -80,15 +80,3 @@ def test_dataframe_with_integer_columns_name_and_multiindex(self, mock_nowstr): assert_frame_equal(item1.dataframe, dataframe) assert_frame_equal(item2.dataframe, dataframe) - - def test_get_serializable_dict(self, mock_nowstr): - dataframe = DataFrame([{"key": np.array([1])}], Index([0], name="myIndex")) - item = PandasDataFrameItem.factory(dataframe) - serializable = item.as_serializable_dict() - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "application/vnd.dataframe", - "value": dataframe.fillna("NaN").to_dict(orient="tight"), - } diff --git a/skore/tests/unit/item/test_pandas_series_item.py b/skore/tests/unit/item/test_pandas_series_item.py index 6429f9cb3..34e37635c 100644 --- a/skore/tests/unit/item/test_pandas_series_item.py +++ b/skore/tests/unit/item/test_pandas_series_item.py @@ -80,15 +80,3 @@ def test_series_with_integer_indexes_name_and_multiindex(self, mock_nowstr): assert_series_equal(item1.series, series) assert_series_equal(item2.series, series) - - def test_get_serializable_dict(self, mock_nowstr): - series = Series([np.array([1])], Index([0], name="myIndex")) - item = PandasSeriesItem.factory(series) - serializable = item.as_serializable_dict() - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "text/markdown", - "value": series.to_list(), - } diff --git a/skore/tests/unit/item/test_pickle_item.py b/skore/tests/unit/item/test_pickle_item.py index c0a10adff..a069d458c 100644 --- a/skore/tests/unit/item/test_pickle_item.py +++ b/skore/tests/unit/item/test_pickle_item.py @@ -48,15 +48,3 @@ def test_object(self, mock_nowstr): assert item1.object is int assert item2.object is int - - def test_get_serializable_dict(self, mock_nowstr): - item = PickleItem.factory(int) - serializable = item.as_serializable_dict() - - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "text/markdown", - "value": "```python\n\n```", - } diff --git a/skore/tests/unit/item/test_pillow_image_item.py b/skore/tests/unit/item/test_pillow_image_item.py index a9e4d21f2..3eafac5d4 100644 --- a/skore/tests/unit/item/test_pillow_image_item.py +++ b/skore/tests/unit/item/test_pillow_image_item.py @@ -1,4 +1,3 @@ -import io import json import PIL.Image @@ -51,21 +50,3 @@ def test_image(self): assert item1.image == image assert item2.image == image - - def test_as_serializable_dict(self, mock_nowstr): - image = PIL.Image.new("RGB", (100, 100), color="red") - item = PillowImageItem.factory(image) - - with io.BytesIO() as stream: - image.save(stream, format="png") - - png_bytes = stream.getvalue() - png_b64_str = bytes_to_b64_str(png_bytes) - - assert item.as_serializable_dict() == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "image/png;base64", - "value": png_b64_str, - } diff --git a/skore/tests/unit/item/test_plotly_figure_item.py b/skore/tests/unit/item/test_plotly_figure_item.py index 406c167b3..fee680eca 100644 --- a/skore/tests/unit/item/test_plotly_figure_item.py +++ b/skore/tests/unit/item/test_plotly_figure_item.py @@ -1,4 +1,3 @@ -import base64 import json import plotly.graph_objects @@ -42,20 +41,3 @@ def test_figure(self): assert item1.figure == figure assert item2.figure == figure - - def test_as_serializable_dict(self, mock_nowstr): - bar = plotly.graph_objects.Bar(x=[1, 2, 3], y=[1, 3, 2]) - figure = plotly.graph_objects.Figure(data=[bar]) - figure_str = plotly.io.to_json(figure, engine="json") - figure_bytes = figure_str.encode("utf-8") - figure_b64_str = base64.b64encode(figure_bytes).decode() - - item = PlotlyFigureItem.factory(figure) - - assert item.as_serializable_dict() == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "application/vnd.plotly.v1+json;base64", - "value": figure_b64_str, - } diff --git a/skore/tests/unit/item/test_polars_dataframe_item.py b/skore/tests/unit/item/test_polars_dataframe_item.py index 923c9ee1f..49ade3850 100644 --- a/skore/tests/unit/item/test_polars_dataframe_item.py +++ b/skore/tests/unit/item/test_polars_dataframe_item.py @@ -46,15 +46,3 @@ def test_dataframe_with_complex_object(self, mock_nowstr): with pytest.raises(PolarsToJSONError): PolarsDataFrameItem.factory(dataframe) - - def test_get_serializable_dict(self, mock_nowstr): - dataframe = DataFrame([{"key": "value"}]) - item = PolarsDataFrameItem.factory(dataframe) - serializable = item.as_serializable_dict() - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "application/vnd.dataframe", - "value": dataframe.to_pandas().fillna("NaN").to_dict(orient="tight"), - } diff --git a/skore/tests/unit/item/test_polars_series_item.py b/skore/tests/unit/item/test_polars_series_item.py index 9a45155fe..7d79ba4a2 100644 --- a/skore/tests/unit/item/test_polars_series_item.py +++ b/skore/tests/unit/item/test_polars_series_item.py @@ -48,15 +48,3 @@ def test_series_with_complex_object(self, mock_nowstr): item = PolarsSeriesItem.factory(series) assert_series_equal(item.series, series, check_dtypes=False) - - def test_get_serializable_dict(self, mock_nowstr): - series = Series([np.array([1, 2])]) - item = PolarsSeriesItem.factory(series) - serializable = item.as_serializable_dict() - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "text/markdown", - "value": series.to_list(), - } diff --git a/skore/tests/unit/item/test_primitive_item.py b/skore/tests/unit/item/test_primitive_item.py index f8963b368..6408beab9 100644 --- a/skore/tests/unit/item/test_primitive_item.py +++ b/skore/tests/unit/item/test_primitive_item.py @@ -29,29 +29,3 @@ def test_factory_exception(self): with pytest.raises(ItemTypeError): PrimitiveItem.factory("") - - @pytest.mark.parametrize( - "primitive", - [ - 0, - 1.1, - True, - [0, 1, 2], - (0, 1, 2), - {"a": 0}, - ], - ) - def test_get_serializable_dict( - self, monkeypatch, mock_nowstr, MockDatetime, primitive - ): - monkeypatch.setattr("skore.persistence.item.item.datetime", MockDatetime) - - item = PrimitiveItem.factory(primitive) - serializable = item.as_serializable_dict() - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "text/markdown", - "value": primitive, - } diff --git a/skore/tests/unit/item/test_sklearn_base_estimator_item.py b/skore/tests/unit/item/test_sklearn_base_estimator_item.py index f2a2ebb3e..b4d3d371a 100644 --- a/skore/tests/unit/item/test_sklearn_base_estimator_item.py +++ b/skore/tests/unit/item/test_sklearn_base_estimator_item.py @@ -106,16 +106,3 @@ def test_estimator_untrusted(self, mock_nowstr): assert isinstance(item1.estimator, Estimator) assert isinstance(item2.estimator, Estimator) - - def test_get_serializable_dict(self, mock_nowstr): - estimator = Estimator() - item = SklearnBaseEstimatorItem.factory(estimator) - serializable = item.as_serializable_dict() - - assert serializable == { - "updated_at": mock_nowstr, - "created_at": mock_nowstr, - "note": None, - "media_type": "application/vnd.sklearn.estimator+html", - "value": item.estimator_html_repr, - }