Add unified logging (#1462)

This commit is contained in:
Nikhil Rao 2023-07-30 19:58:48 -07:00 committed by GitHub
parent e1cb09e9d4
commit 068bcd906e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 370 additions and 237 deletions

View File

@ -1,4 +1,5 @@
"""Constants used throughout the package."""
from __future__ import annotations
import os
import platform
@ -250,6 +251,18 @@ class LogLevel(str, Enum):
ERROR = "error"
CRITICAL = "critical"
def __le__(self, other: LogLevel) -> bool:
"""Compare log levels.
Args:
other: The other log level.
Returns:
True if the log level is less than or equal to the other log level.
"""
levels = list(LogLevel)
return levels.index(self) <= levels.index(other)
# Templates
class Template(str, Enum):

View File

@ -37,8 +37,8 @@ def get_engine(url: Optional[str] = None):
if url is None:
raise ValueError("No database url configured")
if not Path(constants.ALEMBIC_CONFIG).exists():
console.print(
"[red]Database is not initialized, run [bold]reflex db init[/bold] first."
console.warn(
"Database is not initialized, run [bold]reflex db init[/bold] first."
)
echo_db_query = False
if conf.env == constants.Env.DEV and constants.SQLALCHEMY_ECHO:

View File

@ -14,7 +14,7 @@ from reflex.config import get_config
from reflex.utils import build, console, exec, prerequisites, processes, telemetry
# Create the app.
cli = typer.Typer()
cli = typer.Typer(add_completion=False)
def version(value: bool):
@ -35,25 +35,33 @@ def version(value: bool):
def main(
version: bool = typer.Option(
None,
"--version",
"-v",
"--version",
callback=version,
help="Get the Reflex version.",
is_eager=True,
),
):
"""Reflex CLI global configuration."""
"""Reflex CLI to create, run, and deploy apps."""
pass
@cli.command()
def init(
name: str = typer.Option(None, help="Name of the app to be initialized."),
name: str = typer.Option(
None, metavar="APP_NAME", help="The name of the app to be initialized."
),
template: constants.Template = typer.Option(
constants.Template.DEFAULT, help="Template to use for the app."
constants.Template.DEFAULT, help="The template to initialize the app with."
),
loglevel: constants.LogLevel = typer.Option(
constants.LogLevel.INFO, help="The log level to use."
),
):
"""Initialize a new Reflex app in the current directory."""
# Set the log level.
console.set_log_level(loglevel)
# Get the app name.
app_name = prerequisites.get_default_app_name() if name is None else name
console.rule(f"[bold]Initializing {app_name}")
@ -78,7 +86,7 @@ def init(
prerequisites.initialize_gitignore()
# Finish initializing the app.
console.log(f"[bold green]Finished Initializing: {app_name}")
console.success(f"Finished Initializing: {app_name}")
@cli.command()
@ -90,16 +98,16 @@ def run(
False, "--frontend-only", help="Execute only frontend."
),
backend: bool = typer.Option(False, "--backend-only", help="Execute only backend."),
loglevel: constants.LogLevel = typer.Option(
constants.LogLevel.ERROR, help="The log level to use."
),
frontend_port: str = typer.Option(None, help="Specify a different frontend port."),
backend_port: str = typer.Option(None, help="Specify a different backend port."),
backend_host: str = typer.Option(None, help="Specify the backend host."),
loglevel: constants.LogLevel = typer.Option(
constants.LogLevel.INFO, help="The log level to use."
),
):
"""Run the app in the current directory."""
# Check that the app is initialized.
prerequisites.check_initialized(frontend=frontend)
# Set the log level.
console.set_log_level(loglevel)
# Set ports as os env variables to take precedence over config and
# .env variables(if override_os_envs flag in config is set to False).
@ -120,6 +128,9 @@ def run(
frontend = True
backend = True
# Check that the app is initialized.
prerequisites.check_initialized(frontend=frontend)
# If something is running on the ports, ask the user if they want to kill or change it.
if frontend and processes.is_process_on_port(frontend_port):
frontend_port = processes.change_or_terminate_port(frontend_port, "frontend")
@ -158,14 +169,12 @@ def run(
# Run the frontend and backend.
if frontend:
setup_frontend(Path.cwd(), loglevel)
threading.Thread(
target=frontend_cmd, args=(Path.cwd(), frontend_port, loglevel)
).start()
setup_frontend(Path.cwd())
threading.Thread(target=frontend_cmd, args=(Path.cwd(), frontend_port)).start()
if backend:
threading.Thread(
target=backend_cmd,
args=(app.__name__, backend_host, backend_port, loglevel),
args=(app.__name__, backend_host, backend_port),
).start()
# Display custom message when there is a keyboard interrupt.
@ -217,8 +226,14 @@ def export(
backend: bool = typer.Option(
True, "--frontend-only", help="Export only frontend.", show_default=False
),
loglevel: constants.LogLevel = typer.Option(
constants.LogLevel.INFO, help="The log level to use."
),
):
"""Export the app to a zip file."""
# Set the log level.
console.set_log_level(loglevel)
# Check that the app is initialized.
prerequisites.check_initialized(frontend=frontend)
@ -233,7 +248,7 @@ def export(
# Export the app.
config = get_config()
build.export_app(
build.export(
backend=backend,
frontend=frontend,
zip=zipping,
@ -244,12 +259,12 @@ def export(
telemetry.send("export", config.telemetry_enabled)
if zipping:
console.rule(
console.log(
"""Backend & Frontend compiled. See [green bold]backend.zip[/green bold]
and [green bold]frontend.zip[/green bold]."""
)
else:
console.rule(
console.log(
"""Backend & Frontend compiled. See [green bold]app[/green bold]
and [green bold].web/_static[/green bold] directories."""
)
@ -261,16 +276,22 @@ db_cli = typer.Typer()
@db_cli.command(name="init")
def db_init():
"""Create database schema and migration configuration."""
# Check the database url.
if get_config().db_url is None:
console.print("[red]db_url is not configured, cannot initialize.")
console.error("db_url is not configured, cannot initialize.")
return
# Check the alembic config.
if Path(constants.ALEMBIC_CONFIG).exists():
console.print(
"[red]Database is already initialized. Use "
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.",
)
return
# Initialize the database.
prerequisites.get_app()
model.Model.alembic_init()
model.Model.migrate(autogenerate=True)
@ -302,8 +323,8 @@ def makemigrations(
except CommandError as command_error:
if "Target database is not up to date." not in str(command_error):
raise
console.print(
f"[red]{command_error} Run [bold]reflex db migrate[/bold] to update database."
console.error(
f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
)

View File

@ -151,6 +151,7 @@ class AppHarness:
reflex.reflex.init(
name=self.app_name,
template=reflex.constants.Template.DEFAULT,
loglevel=reflex.constants.LogLevel.INFO,
)
self.app_module_path.write_text(source_code)
with chdir(self.app_path):

View File

@ -9,12 +9,10 @@ import subprocess
from pathlib import Path
from typing import Optional, Union
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
from reflex import constants
from reflex.config import get_config
from reflex.utils import console, path_ops, prerequisites
from reflex.utils.processes import new_process
from reflex.utils.processes import new_process, show_progress
def update_json_file(file_path: str, update_dict: dict[str, Union[int, str]]):
@ -39,7 +37,9 @@ def update_json_file(file_path: str, update_dict: dict[str, Union[int, str]]):
def set_reflex_project_hash():
"""Write the hash of the Reflex project to a REFLEX_JSON."""
update_json_file(constants.REFLEX_JSON, {"project_hash": random.getrandbits(128)})
project_hash = random.getrandbits(128)
console.debug(f"Setting project hash to {project_hash}.")
update_json_file(constants.REFLEX_JSON, {"project_hash": project_hash})
def set_environment_variables():
@ -86,21 +86,19 @@ def generate_sitemap_config(deploy_url: str):
f.write(templates.SITEMAP_CONFIG(config=config))
def export_app(
def export(
backend: bool = True,
frontend: bool = True,
zip: bool = False,
deploy_url: Optional[str] = None,
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
):
"""Zip up the app for deployment.
"""Export the app for deployment.
Args:
backend: Whether to zip up the backend app.
frontend: Whether to zip up the frontend app.
zip: Whether to zip the app.
deploy_url: The URL of the deployed app.
loglevel: The log level to use.
"""
# Remove the static folder.
path_ops.rm(constants.WEB_STATIC_DIR)
@ -111,13 +109,6 @@ def export_app(
generate_sitemap_config(deploy_url)
command = "export-sitemap"
# Create a progress object
progress = Progress(
*Progress.get_default_columns()[:-1],
MofNCompleteColumn(),
TimeElapsedColumn(),
)
checkpoints = [
"Linting and checking ",
"Compiled successfully",
@ -130,36 +121,12 @@ def export_app(
"Export successful",
]
# Add a single task to the progress object
task = progress.add_task("Creating Production Build: ", total=len(checkpoints))
# Start the subprocess with the progress bar.
try:
with progress, new_process(
[prerequisites.get_package_manager(), "run", command],
cwd=constants.WEB_DIR,
) as export_process:
assert export_process.stdout is not None, "No stdout for export process."
for line in export_process.stdout:
if loglevel == constants.LogLevel.DEBUG:
print(line, end="")
# Check for special strings and update the progress bar.
for special_string in checkpoints:
if special_string in line:
if special_string == checkpoints[-1]:
progress.update(task, completed=len(checkpoints))
break # Exit the loop if the completion message is found
else:
progress.update(task, advance=1)
break
except Exception as e:
console.print(f"[red]Export process errored: {e}")
console.print(
"[red]Run in with [bold]--loglevel debug[/bold] to see the full error."
)
os._exit(1)
process = new_process(
[prerequisites.get_package_manager(), "run", command],
cwd=constants.WEB_DIR,
)
show_progress("Creating Production Build", process, checkpoints)
# Zip up the app.
if zip:
@ -203,14 +170,12 @@ def posix_export(backend: bool = True, frontend: bool = True):
def setup_frontend(
root: Path,
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
disable_telemetry: bool = True,
):
"""Set up the frontend to run the app.
Args:
root: The root path of the project.
loglevel: The log level to use.
disable_telemetry: Whether to disable the Next telemetry.
"""
# Install frontend packages.
@ -242,15 +207,13 @@ def setup_frontend(
def setup_frontend_prod(
root: Path,
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
disable_telemetry: bool = True,
):
"""Set up the frontend for prod mode.
Args:
root: The root path of the project.
loglevel: The log level to use.
disable_telemetry: Whether to disable the Next telemetry.
"""
setup_frontend(root, loglevel, disable_telemetry)
export_app(loglevel=loglevel, deploy_url=get_config().deploy_url)
setup_frontend(root, disable_telemetry)
export(deploy_url=get_config().deploy_url)

View File

@ -5,56 +5,123 @@ from __future__ import annotations
from typing import List, Optional
from rich.console import Console
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
from rich.prompt import Prompt
from rich.status import Status
from reflex.constants import LogLevel
# Console for pretty printing.
_console = Console()
# The current log level.
LOG_LEVEL = LogLevel.INFO
def deprecate(msg: str) -> None:
"""Print a deprecation warning.
def set_log_level(log_level: LogLevel):
"""Set the log level.
Args:
msg: The deprecation message.
log_level: The log level to set.
"""
_console.print(f"[yellow]DeprecationWarning: {msg}[/yellow]")
global LOG_LEVEL
LOG_LEVEL = log_level
def warn(msg: str) -> None:
"""Print a warning about bad usage in Reflex.
def print(msg: str, **kwargs):
"""Print a message.
Args:
msg: The warning message.
msg: The message to print.
kwargs: Keyword arguments to pass to the print function.
"""
_console.print(f"[orange1]UsageWarning: {msg}[/orange1]")
_console.print(msg, **kwargs)
def log(msg: str) -> None:
def debug(msg: str, **kwargs):
"""Print a debug message.
Args:
msg: The debug message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.DEBUG:
print(f"[blue]Debug: {msg}[/blue]", **kwargs)
def info(msg: str, **kwargs):
"""Print an info message.
Args:
msg: The info message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.INFO:
print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
def success(msg: str, **kwargs):
"""Print a success message.
Args:
msg: The success message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.INFO:
print(f"[green]Success: {msg}[/green]", **kwargs)
def log(msg: str, **kwargs):
"""Takes a string and logs it to the console.
Args:
msg: The message to log.
kwargs: Keyword arguments to pass to the print function.
"""
_console.log(msg)
if LOG_LEVEL <= LogLevel.INFO:
_console.log(msg, **kwargs)
def print(msg: str) -> None:
"""Prints the given message to the console.
Args:
msg: The message to print to the console.
"""
_console.print(msg)
def rule(title: str) -> None:
def rule(title: str, **kwargs):
"""Prints a horizontal rule with a title.
Args:
title: The title of the rule.
kwargs: Keyword arguments to pass to the print function.
"""
_console.rule(title)
_console.rule(title, **kwargs)
def warn(msg: str, **kwargs):
"""Print a warning message.
Args:
msg: The warning message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.WARNING:
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
def deprecate(msg: str, **kwargs):
"""Print a deprecation warning.
Args:
msg: The deprecation message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.WARNING:
print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
def error(msg: str, **kwargs):
"""Print an error message.
Args:
msg: The error message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.ERROR:
print(f"[red]Error: {msg}[/red]", **kwargs)
def ask(
@ -69,19 +136,33 @@ def ask(
default: The default option selected.
Returns:
A string
A string with the user input.
"""
return Prompt.ask(question, choices=choices, default=default) # type: ignore
def status(msg: str) -> Status:
"""Returns a status,
which can be used as a context manager.
def progress():
"""Create a new progress bar.
Args:
msg: The message to be used as status title.
Returns:
The status of the console.
A new progress bar.
"""
return _console.status(msg)
return Progress(
*Progress.get_default_columns()[:-1],
MofNCompleteColumn(),
TimeElapsedColumn(),
)
def status(*args, **kwargs):
"""Create a status with a spinner.
Args:
*args: Args to pass to the status.
**kwargs: Kwargs to pass to the status.
Returns:
A new status.
"""
return _console.status(*args, **kwargs)

View File

@ -3,12 +3,8 @@
from __future__ import annotations
import os
import platform
import subprocess
from pathlib import Path
from rich import print
from reflex import constants
from reflex.config import get_config
from reflex.utils import console, prerequisites, processes
@ -28,13 +24,11 @@ def start_watching_assets_folder(root):
def run_process_and_launch_url(
run_command: list[str],
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
):
"""Run the process and launch the URL.
Args:
run_command: The command to run.
loglevel: The log level to use.
"""
process = new_process(
run_command,
@ -45,22 +39,20 @@ def run_process_and_launch_url(
for line in process.stdout:
if "ready started server on" in line:
url = line.split("url: ")[-1].strip()
print(f"App running at: [bold green]{url}")
if loglevel == constants.LogLevel.DEBUG:
print(line, end="")
console.print(f"App running at: [bold green]{url}")
else:
console.debug(line)
def run_frontend(
root: Path,
port: str,
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
):
"""Run the frontend.
Args:
root: The root path of the project.
port: The port to run the frontend on.
loglevel: The log level to use.
"""
# Start watching asset folder.
start_watching_assets_folder(root)
@ -68,29 +60,25 @@ def run_frontend(
# Run the frontend in development mode.
console.rule("[bold green]App Running")
os.environ["PORT"] = get_config().frontend_port if port is None else port
run_process_and_launch_url(
[prerequisites.get_package_manager(), "run", "dev"], loglevel
)
run_process_and_launch_url([prerequisites.get_package_manager(), "run", "dev"])
def run_frontend_prod(
root: Path,
port: str,
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
):
"""Run the frontend.
Args:
root: The root path of the project (to keep same API as run_frontend).
port: The port to run the frontend on.
loglevel: The log level to use.
"""
# Set the port.
os.environ["PORT"] = get_config().frontend_port if port is None else port
# Run the frontend in production mode.
console.rule("[bold green]App Running")
run_process_and_launch_url([constants.NPM_PATH, "run", "prod"], loglevel)
run_process_and_launch_url([constants.NPM_PATH, "run", "prod"])
def run_backend(
@ -107,20 +95,23 @@ def run_backend(
port: The app port
loglevel: The log level.
"""
cmd = [
"uvicorn",
f"{app_name}:{constants.APP_VAR}.{constants.API_VAR}",
"--host",
host,
"--port",
str(port),
"--log-level",
loglevel,
"--reload",
"--reload-dir",
app_name.split(".")[0],
]
subprocess.run(cmd)
new_process(
[
"uvicorn",
f"{app_name}:{constants.APP_VAR}.{constants.API_VAR}",
"--host",
host,
"--port",
str(port),
"--log-level",
loglevel,
"--reload",
"--reload-dir",
app_name.split(".")[0],
],
run=True,
show_logs=True,
)
def run_backend_prod(
@ -147,7 +138,7 @@ def run_backend_prod(
str(port),
f"{app_name}:{constants.APP_VAR}",
]
if platform.system() == "Windows"
if prerequisites.IS_WINDOWS
else [
*constants.RUN_BACKEND_PROD,
"--bind",
@ -164,4 +155,4 @@ def run_backend_prod(
"--workers",
str(num_workers),
]
subprocess.run(command)
new_process(command, run=True, show_logs=True)

View File

@ -7,11 +7,9 @@ import json
import os
import platform
import re
import subprocess
import sys
import tempfile
import threading
from datetime import datetime
from fileinput import FileInput
from pathlib import Path
from types import ModuleType
@ -26,34 +24,32 @@ from redis import Redis
from reflex import constants, model
from reflex.config import get_config
from reflex.utils import console, path_ops
from reflex.utils.processes import new_process, show_logs, show_status
IS_WINDOWS = platform.system() == "Windows"
def check_node_version():
def check_node_version() -> bool:
"""Check the version of Node.js.
Returns:
Whether the version of Node.js is valid.
"""
try:
# Run the node -v command and capture the output
result = subprocess.run(
[constants.NODE_PATH, "-v"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# The output will be in the form "vX.Y.Z", but version.parse() can handle it
current_version = version.parse(result.stdout.decode())
# Compare the version numbers
return (
current_version >= version.parse(constants.NODE_VERSION_MIN)
if IS_WINDOWS
else current_version == version.parse(constants.NODE_VERSION)
)
except Exception:
# Run the node -v command and capture the output.
result = new_process([constants.NODE_PATH, "-v"], run=True)
except FileNotFoundError:
return False
# The output will be in the form "vX.Y.Z", but version.parse() can handle it
current_version = version.parse(result.stdout) # type: ignore
# Compare the version numbers
return (
current_version >= version.parse(constants.NODE_VERSION_MIN)
if IS_WINDOWS
else current_version == version.parse(constants.NODE_VERSION)
)
def get_bun_version() -> Optional[version.Version]:
"""Get the version of bun.
@ -63,13 +59,9 @@ def get_bun_version() -> Optional[version.Version]:
"""
try:
# Run the bun -v command and capture the output
result = subprocess.run(
[constants.BUN_PATH, "-v"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
return version.parse(result.stdout.decode().strip())
except Exception:
result = new_process([constants.BUN_PATH, "-v"], run=True)
return version.parse(result.stdout) # type: ignore
except FileNotFoundError:
return None
@ -98,7 +90,7 @@ def get_install_package_manager() -> str:
get_config()
# On Windows, we use npm instead of bun.
if platform.system() == "Windows":
if IS_WINDOWS:
return get_windows_package_manager()
# On other platforms, we use bun.
@ -114,7 +106,7 @@ def get_package_manager() -> str:
"""
get_config()
if platform.system() == "Windows":
if IS_WINDOWS:
return get_windows_package_manager()
return constants.NPM_PATH
@ -141,7 +133,7 @@ def get_redis() -> Optional[Redis]:
if config.redis_url is None:
return None
redis_url, redis_port = config.redis_url.split(":")
print("Using redis at", config.redis_url)
console.info(f"Using redis at {config.redis_url}")
return Redis(host=redis_url, port=int(redis_port), db=0)
@ -173,8 +165,8 @@ def get_default_app_name() -> str:
# Make sure the app is not named "reflex".
if app_name == constants.MODULE_NAME:
console.print(
f"[red]The app directory cannot be named [bold]{constants.MODULE_NAME}."
console.error(
f"The app directory cannot be named [bold]{constants.MODULE_NAME}[/bold]."
)
raise typer.Exit()
@ -192,6 +184,7 @@ def create_config(app_name: str):
config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
with open(constants.CONFIG_FILE, "w") as f:
console.debug(f"Creating {constants.CONFIG_FILE}")
f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name))
@ -204,8 +197,10 @@ def initialize_gitignore():
if os.path.exists(constants.GITIGNORE_FILE):
with open(constants.GITIGNORE_FILE, "r") as f:
files |= set([line.strip() for line in f.readlines()])
# Write files to the .gitignore file.
with open(constants.GITIGNORE_FILE, "w") as f:
console.debug(f"Creating {constants.GITIGNORE_FILE}")
f.write(f"{(path_ops.join(sorted(files))).lstrip()}")
@ -256,16 +251,22 @@ def initialize_bun():
"""Check that bun requirements are met, and install if not."""
if IS_WINDOWS:
# Bun is not supported on Windows.
console.debug("Skipping bun installation on Windows.")
return
# Check the bun version.
if get_bun_version() != version.parse(constants.BUN_VERSION):
bun_version = get_bun_version()
if bun_version != version.parse(constants.BUN_VERSION):
console.debug(
f"Current bun version ({bun_version}) does not match ({constants.BUN_VERSION})."
)
remove_existing_bun_installation()
install_bun()
def remove_existing_bun_installation():
"""Remove existing bun installation."""
console.debug("Removing existing bun installation.")
if os.path.exists(constants.BUN_PATH):
path_ops.rm(constants.BUN_ROOT_PATH)
@ -279,17 +280,13 @@ def initialize_node():
def download_and_run(url: str, *args, **env):
"""Download and run a script.
Args:
url: The url of the script.
args: The arguments to pass to the script.
env: The environment variables to use.
Raises:
Exit: if installation failed
"""
# Download the script
console.debug(f"Downloading {url}")
response = httpx.get(url)
if response.status_code != httpx.codes.OK:
response.raise_for_status()
@ -300,13 +297,9 @@ def download_and_run(url: str, *args, **env):
f.write(response.text)
# Run the script.
env = {
**os.environ,
**env,
}
result = subprocess.run(["bash", f.name, *args], env=env)
if result.returncode != 0:
raise typer.Exit(code=result.returncode)
env = {**os.environ, **env}
process = new_process(["bash", f.name, *args], env=env)
show_logs(f"Installing {url}", process)
def install_node():
@ -318,8 +311,8 @@ def install_node():
"""
# NVM is not supported on Windows.
if IS_WINDOWS:
console.print(
f"[red]Node.js version {constants.NODE_VERSION} or higher is required to run Reflex."
console.error(
f"Node.js version {constants.NODE_VERSION} or higher is required to run Reflex."
)
raise typer.Exit()
@ -330,7 +323,7 @@ def install_node():
# Install node.
# We use bash -c as we need to source nvm.sh to use nvm.
result = subprocess.run(
process = new_process(
[
"bash",
"-c",
@ -338,8 +331,7 @@ def install_node():
],
env=env,
)
if result.returncode != 0:
raise typer.Exit(code=result.returncode)
show_logs("Installing node", process)
def install_bun():
@ -350,13 +342,15 @@ def install_bun():
"""
# Bun is not supported on Windows.
if IS_WINDOWS:
console.debug("Skipping bun installation on Windows.")
return
# Skip if bun is already installed.
if os.path.exists(constants.BUN_PATH):
console.debug("Skipping bun installation as it is already installed.")
return
# Check if unzip is installed
# if unzip is installed
unzip_path = path_ops.which("unzip")
if unzip_path is None:
raise FileNotFoundError("Reflex requires unzip to be installed.")
@ -371,24 +365,21 @@ def install_bun():
def install_frontend_packages():
"""Installs the base and custom frontend packages."""
# Install the frontend packages.
console.rule("[bold]Installing frontend packages")
# Install the base packages.
subprocess.run(
[get_install_package_manager(), "install"],
process = new_process(
[get_install_package_manager(), "install", "--loglevel", "silly"],
cwd=constants.WEB_DIR,
stdout=subprocess.PIPE,
)
show_status("Installing base frontend packages", process)
# Install the app packages.
packages = get_config().frontend_packages
if len(packages) > 0:
subprocess.run(
process = new_process(
[get_install_package_manager(), "add", *packages],
cwd=constants.WEB_DIR,
stdout=subprocess.PIPE,
)
show_status("Installing custom frontend packages", process)
def check_initialized(frontend: bool = True):
@ -406,22 +397,22 @@ def check_initialized(frontend: bool = True):
# Check if the app is initialized.
if not (has_config and has_reflex_dir and has_web_dir):
console.print(
f"[red]The app is not initialized. Run [bold]{constants.MODULE_NAME} init[/bold] first."
console.error(
f"The app is not initialized. Run [bold]{constants.MODULE_NAME} init[/bold] first."
)
raise typer.Exit()
# Check that the template is up to date.
if frontend and not is_latest_template():
console.print(
"[red]The base app template has updated. Run [bold]reflex init[/bold] again."
console.error(
"The base app template has updated. Run [bold]reflex init[/bold] again."
)
raise typer.Exit()
# Print a warning for Windows users.
if IS_WINDOWS:
console.print(
"[yellow][WARNING] We strongly advise using Windows Subsystem for Linux (WSL) for optimal performance with reflex."
console.warn(
"We strongly advise using Windows Subsystem for Linux (WSL) for optimal performance with reflex."
)
@ -462,17 +453,16 @@ def initialize_frontend_dependencies():
def check_admin_settings():
"""Check if admin settings are set and valid for logging in cli app."""
admin_dash = get_config().admin_dash
current_time = datetime.now()
if admin_dash:
if not admin_dash.models:
console.print(
f"[yellow][Admin Dashboard][/yellow] :megaphone: Admin dashboard enabled, but no models defined in [bold magenta]rxconfig.py[/bold magenta]. Time: {current_time}"
console.log(
f"[yellow][Admin Dashboard][/yellow] :megaphone: Admin dashboard enabled, but no models defined in [bold magenta]rxconfig.py[/bold magenta]."
)
else:
console.print(
f"[yellow][Admin Dashboard][/yellow] Admin enabled, building admin dashboard. Time: {current_time}"
console.log(
f"[yellow][Admin Dashboard][/yellow] Admin enabled, building admin dashboard."
)
console.print(
console.log(
"Admin dashboard running at: [bold green]http://localhost:8000/admin[/bold green]"
)
@ -484,8 +474,8 @@ def check_db_initialized() -> bool:
True if alembic is initialized (or if database is not used).
"""
if get_config().db_url is not None and not Path(constants.ALEMBIC_CONFIG).exists():
console.print(
"[red]Database is not initialized. Run [bold]reflex db init[/bold] first."
console.error(
"Database is not initialized. Run [bold]reflex db init[/bold] first."
)
return False
return True
@ -501,14 +491,14 @@ def check_schema_up_to_date():
connection=connection,
write_migration_scripts=False,
):
console.print(
"[red]Detected database schema changes. Run [bold]reflex db makemigrations[/bold] "
console.error(
"Detected database schema changes. Run [bold]reflex db makemigrations[/bold] "
"to generate migration scripts.",
)
except CommandError as command_error:
if "Target database is not up to date." in str(command_error):
console.print(
f"[red]{command_error} Run [bold]reflex db migrate[/bold] to update database."
console.error(
f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
)
@ -527,7 +517,7 @@ def migrate_to_reflex():
return
# Rename pcconfig to rxconfig.
console.print(
console.log(
f"[bold]Renaming {constants.OLD_CONFIG_FILE} to {constants.CONFIG_FILE}"
)
os.rename(constants.OLD_CONFIG_FILE, constants.CONFIG_FILE)

View File

@ -7,8 +7,7 @@ import os
import signal
import subprocess
import sys
from datetime import datetime
from typing import Optional
from typing import List, Optional
from urllib.parse import urlparse
import psutil
@ -101,7 +100,7 @@ def change_or_terminate_port(port, _type) -> str:
Returns:
The new port or the current one.
"""
console.print(
console.info(
f"Something is already running on port [bold underline]{port}[/bold underline]. This is the port the {_type} runs on."
)
frontend_action = console.ask("Kill or change it?", choices=["k", "c", "n"])
@ -115,41 +114,119 @@ def change_or_terminate_port(port, _type) -> str:
if is_process_on_port(new_port):
return change_or_terminate_port(new_port, _type)
else:
console.print(
console.info(
f"The {_type} will run on port [bold underline]{new_port}[/bold underline]."
)
return new_port
else:
console.print("Exiting...")
console.log("Exiting...")
sys.exit()
def new_process(args, **kwargs):
def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
"""Wrapper over subprocess.Popen to unify the launch of child processes.
Args:
args: A string, or a sequence of program arguments.
run: Whether to run the process to completion.
show_logs: Whether to show the logs of the process.
**kwargs: Kwargs to override default wrap values to pass to subprocess.Popen as arguments.
Returns:
Execute a child program in a new process.
"""
# Add the node bin path to the PATH environment variable.
env = {
**os.environ,
"PATH": os.pathsep.join([constants.NODE_BIN_PATH, os.environ["PATH"]]),
}
kwargs = {
"env": env,
"stderr": subprocess.STDOUT,
"stdout": subprocess.PIPE,
"stderr": None if show_logs else subprocess.STDOUT,
"stdout": None if show_logs else subprocess.PIPE,
"universal_newlines": True,
"encoding": "UTF-8",
**kwargs,
}
return subprocess.Popen(
args,
**kwargs,
)
console.debug(f"Running command: {args}")
fn = subprocess.run if run else subprocess.Popen
return fn(args, **kwargs)
def stream_logs(
message: str,
process: subprocess.Popen,
):
"""Stream the logs for a process.
Args:
message: The message to display.
process: The process.
Yields:
The lines of the process output.
"""
with process:
console.debug(message)
if process.stdout is None:
return
for line in process.stdout:
console.debug(line, end="")
yield line
if process.returncode != 0:
console.error(f"Error during {message}")
console.error(
"Run in with [bold]--loglevel debug[/bold] to see the full error."
)
os._exit(1)
def show_logs(
message: str,
process: subprocess.Popen,
):
"""Show the logs for a process.
Args:
message: The message to display.
process: The process.
"""
for _ in stream_logs(message, process):
pass
def show_status(message: str, process: subprocess.Popen):
"""Show the status of a process.
Args:
message: The initial message to display.
process: The process.
"""
with console.status(message) as status:
for line in stream_logs(message, process):
status.update(f"{message}: {line}")
def show_progress(message: str, process: subprocess.Popen, checkpoints: List[str]):
"""Show a progress bar for a process.
Args:
message: The message to display.
process: The process.
checkpoints: The checkpoints to advance the progress bar.
"""
# Iterate over the process output.
with console.progress() as progress:
task = progress.add_task(f"{message}: ", total=len(checkpoints))
for line in stream_logs(message, process):
# Check for special strings and update the progress bar.
for special_string in checkpoints:
if special_string in line:
progress.update(task, advance=1)
if special_string == checkpoints[-1]:
progress.update(task, completed=len(checkpoints))
break
def catch_keyboard_interrupt(signal, frame):
@ -159,5 +236,4 @@ def catch_keyboard_interrupt(signal, frame):
signal: The keyboard interrupt signal.
frame: The current stack frame.
"""
current_time = datetime.now().isoformat()
console.print(f"\nReflex app stopped at time: {current_time}")
console.log("Reflex app stopped.")

View File

@ -1,5 +1,4 @@
import os
import subprocess
import typing
from pathlib import Path
from typing import Any, List, Union
@ -529,10 +528,6 @@ def test_node_install_unix(tmp_path, mocker):
nvm_root_path = tmp_path / ".reflex" / ".nvm"
mocker.patch("reflex.utils.prerequisites.constants.NVM_DIR", nvm_root_path)
subprocess_run = mocker.patch(
"reflex.utils.prerequisites.subprocess.run",
return_value=subprocess.CompletedProcess(args="", returncode=0),
)
mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False)
class Resp(Base):
@ -540,13 +535,15 @@ def test_node_install_unix(tmp_path, mocker):
text = "test"
mocker.patch("httpx.get", return_value=Resp())
mocker.patch("reflex.utils.prerequisites.download_and_run")
download = mocker.patch("reflex.utils.prerequisites.download_and_run")
mocker.patch("reflex.utils.prerequisites.new_process")
mocker.patch("reflex.utils.prerequisites.show_logs")
prerequisites.install_node()
assert nvm_root_path.exists()
subprocess_run.assert_called()
subprocess_run.call_count = 2
download.assert_called()
download.call_count = 2
def test_bun_install_without_unzip(mocker):