reflex 0.8.2a1__py3-none-any.whl → 0.8.3__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 (40) hide show
  1. reflex/.templates/web/utils/state.js +7 -2
  2. reflex/__init__.py +1 -0
  3. reflex/__init__.pyi +2 -0
  4. reflex/app.py +8 -11
  5. reflex/compiler/compiler.py +10 -38
  6. reflex/components/base/error_boundary.py +6 -5
  7. reflex/components/core/__init__.py +1 -0
  8. reflex/components/core/__init__.pyi +3 -0
  9. reflex/components/core/window_events.py +104 -0
  10. reflex/components/core/window_events.pyi +84 -0
  11. reflex/components/el/__init__.pyi +4 -0
  12. reflex/components/el/elements/__init__.py +1 -0
  13. reflex/components/el/elements/__init__.pyi +5 -0
  14. reflex/components/el/elements/forms.py +4 -2
  15. reflex/components/el/elements/typography.py +7 -0
  16. reflex/components/el/elements/typography.pyi +246 -0
  17. reflex/components/lucide/icon.py +303 -292
  18. reflex/components/lucide/icon.pyi +303 -292
  19. reflex/components/radix/themes/components/separator.py +4 -4
  20. reflex/components/radix/themes/components/separator.pyi +3 -3
  21. reflex/components/recharts/recharts.py +2 -2
  22. reflex/components/sonner/toast.py +1 -1
  23. reflex/config.py +3 -3
  24. reflex/constants/installer.py +2 -1
  25. reflex/event.py +71 -10
  26. reflex/model.py +55 -0
  27. reflex/plugins/base.py +2 -2
  28. reflex/reflex.py +33 -0
  29. reflex/route.py +4 -4
  30. reflex/state.py +9 -4
  31. reflex/utils/console.py +3 -3
  32. reflex/utils/exec.py +22 -6
  33. reflex/utils/format.py +1 -1
  34. reflex/utils/processes.py +28 -38
  35. reflex/utils/types.py +1 -1
  36. {reflex-0.8.2a1.dist-info → reflex-0.8.3.dist-info}/METADATA +1 -1
  37. {reflex-0.8.2a1.dist-info → reflex-0.8.3.dist-info}/RECORD +40 -38
  38. {reflex-0.8.2a1.dist-info → reflex-0.8.3.dist-info}/WHEEL +0 -0
  39. {reflex-0.8.2a1.dist-info → reflex-0.8.3.dist-info}/entry_points.txt +0 -0
  40. {reflex-0.8.2a1.dist-info → reflex-0.8.3.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ from reflex.components.core.breakpoints import Responsive
6
6
  from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent
7
7
  from reflex.vars.base import LiteralVar, Var
8
8
 
9
- LiteralSeperatorSize = Literal["1", "2", "3", "4"]
9
+ LiteralSeparatorSize = Literal["1", "2", "3", "4"]
10
10
 
11
11
 
12
12
  class Separator(RadixThemesComponent):
@@ -14,10 +14,10 @@ class Separator(RadixThemesComponent):
14
14
 
15
15
  tag = "Separator"
16
16
 
17
- # The size of the select: "1" | "2" | "3" | "4"
18
- size: Var[Responsive[LiteralSeperatorSize]] = LiteralVar.create("4")
17
+ # The size of the separator: "1" | "2" | "3" | "4"
18
+ size: Var[Responsive[LiteralSeparatorSize]] = LiteralVar.create("4")
19
19
 
20
- # The color of the select
20
+ # The color of the separator
21
21
  color_scheme: Var[LiteralAccentColor]
22
22
 
23
23
  # The orientation of the separator.
@@ -11,7 +11,7 @@ from reflex.components.radix.themes.base import RadixThemesComponent
11
11
  from reflex.event import EventType, PointerEventInfo
12
12
  from reflex.vars.base import Var
13
13
 
14
- LiteralSeperatorSize = Literal["1", "2", "3", "4"]
14
+ LiteralSeparatorSize = Literal["1", "2", "3", "4"]
15
15
 
16
16
  class Separator(RadixThemesComponent):
17
17
  @classmethod
@@ -127,8 +127,8 @@ class Separator(RadixThemesComponent):
127
127
 
128
128
  Args:
129
129
  *children: Child components.
130
- size: The size of the select: "1" | "2" | "3" | "4"
131
- color_scheme: The color of the select
130
+ size: The size of the separator: "1" | "2" | "3" | "4"
131
+ color_scheme: The color of the separator
132
132
  orientation: The orientation of the separator.
133
133
  decorative: When true, signifies that it is purely visual, carries no semantic meaning, and ensures it is not present in the accessibility tree.
134
134
  style: The style of the component.
@@ -8,7 +8,7 @@ from reflex.components.component import Component, MemoizationLeaf, NoSSRCompone
8
8
  class Recharts(Component):
9
9
  """A component that wraps a recharts lib."""
10
10
 
11
- library = "recharts@3.0.2"
11
+ library = "recharts@3.1.0"
12
12
 
13
13
  def _get_style(self) -> dict:
14
14
  return {"wrapperStyle": self.style}
@@ -17,7 +17,7 @@ class Recharts(Component):
17
17
  class RechartsCharts(NoSSRComponent, MemoizationLeaf):
18
18
  """A component that wraps a recharts lib."""
19
19
 
20
- library = "recharts@3.0.2"
20
+ library = "recharts@3.1.0"
21
21
 
22
22
 
23
23
  LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"]
@@ -171,7 +171,7 @@ class ToastProps(NoExtrasAllowedProps):
171
171
  class Toaster(Component):
172
172
  """A Toaster Component for displaying toast notifications."""
173
173
 
174
- library: str | None = "sonner@2.0.5"
174
+ library: str | None = "sonner@2.0.6"
175
175
 
176
176
  tag = "Toaster"
177
177
 
reflex/config.py CHANGED
@@ -294,14 +294,14 @@ class Config(BaseConfig):
294
294
  if env_loglevel or self.loglevel != LogLevel.DEFAULT:
295
295
  console.set_log_level(env_loglevel or self.loglevel)
296
296
 
297
- # Add builtin plugins if not disabled.
298
- self._add_builtin_plugins()
299
-
300
297
  # Update the config from environment variables.
301
298
  env_kwargs = self.update_from_env()
302
299
  for key, env_value in env_kwargs.items():
303
300
  setattr(self, key, env_value)
304
301
 
302
+ # Add builtin plugins if not disabled.
303
+ self._add_builtin_plugins()
304
+
305
305
  # Update default URLs if ports were set
306
306
  kwargs.update(env_kwargs)
307
307
  self._non_default_attributes = set(kwargs.keys())
@@ -143,10 +143,11 @@ class PackageJson(SimpleNamespace):
143
143
  "postcss-import": "16.1.1",
144
144
  "@react-router/dev": _react_router_version,
145
145
  "@react-router/fs-routes": _react_router_version,
146
- "rolldown-vite": "7.0.8",
146
+ "rolldown-vite": "7.0.9",
147
147
  }
