From c4f2af8981972c42f27f6a25ac7b13d512f0084c Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 12 May 2025 23:21:31 -0700 Subject: [PATCH 01/18] remove pydantic as base class of component --- reflex/app.py | 2 +- reflex/components/component.py | 371 +++++++++++++++--- reflex/components/datadisplay/code.py | 2 +- reflex/components/el/elements/forms.py | 2 +- reflex/components/radix/themes/base.py | 4 +- .../radix/themes/components/icon_button.py | 4 +- reflex/components/radix/themes/layout/list.py | 2 +- reflex/utils/pyi_generator.py | 14 +- reflex/utils/types.py | 14 + 9 files changed, 350 insertions(+), 65 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index bc84f6a788d..813f7791847 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1429,7 +1429,7 @@ def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs): ) if self.theme is not None: # Fix #2992 by removing the top-level appearance prop - self.theme.appearance = None + self.theme.appearance = None # pyright: ignore[reportAttributeAccessIssue] progress.advance(task) # Compile the app root. diff --git a/reflex/components/component.py b/reflex/components/component.py index 008cae5f42e..2abd054787d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -7,19 +7,32 @@ import dataclasses import functools import inspect +import sys import typing -from abc import ABC, abstractmethod +from abc import ABC, ABCMeta, abstractmethod from collections.abc import Callable, Iterator, Mapping, Sequence +from dataclasses import _MISSING_TYPE, MISSING from functools import wraps from hashlib import md5 from types import SimpleNamespace -from typing import Any, ClassVar, TypeVar, cast, get_args, get_origin +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + ClassVar, + ForwardRef, + Generic, + TypeVar, + _eval_type, # pyright: ignore [reportAttributeAccessIssue] + cast, + get_args, + get_origin, +) -import pydantic.v1 from rich.markup import escape +from typing_extensions import dataclass_transform import reflex.state -from reflex.base import Base from reflex.compiler.templates import STATEFUL_COMPONENT from reflex.components.core.breakpoints import Breakpoints from reflex.components.dynamic import load_dynamic_serializer @@ -62,26 +75,289 @@ from reflex.vars.sequence import LiteralArrayVar, LiteralStringVar, StringVar -class BaseComponent(Base, ABC): +def resolve_annotations( + raw_annotations: Mapping[str, type[Any]], module_name: str | None +) -> dict[str, type[Any]]: + """Partially taken from typing.get_type_hints. + + Resolve string or ForwardRef annotations into type objects if possible. + + Args: + raw_annotations: The raw annotations to resolve. + module_name: The name of the module. + + Returns: + The resolved annotations. + """ + module = sys.modules.get(module_name, None) if module_name is not None else None + + base_globals: dict[str, Any] | None = ( + module.__dict__ if module is not None else None + ) + + annotations = {} + for name, value in raw_annotations.items(): + if isinstance(value, str): + if sys.version_info == (3, 10, 0): + value = ForwardRef(value, is_argument=False) + else: + value = ForwardRef(value, is_argument=False, is_class=True) + try: + if sys.version_info >= (3, 13): + value = _eval_type(value, base_globals, None, type_params=()) + else: + value = _eval_type(value, base_globals, None) + except NameError: + # this is ok, it can be fixed with update_forward_refs + pass + annotations[name] = value + return annotations + + +FIELD_TYPE = TypeVar("FIELD_TYPE") + + +class ComponentField(Generic[FIELD_TYPE]): + """A field for a component.""" + + def __init__( + self, + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], FIELD_TYPE] | None = None, + is_javascript: bool | None = None, + annotated_type: type[Any] | _MISSING_TYPE = MISSING, + ) -> None: + """Initialize the field. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + is_javascript: Whether the field is a javascript property. + annotated_type: The annotated type for the field. + """ + self.default = default + self.default_factory = default_factory + self.is_javascript = is_javascript + self.annotated_type = annotated_type + type_origin = get_origin(annotated_type) or annotated_type + if type_origin is Annotated: + type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue] + self.type_origin = type_origin + + def default_value(self) -> FIELD_TYPE: + """Get the default value for the field. + + Returns: + The default value for the field. + + Raises: + ValueError: If no default value or factory is provided. + """ + if self.default is not MISSING: + return self.default + if self.default_factory is not None: + return self.default_factory() + raise ValueError("No default value or factory provided.") + + def __repr__(self) -> str: + """Represent the field in a readable format. + + Returns: + The string representation of the field. + """ + annotated_type_str = ( + f", annotated_type={self.annotated_type!r}" + if self.annotated_type is not MISSING + else "" + ) + if self.default is not MISSING: + 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 field( + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], FIELD_TYPE] | None = None, + is_javascript_property: bool | None = None, +) -> FIELD_TYPE: + """Create a field for a component. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + is_javascript_property: Whether the field is a javascript property. + + Returns: + The field for the component. + + Raises: + ValueError: If both default and default_factory are specified. + """ + if default is not MISSING and default_factory is not None: + raise ValueError("cannot specify both default and default_factory") + return ComponentField( # pyright: ignore [reportReturnType] + default=default, + default_factory=default_factory, + is_javascript=is_javascript_property, + ) + + +@dataclass_transform(kw_only_default=True) +class BaseComponentMeta(ABCMeta): + """Meta class for BaseComponent.""" + + if TYPE_CHECKING: + _fields: Mapping[str, ComponentField] + _js_fields: Mapping[str, ComponentField] + + def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> type: + """Create a new class. + + Args: + name: The name of the class. + bases: The bases of the class. + namespace: The namespace of the class. + + Returns: + The new class. + """ + # Add the field to the class + fields: dict[str, ComponentField] = {} + js_fields: dict[str, ComponentField] = {} + resolved_annotations = resolve_annotations( + namespace.get("__annotations__", {}), namespace["__module__"] + ) + + for base in bases: + if hasattr(base, "_fields"): + fields.update(base._fields) + js_fields.update(base._js_fields) + + for key, value, inherited_field in [ + (key, value, inherited_field) + for key, value in namespace.items() + if key not in resolved_annotations + and ((inherited_field := fields.get(key)) is not None) + ]: + new_value = ComponentField( + default=value, + is_javascript=inherited_field.is_javascript, + annotated_type=inherited_field.annotated_type, + ) + + if new_value.is_javascript: + js_fields[key] = new_value + + fields[key] = new_value + + for key, annotation in resolved_annotations.items(): + value = namespace.get(key, MISSING) + + if types.is_classvar(annotation): + # If the annotation is a classvar, skip it. + continue + + if value is MISSING: + value = ComponentField( + default=None, is_javascript=True, annotated_type=annotation + ) + elif not isinstance(value, ComponentField): + value = ComponentField( + default=value, + is_javascript=( + True + if (existing_field := fields.get(key)) is None + else existing_field.is_javascript + ), + annotated_type=annotation, + ) + else: + value = ComponentField( + default=value.default, + default_factory=value.default_factory, + is_javascript=value.is_javascript, + annotated_type=annotation, + ) + + if value.is_javascript: + js_fields[key] = value + + fields[key] = value + + namespace["_fields"] = fields + namespace["_js_fields"] = js_fields + return super().__new__(cls, name, bases, namespace) + + +class BaseComponent(metaclass=BaseComponentMeta): """The base class for all Reflex components. This is something that can be rendered as a Component via the Reflex compiler. """ # The children nested within the component. - children: list[BaseComponent] = pydantic.v1.Field(default_factory=list) + children: list[BaseComponent] = field( + default_factory=list, is_javascript_property=False + ) # The library that the component is based on. - library: str | None = pydantic.v1.Field(default_factory=lambda: None) + library: str | None = field(default=None, is_javascript_property=False) # List here the non-react dependency needed by `library` - lib_dependencies: list[str] = pydantic.v1.Field(default_factory=list) + lib_dependencies: list[str] = field( + default_factory=list, is_javascript_property=False + ) # List here the dependencies that need to be transpiled by Next.js - transpile_packages: list[str] = pydantic.v1.Field(default_factory=list) + transpile_packages: list[str] = field( + default_factory=list, is_javascript_property=False + ) # The tag to use when rendering the component. - tag: str | None = pydantic.v1.Field(default_factory=lambda: None) + tag: str | None = field(default=None, is_javascript_property=False) + + def __init__( + self, + **kwargs, + ): + """Initialize the component. + + Args: + **kwargs: The kwargs to pass to the component. + """ + 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. + + Args: + **kwargs: The kwargs to set. + """ + for key, value in kwargs.items(): + setattr(self, key, value) + return self + + @classmethod + def get_fields(cls) -> Mapping[str, ComponentField]: + """Get the fields of the component. + + Returns: + The fields of the component. + """ + return cls._fields + + @classmethod + def get_js_fields(cls) -> Mapping[str, ComponentField]: + """Get the javascript fields of the component. + + Returns: + The javascript fields of the component. + """ + return cls._js_fields @abstractmethod def render(self) -> dict: @@ -258,39 +534,39 @@ class Component(BaseComponent, ABC): """A component with style, event trigger and other props.""" # The style of the component. - style: Style = pydantic.v1.Field(default_factory=Style) + style: Style = field(default_factory=Style, is_javascript_property=False) # A mapping from event triggers to event chains. - event_triggers: dict[str, EventChain | Var] = pydantic.v1.Field( - default_factory=dict + event_triggers: dict[str, EventChain | Var] = field( + default_factory=dict, is_javascript_property=False ) # The alias for the tag. - alias: str | None = pydantic.v1.Field(default_factory=lambda: None) + alias: str | None = field(default=None, is_javascript_property=False) # Whether the component is a global scope tag. True for tags like `html`, `head`, `body`. _is_tag_in_global_scope: ClassVar[bool] = False # Whether the import is default or named. - is_default: bool | None = pydantic.v1.Field(default_factory=lambda: False) + is_default: bool | None = field(default=False, is_javascript_property=False) # A unique key for the component. - key: Any = pydantic.v1.Field(default_factory=lambda: None) + key: Any = field(default=None, is_javascript_property=False) # The id for the component. - id: Any = pydantic.v1.Field(default_factory=lambda: None) + id: Any = field(default=None, is_javascript_property=False) # The Var to pass as the ref to the component. - ref: Var | None = pydantic.v1.Field(default_factory=lambda: None) + ref: Var | None = field(default=None, is_javascript_property=False) # The class name for the component. - class_name: Any = pydantic.v1.Field(default_factory=lambda: None) + class_name: Any = field(default=None, is_javascript_property=False) # Special component props. - special_props: list[Var] = pydantic.v1.Field(default_factory=list) + special_props: list[Var] = field(default_factory=list, is_javascript_property=False) # Whether the component should take the focus once the page is loaded - autofocus: bool = pydantic.v1.Field(default_factory=lambda: False) + autofocus: bool = field(default=False, is_javascript_property=False) # components that cannot be children _invalid_children: ClassVar[list[str]] = [] @@ -305,14 +581,18 @@ class Component(BaseComponent, ABC): _rename_props: ClassVar[dict[str, str]] = {} # custom attribute - custom_attrs: dict[str, Var | Any] = pydantic.v1.Field(default_factory=dict) + custom_attrs: dict[str, Var | Any] = field( + default_factory=dict, is_javascript_property=False + ) # When to memoize this component and its children. - _memoization_mode: MemoizationMode = MemoizationMode() + _memoization_mode: MemoizationMode = field( + default_factory=MemoizationMode, is_javascript_property=False + ) # State class associated with this component instance - State: type[reflex.state.State] | None = pydantic.v1.Field( - default_factory=lambda: None + State: type[reflex.state.State] | None = field( + default=None, is_javascript_property=False ) def add_imports(self) -> ImportDict | list[ImportDict]: @@ -409,25 +689,6 @@ def __init_subclass__(cls, **kwargs): """ super().__init_subclass__(**kwargs) - # Get all the props for the component. - props = cls.get_props() - - # Convert fields to props, setting default values. - for field in cls.get_fields().values(): - # If the field is not a component prop, skip it. - if field.name not in props: - continue - - # Set default values for any props. - if field.type_ is Var: - field.required = False - if field.default is not None: - field.default_factory = functools.partial( - LiteralVar.create, field.default - ) - elif field.type_ is EventHandler: - field.required = False - # Ensure renamed props from parent classes are applied to the subclass. if cls._rename_props: inherited_rename_props = {} @@ -496,7 +757,9 @@ def _post_init(self, *args, **kwargs): is_var = False elif key in props: # Set the field type. - is_var = field.type_ is Var if (field := fields.get(key)) else False + is_var = ( + field.type_origin is Var if (field := fields.get(key)) else False + ) else: continue @@ -593,11 +856,7 @@ def determine_key(value: Any): kwargs["style"] = Style( { - **( - fields_style.default_factory() - if fields_style.default_factory - else fields_style.default - ), + **fields_style.default_value(), **style, **{attr: value for attr, value in kwargs.items() if attr not in fields}, } @@ -651,13 +910,13 @@ def get_event_triggers( triggers = DEFAULT_TRIGGERS.copy() # Look for component specific triggers, # e.g. variable declared as EventHandler types. - for field in self.get_fields().values(): - if field.type_ is EventHandler: + for name, field in self.get_fields().items(): + if field.type_origin is EventHandler: args_spec = None - annotation = field.annotation + annotation = field.annotated_type if (metadata := getattr(annotation, "__metadata__", None)) is not None: args_spec = metadata[0] - triggers[field.name] = args_spec or (no_args_event_spec) + triggers[name] = args_spec or (no_args_event_spec) return triggers def __repr__(self) -> str: @@ -864,7 +1123,7 @@ def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T: Returns: The component. """ - comp = cls.construct(id=props.get("id"), children=list(children)) + comp = cls(id=props.get("id"), children=list(children)) comp._post_init(children=list(children), **props) return comp @@ -881,7 +1140,7 @@ def _unsafe_create( Returns: The component. """ - comp = cls.construct(id=props.get("id"), children=list(children)) + comp = cls(id=props.get("id"), children=list(children)) for prop, value in props.items(): setattr(comp, prop, value) return comp diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index 4ddb1ecfc8d..175789bb166 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -546,7 +546,7 @@ def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> Var: }}""", _var_data=VarData( imports={ - cls.__fields__["library"].default: [ + cls.get_fields()["library"].default_value(): [ ImportVar(tag="PrismAsyncLight", alias="SyntaxHighlighter") ] }, diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index 9d7e366e116..da172ac057c 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -189,7 +189,7 @@ def create(cls, *children, **props): # Render the form hooks and use the hash of the resulting code to create a unique name. props["handle_submit_unique_name"] = "" form = super().create(*children, **props) - form.handle_submit_unique_name = md5( + form.handle_submit_unique_name = md5( # pyright: ignore[reportAttributeAccessIssue] str(form._get_all_hooks()).encode("utf-8") ).hexdigest() return form diff --git a/reflex/components/radix/themes/base.py b/reflex/components/radix/themes/base.py index bbc791285a4..490d8f49171 100644 --- a/reflex/components/radix/themes/base.py +++ b/reflex/components/radix/themes/base.py @@ -135,7 +135,9 @@ def create( """ component = super().create(*children, **props) if component.library is None: - component.library = RadixThemesComponent.__fields__["library"].default + component.library = RadixThemesComponent.get_fields()[ + "library" + ].default_value() component.alias = "RadixThemes" + (component.tag or type(component).__name__) return component diff --git a/reflex/components/radix/themes/components/icon_button.py b/reflex/components/radix/themes/components/icon_button.py index aafb9e1ebd6..7d865365be7 100644 --- a/reflex/components/radix/themes/components/icon_button.py +++ b/reflex/components/radix/themes/components/icon_button.py @@ -75,7 +75,7 @@ def create(cls, *children, **props) -> Component: ) if "size" in props: if isinstance(props["size"], str): - children[0].size = RADIX_TO_LUCIDE_SIZE[props["size"]] + children[0].size = RADIX_TO_LUCIDE_SIZE[props["size"]] # pyright: ignore[reportAttributeAccessIssue] else: size_map_var = Match.create( props["size"], @@ -84,7 +84,7 @@ def create(cls, *children, **props) -> Component: ) if not isinstance(size_map_var, Var): raise ValueError(f"Match did not return a Var: {size_map_var}") - children[0].size = size_map_var + children[0].size = size_map_var # pyright: ignore[reportAttributeAccessIssue] return super().create(*children, **props) def add_style(self): diff --git a/reflex/components/radix/themes/layout/list.py b/reflex/components/radix/themes/layout/list.py index e976b7146d1..2cf460638e0 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/reflex/components/radix/themes/layout/list.py @@ -170,7 +170,7 @@ def create(cls, *children, **props): """ for child in children: if isinstance(child, Text): - child.as_ = "span" + child.as_ = "span" # pyright: ignore[reportAttributeAccessIssue] elif isinstance(child, Icon) and "display" not in child.style: child.style["display"] = "inline" return super().create(*children, **props) diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index 6de5c3c3cf1..8a56386b2ad 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -10,6 +10,7 @@ import logging import re import subprocess +import sys import typing from collections.abc import Callable, Iterable, Sequence from fileinput import FileInput @@ -387,13 +388,22 @@ def _extract_class_props_as_ast_nodes( if isinstance(default, Var): default = default._decode() + modules = {cls.__module__ for cls in target_class.__mro__} + available_vars = {} + for module in modules: + available_vars.update(sys.modules[module].__dict__) + kwargs.append( ( ast.arg( arg=name, annotation=ast.Name( id=OVERWRITE_TYPES.get( - name, _get_type_hint(value, type_hint_globals) + name, + _get_type_hint( + value, + type_hint_globals | available_vars, + ), ) ), ), @@ -1227,7 +1237,7 @@ def scan_all( continue subprocess.run(["git", "checkout", changed_file]) - if cpu_count() == 1 or len(file_targets) < 5: + if True: self._scan_files(file_targets) else: self._scan_files_multiprocess(file_targets) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 486d68614bb..f0a632e9696 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -254,6 +254,20 @@ def is_optional(cls: GenericType) -> bool: return is_union(cls) and type(None) in get_args(cls) +def is_classvar(a_type: Any) -> bool: + """Check if a type is a ClassVar. + + Args: + a_type: The type to check. + + Returns: + Whether the type is a ClassVar. + """ + return a_type is ClassVar or ( + type(a_type) is _GenericAlias and a_type.__origin__ is ClassVar + ) + + def true_type_for_pydantic_field(f: ModelField): """Get the type for a pydantic field. From 4e441d5be498b99edf3c173f65b8a614fbb60dbf Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 12 May 2025 23:43:38 -0700 Subject: [PATCH 02/18] remove deprecation --- reflex/components/component.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 2abd054787d..63b1d3955fb 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -703,12 +703,6 @@ def __init__(self, **kwargs): Args: **kwargs: The kwargs to pass to the component. """ - console.deprecate( - "component-direct-instantiation", - reason="Use the `create` method instead.", - deprecation_version="0.7.2", - removal_version="0.8.0", - ) super().__init__( children=kwargs.get("children", []), ) From db61bb3acb840905e965f5f5ac023cfcd503a1d3 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 12 May 2025 23:52:56 -0700 Subject: [PATCH 03/18] fix things with components --- reflex/compiler/utils.py | 2 +- reflex/components/component.py | 13 +++++++++++++ tests/units/components/test_component.py | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 0bdb55ca4da..ed0e258a229 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -309,7 +309,7 @@ def compile_custom_component( A tuple of the compiled component and the imports required by the component. """ # Render the component. - render = component.get_component(component) + render = component.get_component() # Get the imports. imports: ParsedImportDict = { diff --git a/reflex/components/component.py b/reflex/components/component.py index 63b1d3955fb..1281e133173 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -341,6 +341,19 @@ def set(self, **kwargs): setattr(self, key, value) return self + def __eq__(self, value: Any) -> bool: + """Check if the component is equal to another value. + + Args: + value: The value to compare to. + + Returns: + Whether the component is equal to the value. + """ + return type(self) is type(value) and bool( + getattr(self, key) == getattr(value, key) for key in self.get_fields() + ) + @classmethod def get_fields(cls) -> Mapping[str, ComponentField]: """Get the fields of the component. diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 2a994393493..c516f85e03e 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -911,7 +911,7 @@ def my_component(width: Var[int], color: Var[str]): assert len(ccomponent.children) == 1 assert isinstance(ccomponent.children[0], Text) - component = ccomponent.get_component(ccomponent) + component = ccomponent.get_component() assert isinstance(component, Box) @@ -1823,7 +1823,7 @@ def outer(c: Component): assert "outer" not in custom_comp._get_all_imports() # The imports are only resolved during compilation. - custom_comp.get_component(custom_comp) + custom_comp.get_component() _, imports_inner = compile_custom_component(custom_comp) assert "inner" in imports_inner assert "outer" not in imports_inner From 25615412e0f8bf17407353c06f0829d5d783d84e Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 12 May 2025 23:57:17 -0700 Subject: [PATCH 04/18] don't post init twice --- reflex/components/component.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 1281e133173..db796c70892 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -1130,9 +1130,7 @@ def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T: Returns: The component. """ - comp = cls(id=props.get("id"), children=list(children)) - comp._post_init(children=list(children), **props) - return comp + return cls(id=props.get("id"), children=list(children), **props) @classmethod def _unsafe_create( From 6cb08a1ff46393b0d9f272f629267fdb6a826ff1 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 00:11:53 -0700 Subject: [PATCH 05/18] do the new guy --- reflex/components/component.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index db796c70892..dd17a44eb76 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -719,6 +719,12 @@ def __init__(self, **kwargs): super().__init__( children=kwargs.get("children", []), ) + console.deprecate( + "component-direct-instantiation", + reason="Use the `create` method instead.", + deprecation_version="0.7.2", + removal_version="0.8.0", + ) self._post_init(**kwargs) def _post_init(self, *args, **kwargs): @@ -1130,7 +1136,10 @@ def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T: Returns: The component. """ - return cls(id=props.get("id"), children=list(children), **props) + comp = cls.__new__(cls) + super(Component, comp).__init__(id=props.get("id"), children=list(children)) + comp._post_init(children=children, **props) + return comp @classmethod def _unsafe_create( @@ -1145,7 +1154,8 @@ def _unsafe_create( Returns: The component. """ - comp = cls(id=props.get("id"), children=list(children)) + comp = cls.__new__(cls) + super(Component, comp).__init__(id=props.get("id"), children=list(children)) for prop, value in props.items(): setattr(comp, prop, value) return comp From 8afe41704195c31ab5a53d932916d149fbf6af6a Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 00:13:04 -0700 Subject: [PATCH 06/18] keep that one as is --- reflex/components/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index dd17a44eb76..0b48b4907f8 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -1138,7 +1138,7 @@ def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T: """ comp = cls.__new__(cls) super(Component, comp).__init__(id=props.get("id"), children=list(children)) - comp._post_init(children=children, **props) + comp._post_init(children=list(children), **props) return comp @classmethod From 0dc10fca7380f31f80a840baffb6dae17ac93342 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 00:41:04 -0700 Subject: [PATCH 07/18] fix unit tests --- reflex/components/component.py | 4 +- .../datadisplay/shiki_code_block.py | 183 ++++++++++-------- 2 files changed, 99 insertions(+), 88 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 0b48b4907f8..8799177c76f 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -2014,10 +2014,10 @@ class CustomComponent(Component): library = f"$/{Dirs.COMPONENTS_PATH}" # The function that creates the component. - component_fn: Callable[..., Component] = Component.create + component_fn: Callable[..., Component] = field(default=Component.create) # The props of the component. - props: dict[str, Any] = {} + props: dict[str, Any] = field(default_factory=dict) def _post_init(self, **kwargs): """Initialize the custom component. diff --git a/reflex/components/datadisplay/shiki_code_block.py b/reflex/components/datadisplay/shiki_code_block.py index 816f5fa68a4..bcd348cce80 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/reflex/components/datadisplay/shiki_code_block.py @@ -2,11 +2,12 @@ from __future__ import annotations +import dataclasses import re from collections import defaultdict +from dataclasses import dataclass from typing import Any, Literal -from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.core.colors import color from reflex.components.core.cond import color_mode_cond @@ -410,99 +411,109 @@ class ShikiDecorations(NoExtrasAllowedProps): always_wrap: bool = False -class ShikiBaseTransformers(Base): +@dataclass(kw_only=True) +class ShikiBaseTransformers: """Base for creating transformers.""" - library: str - fns: list[FunctionStringVar] - style: Style | None + library: str = "" + fns: list[FunctionStringVar] = dataclasses.field(default_factory=list) + style: Style | None = dataclasses.field(default=None) +@dataclass(kw_only=True) class ShikiJsTransformer(ShikiBaseTransformers): """A Wrapped shikijs transformer.""" library: str = "@shikijs/transformers@3.3.0" - fns: list[FunctionStringVar] = [ - FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS - ] - style: Style | None = Style( - { - "code": {"line-height": "1.7", "font-size": "0.875em", "display": "grid"}, - # Diffs - ".diff": { - "margin": "0 -24px", - "padding": "0 24px", - "width": "calc(100% + 48px)", - "display": "inline-block", - }, - ".diff.add": { - "background-color": "rgba(16, 185, 129, .14)", - "position": "relative", - }, - ".diff.remove": { - "background-color": "rgba(244, 63, 94, .14)", - "opacity": "0.7", - "position": "relative", - }, - ".diff.remove:after": { - "position": "absolute", - "left": "10px", - "content": "'-'", - "color": "#b34e52", - }, - ".diff.add:after": { - "position": "absolute", - "left": "10px", - "content": "'+'", - "color": "#18794e", - }, - # Highlight - ".highlighted": { - "background-color": "rgba(142, 150, 170, .14)", - "margin": "0 -24px", - "padding": "0 24px", - "width": "calc(100% + 48px)", - "display": "inline-block", - }, - ".highlighted.error": { - "background-color": "rgba(244, 63, 94, .14)", - }, - ".highlighted.warning": { - "background-color": "rgba(234, 179, 8, .14)", - }, - # Highlighted Word - ".highlighted-word": { - "background-color": color("gray", 2), - "border": f"1px solid {color('gray', 5)}", - "padding": "1px 3px", - "margin": "-1px -3px", - "border-radius": "4px", - }, - # Focused Lines - ".has-focused .line:not(.focused)": { - "opacity": "0.7", - "filter": "blur(0.095rem)", - "transition": "filter .35s, opacity .35s", - }, - ".has-focused:hover .line:not(.focused)": { - "opacity": "1", - "filter": "none", - }, - # White Space - # ".tab, .space": { - # "position": "relative", # noqa: ERA001 - # }, - # ".tab::before": { - # "content": "'⇥'", # noqa: ERA001 - # "position": "absolute", # noqa: ERA001 - # "opacity": "0.3",# noqa: ERA001 - # }, - # ".space::before": { - # "content": "'·'", # noqa: ERA001 - # "position": "absolute", # noqa: ERA001 - # "opacity": "0.3", # noqa: ERA001 - # }, - } + fns: list[FunctionStringVar] = dataclasses.field( + default_factory=lambda: [ + FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS + ] + ) + style: Style | None = dataclasses.field( + default_factory=lambda: Style( + { + "code": { + "line-height": "1.7", + "font-size": "0.875em", + "display": "grid", + }, + # Diffs + ".diff": { + "margin": "0 -24px", + "padding": "0 24px", + "width": "calc(100% + 48px)", + "display": "inline-block", + }, + ".diff.add": { + "background-color": "rgba(16, 185, 129, .14)", + "position": "relative", + }, + ".diff.remove": { + "background-color": "rgba(244, 63, 94, .14)", + "opacity": "0.7", + "position": "relative", + }, + ".diff.remove:after": { + "position": "absolute", + "left": "10px", + "content": "'-'", + "color": "#b34e52", + }, + ".diff.add:after": { + "position": "absolute", + "left": "10px", + "content": "'+'", + "color": "#18794e", + }, + # Highlight + ".highlighted": { + "background-color": "rgba(142, 150, 170, .14)", + "margin": "0 -24px", + "padding": "0 24px", + "width": "calc(100% + 48px)", + "display": "inline-block", + }, + ".highlighted.error": { + "background-color": "rgba(244, 63, 94, .14)", + }, + ".highlighted.warning": { + "background-color": "rgba(234, 179, 8, .14)", + }, + # Highlighted Word + ".highlighted-word": { + "background-color": color("gray", 2), + "border": f"1px solid {color('gray', 5)}", + "padding": "1px 3px", + "margin": "-1px -3px", + "border-radius": "4px", + }, + # Focused Lines + ".has-focused .line:not(.focused)": { + "opacity": "0.7", + "filter": "blur(0.095rem)", + "transition": "filter .35s, opacity .35s", + }, + ".has-focused:hover .line:not(.focused)": { + "opacity": "1", + "filter": "none", + }, + # White Space + # ".tab, .space": { + # "position": "relative", # noqa: ERA001 + # }, + # ".tab::before": { + # "content": "'⇥'", # noqa: ERA001 + # "position": "absolute", # noqa: ERA001 + # "opacity": "0.3",# noqa: ERA001 + # }, + # ".space::before": { + # "content": "'·'", # noqa: ERA001 + # "position": "absolute", # noqa: ERA001 + # "opacity": "0.3", # noqa: ERA001 + # }, + } + ) ) def __init__(self, **kwargs): From 79ae642a8dc938f7bb23186f1ffd49abdfdaff2d Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 00:49:07 -0700 Subject: [PATCH 08/18] invert bases --- reflex/components/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 8799177c76f..9c8e889f158 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -228,7 +228,7 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty namespace.get("__annotations__", {}), namespace["__module__"] ) - for base in bases: + for base in bases[::-1]: if hasattr(base, "_fields"): fields.update(base._fields) js_fields.update(base._js_fields) From e79ddf7f1e62129f27d600388c441aee8e63b4c2 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 00:50:38 -0700 Subject: [PATCH 09/18] precommit --- pyi_hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 69c97b98ef1..d05564bb50c 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -24,7 +24,7 @@ "reflex/components/datadisplay/__init__.pyi": "cf087efa8b3960decc6b231cc986cfa9", "reflex/components/datadisplay/code.pyi": "651fc3d417b998eb1c3d072328f505d0", "reflex/components/datadisplay/dataeditor.pyi": "601c59f3ced6ab94fcf5527b90472a4f", - "reflex/components/datadisplay/shiki_code_block.pyi": "9aa77c0834d2eea2c1693f01971fb244", + "reflex/components/datadisplay/shiki_code_block.pyi": "ac16fd6c23eef7ce0185437ecf2d529d", "reflex/components/el/__init__.pyi": "09042a2db5e0637e99b5173430600522", "reflex/components/el/element.pyi": "323cfb5d67d8ccb58ac36c7cc7641dc3", "reflex/components/el/elements/__init__.pyi": "280ed457675f3720e34b560a3f617739", From e44879224bdd7355009b647b3d6270b583a14aee Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 01:03:58 -0700 Subject: [PATCH 10/18] write that list comp as dict --- reflex/components/component.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 9c8e889f158..3d0a1a80f49 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -336,6 +336,9 @@ def set(self, **kwargs): Args: **kwargs: The kwargs to set. + + Returns: + The component with the updated props. """ for key, value in kwargs.items(): setattr(self, key, value) @@ -920,17 +923,20 @@ def get_event_triggers( Returns: The event triggers. """ - triggers = DEFAULT_TRIGGERS.copy() # Look for component specific triggers, # e.g. variable declared as EventHandler types. - for name, field in self.get_fields().items(): - if field.type_origin is EventHandler: - args_spec = None - annotation = field.annotated_type - if (metadata := getattr(annotation, "__metadata__", None)) is not None: - args_spec = metadata[0] - triggers[name] = args_spec or (no_args_event_spec) - return triggers + return DEFAULT_TRIGGERS | { + name: ( + metadata[0] + if ( + (metadata := getattr(field.annotated_type, "__metadata__", None)) + is not None + ) + else no_args_event_spec + ) + for name, field in self.get_fields().items() + if field.type_origin is EventHandler + } def __repr__(self) -> str: """Represent the component in React. From a40628b2099b9e1772a17cd641801755c2855323 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 01:25:38 -0700 Subject: [PATCH 11/18] mro is weird --- reflex/components/component.py | 38 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 3d0a1a80f49..4415cfaafc8 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -207,6 +207,8 @@ class BaseComponentMeta(ABCMeta): """Meta class for BaseComponent.""" if TYPE_CHECKING: + _inherited_fields: Mapping[str, ComponentField] + _own_fields: Mapping[str, ComponentField] _fields: Mapping[str, ComponentField] _js_fields: Mapping[str, ComponentField] @@ -222,22 +224,24 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty The new class. """ # Add the field to the class - fields: dict[str, ComponentField] = {} - js_fields: dict[str, ComponentField] = {} + inherited_fields: dict[str, ComponentField] = {} + own_fields: dict[str, ComponentField] = {} resolved_annotations = resolve_annotations( namespace.get("__annotations__", {}), namespace["__module__"] ) for base in bases[::-1]: - if hasattr(base, "_fields"): - fields.update(base._fields) - js_fields.update(base._js_fields) + if hasattr(base, "_inherited_fields"): + inherited_fields.update(base._inherited_fields) + for base in bases[::-1]: + if hasattr(base, "_own_fields"): + inherited_fields.update(base._own_fields) for key, value, inherited_field in [ (key, value, inherited_field) for key, value in namespace.items() if key not in resolved_annotations - and ((inherited_field := fields.get(key)) is not None) + and ((inherited_field := inherited_fields.get(key)) is not None) ]: new_value = ComponentField( default=value, @@ -245,10 +249,7 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty annotated_type=inherited_field.annotated_type, ) - if new_value.is_javascript: - js_fields[key] = new_value - - fields[key] = new_value + own_fields[key] = new_value for key, annotation in resolved_annotations.items(): value = namespace.get(key, MISSING) @@ -266,7 +267,7 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty default=value, is_javascript=( True - if (existing_field := fields.get(key)) is None + if (existing_field := inherited_fields.get(key)) is None else existing_field.is_javascript ), annotated_type=annotation, @@ -279,13 +280,16 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty annotated_type=annotation, ) - if value.is_javascript: - js_fields[key] = value + own_fields[key] = value - fields[key] = value - - namespace["_fields"] = fields - namespace["_js_fields"] = js_fields + namespace["_own_fields"] = own_fields + namespace["_inherited_fields"] = inherited_fields + namespace["_fields"] = inherited_fields | own_fields + namespace["_js_fields"] = { + key: value + for key, value in own_fields.items() + if value.is_javascript is True + } return super().__new__(cls, name, bases, namespace) From ce691e2ef64ce17d9e605390edf7f344c7d05f52 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 01:26:48 -0700 Subject: [PATCH 12/18] add outer type and type for a bit of compatibility --- reflex/components/component.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 4415cfaafc8..6db665e6321 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -138,11 +138,11 @@ def __init__( self.default = default self.default_factory = default_factory self.is_javascript = is_javascript - self.annotated_type = annotated_type + self.outer_type_ = self.annotated_type = annotated_type type_origin = get_origin(annotated_type) or annotated_type if type_origin is Annotated: type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue] - self.type_origin = type_origin + self.type_ = self.type_origin = type_origin def default_value(self) -> FIELD_TYPE: """Get the default value for the field. From 2d54ac6a8abcb9844b981eda913385fb7615a591 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 14:17:05 -0700 Subject: [PATCH 13/18] do not think about this one, __pydantic_validate_values__ is the guy who determines subfield validation --- reflex/vars/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index d9d77d2242e..8c29f5bbc85 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -1310,6 +1310,21 @@ def range( if not TYPE_CHECKING: + def __getattribute__(self, name: str): + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute of the var. + + # noqa: DAR101 self + """ + if name == "__pydantic_validate_values__": + return lambda *args, **kwargs: None + return super().__getattribute__(name) + def __getitem__(self, key: Any) -> Var: """Get the item from the var. From 778f15a74884c772ae4b422781fe9e2f4ef9067c Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 14:23:32 -0700 Subject: [PATCH 14/18] maybe --- reflex/vars/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 8c29f5bbc85..75b2f011286 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -88,6 +88,12 @@ warnings.filterwarnings("ignore", message="fields may not start with an underscore") +_PYDANTIC_VALIDATE_VALUES = "__pydantic_validate_values__" + + +def _PYDANTIC_VALIDATOR(*args, **kwargs): + return None + @dataclasses.dataclass( eq=False, @@ -1321,8 +1327,8 @@ def __getattribute__(self, name: str): # noqa: DAR101 self """ - if name == "__pydantic_validate_values__": - return lambda *args, **kwargs: None + if name == _PYDANTIC_VALIDATE_VALUES: + return _PYDANTIC_VALIDATOR return super().__getattribute__(name) def __getitem__(self, key: Any) -> Var: From d4badf586d0c44a4eece43aa3543e102f67fa5f3 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 14:38:08 -0700 Subject: [PATCH 15/18] META --- reflex/vars/base.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 75b2f011286..0d8842c27fa 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -91,7 +91,7 @@ _PYDANTIC_VALIDATE_VALUES = "__pydantic_validate_values__" -def _PYDANTIC_VALIDATOR(*args, **kwargs): +def _pydantic_validator(*args, **kwargs): return None @@ -369,11 +369,26 @@ def can_use_in_object_var(cls: GenericType) -> bool: ) +class MetaclassVar(type): + """Metaclass for the Var class.""" + + def __setattr__(cls, name: str, value: Any): + """Set an attribute on the class. + + Args: + name: The name of the attribute. + value: The value of the attribute. + """ + if name == _PYDANTIC_VALIDATE_VALUES: + value = _pydantic_validator + super().__setattr__(name, value) + + @dataclasses.dataclass( eq=False, frozen=True, ) -class Var(Generic[VAR_TYPE]): +class Var(Generic[VAR_TYPE], metaclass=MetaclassVar): """Base class for immutable vars.""" # The name of the var. @@ -1316,21 +1331,6 @@ def range( if not TYPE_CHECKING: - def __getattribute__(self, name: str): - """Get an attribute of the var. - - Args: - name: The name of the attribute. - - Returns: - The attribute of the var. - - # noqa: DAR101 self - """ - if name == _PYDANTIC_VALIDATE_VALUES: - return _PYDANTIC_VALIDATOR - return super().__getattribute__(name) - def __getitem__(self, key: Any) -> Var: """Get the item from the var. From a610c271a33465ae5a15231a76ed8734abefc52a Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 13 May 2025 14:47:24 -0700 Subject: [PATCH 16/18] a bit simpler --- reflex/vars/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 0d8842c27fa..91d29afa109 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -379,9 +379,9 @@ def __setattr__(cls, name: str, value: Any): name: The name of the attribute. value: The value of the attribute. """ - if name == _PYDANTIC_VALIDATE_VALUES: - value = _pydantic_validator - super().__setattr__(name, value) + super().__setattr__( + name, value if name != _PYDANTIC_VALIDATE_VALUES else _pydantic_validator + ) @dataclasses.dataclass( From 37d82725b5640a623b92b1447f61bcb24721560f Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 16 May 2025 14:07:39 -0700 Subject: [PATCH 17/18] make _ as not js --- reflex/components/component.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 6db665e6321..664633e16d9 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -260,13 +260,15 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty if value is MISSING: value = ComponentField( - default=None, is_javascript=True, annotated_type=annotation + default=None, + is_javascript=(key[0] != "_"), + annotated_type=annotation, ) elif not isinstance(value, ComponentField): value = ComponentField( default=value, is_javascript=( - True + (key[0] != "_") if (existing_field := inherited_fields.get(key)) is None else existing_field.is_javascript ), From 365fc8df203a3010a8d207ae0afc4e3541b143a5 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 19 May 2025 18:11:32 -0700 Subject: [PATCH 18/18] add field_specifiers --- reflex/components/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 014d2e2ce41..c3a1f9e057c 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -202,7 +202,7 @@ def field( ) -@dataclass_transform(kw_only_default=True) +@dataclass_transform(kw_only_default=True, field_specifiers=(field,)) class BaseComponentMeta(ABCMeta): """Meta class for BaseComponent."""