22import os
33import re
44from collections .abc import Coroutine , Sequence
5+ from dataclasses import dataclass
56from email .utils import formatdate
67from logging import getLogger
78from pathlib import Path
89from typing import Any , Callable
910
1011from reactpy import html
1112from reactpy .backend .middleware import ReactPyMiddleware
12- from reactpy .backend .utils import dict_to_byte_list , find_and_replace , vdom_head_to_html
13+ from reactpy .backend .utils import dict_to_byte_list , replace_many , vdom_head_to_html
1314from reactpy .core .types import VdomDict
1415from reactpy .types import RootComponentConstructor
1516
1617_logger = getLogger (__name__ )
1718
1819
1920class ReactPy (ReactPyMiddleware ):
20- cached_index_html = ""
21- etag = ""
22- last_modified = ""
23- templates_dir = Path (__file__ ).parent .parent / "templates"
24- index_html_path = templates_dir / "index.html"
2521 multiple_root_components = False
2622
2723 def __init__ (
2824 self ,
2925 root_component : RootComponentConstructor ,
3026 * ,
31- path_prefix : str = "reactpy/" ,
27+ path_prefix : str = "/ reactpy/" ,
3228 web_modules_dir : Path | None = None ,
3329 http_headers : dict [str , str | int ] | None = None ,
3430 html_head : VdomDict | None = None ,
3531 html_lang : str = "en" ,
3632 ) -> None :
3733 super ().__init__ (
38- app = self . reactpy_app ,
34+ app = ReactPyApp ( self ) ,
3935 root_components = [],
4036 path_prefix = path_prefix ,
4137 web_modules_dir = web_modules_dir ,
@@ -46,7 +42,17 @@ def __init__(
4642 self .html_head = html_head or html .head ()
4743 self .html_lang = html_lang
4844
49- async def reactpy_app (
45+
46+ @dataclass
47+ class ReactPyApp :
48+ parent : ReactPy
49+ _cached_index_html = ""
50+ _etag = ""
51+ _last_modified = ""
52+ _templates_dir = Path (__file__ ).parent .parent / "templates"
53+ _index_html_path = _templates_dir / "index.html"
54+
55+ async def __call__ (
5056 self ,
5157 scope : dict [str , Any ],
5258 receive : Callable [..., Coroutine ],
@@ -65,18 +71,18 @@ async def reactpy_app(
6571 return
6672
6773 # Store the HTTP response in memory for performance
68- if not self .cached_index_html :
74+ if not self ._cached_index_html :
6975 self .process_index_html ()
7076
7177 # Return headers for all HTTP responses
7278 request_headers = dict (scope ["headers" ])
7379 response_headers : dict [str , str | int ] = {
74- "etag" : self .etag ,
75- "last-modified" : self .last_modified ,
80+ "etag" : self ._etag ,
81+ "last-modified" : self ._last_modified ,
7682 "access-control-allow-origin" : "*" ,
7783 "cache-control" : "max-age=60, public" ,
78- "content-length" : len (self .cached_index_html ),
79- ** self .extra_headers ,
84+ "content-length" : len (self ._cached_index_html ),
85+ ** self .parent . extra_headers ,
8086 }
8187
8288 # Browser is asking for the headers
@@ -91,7 +97,7 @@ async def reactpy_app(
9197 )
9298
9399 # Browser already has the content cached
94- if request_headers .get (b"if-none-match" ) == self .etag .encode ():
100+ if request_headers .get (b"if-none-match" ) == self ._etag .encode ():
95101 response_headers .pop ("content-length" )
96102 return await http_response (
97103 scope ["method" ],
@@ -107,37 +113,37 @@ async def reactpy_app(
107113 scope ["method" ],
108114 send ,
109115 200 ,
110- self .cached_index_html ,
116+ self ._cached_index_html ,
111117 content_type = b"text/html" ,
112118 headers = dict_to_byte_list (response_headers ),
113119 )
114120
115121 def match_dispatch_path (self , scope : dict ) -> bool :
116122 """Method override to remove `dotted_path` from the dispatcher URL."""
117- return str (scope ["path" ]) == self .dispatcher_path
123+ return str (scope ["path" ]) == self .parent . dispatcher_path
118124
119125 def process_index_html (self ):
120126 """Process the index.html and store the results in memory."""
121- with open (self .index_html_path , encoding = "utf-8" ) as file_handle :
127+ with open (self ._index_html_path , encoding = "utf-8" ) as file_handle :
122128 cached_index_html = file_handle .read ()
123129
124- self .cached_index_html = find_and_replace (
130+ self ._cached_index_html = replace_many (
125131 cached_index_html ,
126132 {
127- 'from "index.ts"' : f'from "{ self .static_path } index.js"' ,
128- '<html lang="en">' : f'<html lang="{ self .html_lang } ">' ,
129- "<head></head>" : vdom_head_to_html (self .html_head ),
130- "{path_prefix}" : self .path_prefix ,
133+ 'from "index.ts"' : f'from "{ self .parent . static_path } index.js"' ,
134+ '<html lang="en">' : f'<html lang="{ self .parent . html_lang } ">' ,
135+ "<head></head>" : vdom_head_to_html (self .parent . html_head ),
136+ "{path_prefix}" : self .parent . path_prefix ,
131137 "{reconnect_interval}" : "750" ,
132138 "{reconnect_max_interval}" : "60000" ,
133139 "{reconnect_max_retries}" : "150" ,
134140 "{reconnect_backoff_multiplier}" : "1.25" ,
135141 },
136142 )
137143
138- self .etag = f'"{ hashlib .md5 (self .cached_index_html .encode (), usedforsecurity = False ).hexdigest ()} "'
139- self .last_modified = formatdate (
140- os .stat (self .index_html_path ).st_mtime , usegmt = True
144+ self ._etag = f'"{ hashlib .md5 (self ._cached_index_html .encode (), usedforsecurity = False ).hexdigest ()} "'
145+ self ._last_modified = formatdate (
146+ os .stat (self ._index_html_path ).st_mtime , usegmt = True
141147 )
142148
143149
0 commit comments