diff --git a/shiny/__init__.py b/shiny/__init__.py index ae11bd126..4b00b42f2 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -1,6 +1,6 @@ """A package for building reactive web applications.""" -__version__ = "0.6.1.9004" +__version__ = "0.6.1.9005" from ._shinyenv import is_pyodide as _is_pyodide diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app.py index e1a8cb513..e4b0fe900 100644 --- a/shiny/api-examples/Renderer/app.py +++ b/shiny/api-examples/Renderer/app.py @@ -28,11 +28,11 @@ class render_capitalize(Renderer[str]): Whether to render a placeholder value. (Defaults to `True`) """ - def auto_output_ui(self, id: str): + def auto_output_ui(self): """ Express UI for the renderer """ - return ui.output_text_verbatim(id, placeholder=self.placeholder) + return ui.output_text_verbatim(self.output_name, placeholder=self.placeholder) def __init__( self, @@ -94,11 +94,11 @@ class render_upper(Renderer[str]): Note: This renderer is equivalent to `render_capitalize(to="upper")`. """ - def auto_output_ui(self, id: str): + def auto_output_ui(self): """ Express UI for the renderer """ - return ui.output_text_verbatim(id, placeholder=True) + return ui.output_text_verbatim(self.output_name, placeholder=True) async def transform(self, value: str) -> str: """ diff --git a/shiny/express/_output.py b/shiny/express/_output.py index ef614efd7..7fce0d9f0 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -120,8 +120,6 @@ def suspend_display_ctxmgr() -> Generator[None, None, None]: def null_ui( - id: str, - *args: object, **kwargs: object, ) -> ui.TagList: return ui.TagList() diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 6ecdc55d5..c6a42b993 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -256,8 +256,8 @@ class data_frame(Renderer[DataFrameResult]): objects you can return from the rendering function to specify options. """ - def auto_output_ui(self, id: str) -> Tag: - return ui.output_data_frame(id=id) + def auto_output_ui(self) -> Tag: + return ui.output_data_frame(id=self.output_id) async def transform(self, value: DataFrameResult) -> Jsonifiable: if not isinstance(value, AbstractTabularData): diff --git a/shiny/render/_display.py b/shiny/render/_display.py index da597a9f1..f71eb1109 100644 --- a/shiny/render/_display.py +++ b/shiny/render/_display.py @@ -20,7 +20,6 @@ class display(Renderer[None]): def auto_output_ui( self, - id: str, *, inline: bool | MISSING_TYPE = MISSING, container: TagFunction | MISSING_TYPE = MISSING, @@ -35,7 +34,7 @@ def auto_output_ui( set_kwargs_value(kwargs, "fillable", fillable, self.fillable) return _ui.output_ui( - id, + self.output_id, # (possibly) contains `inline`, `container`, `fill`, and `fillable` keys! **kwargs, # pyright: ignore[reportGeneralTypeIssues] ) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 8f0887f4e..54356da60 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -93,14 +93,13 @@ class text(Renderer[str]): def auto_output_ui( self, - id: str, *, inline: bool | MISSING_TYPE = MISSING, ) -> Tag: kwargs: dict[str, Any] = {} set_kwargs_value(kwargs, "inline", inline, self.inline) - return _ui.output_text(id, **kwargs) + return _ui.output_text(self.output_id, **kwargs) def __init__( self, @@ -155,13 +154,12 @@ class code(Renderer[str]): def auto_output_ui( self, - id: str, *, placeholder: bool | MISSING_TYPE = MISSING, ) -> Tag: kwargs: dict[str, bool] = {} set_kwargs_value(kwargs, "placeholder", placeholder, self.placeholder) - return _ui.output_code(id, **kwargs) + return _ui.output_code(self.output_id, **kwargs) def __init__( self, @@ -243,7 +241,6 @@ class plot(Renderer[object]): def auto_output_ui( self, - id: str, *, width: str | float | int | MISSING_TYPE = MISSING, height: str | float | int | MISSING_TYPE = MISSING, @@ -253,7 +250,7 @@ def auto_output_ui( set_kwargs_value(kwargs, "width", width, self.width) set_kwargs_value(kwargs, "height", height, self.height) return _ui.output_plot( - id, + self.output_id, # (possibly) contains `width` and `height` keys! **kwargs, # pyright: ignore[reportGeneralTypeIssues] ) @@ -413,9 +410,9 @@ class image(Renderer[ImgData]): * ~shiny.render.plot """ - def auto_output_ui(self, id: str, **kwargs: object): + def auto_output_ui(self, **kwargs: object): return _ui.output_image( - id, + self.output_id, **kwargs, # pyright: ignore[reportGeneralTypeIssues] ) # TODO: Make width/height handling consistent with render_plot @@ -506,8 +503,8 @@ class table(Renderer[TableResult]): * ~shiny.ui.output_table for the corresponding UI component to this render function. """ - def auto_output_ui(self, id: str, **kwargs: TagAttrValue) -> Tag: - return _ui.output_table(id, **kwargs) + def auto_output_ui(self, **kwargs: TagAttrValue) -> Tag: + return _ui.output_table(self.output_id, **kwargs) # TODO: Deal with kwargs def __init__( @@ -585,8 +582,8 @@ class ui(Renderer[TagChild]): * ~shiny.ui.output_ui """ - def auto_output_ui(self, id: str) -> Tag: - return _ui.output_ui(id) + def auto_output_ui(self) -> Tag: + return _ui.output_ui(self.output_id) async def transform(self, value: TagChild) -> Jsonifiable: session = require_active_session(None) @@ -623,9 +620,9 @@ class download(Renderer[str]): * ~shiny.ui.download_button """ - def auto_output_ui(self, id: str) -> Tag: + def auto_output_ui(self) -> Tag: return _ui.download_button( - id, + self.output_id, label=self.label, ) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 0cbf4f8c4..2e3b93ec4 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -35,6 +35,7 @@ "Jsonifiable", "ValueFn", "AsyncValueFn", + "RendererBaseT", ) RendererBaseT = TypeVar("RendererBaseT", bound="RendererBase") @@ -106,42 +107,46 @@ class RendererBase(ABC): # A: No. Even if we had a `P` in the Generic, the calling decorator would not have access to it. # Idea: Possibly use a chained method of `.ui_kwargs()`? https://github.com/posit-dev/py-shiny/issues/971 _auto_output_ui_kwargs: dict[str, Any] = dict() - # _auto_output_ui_args: tuple[Any, ...] = tuple() __name__: str """ Name of output function supplied. (The value will not contain any module prefix.) - Set within `.__call__()` method. + Set within `Renderer.__call__()` method. """ # Meta output_id: str """ - Output function name or ID (provided to `@output(id=)`). This value will contain any module prefix. + Output function name or ID (provided to `@output(id=)`). - Set when the output is registered with the session. + This value **will not** contain a module prefix (or session name-spacing). To get + the fully resolved ID, call + `shiny.session.require_active_session(None).ns(self.output_id)`. + + An initial value of `.__name__` (set within `Renderer.__call__(_fn)`) will be used until the + output renderer is registered within the session. """ def _set_output_metadata( self, *, - output_name: str, + output_id: str, ) -> None: """ Method to be called within `@output` to set the renderer's metadata. Parameters ---------- - output_name : str - Output function name or ID (provided to `@output(id=)`). This value will contain any module prefix. + output_id : str + Output function name or ID (provided to `@output(id=)`). This value **will + not** contain a module prefix (or session name-spacing). """ - self.output_id = output_name + self.output_id = output_id def auto_output_ui( self, - id: str, - # *args: object, + # * # **kwargs: object, ) -> DefaultUIFnResultOrNone: return None @@ -174,7 +179,6 @@ def tagify(self) -> DefaultUIFnResult: def _render_auto_output_ui(self) -> DefaultUIFnResultOrNone: return self.auto_output_ui( - self.__name__, # Pass the `@output_args(foo="bar")` kwargs through to the auto_output_ui function. **self._auto_output_ui_kwargs, ) @@ -206,11 +210,8 @@ def _auto_register(self) -> None: s = get_current_session() if s is not None: - from ._renderer import RendererBase - - # Cast to avoid circular import as this mixin is ONLY used within RendererBase - renderer_self = cast(RendererBase, self) - s.output(renderer_self) + # Register output on reactive graph + s.output(self) # We mark the fact that we're auto-registered so that, if an explicit # registration now occurs, we can undo this auto-registration. self._auto_registered = True @@ -309,16 +310,22 @@ def __call__(self, _fn: ValueFn[IT]) -> Self: if not callable(_fn): raise TypeError("Value function must be callable") + # Set value function with extra meta information + self.fn = AsyncValueFn(_fn) + # Copy over function name as it is consistent with how Session and Output # retrieve function names - self.__name__: str = _fn.__name__ + self.__name__ = _fn.__name__ - # Set value function with extra meta information - self.fn: AsyncValueFn[IT] = AsyncValueFn(_fn) + # Set the value of `output_id` to the function name. + # This helps with testing and other situations where no session is present + # for auto-registration to occur. + self.output_id = self.__name__ # Allow for App authors to not require `@output` self._auto_register() + # Return self for possible chaining of methods! return self def __init__( diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 03ae97af7..0f2d41364 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -256,10 +256,6 @@ def __init__( "`shiny.render.transformer.output_transformer()` and `shiny.render.transformer.OutputRenderer()` output render function utilities have been superseded by `shiny.render.renderer.Renderer` and will be removed in the near future." ) - # Copy over function name as it is consistent with how Session and Output - # retrieve function names - self.__name__ = value_fn.__name__ - if not is_async_callable(transform_fn): raise TypeError( self.__class__.__name__ @@ -278,7 +274,11 @@ def __init__( self._value_fn = value_fn self._value_fn_is_async = is_async_callable(value_fn) # legacy key + # Copy over function name as it is consistent with how Session and Output + # retrieve function names self.__name__ = value_fn.__name__ + # Initial value for output_id until it is set by the Session + self.output_id = value_fn.__name__ self._transformer = transform_fn self._params = params @@ -333,7 +333,6 @@ async def _run(self) -> OT: def auto_output_ui( self, - id: str, **kwargs: object, ) -> DefaultUIFnResultOrNone: if self._default_ui is None: @@ -348,7 +347,7 @@ def auto_output_ui( } ) - return self._default_ui(id, *self._default_ui_args, **kwargs) + return self._default_ui(self.output_id, *self._default_ui_args, **kwargs) async def render(self) -> Jsonifiable: ret = await self._run() diff --git a/shiny/session/_session.py b/shiny/session/_session.py index f2618aaa3..c8791f1c2 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -1002,10 +1002,11 @@ def set_renderer(renderer: RendererBaseT) -> RendererBaseT: ) # Get the (possibly namespaced) output id - output_name = self._ns(id or renderer.__name__) + output_id = id or renderer.__name__ + output_name = self._ns(output_id) # renderer is a Renderer object. Give it a bit of metadata. - renderer._set_output_metadata(output_name=output_name) + renderer._set_output_metadata(output_id=output_name) renderer._on_register() diff --git a/shiny/templates/package-templates/js-output/custom_component/custom_component.py b/shiny/templates/package-templates/js-output/custom_component/custom_component.py index 1bf568912..ec48352a4 100644 --- a/shiny/templates/package-templates/js-output/custom_component/custom_component.py +++ b/shiny/templates/package-templates/js-output/custom_component/custom_component.py @@ -27,8 +27,8 @@ class render_custom_component(Renderer[int]): """ # The UI used within Shiny Express mode - def auto_output_ui(self, id: str) -> Tag: - return custom_component(id, height=self.height) + def auto_output_ui(self) -> Tag: + return custom_component(self.output_id, height=self.height) # The init method is used to set up the renderer's parameters. # If no parameters are needed, then the `__init__()` method can be omitted. diff --git a/shiny/templates/package-templates/js-react/custom_component/custom_component.py b/shiny/templates/package-templates/js-react/custom_component/custom_component.py index f68f90a12..7dfd36ddc 100644 --- a/shiny/templates/package-templates/js-react/custom_component/custom_component.py +++ b/shiny/templates/package-templates/js-react/custom_component/custom_component.py @@ -39,8 +39,8 @@ class render_custom_component(Renderer[str]): """ # The UI used within Shiny Express mode - def auto_output_ui(self, id: str) -> Tag: - return output_custom_component(id) + def auto_output_ui(self) -> Tag: + return output_custom_component(self.output_id) # # There are no parameters being supplied to the `output_custom_component` rendering function. # # Therefore, we can omit the `__init__()` method. diff --git a/tests/pytest/test_output_transformer.py b/tests/pytest/test_output_transformer.py index 4033f2197..40d75de79 100644 --- a/tests/pytest/test_output_transformer.py +++ b/tests/pytest/test_output_transformer.py @@ -244,15 +244,12 @@ def async_renderer( test_val = "Test: Hello World!" - def app_render_fn() -> str: - return test_val - # ## Test Sync: X ============================================= - renderer_sync = async_renderer(app_render_fn) - renderer_sync._set_output_metadata( - output_name="renderer_sync", - ) + @async_renderer + def renderer_sync() -> str: + return test_val + # All renderers are async in execution. assert not is_async_callable(renderer_sync) @@ -264,14 +261,11 @@ def app_render_fn() -> str: async_test_val = "Async: Hello World!" - async def async_app_render_fn() -> str: + @async_renderer + async def renderer_async() -> str: await asyncio.sleep(0) return async_test_val - renderer_async = async_renderer(async_app_render_fn) - renderer_async._set_output_metadata( - output_name="renderer_async", - ) if not is_async_callable(renderer_async): raise RuntimeError("Expected `renderer_async` to be a coro function")