reflex 0.7.8a1__py3-none-any.whl → 0.7.9__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 (44) 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 +191 -87
  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/page.py +8 -0
  30. reflex/reflex.py +277 -265
  31. reflex/testing.py +30 -24
  32. reflex/utils/codespaces.py +6 -2
  33. reflex/utils/console.py +4 -3
  34. reflex/utils/exec.py +60 -24
  35. reflex/utils/format.py +17 -2
  36. reflex/utils/prerequisites.py +43 -30
  37. reflex/utils/processes.py +6 -6
  38. reflex/utils/types.py +11 -6
  39. reflex/vars/base.py +19 -1
  40. {reflex-0.7.8a1.dist-info → reflex-0.7.9.dist-info}/METADATA +6 -9
  41. {reflex-0.7.8a1.dist-info → reflex-0.7.9.dist-info}/RECORD +44 -44
  42. {reflex-0.7.8a1.dist-info → reflex-0.7.9.dist-info}/WHEEL +0 -0
  43. {reflex-0.7.8a1.dist-info → reflex-0.7.9.dist-info}/entry_points.txt +0 -0
  44. {reflex-0.7.8a1.dist-info → reflex-0.7.9.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 current = ref?.current;
247
+ if (current === undefined || current?.focus === undefined) {
248
+ console.error(
249
+ `No element found for ref ${event.payload.ref} in _set_focus`,
250
+ );
251
+ } else {
252
+ current.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,8 +488,8 @@ 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)
443
- self._add_cors()
491
+ self._api = Starlette(lifespan=self._run_lifespan_tasks)
492
+ App._add_cors(self._api)
444
493
  self._add_default_endpoints()
445
494
 
446
495
  for clz in App.__mro__:
@@ -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,9 +613,6 @@ 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.")
571
-
572
616
  # For py3.9 compatibility when redis is used, we MUST add any decorator pages
573
617
  # before compiling the app in a thread to avoid event loop error (REF-2172).
574
618
  self._apply_decorated_pages()
@@ -580,34 +624,71 @@ class App(MiddlewareMixin, LifespanMixin):
580
624
  # Force background compile errors to print eagerly
581
625
  lambda f: f.result()
582
626
  )
583
- # Wait for the compile to finish in prod mode to ensure all optional endpoints are mounted.
584
- if is_prod_mode():
585
- compile_future.result()
627
+ # Wait for the compile to finish to ensure all optional endpoints are mounted.
628
+ compile_future.result()
586
629
 
587
- return self.api
630
+ if not self._api:
631
+ raise ValueError("The app has not been initialized.")
632
+ if self._cached_fastapi_app is not None:
633
+ asgi_app = self._cached_fastapi_app
634
+ asgi_app.mount("", self._api)
635
+ App._add_cors(asgi_app)
636
+ else:
637
+ asgi_app = self._api
638
+
639
+ if self.api_transformer is not None:
640
+ api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
641
+ [self.api_transformer]
642
+ if not isinstance(self.api_transformer, Sequence)
643
+ else self.api_transformer
644
+ )
645
+
646
+ for api_transformer in api_transformers:
647
+ if isinstance(api_transformer, Starlette):
648
+ # Mount the api to the fastapi app.
649
+ App._add_cors(api_transformer)
650
+ api_transformer.mount("", asgi_app)
651
+ asgi_app = api_transformer
652
+ else:
653
+ # Transform the asgi app.
654
+ asgi_app = api_transformer(asgi_app)
655
+
656
+ return asgi_app
588
657
 
589
658
  def _add_default_endpoints(self):
590
659
  """Add default api endpoints (ping)."""
591
660
  # To test the server.
592
- if not self.api:
661
+ if not self._api:
593
662
  return
594
663
 
595
- self.api.get(str(constants.Endpoint.PING))(ping)
596
- self.api.get(str(constants.Endpoint.HEALTH))(health)
664
+ self._api.add_route(
665
+ str(constants.Endpoint.PING),
666
+ ping,
667
+ methods=["GET"],
668
+ )
669
+ self._api.add_route(
670
+ str(constants.Endpoint.HEALTH),
671
+ health,
672
+ methods=["GET"],
673
+ )
597
674
 
598
675
  def _add_optional_endpoints(self):
599
676
  """Add optional api endpoints (_upload)."""
600
- if not self.api:
677
+ if not self._api:
601
678
  return
602
679
  upload_is_used_marker = (
603
680
  prerequisites.get_backend_dir() / constants.Dirs.UPLOAD_IS_USED
604
681
  )
605
682
  if Upload.is_used or upload_is_used_marker.exists():
606
683
  # To upload files.
607
- self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
684
+ self._api.add_route(
685
+ str(constants.Endpoint.UPLOAD),
686
+ upload(self),
687
+ methods=["POST"],
688
+ )
608
689
 
609
690
  # To access uploaded files.
