Compare commits

...

9 Commits

Author SHA1 Message Date
Masen Furer
fe4a4ee81b
Fixup deduped package names in collapse() 2024-04-30 14:58:38 -07:00
Masen Furer
feb74eef7a
py38 compatibility 2024-04-30 14:34:26 -07:00
Masen Furer
79c86f7762
markdown.pyi: remove unused import 2024-04-30 13:58:35 -07:00
Masen Furer
e222d17d2c
Ensure that conflicting packages are not installed
Allow the user to override version pins globally via Config.frontend_packages
2024-04-30 13:57:09 -07:00
Masen Furer
8643ccb758
Fixes from testing on reflex-web 2024-04-30 13:34:12 -07:00
Masen Furer
622f0f0dc8
fix pininput to use _get_imports_list 2024-04-30 12:52:50 -07:00
Masen Furer
3e41095df0
fix vars.pyi manually 2024-04-30 12:52:14 -07:00
Masen Furer
3423fec2a6
Pass static checks 2024-04-30 12:24:36 -07:00
Masen Furer
35252464a0
WiP: use ImportList internally instead of ImportDict
* deprecate `_get_imports` in favor of new `_get_imports_list`
* `_get_all_imports` now returns an `ImportList`
* Compiler uses `ImportList.collapse` to get an `ImportDict`
2024-04-29 19:05:52 -07:00
26 changed files with 639 additions and 313 deletions

View File

