Skip to content

Commit 30b09eb

Browse files
committed
Merge branch 'workspace-config' of https://github.com/Archmonger/reactpy into workspace-config
2 parents 0be93c7 + 9d71dc6 commit 30b09eb

File tree

15 files changed

+235
-184
lines changed

15 files changed

+235
-184
lines changed

docs/source/about/changelog.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ Changelog
1818
Unreleased
1919
----------
2020

21-
Nothing yet...
21+
**Fixed**
22+
23+
- :pull:`1118` - `module_from_template` is broken with a recent release of `requests`
2224

2325

2426
v1.0.2
@@ -35,11 +37,15 @@ v1.0.1
3537
**Changed**
3638

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

3943
**Fixed**
4044

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

4450

4551
v1.0.0

src/py/reactpy/.temp.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from reactpy import component, html, run, use_state
2+
from reactpy.core.types import State
3+
4+
5+
@component
6+
def Item(item: str, all_items: State[list[str]]):
7+
color = use_state(None)
8+
9+
def deleteme(event):
10+
all_items.set_value([i for i in all_items.value if (i != item)])
11+
12+
def colorize(event):
13+
color.set_value("blue" if not color.value else None)
14+
15+
return html.div(
16+
{"id": item, "style": {"background_color": color.value}},
17+
html.button({"on_click": colorize}, f"Color {item}"),
18+
html.button({"on_click": deleteme}, f"Delete {item}"),
19+
)
20+
21+
22+
@component
23+
def App():
24+
items = use_state(["A", "B", "C"])
25+
return html._([Item(item, items, key=item) for item in items.value])
26+
27+
28+
run(App)

src/py/reactpy/reactpy/backend/_common.py

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,49 @@
1414
from reactpy.utils import vdom_to_html
1515

1616
if TYPE_CHECKING:
17+
import uvicorn
1718
from asgiref.typing import ASGIApplication
1819

1920
PATH_PREFIX = PurePosixPath("/_reactpy")
2021
MODULES_PATH = PATH_PREFIX / "modules"
2122
ASSETS_PATH = PATH_PREFIX / "assets"
2223
STREAM_PATH = PATH_PREFIX / "stream"
23-
2424
CLIENT_BUILD_DIR = Path(_reactpy_file_path).parent / "_static" / "app" / "dist"
2525

