reflex 0.8.3a2__py3-none-any.whl → 0.8.3a3__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.

@@ -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.
reflex/environment.py CHANGED
@@ -650,9 +650,6 @@ class EnvironmentVariables:
650
650
  # Enable full logging of debug messages to reflex user directory.
651
651
  REFLEX_ENABLE_FULL_LOGGING: EnvVar[bool] = env_var(False)
652
652
 
653
- # The path to the reflex errors log file. If not set, no separate error log will be used.
654
- REFLEX_ERROR_LOG_FILE: EnvVar[Path | None] = env_var(None)
655
-
656
653
 
657
654
  environment = EnvironmentVariables()
658
655
 
reflex/event.py CHANGED
@@ -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
 
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/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/testing.py CHANGED
@@ -10,6 +10,7 @@ import inspect
10
10
  import os
11
11
  import platform
12
12
  import re
13
+ import signal
13
14
  import socket
14
15
  import socketserver
15
16
  import subprocess
@@ -18,14 +19,17 @@ import textwrap
18
19
  import threading
19
20
  import time
20
21
  import types
21
- from collections.abc import Callable, Coroutine, Sequence
22
+ from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
22
23
  from http.server import SimpleHTTPRequestHandler
23
- from io import TextIOWrapper
24
24
  from pathlib import Path
25
25
  from typing import TYPE_CHECKING, Any, Literal, TypeVar
26
26
 
27
+ import uvicorn
28
+
27
29
  import reflex
30
+ import reflex.environment
28
31
  import reflex.reflex
32
+ import reflex.utils.build
29
33
  import reflex.utils.exec
30
34
  import reflex.utils.format
31
35
  import reflex.utils.prerequisites
@@ -41,7 +45,9 @@ from reflex.state import (
41
45
  StateManagerRedis,
42
46
  reload_state_module,
43
47
  )
48
+ from reflex.utils import console
44
49
  from reflex.utils.export import export
50
+ from reflex.utils.types import ASGIApp
45
51
 
46
52
  try:
47
53
  from selenium import webdriver
@@ -95,32 +101,6 @@ class chdir(contextlib.AbstractContextManager): # noqa: N801
95
101
  os.chdir(self._old_cwd.pop())
96
102
 
97
103
 
98
- class ReflexProcessLoggedErrorError(RuntimeError):
99
- """Exception raised when the reflex process logs contain errors."""
100
-
101
-
102
- class ReflexProcessExitNonZeroError(RuntimeError):
103
- """Exception raised when the reflex process exits with a non-zero status."""
104
-
105
-
106
- def _is_port_responsive(port: int) -> bool:
107
- """Check if a port is responsive.
108
-
109
- Args:
110
- port: the port to check
111
-
112
- Returns:
113
- True if the port is responsive, False otherwise
114
- """
115
- try:
116
- with contextlib.closing(
117
- socket.socket(socket.AF_INET, socket.SOCK_STREAM)
118
- ) as sock:
119
- return sock.connect_ex(("127.0.0.1", port)) == 0
120
- except (OverflowError, PermissionError, OSError):
121
- return False
122
-
123
-
124
104
  @dataclasses.dataclass
125
105
  class AppHarness:
126
106
  """AppHarness executes a reflex app in-process for testing."""
@@ -133,15 +113,14 @@ class AppHarness:
133
113
  app_module_path: Path
134
114
  app_module: types.ModuleType | None = None
135
115
  app_instance: reflex.App | None = None
136
- reflex_process: subprocess.Popen | None = None
137
- reflex_process_log_path: Path | None = None
138
- reflex_process_error_log_path: Path | None = None
116
+ app_asgi: ASGIApp | None = None
117
+ frontend_process: subprocess.Popen | None = None
139
118
  frontend_url: str | None = None
140
- backend_port: int | None = None
141
- frontend_port: int | None = None
119
+ frontend_output_thread: threading.Thread | None = None
120
+ backend_thread: threading.Thread | None = None
121
+ backend: uvicorn.Server | None = None
142
122
  state_manager: StateManager | None = None
143
123
  _frontends: list[WebDriver] = dataclasses.field(default_factory=list)
144
- _reflex_process_log_fn: TextIOWrapper | None = None
145
124
 
146
125
  @classmethod
147
126
  def create(
@@ -294,116 +273,82 @@ class AppHarness:
294
273
  # Ensure the AppHarness test does not skip State assignment due to running via pytest
295
274
  os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None)
296
275
  os.environ[reflex.constants.APP_HARNESS_FLAG] = "true"
276
+ # Ensure we actually compile the app during first initialization.
297
277
  self.app_instance, self.app_module = (
298
278
  reflex.utils.prerequisites.get_and_validate_app(
299
279
  # Do not reload the module for pre-existing apps (only apps generated from source)
300
280
  reload=self.app_source is not None
301
281
  )
302
282
  )