610
- self.api.mount(
691
+ self._api.mount(
611
692
  str(constants.Endpoint.UPLOAD),
612
693
  StaticFiles(directory=get_upload_dir()),
613
694
  name="uploaded_files",
@@ -616,17 +697,22 @@ class App(MiddlewareMixin, LifespanMixin):
616
697
  upload_is_used_marker.parent.mkdir(parents=True, exist_ok=True)
617
698
  upload_is_used_marker.touch()
618
699
  if codespaces.is_running_in_codespaces():
619
- self.api.get(str(constants.Endpoint.AUTH_CODESPACE))(
620
- codespaces.auth_codespace
700
+ self._api.add_route(
701
+ str(constants.Endpoint.AUTH_CODESPACE),
702
+ codespaces.auth_codespace,
703
+ methods=["GET"],
621
704
  )
622
705
  if environment.REFLEX_ADD_ALL_ROUTES_ENDPOINT.get():
623
706
  self.add_all_routes_endpoint()
624
707
 
625
- def _add_cors(self):
626
- """Add CORS middleware to the app."""
627
- if not self.api:
628
- return
629
- self.api.add_middleware(
708
+ @staticmethod
709
+ def _add_cors(api: Starlette):
710
+ """Add CORS middleware to the app.
711
+
712
+ Args:
713
+ api: The Starlette app to add CORS middleware to.
714
+ """
715
+ api.add_middleware(
630
716
  cors.CORSMiddleware,
631
717
  allow_credentials=True,
632
718
  allow_methods=["*"],
@@ -719,22 +805,37 @@ class App(MiddlewareMixin, LifespanMixin):
719
805
  # Check if the route given is valid
720
806
  verify_route_validity(route)
721
807
 
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)
808
+ unevaluated_page = UnevaluatedPage(
809
+ component=component,
810
+ route=route,
811
+ title=title,
812
+ description=description,
813
+ image=image,
814
+ on_load=on_load,
815
+ meta=meta,
816
+ context=context,
817
+ )
727
818
 
728
819
  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
- )
820
+ if self._unevaluated_pages[route].component is component:
821
+ unevaluated_page = unevaluated_page.merged_with(
822
+ self._unevaluated_pages[route]
823
+ )
824
+ console.warn(
825
+ f"Page {route} is being redefined with the same component."
826
+ )
827
+ else:
828
+ route_name = (
829
+ f"`{route}` or `/`"
830
+ if route == constants.PageNames.INDEX_ROUTE
831
+ else f"`{route}`"
832
+ )
833
+ existing_component = self._unevaluated_pages[route].component
834
+ raise exceptions.RouteValueError(
835
+ f"Tried to add page {readable_name_from_component(component)} with route {route_name} but "
836
+ f"page {readable_name_from_component(existing_component)} with the same route already exists. "
837
+ "Make sure you do not have two pages with the same route."
838
+ )
738
839
 
739
840
  # Setup dynamic args for the route.
740
841
  # this state assignment is only required for tests using the deprecated state kwarg for App
@@ -746,16 +847,7 @@ class App(MiddlewareMixin, LifespanMixin):
746
847
  on_load if isinstance(on_load, list) else [on_load]
747
848
  )
748
849
 
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
- )
850
+ self._unevaluated_pages[route] = unevaluated_page
759
851
 
760
852
  def _compile_page(self, route: str, save_page: bool = True):
761
853
  """Compile a page.
@@ -885,7 +977,7 @@ class App(MiddlewareMixin, LifespanMixin):
885
977
  return
886
978
 
887
979
  # Get the admin dash.
888
- if not self.api:
980
+ if not self._api:
889
981
  return
890
982
 
891
983
  admin_dash = self.admin_dash
@@ -906,7 +998,7 @@ class App(MiddlewareMixin, LifespanMixin):
906
998
  view = admin_dash.view_overrides.get(model, ModelView)
907
999
  admin.add_view(view(model))
908
1000
 
909
- admin.mount_to(self.api)
1001
+ admin.mount_to(self._api)
910
1002
 
911
1003
  def _get_frontend_packages(self, imports: dict[str, set[ImportVar]]):
912
1004
  """Gets the frontend packages to be installed and filters out the unnecessary ones.
@@ -1021,8 +1113,9 @@ class App(MiddlewareMixin, LifespanMixin):
1021
1113
 
1022
1114
  This can move back into `compile_` when py39 support is dropped.
1023
1115
  """
1116
+ app_name = get_config().app_name
1024
1117
  # Add the @rx.page decorated pages to collect on_load events.
1025
- for render, kwargs in DECORATED_PAGES[get_config().app_name]:
1118
+ for render, kwargs in DECORATED_PAGES[app_name]:
1026
1119
  self.add_page(render, **kwargs)
1027
1120
 
1028
1121
  def _validate_var_dependencies(self, state: type[BaseState] | None = None) -> None:
@@ -1191,9 +1284,8 @@ class App(MiddlewareMixin, LifespanMixin):
1191
1284
 
1192
1285
  progress.advance(task)
1193
1286
 
1194
- # Track imports and custom components found.
1287
+ # Track imports found.
1195
1288
  all_imports = {}
1196
- custom_components = set()
1197
1289
 
1198
1290
  # This has to happen before compiling stateful components as that
1199
1291
  # prevents recursive functions from reaching all components.
@@ -1204,9 +1296,6 @@ class App(MiddlewareMixin, LifespanMixin):
1204
1296
  # Add the app wrappers from this component.
1205
1297
  app_wrappers.update(component._get_all_app_wrap_components())
1206
1298
 
1207
- # Add the custom components from the page to the set.
1208
- custom_components |= component._get_all_custom_components()
1209
-
1210
1299
  if (toaster := self.toaster) is not None:
1211
1300
  from reflex.components.component import memo
1212
1301
 
@@ -1224,9 +1313,6 @@ class App(MiddlewareMixin, LifespanMixin):
1224
1313
  if component is not None:
1225
1314
  app_wrappers[key] = component
1226
1315
 
1227
- for component in app_wrappers.values():
1228
- custom_components |= component._get_all_custom_components()
1229
-
1230
1316
  if self.error_boundary:
1231
1317
  from reflex.compiler.compiler import into_component
1232
1318
 
@@ -1351,7 +1437,7 @@ class App(MiddlewareMixin, LifespanMixin):
1351
1437
  custom_components_output,
1352
1438
  custom_components_result,
1353
1439
  custom_components_imports,
1354
- ) = compiler.compile_components(custom_components)
1440
+ ) = compiler.compile_components(set(CUSTOM_COMPONENTS.values()))
1355
1441
  compile_results.append((custom_components_output, custom_components_result))
1356
1442
  all_imports.update(custom_components_imports)
1357
1443
 
@@ -1402,12 +1488,15 @@ class App(MiddlewareMixin, LifespanMixin):
1402
1488
 
1403
1489
  def add_all_routes_endpoint(self):
1404
1490
  """Add an endpoint to the app that returns all the routes."""
1405
- if not self.api:
1491
+ if not self._api:
1406
1492
  return
1407
1493
 
1408
- @self.api.get(str(constants.Endpoint.ALL_ROUTES))
1409
- async def all_routes():
1410
- return list(self._unevaluated_pages.keys())
1494
+ async def all_routes(_request: Request) -> Response:
1495
+ return JSONResponse(list(self._unevaluated_pages.keys()))
1496
+
1497
+ self._api.add_route(
1498
+ str(constants.Endpoint.ALL_ROUTES), all_routes, methods=["GET"]
1499
+ )
1411
1500
 
1412
1501
  @contextlib.asynccontextmanager
1413
1502
  async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
@@ -1662,18 +1751,24 @@ async def process(
1662
1751
  raise
1663
1752
 
1664
1753
 
1665
- async def ping() -> str:
1754
+ async def ping(_request: Request) -> Response:
1666
1755
  """Test API endpoint.
1667
1756
 
1757
+ Args:
1758
+ _request: The Starlette request object.
1759
+
1668
1760
  Returns:
1669
1761
  The response.
1670
1762
  """
1671
- return "pong"
1763
+ return JSONResponse("pong")
1672
1764
 
1673
1765
 
1674
- async def health() -> JSONResponse:
1766
+ async def health(_request: Request) -> JSONResponse:
1675
1767
  """Health check endpoint to assess the status of the database and Redis services.
1676
1768
 
1769
+ Args:
1770
+ _request: The Starlette request object.
1771
+
1677
1772
  Returns:
1678
1773
  JSONResponse: A JSON object with the health status:
1679
1774
  - "status" (bool): Overall health, True if all checks pass.
@@ -1715,12 +1810,11 @@ def upload(app: App):
1715
1810
  The upload function.
1716
1811
  """
1717
1812
 
1718
- async def upload_file(request: Request, files: list[FastAPIUploadFile]):
1813
+ async def upload_file(request: Request):
1719
1814
  """Upload a file.
1720
1815
 
1721
1816
  Args:
1722
- request: The FastAPI request object.
1723
- files: The file(s) to upload.
1817
+ request: The Starlette request object.
1724
1818
 
1725
1819
  Returns:
1726
1820
  StreamingResponse yielding newline-delimited JSON of StateUpdate
@@ -1733,6 +1827,12 @@ def upload(app: App):
1733
1827
  """
1734
1828
  from reflex.utils.exceptions import UploadTypeError, UploadValueError
1735
1829
 
1830
+ # Get the files from the request.
1831
+ files = await request.form()
1832
+ files = files.getlist("files")
1833
+ if not files:
1834
+ raise UploadValueError("No files were uploaded.")
1835
+
1736
1836
  token = request.headers.get("reflex-client-token")
1737
1837
  handler = request.headers.get("reflex-event-handler")
1738
1838
 
@@ -1785,6 +1885,10 @@ def upload(app: App):
1785
1885
  # event is handled.
1786
1886
  file_copies = []
1787
1887
  for file in files:
1888
+ if not isinstance(file, StarletteUploadFile):
1889
+ raise UploadValueError(
1890
+ "Uploaded file is not an UploadFile." + str(file)
1891
+ )
1788
1892
  content_copy = io.BytesIO()
1789
1893
  content_copy.write(await file.read())
1790
1894
  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: