diff --git a/src/streamsync/core.py b/src/streamsync/core.py index 0b020babe..c09736724 100644 --- a/src/streamsync/core.py +++ b/src/streamsync/core.py @@ -17,7 +17,7 @@ import json import math from streamsync.ss_types import Readable, InstancePath, StreamsyncEvent, StreamsyncEventResult, StreamsyncFileItem -from streamsync.core_ui import ComponentTree, SessionComponentTree +from streamsync.core_ui import ComponentTree, SessionComponentTree, use_component_tree class Config: @@ -1216,14 +1216,15 @@ def _call_handler_callable(self, event_type, target_component, instance_path, pa arg_values.append(session_info) elif arg == "ui": from streamsync.ui import StreamsyncUIManager - ui_manager = StreamsyncUIManager(self.session.session_component_tree) + ui_manager = StreamsyncUIManager() arg_values.append(ui_manager) result = None - if is_async_handler: - result, captured_stdout = self._async_handler_executor(callable_handler, arg_values) - else: - result, captured_stdout = self._sync_handler_executor(callable_handler, arg_values) + with use_component_tree(self.session.session_component_tree): + if is_async_handler: + result, captured_stdout = self._async_handler_executor(callable_handler, arg_values) + else: + result, captured_stdout = self._sync_handler_executor(callable_handler, arg_values) if captured_stdout: self.session_state.add_log_entry( diff --git a/src/streamsync/core_ui.py b/src/streamsync/core_ui.py index 5e03d4e31..5671aed48 100644 --- a/src/streamsync/core_ui.py +++ b/src/streamsync/core_ui.py @@ -1,11 +1,13 @@ +import contextlib from contextvars import ContextVar from typing import Any, Dict, List, Optional, Union import uuid from pydantic import BaseModel, Field -current_parent_container: ContextVar[Union["Component", None]] = \ - ContextVar("current_parent_container") + +current_parent_container: ContextVar[Union["Component", None]] = ContextVar("current_parent_container") +_current_component_tree: ContextVar[Union["ComponentTree", None]] = ContextVar("current_component_tree", default=None) # This variable is thread safe and context safe @@ -161,3 +163,35 @@ def fetch_updates(self): class UIError(Exception): ... + +@contextlib.contextmanager +def use_component_tree(component_tree: ComponentTree): + """ + Declares the component tree that will be manipulated during a context. + + The declared tree can be retrieved with the `current_component_tree` method. + + >>> with use_component_tree(component_tree): + >>> ui_manager = StreamsyncUIManager() + >>> ui_manager.create_component("text", text="Hello, world!") + + :param component_tree: + """ + token = _current_component_tree.set(component_tree) + yield + _current_component_tree.reset(token) + + +def current_component_tree() -> ComponentTree: + """ + Retrieves the component tree of the current context or the base + one if no context has been declared. + + :return: + """ + tree = _current_component_tree.get() + if tree is None: + from streamsync.core import base_component_tree + return base_component_tree + + return tree diff --git a/src/streamsync/ui.py b/src/streamsync/ui.py index f69e26a6a..3d770c7a1 100644 --- a/src/streamsync/ui.py +++ b/src/streamsync/ui.py @@ -737,10 +737,11 @@ class StreamsyncUIManager(StreamsyncUI): frontend, allowing methods to adapt to changes in the UI components without manual updates. """ - + # Hardcoded classes for proof-of-concept purposes - def Root(self, + @staticmethod + def Root( content: RootProps = {}, *, id: Optional[str] = None, @@ -752,7 +753,7 @@ def Root(self, """ The root component of the application, which serves as the starting point of the component hierarchy. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'root', content=content, id=id, @@ -762,7 +763,8 @@ def Root(self, visible=visible) return component - def Page(self, + @staticmethod + def Page( content: PageProps = {}, *, id: Optional[str] = None, @@ -774,7 +776,7 @@ def Page(self, """ A container component representing a single page within the application. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'page', content=content, id=id, @@ -784,7 +786,8 @@ def Page(self, visible=visible) return component - def Sidebar(self, + @staticmethod + def Sidebar( content: SidebarProps = {}, *, id: Optional[str] = None, @@ -796,7 +799,7 @@ def Sidebar(self, """ A container component that organises its children in a sidebar. Its parent must be a Page component. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'sidebar', content=content, id=id, @@ -806,7 +809,8 @@ def Sidebar(self, visible=visible) return component - def Button(self, + @staticmethod + def Button( content: ButtonProps = {}, *, id: Optional[str] = None, @@ -818,7 +822,7 @@ def Button(self, """ A standalone button component that can be linked to a click event handler. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'button', content=content, id=id, @@ -828,7 +832,8 @@ def Button(self, visible=visible) return component - def Text(self, + @staticmethod + def Text( content: TextProps = {}, *, id: Optional[str] = None, @@ -840,7 +845,7 @@ def Text(self, """ A component to display plain text or formatted text using Markdown syntax. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'text', content=content, id=id, @@ -850,7 +855,8 @@ def Text(self, visible=visible) return component - def Section(self, + @staticmethod + def Section( content: SectionProps = {}, *, id: Optional[str] = None, @@ -862,7 +868,7 @@ def Section(self, """ A container component that divides the layout into sections, with an optional title. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'section', content=content, id=id, @@ -872,7 +878,8 @@ def Section(self, visible=visible) return component - def Header(self, + @staticmethod + def Header( content: HeaderProps = {}, *, id: Optional[str] = None, @@ -884,7 +891,7 @@ def Header(self, """ A container component that typically contains the main navigation elements. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'header', content=content, id=id, @@ -894,7 +901,8 @@ def Header(self, visible=visible) return component - def Heading(self, + @staticmethod + def Heading( content: HeadingProps = {}, *, id: Optional[str] = None, @@ -906,7 +914,7 @@ def Heading(self, """ A text component used to display headings or titles in different sizes and styles. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'heading', content=content, id=id, @@ -916,7 +924,8 @@ def Heading(self, visible=visible) return component - def DataFrame(self, + @staticmethod + def DataFrame( content: DataFrameProps = {}, *, id: Optional[str] = None, @@ -928,7 +937,7 @@ def DataFrame(self, """ A component to display Pandas DataFrames. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'dataframe', content=content, id=id, @@ -938,7 +947,8 @@ def DataFrame(self, visible=visible) return component - def HTMLElement(self, + @staticmethod + def HTMLElement( content: HTMLElementProps = {}, *, id: Optional[str] = None, @@ -950,7 +960,7 @@ def HTMLElement(self, """ A generic component that creates customisable HTML elements, which can serve as containers for other components. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'html', content=content, id=id, @@ -960,7 +970,8 @@ def HTMLElement(self, visible=visible) return component - def Pagination(self, + @staticmethod + def Pagination( content: PaginationProps = {}, *, id: Optional[str] = None, @@ -972,7 +983,7 @@ def Pagination(self, """ A component that can help you paginate records, for example from a Repeater or a DataFrame. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'pagination', content=content, id=id, @@ -982,7 +993,8 @@ def Pagination(self, visible=visible) return component - def Repeater(self, + @staticmethod + def Repeater( content: RepeaterProps = {}, *, id: Optional[str] = None, @@ -994,7 +1006,7 @@ def Repeater(self, """ A container component that repeats its child components based on a dictionary. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'repeater', content=content, id=id, @@ -1004,7 +1016,8 @@ def Repeater(self, visible=visible) return component - def Column(self, + @staticmethod + def Column( content: ColumnProps = {}, *, id: Optional[str] = None, @@ -1016,7 +1029,7 @@ def Column(self, """ A layout component that organises its child components in columns. Must be inside a Column Container component. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'column', content=content, id=id, @@ -1026,7 +1039,8 @@ def Column(self, visible=visible) return component - def ColumnContainer(self, + @staticmethod + def ColumnContainer( content: ColumnContainerProps = {}, *, id: Optional[str] = None, @@ -1038,7 +1052,7 @@ def ColumnContainer(self, """ Serves as container for Column components """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'columns', content=content, id=id, @@ -1048,7 +1062,8 @@ def ColumnContainer(self, visible=visible) return component - def Tab(self, + @staticmethod + def Tab( content: TabProps = {}, *, id: Optional[str] = None, @@ -1060,7 +1075,7 @@ def Tab(self, """ A container component that displays its child components as a tab inside a Tab Container. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'tab', content=content, id=id, @@ -1070,7 +1085,8 @@ def Tab(self, visible=visible) return component - def TabContainer(self, + @staticmethod + def TabContainer( content: TabContainerProps = {}, *, id: Optional[str] = None, @@ -1082,7 +1098,7 @@ def TabContainer(self, """ A container component for organising and displaying Tab components in a tabbed interface. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'tabs', content=content, id=id, @@ -1092,7 +1108,8 @@ def TabContainer(self, visible=visible) return component - def Link(self, + @staticmethod + def Link( content: LinkProps = {}, *, id: Optional[str] = None, @@ -1104,7 +1121,7 @@ def Link(self, """ A component to create a hyperlink. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'link', content=content, id=id, @@ -1114,7 +1131,8 @@ def Link(self, visible=visible) return component - def HorizontalStack(self, + @staticmethod + def HorizontalStack( content: HorizontalStackProps = {}, *, id: Optional[str] = None, @@ -1126,7 +1144,7 @@ def HorizontalStack(self, """ A layout component that stacks its child components horizontally, wrapping them to the next row if necessary. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'horizontalstack', content=content, id=id, @@ -1136,7 +1154,8 @@ def HorizontalStack(self, visible=visible) return component - def Separator(self, + @staticmethod + def Separator( content: SeparatorProps = {}, *, id: Optional[str] = None, @@ -1148,7 +1167,7 @@ def Separator(self, """ A visual component to create a separation between adjacent elements. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'separator', content=content, id=id, @@ -1158,7 +1177,8 @@ def Separator(self, visible=visible) return component - def Image(self, + @staticmethod + def Image( content: ImageProps = {}, *, id: Optional[str] = None, @@ -1170,7 +1190,7 @@ def Image(self, """ A component to display images. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'image', content=content, id=id, @@ -1180,7 +1200,8 @@ def Image(self, visible=visible) return component - def PDF(self, + @staticmethod + def PDF( content: PDFProps = {}, *, id: Optional[str] = None, @@ -1192,7 +1213,7 @@ def PDF(self, """ A component to embed PDF documents. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'pdf', content=content, id=id, @@ -1202,7 +1223,8 @@ def PDF(self, visible=visible) return component - def IFrame(self, + @staticmethod + def IFrame( content: IFrameProps = {}, *, id: Optional[str] = None, @@ -1214,7 +1236,7 @@ def IFrame(self, """ A component to embed an external resource in an iframe. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'iframe', content=content, id=id, @@ -1224,7 +1246,8 @@ def IFrame(self, visible=visible) return component - def GoogleMaps(self, + @staticmethod + def GoogleMaps( content: GoogleMapsProps = {}, *, id: Optional[str] = None, @@ -1236,7 +1259,7 @@ def GoogleMaps(self, """ A component to embed a Google Map. It can be used to display a map with markers. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'googlemaps', content=content, id=id, @@ -1246,7 +1269,8 @@ def GoogleMaps(self, visible=visible) return component - def Mapbox(self, + @staticmethod + def Mapbox( content: MapboxProps = {}, *, id: Optional[str] = None, @@ -1258,7 +1282,7 @@ def Mapbox(self, """ A component to embed a Mapbox map. It can be used to display a map with markers. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'mapbox', content=content, id=id, @@ -1268,7 +1292,8 @@ def Mapbox(self, visible=visible) return component - def Icon(self, + @staticmethod + def Icon( content: IconProps = {}, *, id: Optional[str] = None, @@ -1280,7 +1305,7 @@ def Icon(self, """ A component to display an icon """ - component = self.create_component( + component = StreamsyncUI.create_component( 'icon', content=content, id=id, @@ -1290,7 +1315,8 @@ def Icon(self, visible=visible) return component - def Timer(self, + @staticmethod + def Timer( content: TimerProps = {}, *, id: Optional[str] = None, @@ -1302,7 +1328,7 @@ def Timer(self, """ A component that emits an event repeatedly at specified time intervals, enabling time-based refresh. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'timer', content=content, id=id, @@ -1312,7 +1338,8 @@ def Timer(self, visible=visible) return component - def TextInput(self, + @staticmethod + def TextInput( content: TextInputProps = {}, *, id: Optional[str] = None, @@ -1325,7 +1352,7 @@ def TextInput(self, """ A user input component that allows users to enter single-line text values. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'textinput', content=content, id=id, @@ -1336,7 +1363,8 @@ def TextInput(self, binding=binding) return component - def TextareaInput(self, + @staticmethod + def TextareaInput( content: TextareaInputProps = {}, *, id: Optional[str] = None, @@ -1349,7 +1377,7 @@ def TextareaInput(self, """ A user input component that allows users to enter multi-line text values. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'textareainput', content=content, id=id, @@ -1360,7 +1388,8 @@ def TextareaInput(self, binding=binding) return component - def NumberInput(self, + @staticmethod + def NumberInput( content: NumberInputProps = {}, *, id: Optional[str] = None, @@ -1373,7 +1402,7 @@ def NumberInput(self, """ A user input component that allows users to enter numeric values. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'numberinput', content=content, id=id, @@ -1384,7 +1413,8 @@ def NumberInput(self, binding=binding) return component - def SliderInput(self, + @staticmethod + def SliderInput( content: SliderInputProps = {}, *, id: Optional[str] = None, @@ -1397,7 +1427,7 @@ def SliderInput(self, """ A user input component that allows users to select numeric values using a slider with optional constraints like min, max, and step. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'sliderinput', content=content, id=id, @@ -1408,7 +1438,8 @@ def SliderInput(self, binding=binding) return component - def DateInput(self, + @staticmethod + def DateInput( content: DateInputProps = {}, *, id: Optional[str] = None, @@ -1421,7 +1452,7 @@ def DateInput(self, """ A user input component that allows users to select a date using a date picker interface. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'dateinput', content=content, id=id, @@ -1432,7 +1463,8 @@ def DateInput(self, binding=binding) return component - def RadioInput(self, + @staticmethod + def RadioInput( content: RadioInputProps = {}, *, id: Optional[str] = None, @@ -1445,7 +1477,7 @@ def RadioInput(self, """ A user input component that allows users to choose a single value from a list of options using radio buttons. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'radioinput', content=content, id=id, @@ -1456,7 +1488,8 @@ def RadioInput(self, binding=binding) return component - def CheckboxInput(self, + @staticmethod + def CheckboxInput( content: CheckboxInputProps = {}, *, id: Optional[str] = None, @@ -1469,7 +1502,7 @@ def CheckboxInput(self, """ A user input component that allows users to choose multiple values from a list of options using checkboxes. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'checkboxinput', content=content, id=id, @@ -1480,7 +1513,8 @@ def CheckboxInput(self, binding=binding) return component - def DropdownInput(self, + @staticmethod + def DropdownInput( content: DropdownInputProps = {}, *, id: Optional[str] = None, @@ -1493,7 +1527,7 @@ def DropdownInput(self, """ A user input component that allows users to select a single value from a list of options using a dropdown menu. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'dropdowninput', content=content, id=id, @@ -1504,7 +1538,8 @@ def DropdownInput(self, binding=binding) return component - def SelectInput(self, + @staticmethod + def SelectInput( content: SelectInputProps = {}, *, id: Optional[str] = None, @@ -1517,7 +1552,7 @@ def SelectInput(self, """ A user input component that allows users to select a single value from a searchable list of options. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'selectinput', content=content, id=id, @@ -1528,7 +1563,8 @@ def SelectInput(self, binding=binding) return component - def MultiselectInput(self, + @staticmethod + def MultiselectInput( content: MultiselectInputProps = {}, *, id: Optional[str] = None, @@ -1541,7 +1577,7 @@ def MultiselectInput(self, """ A user input component that allows users to select multiple values from a searchable list of options. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'multiselectinput', content=content, id=id, @@ -1552,7 +1588,8 @@ def MultiselectInput(self, binding=binding) return component - def FileInput(self, + @staticmethod + def FileInput( content: FileInputProps = {}, *, id: Optional[str] = None, @@ -1565,7 +1602,7 @@ def FileInput(self, """ A user input component that allows users to upload files. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'fileinput', content=content, id=id, @@ -1576,7 +1613,8 @@ def FileInput(self, binding=binding) return component - def WebcamCapture(self, + @staticmethod + def WebcamCapture( content: WebcamCaptureProps = {}, *, id: Optional[str] = None, @@ -1588,7 +1626,7 @@ def WebcamCapture(self, """ A user input component that allows users to capture images using their webcam. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'webcamcapture', content=content, id=id, @@ -1598,7 +1636,8 @@ def WebcamCapture(self, visible=visible) return component - def VegaLiteChart(self, + @staticmethod + def VegaLiteChart( content: VegaLiteChartProps = {}, *, id: Optional[str] = None, @@ -1610,7 +1649,7 @@ def VegaLiteChart(self, """ A component that displays Vega-Lite/Altair charts. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'vegalitechart', content=content, id=id, @@ -1620,7 +1659,8 @@ def VegaLiteChart(self, visible=visible) return component - def PlotlyGraph(self, + @staticmethod + def PlotlyGraph( content: PlotlyGraphProps = {}, *, id: Optional[str] = None, @@ -1632,7 +1672,7 @@ def PlotlyGraph(self, """ A component that displays Plotly graphs. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'plotlygraph', content=content, id=id, @@ -1642,7 +1682,8 @@ def PlotlyGraph(self, visible=visible) return component - def Metric(self, + @staticmethod + def Metric( content: MetricProps = {}, *, id: Optional[str] = None, @@ -1654,7 +1695,7 @@ def Metric(self, """ A component that prominently displays a metric value and associated information. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'metric', content=content, id=id, @@ -1664,7 +1705,8 @@ def Metric(self, visible=visible) return component - def Message(self, + @staticmethod + def Message( content: MessageProps = {}, *, id: Optional[str] = None, @@ -1676,7 +1718,7 @@ def Message(self, """ A component that displays a message in various styles, including success, error, warning, and informational. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'message', content=content, id=id, @@ -1686,7 +1728,8 @@ def Message(self, visible=visible) return component - def VideoPlayer(self, + @staticmethod + def VideoPlayer( content: VideoPlayerProps = {}, *, id: Optional[str] = None, @@ -1698,7 +1741,7 @@ def VideoPlayer(self, """ A video player component that can play various video formats. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'videoplayer', content=content, id=id, @@ -1708,7 +1751,8 @@ def VideoPlayer(self, visible=visible) return component - def Chat(self, + @staticmethod + def Chat( content: ChatProps = {}, *, id: Optional[str] = None, @@ -1720,7 +1764,7 @@ def Chat(self, """ A chat component to build human-to-AI interactions. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'chat', content=content, id=id, @@ -1730,7 +1774,8 @@ def Chat(self, visible=visible) return component - def Step(self, + @staticmethod + def Step( content: StepProps = {}, *, id: Optional[str] = None, @@ -1742,7 +1787,7 @@ def Step(self, """ A container component that displays its child components as a step inside a Step Container. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'step', content=content, id=id, @@ -1752,7 +1797,8 @@ def Step(self, visible=visible) return component - def StepContainer(self, + @staticmethod + def StepContainer( content: StepContainerProps = {}, *, id: Optional[str] = None, @@ -1764,7 +1810,7 @@ def StepContainer(self, """ A container component for displaying Step components, allowing you to implement a stepped workflow. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'steps', content=content, id=id, @@ -1774,7 +1820,8 @@ def StepContainer(self, visible=visible) return component - def RatingInput(self, + @staticmethod + def RatingInput( content: RatingInputProps = {}, *, id: Optional[str] = None, @@ -1787,7 +1834,7 @@ def RatingInput(self, """ A user input component that allows users to provide a rating. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'ratinginput', content=content, id=id, @@ -1798,7 +1845,8 @@ def RatingInput(self, binding=binding) return component - def Tags(self, + @staticmethod + def Tags( content: TagsProps = {}, *, id: Optional[str] = None, @@ -1810,7 +1858,7 @@ def Tags(self, """ A component to display coloured tag pills. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'tags', content=content, id=id, @@ -1820,7 +1868,8 @@ def Tags(self, visible=visible) return component - def SwitchInput(self, + @staticmethod + def SwitchInput( content: SwitchInputProps = {}, *, id: Optional[str] = None, @@ -1833,7 +1882,7 @@ def SwitchInput(self, """ A user input component with a simple on/off status. """ - component = self.create_component( + component = StreamsyncUI.create_component( 'switchinput', content=content, id=id, @@ -1844,7 +1893,8 @@ def SwitchInput(self, binding=binding) return component - def Avatar(self, + @staticmethod + def Avatar( content: AvatarProps = {}, *, id: Optional[str] = None, @@ -1856,7 +1906,7 @@ def Avatar(self, """ A component to display user avatars. """ - component = self.create_container_component( + component = StreamsyncUI.create_container_component( 'avatar', content=content, id=id, diff --git a/src/streamsync/ui_manager.py b/src/streamsync/ui_manager.py index 0b68f67e5..8ed13ddd7 100644 --- a/src/streamsync/ui_manager.py +++ b/src/streamsync/ui_manager.py @@ -1,9 +1,8 @@ from json import dumps as json_dumps -from typing import Optional, Union +from typing import Optional -from streamsync.core import base_component_tree -from streamsync.core_ui import (Component, SessionComponentTree, UIError, - current_parent_container) +from streamsync.core_ui import (Component, UIError, + current_parent_container, current_component_tree, ComponentTree) class StreamsyncUI: @@ -13,11 +12,6 @@ class StreamsyncUI: This class offers context managers and methods to dynamically create, find, and organize UI components based on a structured component tree. """ - - def __init__(self, component_tree: Union[SessionComponentTree, None] = None): - self.component_tree = component_tree or base_component_tree - self.root_component = self.component_tree.get_component('root') - def __enter__(self): return self @@ -30,13 +24,25 @@ def assert_in_container(): if container is None: raise UIError("A component can only be created inside a container") + @property + def component_tree(self) -> ComponentTree: + """ + Returns the component tree representation + + :return: + """ + return current_component_tree() + @property def root(self) -> Component: - if not self.root_component: + tree = current_component_tree() + root_component = tree.get_component('root') + if not root_component: raise RuntimeError("Failed to acquire root component") - return self.root_component + return root_component - def find(self, component_id: str) \ + @staticmethod + def find(component_id: str) \ -> Component: """ Retrieves a component by its ID from the current session's component tree. @@ -57,100 +63,107 @@ def find(self, component_id: str) \ >>> my_component = ui.find("my-component-id") >>> print(my_component.properties) """ - component = self.component_tree.get_component(component_id) + # Example context manager for finding components + component = current_component_tree().get_component(component_id) if component is None: raise RuntimeError(f"Component {component_id} not found") return component - def _prepare_handlers(self, raw_handlers: Optional[dict]): - handlers = {} - if raw_handlers is not None: - for event, handler in raw_handlers.items(): - if callable(handler): - handlers[event] = handler.__name__ - else: - handlers[event] = handler - return handlers - - def _prepare_binding(self, raw_binding: Optional[dict]): - if raw_binding is not None: - if len(raw_binding) == 1: - binding = { - "eventType": list(raw_binding.keys())[0], - "stateRef": list(raw_binding.values())[0] - } - return binding - elif len(raw_binding) != 0: - raise RuntimeError('Improper binding configuration') - - def _prepare_value(self, value): - if isinstance(value, dict): - return json_dumps(value) - return str(value) - - def _create_component( - self, - component_type: str, - **kwargs) -> Component: - parent_container = current_parent_container.get(None) - if kwargs.get("id", False) is None: - kwargs.pop("id") - - if kwargs.get("position", False) is None: - kwargs.pop("position") - - if kwargs.get("parentId", False) is None: - kwargs.pop("parentId") - - if "parentId" in kwargs: - parent_id: str = kwargs.pop("parentId") - else: - parent_id = "root" if not parent_container else parent_container.id - - # Converting all passed content values to strings - raw_content: dict = kwargs.pop("content", {}) - content = {key: self._prepare_value(value) for key, value in raw_content.items()} - - position: Optional[int] = kwargs.pop("position", None) - is_positionless: bool = kwargs.pop("positionless", False) - raw_handlers: dict = kwargs.pop("handlers", {}) - raw_binding: dict = kwargs.pop("binding", {}) - - handlers = self._prepare_handlers(raw_handlers) or None - binding = self._prepare_binding(raw_binding) or None - - component = Component( - type=component_type, - parentId=parent_id, - flag="cmc", - content=content, - handlers=handlers, - binding=binding, - **kwargs - ) - - # We're determining the position separately - # due to that we need to know whether ID of the component - # is present within base component tree - # or a session-specific one - component.position = \ - position if position is not None else \ - self.component_tree.determine_position( - component.id, - parent_id, - is_positionless=is_positionless - ) - - self.component_tree.attach(component) - return component + @staticmethod + def create_container_component(component_type: str, **kwargs) -> Component: + component_tree = current_component_tree() + container = _create_component(component_tree, component_type, **kwargs) + component_tree.attach(container) - def create_container_component(self, component_type: str, **kwargs) \ - -> Component: - container = self._create_component(component_type, **kwargs) return container - def create_component(self, component_type: str, **kwargs) \ - -> Component: - self.assert_in_container() - component = self._create_component(component_type, **kwargs) + @staticmethod + def create_component(component_type: str, **kwargs) -> Component: + StreamsyncUI.assert_in_container() + component_tree = current_component_tree() + component = _create_component(component_tree, component_type, **kwargs) + component_tree.attach(component) + return component + + +def _prepare_handlers(raw_handlers: Optional[dict]): + handlers = {} + if raw_handlers is not None: + for event, handler in raw_handlers.items(): + if callable(handler): + handlers[event] = handler.__name__ + else: + handlers[event] = handler + return handlers + +def _prepare_binding(raw_binding): + if raw_binding is not None: + if len(raw_binding) == 1: + binding = { + "eventType": list(raw_binding.keys())[0], + "stateRef": list(raw_binding.values())[0] + } + return binding + elif len(raw_binding) != 0: + raise RuntimeError('Improper binding configuration') + + +def _prepare_value(value): + if isinstance(value, dict): + return json_dumps(value) + return str(value) + + +def _create_component(component_tree: ComponentTree, component_type: str, **kwargs) -> Component: + + parent_container = current_parent_container.get(None) + if kwargs.get("id", False) is None: + kwargs.pop("id") + + if kwargs.get("position", False) is None: + kwargs.pop("position") + + if kwargs.get("parentId", False) is None: + kwargs.pop("parentId") + + if "parentId" in kwargs: + parent_id: str = kwargs.pop("parentId") + else: + parent_id = "root" if not parent_container else parent_container.id + + # Converting all passed content values to strings + raw_content: dict = kwargs.pop("content", {}) + content = {key: _prepare_value(value) for key, value in raw_content.items()} + + position: Optional[int] = kwargs.pop("position", None) + is_positionless: bool = kwargs.pop("positionless", False) + raw_handlers: dict = kwargs.pop("handlers", {}) + raw_binding: dict = kwargs.pop("binding", {}) + + handlers = _prepare_handlers(raw_handlers) or None + binding = _prepare_binding(raw_binding) or None + + component = Component( + type=component_type, + parentId=parent_id, + flag="cmc", + content=content, + handlers=handlers, + binding=binding, + **kwargs + ) + + # We're determining the position separately + # due to that we need to know whether ID of the component + # is present within base component tree + # or a session-specific one + component.position = \ + position if position is not None else \ + component_tree.determine_position( + component.id, + parent_id, + is_positionless=is_positionless + ) + + return component diff --git a/tests/test_ui.py b/tests/test_ui.py index 53d73cf50..dce18be0f 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -1,5 +1,5 @@ import contextlib -from streamsync.core_ui import ComponentTree, UIError +from streamsync.core_ui import ComponentTree, UIError, use_component_tree from streamsync.ui import StreamsyncUIManager import streamsync as ss @@ -22,7 +22,8 @@ def use_new_ss_session(): @contextlib.contextmanager def use_ui_manager(): with use_new_ss_session() as session: - yield StreamsyncUIManager(session.session_component_tree) + with use_component_tree(session.session_component_tree): + yield StreamsyncUIManager() class TestComponentTree: diff --git a/ui/tools/generator.js b/ui/tools/generator.js index 96ab93404..c885ca4c2 100644 --- a/ui/tools/generator.js +++ b/ui/tools/generator.js @@ -103,7 +103,7 @@ class StreamsyncUIManager(StreamsyncUI): frontend, allowing methods to adapt to changes in the UI components without manual updates. """ - + # Hardcoded classes for proof-of-concept purposes `; } @@ -120,7 +120,8 @@ function generateMethods(data) { const bindPass = `, binding=binding`; return ` - def ${component.nameTrim}(self, + @staticmethod + def ${component.nameTrim}( content: ${component.nameTrim}Props = {}, *, id: Optional[str] = None, @@ -132,7 +133,7 @@ function generateMethods(data) { """ ${component.description} """ - component = self.${component.allowedChildrenTypes?.length ? "create_container_component" : "create_component"}( + component = StreamsyncUI.${component.allowedChildrenTypes?.length ? "create_container_component" : "create_component"}( '${component.type}', content=content, id=id,