1
0
Khaleel Al-Adhami 1 сар өмнө
parent
commit
9ea4a78865

+ 0 - 1
pyproject.toml

@@ -42,7 +42,6 @@ dependencies = [
   "setuptools >=75.0",
   "starlette-admin >=0.11.0,<1.0",
   "sqlmodel >=0.0.14,<0.1",
-  "termcolor >=2.5.0,<2.6",
   "tomlkit >=0.12.4,<1.0",
   "twine >=4.0.0,<7.0",
   "typer >=0.15.1,<1.0",

+ 5 - 5
reflex/custom_components/custom_components.py

@@ -338,7 +338,7 @@ def init(
     # Check the name follows the convention if picked.
     name_variants = _validate_library_name(library_name)
 
-    console.rule(f"Initializing {name_variants.package_name} project", bold=True)
+    console.rule(f"Initializing {name_variants.package_name} project")
 
     _populate_custom_component_project(name_variants)
 
@@ -351,14 +351,14 @@ def init(
 
     if install:
         package_name = name_variants.package_name
-        console.rule(f"Installing {package_name} in editable mode.", bold=True)
+        console.rule(f"Installing {package_name} in editable mode.")
         if _pip_install_on_demand(package_name=".", install_args=["-e"]):
             console.info(f"Package {package_name} installed!")
         else:
             raise typer.Exit(code=1)
 
     console.success("Custom component initialized successfully!", bold=True)
-    console.rule("Project Summary", bold=True)
+    console.rule("Project Summary")
     console.print(
         f"[{CustomComponents.PACKAGE_README}]: Package description. Please add usage examples."
     )
@@ -829,7 +829,7 @@ def _collect_details_for_gallery():
     import reflex_cli.constants
     from reflex_cli.utils import hosting
 
-    console.rule("[bold]Authentication with Reflex Services")
+    console.rule("Authentication with Reflex Services")
     console.print("First let's log in to Reflex backend services.")
     access_token, _ = hosting.authenticated_token()
 
@@ -839,7 +839,7 @@ def _collect_details_for_gallery():
         )
         raise typer.Exit(code=1)
 
-    console.rule("[bold]Custom Component Information")
+    console.rule("Custom Component Information")
     params = {}
     package_name = None
     try:

+ 7 - 2
reflex/model.py

@@ -24,6 +24,7 @@ from reflex.base import Base
 from reflex.config import environment, get_config
 from reflex.utils import console
 from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key
+from reflex.utils.terminal import colored
 
 _ENGINE: dict[str, sqlalchemy.engine.Engine] = {}
 _ASYNC_ENGINE: dict[str, sqlalchemy.ext.asyncio.AsyncEngine] = {}
@@ -91,7 +92,9 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine:
 
     if not environment.ALEMBIC_CONFIG.get().exists():
         console.warn(
-            "Database is not initialized, run [bold]reflex db init[/bold] first."
+            "Database is not initialized, run "
+            + colored("reflex db init", attrs=("bold",))
+            + " first."
         )
     _ENGINE[url] = sqlmodel.create_engine(
         url,
@@ -133,7 +136,9 @@ def get_async_engine(url: str | None) -> sqlalchemy.ext.asyncio.AsyncEngine:
 
     if not environment.ALEMBIC_CONFIG.get().exists():
         console.warn(
-            "Database is not initialized, run [bold]reflex db init[/bold] first."
+            "Database is not initialized, run "
+            + colored("reflex db init", attrs=("bold",))
+            + " first."
         )
     _ASYNC_ENGINE[url] = sqlalchemy.ext.asyncio.create_async_engine(
         url,

+ 10 - 6
reflex/reflex.py

@@ -15,6 +15,7 @@ from reflex.custom_components.custom_components import custom_components_cli
 from reflex.state import reset_disk_state_manager
 from reflex.utils import console, redir, telemetry
 from reflex.utils.exec import should_use_granian
+from reflex.utils.terminal import colored
 
 # Disable typer+rich integration for help panels
 typer.core.rich = None  # pyright: ignore [reportPrivateImportUsage]
@@ -79,7 +80,7 @@ def _init(
 
     # Validate the app name.
     app_name = prerequisites.validate_app_name(name)
-    console.rule(f"Initializing {app_name}", bold=True)
+    console.rule(f"Initializing {app_name}")
 
     # Check prerequisites.
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
@@ -200,7 +201,7 @@ def _run(
     # Reload the config to make sure the env vars are persistent.
     get_config(reload=True)
 
-    console.rule("Starting Reflex App", bold=True)
+    console.rule("Starting Reflex App")
 
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
 
@@ -458,9 +459,10 @@ def db_init():
     if environment.ALEMBIC_CONFIG.get().exists():
         console.error(
             "Database is already initialized. Use "
-            "[bold]reflex db makemigrations[/bold] to create schema change "
-            "scripts and [bold]reflex db migrate[/bold] to apply migrations "
-            "to a new or existing database.",
+            + colored("reflex db makemigrations", attrs=("bold",))
+            + " to create schema change scripts and "
+            + colored("reflex db migrate", attrs=("bold",))
+            + " to apply migrations to a new or existing database.",
         )
         return
 
@@ -510,7 +512,9 @@ def makemigrations(
             if "Target database is not up to date." not in str(command_error):
                 raise
             console.error(
-                f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
+                f"{command_error} Run "
+                + colored("reflex db migrate", attrs=("bold",))
+                + " to update database."
             )
 
 

+ 17 - 5
reflex/utils/console.py

@@ -15,9 +15,8 @@ from dataclasses import dataclass
 from pathlib import Path
 from types import FrameType
 
-from termcolor import colored
-
 from reflex.constants import LogLevel
+from reflex.utils.terminal import colored
 
 
 def _get_terminal_width() -> int:
@@ -74,7 +73,7 @@ class Status:
     """A status class for displaying a spinner."""
 
     message: str = "Loading"
-    _reprinter: Reprinter | None = None
+    _reprinter: Reprinter | None = dataclasses.field(default=None, init=False)
 
     def __enter__(self):
         """Enter the context manager.
@@ -148,7 +147,20 @@ class Console:
         left_padding = remaining_width // 2
         right_padding = remaining_width - left_padding
 
-        rule_line = "─" * left_padding + f" {title} " + "─" * right_padding
+        color = kwargs.pop("color", None)
+        bold = kwargs.pop("bold", True)
+        rule_color = "green" if color is None else color
+        title = colored(title, color, attrs=("bold",) if bold else ())
+
+        rule_line = (
+            " "
+            + colored("─" * left_padding, rule_color)
+            + " "
+            + title
+            + " "
+            + colored("─" * right_padding, rule_color)
+            + " "
+        )
         self.print(rule_line, **kwargs)
 
     def status(self, *args, **kwargs):
@@ -298,7 +310,7 @@ def debug(msg: str, dedupe: bool = False, **kwargs):
                 return
             else:
                 _EMITTED_DEBUG.add(msg)
-        kwargs.setdefault("color", "purple")
+        kwargs.setdefault("color", "magenta")
         print(msg, **kwargs)
 
 

+ 10 - 4
reflex/utils/exec.py

@@ -22,6 +22,7 @@ from reflex.constants.base import LogLevel
 from reflex.utils import console, path_ops
 from reflex.utils.decorator import once
 from reflex.utils.prerequisites import get_web_dir
+from reflex.utils.terminal import colored
 
 # For uvicorn windows bug fix (#2335)
 frontend_process = None
@@ -67,7 +68,10 @@ def kill(proc_pid: int):
 def notify_backend():
     """Output a string notifying where the backend is running."""
     console.print(
-        f"Backend running at: [bold green]http://0.0.0.0:{get_config().backend_port}[/bold green]"
+        "Backend running at: "
+        + colored(
+            f"http://0.0.0.0:{get_config().backend_port}", "green", attrs=("bold",)
+        )
     )
 
 
@@ -113,7 +117,9 @@ def run_process_and_launch_url(
                             url = urljoin(url, get_config().frontend_path)
 
                         console.print(
-                            f"App running at: [bold green]{url}[/bold green]{' (Frontend-only mode)' if not backend_present else ''}"
+                            "App running at: "
+                            + colored(url, "green", attrs=("bold",))
+                            + (" (Frontend-only mode)" if not backend_present else "")
                         )
                         if backend_present:
                             notify_backend()
@@ -153,7 +159,7 @@ def run_frontend(root: Path, port: str, backend_present: bool = True):
     prerequisites.validate_frontend_dependencies(init=False)
 
     # Run the frontend in development mode.
-    console.rule("App Running", color="green", bold=True)
+    console.rule("App Running", color="green")
     os.environ["PORT"] = str(get_config().frontend_port if port is None else port)
     run_process_and_launch_url(
         [
@@ -180,7 +186,7 @@ def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
     # validate dependencies before run
     prerequisites.validate_frontend_dependencies(init=False)
     # Run the frontend in production mode.
-    console.rule("App Running", color="green", bold=True)
+    console.rule("App Running", color="green")
     run_process_and_launch_url(
         [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
         backend_present,

+ 1 - 1
reflex/utils/export.py

@@ -51,7 +51,7 @@ def export(
     exec.output_system_info()
 
     # Compile the app in production mode and export it.
-    console.rule("Compiling production app and preparing for export.", bold=True)
+    console.rule("Compiling production app and preparing for export.")
 
     if frontend:
         # Ensure module can be imported and app.compile() is called.

+ 20 - 50
reflex/utils/prerequisites.py

@@ -8,7 +8,6 @@ import functools
 import importlib
 import importlib.metadata
 import importlib.util
-import io
 import json
 import os
 import platform
@@ -44,6 +43,7 @@ from reflex.utils.exceptions import (
 )
 from reflex.utils.format import format_library_name
 from reflex.utils.registry import get_npm_registry
+from reflex.utils.terminal import _can_colorize, colored
 
 if typing.TYPE_CHECKING:
     from reflex.app import App
@@ -453,48 +453,6 @@ def compile_app(reload: bool = False, export: bool = False) -> None:
     get_compiled_app(reload=reload, export=export)
 
 
-def _can_colorize() -> bool:
-    """Check if the output can be colorized.
-
-    Copied from _colorize.can_colorize.
-
-    https://raw.githubusercontent.com/python/cpython/refs/heads/main/Lib/_colorize.py
-
-    Returns:
-        If the output can be colorized
-    """
-    file = sys.stdout
-
-    if not sys.flags.ignore_environment:
-        if os.environ.get("PYTHON_COLORS") == "0":
-            return False
-        if os.environ.get("PYTHON_COLORS") == "1":
-            return True
-    if os.environ.get("NO_COLOR"):
-        return False
-    if os.environ.get("FORCE_COLOR"):
-        return True
-    if os.environ.get("TERM") == "dumb":
-        return False
-
-    if not hasattr(file, "fileno"):
-        return False
-
-    if sys.platform == "win32":
-        try:
-            import nt
-
-            if not nt._supports_virtual_terminal():
-                return False
-        except (ImportError, AttributeError):
-            return False
-
-    try:
-        return os.isatty(file.fileno())
-    except io.UnsupportedOperation:
-        return file.isatty()
-
-
 def compile_or_validate_app(compile: bool = False) -> bool:
     """Compile or validate the app module based on the default config.
 
@@ -609,7 +567,9 @@ def validate_app_name(app_name: str | None = None) -> str:
     # Make sure the app is not named "reflex".
     if app_name.lower() == constants.Reflex.MODULE_NAME:
         console.error(
-            f"The app directory cannot be named [bold]{constants.Reflex.MODULE_NAME}[/bold]."
+            "The app directory cannot be named "
+            + colored(constants.Reflex.MODULE_NAME, attrs=("bold",))
+            + "."
         )
         raise typer.Exit(1)
 
@@ -700,7 +660,9 @@ def rename_app(new_app_name: str, loglevel: constants.LogLevel):
 
     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].")
+    console.success(
+        "App directory renamed to " + colored(new_app_name, attrs=("bold",)) + "."
+    )
 
 
 def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name: str):
@@ -1310,7 +1272,10 @@ def needs_reinit(frontend: bool = True) -> bool:
     """
     if not constants.Config.FILE.exists():
         console.error(
-            f"[cyan]{constants.Config.FILE}[/cyan] not found. Move to the root folder of your project, or run [bold]{constants.Reflex.MODULE_NAME} init[/bold] to start a new project."
+            colored(constants.Config.FILE, "cyan")
+            + " not found. Move to the root folder of your project, or run "
+            + colored(constants.Reflex.MODULE_NAME + " init", attrs=("bold",))
+            + " to start a new project."
         )
         raise typer.Exit(1)
 
@@ -1491,7 +1456,9 @@ def check_db_initialized() -> bool:
         and not environment.ALEMBIC_CONFIG.get().exists()
     ):
         console.error(
-            "Database is not initialized. Run [bold]reflex db init[/bold] first."
+            "Database is not initialized. Run "
+            + colored("reflex db init", attrs=("bold",))
+            + " first."
         )
         return False
     return True
@@ -1508,13 +1475,16 @@ def check_schema_up_to_date():
                 write_migration_scripts=False,
             ):
                 console.error(
-                    "Detected database schema changes. Run [bold]reflex db makemigrations[/bold] "
-                    "to generate migration scripts.",
+                    "Detected database schema changes. Run "
+                    + colored("reflex db makemigrations", attrs=("bold",))
+                    + " to generate migration scripts.",
                 )
         except CommandError as command_error:
             if "Target database is not up to date." in str(command_error):
                 console.error(
-                    f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
+                    f"{command_error} Run "
+                    + colored("reflex db migrate", attrs=("bold",))
+                    + " to update database."
                 )
 
 

+ 9 - 2
reflex/utils/processes.py

@@ -25,6 +25,7 @@ from reflex.utils.printer import (
     MessageComponent,
     ProgressBar,
 )
+from reflex.utils.terminal import colored
 
 
 def kill(pid: int):
@@ -116,7 +117,9 @@ def change_port(port: int, _type: str) -> int:
     if is_process_on_port(new_port):
         return change_port(new_port, _type)
     console.info(
-        f"The {_type} will run on port [bold underline]{new_port}[/bold underline]."
+        f"The {_type} will run on port "
+        + colored(port, attrs=("bold", "underline"))
+        + "."
     )
     return new_port
 
@@ -319,7 +322,11 @@ def stream_logs(
             console.error(line, end="")
         if analytics_enabled:
             telemetry.send("error", context=message)
-        console.error("Run with [bold]--loglevel debug [/bold] for the full log.")
+        console.error(
+            "Run with "
+            + colored("--loglevel debug", attrs=("bold",))
+            + " for the full log."
+        )
         raise typer.Exit(1)
 
 

+ 215 - 0
reflex/utils/terminal.py

@@ -0,0 +1,215 @@
+"""ANSI color formatting for output in terminal."""
+
+from __future__ import annotations
+
+import io
+import os
+import sys
+from functools import reduce
+from typing import Iterable, Literal
+
+from reflex.utils.decorator import once
+
+_Attribute = Literal[
+    "bold",
+    "dark",
+    "italic",
+    "underline",
+    "slow_blink",
+    "rapid_blink",
+    "reverse",
+    "concealed",
+    "strike",
+]
+
+_ATTRIBUTES: dict[_Attribute, int] = {
+    "bold": 1,
+    "dark": 2,
+    "italic": 3,
+    "underline": 4,
+    "slow_blink": 5,
+    "rapid_blink": 6,
+    "reverse": 7,
+    "concealed": 8,
+    "strike": 9,
+}
+
+_Color = Literal[
+    "black",
+    "red",
+    "green",
+    "yellow",
+    "blue",
+    "magenta",
+    "cyan",
+    "light_grey",
+    "dark_grey",
+    "light_red",
+    "light_green",
+    "light_yellow",
+    "light_blue",
+    "light_magenta",
+    "light_cyan",
+    "white",
+]
+
+
+_COLORS: dict[_Color, int] = {
+    "black": 30,
+    "red": 31,
+    "green": 32,
+    "yellow": 33,
+    "blue": 34,
+    "magenta": 35,
+    "cyan": 36,
+    "light_grey": 37,
+    "dark_grey": 90,
+    "light_red": 91,
+    "light_green": 92,
+    "light_yellow": 93,
+    "light_blue": 94,
+    "light_magenta": 95,
+    "light_cyan": 96,
+    "white": 97,
+}
+
+_BackgroundColor = Literal[
+    "on_black",
+    "on_red",
+    "on_green",
+    "on_yellow",
+    "on_blue",
+    "on_magenta",
+    "on_cyan",
+    "on_light_grey",
+    "on_dark_grey",
+    "on_light_red",
+    "on_light_green",
+    "on_light_yellow",
+    "on_light_blue",
+    "on_light_magenta",
+    "on_light_cyan",
+    "on_white",
+]
+
+BACKGROUND_COLORS: dict[_BackgroundColor, int] = {
+    "on_black": 40,
+    "on_red": 41,
+    "on_green": 42,
+    "on_yellow": 43,
+    "on_blue": 44,
+    "on_magenta": 45,
+    "on_cyan": 46,
+    "on_light_grey": 47,
+    "on_dark_grey": 100,
+    "on_light_red": 101,
+    "on_light_green": 102,
+    "on_light_yellow": 103,
+    "on_light_blue": 104,
+    "on_light_magenta": 105,
+    "on_light_cyan": 106,
+    "on_white": 107,
+}
+
+
+_ANSI_CODES = _ATTRIBUTES | BACKGROUND_COLORS | _COLORS
+
+
+_RESET_MARKER = "\033[0m"
+
+
+@once
+def _can_colorize() -> bool:
+    """Check if the output can be colorized.
+
+    Copied from _colorize.can_colorize.
+
+    https://raw.githubusercontent.com/python/cpython/refs/heads/main/Lib/_colorize.py
+
+    Returns:
+        If the output can be colorized
+    """
+    file = sys.stdout
+
+    if not sys.flags.ignore_environment:
+        if os.environ.get("PYTHON_COLORS") == "0":
+            return False
+        if os.environ.get("PYTHON_COLORS") == "1":
+            return True
+    if os.environ.get("NO_COLOR"):
+        return False
+    if os.environ.get("FORCE_COLOR"):
+        return True
+    if os.environ.get("TERM") == "dumb":
+        return False
+
+    if not hasattr(file, "fileno"):
+        return False
+
+    if sys.platform == "win32":
+        try:
+            import nt
+
+            if not nt._supports_virtual_terminal():
+                return False
+        except (ImportError, AttributeError):
+            return False
+
+    try:
+        return os.isatty(file.fileno())
+    except io.UnsupportedOperation:
+        return file.isatty()
+
+
+def _format_str(text: str, ansi_escape_code: int | None) -> str:
+    """Format text with ANSI escape code.
+
+    Args:
+        text: Text to format
+        ansi_escape_code: ANSI escape code
+
+    Returns:
+        Formatted text
+    """
+    if ansi_escape_code is None:
+        return text
+    return f"\033[{ansi_escape_code}m{text}"
+
+
+def colored(
+    text: object,
+    color: _Color | None = None,
+    background_color: _BackgroundColor | None = None,
+    attrs: Iterable[_Attribute] = (),
+) -> str:
+    """Colorize text for terminal output.
+
+    Args:
+        text: Text to colorize
+        color: Text color
+        background_color: Background color
+        attrs: Text attributes
+
+    Returns:
+        Colorized text
+    """
+    result = str(text)
+
+    if not _can_colorize():
+        return result
+
+    ansi_codes_to_apply = [
+        _ANSI_CODES.get(x)
+        for x in [
+            color,
+            background_color,
+            *attrs,
+        ]
+        if x
+    ]
+
+    return (
+        reduce(_format_str, ansi_codes_to_apply, result) + _RESET_MARKER
+        if ansi_codes_to_apply
+        else result
+    )

+ 0 - 11
uv.lock

@@ -1734,7 +1734,6 @@ dependencies = [
     { name = "setuptools" },
     { name = "sqlmodel" },
     { name = "starlette-admin" },
-    { name = "termcolor" },
     { name = "tomlkit" },
     { name = "twine" },
     { name = "typer" },
@@ -1797,7 +1796,6 @@ requires-dist = [
     { name = "setuptools", specifier = ">=75.0" },
     { name = "sqlmodel", specifier = ">=0.0.14,<0.1" },
     { name = "starlette-admin", specifier = ">=0.11.0,<1.0" },
-    { name = "termcolor", specifier = ">=2.5.0,<2.6" },
     { name = "tomlkit", specifier = ">=0.12.4,<1.0" },
     { name = "twine", specifier = ">=4.0.0,<7.0" },
     { name = "typer", specifier = ">=0.15.1,<1.0" },
@@ -2110,15 +2108,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 },
 ]
 
-[[package]]
-name = "termcolor"
-version = "2.5.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 },
-]
-
 [[package]]
 name = "text-unidecode"
 version = "1.3"