Compare commits
12 Commits
main
...
masenf/mul
Author | SHA1 | Date | |
---|---|---|---|
![]() |
04211a3234 | ||
![]() |
e13ff71c2c | ||
![]() |
67624b48df | ||
![]() |
f3ef8b280e | ||
![]() |
86a1e8a07e | ||
![]() |
3121e8a7a9 | ||
![]() |
5fae1bb563 | ||
![]() |
c5f1a2fca9 | ||
![]() |
51bf447eb7 | ||
![]() |
5600bc5a30 | ||
![]() |
38eb84498b | ||
![]() |
90da81fd12 |
229
reflex/app.py
229
reflex/app.py
@ -7,7 +7,9 @@ import concurrent.futures
|
|||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
import functools
|
import functools
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
AsyncIterator,
|
AsyncIterator,
|
||||||
@ -37,6 +39,7 @@ from reflex.admin import AdminDash
|
|||||||
from reflex.base import Base
|
from reflex.base import Base
|
||||||
from reflex.compiler import compiler
|
from reflex.compiler import compiler
|
||||||
from reflex.compiler import utils as compiler_utils
|
from reflex.compiler import utils as compiler_utils
|
||||||
|
from reflex.compiler.compiler import ExecutorSafeFunctions
|
||||||
from reflex.components import connection_modal, connection_pulser
|
from reflex.components import connection_modal, connection_pulser
|
||||||
from reflex.components.base.app_wrap import AppWrap
|
from reflex.components.base.app_wrap import AppWrap
|
||||||
from reflex.components.base.fragment import Fragment
|
from reflex.components.base.fragment import Fragment
|
||||||
@ -754,6 +757,17 @@ class App(Base):
|
|||||||
TimeElapsedColumn(),
|
TimeElapsedColumn(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# try to be somewhat accurate - but still not 100%
|
||||||
|
adhoc_steps_without_executor = 6
|
||||||
|
fixed_pages_within_executor = 5
|
||||||
|
progress.start()
|
||||||
|
task = progress.add_task(
|
||||||
|
"Compiling:",
|
||||||
|
total=len(self.pages)
|
||||||
|
+ fixed_pages_within_executor
|
||||||
|
+ adhoc_steps_without_executor,
|
||||||
|
)
|
||||||
|
|
||||||
# Get the env mode.
|
# Get the env mode.
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
||||||
@ -769,6 +783,8 @@ class App(Base):
|
|||||||
# If a theme component was provided, wrap the app with it
|
# If a theme component was provided, wrap the app with it
|
||||||
app_wrappers[(20, "Theme")] = self.theme
|
app_wrappers[(20, "Theme")] = self.theme
|
||||||
|
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
# Fix up the style.
|
# Fix up the style.
|
||||||
self.style = evaluate_style_namespaces(self.style)
|
self.style = evaluate_style_namespaces(self.style)
|
||||||
|
|
||||||
@ -776,133 +792,150 @@ class App(Base):
|
|||||||
all_imports = {}
|
all_imports = {}
|
||||||
custom_components = set()
|
custom_components = set()
|
||||||
|
|
||||||
# Compile the pages in parallel.
|
for _route, component in self.pages.items():
|
||||||
with progress, concurrent.futures.ThreadPoolExecutor() as thread_pool:
|
# Merge the component style with the app style.
|
||||||
fixed_pages = 7
|
component.add_style(self.style)
|
||||||
task = progress.add_task("Compiling:", total=len(self.pages) + fixed_pages)
|
|
||||||
|
|
||||||
def mark_complete(_=None):
|
component.apply_theme(self.theme)
|
||||||
|
|
||||||
|
# Add component.get_imports() to all_imports.
|
||||||
|
all_imports.update(component.get_imports())
|
||||||
|
|
||||||
|
# Add the app wrappers from this component.
|
||||||
|
app_wrappers.update(component.get_app_wrap_components())
|
||||||
|
|
||||||
|
# Add the custom components from the page to the set.
|
||||||
|
custom_components |= component.get_custom_components()
|
||||||
|
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
# Perform auto-memoization of stateful components.
|
||||||
|
(
|
||||||
|
stateful_components_path,
|
||||||
|
stateful_components_code,
|
||||||
|
page_components,
|
||||||
|
) = compiler.compile_stateful_components(self.pages.values())
|
||||||
|
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
|
||||||
|
if code_uses_state_contexts(stateful_components_code) and self.state is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"To access rx.State in frontend components, at least one "
|
||||||
|
"subclass of rx.State must be defined in the app."
|
||||||
|
)
|
||||||
|
compile_results.append((stateful_components_path, stateful_components_code))
|
||||||
|
|
||||||
|
# Compile the root document before fork.
|
||||||
|
compile_results.append(
|
||||||
|
compiler.compile_document_root(
|
||||||
|
self.head_components,
|
||||||
|
html_lang=self.html_lang,
|
||||||
|
html_custom_attrs=self.html_custom_attrs, # type: ignore
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compile the contexts before fork.
|
||||||
|
compile_results.append(
|
||||||
|
compiler.compile_contexts(self.state, self.theme),
|
||||||
|
)
|
||||||
|
|
||||||
|
app_root = self._app_root(app_wrappers=app_wrappers)
|
||||||
|
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
# Prepopulate the global ExecutorSafeFunctions class with input data required by the compile functions.
|
||||||
|
# This is required for multiprocessing to work, in presence of non-picklable inputs.
|
||||||
|
for route, component in zip(self.pages, page_components):
|
||||||
|
ExecutorSafeFunctions.COMPILE_PAGE_ARGS_BY_ROUTE[route] = (
|
||||||
|
route,
|
||||||
|
component,
|
||||||
|
self.state,
|
||||||
|
)
|
||||||
|
|
||||||
|
ExecutorSafeFunctions.COMPILE_APP_APP_ROOT = app_root
|
||||||
|
ExecutorSafeFunctions.CUSTOM_COMPONENTS = custom_components
|
||||||
|
ExecutorSafeFunctions.STYLE = self.style
|
||||||
|
|
||||||
|
# Use a forking process pool, if possible. Much faster, especially for large sites.
|
||||||
|
# Fallback to ThreadPoolExecutor as something that will always work.
|
||||||
|
executor = None
|
||||||
|
if (
|
||||||
|
platform.system() in ("Linux", "Darwin")
|
||||||
|
and os.environ.get("REFLEX_COMPILE_PROCESSES") is not None
|
||||||
|
):
|
||||||
|
executor = concurrent.futures.ProcessPoolExecutor(
|
||||||
|
max_workers=int(os.environ.get("REFLEX_COMPILE_PROCESSES", 0)) or None,
|
||||||
|
mp_context=multiprocessing.get_context("fork"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
executor = concurrent.futures.ThreadPoolExecutor(
|
||||||
|
max_workers=int(os.environ.get("REFLEX_COMPILE_THREADS", 0)) or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with executor:
|
||||||
|
result_futures = []
|
||||||
|
custom_components_future = None
|
||||||
|
|
||||||
|
def _mark_complete(_=None):
|
||||||
progress.advance(task)
|
progress.advance(task)
|
||||||
|
|
||||||
for _route, component in self.pages.items():
|
def _submit_work(fn, *args, **kwargs):
|
||||||
# Merge the component style with the app style.
|
f = executor.submit(fn, *args, **kwargs)
|
||||||
component.add_style(self.style)
|
f.add_done_callback(_mark_complete)
|
||||||
|
|
||||||
component.apply_theme(self.theme)
|
|
||||||
|
|
||||||
# Add component.get_imports() to all_imports.
|
|
||||||
all_imports.update(component.get_imports())
|
|
||||||
|
|
||||||
# Add the app wrappers from this component.
|
|
||||||
app_wrappers.update(component.get_app_wrap_components())
|
|
||||||
|
|
||||||
# Add the custom components from the page to the set.
|
|
||||||
custom_components |= component.get_custom_components()
|
|
||||||
|
|
||||||
# Perform auto-memoization of stateful components.
|
|
||||||
(
|
|
||||||
stateful_components_path,
|
|
||||||
stateful_components_code,
|
|
||||||
page_components,
|
|
||||||
) = compiler.compile_stateful_components(self.pages.values())
|
|
||||||
|
|
||||||
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
|
|
||||||
if (
|
|
||||||
code_uses_state_contexts(stateful_components_code)
|
|
||||||
and self.state is None
|
|
||||||
):
|
|
||||||
raise RuntimeError(
|
|
||||||
"To access rx.State in frontend components, at least one "
|
|
||||||
"subclass of rx.State must be defined in the app."
|
|
||||||
)
|
|
||||||
compile_results.append((stateful_components_path, stateful_components_code))
|
|
||||||
|
|
||||||
result_futures = []
|
|
||||||
|
|
||||||
def submit_work(fn, *args, **kwargs):
|
|
||||||
"""Submit work to the thread pool and add a callback to mark the task as complete.
|
|
||||||
|
|
||||||
The Future will be added to the `result_futures` list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fn: The function to submit.
|
|
||||||
*args: The args to submit.
|
|
||||||
**kwargs: The kwargs to submit.
|
|
||||||
"""
|
|
||||||
f = thread_pool.submit(fn, *args, **kwargs)
|
|
||||||
f.add_done_callback(mark_complete)
|
|
||||||
result_futures.append(f)
|
result_futures.append(f)
|
||||||
|
|
||||||
# Compile all page components.
|
# Compile all page components.
|
||||||
for route, component in zip(self.pages, page_components):
|
for route in self.pages:
|
||||||
submit_work(
|
_submit_work(ExecutorSafeFunctions.compile_page, route)
|
||||||
compiler.compile_page,
|
|
||||||
route,
|
|
||||||
component,
|
|
||||||
self.state,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compile the app wrapper.
|
# Compile the app wrapper.
|
||||||
app_root = self._app_root(app_wrappers=app_wrappers)
|
_submit_work(ExecutorSafeFunctions.compile_app)
|
||||||
submit_work(compiler.compile_app, app_root)
|
|
||||||
|
|
||||||
# Compile the custom components.
|
# Compile the custom components.
|
||||||
submit_work(compiler.compile_components, custom_components)
|
custom_components_future = executor.submit(
|
||||||
|
ExecutorSafeFunctions.compile_custom_components,
|
||||||
|
)
|
||||||
|
custom_components_future.add_done_callback(_mark_complete)
|
||||||
|
|
||||||
# Compile the root stylesheet with base styles.
|
# Compile the root stylesheet with base styles.
|
||||||
submit_work(compiler.compile_root_stylesheet, self.stylesheets)
|
_submit_work(compiler.compile_root_stylesheet, self.stylesheets)
|
||||||
|
|
||||||
# Compile the root document.
|
|
||||||
submit_work(
|
|
||||||
compiler.compile_document_root,
|
|
||||||
self.head_components,
|
|
||||||
html_lang=self.html_lang,
|
|
||||||
html_custom_attrs=self.html_custom_attrs,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compile the theme.
|
# Compile the theme.
|
||||||
submit_work(compiler.compile_theme, style=self.style)
|
_submit_work(ExecutorSafeFunctions.compile_theme)
|
||||||
|
|
||||||
# Compile the contexts.
|
|
||||||
submit_work(compiler.compile_contexts, self.state, self.theme)
|
|
||||||
|
|
||||||
# Compile the Tailwind config.
|
# Compile the Tailwind config.
|
||||||
if config.tailwind is not None:
|
if config.tailwind is not None:
|
||||||
config.tailwind["content"] = config.tailwind.get(
|
config.tailwind["content"] = config.tailwind.get(
|
||||||
"content", constants.Tailwind.CONTENT
|
"content", constants.Tailwind.CONTENT
|
||||||
)
|
)
|
||||||
submit_work(compiler.compile_tailwind, config.tailwind)
|
_submit_work(compiler.compile_tailwind, config.tailwind)
|
||||||
else:
|
else:
|
||||||
submit_work(compiler.remove_tailwind_from_postcss)
|
_submit_work(compiler.remove_tailwind_from_postcss)
|
||||||
|
|
||||||
# Get imports from AppWrap components.
|
|
||||||
all_imports.update(app_root.get_imports())
|
|
||||||
|
|
||||||
# Iterate through all the custom components and add their imports to the all_imports.
|
|
||||||
for component in custom_components:
|
|
||||||
all_imports.update(component.get_imports())
|
|
||||||
|
|
||||||
# Wait for all compilation tasks to complete.
|
# Wait for all compilation tasks to complete.
|
||||||
for future in concurrent.futures.as_completed(result_futures):
|
for future in concurrent.futures.as_completed(result_futures):
|
||||||
compile_results.append(future.result())
|
compile_results.append(future.result())
|
||||||
|
|
||||||
# Empty the .web pages directory.
|
# Special case for custom_components, since we need the compiled imports
|
||||||
compiler.purge_web_pages_dir()
|
# to install proper frontend packages.
|
||||||
|
*custom_components_result, custom_components_imports = custom_components_future.result()
|
||||||
|
compile_results.append(custom_components_result)
|
||||||
|
all_imports.update(custom_components_imports)
|
||||||
|
|
||||||
# Avoid flickering when installing frontend packages
|
progress.advance(task)
|
||||||
progress.stop()
|
|
||||||
|
|
||||||
# Install frontend packages.
|
# Empty the .web pages directory.
|
||||||
self.get_frontend_packages(all_imports)
|
compiler.purge_web_pages_dir()
|
||||||
|
|
||||||
# Write the pages at the end to trigger the NextJS hot reload only once.
|
progress.advance(task)
|
||||||
write_page_futures = []
|
progress.stop()
|
||||||
for output_path, code in compile_results:
|
|
||||||
write_page_futures.append(
|
# Install frontend packages.
|
||||||
thread_pool.submit(compiler_utils.write_page, output_path, code)
|
self.get_frontend_packages(all_imports)
|
||||||
)
|
|
||||||
for future in concurrent.futures.as_completed(write_page_futures):
|
for output_path, code in compile_results:
|
||||||
future.result()
|
compiler_utils.write_page(output_path, code)
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
||||||
|
@ -67,7 +67,7 @@ def _compile_theme(theme: dict) -> str:
|
|||||||
return templates.THEME.render(theme=theme)
|
return templates.THEME.render(theme=theme)
|
||||||
|
|
||||||
|
|
||||||
def _compile_contexts(state: Optional[Type[BaseState]], theme: Component) -> str:
|
def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) -> str:
|
||||||
"""Compile the initial state and contexts.
|
"""Compile the initial state and contexts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -186,7 +186,9 @@ def _compile_component(component: Component) -> str:
|
|||||||
return templates.COMPONENT.render(component=component)
|
return templates.COMPONENT.render(component=component)
|
||||||
|
|
||||||
|
|
||||||
def _compile_components(components: set[CustomComponent]) -> str:
|
def _compile_components(
|
||||||
|
components: set[CustomComponent],
|
||||||
|
) -> tuple[str, Dict[str, list[ImportVar]]]:
|
||||||
"""Compile the components.
|
"""Compile the components.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -208,9 +210,12 @@ def _compile_components(components: set[CustomComponent]) -> str:
|
|||||||
imports = utils.merge_imports(imports, component_imports)
|
imports = utils.merge_imports(imports, component_imports)
|
||||||
|
|
||||||
# Compile the components page.
|
# Compile the components page.
|
||||||
return templates.COMPONENTS.render(
|
return (
|
||||||
imports=utils.compile_imports(imports),
|
templates.COMPONENTS.render(
|
||||||
components=component_renders,
|
imports=utils.compile_imports(imports),
|
||||||
|
components=component_renders,
|
||||||
|
),
|
||||||
|
imports,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -363,7 +368,7 @@ def compile_theme(style: ComponentStyle) -> tuple[str, str]:
|
|||||||
|
|
||||||
def compile_contexts(
|
def compile_contexts(
|
||||||
state: Optional[Type[BaseState]],
|
state: Optional[Type[BaseState]],
|
||||||
theme: Component,
|
theme: Component | None,
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""Compile the initial state / context.
|
"""Compile the initial state / context.
|
||||||
|
|
||||||
@ -401,7 +406,9 @@ def compile_page(
|
|||||||
return output_path, code
|
return output_path, code
|
||||||
|
|
||||||
|
|
||||||
def compile_components(components: set[CustomComponent]):
|
def compile_components(
|
||||||
|
components: set[CustomComponent],
|
||||||
|
) -> tuple[str, str, Dict[str, list[ImportVar]]]:
|
||||||
"""Compile the custom components.
|
"""Compile the custom components.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -414,8 +421,8 @@ def compile_components(components: set[CustomComponent]):
|
|||||||
output_path = utils.get_components_path()
|
output_path = utils.get_components_path()
|
||||||
|
|
||||||
# Compile the components.
|
# Compile the components.
|
||||||
code = _compile_components(components)
|
code, imports = _compile_components(components)
|
||||||
return output_path, code
|
return output_path, code, imports
|
||||||
|
|
||||||
|
|
||||||
def compile_stateful_components(
|
def compile_stateful_components(
|
||||||
@ -487,3 +494,88 @@ def purge_web_pages_dir():
|
|||||||
|
|
||||||
# Empty out the web pages directory.
|
# Empty out the web pages directory.
|
||||||
utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"])
|
utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"])
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutorSafeFunctions:
|
||||||
|
"""Helper class to allow parallelisation of parts of the compilation process.
|
||||||
|
|
||||||
|
This class (and its class attributes) are available at global scope.
|
||||||
|
|
||||||
|
In a multiprocessing context (like when using a ProcessPoolExecutor), the content of this
|
||||||
|
global class is logically replicated to any FORKED process.
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
* Before the child process is forked, ensure that we stash any input data required by any future
|
||||||
|
function call in the child process.
|
||||||
|
* After the child process is forked, the child process will have a copy of the global class, which
|
||||||
|
includes the previously stashed input data.
|
||||||
|
* Any task submitted to the child process simply needs a way to communicate which input data the
|
||||||
|
requested function call requires.
|
||||||
|
|
||||||
|
Why do we need this? Passing input data directly to child process often not possible because the input data is not picklable.
|
||||||
|
The mechanic described here removes the need to pickle the input data at all.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
* This can never support returning unpicklable OUTPUT data.
|
||||||
|
* Any object mutations done by the child process will not propagate back to the parent process (fork goes one way!).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPILE_PAGE_ARGS_BY_ROUTE = {}
|
||||||
|
COMPILE_APP_APP_ROOT: Component | None = None
|
||||||
|
CUSTOM_COMPONENTS: set[CustomComponent] | None = None
|
||||||
|
STYLE: ComponentStyle | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compile_page(cls, route: str):
|
||||||
|
"""Compile a page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
route: The route of the page to compile.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The path and code of the compiled page.
|
||||||
|
"""
|
||||||
|
return compile_page(*cls.COMPILE_PAGE_ARGS_BY_ROUTE[route])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compile_app(cls):
|
||||||
|
"""Compile the app.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The path and code of the compiled app.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the app root is not set.
|
||||||
|
"""
|
||||||
|
if cls.COMPILE_APP_APP_ROOT is None:
|
||||||
|
raise ValueError("COMPILE_APP_APP_ROOT should be set")
|
||||||
|
return compile_app(cls.COMPILE_APP_APP_ROOT)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compile_custom_components(cls):
|
||||||
|
"""Compile the custom components.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The path and code of the compiled custom components.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the custom components are not set.
|
||||||
|
"""
|
||||||
|
if cls.CUSTOM_COMPONENTS is None:
|
||||||
|
raise ValueError("CUSTOM_COMPONENTS should be set")
|
||||||
|
return compile_components(cls.CUSTOM_COMPONENTS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compile_theme(cls):
|
||||||
|
"""Compile the theme.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The path and code of the compiled theme.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the style is not set.
|
||||||
|
"""
|
||||||
|
if cls.STYLE is None:
|
||||||
|
raise ValueError("STYLE should be set")
|
||||||
|
return compile_theme(cls.STYLE)
|
||||||
|
@ -1265,6 +1265,9 @@ class CustomComponent(Component):
|
|||||||
# The props of the component.
|
# The props of the component.
|
||||||
props: Dict[str, Any] = {}
|
props: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Props that reference other components.
|
||||||
|
component_props: Dict[str, Component] = {}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize the custom component.
|
"""Initialize the custom component.
|
||||||
|
|
||||||
@ -1296,17 +1299,13 @@ class CustomComponent(Component):
|
|||||||
self.props[format.to_camel_case(key)] = value
|
self.props[format.to_camel_case(key)] = value
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Convert the type to a Var, then get the type of the var.
|
|
||||||
if not types._issubclass(type_, Var):
|
|
||||||
type_ = Var[type_]
|
|
||||||
type_ = types.get_args(type_)[0]
|
|
||||||
|
|
||||||
# Handle subclasses of Base.
|
# Handle subclasses of Base.
|
||||||
if types._issubclass(type_, Base):
|
if isinstance(value, Base):
|
||||||
base_value = Var.create(value)
|
base_value = Var.create(value)
|
||||||
|
|
||||||
# Track hooks and imports associated with Component instances.
|
# Track hooks and imports associated with Component instances.
|
||||||
if base_value is not None and types._issubclass(type_, Component):
|
if base_value is not None and isinstance(value, Component):
|
||||||
|
self.component_props[key] = value
|
||||||
value = base_value._replace(
|
value = base_value._replace(
|
||||||
merge_var_data=VarData( # type: ignore
|
merge_var_data=VarData( # type: ignore
|
||||||
imports=value.get_imports(),
|
imports=value.get_imports(),
|
||||||
@ -1373,6 +1372,16 @@ class CustomComponent(Component):
|
|||||||
custom_components |= self.get_component(self).get_custom_components(
|
custom_components |= self.get_component(self).get_custom_components(
|
||||||
seen=seen
|
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_custom_components(seen=seen)
|
||||||
return custom_components
|
return custom_components
|
||||||
|
|
||||||
def _render(self) -> Tag:
|
def _render(self) -> Tag:
|
||||||
|
@ -4,6 +4,7 @@ import pytest
|
|||||||
|
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
from reflex.base import Base
|
from reflex.base import Base
|
||||||
|
from reflex.compiler.compiler import compile_components
|
||||||
from reflex.components.base.bare import Bare
|
from reflex.components.base.bare import Bare
|
||||||
from reflex.components.chakra.layout.box import Box
|
from reflex.components.chakra.layout.box import Box
|
||||||
from reflex.components.component import (
|
from reflex.components.component import (
|
||||||
@ -1269,3 +1270,41 @@ def test_deprecated_props(capsys):
|
|||||||
assert "type={`type1`}" in c2_1_render["props"]
|
assert "type={`type1`}" in c2_1_render["props"]
|
||||||
assert "min={`min1`}" in c2_1_render["props"]
|
assert "min={`min1`}" in c2_1_render["props"]
|
||||||
assert "max={`max1`}" in c2_1_render["props"]
|
assert "max={`max1`}" in c2_1_render["props"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_component_get_imports():
|
||||||
|
class Inner(Component):
|
||||||
|
tag = "Inner"
|
||||||
|
library = "inner"
|
||||||
|
|
||||||
|
class Other(Component):
|
||||||
|
tag = "Other"
|
||||||
|
library = "other"
|
||||||
|
|
||||||
|
@rx.memo
|
||||||
|
def wrapper():
|
||||||
|
return Inner.create()
|
||||||
|
|
||||||
|
@rx.memo
|
||||||
|
def outer(c: Component):
|
||||||
|
return Other.create(c)
|
||||||
|
|
||||||
|
custom_comp = wrapper()
|
||||||
|
|
||||||
|
# Inner is not imported directly, but it is imported by the custom component.
|
||||||
|
assert "inner" not in custom_comp.get_imports()
|
||||||
|
|
||||||
|
# The imports are only resolved during compilation.
|
||||||
|
_, _, imports_inner = compile_components(custom_comp.get_custom_components())
|
||||||
|
assert "inner" in imports_inner
|
||||||
|
|
||||||
|
outer_comp = outer(c=wrapper())
|
||||||
|
|
||||||
|
# Libraries are not imported directly, but are imported by the custom component.
|
||||||
|
assert "inner" not in outer_comp.get_imports()
|
||||||
|
assert "other" not in outer_comp.get_imports()
|
||||||
|
|
||||||
|
# The imports are only resolved during compilation.
|
||||||
|
_, _, imports_outer = compile_components(outer_comp.get_custom_components())
|
||||||
|
assert "inner" in imports_outer
|
||||||
|
assert "other" in imports_outer
|
||||||
|
Loading…
Reference in New Issue
Block a user