303
- # Have to compile to ensure all state is available.
304
- _ = self.app_instance()
305
- self.state_manager = (
306
- self.app_instance._state_manager if self.app_instance else None
307
- )
308
- if isinstance(self.state_manager, StateManagerDisk):
309
- object.__setattr__(
310
- self.state_manager, "states_directory", self.app_path / ".states"
283
+ self.app_asgi = self.app_instance()
284
+ if self.app_instance and isinstance(
285
+ self.app_instance._state_manager, StateManagerRedis
286
+ ):
287
+ if self.app_instance._state is None:
288
+ msg = "State is not set."
289
+ raise RuntimeError(msg)
290
+ # Create our own redis connection for testing.
291
+ self.state_manager = StateManagerRedis.create(self.app_instance._state)
292
+ else:
293
+ self.state_manager = (
294
+ self.app_instance._state_manager if self.app_instance else None
311
295
  )
312
296
 
313
297
  def _reload_state_module(self):
314
298
  """Reload the rx.State module to avoid conflict when reloading."""
315
299
  reload_state_module(module=f"{self.app_name}.{self.app_name}")
316
300
 
317
- def _start_subprocess(
318
- self, backend: bool = True, frontend: bool = True, mode: str = "dev"
319
- ):
320
- """Start the reflex app using subprocess instead of threads.
321
-
322
- Args:
323
- backend: Whether to start the backend server.
324
- frontend: Whether to start the frontend server.
325
- mode: The mode to run the app in (dev, prod, etc.).
326
- """
327
- self.reflex_process_log_path = self.app_path / "reflex.log"
328
- self.reflex_process_error_log_path = self.app_path / "reflex_error.log"
329
- self._reflex_process_log_fn = self.reflex_process_log_path.open("w")
330
- command = [
331
- sys.executable,
332
- "-u",
333
- "-m",
334
- "reflex",
335
- "run",
336
- "--env",
337
- mode,
338
- "--loglevel",
339
- "debug",
340
- ]
341
- if backend:
342
- if self.backend_port is None:
343
- self.backend_port = reflex.utils.processes.handle_port(
344
- "backend", 48000, auto_increment=True
345
- )
346
- command.extend(["--backend-port", str(self.backend_port)])
347
- if not frontend:
348
- command.append("--backend-only")
349
- if frontend:
350
- if self.frontend_port is None:
351
- self.frontend_port = reflex.utils.processes.handle_port(
352
- "frontend", 43000, auto_increment=True
353
- )
354
- command.extend(["--frontend-port", str(self.frontend_port)])
355
- if not backend:
356
- command.append("--frontend-only")
357
- self.reflex_process = subprocess.Popen(
358
- command,
359
- stdout=self._reflex_process_log_fn,
360
- stderr=self._reflex_process_log_fn,
361
- cwd=self.app_path,
362
- env={
363
- **os.environ,
364
- "REFLEX_ERROR_LOG_FILE": str(self.reflex_process_error_log_path),
365
- "PYTEST_CURRENT_TEST": "",
366
- "APP_HARNESS_FLAG": "true",
367
- },
368
- )
369
- self._wait_for_servers(backend=backend, frontend=frontend)
301
+ def _get_backend_shutdown_handler(self):
302
+ if self.backend is None:
303
+ msg = "Backend was not initialized."
304
+ raise RuntimeError(msg)
370
305
 
371
- def _wait_for_servers(self, backend: bool, frontend: bool):
372
- """Wait for both frontend and backend servers to be ready by parsing console output.
306
+ original_shutdown = self.backend.shutdown
373
307
 
374
- Args:
375
- backend: Whether to wait for the backend server to be ready.
376
- frontend: Whether to wait for the frontend server to be ready.
308
+ async def _shutdown(*args, **kwargs) -> None:
309
+ # ensure redis is closed before event loop
310
+ if self.app_instance is not None and isinstance(
311
+ self.app_instance._state_manager, StateManagerRedis
312
+ ):
313
+ with contextlib.suppress(ValueError):
314
+ await self.app_instance._state_manager.close()
377
315
 
378
- Raises:
379
- RuntimeError: If servers did not start properly.
380
- """
381
- if self.reflex_process is None or self.reflex_process.pid is None:
382
- msg = "Reflex process has no pid."
383
- raise RuntimeError(msg)
316
+ # socketio shutdown handler
317
+ if self.app_instance is not None and self.app_instance.sio is not None:
318
+ with contextlib.suppress(TypeError):
319
+ await self.app_instance.sio.shutdown()
384
320
 
385
- if backend and self.backend_port is None:
386
- msg = "Backend port is not set."
387
- raise RuntimeError(msg)
321
+ # sqlalchemy async engine shutdown handler
322
+ try:
323
+ async_engine = reflex.model.get_async_engine(None)
324
+ except ValueError:
325
+ pass
326
+ else:
327
+ await async_engine.dispose()
388
328
 
389
- if frontend and self.frontend_port is None:
390
- msg = "Frontend port is not set."
391
- raise RuntimeError(msg)
329
+ await original_shutdown(*args, **kwargs)
392
330
 
393
- frontend_ready = False
394
- backend_ready = False
331
+ return _shutdown
395
332
 
396
- while not ((not frontend or frontend_ready) and (not backend or backend_ready)):
397
- if backend and self.backend_port and _is_port_responsive(self.backend_port):
398
- backend_ready = True
399
- if (
400
- frontend
401
- and self.frontend_port
402
- and _is_port_responsive(self.frontend_port)
403
- ):
404
- frontend_ready = True
405
- self.frontend_url = f"http://localhost:{self.frontend_port}/"
406
- time.sleep(POLL_INTERVAL)
333
+ def _start_backend(self, port: int = 0):
334
+ if self.app_asgi is None:
335
+ msg = "App was not initialized."
336
+ raise RuntimeError(msg)
337
+ self.backend = uvicorn.Server(
338
+ uvicorn.Config(
339
+ app=self.app_asgi,
340
+ host="127.0.0.1",
341
+ port=port,
342
+ )
343
+ )
344
+ self.backend.shutdown = self._get_backend_shutdown_handler()
345
+ with chdir(self.app_path):
346
+ print( # noqa: T201
347
+ "Creating backend in a new thread..."
348
+ ) # for pytest diagnosis
349
+ self.backend_thread = threading.Thread(target=self.backend.run)
350
+ self.backend_thread.start()
351
+ print("Backend started.") # for pytest diagnosis #noqa: T201
407
352
 
408
353
  async def _reset_backend_state_manager(self):
409
354
  """Reset the StateManagerRedis event loop affinity.
@@ -431,14 +376,78 @@ class AppHarness:
431
376
  msg = "Failed to reset state manager."
432
377
  raise RuntimeError(msg)
433
378
 
379
+ def _start_frontend(self):
380
+ # Set up the frontend.
381
+ with chdir(self.app_path):
382
+ config = reflex.config.get_config()
383
+ print("Polling for servers...") # for pytest diagnosis #noqa: T201
384
+ config.api_url = "http://{}:{}".format(
385
+ *self._poll_for_servers(timeout=30).getsockname(),
386
+ )
387
+ print("Building frontend...") # for pytest diagnosis #noqa: T201
388
+ reflex.utils.build.setup_frontend(self.app_path)
389
+
390
+ print("Frontend starting...") # for pytest diagnosis #noqa: T201
391
+
392
+ # Start the frontend.
393
+ self.frontend_process = reflex.utils.processes.new_process(
394
+ [
395
+ *reflex.utils.prerequisites.get_js_package_executor(raise_on_none=True)[
396
+ 0
397
+ ],
398
+ "run",
399
+ "dev",
400
+ ],
401
+ cwd=self.app_path / reflex.utils.prerequisites.get_web_dir(),
402
+ env={"PORT": "0", "NO_COLOR": "1"},
403
+ **FRONTEND_POPEN_ARGS,
404
+ )
405
+
406
+ def _wait_frontend(self):
407
+ if self.frontend_process is None or self.frontend_process.stdout is None:
408
+ msg = "Frontend process has no stdout."
409
+ raise RuntimeError(msg)
410
+ while self.frontend_url is None:
411
+ line = self.frontend_process.stdout.readline()
412
+ if not line:
413
+ break
414
+ print(line) # for pytest diagnosis #noqa: T201
415
+ m = re.search(reflex.constants.ReactRouter.FRONTEND_LISTENING_REGEX, line)
416
+ if m is not None:
417
+ self.frontend_url = m.group(1)
418
+ config = reflex.config.get_config()
419
+ config.deploy_url = self.frontend_url
420
+ break
421
+ if self.frontend_url is None:
422
+ msg = "Frontend did not start"
423
+ raise RuntimeError(msg)
424
+
425
+ def consume_frontend_output():
426
+ while True:
427
+ try:
428
+ line = (
429
+ self.frontend_process.stdout.readline() # pyright: ignore [reportOptionalMemberAccess]
430
+ )
431
+ # catch I/O operation on closed file.
432
+ except ValueError as e:
433
+ console.error(str(e))
434
+ break
435
+ if not line:
436
+ break
437
+
438
+ self.frontend_output_thread = threading.Thread(target=consume_frontend_output)
439
+ self.frontend_output_thread.start()
440
+
434
441
  def start(self) -> AppHarness:
435
- """Start the app using reflex run subprocess.
442
+ """Start the backend in a new thread and dev frontend as a separate process.
436
443
 
437
444
  Returns:
438
445
  self
439
446
  """
440
447
  self._initialize_app()
441
- self._start_subprocess()
448
+ self._start_backend()
449
+ self._start_frontend()
450
+ self._wait_frontend()
442
451
  return self
443
452
 
444
453
  @staticmethod
@@ -467,48 +476,43 @@ class AppHarness:
467
476
  return self.start()
468
477
 
469
478
  def stop(self) -> None:
470
- """Stop the reflex subprocess."""
479
+ """Stop the frontend and backend servers."""
480
+ import psutil
481
+
482
+ # Quit browsers first to avoid any lingering events being sent during shutdown.
471
483
  for driver in self._frontends:
472
484
  driver.quit()
473
485
 
474
- self._stop_reflex()
475
486
  self._reload_state_module()
476
487
 
477
- def _stop_reflex(self):
478
- returncode = None
479
- # Check if the process exited on its own or we have to kill it.
480
- if (
481
- self.reflex_process is not None
482
- and (returncode := self.reflex_process.poll()) is None
483
- ):
484
- try:
485
- # Kill server and children recursively.
486
- reflex.utils.exec.kill(self.reflex_process.pid)
487
- except (ProcessLookupError, OSError):
488
- pass
489
- finally:
490
- self.reflex_process = None
491
- if self._reflex_process_log_fn is not None:
492
- with contextlib.suppress(Exception):
493
- self._reflex_process_log_fn.close()
494
- if self.reflex_process_log_path is not None:
495
- print(self.reflex_process_log_path.read_text()) # noqa: T201 for pytest debugging
496
- # If there are errors in the logs, raise an exception.
497
- if (
498
- self.reflex_process_error_log_path is not None
499
- and self.reflex_process_error_log_path.exists()
500
- ):
501
- error_log_content = self.reflex_process_error_log_path.read_text()
502
- if error_log_content:
503
- msg = f"Reflex process error log contains errors:\n{error_log_content}"
504
- raise ReflexProcessLoggedErrorError(msg)
505
- # When the process exits non-zero, but wasn't killed, it is a test failure.
506
- if returncode is not None and returncode != 0:
507
- msg = (
508
- f"Reflex process exited with code {returncode}. "
509
- "Check the logs for more details."
488
+ if self.backend is not None:
489
+ self.backend.should_exit = True
490
+ if self.frontend_process is not None:
491
+ # https://stackoverflow.com/a/70565806
492
+ frontend_children = psutil.Process(self.frontend_process.pid).children(
493
+ recursive=True,
510
494
  )
511
- raise ReflexProcessExitNonZeroError(msg)
495
+ if sys.platform == "win32":
496
+ self.frontend_process.terminate()
497
+ else:
498
+ pgrp = os.getpgid(self.frontend_process.pid)
499
+ os.killpg(pgrp, signal.SIGTERM)
500
+ # kill any remaining child processes
501
+ for child in frontend_children:
502
+ # It's okay if the process is already gone.
503
+ with contextlib.suppress(psutil.NoSuchProcess):
504
+ child.terminate()
505
+ _, still_alive = psutil.wait_procs(frontend_children, timeout=3)
506
+ for child in still_alive:
507
+ # It's okay if the process is already gone.
508
+ with contextlib.suppress(psutil.NoSuchProcess):
509
+ child.kill()
510
+ # wait for main process to exit
511
+ self.frontend_process.communicate()
512
+ if self.backend_thread is not None:
513
+ self.backend_thread.join()
514
+ if self.frontend_output_thread is not None:
515
+ self.frontend_output_thread.join()
512
516
 
513
517
  def __exit__(self, *excinfo) -> None:
514
518
  """Contextmanager protocol for `stop()`.
@@ -577,6 +581,39 @@ class AppHarness:
577
581
  await asyncio.sleep(step)
578
582
  return False
579
583
 
584
+ def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket:
585
+ """Poll backend server for listening sockets.
586
+
587
+ Args:
588
+ timeout: how long to wait for listening socket.
589
+
590
+ Returns:
591
+ first active listening socket on the backend
592
+
593
+ Raises:
594
+ RuntimeError: when the backend hasn't started running
595
+ TimeoutError: when server or sockets are not ready
596
+ """
597
+ if self.backend is None:
598
+ msg = "Backend is not running."
599
+ raise RuntimeError(msg)
600
+ backend = self.backend
601
+ # check for servers to be initialized
602
+ if not self._poll_for(
603
+ target=lambda: getattr(backend, "servers", False),
604
+ timeout=timeout,
605
+ ):
606
+ msg = "Backend servers are not initialized."
607
+ raise TimeoutError(msg)
608
+ # check for sockets to be listening
609
+ if not self._poll_for(
610
+ target=lambda: getattr(backend.servers[0], "sockets", False),
611
+ timeout=timeout,
612
+ ):
613
+ msg = "Backend is not listening."
614
+ raise TimeoutError(msg)
615
+ return backend.servers[0].sockets[0]
616
+
580
617
  def frontend(
581
618
  self,
582
619
  driver_clz: type[WebDriver] | None = None,
@@ -663,18 +700,71 @@ class AppHarness:
663
700
  The state instance associated with the given token
664
701
 
665
702
  Raises:
666
- AssertionError: when the state manager is not initialized
703
+ RuntimeError: when the app hasn't started running
667
704
  """
668
- assert self.state_manager is not None, "State manager is not initialized."
669
- if isinstance(self.state_manager, StateManagerDisk):
670
- self.state_manager.states.clear() # always reload from disk
705
+ if self.state_manager is None:
706
+ msg = "state_manager is not set."
707
+ raise RuntimeError(msg)
671
708
  try:
672
709
  return await self.state_manager.get_state(token)
673
710
  finally:
674
- await self._reset_backend_state_manager()
675
711
  if isinstance(self.state_manager, StateManagerRedis):
676
712
  await self.state_manager.close()
677
713
 
714
+ async def set_state(self, token: str, **kwargs) -> None:
715
+ """Set the state associated with the given token.
716
+
717
+ Args:
718
+ token: The state token to set.
719
+ kwargs: Attributes to set on the state.
720
+
721
+ Raises:
722
+ RuntimeError: when the app hasn't started running
723
+ """
724
+ if self.state_manager is None:
725
+ msg = "state_manager is not set."
726
+ raise RuntimeError(msg)
727
+ state = await self.get_state(token)
728
+ for key, value in kwargs.items():
729
+ setattr(state, key, value)
730
+ try:
731
+ await self.state_manager.set_state(token, state)
732
+ finally:
733
+ if isinstance(self.state_manager, StateManagerRedis):
734
+ await self.state_manager.close()
735
+
736
+ @contextlib.asynccontextmanager
737
+ async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
738
+ """Modify the state associated with the given token and send update to frontend.
739
+
740
+ Args:
741
+ token: The state token to modify
742
+
743
+ Yields:
744
+ The state instance associated with the given token
745
+
746
+ Raises:
747
+ RuntimeError: when the app hasn't started running
748
+ """
749
+ if self.state_manager is None:
750
+ msg = "state_manager is not set."
751
+ raise RuntimeError(msg)
752
+ if self.app_instance is None:
753
+ msg = "App is not running."
754
+ raise RuntimeError(msg)
755
+ app_state_manager = self.app_instance.state_manager
756
+ if isinstance(self.state_manager, StateManagerRedis):
757
+ # Temporarily replace the app's state manager with our own, since
758
+ # the redis connection is on the backend_thread event loop
759
+ self.app_instance._state_manager = self.state_manager
760
+ try:
761
+ async with self.app_instance.modify_state(token) as state:
762
+ yield state
763
+ finally:
764
+ if isinstance(self.state_manager, StateManagerRedis):
765
+ self.app_instance._state_manager = app_state_manager
766
+ await self.state_manager.close()
767
+
678
768
  def poll_for_content(
679
769
  self,
680
770
  element: WebElement,
@@ -917,7 +1007,7 @@ class AppHarnessProd(AppHarness):
917
1007
  root=web_root,
918
1008
  error_page_map=error_page_map,
919
1009
  ) as self.frontend_server:
920
- self.frontend_url = "http://localhost:{1}/".format(
1010
+ self.frontend_url = "http://localhost:{1}".format(
921
1011
  *self.frontend_server.socket.getsockname()
922
1012
  )
923
1013
  self.frontend_server.serve_forever()
@@ -926,7 +1016,10 @@ class AppHarnessProd(AppHarness):
926
1016
  # Set up the frontend.
927
1017
  with chdir(self.app_path):
928
1018
  config = reflex.config.get_config()
929
- config.api_url = f"http://localhost:{self.backend_port}"
1019
+ print("Polling for servers...") # for pytest diagnosis #noqa: T201
1020
+ config.api_url = "http://{}:{}".format(
1021
+ *self._poll_for_servers(timeout=30).getsockname(),
1022
+ )
930
1023
  print("Building frontend...") # for pytest diagnosis #noqa: T201
931
1024
 
932
1025
  get_config().loglevel = reflex.constants.LogLevel.INFO
@@ -955,17 +1048,32 @@ class AppHarnessProd(AppHarness):
955
1048
  msg = "Frontend did not start"
956
1049
  raise RuntimeError(msg)
957
1050
 
958
- def start(self) -> AppHarness:
959
- """Start the app using reflex run subprocess.
1051
+ def _start_backend(self):
1052
+ if self.app_asgi is None:
1053
+ msg = "App was not initialized."
1054
+ raise RuntimeError(msg)
1055
+ environment.REFLEX_SKIP_COMPILE.set(True)
1056
+ self.backend = uvicorn.Server(
1057
+ uvicorn.Config(
1058
+ app=self.app_asgi,
1059
+ host="127.0.0.1",
1060
+ port=0,
1061
+ workers=reflex.utils.processes.get_num_workers(),
1062
+ ),
1063
+ )
1064
+ self.backend.shutdown = self._get_backend_shutdown_handler()
1065
+ print( # noqa: T201
1066
+ "Creating backend in a new thread..."
1067
+ )
1068
+ self.backend_thread = threading.Thread(target=self.backend.run)
1069
+ self.backend_thread.start()
1070
+ print("Backend started.") # for pytest diagnosis #noqa: T201
960
1071
 
961
- Returns:
962
- self
963
- """
964
- self._initialize_app()
965
- self._start_subprocess(frontend=False, mode="prod")
966
- self._start_frontend()
967
- self._wait_frontend()
968
- return self
1072
+ def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket:
1073
+ try:
1074
+ return super()._poll_for_servers(timeout)
1075
+ finally:
1076
+ environment.REFLEX_SKIP_COMPILE.set(None)
969
1077
 
970
1078
  def stop(self):
971
1079
  """Stop the frontend python webserver."""
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()
@@ -128,21 +128,6 @@ def should_use_log_file_console() -> bool:
128
128
  return environment.REFLEX_ENABLE_FULL_LOGGING.get()
129
129
 
130
130
 
131
- @once
132
- def error_log_file_console():
133
- """Create a console that logs errors to a file.
134
-
135
- Returns:
136
- A Console object that logs errors to a file.
137
- """
138
- from reflex.environment import environment
139
-
140
- if not (env_error_log_file := environment.REFLEX_ERROR_LOG_FILE.get()):
141
- return None
142
- env_error_log_file.parent.mkdir(parents=True, exist_ok=True)
143
- return Console(file=env_error_log_file.open("a", encoding="utf-8"))
144
-
145
-
146
131
  def print_to_log_file(msg: str, *, dedupe: bool = False, **kwargs):
147
132
  """Print a message to the log file.
148
133
 
@@ -250,9 +235,9 @@ def warn(msg: str, *, dedupe: bool = False, **kwargs):
250
235
  """
251
236
  if _LOG_LEVEL <= LogLevel.WARNING:
252
237
  if dedupe:
253
- if msg in _EMIITED_WARNINGS:
238
+ if msg in _EMITTED_WARNINGS:
254
239
  return
255
- _EMIITED_WARNINGS.add(msg)
240
+ _EMITTED_WARNINGS.add(msg)
256
241
  print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
257
242
  if should_use_log_file_console():
258
243
  print_to_log_file(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
@@ -343,8 +328,6 @@ def error(msg: str, *, dedupe: bool = False, **kwargs):
343
328
  print(f"[red]{msg}[/red]", **kwargs)
344
329
  if should_use_log_file_console():
345
330
  print_to_log_file(f"[red]{msg}[/red]", **kwargs)
346
- if error_log_file := error_log_file_console():
347
- error_log_file.print(f"[red]{msg}[/red]", **kwargs)
348
331
 
349
332
 
350
333
  def ask(
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():
@@ -603,8 +606,6 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
603
606
 
604
607
  # Our default args, then env args (env args win on conflicts)
605
608
  command = [
606
- sys.executable,
607
- "-m",
608
609
  "gunicorn",
609
610
  "--preload",
610
611
  *("--worker-class", "uvicorn.workers.UvicornH11Worker"),
@@ -641,8 +642,6 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel):
641
642
  from reflex.utils import processes
642
643
 
643
644
  command = [
644
- sys.executable,
645
- "-m",
646
645
  "granian",
647
646
  *("--log-level", "critical"),
648
647
  *("--host", host),
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reflex
3
- Version: 0.8.3a2
3
+ Version: 0.8.3a3
4
4
  Summary: Web apps in pure Python.
5
5
  Project-URL: homepage, https://reflex.dev
6
6
  Project-URL: repository, https://github.com/reflex-dev/reflex
@@ -6,16 +6,16 @@ reflex/app.py,sha256=xrP6jq5D5tILtNzNluhRJ_kkWuRIekj8NFA5plA-Spc,75596
6
6
  reflex/assets.py,sha256=l5O_mlrTprC0lF7Rc_McOe3a0OtSLnRdNl_PqCpDCBA,3431
7
7
  reflex/base.py,sha256=Oh664QL3fZEHErhUasFqP7fE4olYf1y-9Oj6uZI2FCU,1173
8
8
  reflex/config.py,sha256=HgJ57Op-glTro23GoQKmyXwUplvGYgZFKjvClYpD27s,19359
9
- reflex/environment.py,sha256=aSZwDavbkKdNKL2oMiZvSeIiWps65eIMr0_xK826EFA,23385
10
- reflex/event.py,sha256=QjSYc91n8XwSXgjQ3cCWwiaZwS0IbZL5z4LiPRS6ioA,72774
9
+ reflex/environment.py,sha256=PQF1QSLgu_tpUUP0vXWdvuC4t3wFts94nw2urt9Id8o,23227
10
+ reflex/event.py,sha256=sLjwMSsfQ3sZowvg-Og3gTVU4mqdwD587Q9Psgdfyzw,72776
11
11
  reflex/model.py,sha256=l1-6fm7NHRFWH-xK9oV9UzAVfvKeUXG1f-tCrF7vmfI,19403
12
12
  reflex/page.py,sha256=Bn8FTlUtjjKwUtpQA5r5eaWE_ZUb8i4XgrQi8FWbzNA,1880
13
13
  reflex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  reflex/reflex.py,sha256=1sRT6INn5ZdbPZAL-g5C9hU6ZwMEMHJV6gUvNTx3KSg,22616
15
- reflex/route.py,sha256=UfhKxFwe3EYto66TZv5K2Gp6ytRbcL-_v72DJ5girEs,7691
15
+ reflex/route.py,sha256=ZRlXl1HeKWM3W-yFCnRFIA_6I3W9JBqe2CKzCpWX7Vg,7699
16
16
  reflex/state.py,sha256=mrYV9wEdJ9DdWbAadgdWHbxc6onOU56dT8f4zkbznPo,92127
17
17
  reflex/style.py,sha256=JxbXXA4MTnXrk0XHEoMBoNC7J-M2oL5Hl3W_QmXvmBg,13222
18
- reflex/testing.py,sha256=92KUvlXE9w0N4IhqVOEOF8gIFJGuRxj7edeSWJrPGgk,35092
18
+ reflex/testing.py,sha256=6EXQN9K0tYfzEDe2aSRh4xLM_Jb_oIrI_qH2F_e0KXc,39423
19
19
  reflex/.templates/apps/blank/assets/favicon.ico,sha256=baxxgDAQ2V4-G5Q4S2yK5uUJTUGkv-AOWBQ0xd6myUo,4286
20
20
  reflex/.templates/apps/blank/code/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  reflex/.templates/apps/blank/code/blank.py,sha256=wry9E3VjC7qtt_gzqNOyo4KZAAlzVyNp3uhFkcLZmM0,898
@@ -251,8 +251,8 @@ reflex/components/radix/themes/components/segmented_control.py,sha256=E2kqQ8rQ45
251
251
  reflex/components/radix/themes/components/segmented_control.pyi,sha256=ehTNqJjQLI7tO2uZD6s037ntbYAYe9t3w5M8jjTe6eA,8109
252
252
  reflex/components/radix/themes/components/select.py,sha256=pHOKI3H954ZlVHK5lf074D6SuEsEglaZVeJkgPFDGPE,8083
253
253
  reflex/components/radix/themes/components/select.pyi,sha256=-CdGS9Nog-kNRE6Q81GadBiciI_2MxlHeJtkwF74WxA,35724
254
- reflex/components/radix/themes/components/separator.py,sha256=pD7SIh9h32esmfhzyA-9IwPb_08B5b2V0crbYcVAUd8,985
255
- reflex/components/radix/themes/components/separator.pyi,sha256=m5D8nqz-x-LZWQDxOxRs3emH7SRUfAxX6UBZ46U2h0w,5006
254
+ reflex/components/radix/themes/components/separator.py,sha256=Yj0Y34gGTdm3LWcjtqGazWflkfpVBVEBHvXSAujbf5A,991
255
+ reflex/components/radix/themes/components/separator.pyi,sha256=YEfu92fbRCaGri7zhunvahjY4bmOlsZjB6UwvKUBiKI,5012
256
256
  reflex/components/radix/themes/components/skeleton.py,sha256=oHltF5lOzE8T0poYtIXj3f2x8O_iZ56HCtx0a9AJ_Kw,918
257
257
  reflex/components/radix/themes/components/skeleton.pyi,sha256=EuDA_WY91nrzZZ50M0UWdGSc37YCmJX7SHSneQXhPVk,3944
258
258
  reflex/components/radix/themes/components/slider.py,sha256=8t-V8XTftdSZ3M7KBwu244k8JyGp6eMCz9t3vO5yLlQ,3440
@@ -364,7 +364,7 @@ reflex/middleware/__init__.py,sha256=x7xTeDuc73Hjj43k1J63naC9x8vzFxl4sq7cCFBX7sk
364
364
  reflex/middleware/hydrate_middleware.py,sha256=1ch7bx2ZhojOR15b-LHD2JztrWCnpPJjTe8MWHJe-5Y,1510
365
365
  reflex/middleware/middleware.py,sha256=p5VVoIgQ_NwOg_GOY6g0S4fmrV76_VE1zt-HiwbMw-s,1158
366
366
  reflex/plugins/__init__.py,sha256=aARE63iAH2pNVdGmFU4qkXDiPoeBxINfkFzwk-NZe0w,351
367
- reflex/plugins/base.py,sha256=fVez3g3OVlu0NZ-ldMMAYFxpj1avyxBoJSNH1wUdJYE,3057
367
+ reflex/plugins/base.py,sha256=Jgj7Xrpk0ztM57kl0hN3PUHYrGSXu7oj7OPyIWQJUcE,3059
368
368
  reflex/plugins/shared_tailwind.py,sha256=UXUndEEcYBZ02klymw-vSZv01IZVLJG3oSaBHpQ617U,6426
369
369
  reflex/plugins/sitemap.py,sha256=Jj47uSMnkxndl7awkl48EhlQylBfY00WuMBNyTBcZHA,6186
370
370
  reflex/plugins/tailwind_v3.py,sha256=7bXI-zsGoS1pW27-_gskxGaUOQ7NQMPcYkoI5lnmIMA,4819
@@ -373,12 +373,12 @@ reflex/utils/__init__.py,sha256=y-AHKiRQAhk2oAkvn7W8cRVTZVK625ff8tTwvZtO7S4,24
373
373
  reflex/utils/build.py,sha256=lk8hE69onG95dv-LxRhjtEugct1g-KcWPUDorzqeGIE,7964
374
374
  reflex/utils/codespaces.py,sha256=kEQ-j-jclTukFpXDlYgNp95kYMGDrQmP3VNEoYGZ1u4,3052
375
375
  reflex/utils/compat.py,sha256=aSJH_M6iomgHPQ4onQ153xh1MWqPi3HSYDzE68N6gZM,2635
376
- reflex/utils/console.py,sha256=s9M3yL7G8-mW3gXuVBMOgoDYk9EfHJ0sL1XADwye67o,12088
376
+ reflex/utils/console.py,sha256=_qC1KxqmIThrQnoCqIr4AMGgXfnByeF97YbA77PPuTQ,11531
377
377
  reflex/utils/decorator.py,sha256=DVrlVGljV5OchMs-5_y1CbbqnCWlH6lv-dFko8yHxVY,1738
378
378
  reflex/utils/exceptions.py,sha256=Wwu7Ji2xgq521bJKtU2NgjwhmFfnG8erirEVN2h8S-g,8884
379
- reflex/utils/exec.py,sha256=_LmatrFVNQJPJEqh6Ov3p14UQedPHZj92hLb_HmEqHs,21993
379
+ reflex/utils/exec.py,sha256=KQ5tS5vz3wuu9zDGPR6clSE0IoY_vcIbJGCB5bqiDJM,21987
380
380
  reflex/utils/export.py,sha256=Z2AHuhkxGQzOi9I90BejQ4qEcD0URr2i-ZU5qTJt7eQ,2562
381
- reflex/utils/format.py,sha256=6lgPpYsArWDwGuC_BT-X9g4BnCG14vvH7-oNjrCA5Xc,21119
381
+ reflex/utils/format.py,sha256=FZe5NA0U3K0n0k7r8RIGcx-eHpN7sf8eQX9w1C8_uR8,21120
382
382
  reflex/utils/imports.py,sha256=Ov-lqv-PfsPl3kTEW13r5aDauIfn6TqzEMyv42RKLOA,3761
383
383
  reflex/utils/lazy_loader.py,sha256=BiY9OvmAJDCz10qpuyTYv9duXgMFQa6RXKQmTO9hqKU,4453
384
384
  reflex/utils/misc.py,sha256=zbYIl7mI08is9enr851sj7PnDaNeVVvq5jDmQ4wdlCE,2879
@@ -401,8 +401,8 @@ reflex/vars/number.py,sha256=tO7pnvFaBsedq1HWT4skytnSqHWMluGEhUbjAUMx8XQ,28190
401
401
  reflex/vars/object.py,sha256=BDmeiwG8v97s_BnR1Egq3NxOKVjv9TfnREB3cz0zZtk,17322
402
402
  reflex/vars/sequence.py,sha256=1kBrqihspyjyQ1XDqFPC8OpVGtZs_EVkOdIKBro5ilA,55249
403
403
  scripts/hatch_build.py,sha256=-4pxcLSFmirmujGpQX9UUxjhIC03tQ_fIQwVbHu9kc0,1861
404
- reflex-0.8.3a2.dist-info/METADATA,sha256=EXg7yHNzrGXELUGgv9wt1N8WikdQ3NDHBzPsqqR2I_k,12371
405
- reflex-0.8.3a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
406
- reflex-0.8.3a2.dist-info/entry_points.txt,sha256=Rxt4dXc7MLBNt5CSHTehVPuSe9Xqow4HLX55nD9tQQ0,45
407
- reflex-0.8.3a2.dist-info/licenses/LICENSE,sha256=dw3zLrp9f5ObD7kqS32vWfhcImfO52PMmRqvtxq_YEE,11358
408
- reflex-0.8.3a2.dist-info/RECORD,,
404
+ reflex-0.8.3a3.dist-info/METADATA,sha256=z4r4mAqtN5umJxNbaXQGmMl__HnItjgvmO_E2N3MiIM,12371
405
+ reflex-0.8.3a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
406
+ reflex-0.8.3a3.dist-info/entry_points.txt,sha256=Rxt4dXc7MLBNt5CSHTehVPuSe9Xqow4HLX55nD9tQQ0,45
407
+ reflex-0.8.3a3.dist-info/licenses/LICENSE,sha256=dw3zLrp9f5ObD7kqS32vWfhcImfO52PMmRqvtxq_YEE,11358
408
+ reflex-0.8.3a3.dist-info/RECORD,,