@ -79,7 +79,7 @@ from reflex.state import (
)
from reflex.utils import console, exceptions, format, prerequisites, types
from reflex.utils.exec import is_testing_env, should_skip_compile
from reflex.utils.imports import ImportVar
from reflex.utils.imports import ImportList, split_library_name_version
# Define custom types.
ComponentCallable = Callable[[], Component]
@ -618,27 +618,17 @@ class App(Base):
admin.mount_to(self.api)
def get_frontend_packages(self, imports: Dict[str, set[ImportVar]]):
def get_frontend_packages(self, imports: ImportList):
"""Gets the frontend packages to be installed and filters out the unnecessary ones.
Args:
imports: A dictionary containing the imports used in the current page.
imports: A list containing the imports used in the current page.
Example:
>>> get_frontend_packages({"react": "16.14.0", "react-dom": "16.14.0"})
"""
page_imports = {
i
for i, tags in imports.items()
if i
not in [
*constants.PackageJson.DEPENDENCIES.keys(),
*constants.PackageJson.DEV_DEPENDENCIES.keys(),
]
and not any(i.startswith(prefix) for prefix in ["/", ".", "next/"])
and i != ""
and any(tag.install for tag in tags)
}
page_imports = ImportList(i for i in imports if i.install and i.package)
inferred_libraries = [i.library for i in page_imports]
frontend_packages = get_config().frontend_packages
_frontend_packages = []
for package in frontend_packages:
@ -647,14 +637,21 @@ class App(Base):
f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
)
continue
if package in page_imports:
lib, version = split_library_name_version(package)
if (
lib in inferred_libraries
and version is None
or version == page_imports[inferred_libraries.index(lib)].version
):
console.warn(
f"React packages and their dependencies are inferred from Component.library and Component.lib_dependencies, remove `{package}` from `frontend_packages`"
)
continue
_frontend_packages.append(package)
page_imports.update(_frontend_packages)
prerequisites.install_frontend_packages(page_imports, get_config())
page_imports.extend(_frontend_packages)
prerequisites.install_frontend_packages(
set(page_imports.collapse()), get_config()
)
def _app_root(self, app_wrappers: dict[tuple[int, str], Component]) -> Component:
for component in tuple(app_wrappers.values()):
@ -794,7 +791,7 @@ class App(Base):
self.style = evaluate_style_namespaces(self.style)
# Track imports and custom components found.
all_imports = {}
all_imports = ImportList()
custom_components = set()
for _route, component in self.pages.items():
@ -804,7 +801,7 @@ class App(Base):
component.apply_theme(self.theme)
# Add component._get_all_imports() to all_imports.
all_imports.update(component._get_all_imports())
all_imports.extend(component._get_all_imports())
# Add the app wrappers from this component.
app_wrappers.update(component._get_all_app_wrap_components())
@ -932,10 +929,10 @@ class App(Base):
custom_components_imports,
) = custom_components_future.result()
compile_results.append(custom_components_result)
all_imports.update(custom_components_imports)
all_imports.extend(custom_components_imports)
# Get imports from AppWrap components.
all_imports.update(app_root._get_all_imports())
all_imports.extend(app_root._get_all_imports())
progress.advance(task)
@ -951,7 +948,7 @@ class App(Base):
# Setup the next.config.js
transpile_packages = [
package
for package, import_vars in all_imports.items()
for package, import_vars in all_imports.collapse().items()
if any(import_var.transpile for import_var in import_vars)
]
prerequisites.update_next_config(

View File

@ -19,7 +19,7 @@ from reflex.config import get_config
from reflex.state import BaseState
from reflex.style import LIGHT_COLOR_MODE
from reflex.utils.exec import is_prod_mode
from reflex.utils.imports import ImportVar
from reflex.utils.imports import ImportList, ImportVar
from reflex.vars import Var
@ -188,7 +188,7 @@ def _compile_component(component: Component) -> str:
def _compile_components(
components: set[CustomComponent],
) -> tuple[str, Dict[str, list[ImportVar]]]:
) -> tuple[str, ImportList]:
"""Compile the components.
Args:
@ -197,25 +197,34 @@ def _compile_components(
Returns:
The compiled components.
"""
imports = {
"react": [ImportVar(tag="memo")],
f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="E"), ImportVar(tag="isTrue")],
}
_imports = ImportList(
[
ImportVar(package="react", tag="memo"),
ImportVar(
package=f"/{constants.Dirs.STATE_PATH}",
tag="E",
),
ImportVar(
package=f"/{constants.Dirs.STATE_PATH}",
tag="isTrue",
),
]
)
component_renders = []
# Compile each component.
for component in components:
component_render, component_imports = utils.compile_custom_component(component)
component_renders.append(component_render)
imports = utils.merge_imports(imports, component_imports)
_imports.extend(component_imports)
# Compile the components page.
return (
templates.COMPONENTS.render(
imports=utils.compile_imports(imports),
imports=utils.compile_imports(_imports),
components=component_renders,
),
imports,
_imports,
)
@ -235,7 +244,7 @@ def _compile_stateful_components(
Returns:
The rendered stateful components code.
"""
all_import_dicts = []
all_imports = []
rendered_components = {}
def get_shared_components_recursive(component: BaseComponent):
@ -266,7 +275,7 @@ def _compile_stateful_components(
rendered_components.update(
{code: None for code in component._get_all_custom_code()},
)
all_import_dicts.append(component._get_all_imports())
all_imports.extend(component._get_all_imports())
# Indicate that this component now imports from the shared file.
component.rendered_as_shared = True
@ -275,9 +284,11 @@ def _compile_stateful_components(
get_shared_components_recursive(page_component)
# Don't import from the file that we're about to create.
all_imports = utils.merge_imports(*all_import_dicts)
all_imports.pop(
f"/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}", None
all_imports = ImportList(
imp
for imp in all_imports
if imp.library
!= f"/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}"
)
return templates.STATEFUL_COMPONENTS.render(
@ -408,7 +419,7 @@ def compile_page(
def compile_components(
components: set[CustomComponent],
) -> tuple[str, str, Dict[str, list[ImportVar]]]:
) -> tuple[str, str, ImportList]:
"""Compile the custom components.
Args:

View File

@ -88,16 +88,16 @@ def validate_imports(import_dict: imports.ImportDict):
used_tags[import_name] = lib
def compile_imports(import_dict: imports.ImportDict) -> list[dict]:
"""Compile an import dict.
def compile_imports(import_list: imports.ImportList) -> list[dict]:
"""Compile an import list.
Args:
import_dict: The import dict to compile.
import_list: The import list to compile.
Returns:
The list of import dict.
The list of template import dict.
"""
collapsed_import_dict = imports.collapse_imports(import_dict)
collapsed_import_dict = import_list.collapse()
validate_imports(collapsed_import_dict)
import_dicts = []
for lib, fields in collapsed_import_dict.items():
@ -114,9 +114,6 @@ def compile_imports(import_dict: imports.ImportDict) -> list[dict]:
import_dicts.append(get_import_dict(module))
continue
# remove the version before rendering the package imports
lib = format.format_library_name(lib)
import_dicts.append(get_import_dict(lib, default, rest))
return import_dicts
@ -237,7 +234,7 @@ def compile_client_storage(state: Type[BaseState]) -> dict[str, dict]:
def compile_custom_component(
component: CustomComponent,
) -> tuple[dict, imports.ImportDict]:
) -> tuple[dict, imports.ImportList]:
"""Compile a custom component.
Args:
@ -250,11 +247,12 @@ def compile_custom_component(
render = component.get_component(component)
# Get the imports.
imports = {
lib: fields
for lib, fields in render._get_all_imports().items()
if lib != component.library
}
component_library_name = format.format_library_name(component.library or "")
_imports = imports.ImportList(
imp
for imp in render._get_all_imports()
if imp.library != component_library_name
)
# Concatenate the props.
props = [prop._var_name for prop in component.get_prop_vars()]
@ -268,7 +266,7 @@ def compile_custom_component(
"hooks": {**render._get_all_hooks_internal(), **render._get_all_hooks()},
"custom_code": render._get_all_custom_code(),
},
imports,
_imports,
)

View File

@ -35,19 +35,18 @@ class ChakraComponent(Component):
@classmethod
@lru_cache(maxsize=None)
def _get_dependencies_imports(cls) -> imports.ImportDict:
def _get_dependencies_imports(cls) -> List[imports.ImportVar]:
"""Get the imports from lib_dependencies for installing.
Returns:
The dependencies imports of the component.
"""
return {
dep: [imports.ImportVar(tag=None, render=False)]
for dep in [
"@chakra-ui/system@2.5.7",
"framer-motion@10.16.4",
]
}
return [
imports.ImportVar(
package="@chakra-ui/system@2.5.7", tag=None, render=False
),
imports.ImportVar(package="framer-motion@10.16.4", tag=None, render=False),
]
class ChakraProvider(ChakraComponent):
@ -68,13 +67,21 @@ class ChakraProvider(ChakraComponent):
theme=Var.create("extendTheme(theme)", _var_is_local=False),
)
def _get_imports(self) -> imports.ImportDict:
_imports = super()._get_imports()
_imports.setdefault(self.__fields__["library"].default, []).append(
imports.ImportVar(tag="extendTheme", is_default=False),
)
_imports.setdefault("/utils/theme.js", []).append(
imports.ImportVar(tag="theme", is_default=True),
def _get_imports_list(self) -> List[imports.ImportVar]:
_imports = super()._get_imports_list()
_imports.extend(
[
imports.ImportVar(
package=self.__fields__["library"].default,
tag="extendTheme",
is_default=False,
),
imports.ImportVar(
package="/utils/theme.js",
tag="theme",
is_default=True,
),
],
)
return _imports

View File

@ -9,7 +9,7 @@ from reflex.components.component import Component
from reflex.components.tags.tag import Tag
from reflex.constants import EventTriggers
from reflex.utils import format
from reflex.utils.imports import ImportDict, merge_imports
from reflex.utils.imports import ImportVar
from reflex.vars import Var
@ -63,18 +63,18 @@ class PinInput(ChakraComponent):
# The name of the form field
name: Var[str]
def _get_imports(self) -> ImportDict:
def _get_imports_list(self) -> list[ImportVar]:
"""Include PinInputField explicitly because it may not be a child component at compile time.
Returns:
The merged import dict.
"""
range_var = Var.range(0)
return merge_imports(
super()._get_imports(),
PinInputField()._get_all_imports(), # type: ignore
range_var._var_data.imports if range_var._var_data is not None else {},
)
return [
*super()._get_imports_list(),
*PinInputField()._get_all_imports(), # type: ignore
*(range_var._var_data.imports if range_var._var_data is not None else []),
]
def get_event_triggers(self) -> dict[str, Union[Var, Any]]:
"""Get the event triggers that pass the component's value to the handler.

View File

@ -13,7 +13,7 @@ from reflex.components.component import Component
from reflex.components.tags.tag import Tag
from reflex.constants import EventTriggers
from reflex.utils import format
from reflex.utils.imports import ImportDict, merge_imports
from reflex.utils.imports import ImportVar
from reflex.vars import Var
class PinInput(ChakraComponent):

View File

@ -1,5 +1,6 @@
"""A link component."""
from __future__ import annotations
from reflex.components.chakra import ChakraComponent
from reflex.components.component import Component
@ -30,8 +31,8 @@ class Link(ChakraComponent):
# If true, the link will open in new tab.
is_external: Var[bool]
def _get_imports(self) -> imports.ImportDict:
return {**super()._get_imports(), **next_link._get_imports()}
def _get_imports_list(self) -> list[imports.ImportVar]:
return [*super()._get_imports_list(), *next_link._get_imports_list()]
@classmethod
def create(cls, *children, **props) -> Component:

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import copy
import itertools
import typing
from abc import ABC, abstractmethod
from functools import lru_cache, wraps
@ -95,11 +96,11 @@ class BaseComponent(Base, ABC):
"""
@abstractmethod
def _get_all_imports(self) -> imports.ImportDict:
def _get_all_imports(self) -> imports.ImportList:
"""Get all the libraries and fields that are used by the component.
Returns:
The import dict with the required imports.
The list of all required ImportVar.
"""
@abstractmethod
@ -994,17 +995,22 @@ class Component(BaseComponent, ABC):
# Return the dynamic imports
return dynamic_imports
def _get_props_imports(self) -> List[str]:
def _get_props_imports(self) -> imports.ImportList:
"""Get the imports needed for components props.
Returns:
The imports for the components props of the component.
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
]
return imports.ImportList(
sum(
(
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.
@ -1020,97 +1026,133 @@ class Component(BaseComponent, ABC):
or format.format_library_name(dep or "") in self.transpile_packages
)
def _get_dependencies_imports(self) -> imports.ImportDict:
def _get_dependencies_imports(self) -> List[ImportVar]:
"""Get the imports from lib_dependencies for installing.
Returns:
The dependencies imports of the component.
"""
return {
dep: [
ImportVar(
tag=None,
render=False,
transpile=self._should_transpile(dep),
)
]
return imports.ImportList(
ImportVar(
package=dep,
tag=None,
render=False,
transpile=self._should_transpile(dep),
)
for dep in self.lib_dependencies
}
)
def _get_hooks_imports(self) -> imports.ImportDict:
def _get_hooks_imports(self) -> imports.ImportList:
"""Get the imports required by certain hooks.
Returns:
The imports required for all selected hooks.
"""
_imports = {}
_imports = imports.ImportList()
if self._get_ref_hook():
# Handle hooks needed for attaching react refs to DOM nodes.
_imports.setdefault("react", set()).add(ImportVar(tag="useRef"))
_imports.setdefault(f"/{Dirs.STATE_PATH}", set()).add(ImportVar(tag="refs"))
_imports.extend(
[
ImportVar(package="react", tag="useRef"),
ImportVar(package=f"/{Dirs.STATE_PATH}", tag="refs"),
]
)
if self._get_mount_lifecycle_hook():
# Handle hooks for `on_mount` / `on_unmount`.
_imports.setdefault("react", set()).add(ImportVar(tag="useEffect"))
_imports.append(ImportVar(package="react", tag="useEffect"))
if self._get_special_hooks():
# Handle additional internal hooks (autofocus, etc).
_imports.setdefault("react", set()).update(
{
ImportVar(tag="useRef"),
ImportVar(tag="useEffect"),
},
_imports.extend(
[
ImportVar(package="react", tag="useEffect"),
ImportVar(package="react", tag="useRef"),
]
)
user_hooks = self._get_hooks()
if user_hooks is not None and isinstance(user_hooks, Var):
_imports = imports.merge_imports(_imports, user_hooks._var_data.imports) # type: ignore
if (
user_hooks is not None
and isinstance(user_hooks, Var)
and user_hooks._var_data is not None
):
_imports.extend(user_hooks._var_data.imports)
return _imports
def _get_imports(self) -> imports.ImportDict:
"""Get all the libraries and fields that are used by the component.
"""Deprecated method to get all the libraries and fields used by the component.
Returns:
The imports needed by the component.
"""
_imports = {}
return {}
def _get_imports_list(self) -> List[ImportVar]:
"""Internal method to get the imports as a list.
Returns:
The imports as a list.
"""
_imports = imports.ImportList(
itertools.chain(
self._get_props_imports(),
self._get_dependencies_imports(),
self._get_hooks_imports(),
)
)
# Handle deprecated _get_imports
import_dict = self._get_imports()
if import_dict:
console.deprecate(
feature_name="_get_imports",
reason="use add_imports instead",
deprecation_version="0.5.0",
removal_version="0.6.0",
)
_imports.extend(imports.ImportList.from_import_dict(import_dict))
# Import this component's tag from the main library.
if self.library is not None and self.tag is not None:
_imports[self.library] = {self.import_var}
_imports.append(self.import_var)
# Get static imports required for event processing.
event_imports = Imports.EVENTS if self.event_triggers else {}
if self.event_triggers:
_imports.extend(Imports.EVENTS)
# Collect imports from Vars used directly by this component.
var_imports = [
var._var_data.imports for var in self._get_vars() if var._var_data
]
for var in self._get_vars():
if var._var_data:
_imports.extend(var._var_data.imports)
return _imports
return imports.merge_imports(
*self._get_props_imports(),
self._get_dependencies_imports(),
self._get_hooks_imports(),
_imports,
event_imports,
*var_imports,
)
def _get_all_imports(self, collapse: bool = False) -> imports.ImportDict:
def _get_all_imports(self, collapse: bool = False) -> imports.ImportList:
"""Get all the libraries and fields that are used by the component and its children.
Args:
collapse: Whether to collapse the imports by removing duplicates.
collapse: Whether to collapse the imports into a dict (deprecated).
Returns:
The import dict with the required imports.
The list of all required imports.
"""
_imports = imports.merge_imports(
self._get_imports(), *[child._get_all_imports() for child in self.children]
_imports = imports.ImportList(
self._get_imports_list()
+ sum((child._get_all_imports() for child in self.children), [])
)
return imports.collapse_imports(_imports) if collapse else _imports
if collapse:
console.deprecate(
feature_name="collapse kwarg to _get_all_imports",
reason="use ImportList.collapse instead",
deprecation_version="0.5.0",
removal_version="0.6.0",
)
return _imports.collapse() # type: ignore
return _imports
def _get_mount_lifecycle_hook(self) -> str | None:
"""Generate the component lifecycle hook.
@ -1296,6 +1338,7 @@ class Component(BaseComponent, ABC):
tag = self.tag.partition(".")[0] if self.tag else None
alias = self.alias.partition(".")[0] if self.alias else None
return ImportVar(
package=self.library,
tag=tag,
is_default=self.is_default,
alias=alias,
@ -1550,33 +1593,29 @@ memo = custom_component
class NoSSRComponent(Component):
"""A dynamic component that is not rendered on the server."""
def _get_imports(self) -> imports.ImportDict:
def _get_imports_list(self) -> list[ImportVar]:
"""Get the imports for the component.
Returns:
The imports for dynamically importing the component at module load time.
"""
# Next.js dynamic import mechanism.
dynamic_import = {"next/dynamic": [ImportVar(tag="dynamic", is_default=True)]}
return [
*super()._get_imports_list(),
# Next.js dynamic import mechanism.
ImportVar(package="next/dynamic", tag="dynamic", is_default=True),
]
# The normal imports for this component.
_imports = super()._get_imports()
@property
def import_var(self) -> ImportVar:
"""Will not actually render the tag to import, get it dynamically instead.
# Do NOT import the main library/tag statically.
if self.library is not None:
_imports[self.library] = [
imports.ImportVar(
tag=None,
render=False,
transpile=self._should_transpile(self.library),
),
]
return imports.merge_imports(
dynamic_import,
_imports,
self._get_dependencies_imports(),
)
Returns:
An import var.
"""
imp = super().import_var
imp.tag = None
imp.render = False
return imp
def _get_dynamic_imports(self) -> str:
opts_fragment = ", { ssr: false });"
@ -1893,18 +1932,21 @@ class StatefulComponent(BaseComponent):
"""
return {}
def _get_all_imports(self) -> imports.ImportDict:
def _get_all_imports(self) -> imports.ImportList:
"""Get all the libraries and fields that are used by the component.
Returns:
The import dict with the required imports.
The list of all required imports.
"""
if self.rendered_as_shared:
return {
f"/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}": [
ImportVar(tag=self.tag)
return imports.ImportList(
[
imports.ImportVar(
package=f"/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}",
tag=self.tag,
)
]
}
)
return self.component._get_all_imports()
def _get_all_dynamic_imports(self) -> set[str]:

View File

@ -51,11 +51,14 @@ has_too_many_connection_errors: Var = Var.create_safe(
class WebsocketTargetURL(Bare):
"""A component that renders the websocket target URL."""
def _get_imports(self) -> imports.ImportDict:
return {
f"/{Dirs.STATE_PATH}": [imports.ImportVar(tag="getBackendURL")],
"/env.json": [imports.ImportVar(tag="env", is_default=True)],
}
def _get_imports_list(self) -> list[imports.ImportVar]:
return [
imports.ImportVar(
library=f"/{Dirs.STATE_PATH}",
tag="getBackendURL",
),
imports.ImportVar(library="/env.json", tag="env", is_default=True),
]
@classmethod
def create(cls) -> Component:

View File

@ -12,9 +12,9 @@ from reflex.style import LIGHT_COLOR_MODE, color_mode
from reflex.utils import format, imports
from reflex.vars import BaseVar, Var, VarData
_IS_TRUE_IMPORT = {
f"/{Dirs.STATE_PATH}": {imports.ImportVar(tag="isTrue")},
}
_IS_TRUE_IMPORT = imports.ImportList(
[imports.ImportVar(library=f"/{Dirs.STATE_PATH}", tag="isTrue")]
)
class Cond(MemoizationLeaf):
@ -95,11 +95,13 @@ class Cond(MemoizationLeaf):
cond_state=f"isTrue({self.cond._var_full_name})",
)
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
getattr(self.cond._var_data, "imports", {}),
_IS_TRUE_IMPORT,
def _get_imports_list(self) -> imports.ImportList:
return imports.ImportList(
[
*super()._get_imports_list(),
*getattr(self.cond._var_data, "imports", []),
*_IS_TRUE_IMPORT,
]
)
def _apply_theme(self, theme: Component):

View File

@ -112,7 +112,7 @@ class DebounceInput(Component):
)._replace(
_var_type=Type[Component],
merge_var_data=VarData( # type: ignore
imports=child._get_imports(),
imports=child._get_imports_list(),
hooks=child._get_hooks_internal(),
),
),

View File

@ -1,5 +1,7 @@
"""rx.match."""
from __future__ import annotations
import textwrap
from typing import Any, Dict, List, Optional, Tuple, Union
@ -268,11 +270,11 @@ class Match(MemoizationLeaf):
tag.name = "match"
return dict(tag)
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
getattr(self.cond._var_data, "imports", {}),
)
def _get_imports_list(self) -> list[imports.ImportVar]:
return [
*super()._get_imports_list(),
*getattr(self.cond._var_data, "imports", []),
]
def _apply_theme(self, theme: Component):
"""Apply the theme to this component.

View File

@ -7,7 +7,6 @@ from functools import lru_cache
from hashlib import md5
from typing import Any, Callable, Dict, Union
from reflex.compiler import utils
from reflex.components.component import Component, CustomComponent
from reflex.components.radix.themes.layout.list import (
ListItem,
@ -154,47 +153,53 @@ class Markdown(Component):
return custom_components
def _get_imports(self) -> imports.ImportDict:
def _get_imports_list(self) -> list[imports.ImportVar]:
# Import here to avoid circular imports.
from reflex.components.datadisplay.code import CodeBlock
from reflex.components.radix.themes.typography.code import Code
imports = super()._get_imports()
_imports = super()._get_imports_list()
# Special markdown imports.
imports.update(
{
"": [ImportVar(tag="katex/dist/katex.min.css")],
"remark-math@5.1.1": [
ImportVar(tag=_REMARK_MATH._var_name, is_default=True)
],
"remark-gfm@3.0.1": [
ImportVar(tag=_REMARK_GFM._var_name, is_default=True)
],
"remark-unwrap-images@4.0.0": [
ImportVar(tag=_REMARK_UNWRAP_IMAGES._var_name, is_default=True)
],
"rehype-katex@6.0.3": [
ImportVar(tag=_REHYPE_KATEX._var_name, is_default=True)
],
"rehype-raw@6.1.1": [
ImportVar(tag=_REHYPE_RAW._var_name, is_default=True)
],
}
_imports.extend(
[
ImportVar(library="", tag="katex/dist/katex.min.css"),
ImportVar(
package="remark-math@5.1.1",
tag=_REMARK_MATH._var_name,
is_default=True,
),
ImportVar(
package="remark-gfm@3.0.1",
tag=_REMARK_GFM._var_name,
is_default=True,
),
ImportVar(
package="remark-unwrap-images@4.0.0",
tag=_REMARK_UNWRAP_IMAGES._var_name,
is_default=True,
),
ImportVar(
package="rehype-katex@6.0.3",
tag=_REHYPE_KATEX._var_name,
is_default=True,
),
ImportVar(
package="rehype-raw@6.1.1",
tag=_REHYPE_RAW._var_name,
is_default=True,
),
]
)
# Get the imports for each component.
for component in self.component_map.values():
imports = utils.merge_imports(
imports, component(_MOCK_ARG)._get_all_imports()
)
_imports.extend(component(_MOCK_ARG)._get_all_imports())
# Get the imports for the code components.
imports = utils.merge_imports(
imports, CodeBlock.create(theme="light")._get_imports()
)
imports = utils.merge_imports(imports, Code.create()._get_imports())
return imports
_imports.extend(CodeBlock.create(theme="light")._get_all_imports())
_imports.extend(Code.create()._get_all_imports())
return _imports
def get_component(self, tag: str, **props) -> Component:
"""Get the component for a tag and props.

View File

@ -11,7 +11,6 @@ import textwrap
from functools import lru_cache
from hashlib import md5
from typing import Any, Callable, Dict, Union
from reflex.compiler import utils
from reflex.components.component import Component, CustomComponent
from reflex.components.radix.themes.layout.list import (
ListItem,

View File

@ -425,12 +425,12 @@ class AccordionRoot(AccordionComponent):
accordion_theme_root
)
def _get_imports(self):
return imports.merge_imports(
super()._get_imports(),
self._var_data.imports if self._var_data else {},
{"@emotion/react": [imports.ImportVar(tag="keyframes")]},
)
def _get_imports_list(self) -> list[imports.ImportVar]:
return [
*super()._get_imports_list(),
*(self._var_data.imports if self._var_data else {}),
imports.ImportVar(package="@emotion/react", tag="keyframes"),
]
def get_event_triggers(self) -> Dict[str, Any]:
"""Get the events triggers signatures for the component.
@ -644,12 +644,6 @@ class AccordionContent(AccordionComponent):
def _apply_theme(self, theme: Component):
self.style = Style({**self.style})
# def _get_imports(self):
# return {
# **super()._get_imports(),
# "@emotion/react": [imports.ImportVar(tag="keyframes")],
# }
class Accordion(ComponentNamespace):
"""Accordion component."""

View File

@ -243,13 +243,11 @@ class ThemePanel(RadixThemesComponent):
# Whether the panel is open. Defaults to False.
default_open: Var[bool]
def _get_imports(self) -> dict[str, list[imports.ImportVar]]:
return imports.merge_imports(
super()._get_imports(),
{
"react": [imports.ImportVar(tag="useEffect")],
},
)
def _get_imports_list(self) -> list[imports.ImportVar]:
return [
*super()._get_imports_list(),
imports.ImportVar(package="react", tag="useEffect"),
]
def _get_hooks(self) -> str | None:
# The panel freezes the tab if the user color preference differs from the

View File

@ -59,8 +59,8 @@ class Link(RadixThemesComponent, A, MemoizationLeaf):
# If True, the link will open in a new tab
is_external: Var[bool]
def _get_imports(self) -> imports.ImportDict:
return {**super()._get_imports(), **next_link._get_imports()}
def _get_imports_list(self) -> list[imports.ImportVar]:
return [*super()._get_imports_list(), *next_link._get_imports_list()]
@classmethod
def create(cls, *children, **props) -> Component:

View File

@ -6,7 +6,7 @@ from types import SimpleNamespace
from reflex.base import Base
from reflex.constants import Dirs
from reflex.utils.imports import ImportVar
from reflex.utils.imports import ImportList, ImportVar
# The prefix used to create setters for state vars.
SETTER_PREFIX = "set_"
@ -102,11 +102,13 @@ class ComponentName(Enum):
class Imports(SimpleNamespace):
"""Common sets of import vars."""
EVENTS = {
"react": {ImportVar(tag="useContext")},
f"/{Dirs.CONTEXTS_PATH}": {ImportVar(tag="EventLoopContext")},
f"/{Dirs.STATE_PATH}": {ImportVar(tag=CompileVars.TO_EVENT)},
}
EVENTS: ImportList = ImportList(
[
ImportVar(package="react", tag="useContext"),
ImportVar(package=f"/{Dirs.CONTEXTS_PATH}", tag="EventLoopContext"),
ImportVar(package=f"/{Dirs.STATE_PATH}", tag=CompileVars.TO_EVENT),
]
)
class Hooks(SimpleNamespace):

View File

@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union
from reflex import constants
from reflex.utils import exceptions, serializers, types
from reflex.utils.imports import split_library_name_version
from reflex.utils.serializers import serialize
from reflex.vars import BaseVar, Var
@ -716,11 +717,7 @@ def format_library_name(library_fullname: str):
Returns:
The name without the @version if it was part of the name
"""
lib, at, version = library_fullname.rpartition("@")
if not lib:
lib = at + version
return lib
return split_library_name_version(library_fullname)[0]
def json_dumps(obj: Any) -> str:

View File

@ -6,6 +6,7 @@ from collections import defaultdict
from typing import Dict, List, Optional
from reflex.base import Base
from reflex.constants.installer import PackageJson
def merge_imports(*imports) -> ImportDict:
@ -36,9 +37,29 @@ def collapse_imports(imports: ImportDict) -> ImportDict:
return {lib: list(set(import_vars)) for lib, import_vars in imports.items()}
def split_library_name_version(library_fullname: str):
"""Split the name of a library from its version.
Args:
library_fullname: The fullname of the library.
Returns:
A tuple of the library name and version.
"""
lib, at, version = library_fullname.rpartition("@")
if not lib:
lib = at + version
version = None
return lib, version
class ImportVar(Base):
"""An import var."""
# The package name associated with the tag
library: Optional[str]
# The name of the import tag.
tag: Optional[str]
@ -48,6 +69,12 @@ class ImportVar(Base):
# The tag alias.
alias: Optional[str] = None
# The following fields provide extra information about the import,
# but are not factored in when considering hash or equality
# The version of the package
version: Optional[str]
# Whether this import need to install the associated lib
install: Optional[bool] = True
@ -58,6 +85,43 @@ class ImportVar(Base):
# https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages
transpile: Optional[bool] = False
def __init__(
self,
*,
package: Optional[str] = None,
**kwargs,
):
"""Create a new ImportVar.
Args:
package: The package to install for this import.
**kwargs: The import var fields.
Raises:
ValueError: If the package is provided with library or version.
"""
if package is not None:
if (
kwargs.get("library", None) is not None
or kwargs.get("version", None) is not None
):
raise ValueError(
"Cannot provide 'library' or 'version' as keyword arguments when "
"specifying 'package' as an argument"
)
kwargs["library"], kwargs["version"] = split_library_name_version(package)
install = (
package is not None
# TODO: handle version conflicts
and package not in PackageJson.DEPENDENCIES
and package not in PackageJson.DEV_DEPENDENCIES
and not any(package.startswith(prefix) for prefix in ["/", ".", "next/"])
and package != ""
)
kwargs.setdefault("install", install)
super().__init__(**kwargs)
@property
def name(self) -> str:
"""The name of the import.
@ -72,6 +136,17 @@ class ImportVar(Base):
else:
return self.tag or ""
@property
def package(self) -> str | None:
"""The package to install for this import.
Returns:
The library name and (optional) version to be installed by npm/bun.
"""
if self.version:
return f"{self.library}@{self.version}"
return self.library
def __hash__(self) -> int:
"""Define a hash function for the import var.
@ -80,14 +155,140 @@ class ImportVar(Base):
"""
return hash(
(
self.library,
self.tag,
self.is_default,
self.alias,
self.install,
self.render,
self.transpile,
)
)
def __eq__(self, other: ImportVar) -> bool:
"""Define equality for the import var.
Args:
other: The other import var to compare.
Returns:
Whether the two import vars are equal.
"""
if type(self) != type(other):
return NotImplemented
return (self.library, self.tag, self.is_default, self.alias) == (
other.library,
other.tag,
other.is_default,
other.alias,
)
def collapse(self, other_import_var: ImportVar) -> ImportVar:
"""Collapse two import vars together.
Args:
other_import_var: The other import var to collapse with.
Returns:
The collapsed import var with sticky props perserved.
Raises:
ValueError: If the two import vars have conflicting properties.
"""
if self != other_import_var:
raise ValueError("Cannot collapse two import vars with different hashes")
if (
self.version is not None
and other_import_var.version is not None
and self.version != other_import_var.version
):
raise ValueError(
"Cannot collapse two import vars with conflicting version specifiers: "
f"{self} {other_import_var}"
)
return type(self)(
library=self.library,
version=self.version or other_import_var.version,
tag=self.tag,
is_default=self.is_default,
alias=self.alias,
install=self.install or other_import_var.install,
render=self.render or other_import_var.render,
transpile=self.transpile or other_import_var.transpile,
)
class ImportList(List[ImportVar]):
"""A list of import vars."""
def __init__(self, *args, **kwargs):
"""Create a new ImportList (wrapper over `list`).
Any items that are not already `ImportVar` will be assumed as dicts to convert
into an ImportVar.
Args:
*args: The args to pass to list.__init__
**kwargs: The kwargs to pass to list.__init__
"""
super().__init__(*args, **kwargs)
for ix, value in enumerate(self):
if not isinstance(value, ImportVar):
# convert dicts to ImportVar
self[ix] = ImportVar(**value)
@classmethod
def from_import_dict(
cls, import_dict: ImportDict | Dict[str, set[ImportVar]]
) -> ImportList:
"""Create an import list from an import dict.
Args:
import_dict: The import dict to convert.
Returns:
The import list.
"""
return cls(
ImportVar(package=lib, **imp.dict())
for lib, imps in import_dict.items()
for imp in imps
)
def collapse(self) -> ImportDict:
"""When collapsing an import list, prefer packages with version specifiers.
Returns:
The collapsed import dict ({package_spec: [import_var1, ...]}).
Raises:
ValueError: If two imports have conflicting version specifiers.
"""
collapsed: dict[str, dict[ImportVar, ImportVar]] = {}
for imp in self:
lib = imp.library or ""
collapsed.setdefault(lib, {})
if imp in collapsed[lib]:
# Need to check if the current import has any special properties that need to
# be preserved, like the version specifier, install, or transpile.
existing_imp = collapsed[lib][imp]
collapsed[lib][imp] = existing_imp.collapse(imp)
else:
collapsed[lib][imp] = imp
# Check that all tags in the given library have the same version.
deduped: ImportDict = {}
for lib, imps in collapsed.items():
packages = {imp.package for imp in imps if imp.version is not None}
if len(packages) > 1:
raise ValueError(
f"Imports from {lib} have conflicting version specifiers: "
f"{packages} {imps}"
)
package = lib
if packages:
package = packages.pop() or ""
deduped[package] = list(imps.values())
return deduped
ImportDict = Dict[str, List[ImportVar]]

View File

@ -22,6 +22,7 @@ from typing import (
List,
Literal,
Optional,
Sequence,
Tuple,
Type,
Union,
@ -34,10 +35,10 @@ from typing import (
from reflex import constants
from reflex.base import Base
from reflex.utils import console, format, imports, serializers, types
from reflex.utils import console, format, serializers, types
# This module used to export ImportVar itself, so we still import it for export here
from reflex.utils.imports import ImportDict, ImportVar
from reflex.utils.imports import ImportDict, ImportList, ImportVar
if TYPE_CHECKING:
from reflex.state import BaseState
@ -116,7 +117,7 @@ class VarData(Base):
state: str = ""
# Imports needed to render this var
imports: ImportDict = {}
imports: ImportList = ImportList()
# Hooks that need to be present in the component to render this var
hooks: Dict[str, None] = {}
@ -126,6 +127,39 @@ class VarData(Base):
# segments.
interpolations: List[Tuple[int, int]] = []
def __init__(
self,
imports: ImportList
| Sequence[ImportVar | Dict[str, Optional[Union[str, bool]]]]
| ImportDict
| Dict[str, set[ImportVar]]
| None = None,
**kwargs,
):
"""Initialize the VarData.
If imports is an ImportDict it will be converted to an ImportList and a
deprecation warning will be displayed.
Args:
imports: The imports needed to render this var.
**kwargs: Additional fields to set.
"""
if isinstance(imports, dict):
imports = ImportList.from_import_dict(imports)
console.deprecate(
feature_name="Passing ImportDict for VarData",
reason="use ImportList instead",
deprecation_version="0.5.0",
removal_version="0.6.0",
)
else:
imports = ImportList(imports or [])
super().__init__(
imports=imports, # type: ignore
**kwargs,
)
@classmethod
def merge(cls, *others: VarData | None) -> VarData | None:
"""Merge multiple var data objects.
@ -137,14 +171,14 @@ class VarData(Base):
The merged var data object.
"""
state = ""
_imports = {}
_imports = ImportList()
hooks = {}
interpolations = []
for var_data in others:
if var_data is None:
continue
state = state or var_data.state
_imports = imports.merge_imports(_imports, var_data.imports)
_imports.extend(var_data.imports)
hooks.update(var_data.hooks)
interpolations += var_data.interpolations
@ -180,11 +214,18 @@ class VarData(Base):
# Don't compare interpolations - that's added in by the decoder, and
# not part of the vardata itself.
if not isinstance(self.imports, ImportList):
self_imports = ImportList(self.imports).collapse()
else:
self_imports = self.imports.collapse()
if not isinstance(other.imports, ImportList):
other_imports = ImportList(other.imports).collapse()
else:
other_imports = other.imports.collapse()
return (
self.state == other.state
and self.hooks.keys() == other.hooks.keys()
and imports.collapse_imports(self.imports)
== imports.collapse_imports(other.imports)
and self_imports == other_imports
)
def dict(self) -> dict:
@ -196,10 +237,7 @@ class VarData(Base):
return {
"state": self.state,
"interpolations": list(self.interpolations),
"imports": {
lib: [import_var.dict() for import_var in import_vars]
for lib, import_vars in self.imports.items()
},
"imports": [import_var.dict() for import_var in self.imports],
"hooks": self.hooks,
}
@ -1042,11 +1080,12 @@ class Var:
",", other, fn="spreadArraysOrObjects", flip=flip
)._replace(
merge_var_data=VarData(
imports={
f"/{constants.Dirs.STATE_PATH}": [
ImportVar(tag="spreadArraysOrObjects")
]
},
imports=[
ImportVar(
package=f"/{constants.Dirs.STATE_PATH}",
tag="spreadArraysOrObjects",
),
],
),
)
return self.operation("+", other, flip=flip)
@ -1595,11 +1634,11 @@ class Var:
v2._var_data,
step._var_data,
VarData(
imports={
"/utils/helpers/range.js": [
ImportVar(tag="range", is_default=True),
],
},
imports=[
ImportVar(
package="/utils/helpers/range", tag="range", is_default=True
),
]
),
),
)
@ -1627,9 +1666,9 @@ class Var:
_var_is_string=False,
_var_full_name_needs_state_prefix=False,
merge_var_data=VarData(
imports={
f"/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")],
},
imports=[
ImportVar(package=f"/{constants.Dirs.STATE_PATH}", tag="refs")
],
),
)
@ -1667,10 +1706,14 @@ class Var:
format.format_state_name(state_name)
): None
},
imports={
f"/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")],
"react": [ImportVar(tag="useContext")],
},
imports=ImportList(
[
ImportVar(
package=f"/{constants.Dirs.CONTEXTS_PATH}", tag="StateContexts"
),
ImportVar(package="react", tag="useContext"),
]
),
)
self._var_data = VarData.merge(self._var_data, new_var_data)
self._var_full_name_needs_state_prefix = True

View File

@ -9,7 +9,7 @@ from reflex.base import Base as Base
from reflex.state import State as State
from reflex.state import BaseState as BaseState
from reflex.utils import console as console, format as format, types as types
from reflex.utils.imports import ImportVar
from reflex.utils.imports import ImportDict, ImportList, ImportVar
from types import FunctionType
from typing import (
Any,
@ -18,6 +18,7 @@ from typing import (
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Type,
@ -35,9 +36,18 @@ def _extract_var_data(value: Iterable) -> list[VarData | None]: ...
class VarData(Base):
state: str
imports: dict[str, set[ImportVar]]
imports: ImportList
hooks: Dict[str, None]
interpolations: List[Tuple[int, int]]
def __init__(
self,
imports: ImportList
| Sequence[ImportVar | Dict[str, Optional[Union[str, bool]]]]
| ImportDict
| Dict[str, set[ImportVar]]
| None = None,
**kwargs,
): ...
@classmethod
def merge(cls, *others: VarData | None) -> VarData | None: ...

View File

@ -4,8 +4,7 @@ from typing import List
import pytest
from reflex.compiler import compiler, utils
from reflex.utils import imports
from reflex.utils.imports import ImportVar
from reflex.utils.imports import ImportList, ImportVar
@pytest.mark.parametrize(
@ -48,43 +47,56 @@ def test_compile_import_statement(
@pytest.mark.parametrize(
"import_dict,test_dicts",
"import_list,test_dicts",
[
({}, []),
(ImportList(), []),
(
{"axios": [ImportVar(tag="axios", is_default=True)]},
ImportList([ImportVar(library="axios", tag="axios", is_default=True)]),
[{"lib": "axios", "default": "axios", "rest": []}],
),
(
{"axios": [ImportVar(tag="foo"), ImportVar(tag="bar")]},
ImportList(
[
ImportVar(library="axios", tag="foo"),
ImportVar(library="axios", tag="bar"),
]
),
[{"lib": "axios", "default": "", "rest": ["bar", "foo"]}],
),
(
{
"axios": [
ImportVar(tag="axios", is_default=True),
ImportVar(tag="foo"),
ImportVar(tag="bar"),
],
"react": [ImportVar(tag="react", is_default=True)],
},
ImportList(
[
ImportVar(library="axios", tag="axios", is_default=True),
ImportVar(library="axios", tag="foo"),
ImportVar(library="axios", tag="bar"),
ImportVar(library="react", tag="react", is_default=True),
]
),
[
{"lib": "axios", "default": "axios", "rest": ["bar", "foo"]},
{"lib": "react", "default": "react", "rest": []},
],
),
(
{"": [ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")]},
ImportList(
[
ImportVar(library="", tag="lib1.js"),
ImportVar(library="", tag="lib2.js"),
]
),
[
{"lib": "lib1.js", "default": "", "rest": []},
{"lib": "lib2.js", "default": "", "rest": []},
],
),
(
{
"": [ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")],
"axios": [ImportVar(tag="axios", is_default=True)],
},
ImportList(
[
ImportVar(library="", tag="lib1.js"),
ImportVar(library="", tag="lib2.js"),
ImportVar(library="axios", tag="axios", is_default=True),
]
),
[
{"lib": "lib1.js", "default": "", "rest": []},
{"lib": "lib2.js", "default": "", "rest": []},
@ -93,14 +105,14 @@ def test_compile_import_statement(
),
],
)
def test_compile_imports(import_dict: imports.ImportDict, test_dicts: List[dict]):
def test_compile_imports(import_list: ImportList, test_dicts: List[dict]):
"""Test the compile_imports function.
Args:
import_dict: The import dictionary.
import_list: The list of ImportVar.
test_dicts: The expected output.
"""
imports = utils.compile_imports(import_dict)
imports = utils.compile_imports(import_list)
for import_dict, test_dict in zip(imports, test_dicts):
assert import_dict["lib"] == test_dict["lib"]
assert import_dict["default"] == test_dict["default"]

View File

@ -9,14 +9,14 @@ from reflex.components.radix.themes.typography.text import Text
def test_websocket_target_url():
url = WebsocketTargetURL.create()
_imports = url._get_all_imports(collapse=True)
assert list(_imports.keys()) == ["/utils/state", "/env.json"]
_imports = url._get_all_imports()
assert [i.library for i in _imports] == ["/utils/state", "/env.json"]
def test_connection_banner():
banner = ConnectionBanner.create()
_imports = banner._get_all_imports(collapse=True)
assert list(_imports.keys()) == [
_imports = banner._get_all_imports().collapse()
assert list(_imports) == [
"react",
"/utils/context",
"/utils/state",
@ -31,8 +31,8 @@ def test_connection_banner():
def test_connection_modal():
modal = ConnectionModal.create()
_imports = modal._get_all_imports(collapse=True)
assert list(_imports.keys()) == [
_imports = modal._get_all_imports().collapse()
assert list(_imports) == [
"react",
"/utils/context",
"/utils/state",
@ -48,4 +48,4 @@ def test_connection_modal():
def test_connection_pulser():
pulser = ConnectionPulser.create()
_custom_code = pulser._get_all_custom_code()
_imports = pulser._get_all_imports(collapse=True)
_imports = pulser._get_all_imports()

View File

@ -296,11 +296,11 @@ def test_get_imports(component1, component2):
"""
c1 = component1.create()
c2 = component2.create(c1)
assert c1._get_all_imports() == {"react": [ImportVar(tag="Component")]}
assert c2._get_all_imports() == {
"react-redux": [ImportVar(tag="connect")],
"react": [ImportVar(tag="Component")],
}
assert c1._get_all_imports() == [ImportVar(library="react", tag="Component")]
assert c2._get_all_imports() == [
ImportVar(library="react-redux", tag="connect"),
ImportVar(library="react", tag="Component"),
]
def test_get_custom_code(component1, component2):
@ -1514,22 +1514,24 @@ def test_custom_component_get_imports():
custom_comp = wrapper()
# Inner is not imported directly, but it is imported by the custom component.
assert "inner" not in custom_comp._get_all_imports()
inner_import = ImportVar(library="inner", tag="Inner")
assert inner_import not in custom_comp._get_all_imports()
# The imports are only resolved during compilation.
_, _, imports_inner = compile_components(custom_comp._get_all_custom_components())
assert "inner" in imports_inner
assert inner_import 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_all_imports()
assert "other" not in outer_comp._get_all_imports()
other_import = ImportVar(library="other", tag="Other")
assert inner_import not in outer_comp._get_all_imports()
assert other_import not in outer_comp._get_all_imports()
# The imports are only resolved during compilation.
_, _, imports_outer = compile_components(outer_comp._get_all_custom_components())
assert "inner" in imports_outer
assert "other" in imports_outer
assert inner_import in imports_outer
assert other_import in imports_outer
def test_custom_component_declare_event_handlers_in_fields():

View File

@ -837,7 +837,7 @@ def test_state_with_initial_computed_var(
(f"{BaseVar(_var_name='var', _var_type=str)}", "${var}"),
(
f"testing f-string with {BaseVar(_var_name='myvar', _var_type=int)._var_set_state('state')}",
'testing f-string with $<reflex.Var>{"state": "state", "interpolations": [], "imports": {"/utils/context": [{"tag": "StateContexts", "is_default": false, "alias": null, "install": true, "render": true, "transpile": false}], "react": [{"tag": "useContext", "is_default": false, "alias": null, "install": true, "render": true, "transpile": false}]}, "hooks": {"const state = useContext(StateContexts.state)": null}, "string_length": 13}</reflex.Var>{state.myvar}',
'testing f-string with $<reflex.Var>{"state": "state", "interpolations": [], "imports": [{"library": "/utils/context", "tag": "StateContexts", "is_default": false, "alias": null, "version": null, "install": false, "render": true, "transpile": false}, {"library": "react", "tag": "useContext", "is_default": false, "alias": null, "version": null, "install": false, "render": true, "transpile": false}], "hooks": {"const state = useContext(StateContexts.state)": null}, "string_length": 13}</reflex.Var>{state.myvar}',
),
(
f"testing local f-string {BaseVar(_var_name='x', _var_is_local=True, _var_type=str)}",