From 7efd172a5746e3e9695cdfef1d99a140ba07b914 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:12:59 +0000 Subject: [PATCH 1/6] Initial plan From 62740061a28d8b36d625344e46d7a8e4f0265466 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:21:14 +0000 Subject: [PATCH 2/6] Implement flask-hmr package with CLI and HMR functionality Co-authored-by: CNSeniorious000 <74518716+CNSeniorious000@users.noreply.github.com> --- packages/flask-hmr/README.md | 84 ++++++++++++++ packages/flask-hmr/flask_hmr.py | 181 ++++++++++++++++++++++++++++++ packages/flask-hmr/pyproject.toml | 31 +++++ pyproject.toml | 2 + 4 files changed, 298 insertions(+) create mode 100644 packages/flask-hmr/README.md create mode 100644 packages/flask-hmr/flask_hmr.py create mode 100644 packages/flask-hmr/pyproject.toml diff --git a/packages/flask-hmr/README.md b/packages/flask-hmr/README.md new file mode 100644 index 0000000..94c756d --- /dev/null +++ b/packages/flask-hmr/README.md @@ -0,0 +1,84 @@ +# flask-hmr + +Hot Module Replacement for Flask applications, providing instant server reloading when your code changes. + +## Installation + +```bash +pip install flask-hmr +``` + +## Usage + +Replace your `flask run` command with `flask-hmr`: + +```bash +# Instead of: flask run +flask-hmr app:app + +# With custom host and port +flask-hmr app:app --host 0.0.0.0 --port 8000 + +# Enable Flask debug mode +flask-hmr app:app --debug + +# Clear terminal on reload +flask-hmr app:app --clear +``` + +## Features + +- **Fast Hot Module Replacement**: Only reloads changed modules, not the entire application +- **Flask Integration**: Works seamlessly with Flask applications +- **Configurable**: Support for custom host, port, and debug settings +- **Smart Watching**: Automatically detects which files to watch based on your imports + +## Example + +Given a Flask app in `app.py`: + +```python +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello, World!" + +if __name__ == "__main__": + app.run() +``` + +Start it with HMR: + +```bash +flask-hmr app:app +``` + +Now when you modify your route handlers, view functions, or imported modules, the server will automatically reload with your changes. + +## Command Line Options + +- `slug`: The module and app variable (e.g., `app:app`, `myproject.wsgi:application`) +- `--host`: Host to bind to (default: 127.0.0.1) +- `--port`: Port to bind to (default: 5000) +- `--debug`: Enable Flask debug mode +- `--clear`: Clear terminal before restarting server +- `--reload-include`: Additional paths to watch for changes +- `--reload-exclude`: Paths to exclude from watching (default: .venv) + +## Comparison with Standard Flask + +| Feature | `flask run --reload` | `flask-hmr` | +|---------|---------------------|-------------| +| Reload Speed | Full process restart | Hot module replacement | +| Memory Usage | Higher (new process) | Lower (same process) | +| State Preservation | Lost on reload | Preserved where possible | +| Startup Time | Slower | Faster | + +## Requirements + +- Python >=3.12 +- Flask >=2.0.0 +- hmr >=0.5.0 \ No newline at end of file diff --git a/packages/flask-hmr/flask_hmr.py b/packages/flask-hmr/flask_hmr.py new file mode 100644 index 0000000..5f3aa1d --- /dev/null +++ b/packages/flask-hmr/flask_hmr.py @@ -0,0 +1,181 @@ +import sys +from functools import cached_property +from pathlib import Path +from typing import TYPE_CHECKING, Annotated, override + +from typer import Argument, Option, Typer, secho + +app = Typer(help="Hot Module Replacement for Flask", add_completion=False, pretty_exceptions_show_locals=False) + + +@app.command(no_args_is_help=True) +def main( + slug: Annotated[str, Argument()] = "app:app", + reload_include: list[str] = [str(Path.cwd())], # noqa: B006, B008 + reload_exclude: list[str] = [".venv"], # noqa: B006 + host: str = "127.0.0.1", + port: int = 5000, + debug: Annotated[bool, Option("--debug", help="Enable Flask debug mode")] = False, # noqa: FBT002 + clear: Annotated[bool, Option("--clear", help="Clear the terminal before restarting the server")] = False, # noqa: FBT002 +): + if ":" not in slug: + secho("Invalid slug: ", fg="red", nl=False) + secho(slug, fg="yellow") + exit(1) + module, attr = slug.split(":") + + fragment = module.replace(".", "/") + + file: Path | None + is_package = False + for path in ("", *sys.path): + if (file := Path(path, f"{fragment}.py")).is_file(): + is_package = False + break + if (file := Path(path, fragment, "__init__.py")).is_file(): + is_package = True + break + else: + file = None + + if file is None: + secho("Module", fg="red", nl=False) + secho(f" {module} ", fg="yellow", nl=False) + secho("not found.", fg="red") + exit(1) + + if module in sys.modules: + return secho( + f"It seems you've already imported `{module}` as a normal module. You should call `reactivity.hmr.core.patch_meta_path()` before it.", + fg="red", + ) + + from atexit import register + from importlib.machinery import ModuleSpec + from logging import getLogger + from threading import Event, Thread + + from reactivity.hmr.core import ReactiveModule, ReactiveModuleLoader, SyncReloader, __version__, is_relative_to_any + from reactivity.hmr.utils import load + from watchfiles import Change + + if TYPE_CHECKING: + from flask import Flask + + cwd = str(Path.cwd()) + if cwd not in sys.path: + sys.path.insert(0, cwd) + + @register + def _(): + stop_server() + + def stop_server(): + pass + + def start_server(app: "Flask"): + nonlocal stop_server + + from werkzeug.serving import make_server + + server = make_server(host, port, app, threaded=True) + finish = Event() + + def run_server(): + watched_paths = [Path(p).resolve() for p in (file, *reload_include)] + ignored_paths = [Path(p).resolve() for p in reloader.excludes] + if all(is_relative_to_any(path, ignored_paths) or not is_relative_to_any(path, watched_paths) for path in ReactiveModule.instances): + logger.error("No files to watch for changes. The server will never reload.") + + secho(f"* Running on http://{host}:{port}", fg="green") + server.serve_forever(poll_interval=0.1) + finish.set() + + Thread(target=run_server, daemon=True).start() + + def stop_server(): + server.shutdown() + finish.wait() + + class Reloader(SyncReloader): + def __init__(self): + super().__init__(str(file), reload_include, reload_exclude) + self.error_filter.exclude_filenames.add(__file__) # exclude error stacks within this file + + @cached_property + @override + def entry_module(self): + if "." in module: + __import__(module.rsplit(".", 1)[0]) # ensure parent modules are imported + + if __version__ >= "0.6.4": + from reactivity.hmr.core import _loader as loader + else: + loader = ReactiveModuleLoader(file) # type: ignore + + spec = ModuleSpec(module, loader, origin=str(file), is_package=is_package) + sys.modules[module] = mod = loader.create_module(spec) + loader.exec_module(mod) + return mod + + @override + def run_entry_file(self): + stop_server() + with self.error_filter: + load(self.entry_module) + flask_app = getattr(self.entry_module, attr) + + # Configure Flask app + if debug: + flask_app.config['DEBUG'] = True + + start_server(flask_app) + + @override + def on_events(self, events): + if events: + paths: list[Path] = [] + for type, file in events: + path = Path(file).resolve() + if type != Change.deleted and path in ReactiveModule.instances: + paths.append(path) + if not paths: + return + + if clear: + print("\033c", end="") + logger.warning("Watchfiles detected changes in %s. Reloading...", ", ".join(map(_display_path, paths))) + return super().on_events(events) + + @override + def start_watching(self): + from dowhen import when + + def log_server_restart(): + logger.warning("Application '%s' has changed. Restarting server...", slug) + + def log_module_reload(self: ReactiveModule): + ns = self.__dict__ + logger.info("Reloading module '%s' from %s", ns["__name__"], _display_path(ns["__file__"])) + + with ( + when(ReactiveModule._ReactiveModule__load.method, "").do(log_module_reload), # type: ignore # noqa: SLF001 + when(self.run_entry_file, "").do(log_server_restart), + ): + return super().start_watching() + + logger = getLogger("flask.hmr") + (reloader := Reloader()).keep_watching_until_interrupt() + stop_server() + + +def _display_path(path: str | Path): + p = Path(path).resolve() + try: + return f"'{p.relative_to(Path.cwd())}'" + except ValueError: + return f"'{p}'" + + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/packages/flask-hmr/pyproject.toml b/packages/flask-hmr/pyproject.toml new file mode 100644 index 0000000..38ae6f0 --- /dev/null +++ b/packages/flask-hmr/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "flask-hmr" +description = "Hot Module Reloading for Flask" +version = "0.0.1" +readme = "README.md" +requires-python = ">=3.12" +keywords = ["flask", "hot-reload", "hmr", "reload", "server"] +authors = [{ name = "Muspi Merol", email = "me@promplate.dev" }] +license = "MIT" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: OS Independent", +] +dependencies = [ + "dowhen~=0.1", + "hmr>=0.5.0,<0.7", + "typer-slim>=0.15.4,<1", + "flask>=2.0.0", + "werkzeug>=2.0.0", +] + +[project.scripts] +flask-hmr = "flask_hmr:app" + +[project.urls] +Homepage = "https://github.com/promplate/hmr" + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6674d07..e7d39c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "0" requires-python = ">=3.12" dependencies = [ "fastapi-reloader", + "flask-hmr", "hmr~=0.6.0", "hmr-daemon", "ruff~=0.12.0", @@ -15,6 +16,7 @@ members = ["examples/*", "packages/*"] [tool.uv.sources] uvicorn-hmr = { workspace = true } +flask-hmr = { workspace = true } fastapi-reloader = { workspace = true } hmr-daemon = { workspace = true } From ac56055c435909c17a35c1e3f8b0b3b5f2246052 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:24:24 +0000 Subject: [PATCH 3/6] Add flask-hmr demo example and fix linting issues Co-authored-by: CNSeniorious000 <74518716+CNSeniorious000@users.noreply.github.com> --- examples/flask-hmr-demo/README.md | 30 ++++++++++++++++++++++++++++++ examples/flask-hmr-demo/app.py | 16 ++++++++++++++++ packages/flask-hmr/flask_hmr.py | 8 ++++---- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 examples/flask-hmr-demo/README.md create mode 100644 examples/flask-hmr-demo/app.py diff --git a/examples/flask-hmr-demo/README.md b/examples/flask-hmr-demo/README.md new file mode 100644 index 0000000..32bd6be --- /dev/null +++ b/examples/flask-hmr-demo/README.md @@ -0,0 +1,30 @@ +# Flask HMR Demo + +This is a simple example showing how to use flask-hmr. + +## Usage + +Instead of: +```bash +python app.py +# or +flask run +``` + +Use: +```bash +flask-hmr app:app +``` + +## Features + +- Hot module replacement for Flask applications +- Faster reloads compared to Flask's built-in reload +- Preserves application state where possible + +## Test the HMR + +1. Start the server: `flask-hmr app:app` +2. Visit http://127.0.0.1:5000 +3. Modify the hello() function in app.py +4. See the changes reflected immediately without manual restart \ No newline at end of file diff --git a/examples/flask-hmr-demo/app.py b/examples/flask-hmr-demo/app.py new file mode 100644 index 0000000..c56669f --- /dev/null +++ b/examples/flask-hmr-demo/app.py @@ -0,0 +1,16 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello from Flask with HMR!" + +@app.route("/api/status") +def status(): + return {"status": "running", "hmr": "enabled"} + +if __name__ == "__main__": + # Instead of app.run(), use flask-hmr: + # flask-hmr app:app + app.run(debug=True) \ No newline at end of file diff --git a/packages/flask-hmr/flask_hmr.py b/packages/flask-hmr/flask_hmr.py index 5f3aa1d..d959937 100644 --- a/packages/flask-hmr/flask_hmr.py +++ b/packages/flask-hmr/flask_hmr.py @@ -86,7 +86,7 @@ def run_server(): ignored_paths = [Path(p).resolve() for p in reloader.excludes] if all(is_relative_to_any(path, ignored_paths) or not is_relative_to_any(path, watched_paths) for path in ReactiveModule.instances): logger.error("No files to watch for changes. The server will never reload.") - + secho(f"* Running on http://{host}:{port}", fg="green") server.serve_forever(poll_interval=0.1) finish.set() @@ -124,11 +124,11 @@ def run_entry_file(self): with self.error_filter: load(self.entry_module) flask_app = getattr(self.entry_module, attr) - + # Configure Flask app if debug: flask_app.config['DEBUG'] = True - + start_server(flask_app) @override @@ -178,4 +178,4 @@ def _display_path(path: str | Path): if __name__ == "__main__": - app() \ No newline at end of file + app() From d5124ad9f5e57b734cc62f09d13f1eda5e451500 Mon Sep 17 00:00:00 2001 From: Muspi Merol Date: Tue, 5 Aug 2025 12:43:29 +0800 Subject: [PATCH 4/6] Potential fix for code scanning alert no. 3: Flask app is run in debug mode Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- examples/flask-hmr-demo/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flask-hmr-demo/app.py b/examples/flask-hmr-demo/app.py index c56669f..3a1bc6f 100644 --- a/examples/flask-hmr-demo/app.py +++ b/examples/flask-hmr-demo/app.py @@ -13,4 +13,4 @@ def status(): if __name__ == "__main__": # Instead of app.run(), use flask-hmr: # flask-hmr app:app - app.run(debug=True) \ No newline at end of file + app.run() \ No newline at end of file From 1b90a24c5e21daba454a977c622357cf9d88353f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 04:50:03 +0000 Subject: [PATCH 5/6] Add missing pyproject.toml to flask-hmr-demo example Co-authored-by: CNSeniorious000 <74518716+CNSeniorious000@users.noreply.github.com> --- examples/flask-hmr-demo/pyproject.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 examples/flask-hmr-demo/pyproject.toml diff --git a/examples/flask-hmr-demo/pyproject.toml b/examples/flask-hmr-demo/pyproject.toml new file mode 100644 index 0000000..f2ed4c1 --- /dev/null +++ b/examples/flask-hmr-demo/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "flask-hmr-demo" +version = "0" +requires-python = ">=3.12" +dependencies = [ + "flask~=3.1.0", + "flask-hmr", +] + +[tool.uv] +package = false + +[tool.uv.sources] +flask-hmr = { workspace = true } \ No newline at end of file From 4ba466ae355745dc72a69bcf2d96f8d4bcae8ff9 Mon Sep 17 00:00:00 2001 From: Muspi Merol Date: Tue, 5 Aug 2025 13:02:06 +0800 Subject: [PATCH 6/6] Reformat with ruff --- examples/flask-hmr-demo/app.py | 5 ++++- packages/flask-hmr/flask_hmr.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/flask-hmr-demo/app.py b/examples/flask-hmr-demo/app.py index 3a1bc6f..8a7d644 100644 --- a/examples/flask-hmr-demo/app.py +++ b/examples/flask-hmr-demo/app.py @@ -2,15 +2,18 @@ app = Flask(__name__) + @app.route("/") def hello(): return "Hello from Flask with HMR!" + @app.route("/api/status") def status(): return {"status": "running", "hmr": "enabled"} + if __name__ == "__main__": # Instead of app.run(), use flask-hmr: # flask-hmr app:app - app.run() \ No newline at end of file + app.run() diff --git a/packages/flask-hmr/flask_hmr.py b/packages/flask-hmr/flask_hmr.py index d959937..330ac4d 100644 --- a/packages/flask-hmr/flask_hmr.py +++ b/packages/flask-hmr/flask_hmr.py @@ -127,7 +127,7 @@ def run_entry_file(self): # Configure Flask app if debug: - flask_app.config['DEBUG'] = True + flask_app.config["DEBUG"] = True start_server(flask_app)