Skip to content

Commit 731fc9b

Browse files
authored
Merge branch 'main' into async-effects
2 parents 2d0c1ae + 701e462 commit 731fc9b

File tree

19 files changed

+271
-211
lines changed

19 files changed

+271
-211
lines changed

docs/source/about/changelog.rst

+8-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.
2323
Unreleased
2424
----------
2525

26-
Nothing yet...
26+
**Fixed**
27+
28+
- :pull:`1118` - `module_from_template` is broken with a recent release of `requests`
29+
- :pull:`1131` - `module_from_template` did not work when using Flask backend
2730

2831

2932
v1.0.2
@@ -40,11 +43,15 @@ v1.0.1
4043
**Changed**
4144

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

4449
**Fixed**
4550

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

4956

5057
v1.0.0

pyproject.toml

+19-7
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ dependencies = [
1313
"invoke",
1414
# lint
1515
"black",
16-
"ruff==0.0.278", # Ruff is moving really fast, so pinning for now.
16+
"ruff==0.0.278", # Ruff is moving really fast, so pinning for now.
1717
"toml",
1818
"flake8",
1919
"flake8-pyproject",
@@ -32,9 +32,11 @@ publish = "invoke publish {args}"
3232
docs = "invoke docs {args}"
3333
check = ["lint-py", "lint-js", "test-py", "test-js", "test-docs"]
3434

35+
lint = ["lint-py", "lint-js"]
3536
lint-py = "invoke lint-py {args}"
3637
lint-js = "invoke lint-js {args}"
3738

39+
test = ["test-py", "test-js", "test-docs"]
3840
test-py = "invoke test-py {args}"
3941
test-js = "invoke test-js"
4042
test-docs = "invoke test-docs"
@@ -56,7 +58,7 @@ warn_unused_ignores = true
5658
# --- Flake8 ---------------------------------------------------------------------------
5759

5860
[tool.flake8]
59-
select = ["RPY"] # only need to check with reactpy-flake8
61+
select = ["RPY"] # only need to check with reactpy-flake8
6062
exclude = ["**/node_modules/*", ".eggs/*", ".tox/*", "**/venv/*"]
6163

6264
# --- Ruff -----------------------------------------------------------------------------
@@ -95,27 +97,37 @@ select = [
9597
]
9698
ignore = [
9799
# TODO: turn this on later
98-
"N802", "N806", # allow TitleCase functions/variables
100+
"N802",
101+
"N806", # allow TitleCase functions/variables
99102
# We're not any cryptography
100103
"S311",
101104
# For loop variable re-assignment seems like an uncommon mistake
102105
"PLW2901",
103106
# Let Black deal with line-length
104107
"E501",
105108
# Allow args/attrs to shadow built-ins
106-
"A002", "A003",
109+
"A002",
110+
"A003",
107111
# Allow unused args (useful for documenting what the parameter is for later)
108-
"ARG001", "ARG002", "ARG005",
112+
"ARG001",
113+
"ARG002",
114+
"ARG005",
109115
# Allow non-abstract empty methods in abstract base classes
110116
"B027",
111117
# Allow boolean positional values in function calls, like `dict.get(... True)`
112118
"FBT003",
113119
# If we're making an explicit comparison to a falsy value it was probably intentional
114120
"PLC1901",
115121
# Ignore checks for possible passwords
116-
"S105", "S106", "S107",
122+
"S105",
123+
"S106",
124+
"S107",
117125
# Ignore complexity
118-
"C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915",
126+
"C901",
127+
"PLR0911",
128+
"PLR0912",
129+
"PLR0913",
130+
"PLR0915",
119131
]
120132
unfixable = [
121133
# Don't touch unused imports

requirements.txt

-9
This file was deleted.

src/py/reactpy/.temp.py

+28
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

+35-37
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

+19-13
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

+11-11
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

0 commit comments

Comments
 (0)