Skip to content

Commit

Permalink
First pass at integration. Still more work to do
Browse files Browse the repository at this point in the history
  • Loading branch information
schloerke committed Jul 3, 2024
1 parent 7de1130 commit 4fe7cf2
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 291 deletions.
23 changes: 4 additions & 19 deletions shiny/render/_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@
# * For this release: Immediately make PR to remove `.input_` from `.input_cell_selection()`
# TODO-barret-render.data_frame; Docs
# TODO-barret-render.data_frame; Add examples!
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
Literal,
TypeVar,
Union,
cast,
)
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Literal, Union, cast

from htmltools import Tag

Expand All @@ -42,18 +32,17 @@
SelectionModes,
as_cell_selection,
assert_patches_shape,
cast_to_pandas,
cell_patch_processed_to_jsonifiable,
wrap_shiny_html,
)
from ._data_frame_utils._styles import as_browser_style_infos
from ._data_frame_utils._tbl_data import as_data_frame_like, serialize_dtype
from ._data_frame_utils._types import (
ColumnFilter,
ColumnSort,
FrameRender,
frame_render_to_jsonifiable,
)
from ._data_frame_utils._unsafe import serialize_numpy_dtype

# as_selection_location_js,
from .renderer import Jsonifiable, Renderer, ValueFn
Expand All @@ -63,10 +52,6 @@

from ..session import Session

DataFrameT = TypeVar("DataFrameT", bound=pd.DataFrame)
# TODO-barret-render.data_frame; Pandas, Polars, api compat, etc.; Today, we only support Pandas


from ._data_frame_utils._datagridtable import DataFrameResult

# # TODO-future; Use `dataframe-api-compat>=0.2.6` to injest dataframes and return standardized dataframe structures
Expand Down Expand Up @@ -820,7 +805,7 @@ async def render(self) -> JsonifiableDict | None:

if not isinstance(value, AbstractTabularData):
value = DataGrid(
cast_to_pandas(
as_data_frame_like(
value,
"@render.data_frame doesn't know how to render objects of type",
)
Expand Down Expand Up @@ -970,7 +955,7 @@ async def update_sort(
val_dict: ColumnSort
if isinstance(val, int):
col: pd.Series[Any] = data.iloc[:, val]
desc = serialize_numpy_dtype(col)["type"] == "numeric"
desc = serialize_dtype(col)["type"] == "numeric"
val_dict = {"col": val, "desc": desc}
val_dict: ColumnSort = (
val if isinstance(val, dict) else {"col": val, "desc": True}
Expand Down
6 changes: 2 additions & 4 deletions shiny/render/_data_frame_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from ._datagridtable import (
AbstractTabularData,
CellHtml,
DataGrid,
DataTable,
cast_to_pandas,
wrap_shiny_html,
)
from ._html import wrap_shiny_html
from ._patch import (
CellPatch,
CellPatchProcessed,
Expand All @@ -24,12 +22,12 @@
SelectionModes,
as_cell_selection,
)
from ._types import CellHtml

__all__ = (
"AbstractTabularData",
"DataGrid",
"DataTable",
"cast_to_pandas",
"wrap_shiny_html",
"CellHtml",
"CellPatch",
Expand Down
163 changes: 12 additions & 151 deletions shiny/render/_data_frame_utils/_datagridtable.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,26 @@
from __future__ import annotations

import abc
import json

# TODO-barret-future; make DataTable and DataGrid generic? By currently accepting `object`, it is difficult to capture the generic type of the data.
from typing import (
TYPE_CHECKING,
Literal,
Protocol,
TypeVar,
Union,
cast,
overload,
runtime_checkable,
)

from htmltools import TagNode
from typing import TYPE_CHECKING, Literal, Union

from ..._docstring import add_example, no_example
from ..._typing_extensions import TypedDict
from ...session._utils import RenderedDeps, require_active_session
from ...types import Jsonifiable
from ._selection import (
RowSelectionModeDeprecated,
SelectionModeInput,
SelectionModes,
as_selection_modes,
)
from ._styles import StyleFn, StyleInfo, as_browser_style_infos, as_style_infos
from ._types import FrameJson
from ._unsafe import is_shiny_html, serialize_numpy_dtypes
from ._tbl_data import as_data_frame_like, serialize_frame
from ._types import DataFrameLike, FrameJson

if TYPE_CHECKING:
import pandas as pd

from ...session import Session

DataFrameT = TypeVar("DataFrameT", bound=pd.DataFrame)
# TODO-future; Pandas, Polars, api compat, etc.; Today, we only support Pandas

DataFrameResult = Union[
None,
pd.DataFrame,
DataFrameLike,
"DataGrid",
"DataTable",
]
Expand Down Expand Up @@ -120,7 +99,7 @@ class DataGrid(AbstractTabularData):
* :class:`~shiny.render.DataTable`
"""

data: pd.DataFrame
data: DataFrameLike
width: str | float | None
height: str | float | None
summary: bool | str
Expand All @@ -131,7 +110,7 @@ class DataGrid(AbstractTabularData):

def __init__(
self,
data: pd.DataFrame | PandasCompatible,
data: DataFrameLike,
*,
width: str | float | None = "fit-content",
height: str | float | None = None,
Expand All @@ -143,7 +122,7 @@ def __init__(
row_selection_mode: RowSelectionModeDeprecated = "deprecated",
):

self.data = cast_to_pandas(
self.data = as_data_frame_like(
data,
"The DataGrid() constructor didn't expect a 'data' argument of type",
)
Expand All @@ -163,7 +142,7 @@ def __init__(

def to_payload(self) -> FrameJson:
res: FrameJson = {
**serialize_pandas_df(self.data),
**serialize_frame(self.data),
"options": {
"width": self.width,
"height": self.height,
Expand Down Expand Up @@ -244,7 +223,7 @@ class DataTable(AbstractTabularData):
* :class:`~shiny.render.DataGrid`
"""

data: pd.DataFrame
data: DataFrameLike
width: str | float | None
height: str | float | None
summary: bool | str
Expand All @@ -254,7 +233,7 @@ class DataTable(AbstractTabularData):

def __init__(
self,
data: pd.DataFrame | PandasCompatible,
data: DataFrameLike,
*,
width: str | float | None = "fit-content",
height: str | float | None = "500px",
Expand All @@ -266,7 +245,7 @@ def __init__(
styles: StyleInfo | list[StyleInfo] | StyleFn | None = None,
):

self.data = cast_to_pandas(
self.data = as_data_frame_like(
data,
"The DataTable() constructor didn't expect a 'data' argument of type",
)
Expand All @@ -286,7 +265,7 @@ def __init__(

def to_payload(self) -> FrameJson:
res: FrameJson = {
**serialize_pandas_df(self.data),
**serialize_frame(self.data),
"options": {
"width": self.width,
"height": self.height,
Expand All @@ -301,121 +280,3 @@ def to_payload(self) -> FrameJson:
},
}
return res


def serialize_pandas_df(df: "pd.DataFrame") -> FrameJson:

columns = df.columns.tolist()
columns_set = set(columns)
if len(columns_set) != len(columns):
raise ValueError(
"The column names of the pandas DataFrame are not unique."
" This is not supported by the data_frame renderer."
)

# Currently, we don't make use of the index; drop it so we don't error trying to
# serialize it or something
df = df.reset_index(drop=True)

# # Can we keep the original column information?
# # Maybe we need to inspect the original columns for any "unknown" column type. See if it contains any HTML or Tag objects
# for col in columns:
# if df[col].dtype.name == "unknown":
# print(df[col].to_list())
# raise ValueError(
# "The pandas DataFrame contains columns of type 'object'."
# " This is not supported by the data_frame renderer."
# )

type_hints = serialize_numpy_dtypes(df)

# Auto opt-in for html columns
html_columns = [
i for i, type_hint in enumerate(type_hints) if type_hint["type"] == "html"
]

if len(html_columns) > 0:
# Enable copy-on-write mode for the data;
# Use `deep=False` to avoid copying the full data; CoW will copy the necessary data when modified
import pandas as pd

with pd.option_context("mode.copy_on_write", True):
df = df.copy(deep=False)
session = require_active_session(None)

def wrap_shiny_html_with_session(x: TagNode):
return wrap_shiny_html(x, session=session)

for html_column in html_columns:
# _upgrade_ all the HTML columns to `CellHtml` json objects
df[df.columns[html_column]] = df[
df.columns[html_column]
].apply( # pyright: ignore[reportUnknownMemberType]
wrap_shiny_html_with_session
)

# note that date_format iso converts durations to ISO8601 Durations.
# e.g. 1 Day -> P1DT0H0M0S
# see https://en.wikipedia.org/wiki/ISO_8601#Durations
res = json.loads(
# {index: [index], columns: [columns], data: [values]}
df.to_json( # pyright: ignore[reportUnknownMemberType]
None, orient="split", date_format="iso", default_handler=str
)
)

res["typeHints"] = type_hints

# print(json.dumps(res, indent=4))
return res


@runtime_checkable
class PandasCompatible(Protocol):
# Signature doesn't matter, runtime_checkable won't look at it anyway
def to_pandas(self) -> pd.DataFrame: ...


@overload
def cast_to_pandas(x: DataFrameT, error_message_begin: str) -> DataFrameT: ...


@overload
def cast_to_pandas(x: PandasCompatible, error_message_begin: str) -> pd.DataFrame: ...


def cast_to_pandas(
x: DataFrameT | PandasCompatible, error_message_begin: str
) -> DataFrameT | pd.DataFrame:
import pandas as pd

if isinstance(x, pd.DataFrame):
return x

if isinstance(x, PandasCompatible):
return x.to_pandas()

raise TypeError(
error_message_begin
+ f" '{str(type(x))}'. Use either a pandas.DataFrame, or an object"
" that has a .to_pandas() method."
)


class CellHtml(TypedDict):
isShinyHtml: bool
obj: RenderedDeps


@overload
def wrap_shiny_html( # pyright: ignore[reportOverlappingOverload]
x: TagNode, *, session: Session
) -> CellHtml: ...
@overload
def wrap_shiny_html(x: Jsonifiable, *, session: Session) -> Jsonifiable: ...
def wrap_shiny_html(
x: Jsonifiable | TagNode, *, session: Session
) -> Jsonifiable | CellHtml:
if is_shiny_html(x):
return {"isShinyHtml": True, "obj": session._process_ui(x)}
return cast(Jsonifiable, x)
36 changes: 36 additions & 0 deletions shiny/render/_data_frame_utils/_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, cast, overload

from htmltools import HTML, MetadataNode, Tagifiable, TagNode

from ..._typing_extensions import TypeGuard
from ...types import Jsonifiable
from ._types import CellHtml, ReprHtml, SeriesLike

if TYPE_CHECKING:
from ...session import Session


@overload
def wrap_shiny_html( # pyright: ignore[reportOverlappingOverload]
x: TagNode, *, session: Session
) -> CellHtml: ...
@overload
def wrap_shiny_html(x: Jsonifiable, *, session: Session) -> Jsonifiable: ...
def wrap_shiny_html(
x: Jsonifiable | TagNode, *, session: Session
) -> Jsonifiable | CellHtml:
if is_shiny_html(x):
return {"isShinyHtml": True, "obj": session._process_ui(x)}
return cast(Jsonifiable, x)


def col_contains_shiny_html(col: SeriesLike) -> bool:
return any(is_shiny_html(val) for _, val in enumerate(col))


# TODO-barret-test; Add test to assert the union type of `TagNode` contains `str` and (HTML | Tagifiable | MetadataNode | ReprHtml). Until a `is tag renderable` method is available in htmltools, we need to check for these types manually and must stay in sync with the `TagNode` union type.
# TODO-barret-future; Use `TypeIs[HTML | Tagifiable | MetadataNode | ReprHtml]` when it is available from typing_extensions
def is_shiny_html(val: Any) -> TypeGuard[HTML | Tagifiable | MetadataNode | ReprHtml]:
return isinstance(val, (HTML, Tagifiable, MetadataNode, ReprHtml))
Loading

0 comments on commit 4fe7cf2

Please sign in to comment.