26-
try:
26+
27+
async def serve_with_uvicorn(
28+
app: ASGIApplication | Any,
29+
host: str,
30+
port: int,
31+
started: asyncio.Event | None,
32+
) -> None:
33+
"""Run a development server for an ASGI application"""
2734
import uvicorn
28-
except ImportError: # nocov
29-
pass
30-
else:
31-
32-
async def serve_development_asgi(
33-
app: ASGIApplication | Any,
34-
host: str,
35-
port: int,
36-
started: asyncio.Event | None,
37-
) -> None:
38-
"""Run a development server for an ASGI application"""
39-
server = uvicorn.Server(
40-
uvicorn.Config(
41-
app,
42-
host=host,
43-
port=port,
44-
loop="asyncio",
45-
reload=True,
46-
)
35+
36+
server = uvicorn.Server(
37+
uvicorn.Config(
38+
app,
39+
host=host,
40+
port=port,
41+
loop="asyncio",
4742
)
48-
server.config.setup_event_loop()
49-
coros: list[Awaitable[Any]] = [server.serve()]
43+
)
44+
server.config.setup_event_loop()
45+
coros: list[Awaitable[Any]] = [server.serve()]
5046

51-
# If a started event is provided, then use it signal based on `server.started`
52-
if started:
53-
coros.append(_check_if_started(server, started))
47+
# If a started event is provided, then use it signal based on `server.started`
48+
if started:
49+
coros.append(_check_if_started(server, started))
5450

55-
try:
56-
await asyncio.gather(*coros)
57-
finally:
58-
# Since we aren't using the uvicorn's `run()` API, we can't guarantee uvicorn's
59-
# order of operations. So we need to make sure `shutdown()` always has an initialized
60-
# list of `self.servers` to use.
61-
if not hasattr(server, "servers"): # nocov
62-
server.servers = []
63-
await asyncio.wait_for(server.shutdown(), timeout=3)
51+
try:
52+
await asyncio.gather(*coros)
53+
finally:
54+
# Since we aren't using the uvicorn's `run()` API, we can't guarantee uvicorn's
55+
# order of operations. So we need to make sure `shutdown()` always has an initialized
56+
# list of `self.servers` to use.
57+
if not hasattr(server, "servers"): # nocov
58+
server.servers = []
59+
await asyncio.wait_for(server.shutdown(), timeout=3)
6460

6561

6662
async def _check_if_started(server: uvicorn.Server, started: asyncio.Event) -> None:
@@ -72,8 +68,7 @@ async def _check_if_started(server: uvicorn.Server, started: asyncio.Event) -> N
7268
def safe_client_build_dir_path(path: str) -> Path:
7369
"""Prevent path traversal out of :data:`CLIENT_BUILD_DIR`"""
7470
return traversal_safe_path(
75-
CLIENT_BUILD_DIR,
76-
*("index.html" if path in ("", "/") else path).split("/"),
71+
CLIENT_BUILD_DIR, *("index.html" if path in {"", "/"} else path).split("/")
7772
)
7873

7974

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

138+
serve_index_route: bool = True
139+
"""Automatically generate and serve the index route (``/``)"""
140+
143141
def __post_init__(self) -> None:
144142
if self.url_prefix and not self.url_prefix.startswith("/"):
145143
msg = "Expected 'url_prefix' to start with '/'"

src/py/reactpy/reactpy/backend/default.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@
55
from sys import exc_info
66
from typing import Any, NoReturn
77

8-
from reactpy.backend.types import BackendImplementation
9-
from reactpy.backend.utils import SUPPORTED_PACKAGES, all_implementations
8+
from reactpy.backend.types import BackendType
9+
from reactpy.backend.utils import SUPPORTED_BACKENDS, all_implementations
1010
from reactpy.types import RootComponentConstructor
1111

1212
logger = getLogger(__name__)
13+
_DEFAULT_IMPLEMENTATION: BackendType[Any] | None = None
1314

1415

16+
# BackendType.Options
17+
class Options: # nocov
18+
"""Configuration options that can be provided to the backend.
19+
This definition should not be used/instantiated. It exists only for
20+
type hinting purposes."""
21+
22+
def __init__(self, *args: Any, **kwds: Any) -> NoReturn:
23+
msg = "Default implementation has no options."
24+
raise ValueError(msg)
25+
26+
27+
# BackendType.configure
1528
def configure(
1629
app: Any, component: RootComponentConstructor, options: None = None
1730
) -> None:
@@ -22,17 +35,13 @@ def configure(
2235
return _default_implementation().configure(app, component)
2336

2437

38+
# BackendType.create_development_app
2539
def create_development_app() -> Any:
2640
"""Create an application instance for development purposes"""
2741
return _default_implementation().create_development_app()
2842

2943

30-
def Options(*args: Any, **kwargs: Any) -> NoReturn: # nocov
31-
"""Create configuration options"""
32-
msg = "Default implementation has no options."
33-
raise ValueError(msg)
34-
35-
44+
# BackendType.serve_development_app
3645
async def serve_development_app(
3746
app: Any,
3847
host: str,
@@ -45,10 +54,7 @@ async def serve_development_app(
4554
)
4655

4756

48-
_DEFAULT_IMPLEMENTATION: BackendImplementation[Any] | None = None
49-
50-
51-
def _default_implementation() -> BackendImplementation[Any]:
57+
def _default_implementation() -> BackendType[Any]:
5258
"""Get the first available server implementation"""
5359
global _DEFAULT_IMPLEMENTATION # noqa: PLW0603
5460

@@ -59,7 +65,7 @@ def _default_implementation() -> BackendImplementation[Any]:
5965
implementation = next(all_implementations())
6066
except StopIteration: # nocov
6167
logger.debug("Backend implementation import failed", exc_info=exc_info())
62-
supported_backends = ", ".join(SUPPORTED_PACKAGES)
68+
supported_backends = ", ".join(SUPPORTED_BACKENDS)
6369
msg = (
6470
"It seems you haven't installed a backend. To resolve this issue, "
6571
"you can install a backend by running:\n\n"

src/py/reactpy/reactpy/backend/fastapi.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@
44

55
from reactpy.backend import starlette
66

7-
serve_development_app = starlette.serve_development_app
8-
"""Alias for :func:`reactpy.backend.starlette.serve_development_app`"""
9-
10-
use_connection = starlette.use_connection
11-
"""Alias for :func:`reactpy.backend.starlette.use_location`"""
12-
13-
use_websocket = starlette.use_websocket
14-
"""Alias for :func:`reactpy.backend.starlette.use_websocket`"""
15-
7+
# BackendType.Options
168
Options = starlette.Options
17-
"""Alias for :class:`reactpy.backend.starlette.Options`"""
189

10+
# BackendType.configure
1911
configure = starlette.configure
20-
"""Alias for :class:`reactpy.backend.starlette.configure`"""
2112

2213

14+
# BackendType.create_development_app
2315
def create_development_app() -> FastAPI:
2416
"""Create a development ``FastAPI`` application instance."""
2517
return FastAPI(debug=True)
18+
19+
20+
# BackendType.serve_development_app
21+
serve_development_app = starlette.serve_development_app
22+
23+
use_connection = starlette.use_connection
24+
25+
use_websocket = starlette.use_websocket

src/py/reactpy/reactpy/backend/flask.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@
4545
logger = logging.getLogger(__name__)
4646

4747

48+
# BackendType.Options
49+
@dataclass
50+
class Options(CommonOptions):
51+
"""Render server config for :func:`reactpy.backend.flask.configure`"""
52+
53+
cors: bool | dict[str, Any] = False
54+
"""Enable or configure Cross Origin Resource Sharing (CORS)
55+
56+
For more information see docs for ``flask_cors.CORS``
57+
"""
58+
59+
60+
# BackendType.configure
4861
def configure(
4962
app: Flask, component: RootComponentConstructor, options: Options | None = None
5063
) -> None:
@@ -69,20 +82,21 @@ def configure(
6982
app.register_blueprint(spa_bp)
7083

7184

85+
# BackendType.create_development_app
7286
def create_development_app() -> Flask:
7387
"""Create an application instance for development purposes"""
7488
os.environ["FLASK_DEBUG"] = "true"
75-
app = Flask(__name__)
76-
return app
89+
return Flask(__name__)
7790

7891

92+
# BackendType.serve_development_app
7993
async def serve_development_app(
8094
app: Flask,
8195
host: str,
8296
port: int,
8397
started: asyncio.Event | None = None,
8498
) -> None:
85-
"""Run an application using a development server"""
99+
"""Run a development server for FastAPI"""
86100
loop = asyncio.get_running_loop()
87101
stopped = asyncio.Event()
88102

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

137151

138-
@dataclass
139-
class Options(CommonOptions):
140-
"""Render server config for :func:`reactpy.backend.flask.configure`"""
141-
142-
cors: bool | dict[str, Any] = False
143-
"""Enable or configure Cross Origin Resource Sharing (CORS)
144-
145-
For more information see docs for ``flask_cors.CORS``
146-
"""
147-
148-
149152
def _setup_common_routes(
150153
api_blueprint: Blueprint,
151154
spa_blueprint: Blueprint,
@@ -166,10 +169,12 @@ def send_modules_dir(path: str = "") -> Any:
166169

167170
index_html = read_client_index_html(options)
168171

169-
@spa_blueprint.route("/")
170-
@spa_blueprint.route("/<path:_>")
171-
def send_client_dir(_: str = "") -> Any:
172-
return index_html
172+
if options.serve_index_route:
173+
174+
@spa_blueprint.route("/")
175+
@spa_blueprint.route("/<path:_>")
176+
def send_client_dir(_: str = "") -> Any:
177+
return index_html
173178

174179

175180
def _setup_single_view_dispatcher_route(

0 commit comments

Comments
 (0)