148
148
  OVERRIDES = {
149
149
  # This should always match the `react` version in DEPENDENCIES for recharts compatibility.
150
150
  "react-is": _react_version,
151
151
  "cookie": "1.0.2",
152
+ "rollup": "4.44.2",
152
153
  }
reflex/event.py CHANGED
@@ -109,7 +109,7 @@ class EventActionsMixin:
109
109
  """
110
110
  return dataclasses.replace(
111
111
  self,
112
- event_actions={"stopPropagation": True, **self.event_actions},
112
+ event_actions={**self.event_actions, "stopPropagation": True},
113
113
  )
114
114
 
115
115
  @property
@@ -121,7 +121,7 @@ class EventActionsMixin:
121
121
  """
122
122
  return dataclasses.replace(
123
123
  self,
124
- event_actions={"preventDefault": True, **self.event_actions},
124
+ event_actions={**self.event_actions, "preventDefault": True},
125
125
  )
126
126
 
127
127
  def throttle(self, limit_ms: int) -> Self:
@@ -135,7 +135,7 @@ class EventActionsMixin:
135
135
  """
136
136
  return dataclasses.replace(
137
137
  self,
138
- event_actions={"throttle": limit_ms, **self.event_actions},
138
+ event_actions={**self.event_actions, "throttle": limit_ms},
139
139
  )
140
140
 
141
141
  def debounce(self, delay_ms: int) -> Self:
@@ -149,7 +149,7 @@ class EventActionsMixin:
149
149
  """
