From a4ee9855093b65ddf1b9a0088e523746824f4aae Mon Sep 17 00:00:00 2001 From: jackie-pc Date: Thu, 8 Feb 2024 11:12:20 -0800 Subject: [PATCH] CLI script to maintain Chakra backed components in rx namespace in older apps (#2322) --- reflex/reflex.py | 16 ++++ reflex/utils/prerequisites.py | 106 ++++++++++++++++++++++++ scripts/migrate_project_to_rx_chakra.py | 13 +++ 3 files changed, 135 insertions(+) create mode 100644 scripts/migrate_project_to_rx_chakra.py diff --git a/reflex/reflex.py b/reflex/reflex.py index 2ba0cc5a8b1..1426d33a69b 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -100,6 +100,9 @@ def _init( # Migrate Pynecone projects to Reflex. prerequisites.migrate_to_reflex() + if prerequisites.should_show_rx_chakra_migration_instructions(): + prerequisites.show_rx_chakra_migration_instructions() + # Initialize the .gitignore. prerequisites.initialize_gitignore() @@ -336,6 +339,7 @@ def logout( db_cli = typer.Typer() +script_cli = typer.Typer() def _skip_compile(): @@ -414,6 +418,17 @@ def makemigrations( ) +@script_cli.command( + name="keep-chakra", + help="Change all rx. references to rx.chakra., to preserve Chakra UI usage.", +) +def keep_chakra(): + """Change all rx. references to rx.chakra., to preserve Chakra UI usage.""" + from reflex.utils import prerequisites + + prerequisites.migrate_to_rx_chakra() + + @cli.command() def deploy( key: Optional[str] = typer.Option( @@ -555,6 +570,7 @@ def demo( cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.") +cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.") cli.add_typer( deployments_cli, name="deployments", diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 2b2109c22cd..909b31035b3 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -4,6 +4,7 @@ import glob import importlib +import inspect import json import os import platform @@ -25,6 +26,7 @@ from packaging import version from redis.asyncio import Redis +import reflex from reflex import constants, model from reflex.compiler import templates from reflex.config import Config, get_config @@ -938,6 +940,110 @@ def prompt_for_template() -> constants.Templates.Kind: return constants.Templates.Kind(template) +def should_show_rx_chakra_migration_instructions() -> bool: + """Should we show the migration instructions for rx.chakra.* => rx.*?. + + Returns: + bool: True if we should show the migration instructions. + """ + if os.getenv("REFLEX_PROMPT_MIGRATE_TO_RX_CHAKRA") == "yes": + return True + + with open(constants.Dirs.REFLEX_JSON, "r") as f: + data = json.load(f) + existing_init_reflex_version = data.get("version", None) + + if existing_init_reflex_version is None: + # They clone a reflex app from git for the first time. + # That app may or may not be 0.4 compatible. + # So let's just show these instructions THIS TIME. + return True + + if constants.Reflex.VERSION < "0.4": + return False + else: + return existing_init_reflex_version < "0.4" + + +def show_rx_chakra_migration_instructions(): + """Show the migration instructions for rx.chakra.* => rx.*.""" + console.log( + "Prior to reflex 0.4.0, rx.* components are based on Chakra UI. They are now based on Radix UI. To stick to Chakra UI, use rx.chakra.*." + ) + console.log("") + console.log( + "[bold]Run `reflex script keep-chakra` to automatically update your app." + ) + console.log("") + console.log("For more details, please see https://TODO") # TODO add link to docs + + +def migrate_to_rx_chakra(): + """Migrate rx.button => r.chakra.button, etc.""" + file_pattern = os.path.join(get_config().app_name, "**/*.py") + file_list = glob.glob(file_pattern, recursive=True) + + # Populate with all rx. components that have been moved to rx.chakra. + patterns = { + rf"\brx\.{name}\b": f"rx.chakra.{name}" + for name in _get_rx_chakra_component_to_migrate() + } + + for file_path in file_list: + with FileInput(file_path, inplace=True) as file: + for _line_num, line in enumerate(file): + for old, new in patterns.items(): + line = re.sub(old, new, line) + print(line, end="") + + +def _get_rx_chakra_component_to_migrate() -> set[str]: + from reflex.components import ChakraComponent + + rx_chakra_names = set(dir(reflex.chakra)) + + names_to_migrate = set() + whitelist = { + "CodeBlock", + "ColorModeIcon", + "MultiSelect", + "MultiSelectOption", + "base", + "code_block", + "color_mode_cond", + "color_mode_icon", + "multi_select", + "multi_select_option", + } + for rx_chakra_name in sorted(rx_chakra_names): + if rx_chakra_name.startswith("_"): + continue + + rx_chakra_object = getattr(reflex.chakra, rx_chakra_name) + try: + if ( + inspect.ismethod(rx_chakra_object) + and inspect.isclass(rx_chakra_object.__self__) + and issubclass(rx_chakra_object.__self__, ChakraComponent) + ): + names_to_migrate.add(rx_chakra_name) + + elif inspect.isclass(rx_chakra_object) and issubclass( + rx_chakra_object, ChakraComponent + ): + names_to_migrate.add(rx_chakra_name) + pass + else: + # For the given rx.chakra., does rx. exist? + # And of these, should we include in migration? + if hasattr(reflex, rx_chakra_name) and rx_chakra_name in whitelist: + names_to_migrate.add(rx_chakra_name) + + except Exception: + raise + return names_to_migrate + + def migrate_to_reflex(): """Migration from Pynecone to Reflex.""" # Check if the old config file exists. diff --git a/scripts/migrate_project_to_rx_chakra.py b/scripts/migrate_project_to_rx_chakra.py new file mode 100644 index 00000000000..b13cccafd43 --- /dev/null +++ b/scripts/migrate_project_to_rx_chakra.py @@ -0,0 +1,13 @@ +"""Migrate project to rx.chakra. I.e. switch usage of rx. to rx.chakra..""" + +import argparse + +if __name__ == "__main__": + # parse args just for the help message (-h, etc) + parser = argparse.ArgumentParser( + description="Migrate project to rx.chakra. I.e. switch usage of rx. to rx.chakra.." + ) + args = parser.parse_args() + from reflex.utils.prerequisites import migrate_to_rx_chakra + + migrate_to_rx_chakra()