Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
50 changes: 46 additions & 4 deletions packages/reflex-base/src/reflex_base/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,35 @@ def __repr__(self) -> str:
return f"ComponentField(default={self.default!r}, is_javascript={self.is_javascript!r}{annotated_type_str})"
return f"ComponentField(default_factory={self.default_factory!r}, is_javascript={self.is_javascript!r}{annotated_type_str})"

def __get__(self, instance: Any, owner: type[Any] | None = None) -> Any:
"""Supply an unset field's default via the descriptor protocol.

With no ``__set__`` this is a non-data descriptor: an explicitly-set
value in the instance ``__dict__`` shadows it, so only unset fields
reach here. Construction can therefore skip materializing every
default onto every instance and let reads resolve them lazily.

Args:
instance: The component instance, or ``None`` for class access.
owner: The owning class.

Returns:
``self`` for class access, otherwise the default. Factory defaults
are cached on the instance so later in-place mutation persists.

Raises:
AttributeError: The field has neither a default nor a factory.
"""
if instance is None:
return self
if self.default is not MISSING:
return self.default
if self.default_factory is not None:
value = self.default_factory()
instance.__dict__[self._name] = value
return value
raise AttributeError(self._name)


def field(
default: FIELD_TYPE | _MISSING_TYPE = MISSING,
Expand Down Expand Up @@ -278,6 +307,15 @@ def _finalize_fields(
if value.is_javascript is True
}

# Install each own field as a class-level descriptor so unset instance
# attributes resolve to their default through ``ComponentField.__get__``
# (inherited fields resolve via the MRO). Skip names already bound to a
# non-field value so a ``@property`` or literal override keeps winning.
for field_name, field_ in own_fields.items():
existing = namespace.get(field_name)
if existing is None or isinstance(existing, ComponentField):
namespace[field_name] = field_
Comment thread
FarhanAliRaza marked this conversation as resolved.


class BaseComponent(metaclass=BaseComponentMeta):
"""The base class for all Reflex components.
Expand Down Expand Up @@ -314,9 +352,6 @@ def __init__(
"""
for key, value in kwargs.items():
setattr(self, key, value)
for name, value in self.get_fields().items():
if name not in kwargs:
setattr(self, name, value.default_value())

def set(self, **kwargs):
"""Set the component props.
Expand Down Expand Up @@ -1070,7 +1105,14 @@ def get_event_triggers(cls) -> dict[str, types.ArgsSpec | Sequence[types.ArgsSpe
"""
# Look for component specific triggers,
# e.g. variable declared as EventHandler types.
return DEFAULT_TRIGGERS | args_specs_from_fields(cls.get_fields()) # pyright: ignore [reportOperatorIssue]
# Cache on the class's own __dict__ (not inherited) so each subclass
# computes its own; the field set is fixed at class creation.
cached = cls.__dict__.get("_event_triggers_cache")
if cached is not None:
return cached
result = DEFAULT_TRIGGERS | args_specs_from_fields(cls.get_fields()) # pyright: ignore [reportOperatorIssue]
cls._event_triggers_cache = result
Comment thread
FarhanAliRaza marked this conversation as resolved.
return result

def __repr__(self) -> str:
"""Represent the component in React.
Expand Down
3 changes: 3 additions & 0 deletions packages/reflex-base/src/reflex_base/components/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
class BaseField(Generic[FIELD_TYPE]):
"""Base field class used by internal metadata classes."""

# Set by ``FieldBasedMeta._finalize_fields`` once the owning class is built.
_name: str

def __init__(
self,
default: FIELD_TYPE | _MISSING_TYPE = MISSING,
Expand Down
11 changes: 4 additions & 7 deletions packages/reflex-base/src/reflex_base/utils/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,16 @@
import json
import os
import re
from functools import lru_cache
from typing import TYPE_CHECKING, Any

from reflex_base import constants
from reflex_base.utils import exceptions

if TYPE_CHECKING:
from reflex_base.components.component import ComponentStyle
from reflex_base.event import (
ArgsSpec,
EventChain,
EventHandler,
EventSpec,
EventType,
)
from reflex_base.event import EventChain, EventHandler, EventSpec, EventType
from reflex_base.utils.types import ArgsSpec

WRAP_MAP = {
"{": "}",
Expand Down Expand Up @@ -174,6 +170,7 @@ def to_snake_case(text: str) -> str:
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_")


@lru_cache(maxsize=4096)
def to_camel_case(text: str, treat_hyphens_as_underscores: bool = True) -> str:
Comment thread
FarhanAliRaza marked this conversation as resolved.
"""Convert a string to camel case.

Expand Down
Loading