150
150
  return dataclasses.replace(
151
151
  self,
152
- event_actions={"debounce": delay_ms, **self.event_actions},
152
+ event_actions={**self.event_actions, "debounce": delay_ms},
153
153
  )
154
154
 
155
155
  @property
@@ -161,7 +161,7 @@ class EventActionsMixin:
161
161
  """
162
162
  return dataclasses.replace(
163
163
  self,
164
- event_actions={"temporal": True, **self.event_actions},
164
+ event_actions={**self.event_actions, "temporal": True},
165
165
  )
166
166
 
167
167
 
@@ -577,7 +577,7 @@ class JavascriptInputEvent:
577
577
  init=True,
578
578
  frozen=True,
579
579
  )
580
- class JavasciptKeyboardEvent:
580
+ class JavascriptKeyboardEvent:
581
581
  """Interface for a Javascript KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent."""
582
582
 
583
583
  key: str = ""
@@ -645,7 +645,7 @@ class KeyInputInfo(TypedDict):
645
645
 
646
646
 
647
647
  def key_event(
648
- e: ObjectVar[JavasciptKeyboardEvent],
648
+ e: ObjectVar[JavascriptKeyboardEvent],
649
649
  ) -> tuple[Var[str], Var[KeyInputInfo]]:
650
650
  """Get the key from a keyboard event.
651
651
 
