瀏覽代碼

`reflex rename`

Elijah 3 月之前
父節點
當前提交
ce1886d413
共有 2 個文件被更改,包括 124 次插入0 次删除
  1. 16 0
      reflex/reflex.py
  2. 108 0
      reflex/utils/prerequisites.py

+ 16 - 0
reflex/reflex.py

@@ -551,6 +551,22 @@ def deploy(
     )
     )
 
 
 
 
+@cli.command()
+def rename(
+    new_name: str = typer.Argument(
+        help="The new name of the app.",
+    ),
+    loglevel: constants.LogLevel = typer.Option(
+        config.loglevel, help="The log level to use."
+    ),
+):
+    """Rename the app."""
+    from reflex.utils import prerequisites
+
+    new_name = prerequisites.validate_app_name(new_name)
+    prerequisites.rename_app(new_name)
+
+
 cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
 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(script_cli, name="script", help="Subcommands running helper scripts.")
 cli.add_typer(
 cli.add_typer(

+ 108 - 0
reflex/utils/prerequisites.py

@@ -2,11 +2,13 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+import ast
 import contextlib
 import contextlib
 import dataclasses
 import dataclasses
 import functools
 import functools
 import importlib
 import importlib
 import importlib.metadata
 import importlib.metadata
+import importlib.util
 import json
 import json
 import os
 import os
 import platform
 import platform
@@ -23,6 +25,7 @@ from pathlib import Path
 from types import ModuleType
 from types import ModuleType
 from typing import Callable, List, Optional
 from typing import Callable, List, Optional
 
 
+import astor
 import httpx
 import httpx
 import typer
 import typer
 from alembic.util.exc import CommandError
 from alembic.util.exc import CommandError
@@ -427,6 +430,111 @@ def validate_app_name(app_name: str | None = None) -> str:
     return app_name
     return app_name
 
 
 
 
+class ImportRenamer(ast.NodeTransformer):
+    """Rename imports in a tree."""
+
+    def __init__(self, old_name, new_name):
+        """Initialize the ImportRenamer."""
+        self.old_name = old_name
+        self.new_name = new_name
+
+    def visit_Import(self, node):
+        """Rename imports of the form `import foo`."""
+        for alias in node.names:
+            if alias.name == self.old_name:
+                alias.name = self.new_name
+        return node
+
+    def visit_ImportFrom(self, node):
+        """Rename imports of the form `from foo import bar`."""
+        if node.module == self.old_name:
+            node.module = self.new_name
+        return node
+
+    def visit_Assign(self, node):
+        """Handle assignments like `config = rx.Config(app_name='foo')`."""
+        if (
+            isinstance(node.targets[0], ast.Name)
+            and node.targets[0].id == "config"
+            and isinstance(node.value, ast.Call)
+            and isinstance(node.value.func, ast.Attribute)
+            and node.value.func.attr == "Config"
+        ):
+            for kw in node.value.keywords:
+                if kw.arg == "app_name" and isinstance(kw.value, ast.Constant):
+                    if kw.value.value == self.old_name:
+                        kw.value = ast.Constant(value=self.new_name)
+
+            # Handle positional arguments
+            if node.value.args and isinstance(node.value.args[0], ast.Constant):
+                if node.value.args[0].value == self.old_name:
+                    node.value.args[0] = ast.Constant(value=self.new_name)
+        return node
+
+
+def rename_imports_and_app_name_in_file(file_path, old_name, new_name):
+    """Rename imports and update the app_name in rxconfig.py."""
+    file_path = Path(file_path)
+    content = file_path.read_text()
+
+    tree = ast.parse(content)
+
+    transformer = ImportRenamer(old_name, new_name)
+    new_tree = transformer.visit(tree)
+    ast.fix_missing_locations(new_tree)
+
+    modified_content = astor.to_source(new_tree)
+
+    file_path.write_text(modified_content)
+
+
+def process_directory(directory, old_name, new_name, exclude_dirs=None):
+    """Process all Python files in a directory, excluding specified directories."""
+    exclude_dirs = exclude_dirs or []
+    directory = Path(directory)
+
+    for root in directory.rglob("*.py"):
+        if not any(root.parts[i] in exclude_dirs for i in range(len(root.parts))):
+            rename_imports_and_app_name_in_file(root, old_name, new_name)
+
+
+def rename_path_up_tree(full_path, old_name, new_name):
+    """Rename all instances of `old_name` in the path (file and directories) to `new_name`."""
+    current_path = Path(full_path)
+    new_path = None
+
+    while True:
+        directory, base = current_path.parent, current_path.name
+
+        if old_name in base:
+            new_base = base.replace(old_name, new_name)
+            new_path = directory / new_base
+            current_path.rename(new_path)
+            current_path = new_path
+        else:
+            new_path = current_path
+
+        # Stop if we've reached the root package
+        if old_name not in directory.name:
+            break
+
+        # Move up the directory tree
+        current_path = directory
+
+    return new_path
+
+
+def rename_app(app_name: str):
+    """Rename the app directory."""
+    config = get_config()
+    process_directory(
+        Path.cwd(), config.app_name, app_name, exclude_dirs=["assets", ".web"]
+    )
+
+    full_path = importlib.util.find_spec(config.module).origin
+    rename_path_up_tree(full_path, config.app_name, app_name)
+
+
 def create_config(app_name: str):
 def create_config(app_name: str):
     """Create a new rxconfig file.
     """Create a new rxconfig file.