|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
|
|
|
import copy
|
|
|
import dataclasses
|
|
|
+import inspect
|
|
|
import typing
|
|
|
from abc import ABC, abstractmethod
|
|
|
from functools import lru_cache, wraps
|
|
@@ -21,6 +22,8 @@ from typing import (
|
|
|
Set,
|
|
|
Type,
|
|
|
Union,
|
|
|
+ get_args,
|
|
|
+ get_origin,
|
|
|
)
|
|
|
|
|
|
from typing_extensions import Self
|
|
@@ -43,6 +46,7 @@ from reflex.constants import (
|
|
|
from reflex.constants.compiler import SpecialAttributes
|
|
|
from reflex.constants.state import FRONTEND_EVENT_STATE
|
|
|
from reflex.event import (
|
|
|
+ EventActionsMixin,
|
|
|
EventCallback,
|
|
|
EventChain,
|
|
|
EventHandler,
|
|
@@ -191,6 +195,25 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
|
|
|
return types._isinstance(obj, type_hint, nested=1)
|
|
|
|
|
|
|
|
|
+def _components_from(
|
|
|
+ component_or_var: Union[BaseComponent, Var],
|
|
|
+) -> tuple[BaseComponent, ...]:
|
|
|
+ """Get the components from a component or Var.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ component_or_var: The component or Var to get the components from.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ The components.
|
|
|
+ """
|
|
|
+ if isinstance(component_or_var, Var):
|
|
|
+ var_data = component_or_var._get_all_var_data()
|
|
|
+ return var_data.components if var_data else ()
|
|
|
+ if isinstance(component_or_var, BaseComponent):
|
|
|
+ return (component_or_var,)
|
|
|
+ return ()
|
|
|
+
|
|
|
+
|
|
|
class Component(BaseComponent, ABC):
|
|
|
"""A component with style, event trigger and other props."""
|
|
|
|
|
@@ -489,7 +512,7 @@ class Component(BaseComponent, ABC):
|
|
|
|
|
|
# Remove any keys that were added as events.
|
|
|
for key in kwargs["event_triggers"]:
|
|
|
- del kwargs[key]
|
|
|
+ kwargs.pop(key, None)
|
|
|
|
|
|
# Place data_ and aria_ attributes into custom_attrs
|
|
|
special_attributes = tuple(
|
|
@@ -665,13 +688,22 @@ class Component(BaseComponent, ABC):
|
|
|
"""
|
|
|
return set()
|
|
|
|
|
|
+ @classmethod
|
|
|
+ def _are_fields_known(cls) -> bool:
|
|
|
+ """Check if all fields are known at compile time. True for most components.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ Whether all fields are known at compile time.
|
|
|
+ """
|
|
|
+ return True
|
|
|
+
|
|
|
@classmethod
|
|
|
@lru_cache(maxsize=None)
|
|
|
- def get_component_props(cls) -> set[str]:
|
|
|
- """Get the props that expected a component as value.
|
|
|
+ def _get_component_prop_names(cls) -> Set[str]:
|
|
|
+ """Get the names of the component props. NOTE: This assumes all fields are known.
|
|
|
|
|
|
Returns:
|
|
|
- The components props.
|
|
|
+ The names of the component props.
|
|
|
"""
|
|
|
return {
|
|
|
name
|
|
@@ -680,6 +712,26 @@ class Component(BaseComponent, ABC):
|
|
|
and types._issubclass(field.outer_type_, Component)
|
|
|
}
|
|
|
|
|
|
+ def _get_components_in_props(self) -> Sequence[BaseComponent]:
|
|
|
+ """Get the components in the props.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ The components in the props
|
|
|
+ """
|
|
|
+ if self._are_fields_known():
|
|
|
+ return [
|
|
|
+ component
|
|
|
+ for name in self._get_component_prop_names()
|
|
|
+ for component in _components_from(getattr(self, name))
|
|
|
+ ]
|
|
|
+ return [
|
|
|
+ component
|
|
|
+ for prop in self.get_props()
|
|
|
+ if (value := getattr(self, prop)) is not None
|
|
|
+ and isinstance(value, (BaseComponent, Var))
|
|
|
+ for component in _components_from(value)
|
|
|
+ ]
|
|
|
+
|
|
|
@classmethod
|
|
|
def create(cls, *children, **props) -> Self:
|
|
|
"""Create the component.
|
|
@@ -1136,6 +1188,9 @@ class Component(BaseComponent, ABC):
|
|
|
if custom_code is not None:
|
|
|
code.add(custom_code)
|
|
|
|
|
|
+ for component in self._get_components_in_props():
|
|
|
+ code |= component._get_all_custom_code()
|
|
|
+
|
|
|
# Add the custom code from add_custom_code method.
|
|
|
for clz in self._iter_parent_classes_with_method("add_custom_code"):
|
|
|
for item in clz.add_custom_code(self):
|
|
@@ -1163,7 +1218,7 @@ class Component(BaseComponent, ABC):
|
|
|
The dynamic imports.
|
|
|
"""
|
|
|
# Store the import in a set to avoid duplicates.
|
|
|
- dynamic_imports = set()
|
|
|
+ dynamic_imports: set[str] = set()
|
|
|
|
|
|
# Get dynamic import for this component.
|
|
|
dynamic_import = self._get_dynamic_imports()
|
|
@@ -1174,25 +1229,12 @@ class Component(BaseComponent, ABC):
|
|
|
for child in self.children:
|
|
|
dynamic_imports |= child._get_all_dynamic_imports()
|
|
|
|
|
|
- for prop in self.get_component_props():
|
|
|
- if getattr(self, prop) is not None:
|
|
|
- dynamic_imports |= getattr(self, prop)._get_all_dynamic_imports()
|
|
|
+ for component in self._get_components_in_props():
|
|
|
+ dynamic_imports |= component._get_all_dynamic_imports()
|
|
|
|
|
|
# Return the dynamic imports
|
|
|
return dynamic_imports
|
|
|
|
|
|
- def _get_props_imports(self) -> List[ParsedImportDict]:
|
|
|
- """Get the imports needed for components props.
|
|
|
-
|
|
|
- Returns:
|
|
|
- The imports for the components props of the component.
|
|
|
- """
|
|
|
- return [
|
|
|
- getattr(self, prop)._get_all_imports()
|
|
|
- for prop in self.get_component_props()
|
|
|
- if getattr(self, prop) is not None
|
|
|
- ]
|
|
|
-
|
|
|
def _should_transpile(self, dep: str | None) -> bool:
|
|
|
"""Check if a dependency should be transpiled.
|
|
|
|
|
@@ -1303,7 +1345,6 @@ class Component(BaseComponent, ABC):
|
|
|
)
|
|
|
|
|
|
return imports.merge_imports(
|
|
|
- *self._get_props_imports(),
|
|
|
self._get_dependencies_imports(),
|
|
|
self._get_hooks_imports(),
|
|
|
_imports,
|
|
@@ -1380,6 +1421,8 @@ class Component(BaseComponent, ABC):
|
|
|
for k in var_data.hooks
|
|
|
}
|
|
|
)
|
|
|
+ for component in var_data.components:
|
|
|
+ vars_hooks.update(component._get_all_hooks())
|
|
|
return vars_hooks
|
|
|
|
|
|
def _get_events_hooks(self) -> dict[str, VarData | None]:
|
|
@@ -1528,6 +1571,9 @@ class Component(BaseComponent, ABC):
|
|
|
refs.add(ref)
|
|
|
for child in self.children:
|
|
|
refs |= child._get_all_refs()
|
|
|
+ for component in self._get_components_in_props():
|
|
|
+ refs |= component._get_all_refs()
|
|
|
+
|
|
|
return refs
|
|
|
|
|
|
def _get_all_custom_components(
|
|
@@ -1551,6 +1597,9 @@ class Component(BaseComponent, ABC):
|
|
|
if not isinstance(child, Component):
|
|
|
continue
|
|
|
custom_components |= child._get_all_custom_components(seen=seen)
|
|
|
+ for component in self._get_components_in_props():
|
|
|
+ if isinstance(component, Component) and component.tag is not None:
|
|
|
+ custom_components |= component._get_all_custom_components(seen=seen)
|
|
|
return custom_components
|
|
|
|
|
|
@property
|
|
@@ -1614,17 +1663,65 @@ class CustomComponent(Component):
|
|
|
# The props of the component.
|
|
|
props: Dict[str, Any] = {}
|
|
|
|
|
|
- # Props that reference other components.
|
|
|
- component_props: Dict[str, Component] = {}
|
|
|
-
|
|
|
- def __init__(self, *args, **kwargs):
|
|
|
+ def __init__(self, **kwargs):
|
|
|
"""Initialize the custom component.
|
|
|
|
|
|
Args:
|
|
|
- *args: The args to pass to the component.
|
|
|
**kwargs: The kwargs to pass to the component.
|
|
|
"""
|
|
|
- super().__init__(*args, **kwargs)
|
|
|
+ component_fn = kwargs.get("component_fn")
|
|
|
+
|
|
|
+ # Set the props.
|
|
|
+ props_types = typing.get_type_hints(component_fn) if component_fn else {}
|
|
|
+ props = {key: value for key, value in kwargs.items() if key in props_types}
|
|
|
+ kwargs = {key: value for key, value in kwargs.items() if key not in props_types}
|
|
|
+
|
|
|
+ event_types = {
|
|
|
+ key
|
|
|
+ for key in props
|
|
|
+ if (
|
|
|
+ (get_origin((annotation := props_types.get(key))) or annotation)
|
|
|
+ == EventHandler
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ def get_args_spec(key: str) -> types.ArgsSpec | Sequence[types.ArgsSpec]:
|
|
|
+ type_ = props_types[key]
|
|
|
+
|
|
|
+ return (
|
|
|
+ args[0]
|
|
|
+ if (args := get_args(type_))
|
|
|
+ else (
|
|
|
+ annotation_args[1]
|
|
|
+ if get_origin(
|
|
|
+ (
|
|
|
+ annotation := inspect.getfullargspec(
|
|
|
+ component_fn
|
|
|
+ ).annotations[key]
|
|
|
+ )
|
|
|
+ )
|
|
|
+ is typing.Annotated
|
|
|
+ and (annotation_args := get_args(annotation))
|
|
|
+ else no_args_event_spec
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ super().__init__(
|
|
|
+ event_triggers={
|
|
|
+ key: EventChain.create(
|
|
|
+ value=props[key],
|
|
|
+ args_spec=get_args_spec(key),
|
|
|
+ key=key,
|
|
|
+ )
|
|
|
+ for key in event_types
|
|
|
+ },
|
|
|
+ **kwargs,
|
|
|
+ )
|
|
|
+
|
|
|
+ to_camel_cased_props = {
|
|
|
+ format.to_camel_case(key) for key in props if key not in event_types
|
|
|
+ }
|
|
|
+ self.get_props = lambda: to_camel_cased_props # pyright: ignore [reportIncompatibleVariableOverride]
|
|
|
|
|
|
# Unset the style.
|
|
|
self.style = Style()
|
|
@@ -1632,51 +1729,36 @@ class CustomComponent(Component):
|
|
|
# Set the tag to the name of the function.
|
|
|
self.tag = format.to_title_case(self.component_fn.__name__)
|
|
|
|
|
|
- # Get the event triggers defined in the component declaration.
|
|
|
- event_triggers_in_component_declaration = self.get_event_triggers()
|
|
|
-
|
|
|
- # Set the props.
|
|
|
- props = typing.get_type_hints(self.component_fn)
|
|
|
- for key, value in kwargs.items():
|
|
|
+ for key, value in props.items():
|
|
|
# Skip kwargs that are not props.
|
|
|
- if key not in props:
|
|
|
+ if key not in props_types:
|
|
|
continue
|
|
|
|
|
|
+ camel_cased_key = format.to_camel_case(key)
|
|
|
+
|
|
|
# Get the type based on the annotation.
|
|
|
- type_ = props[key]
|
|
|
+ type_ = props_types[key]
|
|
|
|
|
|
# Handle event chains.
|
|
|
- if types._issubclass(type_, EventChain):
|
|
|
- value = EventChain.create(
|
|
|
- value=value,
|
|
|
- args_spec=event_triggers_in_component_declaration.get(
|
|
|
- key, no_args_event_spec
|
|
|
- ),
|
|
|
- key=key,
|
|
|
+ if types._issubclass(type_, EventActionsMixin):
|
|
|
+ inspect.getfullargspec(component_fn).annotations[key]
|
|
|
+ self.props[camel_cased_key] = EventChain.create(
|
|
|
+ value=value, args_spec=get_args_spec(key), key=key
|
|
|
)
|
|
|
- self.props[format.to_camel_case(key)] = value
|
|
|
continue
|
|
|
|
|
|
- # Handle subclasses of Base.
|
|
|
- if isinstance(value, Base):
|
|
|
- base_value = LiteralVar.create(value)
|
|
|
-
|
|
|
- # Track hooks and imports associated with Component instances.
|
|
|
- if base_value is not None and isinstance(value, Component):
|
|
|
- self.component_props[key] = value
|
|
|
- value = base_value._replace(
|
|
|
- merge_var_data=VarData(
|
|
|
- imports=value._get_all_imports(),
|
|
|
- hooks=value._get_all_hooks(),
|
|
|
- )
|
|
|
- )
|
|
|
- else:
|
|
|
- value = base_value
|
|
|
- else:
|
|
|
- value = LiteralVar.create(value)
|
|
|
+ value = LiteralVar.create(value)
|
|
|
+ self.props[camel_cased_key] = value
|
|
|
+ setattr(self, camel_cased_key, value)
|
|
|
|
|
|
- # Set the prop.
|
|
|
- self.props[format.to_camel_case(key)] = value
|
|
|
+ @classmethod
|
|
|
+ def _are_fields_known(cls) -> bool:
|
|
|
+ """Check if the fields are known.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ Whether the fields are known.
|
|
|
+ """
|
|
|
+ return False
|
|
|
|
|
|
def __eq__(self, other: Any) -> bool:
|
|
|
"""Check if the component is equal to another.
|
|
@@ -1698,7 +1780,7 @@ class CustomComponent(Component):
|
|
|
return hash(self.tag)
|
|
|
|
|
|
@classmethod
|
|
|
- def get_props(cls) -> Set[str]: # pyright: ignore [reportIncompatibleVariableOverride]
|
|
|
+ def get_props(cls) -> Set[str]:
|
|
|
"""Get the props for the component.
|
|
|
|
|
|
Returns:
|
|
@@ -1735,27 +1817,8 @@ class CustomComponent(Component):
|
|
|
seen=seen
|
|
|
)
|
|
|
|
|
|
- # Fetch custom components from props as well.
|
|
|
- for child_component in self.component_props.values():
|
|
|
- if child_component.tag is None:
|
|
|
- continue
|
|
|
- if child_component.tag not in seen:
|
|
|
- seen.add(child_component.tag)
|
|
|
- if isinstance(child_component, CustomComponent):
|
|
|
- custom_components |= {child_component}
|
|
|
- custom_components |= child_component._get_all_custom_components(
|
|
|
- seen=seen
|
|
|
- )
|
|
|
return custom_components
|
|
|
|
|
|
- def _render(self) -> Tag:
|
|
|
- """Define how to render the component in React.
|
|
|
-
|
|
|
- Returns:
|
|
|
- The tag to render.
|
|
|
- """
|
|
|
- return super()._render(props=self.props)
|
|
|
-
|
|
|
def get_prop_vars(self) -> List[Var]:
|
|
|
"""Get the prop vars.
|
|
|
|
|
@@ -1765,29 +1828,19 @@ class CustomComponent(Component):
|
|
|
return [
|
|
|
Var(
|
|
|
_js_expr=name,
|
|
|
- _var_type=(prop._var_type if isinstance(prop, Var) else type(prop)),
|
|
|
+ _var_type=(
|
|
|
+ prop._var_type
|
|
|
+ if isinstance(prop, Var)
|
|
|
+ else (
|
|
|
+ type(prop)
|
|
|
+ if not isinstance(prop, EventActionsMixin)
|
|
|
+ else EventChain
|
|
|
+ )
|
|
|
+ ),
|
|
|
).guess_type()
|
|
|
for name, prop in self.props.items()
|
|
|
]
|
|
|
|
|
|
- def _get_vars(
|
|
|
- self, include_children: bool = False, ignore_ids: set[int] | None = None
|
|
|
- ) -> Iterator[Var]:
|
|
|
- """Walk all Vars used in this component.
|
|
|
-
|
|
|
- Args:
|
|
|
- include_children: Whether to include Vars from children.
|
|
|
- ignore_ids: The ids to ignore.
|
|
|
-
|
|
|
- Yields:
|
|
|
- Each var referenced by the component (props, styles, event handlers).
|
|
|
- """
|
|
|
- ignore_ids = ignore_ids or set()
|
|
|
- yield from super()._get_vars(
|
|
|
- include_children=include_children, ignore_ids=ignore_ids
|
|
|
- )
|
|
|
- yield from filter(lambda prop: isinstance(prop, Var), self.props.values())
|
|
|
-
|
|
|
@lru_cache(maxsize=None) # noqa: B019
|
|
|
def get_component(self) -> Component:
|
|
|
"""Render the component.
|
|
@@ -2475,6 +2528,7 @@ class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar):
|
|
|
The VarData for the var.
|
|
|
"""
|
|
|
return VarData.merge(
|
|
|
+ self._var_data,
|
|
|
VarData(
|
|
|
imports={
|
|
|
"@emotion/react": [
|
|
@@ -2517,9 +2571,21 @@ class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar):
|
|
|
Returns:
|
|
|
The var.
|
|
|
"""
|
|
|
+ var_datas = [
|
|
|
+ var_data
|
|
|
+ for var in value._get_vars(include_children=True)
|
|
|
+ if (var_data := var._get_all_var_data())
|
|
|
+ ]
|
|
|
+
|
|
|
return LiteralComponentVar(
|
|
|
_js_expr="",
|
|
|
_var_type=type(value),
|
|
|
- _var_data=_var_data,
|
|
|
+ _var_data=VarData.merge(
|
|
|
+ _var_data,
|
|
|
+ *var_datas,
|
|
|
+ VarData(
|
|
|
+ components=(value,),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
_var_value=value,
|
|
|
)
|