Skip to content

Commit

Permalink
Merge branch 'workspace-config' of https://github.com/Archmonger/reactpy
Browse files Browse the repository at this point in the history
 into workspace-config
  • Loading branch information
Archmonger committed Aug 8, 2023
2 parents 0be93c7 + 9d71dc6 commit 30b09eb
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 184 deletions.
8 changes: 7 additions & 1 deletion docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Changelog
Unreleased
----------

Nothing yet...
**Fixed**

- :pull:`1118` - `module_from_template` is broken with a recent release of `requests`


v1.0.2
Expand All @@ -35,11 +37,15 @@ v1.0.1
**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.BackendType``
- :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
28 changes: 28 additions & 0 deletions src/py/reactpy/.temp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from reactpy import component, html, run, use_state
from reactpy.core.types import State


@component
def Item(item: str, all_items: State[list[str]]):
color = use_state(None)

def deleteme(event):
all_items.set_value([i for i in all_items.value if (i != item)])

def colorize(event):
color.set_value("blue" if not color.value else None)

return html.div(
{"id": item, "style": {"background_color": color.value}},
html.button({"on_click": colorize}, f"Color {item}"),
html.button({"on_click": deleteme}, f"Delete {item}"),
)


@component
def App():
items = use_state(["A", "B", "C"])
return html._([Item(item, items, key=item) for item in items.value])


run(App)
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 @@ -14,53 +14,49 @@
from reactpy.utils import vdom_to_html

if TYPE_CHECKING:
import uvicorn
from asgiref.typing import ASGIApplication

PATH_PREFIX = PurePosixPath("/_reactpy")
MODULES_PATH = PATH_PREFIX / "modules"
ASSETS_PATH = PATH_PREFIX / "assets"
STREAM_PATH = PATH_PREFIX / "stream"

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:
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
32 changes: 19 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,26 @@
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 BackendType
from reactpy.backend.utils import SUPPORTED_BACKENDS, all_implementations
from reactpy.types import RootComponentConstructor

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


# BackendType.Options
class Options: # nocov
"""Configuration options that can be provided to the backend.
This definition should not be used/instantiated. It exists only for
type hinting purposes."""

def __init__(self, *args: Any, **kwds: Any) -> NoReturn:
msg = "Default implementation has no options."
raise ValueError(msg)


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


# BackendType.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)


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


_DEFAULT_IMPLEMENTATION: BackendImplementation[Any] | None = None


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

Expand All @@ -59,7 +65,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`"""

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

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


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


# BackendType.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__)


# BackendType.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``
"""


# BackendType.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)


# BackendType.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__)


# BackendType.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

0 comments on commit 30b09eb

Please sign in to comment.