Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filtering to RecallContextManager #1095

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions shiny/express/_recall_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
from types import TracebackType
from typing import Callable, Generic, Mapping, Optional, Type, TypeVar

from htmltools import MetadataNode, Tag, TagList, wrap_displayhook_handler
from htmltools import (
MetadataNode,
Tag,
TagAttrs,
TagChild,
Tagifiable,
TagList,
wrap_displayhook_handler,
)

from .._typing_extensions import ParamSpec
from .._typing_extensions import ParamSpec, TypeGuard

P = ParamSpec("P")
R = TypeVar("R")
Expand All @@ -21,14 +29,22 @@ def __init__(
*,
args: tuple[object, ...] | None = None,
kwargs: Mapping[str, object] | None = None,
filter: Callable[[object], bool] | None = None,
):
self.fn = fn

if args is None:
args = tuple()
self.args: list[object] = list(args)

if kwargs is None:
kwargs = {}
self.args: list[object] = list(args)
self.kwargs: dict[str, object] = dict(kwargs)

if filter is None:
filter = lambda x: True
self.filter = filter

# Let htmltools.wrap_displayhook_handler decide what to do with objects before
# we append them.
self.wrapped_append = wrap_displayhook_handler(self.args.append)
Expand Down Expand Up @@ -63,6 +79,8 @@ def displayhook(self, x: object) -> None:
# the result from the RecallContextManager.
with x:
pass
elif not self.filter(x):
pass
else:
self.wrapped_append(x)

Expand All @@ -88,3 +106,39 @@ def wrapped_fn(*args: P.args, **kwargs: P.kwargs) -> RecallContextManager[R]:
return RecallContextManager(fn, args=args, kwargs=kwargs)

return wrapped_fn


def filter_ui_objects(x: object) -> TypeGuard[TagChild | TagAttrs | None]:
# Can't seem to figure out how to get typing to work
valid_types = ( # type: ignore
dict,
str,
Tagifiable,
Tag,
TagList,
MetadataNode,
str,
float,
list,
tuple,
)

return (
x is None
or isinstance(x, valid_types)
# TODO: Should export ReprHtml protocol class from htmltools and add it to
# valid_types, and remove line below.
or callable(getattr(x, "_repr_html_", None))
)


class UiRecallContextManager(RecallContextManager[R]):
def __init__(
self,
fn: Callable[..., R],
*,
args: tuple[object, ...] | None = None,
kwargs: Mapping[str, object] | None = None,
filter: Callable[[object], bool] | None = filter_ui_objects,
):
super().__init__(fn, args=args, kwargs=kwargs, filter=filter)
74 changes: 52 additions & 22 deletions shiny/express/ui/_cm_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@

from __future__ import annotations

from typing import Literal, Optional

from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, TagFunction, TagList
from typing import Any, Literal, Optional, TypeGuard

from htmltools import (
MetadataNode,
Tag,
TagAttrs,
TagAttrValue,
TagChild,
TagFunction,
TagList,
)

from ... import ui
from ...types import MISSING, MISSING_TYPE
from ...types import MISSING, MISSING_TYPE, NavSetArg
from ...ui._accordion import AccordionPanel
from ...ui._card import CardItem
from ...ui._layout_columns import BreakpointsUser
from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetBar, NavSetCard
from ...ui.css import CssUnit
from .._recall_context import RecallContextManager
from .._recall_context import RecallContextManager, UiRecallContextManager