@@ -1269,7 +1269,7 @@ def call_script(
1269
1269
  Returns:
1270
1270
  EventSpec: An event that will execute the client side javascript.
1271
1271
  """
1272
- callback_kwargs = {}
1272
+ callback_kwargs = {"callback": None}
1273
1273
  if callback is not None:
1274
1274
  callback_kwargs = {
1275
1275
  "callback": str(
@@ -2211,7 +2211,15 @@ class EventNamespace:
2211
2211
 
2212
2212
  @overload
2213
2213
  def __new__(
2214
- cls, func: None = None, *, background: bool | None = None
2214
+ cls,
2215
+ func: None = None,
2216
+ *,
2217
+ background: bool | None = None,
2218
+ stop_propagation: bool | None = None,
2219
+ prevent_default: bool | None = None,
2220
+ throttle: int | None = None,
2221
+ debounce: int | None = None,
2222
+ temporal: bool | None = None,
2215
2223
  ) -> Callable[
2216
2224
  [Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]] # pyright: ignore [reportInvalidTypeVarUse]
2217
2225
  ]: ...
@@ -2222,6 +2230,11 @@ class EventNamespace:
2222
2230
  func: Callable[[BASE_STATE, Unpack[P]], Any],
2223
2231
  *,
2224
2232
  background: bool | None = None,
2233
+ stop_propagation: bool | None = None,
2234
+ prevent_default: bool | None = None,
2235
+ throttle: int | None = None,
2236
+ debounce: int | None = None,
2237
+ temporal: bool | None = None,
2225
2238
  ) -> EventCallback[Unpack[P]]: ...
2226
2239
 
2227
2240
  def __new__(
@@ -2229,6 +2242,11 @@ class EventNamespace:
2229
2242
  func: Callable[[BASE_STATE, Unpack[P]], Any] | None = None,
2230
2243
  *,
2231
2244
  background: bool | None = None,
2245
+ stop_propagation: bool | None = None,
2246
+ prevent_default: bool | None = None,
2247
+ throttle: int | None = None,
2248
+ debounce: int | None = None,
2249
+ temporal: bool | None = None,
2232
2250
  ) -> (
2233
2251
  EventCallback[Unpack[P]]
2234
2252
  | Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]]
@@ -2238,6 +2256,11 @@ class EventNamespace:
2238
2256
  Args:
2239
2257
  func: The function to wrap.
2240
2258
  background: Whether the event should be run in the background. Defaults to False.
2259
+ stop_propagation: Whether to stop the event from bubbling up the DOM tree.
2260
+ prevent_default: Whether to prevent the default behavior of the event.
2261
+ throttle: Throttle the event handler to limit calls (in milliseconds).
2262
+ debounce: Debounce the event handler to delay calls (in milliseconds).
2263
+ temporal: Whether the event should be dropped when the backend is down.
2241
2264
 
2242
2265
  Raises:
2243
2266
  TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402
@@ -2246,6 +2269,30 @@ class EventNamespace:
2246
2269
  The wrapped function.
2247
2270
  """
2248
2271
 
2272
+ def _build_event_actions():
2273
+ """Build event_actions dict from decorator parameters.
2274
+
2275
+ Returns:
2276
+ Dict of event actions to apply, or empty dict if none specified.
2277
+ """
2278
+ if not any(
2279
+ [stop_propagation, prevent_default, throttle, debounce, temporal]
2280
+ ):
2281
+ return {}
2282
+
2283
+ event_actions = {}
2284
+ if stop_propagation is not None:
2285
+ event_actions["stopPropagation"] = stop_propagation
2286
+ if prevent_default is not None:
2287
+ event_actions["preventDefault"] = prevent_default
2288
+ if throttle is not None:
2289
+ event_actions["throttle"] = throttle
2290
+ if debounce is not None:
2291
+ event_actions["debounce"] = debounce
2292
+ if temporal is not None:
2293
+ event_actions["temporal"] = temporal
2294
+ return event_actions
2295
+
2249
2296
  def wrapper(
2250
2297
  func: Callable[[BASE_STATE, Unpack[P]], T],
2251
2298
  ) -> EventCallback[Unpack[P]]:
@@ -2281,8 +2328,22 @@ class EventNamespace:
2281
2328
  object.__setattr__(func, "__name__", name)
2282
2329
  object.__setattr__(func, "__qualname__", name)
2283
2330
  state_cls._add_event_handler(name, func)
2284
- return getattr(state_cls, name)
2331
+ event_callback = getattr(state_cls, name)
2332
+
2333
+ # Apply decorator event actions
2334
+ event_actions = _build_event_actions()
2335
+ if event_actions:
2336
+ # Create new EventCallback with updated event_actions
2337
+ event_callback = dataclasses.replace(
2338
+ event_callback, event_actions=event_actions
2339
+ )
2340
+
2341
+ return event_callback
2285
2342
 
2343
+ # Store decorator event actions on the function for later processing
2344
+ event_actions = _build_event_actions()
2345
+ if event_actions:
2346
+ func._rx_event_actions = event_actions # pyright: ignore [reportFunctionMemberAccess]
2286
2347
  return func # pyright: ignore [reportReturnType]
2287
2348
 
2288
2349
  if func is not None:
reflex/model.py CHANGED
@@ -19,6 +19,7 @@ import sqlalchemy.exc
19
19
  import sqlalchemy.ext.asyncio
20
20
  import sqlalchemy.orm
21
21
  from alembic.runtime.migration import MigrationContext
22
+ from alembic.script.base import Script
22
23
 
23
24
  from reflex.base import Base
24
25
  from reflex.config import get_config
@@ -34,6 +35,41 @@ _AsyncSessionLocal: dict[str | None, sqlalchemy.ext.asyncio.async_sessionmaker]
34
35
  from sqlmodel.ext.asyncio.session import AsyncSession # noqa: E402
35
36
 
36
37
 
38
+ def format_revision(
39
+ rev: Script,
40
+ current_rev: str | None,
41
+ current_reached_ref: list[bool],
42
+ ) -> str:
43
+ """Format a single revision for display.
44
+
45
+ Args:
46
+ rev: The alembic script object
47
+ current_rev: The currently applied revision ID
48
+ current_reached_ref: Mutable reference to track if we've reached current revision
49
+
50
+ Returns:
51
+ Formatted string for display
52
+ """
53
+ current = rev.revision
54
+ message = rev.doc
55
+
56
+ # Determine if this migration is applied
57
+ if current_rev is None:
58
+ is_applied = False
59
+ elif current == current_rev:
60
+ is_applied = True
61
+ current_reached_ref[0] = True
62
+ else:
63
+ is_applied = not current_reached_ref[0]
64
+
65
+ # Show checkmark or X with colors
66
+ status_icon = "[green]✓[/green]" if is_applied else "[red]✗[/red]"
67
+ head_marker = " (head)" if rev.is_head else ""
68
+
69
+ # Format output with message
70
+ return f" [{status_icon}] {current}{head_marker}, {message}"
71
+
72
+
37
73
  def _safe_db_url_for_logging(url: str) -> str:
38
74
  """Remove username and password from the database URL for logging.
39
75
 
@@ -361,6 +397,25 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue
361
397
  directory=str(environment.ALEMBIC_CONFIG.get().parent / "alembic"),
362
398
  )
363
399
 
400
+ @classmethod
401
+ def get_migration_history(cls):
402
+ """Get migration history with current database state.
403
+
404
+ Returns:
405
+ tuple: (current_revision, revisions_list) where revisions_list is in chronological order
406
+ """
407
+ # Get current revision from database
408
+ with cls.get_db_engine().connect() as connection:
409
+ context = MigrationContext.configure(connection)
410
+ current_rev = context.get_current_revision()
411
+
412
+ # Get all revisions from base to head
413
+ _, script_dir = cls._alembic_config()
414
+ revisions = list(script_dir.walk_revisions())
415
+ revisions.reverse() # Reverse to get chronological order (base first)
416
+
417
+ return current_rev, revisions
418
+
364
419
  @classmethod
365
420
  def alembic_autogenerate(
366
421
  cls,
reflex/plugins/base.py CHANGED
@@ -17,7 +17,7 @@ class CommonContext(TypedDict):
17
17
  P = ParamSpec("P")
18
18
 
19
19
 
20
- class AddTaskProtcol(Protocol):
20
+ class AddTaskProtocol(Protocol):
21
21
  """Protocol for adding a task to the pre-compile context."""
22
22
 
23
23
  def __call__(
@@ -39,7 +39,7 @@ class AddTaskProtcol(Protocol):
39
39
  class PreCompileContext(CommonContext):
40
40
  """Context for pre-compile hooks."""
41
41
 
42
- add_save_task: AddTaskProtcol
42
+ add_save_task: AddTaskProtocol
43
43
  add_modify_task: Callable[[str, Callable[[str], str]], None]
44
44
  unevaluated_pages: Sequence["UnevaluatedPage"]
45
45
 
reflex/reflex.py CHANGED
@@ -540,6 +540,39 @@ def migrate():
540
540
  prerequisites.check_schema_up_to_date()
541
541
 
542
542
 
543
+ @db_cli.command()
544
+ def status():
545
+ """Check the status of the database schema."""
546
+ from reflex.model import Model, format_revision
547
+ from reflex.utils import prerequisites
548
+
549
+ prerequisites.get_app()
550
+ if not prerequisites.check_db_initialized():
551
+ console.info(
552
+ "Database is not initialized. Run [bold]reflex db init[/bold] to initialize."
553
+ )
554
+ return
555
+
556
+ # Run alembic check command and display output
557
+ import reflex.config
558
+
559
+ config = reflex.config.get_config()
560
+ console.print(f"[bold]\\[{config.db_url}][/bold]")
561
+
562
+ # Get migration history using Model method
563
+ current_rev, revisions = Model.get_migration_history()
564
+ if current_rev is None and not revisions:
565
+ return
566
+
567
+ current_reached_ref = [current_rev is None]
568
+
569
+ # Show migration history in chronological order
570
+ console.print("<base>")
571
+ for rev in revisions:
572
+ # Format and print the revision
573
+ console.print(format_revision(rev, current_rev, current_reached_ref))
574
+
575
+
543
576
  @db_cli.command()
544
577
  @click.option(
545
578
  "--message",
reflex/route.py CHANGED
@@ -131,7 +131,7 @@ def replace_brackets_with_keywords(input_string: str) -> str:
131
131
  )
132
132
 
133
133
 
134
- def route_specifity(keyworded_route: str) -> tuple[int, int, int]:
134
+ def route_specificity(keyworded_route: str) -> tuple[int, int, int]:
135
135
  """Get the specificity of a route with keywords.
136
136
 
137
137
  The smaller the number, the more specific the route is.
@@ -193,13 +193,13 @@ def get_router(routes: list[str]) -> Callable[[str], str | None]:
193
193
  keyworded_routes = {
194
194
  replace_brackets_with_keywords(route): route for route in routes
195
195
  }
196
- sorted_routes_by_specifity = sorted(
196
+ sorted_routes_by_specificity = sorted(
197
197
  keyworded_routes.items(),
198
- key=lambda item: route_specifity(item[0]),
198
+ key=lambda item: route_specificity(item[0]),
199
199
  )
200
200
  regexed_routes = [
201
201
  (get_route_regex(keyworded_route), original_route)
202
- for keyworded_route, original_route in sorted_routes_by_specifity
202
+ for keyworded_route, original_route in sorted_routes_by_specificity
203
203
  ]
204
204
 
205
205
  def get_route(path: str) -> str | None:
reflex/state.py CHANGED
@@ -1100,7 +1100,12 @@ class BaseState(EvenMoreBasicBaseState):
1100
1100
  Returns:
1101
1101
  The event handler.
1102
1102
  """
1103
- return EventHandler(fn=fn, state_full_name=cls.get_full_name())
1103
+ # Check if function has stored event_actions from decorator
1104
+ event_actions = getattr(fn, "_rx_event_actions", {})
1105
+
1106
+ return EventHandler(
1107
+ fn=fn, state_full_name=cls.get_full_name(), event_actions=event_actions
1108
+ )
1104
1109
 
1105
1110
  @classmethod
1106
1111
  def _create_setvar(cls):
@@ -2412,19 +2417,19 @@ class FrontendEventExceptionState(State):
2412
2417
  """Substate for handling frontend exceptions."""
2413
2418
 
2414
2419
  @event
2415
- def handle_frontend_exception(self, stack: str, component_stack: str) -> None:
2420
+ def handle_frontend_exception(self, info: str, component_stack: str) -> None:
2416
2421
  """Handle frontend exceptions.
2417
2422
 
2418
2423
  If a frontend exception handler is provided, it will be called.
2419
2424
  Otherwise, the default frontend exception handler will be called.
2420
2425
 
2421
2426
  Args:
2422
- stack: The stack trace of the exception.
2427
+ info: The exception information.
2423
2428
  component_stack: The stack trace of the component where the exception occurred.
2424
2429
 
2425
2430
  """
2426
2431
  prerequisites.get_and_validate_app().app.frontend_exception_handler(
2427
- Exception(stack)
2432
+ Exception(info)
2428
2433
  )
2429
2434
 
2430
2435
 
reflex/utils/console.py CHANGED
@@ -31,7 +31,7 @@ _EMITTED_DEPRECATION_WARNINGS = set()
31
31
  _EMITTED_INFO = set()
32
32
 
33
33
  # Warnings which have been printed.
34
- _EMIITED_WARNINGS = set()
34
+ _EMITTED_WARNINGS = set()
35
35
 
36
36
  # Errors which have been printed.
37
37
  _EMITTED_ERRORS = set()
@@ -235,9 +235,9 @@ def warn(msg: str, *, dedupe: bool = False, **kwargs):
235
235
  """
236
236
  if _LOG_LEVEL <= LogLevel.WARNING:
237
237
  if dedupe:
238
- if msg in _EMIITED_WARNINGS:
238
+ if msg in _EMITTED_WARNINGS:
239
239
  return
240
- _EMIITED_WARNINGS.add(msg)
240
+ _EMITTED_WARNINGS.add(msg)
241
241
  print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
242
242
  if should_use_log_file_console():
243
243
  print_to_log_file(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
reflex/utils/exec.py CHANGED
@@ -405,7 +405,10 @@ def get_reload_paths() -> Sequence[Path]:
405
405
  module_path = module_path.parent
406
406
 
407
407
  while module_path.parent.name and _has_child_file(module_path, "__init__.py"):
408
- if _has_child_file(module_path, "rxconfig.py"):
408
+ if (
409
+ _has_child_file(module_path, "rxconfig.py")
410
+ and module_path == Path.cwd()
411
+ ):
409
412
  init_file = module_path / "__init__.py"
410
413
  init_file_content = init_file.read_text()
411
414
  if init_file_content.strip():
@@ -565,6 +568,12 @@ def run_backend_prod(
565
568
  run_uvicorn_backend_prod(host, port, loglevel)
566
569
 
567
570
 
571
+ def _get_backend_workers():
572
+ from reflex.utils import processes
573
+
574
+ return processes.get_num_workers()
575
+
576
+
568
577
  def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
569
578
  """Run the backend in production mode using Uvicorn.
570
579
 
@@ -585,6 +594,7 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
585
594
  "uvicorn",
586
595
  *("--host", host),
587
596
  *("--port", str(port)),
597
+ *("--workers", str(_get_backend_workers())),
588
598
  "--factory",
589
599
  app_module,
590
600
  ]
@@ -598,8 +608,8 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
598
608
  command = [
599
609
  "gunicorn",
600
610
  "--preload",
601
- "--worker-class",
602
- "uvicorn.workers.UvicornH11Worker",
611
+ *("--worker-class", "uvicorn.workers.UvicornH11Worker"),
612
+ *("--threads", str(_get_backend_workers())),
603
613
  *("--bind", f"{host}:{port}"),
604
614
  *env_args,
605
615
  f"{app_module}()",
@@ -639,13 +649,19 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel):
639
649
  *("--interface", str(Interfaces.ASGI)),
640
650
  *("--factory", get_app_instance_from_file()),
641
651
  ]
652
+
653
+ extra_env = {
654
+ environment.REFLEX_SKIP_COMPILE.name: "true", # skip compile for prod backend
655
+ }
656
+
657
+ if "GRANIAN_WORKERS" not in os.environ:
658
+ extra_env["GRANIAN_WORKERS"] = str(_get_backend_workers())
659
+
642
660
  processes.new_process(
643
661
  command,
644
662
  run=True,
645
663
  show_logs=True,
646
- env={
647
- environment.REFLEX_SKIP_COMPILE.name: "true"
648
- }, # skip compile for prod backend
664
+ env=extra_env,
649
665
  )
650
666
 
651
667
 
reflex/utils/format.py CHANGED
@@ -251,7 +251,7 @@ def _escape_js_string(string: str) -> str:
251
251
  The escaped string.
252
252
  """
253
253
 
254
- # TODO: we may need to re-vist this logic after new Var API is implemented.
254
+ # TODO: we may need to re-visit this logic after new Var API is implemented.
255
255
  def escape_outside_segments(segment: str):
256
256
  """Escape backticks in segments outside of `${}`.
257
257
 
reflex/utils/processes.py CHANGED
@@ -8,6 +8,7 @@ import os
8
8
  import signal
9
9
  import socket
10
10
  import subprocess
11
+ import sys
11
12
  from collections.abc import Callable, Generator, Sequence
12
13
  from concurrent import futures
13
14
  from contextlib import closing
@@ -68,12 +69,11 @@ def _can_bind_at_port(
68
69
  """
69
70
  try:
70
71
  with closing(socket.socket(address_family, socket.SOCK_STREAM)) as sock:
72
+ if sys.platform != "win32":
73
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
71
74
  sock.bind((address, port))
72
- except OverflowError:
73
- return False
74
- except PermissionError:
75
- return False
76
- except OSError:
75
+ except (OverflowError, PermissionError, OSError) as e:
76
+ console.warn(f"Unable to bind to {address}:{port} due to: {e}.")
77
77
  return False
78
78
  return True
79
79
 
@@ -87,38 +87,13 @@ def is_process_on_port(port: int) -> bool:
87
87
  Returns:
88
88
  Whether a process is running on the given port.
89
89
  """
90
- return not _can_bind_at_port( # Test IPv4 localhost (127.0.0.1)
91
- socket.AF_INET, "127.0.0.1", port
92
- ) or not _can_bind_at_port(
93
- socket.AF_INET6, "::1", port
94
- ) # Test IPv6 localhost (::1)
90
+ return (
91
+ not _can_bind_at_port(socket.AF_INET, "", port) # Test IPv4 local network
92
+ or not _can_bind_at_port(socket.AF_INET6, "", port) # Test IPv6 local network
93
+ )
95
94
 
96
95
 
97
- def change_port(port: int, _type: str) -> int:
98
- """Change the port.
99
-
100
- Args:
101
- port: The port.
102
- _type: The type of the port.
103
-
104
- Returns:
105
- The new port.
106
-
107
- Raises:
108
- Exit: If the port is invalid or if the new port is occupied.
109
- """
110
- new_port = port + 1
111
- if new_port < 0 or new_port > 65535:
112
- console.error(
113
- f"The {_type} port: {port} is invalid. It must be between 0 and 65535."
114
- )
115
- raise click.exceptions.Exit(1)
116
- if is_process_on_port(new_port):
117
- return change_port(new_port, _type)
118
- console.info(
119
- f"The {_type} will run on port [bold underline]{new_port}[/bold underline]."
120
- )
121
- return new_port
96
+ MAXIMUM_PORT = 2**16 - 1
122
97
 
123
98
 
124
99
  def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
@@ -137,13 +112,28 @@ def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
137
112
  Exit:when the port is in use.
138
113
  """
139
114
  console.debug(f"Checking if {service_name.capitalize()} port: {port} is in use.")
115
+
140
116
  if not is_process_on_port(port):
141
117
  console.debug(f"{service_name.capitalize()} port: {port} is not in use.")
142
118
  return port
119
+
143
120
  if auto_increment:
144
- return change_port(port, service_name)
145
- console.error(f"{service_name.capitalize()} port: {port} is already in use.")
146
- raise click.exceptions.Exit
121
+ for new_port in range(port + 1, MAXIMUM_PORT + 1):
122
+ if not is_process_on_port(new_port):
123
+ console.info(
124
+ f"The {service_name} will run on port [bold underline]{new_port}[/bold underline]."
125
+ )
126
+ return new_port
127
+ console.debug(
128
+ f"{service_name.capitalize()} port: {new_port} is already in use."
129
+ )
130
+
131
+ # If we reach here, it means we couldn't find an available port.
132
+ console.error(f"Unable to find an available port for {service_name}")
133
+ else:
134
+ console.error(f"{service_name.capitalize()} port: {port} is already in use.")
135
+
136
+ raise click.exceptions.Exit(1)
147
137
 
148
138
 
149
139
  @overload