reflex 0.7.8a1__py3-none-any.whl → 0.7.9a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

Files changed (43) hide show
  1. reflex/.templates/jinja/web/tailwind.config.js.jinja2 +65 -31
  2. reflex/.templates/web/utils/state.js +11 -1
  3. reflex/app.py +185 -79
  4. reflex/app_mixins/lifespan.py +2 -2
  5. reflex/compiler/compiler.py +31 -4
  6. reflex/components/base/body.pyi +3 -197
  7. reflex/components/base/link.pyi +4 -392
  8. reflex/components/base/meta.pyi +28 -608
  9. reflex/components/component.py +39 -57
  10. reflex/components/core/upload.py +8 -0
  11. reflex/components/dynamic.py +9 -1
  12. reflex/components/el/elements/metadata.pyi +0 -1
  13. reflex/components/markdown/markdown.py +0 -21
  14. reflex/components/markdown/markdown.pyi +2 -2
  15. reflex/components/radix/primitives/accordion.py +1 -1
  16. reflex/components/radix/primitives/form.py +1 -1
  17. reflex/components/radix/primitives/progress.py +1 -1
  18. reflex/components/radix/primitives/slider.py +1 -1
  19. reflex/components/radix/themes/color_mode.py +1 -1
  20. reflex/components/radix/themes/color_mode.pyi +1 -1
  21. reflex/components/radix/themes/layout/list.pyi +2 -391
  22. reflex/components/recharts/recharts.py +2 -2
  23. reflex/components/sonner/toast.py +1 -1
  24. reflex/config.py +4 -7
  25. reflex/constants/base.py +21 -0
  26. reflex/constants/installer.py +6 -6
  27. reflex/custom_components/custom_components.py +67 -64
  28. reflex/event.py +2 -0
  29. reflex/reflex.py +276 -265
  30. reflex/testing.py +30 -24
  31. reflex/utils/codespaces.py +6 -2
  32. reflex/utils/console.py +4 -3
  33. reflex/utils/exec.py +60 -24
  34. reflex/utils/format.py +17 -2
  35. reflex/utils/prerequisites.py +43 -30
  36. reflex/utils/processes.py +6 -6
  37. reflex/utils/types.py +11 -6
  38. reflex/vars/base.py +19 -1
  39. {reflex-0.7.8a1.dist-info → reflex-0.7.9a1.dist-info}/METADATA +6 -9
  40. {reflex-0.7.8a1.dist-info → reflex-0.7.9a1.dist-info}/RECORD +43 -43
  41. {reflex-0.7.8a1.dist-info → reflex-0.7.9a1.dist-info}/WHEEL +0 -0
  42. {reflex-0.7.8a1.dist-info → reflex-0.7.9a1.dist-info}/entry_points.txt +0 -0
  43. {reflex-0.7.8a1.dist-info → reflex-0.7.9a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,32 +1,66 @@
1
- /** @type {import('tailwindcss').Config} */
2
- module.exports = {
3
- content: {{content|json_dumps}},
4
- theme: {{theme|json_dumps}},
5
- plugins: [
6
- {% for plugin in plugins %}
7
- require({{plugin|json_dumps}}),
8
- {% endfor %}
9
- ],
10
- {% if presets is defined %}
11
- presets: [
12
- {% for preset in presets %}
13
- require({{preset|json_dumps}})
14
- {% endfor %}
15
- ],
16
- {% endif %}
17
- {% if darkMode is defined %}
18
- darkMode: {{darkMode|json_dumps}},
19
- {% endif %}
20
- {% if corePlugins is defined %}
21
- corePlugins: {{corePlugins|json_dumps}},
22
- {% endif %}
23
- {% if important is defined %}
24
- important: {{important|json_dumps}},
25
- {% endif %}
26
- {% if prefix is defined %}
27
- prefix: {{prefix|json_dumps}},
28
- {% endif %}
29
- {% if separator is defined %}
30
- separator: {{separator|json_dumps}},
31
- {% endif %}
1
+ {# Helper macro to render JS objects and arrays #}
2
+ {% macro render_js(val, indent=2, level=0) -%}
3
+ {%- set space = ' ' * (indent * level) -%}
4
+ {%- set next_space = ' ' * (indent * (level + 1)) -%}
5
+
6
+ {%- if val is mapping -%}
7
+ {
8
+ {%- for k, v in val.items() %}
9
+ {{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
10
+ {%- endfor %}
11
+ {{ space }}}
12
+ {%- elif val is iterable and val is not string -%}
13
+ [
14
+ {%- for item in val %}
15
+ {{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
16
+ {%- endfor %}
17
+ {{ space }}]
18
+ {%- else -%}
19
+ {{ val | tojson }}
20
+ {%- endif -%}
21
+ {%- endmacro %}
22
+
23
+ {# Extract destructured imports from plugin dicts only #}
24
+ {%- set imports = [] %}
25
+ {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
26
+ {%- set _ = imports.append(plugin.import) %}
27
+ {%- endfor %}
28
+
29
+ /** @type {import('tailwindcss').Config} */
30
+ {%- for imp in imports %}
31
+ const { {{ imp.name }} } = require({{ imp.from | tojson }});
32
+ {%- endfor %}
33
+
34
+ module.exports = {
35
+ content: {{ render_js(content) }},
36
+ theme: {{ render_js(theme) }},
37
+ {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
38
+ {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
39
+ {% if important is defined %}important: {{ important | tojson }},{% endif %}
40
+ {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
41
+ {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
42
+ {% if presets is defined %}
43
+ presets: [
44
+ {% for preset in presets %}
45
+ require({{ preset | tojson }}){{ "," if not loop.last }}
46
+ {% endfor %}
47
+ ],
48
+ {% endif %}
49
+ plugins: [
50
+ {% for plugin in plugins %}
51
+ {% if plugin is mapping %}
52
+ {% if plugin.call is defined %}
53
+ {{ plugin.call }}(
54
+ {%- if plugin.args is defined -%}
55
+ {{ render_js(plugin.args) }}
56
+ {%- endif -%}
57
+ ){{ "," if not loop.last }}
58
+ {% else %}
59
+ require({{ plugin.name | tojson }}){{ "," if not loop.last }}
60
+ {% endif %}
61
+ {% else %}
62
+ require({{ plugin | tojson }}){{ "," if not loop.last }}
63
+ {% endif %}
64
+ {% endfor %}
65
+ ]
32
66
  };
@@ -178,6 +178,9 @@ export const queueEventIfSocketExists = async (events, socket) => {
178
178
  export const applyEvent = async (event, socket) => {
179
179
  // Handle special events
180
180
  if (event.name == "_redirect") {
181
+ if ((event.payload.path ?? undefined) === undefined) {
182
+ return false;
183
+ }
181
184
  if (event.payload.external) {
182
185
  window.open(event.payload.path, "_blank", "noopener");
183
186
  } else if (event.payload.replace) {
@@ -240,7 +243,14 @@ export const applyEvent = async (event, socket) => {
240
243
  if (event.name == "_set_focus") {
241
244
  const ref =
242
245
  event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
243
- ref.current.focus();
246
+ const focus = ref?.current?.focus;
247
+ if (focus === undefined) {
248
+ console.error(
249
+ `No element found for ref ${event.payload.ref} in _set_focus`,
250
+ );
251
+ } else {
252
+ focus();
253
+ }
244
254
  return false;
245
255
  }
246
256
 
reflex/app.py CHANGED
@@ -13,34 +13,43 @@ import io
13
13
  import json
14
14
  import sys
15
15
  import traceback
16
- from collections.abc import AsyncIterator, Callable, Coroutine, MutableMapping
16
+ from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
17
17
  from datetime import datetime
18
18
  from pathlib import Path
19
19
  from timeit import default_timer as timer
20
20
  from types import SimpleNamespace
21
21
  from typing import TYPE_CHECKING, Any, BinaryIO, get_args, get_type_hints
22
22
 
23
- from fastapi import FastAPI, HTTPException, Request
24
- from fastapi import UploadFile as FastAPIUploadFile
25
- from fastapi.middleware import cors
26
- from fastapi.responses import JSONResponse, StreamingResponse
27
- from fastapi.staticfiles import StaticFiles
23
+ from fastapi import FastAPI
28
24
  from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
29
- from socketio import ASGIApp, AsyncNamespace, AsyncServer
25
+ from socketio import ASGIApp as EngineIOApp
26
+ from socketio import AsyncNamespace, AsyncServer
27
+ from starlette.applications import Starlette
30
28
  from starlette.datastructures import Headers
31
29
  from starlette.datastructures import UploadFile as StarletteUploadFile
30
+ from starlette.exceptions import HTTPException
31
+ from starlette.middleware import cors
32
+ from starlette.requests import Request
33
+ from starlette.responses import JSONResponse, Response, StreamingResponse
34
+ from starlette.staticfiles import StaticFiles
35
+ from typing_extensions import deprecated
32
36
 
33
37
  from reflex import constants
34
38
  from reflex.admin import AdminDash
35
39
  from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
36
40
  from reflex.compiler import compiler
37
41
  from reflex.compiler import utils as compiler_utils
38
- from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme
42
+ from reflex.compiler.compiler import (
43
+ ExecutorSafeFunctions,
44
+ compile_theme,
45
+ readable_name_from_component,
46
+ )
39
47
  from reflex.components.base.app_wrap import AppWrap
40
48
  from reflex.components.base.error_boundary import ErrorBoundary
41
49
  from reflex.components.base.fragment import Fragment
42
50
  from reflex.components.base.strict_mode import StrictMode
43
51
  from reflex.components.component import (
52
+ CUSTOM_COMPONENTS,
44
53
  Component,
45
54
  ComponentStyle,
46
55
  evaluate_style_namespaces,
@@ -97,6 +106,7 @@ from reflex.utils import (
97
106
  )
98
107
  from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
99
108
  from reflex.utils.imports import ImportVar
109
+ from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send
100
110
 
101
111
  if TYPE_CHECKING:
102
112
  from reflex.vars import Var
@@ -284,6 +294,25 @@ class UnevaluatedPage:
284
294
  meta: list[dict[str, str]]
285
295
  context: dict[str, Any] | None
286
296
 
297
+ def merged_with(self, other: UnevaluatedPage) -> UnevaluatedPage:
298
+ """Merge the other page into this one.
299
+
300
+ Args:
301
+ other: The other page to merge with.
302
+
303
+ Returns:
304
+ The merged page.
305
+ """
306
+ return dataclasses.replace(
307
+ self,
308
+ title=self.title if self.title is not None else other.title,
309
+ description=self.description
310
+ if self.description is not None
311
+ else other.description,
312
+ on_load=self.on_load if self.on_load is not None else other.on_load,
313
+ context=self.context if self.context is not None else other.context,
314
+ )
315
+
287
316
 
288
317
  @dataclasses.dataclass()
289
318
  class App(MiddlewareMixin, LifespanMixin):
@@ -365,7 +394,7 @@ class App(MiddlewareMixin, LifespanMixin):
365
394
  _stateful_pages: dict[str, None] = dataclasses.field(default_factory=dict)
366
395
 
367
396
  # The backend API object.
368
- _api: FastAPI | None = None
397
+ _api: Starlette | None = None
369
398
 
370
399
  # The state class to use for the app.
371
400
  _state: type[BaseState] | None = None
@@ -400,14 +429,34 @@ class App(MiddlewareMixin, LifespanMixin):
400
429
  # Put the toast provider in the app wrap.
401
430
  toaster: Component | None = dataclasses.field(default_factory=toast.provider)
402
431
 
432
+ # Transform the ASGI app before running it.
433
+ api_transformer: (
434
+ Sequence[Callable[[ASGIApp], ASGIApp] | Starlette]
435
+ | Callable[[ASGIApp], ASGIApp]
436
+ | Starlette
437
+ | None
438
+ ) = None
439
+
440
+ # FastAPI app for compatibility with FastAPI.
441
+ _cached_fastapi_app: FastAPI | None = None
442
+
403
443
  @property
404
- def api(self) -> FastAPI | None:
444
+ @deprecated("Use `api_transformer=your_fastapi_app` instead.")
445
+ def api(self) -> FastAPI:
405
446
  """Get the backend api.
406
447
 
407
448
  Returns:
408
449
  The backend api.
409
450
  """
410
- return self._api
451
+ if self._cached_fastapi_app is None:
452
+ self._cached_fastapi_app = FastAPI()
453
+ console.deprecate(
454
+ feature_name="App.api",
455
+ reason="Set `api_transformer=your_fastapi_app` instead.",
456
+ deprecation_version="0.7.9",
457
+ removal_version="0.8.0",
458
+ )
459
+ return self._cached_fastapi_app
411
460
 
412
461
  @property
413
462
  def event_namespace(self) -> EventNamespace | None:
@@ -439,7 +488,7 @@ class App(MiddlewareMixin, LifespanMixin):
439
488
  set_breakpoints(self.style.pop("breakpoints"))
440
489
 
441
490
  # Set up the API.
442
- self._api = FastAPI(lifespan=self._run_lifespan_tasks)
491
+ self._api = Starlette(lifespan=self._run_lifespan_tasks)
443
492
  self._add_cors()
444
493
  self._add_default_endpoints()
445
494
 
@@ -505,7 +554,7 @@ class App(MiddlewareMixin, LifespanMixin):
505
554
  )
506
555
 
507
556
  # Create the socket app. Note event endpoint constant replaces the default 'socket.io' path.
508
- socket_app = ASGIApp(self.sio, socketio_path="")
557
+ socket_app = EngineIOApp(self.sio, socketio_path="")
509
558
  namespace = config.get_event_namespace()
510
559
 
511
560
  # Create the event namespace and attach the main app. Not related to any paths.
@@ -514,18 +563,16 @@ class App(MiddlewareMixin, LifespanMixin):
514
563
  # Register the event namespace with the socket.
515
564
  self.sio.register_namespace(self.event_namespace)
516
565
  # Mount the socket app with the API.
517
- if self.api:
566
+ if self._api:
518
567
 
519
568
  class HeaderMiddleware:
520
569
  def __init__(self, app: ASGIApp):
521
570
  self.app = app
522
571
 
523
- async def __call__(
524
- self, scope: MutableMapping[str, Any], receive: Any, send: Callable
525
- ):
572
+ async def __call__(self, scope: Scope, receive: Receive, send: Send):
526
573
  original_send = send
527
574
 
528
- async def modified_send(message: dict):
575
+ async def modified_send(message: Message):
529
576
  if message["type"] == "websocket.accept":
530
577
  if scope.get("subprotocols"):
531
578
  # The following *does* say "subprotocol" instead of "subprotocols", intentionally.
@@ -544,7 +591,7 @@ class App(MiddlewareMixin, LifespanMixin):
544
591
  return await self.app(scope, receive, modified_send)
545
592
 
546
593
  socket_app_with_headers = HeaderMiddleware(socket_app)
547
- self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
594
+ self._api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
548
595
 
549
596
  # Check the exception handlers
550
597
  self._validate_exception_handlers()
@@ -557,7 +604,7 @@ class App(MiddlewareMixin, LifespanMixin):
557
604
  """
558
605
  return f"<App state={self._state.__name__ if self._state else None}>"
559
606
 
560
- def __call__(self) -> FastAPI:
607
+ def __call__(self) -> ASGIApp:
561
608
  """Run the backend api instance.
562
609
 
563
610
  Raises:
@@ -566,8 +613,18 @@ class App(MiddlewareMixin, LifespanMixin):
566
613
  Returns:
567
614
  The backend api.
568
615
  """
569
- if not self.api:
570
- raise ValueError("The app has not been initialized.")
616
+ if self._cached_fastapi_app is not None:
617
+ asgi_app = self._cached_fastapi_app
618
+
619
+ if not asgi_app or not self._api:
620
+ raise ValueError("The app has not been initialized.")
621
+
622
+ asgi_app.mount("", self._api)
623
+ else:
624
+ asgi_app = self._api
625
+
626
+ if not asgi_app:
627
+ raise ValueError("The app has not been initialized.")
571
628
 
572
629
  # For py3.9 compatibility when redis is used, we MUST add any decorator pages
573
630
  # before compiling the app in a thread to avoid event loop error (REF-2172).
@@ -584,30 +641,58 @@ class App(MiddlewareMixin, LifespanMixin):
584
641
  if is_prod_mode():
585
642
  compile_future.result()
586
643
 
587
- return self.api
644
+ if self.api_transformer is not None:
645
+ api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
646
+ [self.api_transformer]
647
+ if not isinstance(self.api_transformer, Sequence)
648
+ else self.api_transformer
649
+ )
650
+
651
+ for api_transformer in api_transformers:
652
+ if isinstance(api_transformer, Starlette):
653
+ # Mount the api to the fastapi app.
654
+ api_transformer.mount("", asgi_app)
655
+ asgi_app = api_transformer
656
+ else:
657
+ # Transform the asgi app.
658
+ asgi_app = api_transformer(asgi_app)
659
+
660
+ return asgi_app
588
661
 
589
662
  def _add_default_endpoints(self):
590
663
  """Add default api endpoints (ping)."""
591
664
  # To test the server.
592
- if not self.api:
665
+ if not self._api:
593
666
  return
594
667
 
595
- self.api.get(str(constants.Endpoint.PING))(ping)
596
- self.api.get(str(constants.Endpoint.HEALTH))(health)
668
+ self._api.add_route(
669
+ str(constants.Endpoint.PING),
670
+ ping,
671
+ methods=["GET"],
672
+ )
673
+ self._api.add_route(
674
+ str(constants.Endpoint.HEALTH),
675
+ health,
676
+ methods=["GET"],
677
+ )
597
678
 
598
679
  def _add_optional_endpoints(self):
599
680
  """Add optional api endpoints (_upload)."""
600
- if not self.api:
681
+ if not self._api:
601
682
  return
602
683
  upload_is_used_marker = (
603
684
  prerequisites.get_backend_dir() / constants.Dirs.UPLOAD_IS_USED
604
685
  )
605
686
  if Upload.is_used or upload_is_used_marker.exists():
606
687
  # To upload files.
607
- self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
688
+ self._api.add_route(
689
+ str(constants.Endpoint.UPLOAD),
690
+ upload(self),
691
+ methods=["POST"],
692
+ )
608
693
 
609
694
  # To access uploaded files.
610
- self.api.mount(
695
+ self._api.mount(
611
696
  str(constants.Endpoint.UPLOAD),
612
697
  StaticFiles(directory=get_upload_dir()),
613
698
  name="uploaded_files",
@@ -616,17 +701,19 @@ class App(MiddlewareMixin, LifespanMixin):
616
701
  upload_is_used_marker.parent.mkdir(parents=True, exist_ok=True)
617
702
  upload_is_used_marker.touch()
618
703
  if codespaces.is_running_in_codespaces():
619
- self.api.get(str(constants.Endpoint.AUTH_CODESPACE))(
620
- codespaces.auth_codespace
704
+ self._api.add_route(
705
+ str(constants.Endpoint.AUTH_CODESPACE),
706
+ codespaces.auth_codespace,
707
+ methods=["GET"],
621
708
  )
622
709
  if environment.REFLEX_ADD_ALL_ROUTES_ENDPOINT.get():
623
710
  self.add_all_routes_endpoint()
624
711
 
625
712
  def _add_cors(self):
626
713
  """Add CORS middleware to the app."""
627
- if not self.api:
714
+ if not self._api:
628
715
  return
629
- self.api.add_middleware(
716
+ self._api.add_middleware(
630
717
  cors.CORSMiddleware,
631
718
  allow_credentials=True,
632
719
  allow_methods=["*"],
@@ -719,22 +806,37 @@ class App(MiddlewareMixin, LifespanMixin):
719
806
  # Check if the route given is valid
720
807
  verify_route_validity(route)
721
808
 
722
- if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
723
- # when the app is reloaded(typically for app harness tests), we should maintain
724
- # the latest render function of a route.This applies typically to decorated pages
725
- # since they are only added when app._compile is called.
726
- self._unevaluated_pages.pop(route)
809
+ unevaluated_page = UnevaluatedPage(
810
+ component=component,
811
+ route=route,
812
+ title=title,
813
+ description=description,
814
+ image=image,
815
+ on_load=on_load,
816
+ meta=meta,
817
+ context=context,
818
+ )
727
819
 
728
820
  if route in self._unevaluated_pages:
729
- route_name = (
730
- f"`{route}` or `/`"
731
- if route == constants.PageNames.INDEX_ROUTE
732
- else f"`{route}`"
733
- )
734
- raise exceptions.RouteValueError(
735
- f"Duplicate page route {route_name} already exists. Make sure you do not have two"
736
- f" pages with the same route"
737
- )
821
+ if self._unevaluated_pages[route].component is component:
822
+ unevaluated_page = unevaluated_page.merged_with(
823
+ self._unevaluated_pages[route]
824
+ )
825
+ console.warn(
826
+ f"Page {route} is being redefined with the same component."
827
+ )
828
+ else:
829
+ route_name = (
830
+ f"`{route}` or `/`"
831
+ if route == constants.PageNames.INDEX_ROUTE
832
+ else f"`{route}`"
833
+ )
834
+ existing_component = self._unevaluated_pages[route].component
835
+ raise exceptions.RouteValueError(
836
+ f"Tried to add page {readable_name_from_component(component)} with route {route_name} but "
837
+ f"page {readable_name_from_component(existing_component)} with the same route already exists. "
838
+ "Make sure you do not have two pages with the same route."
839
+ )
738
840
 
739
841
  # Setup dynamic args for the route.
740
842
  # this state assignment is only required for tests using the deprecated state kwarg for App
@@ -746,16 +848,7 @@ class App(MiddlewareMixin, LifespanMixin):
746
848
  on_load if isinstance(on_load, list) else [on_load]
747
849
  )
748
850
 
749
- self._unevaluated_pages[route] = UnevaluatedPage(
750
- component=component,
751
- route=route,
752
- title=title,
753
- description=description,
754
- image=image,
755
- on_load=on_load,
756
- meta=meta,
757
- context=context,
758
- )
851
+ self._unevaluated_pages[route] = unevaluated_page
759
852
 
760
853
  def _compile_page(self, route: str, save_page: bool = True):
761
854
  """Compile a page.
@@ -885,7 +978,7 @@ class App(MiddlewareMixin, LifespanMixin):
885
978
  return
886
979
 
887
980
  # Get the admin dash.
888
- if not self.api:
981
+ if not self._api:
889
982
  return
890
983
 
891
984
  admin_dash = self.admin_dash
@@ -906,7 +999,7 @@ class App(MiddlewareMixin, LifespanMixin):
906
999
  view = admin_dash.view_overrides.get(model, ModelView)
907
1000
  admin.add_view(view(model))
908
1001
 
909
- admin.mount_to(self.api)
1002
+ admin.mount_to(self._api)
910
1003
 
911
1004
  def _get_frontend_packages(self, imports: dict[str, set[ImportVar]]):
912
1005
  """Gets the frontend packages to be installed and filters out the unnecessary ones.
@@ -1021,9 +1114,11 @@ class App(MiddlewareMixin, LifespanMixin):
1021
1114
 
1022
1115
  This can move back into `compile_` when py39 support is dropped.
1023
1116
  """
1117
+ app_name = get_config().app_name
1024
1118
  # Add the @rx.page decorated pages to collect on_load events.
1025
- for render, kwargs in DECORATED_PAGES[get_config().app_name]:
1119
+ for render, kwargs in DECORATED_PAGES[app_name]:
1026
1120
  self.add_page(render, **kwargs)
1121
+ DECORATED_PAGES[app_name].clear()
1027
1122
 
1028
1123
  def _validate_var_dependencies(self, state: type[BaseState] | None = None) -> None:
1029
1124
  """Validate the dependencies of the vars in the app.
@@ -1191,9 +1286,8 @@ class App(MiddlewareMixin, LifespanMixin):
1191
1286
 
1192
1287
  progress.advance(task)
1193
1288
 
1194
- # Track imports and custom components found.
1289
+ # Track imports found.
1195
1290
  all_imports = {}
1196
- custom_components = set()
1197
1291
 
1198
1292
  # This has to happen before compiling stateful components as that
1199
1293
  # prevents recursive functions from reaching all components.
@@ -1204,9 +1298,6 @@ class App(MiddlewareMixin, LifespanMixin):
1204
1298
  # Add the app wrappers from this component.
1205
1299
  app_wrappers.update(component._get_all_app_wrap_components())
1206
1300
 
1207
- # Add the custom components from the page to the set.
1208
- custom_components |= component._get_all_custom_components()
1209
-
1210
1301
  if (toaster := self.toaster) is not None:
1211
1302
  from reflex.components.component import memo
1212
1303
 
@@ -1224,9 +1315,6 @@ class App(MiddlewareMixin, LifespanMixin):
1224
1315
  if component is not None:
1225
1316
  app_wrappers[key] = component
1226
1317
 
1227
- for component in app_wrappers.values():
1228
- custom_components |= component._get_all_custom_components()
1229
-
1230
1318
  if self.error_boundary:
1231
1319
  from reflex.compiler.compiler import into_component
1232
1320
 
@@ -1351,7 +1439,7 @@ class App(MiddlewareMixin, LifespanMixin):
1351
1439
  custom_components_output,
1352
1440
  custom_components_result,
1353
1441
  custom_components_imports,
1354
- ) = compiler.compile_components(custom_components)
1442
+ ) = compiler.compile_components(set(CUSTOM_COMPONENTS.values()))
1355
1443
  compile_results.append((custom_components_output, custom_components_result))
1356
1444
  all_imports.update(custom_components_imports)
1357
1445
 
@@ -1402,12 +1490,15 @@ class App(MiddlewareMixin, LifespanMixin):
1402
1490
 
1403
1491
  def add_all_routes_endpoint(self):
1404
1492
  """Add an endpoint to the app that returns all the routes."""
1405
- if not self.api:
1493
+ if not self._api:
1406
1494
  return
1407
1495
 
1408
- @self.api.get(str(constants.Endpoint.ALL_ROUTES))
1409
- async def all_routes():
1410
- return list(self._unevaluated_pages.keys())
1496
+ async def all_routes(_request: Request) -> Response:
1497
+ return JSONResponse(list(self._unevaluated_pages.keys()))
1498
+
1499
+ self._api.add_route(
1500
+ str(constants.Endpoint.ALL_ROUTES), all_routes, methods=["GET"]
1501
+ )
1411
1502
 
1412
1503
  @contextlib.asynccontextmanager
1413
1504
  async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
@@ -1662,18 +1753,24 @@ async def process(
1662
1753
  raise
1663
1754
 
1664
1755
 
1665
- async def ping() -> str:
1756
+ async def ping(_request: Request) -> Response:
1666
1757
  """Test API endpoint.
1667
1758
 
1759
+ Args:
1760
+ _request: The Starlette request object.
1761
+
1668
1762
  Returns:
1669
1763
  The response.
1670
1764
  """
1671
- return "pong"
1765
+ return JSONResponse("pong")
1672
1766
 
1673
1767
 
1674
- async def health() -> JSONResponse:
1768
+ async def health(_request: Request) -> JSONResponse:
1675
1769
  """Health check endpoint to assess the status of the database and Redis services.
1676
1770
 
1771
+ Args:
1772
+ _request: The Starlette request object.
1773
+
1677
1774
  Returns:
1678
1775
  JSONResponse: A JSON object with the health status:
1679
1776
  - "status" (bool): Overall health, True if all checks pass.
@@ -1715,12 +1812,11 @@ def upload(app: App):
1715
1812
  The upload function.
1716
1813
  """
1717
1814
 
1718
- async def upload_file(request: Request, files: list[FastAPIUploadFile]):
1815
+ async def upload_file(request: Request):
1719
1816
  """Upload a file.
1720
1817
 
1721
1818
  Args:
1722
- request: The FastAPI request object.
1723
- files: The file(s) to upload.
1819
+ request: The Starlette request object.
1724
1820
 
1725
1821
  Returns:
1726
1822
  StreamingResponse yielding newline-delimited JSON of StateUpdate
@@ -1733,6 +1829,12 @@ def upload(app: App):
1733
1829
  """
1734
1830
  from reflex.utils.exceptions import UploadTypeError, UploadValueError
1735
1831
 
1832
+ # Get the files from the request.
1833
+ files = await request.form()
1834
+ files = files.getlist("files")
1835
+ if not files:
1836
+ raise UploadValueError("No files were uploaded.")
1837
+
1736
1838
  token = request.headers.get("reflex-client-token")
1737
1839
  handler = request.headers.get("reflex-event-handler")
1738
1840
 
@@ -1785,6 +1887,10 @@ def upload(app: App):
1785
1887
  # event is handled.
1786
1888
  file_copies = []
1787
1889
  for file in files:
1890
+ if not isinstance(file, StarletteUploadFile):
1891
+ raise UploadValueError(
1892
+ "Uploaded file is not an UploadFile." + str(file)
1893
+ )
1788
1894
  content_copy = io.BytesIO()
1789
1895
  content_copy.write(await file.read())
1790
1896
  content_copy.seek(0)
@@ -9,7 +9,7 @@ import functools
9
9
  import inspect
10
10
  from collections.abc import Callable, Coroutine
11
11
 
12
- from fastapi import FastAPI
12
+ from starlette.applications import Starlette
13
13
 
14
14
  from reflex.utils import console
15
15
  from reflex.utils.exceptions import InvalidLifespanTaskTypeError
@@ -27,7 +27,7 @@ class LifespanMixin(AppMixin):
27
27
  )
28
28
 
29
29
  @contextlib.asynccontextmanager
30
- async def _run_lifespan_tasks(self, app: FastAPI):
30
+ async def _run_lifespan_tasks(self, app: Starlette):
31
31
  running_tasks = []
32
32
  try:
33
33
  async with contextlib.AsyncExitStack() as stack: