Merge branch 'main' into lendemor/add_N_rules
This commit is contained in:
commit
97b09acd49
22
.github/workflows/integration_tests.yml
vendored
22
.github/workflows/integration_tests.yml
vendored
@ -33,7 +33,7 @@ env:
|
|||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
example-counter:
|
example-counter-and-nba-proxy:
|
||||||
env:
|
env:
|
||||||
OUTPUT_FILE: import_benchmark.json
|
OUTPUT_FILE: import_benchmark.json
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
@ -119,6 +119,26 @@ jobs:
|
|||||||
--benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}"
|
--benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}"
|
||||||
--branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
|
--branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
|
||||||
--app-name "counter"
|
--app-name "counter"
|
||||||
|
- name: Install requirements for nba proxy example
|
||||||
|
working-directory: ./reflex-examples/nba-proxy
|
||||||
|
run: |
|
||||||
|
poetry run uv pip install -r requirements.txt
|
||||||
|
- name: Install additional dependencies for DB access
|
||||||
|
run: poetry run uv pip install psycopg
|
||||||
|
- name: Check export --backend-only before init for nba-proxy example
|
||||||
|
working-directory: ./reflex-examples/nba-proxy
|
||||||
|
run: |
|
||||||
|
poetry run reflex export --backend-only
|
||||||
|
- name: Init Website for nba-proxy example
|
||||||
|
working-directory: ./reflex-examples/nba-proxy
|
||||||
|
run: |
|
||||||
|
poetry run reflex init --loglevel debug
|
||||||
|
- name: Run Website and Check for errors
|
||||||
|
run: |
|
||||||
|
# Check that npm is home
|
||||||
|
npm -v
|
||||||
|
poetry run bash scripts/integration.sh ./reflex-examples/nba-proxy dev
|
||||||
|
|
||||||
|
|
||||||
reflex-web:
|
reflex-web:
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -86,8 +86,8 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
target-version = "py39"
|
target-version = "py39"
|
||||||
output-format = "concise"
|
output-format = "concise"
|
||||||
lint.isort.split-on-trailing-comma = false
|
lint.isort.split-on-trailing-comma = false
|
||||||
lint.select = ["B", "C4", "D", "E", "ERA", "F", "FURB", "I", "N", "PERF", "PTH", "RUF", "SIM", "T", "W"]
|
lint.select = ["B", "C4", "D", "E", "ERA", "F", "FURB", "I", "N", "PERF", "PTH", "RUF", "SIM", "T", "TRY", "W"]
|
||||||
lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
|
lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012", "TRY0"]
|
||||||
lint.pydocstyle.convention = "google"
|
lint.pydocstyle.convention = "google"
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
@ -463,14 +463,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The generated component.
|
The generated component.
|
||||||
|
|
||||||
Raises:
|
|
||||||
exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
|
|
||||||
"""
|
"""
|
||||||
try:
|
return component if isinstance(component, Component) else component()
|
||||||
return component if isinstance(component, Component) else component()
|
|
||||||
except exceptions.MatchTypeError:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def add_page(
|
def add_page(
|
||||||
self,
|
self,
|
||||||
@ -1563,10 +1557,36 @@ class EventNamespace(AsyncNamespace):
|
|||||||
Args:
|
Args:
|
||||||
sid: The Socket.IO session id.
|
sid: The Socket.IO session id.
|
||||||
data: The event data.
|
data: The event data.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
EventDeserializationError: If the event data is not a dictionary.
|
||||||
"""
|
"""
|
||||||
fields = data
|
fields = data
|
||||||
# Get the event.
|
|
||||||
event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
|
if isinstance(fields, str):
|
||||||
|
console.warn(
|
||||||
|
"Received event data as a string. This generally should not happen and may indicate a bug."
|
||||||
|
f" Event data: {fields}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
fields = json.loads(fields)
|
||||||
|
except json.JSONDecodeError as ex:
|
||||||
|
raise exceptions.EventDeserializationError(
|
||||||
|
f"Failed to deserialize event data: {fields}."
|
||||||
|
) from ex
|
||||||
|
|
||||||
|
if not isinstance(fields, dict):
|
||||||
|
raise exceptions.EventDeserializationError(
|
||||||
|
f"Event data must be a dictionary, but received {fields} of type {type(fields)}."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get the event.
|
||||||
|
event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
|
||||||
|
except (TypeError, ValueError) as ex:
|
||||||
|
raise exceptions.EventDeserializationError(
|
||||||
|
f"Failed to deserialize event data: {fields}."
|
||||||
|
) from ex
|
||||||
|
|
||||||
self.token_to_sid[event.token] = sid
|
self.token_to_sid[event.token] = sid
|
||||||
self.sid_to_token[sid] = event.token
|
self.sid_to_token[sid] = event.token
|
||||||
|
@ -7,14 +7,13 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
from reflex import constants
|
from reflex import constants
|
||||||
from reflex.utils import telemetry
|
from reflex.utils import telemetry
|
||||||
from reflex.utils.exec import is_prod_mode
|
from reflex.utils.exec import is_prod_mode
|
||||||
from reflex.utils.prerequisites import get_app
|
from reflex.utils.prerequisites import get_and_validate_app
|
||||||
|
|
||||||
if constants.CompileVars.APP != "app":
|
if constants.CompileVars.APP != "app":
|
||||||
raise AssertionError("unexpected variable name for 'app'")
|
raise AssertionError("unexpected variable name for 'app'")
|
||||||
|
|
||||||
telemetry.send("compile")
|
telemetry.send("compile")
|
||||||
app_module = get_app(reload=False)
|
app, app_module = get_and_validate_app(reload=False)
|
||||||
app = getattr(app_module, constants.CompileVars.APP)
|
|
||||||
# For py3.9 compatibility when redis is used, we MUST add any decorator pages
|
# For py3.9 compatibility when redis is used, we MUST add any decorator pages
|
||||||
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
||||||
app._apply_decorated_pages()
|
app._apply_decorated_pages()
|
||||||
@ -30,7 +29,7 @@ if is_prod_mode():
|
|||||||
# ensure only "app" is exposed.
|
# ensure only "app" is exposed.
|
||||||
del app_module
|
del app_module
|
||||||
del compile_future
|
del compile_future
|
||||||
del get_app
|
del get_and_validate_app
|
||||||
del is_prod_mode
|
del is_prod_mode
|
||||||
del telemetry
|
del telemetry
|
||||||
del constants
|
del constants
|
||||||
|
@ -429,20 +429,22 @@ class Component(BaseComponent, ABC):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
def determine_key(value):
|
||||||
|
# Try to create a var from the value
|
||||||
|
key = value if isinstance(value, Var) else LiteralVar.create(value)
|
||||||
|
|
||||||
|
# Check that the var type is not None.
|
||||||
|
if key is None:
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
# Check whether the key is a component prop.
|
# Check whether the key is a component prop.
|
||||||
if types._issubclass(field_type, Var):
|
if types._issubclass(field_type, Var):
|
||||||
# Used to store the passed types if var type is a union.
|
# Used to store the passed types if var type is a union.
|
||||||
passed_types = None
|
passed_types = None
|
||||||
try:
|
try:
|
||||||
# Try to create a var from the value.
|
kwargs[key] = determine_key(value)
|
||||||
if isinstance(value, Var):
|
|
||||||
kwargs[key] = value
|
|
||||||
else:
|
|
||||||
kwargs[key] = LiteralVar.create(value)
|
|
||||||
|
|
||||||
# Check that the var type is not None.
|
|
||||||
if kwargs[key] is None:
|
|
||||||
raise TypeError
|
|
||||||
|
|
||||||
expected_type = fields[key].outer_type_.__args__[0]
|
expected_type = fields[key].outer_type_.__args__[0]
|
||||||
# validate literal fields.
|
# validate literal fields.
|
||||||
@ -740,22 +742,21 @@ class Component(BaseComponent, ABC):
|
|||||||
# Import here to avoid circular imports.
|
# Import here to avoid circular imports.
|
||||||
from reflex.components.base.bare import Bare
|
from reflex.components.base.bare import Bare
|
||||||
from reflex.components.base.fragment import Fragment
|
from reflex.components.base.fragment import Fragment
|
||||||
from reflex.utils.exceptions import ComponentTypeError
|
from reflex.utils.exceptions import ChildrenTypeError
|
||||||
|
|
||||||
# Filter out None props
|
# Filter out None props
|
||||||
props = {key: value for key, value in props.items() if value is not None}
|
props = {key: value for key, value in props.items() if value is not None}
|
||||||
|
|
||||||
def validate_children(children):
|
def validate_children(children):
|
||||||
for child in children:
|
for child in children:
|
||||||
if isinstance(child, tuple):
|
if isinstance(child, (tuple, list)):
|
||||||
validate_children(child)
|
validate_children(child)
|
||||||
|
|
||||||
# Make sure the child is a valid type.
|
# Make sure the child is a valid type.
|
||||||
if not types._isinstance(child, ComponentChild):
|
if isinstance(child, dict) or not types._isinstance(
|
||||||
raise ComponentTypeError(
|
child, ComponentChild
|
||||||
"Children of Reflex components must be other components, "
|
):
|
||||||
"state vars, or primitive Python types. "
|
raise ChildrenTypeError(component=cls.__name__, child=child)
|
||||||
f"Got child {child} of type {type(child)}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate all the children.
|
# Validate all the children.
|
||||||
validate_children(children)
|
validate_children(children)
|
||||||
|
@ -12,6 +12,7 @@ import threading
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
from importlib.util import find_spec
|
from importlib.util import find_spec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
@ -607,6 +608,9 @@ class Config(Base):
|
|||||||
# The name of the app (should match the name of the app directory).
|
# The name of the app (should match the name of the app directory).
|
||||||
app_name: str
|
app_name: str
|
||||||
|
|
||||||
|
# The path to the app module.
|
||||||
|
app_module_import: Optional[str] = None
|
||||||
|
|
||||||
# The log level to use.
|
# The log level to use.
|
||||||
loglevel: constants.LogLevel = constants.LogLevel.DEFAULT
|
loglevel: constants.LogLevel = constants.LogLevel.DEFAULT
|
||||||
|
|
||||||
@ -729,6 +733,19 @@ class Config(Base):
|
|||||||
"REDIS_URL is required when using the redis state manager."
|
"REDIS_URL is required when using the redis state manager."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app_module(self) -> ModuleType | None:
|
||||||
|
"""Return the app module if `app_module_import` is set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The app module.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
importlib.import_module(self.app_module_import)
|
||||||
|
if self.app_module_import
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module(self) -> str:
|
def module(self) -> str:
|
||||||
"""Get the module name of the app.
|
"""Get the module name of the app.
|
||||||
@ -736,6 +753,8 @@ class Config(Base):
|
|||||||
Returns:
|
Returns:
|
||||||
The module name.
|
The module name.
|
||||||
"""
|
"""
|
||||||
|
if self.app_module is not None:
|
||||||
|
return self.app_module.__name__
|
||||||
return ".".join([self.app_name, self.app_name])
|
return ".".join([self.app_name, self.app_name])
|
||||||
|
|
||||||
def update_from_env(self) -> dict[str, Any]:
|
def update_from_env(self) -> dict[str, Any]:
|
||||||
@ -874,7 +893,7 @@ def get_config(reload: bool = False) -> Config:
|
|||||||
return cached_rxconfig.config
|
return cached_rxconfig.config
|
||||||
|
|
||||||
with _config_lock:
|
with _config_lock:
|
||||||
sys_path = sys.path.copy()
|
orig_sys_path = sys.path.copy()
|
||||||
sys.path.clear()
|
sys.path.clear()
|
||||||
sys.path.append(str(Path.cwd()))
|
sys.path.append(str(Path.cwd()))
|
||||||
try:
|
try:
|
||||||
@ -882,9 +901,14 @@ def get_config(reload: bool = False) -> Config:
|
|||||||
return _get_config()
|
return _get_config()
|
||||||
except Exception:
|
except Exception:
|
||||||
# If the module import fails, try to import with the original sys.path.
|
# If the module import fails, try to import with the original sys.path.
|
||||||
sys.path.extend(sys_path)
|
sys.path.extend(orig_sys_path)
|
||||||
return _get_config()
|
return _get_config()
|
||||||
finally:
|
finally:
|
||||||
|
# Find any entries added to sys.path by rxconfig.py itself.
|
||||||
|
extra_paths = [
|
||||||
|
p for p in sys.path if p not in orig_sys_path and p != str(Path.cwd())
|
||||||
|
]
|
||||||
# Restore the original sys.path.
|
# Restore the original sys.path.
|
||||||
sys.path.clear()
|
sys.path.clear()
|
||||||
sys.path.extend(sys_path)
|
sys.path.extend(extra_paths)
|
||||||
|
sys.path.extend(orig_sys_path)
|
||||||
|
@ -421,12 +421,13 @@ def _run_commands_in_subprocess(cmds: list[str]) -> bool:
|
|||||||
console.debug(f"Running command: {' '.join(cmds)}")
|
console.debug(f"Running command: {' '.join(cmds)}")
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(cmds, capture_output=True, text=True, check=True)
|
result = subprocess.run(cmds, capture_output=True, text=True, check=True)
|
||||||
console.debug(result.stdout)
|
|
||||||
return True
|
|
||||||
except subprocess.CalledProcessError as cpe:
|
except subprocess.CalledProcessError as cpe:
|
||||||
console.error(cpe.stdout)
|
console.error(cpe.stdout)
|
||||||
console.error(cpe.stderr)
|
console.error(cpe.stderr)
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
console.debug(result.stdout)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _make_pyi_files():
|
def _make_pyi_files():
|
||||||
@ -931,10 +932,11 @@ def _get_file_from_prompt_in_loop() -> Tuple[bytes, str] | None:
|
|||||||
file_extension = image_filepath.suffix
|
file_extension = image_filepath.suffix
|
||||||
try:
|
try:
|
||||||
image_file = image_filepath.read_bytes()
|
image_file = image_filepath.read_bytes()
|
||||||
return image_file, file_extension
|
|
||||||
except OSError as ose:
|
except OSError as ose:
|
||||||
console.error(f"Unable to read the {file_extension} file due to {ose}")
|
console.error(f"Unable to read the {file_extension} file due to {ose}")
|
||||||
raise typer.Exit(code=1) from ose
|
raise typer.Exit(code=1) from ose
|
||||||
|
else:
|
||||||
|
return image_file, file_extension
|
||||||
|
|
||||||
console.debug(f"File extension detected: {file_extension}")
|
console.debug(f"File extension detected: {file_extension}")
|
||||||
return None
|
return None
|
||||||
|
@ -1594,7 +1594,7 @@ def get_handler_args(
|
|||||||
|
|
||||||
|
|
||||||
def fix_events(
|
def fix_events(
|
||||||
events: list[EventHandler | EventSpec] | None,
|
events: list[EventSpec | EventHandler] | None,
|
||||||
token: str,
|
token: str,
|
||||||
router_data: dict[str, Any] | None = None,
|
router_data: dict[str, Any] | None = None,
|
||||||
) -> list[Event]:
|
) -> list[Event]:
|
||||||
|
@ -1776,9 +1776,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
state._clean()
|
state._clean()
|
||||||
|
|
||||||
app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
event_specs = (
|
||||||
|
prerequisites.get_and_validate_app().app.backend_exception_handler(ex)
|
||||||
event_specs = app_instance.backend_exception_handler(ex)
|
)
|
||||||
|
|
||||||
if event_specs is None:
|
if event_specs is None:
|
||||||
return StateUpdate()
|
return StateUpdate()
|
||||||
@ -1888,9 +1888,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
telemetry.send_error(ex, context="backend")
|
telemetry.send_error(ex, context="backend")
|
||||||
|
|
||||||
app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
event_specs = (
|
||||||
|
prerequisites.get_and_validate_app().app.backend_exception_handler(ex)
|
||||||
event_specs = app_instance.backend_exception_handler(ex)
|
)
|
||||||
|
|
||||||
yield state._as_state_update(
|
yield state._as_state_update(
|
||||||
handler,
|
handler,
|
||||||
@ -2402,8 +2402,9 @@ class FrontendEventExceptionState(State):
|
|||||||
component_stack: The stack trace of the component where the exception occurred.
|
component_stack: The stack trace of the component where the exception occurred.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
prerequisites.get_and_validate_app().app.frontend_exception_handler(
|
||||||
app_instance.frontend_exception_handler(Exception(stack))
|
Exception(stack)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UpdateVarsInternalState(State):
|
class UpdateVarsInternalState(State):
|
||||||
@ -2441,15 +2442,16 @@ class OnLoadInternalState(State):
|
|||||||
The list of events to queue for on load handling.
|
The list of events to queue for on load handling.
|
||||||
"""
|
"""
|
||||||
# Do not app._compile()! It should be already compiled by now.
|
# Do not app._compile()! It should be already compiled by now.
|
||||||
app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
load_events = prerequisites.get_and_validate_app().app.get_load_events(
|
||||||
load_events = app.get_load_events(self.router.page.path)
|
self.router.page.path
|
||||||
|
)
|
||||||
if not load_events:
|
if not load_events:
|
||||||
self.is_hydrated = True
|
self.is_hydrated = True
|
||||||
return # Fast path for navigation with no on_load events defined.
|
return # Fast path for navigation with no on_load events defined.
|
||||||
self.is_hydrated = False
|
self.is_hydrated = False
|
||||||
return [
|
return [
|
||||||
*fix_events(
|
*fix_events(
|
||||||
load_events,
|
cast(list[Union[EventSpec, EventHandler]], load_events),
|
||||||
self.router.session.client_token,
|
self.router.session.client_token,
|
||||||
router_data=self.router_data,
|
router_data=self.router_data,
|
||||||
),
|
),
|
||||||
@ -2608,7 +2610,7 @@ class StateProxy(wrapt.ObjectProxy):
|
|||||||
"""
|
"""
|
||||||
super().__init__(state_instance)
|
super().__init__(state_instance)
|
||||||
# compile is not relevant to backend logic
|
# compile is not relevant to backend logic
|
||||||
self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
self._self_app = prerequisites.get_and_validate_app().app
|
||||||
self._self_substate_path = tuple(state_instance.get_full_name().split("."))
|
self._self_substate_path = tuple(state_instance.get_full_name().split("."))
|
||||||
self._self_actx = None
|
self._self_actx = None
|
||||||
self._self_mutable = False
|
self._self_mutable = False
|
||||||
@ -3701,8 +3703,7 @@ def get_state_manager() -> StateManager:
|
|||||||
Returns:
|
Returns:
|
||||||
The state manager.
|
The state manager.
|
||||||
"""
|
"""
|
||||||
app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
return prerequisites.get_and_validate_app().app.state_manager
|
||||||
return app.state_manager
|
|
||||||
|
|
||||||
|
|
||||||
class MutableProxy(wrapt.ObjectProxy):
|
class MutableProxy(wrapt.ObjectProxy):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Custom Exceptions."""
|
"""Custom Exceptions."""
|
||||||
|
|
||||||
from typing import NoReturn
|
from typing import Any, NoReturn
|
||||||
|
|
||||||
|
|
||||||
class ReflexError(Exception):
|
class ReflexError(Exception):
|
||||||
@ -31,6 +31,22 @@ class ComponentTypeError(ReflexError, TypeError):
|
|||||||
"""Custom TypeError for component related errors."""
|
"""Custom TypeError for component related errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class ChildrenTypeError(ComponentTypeError):
|
||||||
|
"""Raised when the children prop of a component is not a valid type."""
|
||||||
|
|
||||||
|
def __init__(self, component: str, child: Any):
|
||||||
|
"""Initialize the exception.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component: The name of the component.
|
||||||
|
child: The child that caused the error.
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
f"Component {component} received child {child} of type {type(child)}. "
|
||||||
|
"Accepted types are other components, state vars, or primitive Python types (dict excluded)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventHandlerTypeError(ReflexError, TypeError):
|
class EventHandlerTypeError(ReflexError, TypeError):
|
||||||
"""Custom TypeError for event handler related errors."""
|
"""Custom TypeError for event handler related errors."""
|
||||||
|
|
||||||
@ -171,6 +187,10 @@ class SystemPackageMissingError(ReflexError):
|
|||||||
"""Raised when a system package is missing."""
|
"""Raised when a system package is missing."""
|
||||||
|
|
||||||
|
|
||||||
|
class EventDeserializationError(ReflexError, ValueError):
|
||||||
|
"""Raised when an event cannot be deserialized."""
|
||||||
|
|
||||||
|
|
||||||
def raise_system_package_missing_error(package: str) -> NoReturn:
|
def raise_system_package_missing_error(package: str) -> NoReturn:
|
||||||
"""Raise a SystemPackageMissingError.
|
"""Raise a SystemPackageMissingError.
|
||||||
|
|
||||||
|
@ -240,6 +240,28 @@ def run_backend(
|
|||||||
run_uvicorn_backend(host, port, loglevel)
|
run_uvicorn_backend(host, port, loglevel)
|
||||||
|
|
||||||
|
|
||||||
|
def get_reload_dirs() -> list[str]:
|
||||||
|
"""Get the reload directories for the backend.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The reload directories for the backend.
|
||||||
|
"""
|
||||||
|
config = get_config()
|
||||||
|
reload_dirs = [config.app_name]
|
||||||
|
if config.app_module is not None and config.app_module.__file__:
|
||||||
|
module_path = Path(config.app_module.__file__).resolve().parent
|
||||||
|
while module_path.parent.name:
|
||||||
|
for parent_file in module_path.parent.iterdir():
|
||||||
|
if parent_file == "__init__.py":
|
||||||
|
# go up a level to find dir without `__init__.py`
|
||||||
|
module_path = module_path.parent
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
reload_dirs.append(str(module_path))
|
||||||
|
return reload_dirs
|
||||||
|
|
||||||
|
|
||||||
def run_uvicorn_backend(host, port, loglevel: LogLevel):
|
def run_uvicorn_backend(host, port, loglevel: LogLevel):
|
||||||
"""Run the backend in development mode using Uvicorn.
|
"""Run the backend in development mode using Uvicorn.
|
||||||
|
|
||||||
@ -256,7 +278,7 @@ def run_uvicorn_backend(host, port, loglevel: LogLevel):
|
|||||||
port=port,
|
port=port,
|
||||||
log_level=loglevel.value,
|
log_level=loglevel.value,
|
||||||
reload=True,
|
reload=True,
|
||||||
reload_dirs=[get_config().app_name],
|
reload_dirs=get_reload_dirs(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -281,7 +303,7 @@ def run_granian_backend(host, port, loglevel: LogLevel):
|
|||||||
interface=Interfaces.ASGI,
|
interface=Interfaces.ASGI,
|
||||||
log_level=LogLevels(loglevel.value),
|
log_level=LogLevels(loglevel.value),
|
||||||
reload=True,
|
reload=True,
|
||||||
reload_paths=[Path(get_config().app_name)],
|
reload_paths=[Path(rp) for rp in get_reload_dirs()],
|
||||||
reload_ignore_dirs=[".web"],
|
reload_ignore_dirs=[".web"],
|
||||||
).serve()
|
).serve()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -17,11 +17,12 @@ import stat
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import typing
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, List, NamedTuple, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import typer
|
import typer
|
||||||
@ -42,9 +43,19 @@ from reflex.utils.exceptions import (
|
|||||||
from reflex.utils.format import format_library_name
|
from reflex.utils.format import format_library_name
|
||||||
from reflex.utils.registry import _get_npm_registry
|
from reflex.utils.registry import _get_npm_registry
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from reflex.app import App
|
||||||
|
|
||||||
CURRENTLY_INSTALLING_NODE = False
|
CURRENTLY_INSTALLING_NODE = False
|
||||||
|
|
||||||
|
|
||||||
|
class AppInfo(NamedTuple):
|
||||||
|
"""A tuple containing the app instance and module."""
|
||||||
|
|
||||||
|
app: App
|
||||||
|
module: ModuleType
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Template:
|
class Template:
|
||||||
"""A template for a Reflex app."""
|
"""A template for a Reflex app."""
|
||||||
@ -267,6 +278,22 @@ def windows_npm_escape_hatch() -> bool:
|
|||||||
return environment.REFLEX_USE_NPM.get()
|
return environment.REFLEX_USE_NPM.get()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_app_name(config: Config):
|
||||||
|
"""Check if the app name is set in the config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: The config object.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the app name is not set in the config.
|
||||||
|
"""
|
||||||
|
if not config.app_name:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot get the app module because `app_name` is not set in rxconfig! "
|
||||||
|
"If this error occurs in a reflex test case, ensure that `get_app` is mocked."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_app(reload: bool = False) -> ModuleType:
|
def get_app(reload: bool = False) -> ModuleType:
|
||||||
"""Get the app module based on the default config.
|
"""Get the app module based on the default config.
|
||||||
|
|
||||||
@ -277,22 +304,23 @@ def get_app(reload: bool = False) -> ModuleType:
|
|||||||
The app based on the default config.
|
The app based on the default config.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RuntimeError: If the app name is not set in the config.
|
Exception: If an error occurs while getting the app module.
|
||||||
"""
|
"""
|
||||||
from reflex.utils import telemetry
|
from reflex.utils import telemetry
|
||||||
|
|
||||||
try:
|
try:
|
||||||
environment.RELOAD_CONFIG.set(reload)
|
environment.RELOAD_CONFIG.set(reload)
|
||||||
config = get_config()
|
config = get_config()
|
||||||
if not config.app_name:
|
|
||||||
raise RuntimeError(
|
_check_app_name(config)
|
||||||
"Cannot get the app module because `app_name` is not set in rxconfig! "
|
|
||||||
"If this error occurs in a reflex test case, ensure that `get_app` is mocked."
|
|
||||||
)
|
|
||||||
module = config.module
|
module = config.module
|
||||||
sys.path.insert(0, str(Path.cwd()))
|
sys.path.insert(0, str(Path.cwd()))
|
||||||
app = __import__(module, fromlist=(constants.CompileVars.APP,))
|
app = (
|
||||||
|
__import__(module, fromlist=(constants.CompileVars.APP,))
|
||||||
|
if not config.app_module
|
||||||
|
else config.app_module
|
||||||
|
)
|
||||||
if reload:
|
if reload:
|
||||||
from reflex.state import reload_state_module
|
from reflex.state import reload_state_module
|
||||||
|
|
||||||
@ -301,11 +329,34 @@ def get_app(reload: bool = False) -> ModuleType:
|
|||||||
|
|
||||||
# Reload the app module.
|
# Reload the app module.
|
||||||
importlib.reload(app)
|
importlib.reload(app)
|
||||||
|
|
||||||
return app
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
telemetry.send_error(ex, context="frontend")
|
telemetry.send_error(ex, context="frontend")
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def get_and_validate_app(reload: bool = False) -> AppInfo:
|
||||||
|
"""Get the app instance based on the default config and validate it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reload: Re-import the app module from disk
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The app instance and the app module.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the app instance is not an instance of rx.App.
|
||||||
|
"""
|
||||||
|
from reflex.app import App
|
||||||
|
|
||||||
|
app_module = get_app(reload=reload)
|
||||||
|
app = getattr(app_module, constants.CompileVars.APP)
|
||||||
|
if not isinstance(app, App):
|
||||||
|
raise RuntimeError(
|
||||||
|
"The app instance in the specified app_module_import in rxconfig must be an instance of rx.App."
|
||||||
|
)
|
||||||
|
return AppInfo(app=app, module=app_module)
|
||||||
|
|
||||||
|
|
||||||
def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
|
def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
|
||||||
@ -318,8 +369,7 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
|
|||||||
Returns:
|
Returns:
|
||||||
The compiled app based on the default config.
|
The compiled app based on the default config.
|
||||||
"""
|
"""
|
||||||
app_module = get_app(reload=reload)
|
app, app_module = get_and_validate_app(reload=reload)
|
||||||
app = getattr(app_module, constants.CompileVars.APP)
|
|
||||||
# For py3.9 compatibility when redis is used, we MUST add any decorator pages
|
# For py3.9 compatibility when redis is used, we MUST add any decorator pages
|
||||||
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
||||||
app._apply_decorated_pages()
|
app._apply_decorated_pages()
|
||||||
@ -1153,11 +1203,12 @@ def ensure_reflex_installation_id() -> Optional[int]:
|
|||||||
if installation_id is None:
|
if installation_id is None:
|
||||||
installation_id = random.getrandbits(128)
|
installation_id = random.getrandbits(128)
|
||||||
installation_id_file.write_text(str(installation_id))
|
installation_id_file.write_text(str(installation_id))
|
||||||
# If we get here, installation_id is definitely set
|
|
||||||
return installation_id
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.debug(f"Failed to ensure reflex installation id: {e}")
|
console.debug(f"Failed to ensure reflex installation id: {e}")
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
# If we get here, installation_id is definitely set
|
||||||
|
return installation_id
|
||||||
|
|
||||||
|
|
||||||
def initialize_reflex_user_directory():
|
def initialize_reflex_user_directory():
|
||||||
@ -1371,19 +1422,22 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
|
|||||||
except OSError as ose:
|
except OSError as ose:
|
||||||
console.error(f"Failed to create temp directory for extracting zip: {ose}")
|
console.error(f"Failed to create temp directory for extracting zip: {ose}")
|
||||||
raise typer.Exit(1) from ose
|
raise typer.Exit(1) from ose
|
||||||
|
|
||||||
try:
|
try:
|
||||||
zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
|
zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
|
||||||
# The zip file downloaded from github looks like:
|
# The zip file downloaded from github looks like:
|
||||||
# repo-name-branch/**/*, so we need to remove the top level directory.
|
# repo-name-branch/**/*, so we need to remove the top level directory.
|
||||||
if len(subdirs := os.listdir(unzip_dir)) != 1:
|
|
||||||
console.error(f"Expected one directory in the zip, found {subdirs}")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
template_dir = unzip_dir / subdirs[0]
|
|
||||||
console.debug(f"Template folder is located at {template_dir}")
|
|
||||||
except Exception as uze:
|
except Exception as uze:
|
||||||
console.error(f"Failed to unzip the template: {uze}")
|
console.error(f"Failed to unzip the template: {uze}")
|
||||||
raise typer.Exit(1) from uze
|
raise typer.Exit(1) from uze
|
||||||
|
|
||||||
|
if len(subdirs := os.listdir(unzip_dir)) != 1:
|
||||||
|
console.error(f"Expected one directory in the zip, found {subdirs}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
template_dir = unzip_dir / subdirs[0]
|
||||||
|
console.debug(f"Template folder is located at {template_dir}")
|
||||||
|
|
||||||
# Move the rxconfig file here first.
|
# Move the rxconfig file here first.
|
||||||
path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE)
|
path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE)
|
||||||
new_config = get_config(reload=True)
|
new_config = get_config(reload=True)
|
||||||
|
@ -156,9 +156,10 @@ def _prepare_event(event: str, **kwargs) -> dict:
|
|||||||
def _send_event(event_data: dict) -> bool:
|
def _send_event(event_data: dict) -> bool:
|
||||||
try:
|
try:
|
||||||
httpx.post(POSTHOG_API_URL, json=event_data)
|
httpx.post(POSTHOG_API_URL, json=event_data)
|
||||||
return True
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _send(event, telemetry_enabled, **kwargs):
|
def _send(event, telemetry_enabled, **kwargs):
|
||||||
|
@ -71,9 +71,10 @@ def has_error_modal(driver: WebDriver) -> bool:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
driver.find_element(By.XPATH, CONNECTION_ERROR_XPATH)
|
driver.find_element(By.XPATH, CONNECTION_ERROR_XPATH)
|
||||||
return True
|
|
||||||
except NoSuchElementException:
|
except NoSuchElementException:
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -27,7 +27,7 @@ from reflex.event import (
|
|||||||
from reflex.state import BaseState
|
from reflex.state import BaseState
|
||||||
from reflex.style import Style
|
from reflex.style import Style
|
||||||
from reflex.utils import imports
|
from reflex.utils import imports
|
||||||
from reflex.utils.exceptions import EventFnArgMismatchError
|
from reflex.utils.exceptions import ChildrenTypeError, EventFnArgMismatchError
|
||||||
from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports
|
from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports
|
||||||
from reflex.vars import VarData
|
from reflex.vars import VarData
|
||||||
from reflex.vars.base import LiteralVar, Var
|
from reflex.vars.base import LiteralVar, Var
|
||||||
@ -645,14 +645,17 @@ def test_create_filters_none_props(test_component):
|
|||||||
assert str(component.style["text-align"]) == '"center"'
|
assert str(component.style["text-align"]) == '"center"'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("children", [((None,),), ("foo", ("bar", (None,)))])
|
@pytest.mark.parametrize(
|
||||||
|
"children",
|
||||||
|
[
|
||||||
|
((None,),),
|
||||||
|
("foo", ("bar", (None,))),
|
||||||
|
({"foo": "bar"},),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_component_create_unallowed_types(children, test_component):
|
def test_component_create_unallowed_types(children, test_component):
|
||||||
with pytest.raises(TypeError) as err:
|
with pytest.raises(ChildrenTypeError):
|
||||||
test_component.create(*children)
|
test_component.create(*children)
|
||||||
assert (
|
|
||||||
err.value.args[0]
|
|
||||||
== "Children of Reflex components must be other components, state vars, or primitive Python types. Got child None of type <class 'NoneType'>."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
Loading…
Reference in New Issue
Block a user