__all__ = (
"sidebar",
Expand Down Expand Up @@ -108,7 +116,7 @@ def sidebar(
* If four, then the values will be interpreted as top, right, bottom, and left
respectively.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.sidebar,
kwargs=dict(
width=width,
Expand Down Expand Up @@ -186,7 +194,7 @@ def layout_sidebar(
height
Any valid CSS unit to use for the height.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.layout_sidebar,
kwargs=dict(
fillable=fillable,
Expand Down Expand Up @@ -269,7 +277,7 @@ def layout_column_wrap(
**kwargs
Additional attributes to apply to the containing element.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.layout_column_wrap,
kwargs=dict(
width=width,
Expand Down Expand Up @@ -378,7 +386,7 @@ def layout_columns(
* [Bootstrap CSS Grid](https://getbootstrap.com/docs/5.3/layout/grid/)
* [Bootstrap Breakpoints](https://getbootstrap.com/docs/5.3/layout/breakpoints/)
"""
return RecallContextManager(
return UiRecallContextManager(
ui.layout_columns,
kwargs=dict(
col_widths=col_widths,
Expand Down Expand Up @@ -438,7 +446,7 @@ def card(
# card_body("c"), "d")`, `wrapper` would be called twice, once with `"a"` and
# `"b"` and once with `"d"`).

return RecallContextManager(
return UiRecallContextManager(
ui.card,
kwargs=dict(
full_screen=full_screen,
Expand Down Expand Up @@ -477,7 +485,7 @@ def card_header(
**kwargs
Additional HTML attributes for the returned Tag.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.card_header,
args=args,
kwargs=dict(
Expand Down Expand Up @@ -511,7 +519,7 @@ def card_footer(
Additional HTML attributes for the returned Tag.

"""
return RecallContextManager(
return UiRecallContextManager(
ui.card_footer,
args=args,
kwargs=kwargs,
Expand Down Expand Up @@ -558,6 +566,10 @@ def accordion(
**kwargs
Attributes to this tag.
"""

def _accordion_filter(x: object) -> bool:
return isinstance(x, (AccordionPanel, dict))

return RecallContextManager(
ui.accordion,
kwargs=dict(
Expand All @@ -569,6 +581,7 @@ def accordion(
height=height,
**kwargs,
),
filter=_accordion_filter,
)


Expand Down Expand Up @@ -596,7 +609,7 @@ def accordion_panel(
**kwargs
Tag attributes to the `accordion-body` div Tag.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.accordion_panel,
args=(title,),
kwargs=dict(
Expand Down Expand Up @@ -645,6 +658,7 @@ def navset_tab(
header=header,
footer=footer,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -681,6 +695,7 @@ def navset_pill(
header=header,
footer=footer,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -718,6 +733,7 @@ def navset_underline(
header=header,
footer=footer,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -754,6 +770,7 @@ def navset_hidden(
header=header,
footer=footer,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -796,6 +813,7 @@ def navset_card_tab(
header=header,
footer=footer,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -838,6 +856,7 @@ def navset_card_pill(
header=header,
footer=footer,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -884,6 +903,7 @@ def navset_card_underline(
footer=footer,
placement=placement,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -928,6 +948,7 @@ def navset_pill_list(
well=well,
widths=widths,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -1026,6 +1047,7 @@ def navset_bar(
collapsible=collapsible,
fluid=fluid,
),
filter=_navset_filter,
)


Expand Down Expand Up @@ -1053,7 +1075,7 @@ def nav_panel(
icon
An icon to appear inline with the button/link.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.nav_panel,
args=(title,),
kwargs=dict(
Expand All @@ -1069,7 +1091,7 @@ def nav_control() -> RecallContextManager[NavPanel]:

This function wraps :func:`~shiny.ui.nav_control`.
"""
return RecallContextManager(ui.nav_control)
return UiRecallContextManager(ui.nav_control)


def nav_menu(
Expand Down Expand Up @@ -1100,6 +1122,9 @@ def nav_menu(
Horizontal alignment of the dropdown menu relative to dropdown toggle.
"""

def _nav_menu_filter(x: object) -> TypeGuard[NavPanel | str]:
return isinstance(x, (NavPanel, str))

return RecallContextManager(
ui.nav_menu,
args=(title,),
Expand All @@ -1108,9 +1133,14 @@ def nav_menu(
icon=icon,
align=align,
),
filter=_nav_menu_filter,
)


def _navset_filter(x: object) -> TypeGuard[NavSet | MetadataNode]:
return isinstance(x, (NavSetArg, MetadataNode))


# ======================================================================================
# Value boxes
# ======================================================================================
Expand Down Expand Up @@ -1178,7 +1208,7 @@ def value_box(
**kwargs
Additional attributes to pass to :func:`~shiny.ui.card`.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.value_box,
kwargs=dict(
showcase=showcase,
Expand Down Expand Up @@ -1208,7 +1238,7 @@ def panel_well(**kwargs: TagAttrValue) -> RecallContextManager[Tag]:
A well panel is a simple container with a border and some padding. It's useful for
grouping related content together.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.panel_well,
kwargs=dict(
**kwargs,
Expand Down Expand Up @@ -1252,7 +1282,7 @@ def panel_conditional(
A more powerful (but slower) way to conditionally show UI content is to use
:class:`~shiny.render.ui`.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.panel_conditional,
args=(condition,),
kwargs=dict(**kwargs),
Expand Down Expand Up @@ -1289,7 +1319,7 @@ def panel_fixed(
--------
* :func:`~shiny.ui.panel_absolute`
"""
return RecallContextManager(
return UiRecallContextManager(
ui.panel_fixed,
kwargs=dict(
top=top,
Expand Down Expand Up @@ -1377,7 +1407,7 @@ def panel_absolute(
specify 0 for ``top``, ``left``, ``right``, and ``bottom`` rather than the more
obvious ``width = "100%"`` and ``height = "100%"``.
"""
return RecallContextManager(
return UiRecallContextManager(
ui.panel_absolute,
kwargs=dict(
top=top,
Expand Down Expand Up @@ -1426,7 +1456,7 @@ def tooltip(
options](https://getbootstrap.com/docs/5.3/components/tooltips/#options).
"""

return RecallContextManager(
return UiRecallContextManager(
ui.tooltip,
kwargs=dict(
id=id,
Expand Down Expand Up @@ -1469,7 +1499,7 @@ def popover(
options](https://getbootstrap.com/docs/5.3/components/popovers/#options).
"""

return RecallContextManager(
return UiRecallContextManager(
ui.popover,
kwargs=dict(
title=title,
Expand Down
Loading
Loading