[ENG-1796]reflex rename
- B (#4668)
* `reflex rename`- B * add unit tests * precommit * dont need this comment * move loglevel * calm down, darglint * add current dir to sys path
This commit is contained in:
parent
818788da63
commit
1ca36fa6c1
@ -28,6 +28,8 @@ class Ext(SimpleNamespace):
|
||||
ZIP = ".zip"
|
||||
# The extension for executable files on Windows.
|
||||
EXE = ".exe"
|
||||
# The extension for markdown files.
|
||||
MD = ".md"
|
||||
|
||||
|
||||
class CompileVars(SimpleNamespace):
|
||||
|
@ -573,6 +573,20 @@ def deploy(
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def rename(
|
||||
new_name: str = typer.Argument(..., help="The new name for the app."),
|
||||
loglevel: constants.LogLevel = typer.Option(
|
||||
config.loglevel, help="The log level to use."
|
||||
),
|
||||
):
|
||||
"""Rename the app in the current directory."""
|
||||
from reflex.utils import prerequisites
|
||||
|
||||
prerequisites.validate_app_name(new_name)
|
||||
prerequisites.rename_app(new_name, loglevel)
|
||||
|
||||
|
||||
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(
|
||||
|
@ -7,6 +7,7 @@ import dataclasses
|
||||
import functools
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
@ -463,6 +464,167 @@ def validate_app_name(app_name: str | None = None) -> str:
|
||||
return app_name
|
||||
|
||||
|
||||
def rename_path_up_tree(full_path: str | Path, old_name: str, new_name: str) -> Path:
|
||||
"""Rename all instances of `old_name` in the path (file and directories) to `new_name`.
|
||||
The renaming stops when we reach the directory containing `rxconfig.py`.
|
||||
|
||||
Args:
|
||||
full_path: The full path to start renaming from.
|
||||
old_name: The name to be replaced.
|
||||
new_name: The replacement name.
|
||||
|
||||
Returns:
|
||||
The updated path after renaming.
|
||||
"""
|
||||
current_path = Path(full_path)
|
||||
new_path = None
|
||||
|
||||
while True:
|
||||
directory, base = current_path.parent, current_path.name
|
||||
# Stop renaming when we reach the root dir (which contains rxconfig.py)
|
||||
if current_path.is_dir() and (current_path / "rxconfig.py").exists():
|
||||
new_path = current_path
|
||||
break
|
||||
|
||||
if old_name == base.removesuffix(constants.Ext.PY):
|
||||
new_base = base.replace(old_name, new_name)
|
||||
new_path = directory / new_base
|
||||
current_path.rename(new_path)
|
||||
console.debug(f"Renamed {current_path} -> {new_path}")
|
||||
current_path = new_path
|
||||
else:
|
||||
new_path = current_path
|
||||
|
||||
# Move up the directory tree
|
||||
current_path = directory
|
||||
|
||||
return new_path
|
||||
|
||||
|
||||
def rename_app(new_app_name: str, loglevel: constants.LogLevel):
|
||||
"""Rename the app directory.
|
||||
|
||||
Args:
|
||||
new_app_name: The new name for the app.
|
||||
loglevel: The log level to use.
|
||||
|
||||
Raises:
|
||||
Exit: If the command is not ran in the root dir or the app module cannot be imported.
|
||||
"""
|
||||
# Set the log level.
|
||||
console.set_log_level(loglevel)
|
||||
|
||||
if not constants.Config.FILE.exists():
|
||||
console.error(
|
||||
"No rxconfig.py found. Make sure you are in the root directory of your app."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
config = get_config()
|
||||
module_path = importlib.util.find_spec(config.module)
|
||||
if module_path is None:
|
||||
console.error(f"Could not find module {config.module}.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not module_path.origin:
|
||||
console.error(f"Could not find origin for module {config.module}.")
|
||||
raise typer.Exit(1)
|
||||
console.info(f"Renaming app directory to {new_app_name}.")
|
||||
process_directory(
|
||||
Path.cwd(),
|
||||
config.app_name,
|
||||
new_app_name,
|
||||
exclude_dirs=[constants.Dirs.WEB, constants.Dirs.APP_ASSETS],
|
||||
)
|
||||
|
||||
rename_path_up_tree(Path(module_path.origin), config.app_name, new_app_name)
|
||||
|
||||
console.success(f"App directory renamed to [bold]{new_app_name}[/bold].")
|
||||
|
||||
|
||||
def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name: str):
|
||||
"""Rename imports the file using string replacement as well as app_name in rxconfig.py.
|
||||
|
||||
Args:
|
||||
file_path: The file to process.
|
||||
old_name: The old name to replace.
|
||||
new_name: The new name to use.
|
||||
"""
|
||||
file_path = Path(file_path)
|
||||
content = file_path.read_text()
|
||||
|
||||
# Replace `from old_name.` or `from old_name` with `from new_name`
|
||||
content = re.sub(
|
||||
rf"\bfrom {re.escape(old_name)}(\b|\.|\s)",
|
||||
lambda match: f"from {new_name}{match.group(1)}",
|
||||
content,
|
||||
)
|
||||
|
||||
# Replace `import old_name` with `import new_name`
|
||||
content = re.sub(
|
||||
rf"\bimport {re.escape(old_name)}\b",
|
||||
f"import {new_name}",
|
||||
content,
|
||||
)
|
||||
|
||||
# Replace `app_name="old_name"` in rx.Config
|
||||
content = re.sub(
|
||||
rf'\bapp_name\s*=\s*["\']{re.escape(old_name)}["\']',
|
||||
f'app_name="{new_name}"',
|
||||
content,
|
||||
)
|
||||
|
||||
# Replace positional argument `"old_name"` in rx.Config
|
||||
content = re.sub(
|
||||
rf'\brx\.Config\(\s*["\']{re.escape(old_name)}["\']',
|
||||
f'rx.Config("{new_name}"',
|
||||
content,
|
||||
)
|
||||
|
||||
file_path.write_text(content)
|
||||
|
||||
|
||||
def process_directory(
|
||||
directory: str | Path,
|
||||
old_name: str,
|
||||
new_name: str,
|
||||
exclude_dirs: list | None = None,
|
||||
extensions: list | None = None,
|
||||
):
|
||||
"""Process files with specified extensions in a directory, excluding specified directories.
|
||||
|
||||
Args:
|
||||
directory: The root directory to process.
|
||||
old_name: The old name to replace.
|
||||
new_name: The new name to use.
|
||||
exclude_dirs: List of directory names to exclude. Defaults to None.
|
||||
extensions: List of file extensions to process.
|
||||
"""
|
||||
exclude_dirs = exclude_dirs or []
|
||||
extensions = extensions or [
|
||||
constants.Ext.PY,
|
||||
constants.Ext.MD,
|
||||
] # include .md files, typically used in reflex-web.
|
||||
extensions_set = {ext.lstrip(".") for ext in extensions}
|
||||
directory = Path(directory)
|
||||
|
||||
root_exclude_dirs = {directory / exclude_dir for exclude_dir in exclude_dirs}
|
||||
|
||||
files = (
|
||||
p.resolve()
|
||||
for p in directory.glob("**/*")
|
||||
if p.is_file() and p.suffix.lstrip(".") in extensions_set
|
||||
)
|
||||
|
||||
for file_path in files:
|
||||
if not any(
|
||||
file_path.is_relative_to(exclude_dir) for exclude_dir in root_exclude_dirs
|
||||
):
|
||||
rename_imports_and_app_name(file_path, old_name, new_name)
|
||||
|
||||
|
||||
def create_config(app_name: str):
|
||||
"""Create a new rxconfig file.
|
||||
|
||||
|
@ -1,20 +1,28 @@
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, mock_open
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from reflex import constants
|
||||
from reflex.config import Config
|
||||
from reflex.reflex import cli
|
||||
from reflex.testing import chdir
|
||||
from reflex.utils.prerequisites import (
|
||||
CpuInfo,
|
||||
_update_next_config,
|
||||
cached_procedure,
|
||||
get_cpu_info,
|
||||
initialize_requirements_txt,
|
||||
rename_imports_and_app_name,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config, export, expected_output",
|
||||
@ -224,3 +232,156 @@ def test_get_cpu_info():
|
||||
for attr in ("manufacturer_id", "model_name", "address_width"):
|
||||
value = getattr(cpu_info, attr)
|
||||
assert value.strip() if attr != "address_width" else value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_directory():
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
yield Path(temp_dir)
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config_code,expected",
|
||||
[
|
||||
("rx.Config(app_name='old_name')", 'rx.Config(app_name="new_name")'),
|
||||
('rx.Config(app_name="old_name")', 'rx.Config(app_name="new_name")'),
|
||||
("rx.Config('old_name')", 'rx.Config("new_name")'),
|
||||
('rx.Config("old_name")', 'rx.Config("new_name")'),
|
||||
],
|
||||
)
|
||||
def test_rename_imports_and_app_name(temp_directory, config_code, expected):
|
||||
file_path = temp_directory / "rxconfig.py"
|
||||
content = f"""
|
||||
config = {config_code}
|
||||
"""
|
||||
file_path.write_text(content)
|
||||
|
||||
rename_imports_and_app_name(file_path, "old_name", "new_name")
|
||||
|
||||
updated_content = file_path.read_text()
|
||||
expected_content = f"""
|
||||
config = {expected}
|
||||
"""
|
||||
assert updated_content == expected_content
|
||||
|
||||
|
||||
def test_regex_edge_cases(temp_directory):
|
||||
file_path = temp_directory / "example.py"
|
||||
content = """
|
||||
from old_name.module import something
|
||||
import old_name
|
||||
from old_name import something_else as alias
|
||||
from old_name
|
||||
"""
|
||||
file_path.write_text(content)
|
||||
|
||||
rename_imports_and_app_name(file_path, "old_name", "new_name")
|
||||
|
||||
updated_content = file_path.read_text()
|
||||
expected_content = """
|
||||
from new_name.module import something
|
||||
import new_name
|
||||
from new_name import something_else as alias
|
||||
from new_name
|
||||
"""
|
||||
assert updated_content == expected_content
|
||||
|
||||
|
||||
def test_cli_rename_command(temp_directory):
|
||||
foo_dir = temp_directory / "foo"
|
||||
foo_dir.mkdir()
|
||||
(foo_dir / "__init__").touch()
|
||||
(foo_dir / ".web").mkdir()
|
||||
(foo_dir / "assets").mkdir()
|
||||
(foo_dir / "foo").mkdir()
|
||||
(foo_dir / "foo" / "__init__.py").touch()
|
||||
(foo_dir / "rxconfig.py").touch()
|
||||
(foo_dir / "rxconfig.py").write_text(
|
||||
"""
|
||||
import reflex as rx
|
||||
|
||||
config = rx.Config(
|
||||
app_name="foo",
|
||||
)
|
||||
"""
|
||||
)
|
||||
(foo_dir / "foo" / "components").mkdir()
|
||||
(foo_dir / "foo" / "components" / "__init__.py").touch()
|
||||
(foo_dir / "foo" / "components" / "base.py").touch()
|
||||
(foo_dir / "foo" / "components" / "views.py").touch()
|
||||
(foo_dir / "foo" / "components" / "base.py").write_text(
|
||||
"""
|
||||
import reflex as rx
|
||||
from foo.components import views
|
||||
from foo.components.views import *
|
||||
from .base import *
|
||||
|
||||
def random_component():
|
||||
return rx.fragment()
|
||||
"""
|
||||
)
|
||||
(foo_dir / "foo" / "foo.py").touch()
|
||||
(foo_dir / "foo" / "foo.py").write_text(
|
||||
"""
|
||||
import reflex as rx
|
||||
import foo.components.base
|
||||
from foo.components.base import random_component
|
||||
|
||||
class State(rx.State):
|
||||
pass
|
||||
|
||||
|
||||
def index():
|
||||
return rx.text("Hello, World!")
|
||||
|
||||
app = rx.App()
|
||||
app.add_page(index)
|
||||
"""
|
||||
)
|
||||
|
||||
with chdir(temp_directory / "foo"):
|
||||
result = runner.invoke(cli, ["rename", "bar"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (foo_dir / "rxconfig.py").read_text() == (
|
||||
"""
|
||||
import reflex as rx
|
||||
|
||||
config = rx.Config(
|
||||
app_name="bar",
|
||||
)
|
||||
"""
|
||||
)
|
||||
assert (foo_dir / "bar").exists()
|
||||
assert not (foo_dir / "foo").exists()
|
||||
assert (foo_dir / "bar" / "components" / "base.py").read_text() == (
|
||||
"""
|
||||
import reflex as rx
|
||||
from bar.components import views
|
||||
from bar.components.views import *
|
||||
from .base import *
|
||||
|
||||
def random_component():
|
||||
return rx.fragment()
|
||||
"""
|
||||
)
|
||||
assert (foo_dir / "bar" / "bar.py").exists()
|
||||
assert not (foo_dir / "bar" / "foo.py").exists()
|
||||
assert (foo_dir / "bar" / "bar.py").read_text() == (
|
||||
"""
|
||||
import reflex as rx
|
||||
import bar.components.base
|
||||
from bar.components.base import random_component
|
||||
|
||||
class State(rx.State):
|
||||
pass
|
||||
|
||||
|
||||
def index():
|
||||
return rx.text("Hello, World!")
|
||||
|
||||
app = rx.App()
|
||||
app.add_page(index)
|
||||
"""
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user