Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reactpy.run and configure(...) refactoring #1051

Merged
merged 35 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
876c992
More predictable `reactpy.run` failures
Archmonger Jun 14, 2023
465ef00
move starlette to bottom so fastapi has a chance to run
Archmonger Jun 14, 2023
43137d7
remove testing from all
Archmonger Jun 14, 2023
ab426e6
refactoring and flask with uvicorn
Archmonger Jun 14, 2023
4aebab5
Remove accidental staticmethod
Archmonger Jun 14, 2023
5a3f68d
Add self to protocol
Archmonger Jun 14, 2023
e06db92
Fix windows port assignment bug
Archmonger Jun 14, 2023
8217963
remove unusable reload parameter
Archmonger Jun 14, 2023
49c6a26
remove useless f string
Archmonger Jun 14, 2023
7785536
reset lock file
Archmonger Jun 14, 2023
8cf1d3f
add uvicorn to flask
Archmonger Jun 14, 2023
e799275
Revert "add uvicorn to flask"
Archmonger Jun 14, 2023
6f2a18c
Revert "add uvicorn to flask"
Archmonger Jun 14, 2023
577a6d3
lint fixes
Archmonger Jun 14, 2023
9bbd078
more lint fix
Archmonger Jun 14, 2023
fdc04d9
add changelog
Archmonger Jun 14, 2023
093625c
remove dead requirements.txt
Archmonger Jun 14, 2023
24d484b
bump workflow versions
Archmonger Jun 14, 2023
fd117ca
Merge remote-tracking branch 'upstream/main' into reactpy-run-cleanup
Archmonger Jun 15, 2023
955a845
Revert "bump workflow versions"
Archmonger Jun 15, 2023
67ca3df
Rename SUPPORTED_PACKAGES to SUPPORTED_BACKENDS
Archmonger Jun 15, 2023
12ba3ac
Merge remote-tracking branch 'upstream/main' into reactpy-run-cleanup
Archmonger Jun 15, 2023
0adc456
fix merge issue
Archmonger Jun 15, 2023
a827ca1
move around stuff
Archmonger Jun 15, 2023
9e3248c
don't clutter the routes when not needed
Archmonger Jun 15, 2023
15932a4
import uvicorn if type checking
Archmonger Jul 4, 2023
a28f75f
change options constructor to init
Archmonger Jul 4, 2023
cf90737
BackendProtocol -> BackendType
Archmonger Jul 4, 2023
e68cebb
revert host str
Archmonger Jul 4, 2023
1175fbc
options init docstring
Archmonger Jul 6, 2023
9f31619
fix docstring
Archmonger Jul 6, 2023
a7e4239
rename BackendType comments
Archmonger Jul 6, 2023
e1f69fb
move comment position
Archmonger Jul 6, 2023
5429c33
SO_REUSEADDR
Archmonger Jul 7, 2023
bc3540e
Merge branch 'main' into reactpy-run-cleanup
Archmonger Jul 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ Unreleased
**Changed**

- :pull:`1050` - Warn and attempt to fix missing mime types, which can result in ``reactpy.run`` not working as expected.
- :pull:`1051` - Rename ``reactpy.backend.BackendImplementation`` to ``reactpy.backend.BackendProtocol``
- :pull:`1051` - Allow ``reactpy.run`` to fail in more predictable ways

**Fixed**

- :issue:`930` - better traceback for JSON serialization errors (via :pull:`1008`)
- :issue:`437` - explain that JS component attributes must be JSON (via :pull:`1008`)
- :pull:`1051` - Fix ``reactpy.run`` port assignment sometimes attaching to in-use ports on Windows
- :pull:`1051` - Fix ``reactpy.run`` not recognizing ``fastapi``


v1.0.0
Expand Down
9 changes: 0 additions & 9 deletions requirements.txt

This file was deleted.

72 changes: 35 additions & 37 deletions src/py/reactpy/reactpy/backend/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,47 +23,43 @@

CLIENT_BUILD_DIR = Path(_reactpy_file_path).parent / "_static" / "app" / "dist"

try:

async def serve_with_uvicorn(
app: ASGIApplication | Any,
host: str,
port: int,
started: asyncio.Event | None,
) -> None:
"""Run a development server for an ASGI application"""
import uvicorn
except ImportError: # nocov
pass
else:

async def serve_development_asgi(
app: ASGIApplication | Any,
host: str,
port: int,
started: asyncio.Event | None,
) -> None:
"""Run a development server for an ASGI application"""
server = uvicorn.Server(
uvicorn.Config(
app,
host=host,
port=port,
loop="asyncio",
reload=True,
)

server = uvicorn.Server(
uvicorn.Config(
app,
host=host,
port=port,
loop="asyncio",
)
server.config.setup_event_loop()
coros: list[Awaitable[Any]] = [server.serve()]
)
server.config.setup_event_loop()
coros: list[Awaitable[Any]] = [server.serve()]

# If a started event is provided, then use it signal based on `server.started`
if started:
coros.append(_check_if_started(server, started))
# If a started event is provided, then use it signal based on `server.started`
if started:
coros.append(_check_if_started(server, started))

try:
await asyncio.gather(*coros)
finally:
# Since we aren't using the uvicorn's `run()` API, we can't guarantee uvicorn's
# order of operations. So we need to make sure `shutdown()` always has an initialized
# list of `self.servers` to use.
if not hasattr(server, "servers"): # nocov
server.servers = []
await asyncio.wait_for(server.shutdown(), timeout=3)
try:
await asyncio.gather(*coros)
finally:
# Since we aren't using the uvicorn's `run()` API, we can't guarantee uvicorn's
# order of operations. So we need to make sure `shutdown()` always has an initialized
# list of `self.servers` to use.
if not hasattr(server, "servers"): # nocov
server.servers = []
await asyncio.wait_for(server.shutdown(), timeout=3)


async def _check_if_started(server: uvicorn.Server, started: asyncio.Event) -> None:
async def _check_if_started(server: Any, started: asyncio.Event) -> None:
Archmonger marked this conversation as resolved.
Show resolved Hide resolved
while not server.started:
await asyncio.sleep(0.2)
started.set()
Expand All @@ -72,8 +68,7 @@ async def _check_if_started(server: uvicorn.Server, started: asyncio.Event) -> N
def safe_client_build_dir_path(path: str) -> Path:
"""Prevent path traversal out of :data:`CLIENT_BUILD_DIR`"""
return traversal_safe_path(
CLIENT_BUILD_DIR,
*("index.html" if path in ("", "/") else path).split("/"),
CLIENT_BUILD_DIR, *("index.html" if path in {"", "/"} else path).split("/")
)


Expand Down Expand Up @@ -140,6 +135,9 @@ class CommonOptions:
url_prefix: str = ""
"""The URL prefix where ReactPy resources will be served from"""

serve_index_route: bool = True
"""Automatically generate and serve the index route (``/``)"""

def __post_init__(self) -> None:
if self.url_prefix and not self.url_prefix.startswith("/"):
msg = "Expected 'url_prefix' to start with '/'"
Expand Down
30 changes: 17 additions & 13 deletions src/py/reactpy/reactpy/backend/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@
from sys import exc_info
from typing import Any, NoReturn

from reactpy.backend.types import BackendImplementation
from reactpy.backend.utils import SUPPORTED_PACKAGES, all_implementations
from reactpy.backend.types import BackendProtocol
from reactpy.backend.utils import SUPPORTED_BACKENDS, all_implementations
from reactpy.types import RootComponentConstructor

logger = getLogger(__name__)
_DEFAULT_IMPLEMENTATION: BackendProtocol[Any] | None = None


# BackendProtocol.Options
class Options: # nocov
"""Create configuration options"""

def __call__(self, *args: Any, **kwds: Any) -> NoReturn:
Archmonger marked this conversation as resolved.
Show resolved Hide resolved
msg = "Default implementation has no options."
raise ValueError(msg)


# BackendProtocol.configure
def configure(
app: Any, component: RootComponentConstructor, options: None = None
) -> None:
Expand All @@ -22,17 +33,13 @@ def configure(
return _default_implementation().configure(app, component)


# BackendProtocol.create_development_app
def create_development_app() -> Any:
"""Create an application instance for development purposes"""
return _default_implementation().create_development_app()


def Options(*args: Any, **kwargs: Any) -> NoReturn: # nocov
"""Create configuration options"""
msg = "Default implementation has no options."
raise ValueError(msg)


# BackendProtocol.serve_development_app
async def serve_development_app(
app: Any,
host: str,
Expand All @@ -45,10 +52,7 @@ async def serve_development_app(
)


_DEFAULT_IMPLEMENTATION: BackendImplementation[Any] | None = None


def _default_implementation() -> BackendImplementation[Any]:
def _default_implementation() -> BackendProtocol[Any]:
"""Get the first available server implementation"""
global _DEFAULT_IMPLEMENTATION # noqa: PLW0603

Expand All @@ -59,7 +63,7 @@ def _default_implementation() -> BackendImplementation[Any]:
implementation = next(all_implementations())
except StopIteration: # nocov
logger.debug("Backend implementation import failed", exc_info=exc_info())
supported_backends = ", ".join(SUPPORTED_PACKAGES)
supported_backends = ", ".join(SUPPORTED_BACKENDS)
msg = (
"It seems you haven't installed a backend. To resolve this issue, "
"you can install a backend by running:\n\n"
Expand Down
22 changes: 11 additions & 11 deletions src/py/reactpy/reactpy/backend/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@

from reactpy.backend import starlette

serve_development_app = starlette.serve_development_app
"""Alias for :func:`reactpy.backend.starlette.serve_development_app`"""

use_connection = starlette.use_connection
"""Alias for :func:`reactpy.backend.starlette.use_location`"""

use_websocket = starlette.use_websocket
"""Alias for :func:`reactpy.backend.starlette.use_websocket`"""

# BackendProtocol.Options
Options = starlette.Options
"""Alias for :class:`reactpy.backend.starlette.Options`"""

# BackendProtocol.configure
configure = starlette.configure
"""Alias for :class:`reactpy.backend.starlette.configure`"""


# BackendProtocol.create_development_app
def create_development_app() -> FastAPI:
"""Create a development ``FastAPI`` application instance."""
return FastAPI(debug=True)


# BackendProtocol.serve_development_app
serve_development_app = starlette.serve_development_app

use_connection = starlette.use_connection

use_websocket = starlette.use_websocket
41 changes: 23 additions & 18 deletions src/py/reactpy/reactpy/backend/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@
logger = logging.getLogger(__name__)


# BackendProtocol.Options
@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.flask.configure`"""

cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)

For more information see docs for ``flask_cors.CORS``
"""


# BackendProtocol.configure
def configure(
app: Flask, component: RootComponentConstructor, options: Options | None = None
) -> None:
Expand All @@ -69,20 +82,21 @@ def configure(
app.register_blueprint(spa_bp)


# BackendProtocol.create_development_app
def create_development_app() -> Flask:
"""Create an application instance for development purposes"""
os.environ["FLASK_DEBUG"] = "true"
app = Flask(__name__)
return app
return Flask(__name__)


# BackendProtocol.serve_development_app
async def serve_development_app(
app: Flask,
host: str,
port: int,
started: asyncio.Event | None = None,
) -> None:
"""Run an application using a development server"""
"""Run a development server for FastAPI"""
loop = asyncio.get_running_loop()
stopped = asyncio.Event()

Expand Down Expand Up @@ -135,17 +149,6 @@ def use_connection() -> Connection[_FlaskCarrier]:
return conn


@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.flask.configure`"""

cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)

For more information see docs for ``flask_cors.CORS``
"""


def _setup_common_routes(
api_blueprint: Blueprint,
spa_blueprint: Blueprint,
Expand All @@ -166,10 +169,12 @@ def send_modules_dir(path: str = "") -> Any:

index_html = read_client_index_html(options)

@spa_blueprint.route("/")
@spa_blueprint.route("/<path:_>")
def send_client_dir(_: str = "") -> Any:
return index_html
if options.serve_index_route:

@spa_blueprint.route("/")
@spa_blueprint.route("/<path:_>")
def send_client_dir(_: str = "") -> Any:
return index_html


def _setup_single_view_dispatcher_route(
Expand Down
Loading