diff --git a/README.md b/README.md index c77d7ea3315..5ab35796a25 100644 --- a/README.md +++ b/README.md @@ -56,42 +56,47 @@ mkdir my_app_name cd my_app_name ``` -### 2. Set up a virtual environment +### 2. Install uv -Create and activate virtual environment +Reflex recommends [uv](https://docs.astral.sh/uv/) for managing your project environment and dependencies. +See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for your platform. ```bash -# On Windows: -python -m venv .venv -.venv\Scripts\activate +# macOS/Linux +curl -LsSf https://astral.sh/uv/install.sh | sh -# On macOS/Linux: -python3 -m venv .venv -source .venv/bin/activate +# Windows (PowerShell) +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` -### 3. Install Reflex +### 3. Initialize the Python project -Reflex is available as a pip package (Requires Python 3.10+): +```bash +uv init +``` + +### 4. Add Reflex + +Reflex requires Python 3.10+: ```bash -pip install reflex +uv add reflex ``` -### 4. Initialize the project +### 5. Initialize the project This command initializes a template app in your new directory: ```bash -reflex init +uv run reflex init ``` -### 5. Run the app +### 6. Run the app You can run this app in development mode: ```bash -reflex run +uv run reflex run ``` You should see your app running at http://localhost:3000. @@ -100,7 +105,7 @@ Now you can modify the source code in `my_app_name/my_app_name.py`. Reflex has f ### Troubleshooting -If you installed Reflex without a virtual environment and the `reflex` command is not found, you can run commands using: `python3 -m reflex init` and `python3 -m reflex run` +If the `reflex` command is not on your PATH, run it through uv instead: `uv run reflex init` and `uv run reflex run` ## 🫧 Example App diff --git a/reflex/docs/advanced_onboarding/code_structure.md b/reflex/docs/advanced_onboarding/code_structure.md index 6589f1efd30..f400bb498a7 100644 --- a/reflex/docs/advanced_onboarding/code_structure.md +++ b/reflex/docs/advanced_onboarding/code_structure.md @@ -357,7 +357,7 @@ example-big-app/ │ ├─ state.py │ ├─ template.py ├─ uploaded_files/ -├─ requirements.txt +├─ pyproject.toml ├─ rxconfig.py ``` diff --git a/reflex/docs/advanced_onboarding/configuration.md b/reflex/docs/advanced_onboarding/configuration.md index 09e54e94169..d538c8b7c75 100644 --- a/reflex/docs/advanced_onboarding/configuration.md +++ b/reflex/docs/advanced_onboarding/configuration.md @@ -5,7 +5,7 @@ Reflex apps can be configured using a configuration file, environment variables, ## Configuration File -Running `reflex init` will create an `rxconfig.py` file in your root directory. +Running `uv run reflex init` will create an `rxconfig.py` file in your root directory. You can pass keyword arguments to the `Config` class to configure your app. For example: @@ -31,15 +31,15 @@ You can override the configuration file by setting environment variables. For example, to override the `frontend_port` setting, you can set the `FRONTEND_PORT` environment variable. ```bash -FRONTEND_PORT=3001 reflex run +FRONTEND_PORT=3001 uv run reflex run ``` ## Command Line Arguments -Finally, you can override the configuration file and environment variables by passing command line arguments to `reflex run`. +Finally, you can override the configuration file and environment variables by passing command line arguments to `uv run reflex run`. ```bash -reflex run --frontend-port 3001 +uv run reflex run --frontend-port 3001 ``` See the [CLI reference](/docs/api-reference/cli) for all the arguments available. diff --git a/reflex/docs/advanced_onboarding/how-reflex-works.md b/reflex/docs/advanced_onboarding/how-reflex-works.md index 15762f14acb..98952e23fa9 100644 --- a/reflex/docs/advanced_onboarding/how-reflex-works.md +++ b/reflex/docs/advanced_onboarding/how-reflex-works.md @@ -69,7 +69,7 @@ rx.box(height="1em") We wanted Reflex apps to look and feel like a traditional web app to the end user, while still being easy to build and maintain for the developer. To do this, we built on top of mature and popular web technologies. -When you `reflex run` your app, Reflex compiles the frontend down to a single-page [Next.js](https://nextjs.org) app and serves it on a port (by default `3000`) that you can access in your browser. +When you run `uv run reflex run`, Reflex compiles the frontend down to a single-page [Next.js](https://nextjs.org) app and serves it on a port (by default `3000`) that you can access in your browser. The frontend's job is to reflect the app's state, and send events to the backend when the user interacts with the UI. No actual logic is run on the frontend. @@ -128,7 +128,7 @@ Beyond this, Reflex components can be styled using the full power of CSS. We lev Now let's look at how we added interactivity to our apps. -In Reflex only the frontend compiles to Javascript and runs on the user's browser, while all the state and logic stays in Python and is run on the server. When you `reflex run`, we start a FastAPI server (by default on port `8000`) that the frontend connects to through a websocket. +In Reflex only the frontend compiles to Javascript and runs on the user's browser, while all the state and logic stays in Python and is run on the server. When you run `uv run reflex run`, we start a FastAPI server (by default on port `8000`) that the frontend connects to through a websocket. All the state and logic are defined within a `State` class. diff --git a/reflex/docs/getting_started/basics.md b/reflex/docs/getting_started/basics.md index 9df7d6f7bfe..7d6e7f26ae9 100644 --- a/reflex/docs/getting_started/basics.md +++ b/reflex/docs/getting_started/basics.md @@ -18,10 +18,10 @@ This page gives an introduction to the most common concepts that you will use to - Create pages and navigate between them ``` -[Install](/docs/getting_started/installation) `reflex` using pip. +[Install](/docs/getting_started/installation) `reflex` with uv before continuing. ```bash -pip install reflex +uv add reflex ``` Import the `reflex` library to get started. diff --git a/reflex/docs/getting_started/dashboard_tutorial.md b/reflex/docs/getting_started/dashboard_tutorial.md index 62a85ef633d..757e6e731d1 100644 --- a/reflex/docs/getting_started/dashboard_tutorial.md +++ b/reflex/docs/getting_started/dashboard_tutorial.md @@ -326,9 +326,9 @@ Don't worry if you don't understand the code above, in this tutorial we are goin ## Setup for the tutorial -Check out the [installation docs](/docs/getting_started/installation) to get Reflex set up on your machine. Follow these to create a folder called `dashboard_tutorial`, which you will `cd` into and `pip install reflex`. +Check out the [installation docs](/docs/getting_started/installation) to get Reflex set up on your machine. Follow these to create a folder called `dashboard_tutorial`, which you will `cd` into, then run `uv init` and `uv add reflex`. -We will choose template `0` when we run `reflex init` to get the blank template. Finally run `reflex run` to start the app and confirm everything is set up correctly. +We will choose template `0` when we run `uv run reflex init` to get the blank template. Finally run `uv run reflex run` to start the app and confirm everything is set up correctly. ## Overview diff --git a/reflex/docs/getting_started/project-structure.md b/reflex/docs/getting_started/project-structure.md index 6065441280c..b4e38f8a663 100644 --- a/reflex/docs/getting_started/project-structure.md +++ b/reflex/docs/getting_started/project-structure.md @@ -12,23 +12,36 @@ Let's create a new app called `{app_name}` ```bash mkdir {app_name} cd {app_name} -reflex init +uv init +uv add reflex +uv run reflex init ``` This will create a directory structure like this: ```bash {app_name} +├── .venv ├── .web ├── assets ├── {app_name} │ ├── __init__.py │ └── {app_name}.py -└── rxconfig.py +├── .gitignore +├── .python-version +├── pyproject.toml +├── rxconfig.py +└── uv.lock ``` +`uv init` may also create helper files such as `README.md`, `main.py`, and Git metadata. The tree above focuses on the main files you will interact with while building a Reflex app. + Let's go over each of these directories and files. +## .venv + +`uv add reflex` creates a local virtual environment in `.venv` by default. This keeps your app dependencies isolated from the rest of your system Python. + ## .web This is where the compiled Javascript files will be stored. You will never need to touch this directory, but it can be useful for debugging. @@ -45,14 +58,18 @@ For example, if you save an image to `assets/image.png` you can display it from rx.image(src="https://web.reflex-assets.dev/other/image.png") ``` -j - ## Main Project Initializing your project creates a directory with the same name as your app. This is where you will write your app's logic. Reflex generates a default app within the `{app_name}/{app_name}.py` file. You can modify this file to customize your app. +## Python Project Files + +`pyproject.toml` defines your Python project metadata and dependencies. `uv add reflex` records the Reflex dependency there before you initialize the app. + +`uv.lock` stores the fully resolved dependency set for reproducible installs. Commit it to version control so everyone working on the app gets the same Python package versions. + ## Configuration The `rxconfig.py` file can be used to configure your app. By default it looks something like this: diff --git a/reflex/reflex.py b/reflex/reflex.py index 8aa2dbd9378..f31f7e34f19 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -89,20 +89,22 @@ def _init( # Initialize the .gitignore. frontend_skeleton.initialize_gitignore() - # Initialize the requirements.txt. - needs_user_manual_update = frontend_skeleton.initialize_requirements_txt() - template_msg = f" using the {template} template" if template else "" - # Finish initializing the app. - console.success( - f"Initialized {app_name}{template_msg}." - + ( - f" Make sure to add {constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION} to your requirements.txt or pyproject.toml file." - if needs_user_manual_update - else "" - ) + if Path(constants.PyprojectToml.FILE).exists(): + needs_user_manual_update = False + next_steps = " Run `uv run reflex run` to start the app." + else: + needs_user_manual_update = frontend_skeleton.initialize_requirements_txt() + next_steps = " Install dependencies from `requirements.txt` with `uv pip install -r requirements.txt` (or your preferred installer) before running `uv run reflex run`." + manual_update = ( + f" Make sure to add `{constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION}` to your requirements.txt file." + if needs_user_manual_update + else "" ) + # Finish initializing the app. + console.success(f"Initialized {app_name}{template_msg}.{manual_update}{next_steps}") + @cli.command() @loglevel_option diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index e205643bf8b..73990acd386 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -2,7 +2,6 @@ import json import random -import re from pathlib import Path from reflex_base import constants @@ -42,44 +41,101 @@ def initialize_gitignore( gitignore_file.write_text("\n".join(files_to_ignore) + "\n") -def initialize_requirements_txt() -> bool: +def _read_dependency_file(file_path: Path) -> tuple[str | None, str | None]: + """Read a dependency file with a forgiving encoding strategy. + + Args: + file_path: The file to read. + + Returns: + A tuple of file content and the encoding used to read it. + """ + try: + return file_path.read_text(), None + except UnicodeDecodeError: + pass + except Exception as e: + console.error(f"Failed to read {file_path} due to {e}.") + raise SystemExit(1) from None + + try: + return file_path.read_text(encoding="utf-8"), "utf-8" + except UnicodeDecodeError: + return None, None + except Exception as e: + console.error(f"Failed to read {file_path} due to {e}.") + raise SystemExit(1) from None + + +def _has_reflex_requirement_line(requirements_text: str) -> bool: + """Check whether requirements.txt already contains reflex. + + Returns: + Whether reflex is already present in the requirements text. + """ + return any( + _is_reflex_dependency_spec(line) for line in requirements_text.splitlines() + ) + + +def _is_reflex_dependency_spec(requirement: str) -> bool: + """Check whether a dependency specification refers to the reflex package. + + Args: + requirement: The dependency specification to check. + + Returns: + Whether the specification refers to the reflex package. + """ + requirement = requirement.strip() + if not requirement.lower().startswith("reflex"): + return False + + suffix = requirement[len("reflex") :] + if suffix.startswith("["): + extras_end = suffix.find("]") + if extras_end == -1: + return False + suffix = suffix[extras_end + 1 :] + + return not suffix or suffix.lstrip().startswith(( + "==", + "!=", + ">=", + "<=", + "~=", + ">", + "<", + ";", + "@", + )) + + +def initialize_requirements_txt( + requirements_file_path: Path = Path(constants.RequirementsTxt.FILE), + pyproject_file_path: Path = Path(constants.PyprojectToml.FILE), +) -> bool: """Initialize the requirements.txt file. - If absent and no pyproject.toml file exists, generate one for the user. - If the requirements.txt does not have reflex as dependency, - generate a requirement pinning current version and append to - the requirements.txt file. + + If a project already uses pyproject.toml, leave dependency management to the + package manager. Otherwise ensure requirements.txt pins the current Reflex + version for legacy workflows. Returns: True if the user has to update the requirements.txt file. - - Raises: - SystemExit: If the requirements.txt file cannot be read or written to. """ - requirements_file_path = Path(constants.RequirementsTxt.FILE) - if ( - not requirements_file_path.exists() - and Path(constants.PyprojectToml.FILE).exists() - ): - return True + if not requirements_file_path.exists() and pyproject_file_path.exists(): + return False requirements_file_path.touch(exist_ok=True) - for encoding in [None, "utf-8"]: - try: - content = requirements_file_path.read_text(encoding) - break - except UnicodeDecodeError: - continue - except Exception as e: - console.error(f"Failed to read {requirements_file_path} due to {e}.") - raise SystemExit(1) from None - else: + content, encoding = _read_dependency_file(requirements_file_path) + if content is None: return True - for line in content.splitlines(): - if re.match(r"^reflex[^a-zA-Z0-9]", line): - console.debug(f"{requirements_file_path} already has reflex as dependency.") - return False + if _has_reflex_requirement_line(content): + console.debug(f"{requirements_file_path} already has reflex as dependency.") + return False console.debug( f"Appending {constants.RequirementsTxt.DEFAULTS_STUB} to {requirements_file_path}" diff --git a/reflex/utils/templates.py b/reflex/utils/templates.py index b467e0d326d..d5310b74d1f 100644 --- a/reflex/utils/templates.py +++ b/reflex/utils/templates.py @@ -182,10 +182,15 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str template_code_dir_name=template_name, template_dir=template_dir, ) - req_file = Path("requirements.txt") - if req_file.exists() and len(req_file.read_text().splitlines()) > 1: + pyproject_file = Path(constants.PyprojectToml.FILE) + req_file = Path(constants.RequirementsTxt.FILE) + if pyproject_file.exists(): console.info( - "Run `pip install -r requirements.txt` to install the required python packages for this template." + "Run `uv sync` to install the required Python packages for this template." + ) + elif req_file.exists() and len(req_file.read_text().splitlines()) > 1: + console.info( + "Run `uv pip install -r requirements.txt` to install the required Python packages for this template." ) # Clean up the temp directories. shutil.rmtree(temp_dir) diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 557331fc2c2..ffc3c975780 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -421,6 +421,56 @@ def test_initialize_non_existent_gitignore( assert set(file_content) - expected == set() +def test_initialize_requirements_txt_skips_when_pyproject_exists(tmp_path): + """Test that pyproject-based apps do not get a requirements.txt file.""" + pyproject_file = tmp_path / "pyproject.toml" + pyproject_file.write_text('[project]\nname = "existing-app"\n') + requirements_file = tmp_path / "requirements.txt" + + result = frontend_skeleton.initialize_requirements_txt( + pyproject_file_path=pyproject_file, + requirements_file_path=requirements_file, + ) + + assert not result + assert not requirements_file.exists() + + +def test_initialize_requirements_txt_appends_reflex_to_existing_requirements(tmp_path): + """Test that legacy requirements.txt projects keep working without pyproject.toml.""" + pyproject_file = tmp_path / "pyproject.toml" + requirements_file = tmp_path / "requirements.txt" + requirements_file.write_text("sqlmodel==0.0.37\n") + + result = frontend_skeleton.initialize_requirements_txt( + pyproject_file_path=pyproject_file, + requirements_file_path=requirements_file, + ) + + assert not result + assert not pyproject_file.exists() + assert requirements_file.read_text().endswith( + f"\nreflex=={constants.Reflex.VERSION}" + ) + + +def test_initialize_requirements_txt_preserves_existing_requirements(tmp_path): + """Test that existing requirements.txt projects do not get a second manifest.""" + pyproject_file = tmp_path / "pyproject.toml" + requirements_file = tmp_path / "requirements.txt" + requirements_text = f"reflex=={constants.Reflex.VERSION}\nredis==7.3.0\n" + requirements_file.write_text(requirements_text) + + result = frontend_skeleton.initialize_requirements_txt( + pyproject_file_path=pyproject_file, + requirements_file_path=requirements_file, + ) + + assert not result + assert requirements_file.read_text() == requirements_text + assert not pyproject_file.exists() + + def test_validate_app_name(tmp_path, mocker: MockerFixture): """Test that an error is raised if the app name is reflex or if the name is not according to python package naming conventions.