diff --git a/reflex/app.py b/reflex/app.py index 8af7cb19f43..11aac045448 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1152,11 +1152,12 @@ def _validate_var_dependencies(self, state: type[BaseState] | None = None) -> No for substate in state.class_subclasses: self._validate_var_dependencies(substate) - def _compile(self, export: bool = False): + def _compile(self, export: bool = False, dry_run: bool = False): """Compile the app and output it to the pages folder. Args: export: Whether to compile the app for export. + dry_run: Whether to compile the app without saving it. Raises: ReflexRuntimeError: When any page uses state, but no rx.State subclass is defined. @@ -1170,7 +1171,7 @@ def get_compilation_time() -> str: should_compile = self._should_compile() backend_dir = prerequisites.get_backend_dir() - if not should_compile and backend_dir.exists(): + if not dry_run and not should_compile and backend_dir.exists(): stateful_pages_marker = backend_dir / constants.Dirs.STATEFUL_PAGES if stateful_pages_marker.exists(): with stateful_pages_marker.open("r") as f: @@ -1205,7 +1206,7 @@ def get_compilation_time() -> str: if config.react_strict_mode: app_wrappers[(200, "StrictMode")] = StrictMode.create() - if not should_compile: + if not should_compile and not dry_run: with console.timing("Evaluate Pages (Backend)"): for route in self._unevaluated_pages: console.debug(f"Evaluating page: {route}") @@ -1358,7 +1359,7 @@ def memoized_toast_provider(): # Copy the assets. assets_src = Path.cwd() / constants.Dirs.APP_ASSETS - if assets_src.is_dir(): + if assets_src.is_dir() and not dry_run: with console.timing("Copy assets"): path_ops.update_directory_tree( src=assets_src, @@ -1444,6 +1445,9 @@ def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs): progress.advance(task) progress.stop() + if dry_run: + return + # Install frontend packages. with console.timing("Install Frontend Packages"): self._get_frontend_packages(all_imports) diff --git a/reflex/reflex.py b/reflex/reflex.py index 9ec65ab586c..2715f7b31b7 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -349,6 +349,30 @@ def run( ) +@cli.command() +@loglevel_option +@click.option( + "--dry", + is_flag=True, + default=False, + help="Run the command without making any changes.", +) +def compile(dry: bool): + """Compile the app in the current directory.""" + import time + + from reflex.utils import prerequisites + + # Check the app. + if prerequisites.needs_reinit(): + _init(name=get_config().app_name) + get_config(reload=True) + starting_time = time.monotonic() + prerequisites.compile_app(dry_run=dry) + elapsed_time = time.monotonic() - starting_time + console.success(f"App compiled successfully in {elapsed_time:.3f} seconds.") + + @cli.command() @loglevel_option @click.option( diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index d3c3c7c4242..13c2732836f 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -432,12 +432,15 @@ def validate_app(reload: bool = False) -> None: get_and_validate_app(reload=reload) -def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType: +def get_compiled_app( + reload: bool = False, export: bool = False, dry_run: bool = False +) -> ModuleType: """Get the app module based on the default config after first compiling it. Args: reload: Re-import the app module from disk export: Compile the app for export + dry_run: If True, do not write the compiled app to disk. Returns: The compiled app based on the default config. @@ -446,18 +449,21 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType: # 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). app._apply_decorated_pages() - app._compile(export=export) + app._compile(export=export, dry_run=dry_run) return app_module -def compile_app(reload: bool = False, export: bool = False) -> None: +def compile_app( + reload: bool = False, export: bool = False, dry_run: bool = False +) -> None: """Compile the app module based on the default config. Args: reload: Re-import the app module from disk export: Compile the app for export + dry_run: If True, do not write the compiled app to disk. """ - get_compiled_app(reload=reload, export=export) + get_compiled_app(reload=reload, export=export, dry_run=dry_run) def _can_colorize() -> bool: diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 795864135ae..871e150ee53 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -28,11 +28,7 @@ from typing import get_origin as get_origin_og from typing import get_type_hints as get_type_hints_og -import sqlalchemy from pydantic.v1.fields import ModelField -from sqlalchemy.ext.associationproxy import AssociationProxyInstance -from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import DeclarativeBase, Mapped, QueryableAttribute, Relationship from typing_extensions import Self as Self from typing_extensions import is_typeddict from typing_extensions import override as override @@ -322,6 +318,8 @@ def get_property_hint(attr: Any | None) -> GenericType | None: Returns: The type hint of the property, if it is a property, else None. """ + from sqlalchemy.ext.hybrid import hybrid_property + if not isinstance(attr, (property, hybrid_property)): return None hints = get_type_hints(attr.fget) @@ -340,6 +338,10 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None Returns: The type of the attribute, if accessible, or None """ + import sqlalchemy + from sqlalchemy.ext.associationproxy import AssociationProxyInstance + from sqlalchemy.orm import DeclarativeBase, Mapped, QueryableAttribute, Relationship + from reflex.model import Model try: diff --git a/reflex/vars/base.py b/reflex/vars/base.py index bc1dd5a7daf..2be86877b86 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -39,7 +39,6 @@ ) from rich.markup import escape -from sqlalchemy.orm import DeclarativeBase from typing_extensions import deprecated, override from reflex import constants @@ -3318,18 +3317,17 @@ def dispatch( ).guess_type() -V = TypeVar("V") - -BASE_TYPE = TypeVar("BASE_TYPE", bound=Base | None) -SQLA_TYPE = TypeVar("SQLA_TYPE", bound=DeclarativeBase | None) - if TYPE_CHECKING: from _typeshed import DataclassInstance + from sqlalchemy.orm import DeclarativeBase + SQLA_TYPE = TypeVar("SQLA_TYPE", bound=DeclarativeBase | None) + BASE_TYPE = TypeVar("BASE_TYPE", bound=Base | None) DATACLASS_TYPE = TypeVar("DATACLASS_TYPE", bound=DataclassInstance | None) + MAPPING_TYPE = TypeVar("MAPPING_TYPE", bound=Mapping | None) + V = TypeVar("V") FIELD_TYPE = TypeVar("FIELD_TYPE") -MAPPING_TYPE = TypeVar("MAPPING_TYPE", bound=Mapping | None) class Field(Generic[FIELD_TYPE]):