add unit tests

This commit is contained in:
Elijah 2025-01-21 15:16:26 +00:00
parent 76f7e4c31c
commit 1e3ccf3bc1
3 changed files with 207 additions and 28 deletions

View File

@ -555,6 +555,7 @@ def deploy(
**extra, **extra,
) )
@cli.command() @cli.command()
def rename( def rename(
new_name: str = typer.Argument(..., help="The new name for the app."), new_name: str = typer.Argument(..., help="The new name for the app."),

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import ast
import contextlib import contextlib
import dataclasses import dataclasses
import functools import functools
@ -26,7 +25,6 @@ from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Callable, List, NamedTuple, Optional from typing import Callable, List, NamedTuple, Optional
import astor
import httpx import httpx
import typer import typer
from alembic.util.exc import CommandError from alembic.util.exc import CommandError
@ -480,9 +478,8 @@ def validate_app_name(app_name: str | None = None) -> str:
return app_name return app_name
def rename_path_up_tree(full_path, old_name, new_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`.
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`. The renaming stops when we reach the directory containing `rxconfig.py`.
Args: Args:
@ -491,7 +488,7 @@ def rename_path_up_tree(full_path, old_name, new_name):
new_name: The replacement name. new_name: The replacement name.
Returns: Returns:
Path: The updated path after renaming. The updated path after renaming.
""" """
current_path = Path(full_path) current_path = Path(full_path)
new_path = None new_path = None
@ -521,7 +518,9 @@ def rename_path_up_tree(full_path, old_name, new_name):
def rename_app(app_name: str): def rename_app(app_name: str):
"""Rename the app directory.""" """Rename the app directory."""
if not constants.Config.FILE.exists(): if not constants.Config.FILE.exists():
console.error("No rxconfig.py found. Make sure you are in the root directory of your app.") console.error(
"No rxconfig.py found. Make sure you are in the root directory of your app."
)
raise typer.Exit(1) raise typer.Exit(1)
config = get_config() config = get_config()
@ -534,16 +533,13 @@ def rename_app(app_name: str):
console.error(f"Could not find origin for module {config.module}.") console.error(f"Could not find origin for module {config.module}.")
raise typer.Exit(1) raise typer.Exit(1)
process_directory( process_directory(Path.cwd(), config.app_name, app_name, exclude_dirs=[".web"])
Path.cwd(), config.app_name, app_name, exclude_dirs=[".web"]
)
rename_path_up_tree(Path(module_path.origin), config.app_name, app_name) rename_path_up_tree(Path(module_path.origin), config.app_name, app_name)
def rename_imports_and_app_name(file_path, old_name, new_name): def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name: str):
""" """Rename imports and update the app_name in the file using string replacement.
Rename imports and update the app_name in the file using string replacement.
Handles both keyword and positional arguments for `rx.Config` and import statements. Handles both keyword and positional arguments for `rx.Config` and import statements.
""" """
file_path = Path(file_path) file_path = Path(file_path)
@ -551,15 +547,15 @@ def rename_imports_and_app_name(file_path, old_name, new_name):
# Replace `from old_name.` or `from old_name` with `from new_name` # Replace `from old_name.` or `from old_name` with `from new_name`
content = re.sub( content = re.sub(
rf'\bfrom {re.escape(old_name)}(\b|\.|\s)', rf"\bfrom {re.escape(old_name)}(\b|\.|\s)",
lambda match: f'from {new_name}{match.group(1)}', lambda match: f"from {new_name}{match.group(1)}",
content, content,
) )
# Replace `import old_name` with `import new_name` # Replace `import old_name` with `import new_name`
content = re.sub( content = re.sub(
rf'\bimport {re.escape(old_name)}\b', rf"\bimport {re.escape(old_name)}\b",
f'import {new_name}', f"import {new_name}",
content, content,
) )
@ -580,23 +576,29 @@ def rename_imports_and_app_name(file_path, old_name, new_name):
file_path.write_text(content) file_path.write_text(content)
def process_directory(
def process_directory(directory, old_name, new_name, exclude_dirs=None, extensions=None): directory: str | Path,
""" old_name: str,
Process files with specified extensions in a directory, excluding specified directories. new_name: str,
exclude_dirs: list | None = None,
extensions: list | None = None,
):
"""Process files with specified extensions in a directory, excluding specified directories.
Args: Args:
directory (str or Path): The root directory to process. directory: The root directory to process.
old_name (str): The old name to replace. old_name: The old name to replace.
new_name (str): The new name to use. new_name: The new name to use.
exclude_dirs (list, optional): List of directory names to exclude. Defaults to None. exclude_dirs: List of directory names to exclude. Defaults to None.
extensions (list, optional): List of file extensions to process. Defaults to [".py"]. extensions: List of file extensions to process. Defaults to [".py"].
""" """
exclude_dirs = exclude_dirs or [] exclude_dirs = exclude_dirs or []
extensions = extensions or [".py", ".md"] extensions = extensions or [".py", ".md"]
extensions_set = {ext.lstrip(".") for ext in extensions} extensions_set = {ext.lstrip(".") for ext in extensions}
directory = Path(directory) directory = Path(directory)
root_exclude_dirs = {directory / exclude_dir for exclude_dir in exclude_dirs}
files = ( files = (
p.resolve() p.resolve()
for p in directory.glob("**/*") for p in directory.glob("**/*")
@ -604,11 +606,12 @@ def process_directory(directory, old_name, new_name, exclude_dirs=None, extensio
) )
for file_path in files: for file_path in files:
if not any(part in exclude_dirs for part in file_path.parts): 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) rename_imports_and_app_name(file_path, old_name, new_name)
def create_config(app_name: str): def create_config(app_name: str):
"""Create a new rxconfig file. """Create a new rxconfig file.

View File

@ -1,20 +1,29 @@
import importlib.machinery
import json import json
import re import re
import shutil
import tempfile import tempfile
from pathlib import Path
from unittest.mock import Mock, mock_open from unittest.mock import Mock, mock_open
import pytest import pytest
from typer.testing import CliRunner
from reflex import constants from reflex import constants
from reflex.config import Config from reflex.config import Config
from reflex.reflex import cli
from reflex.testing import chdir
from reflex.utils.prerequisites import ( from reflex.utils.prerequisites import (
CpuInfo, CpuInfo,
_update_next_config, _update_next_config,
cached_procedure, cached_procedure,
get_cpu_info, get_cpu_info,
initialize_requirements_txt, initialize_requirements_txt,
rename_imports_and_app_name,
) )
runner = CliRunner()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config, export, expected_output", "config, export, expected_output",
@ -224,3 +233,169 @@ def test_get_cpu_info():
for attr in ("manufacturer_id", "model_name", "address_width"): for attr in ("manufacturer_id", "model_name", "address_width"):
value = getattr(cpu_info, attr) value = getattr(cpu_info, attr)
assert value.strip() if attr != "address_width" else value assert value.strip() if attr != "address_width" else value
@pytest.fixture
def temp_directory():
"""Create a temporary directory for tests."""
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):
"""Test renaming imports and app_name in a file."""
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):
"""Test regex edge cases in renaming."""
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(mocker, temp_directory):
"""Test the CLI rename command."""
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)
"""
)
mocker.patch(
"importlib.util.find_spec",
mocker.patch(
"importlib.util.find_spec",
return_value=importlib.machinery.ModuleSpec(
name="foo", loader=None, origin=str(Path(foo_dir / "foo" / "foo.py"))
),
),
)
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)
"""
)