reflex 0.8.3a1__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,16 +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 psutil
27
+ import uvicorn
28
28
 
29
29
  import reflex
30
+ import reflex.environment
30
31
  import reflex.reflex
32
+ import reflex.utils.build
31
33
  import reflex.utils.exec
32
34
  import reflex.utils.format
33
35
  import reflex.utils.prerequisites
@@ -43,7 +45,9 @@ from reflex.state import (
43
45
  StateManagerRedis,
44
46
  reload_state_module,
45
47
  )
48
+ from reflex.utils import console
46
49
  from reflex.utils.export import export
50
+ from reflex.utils.types import ASGIApp
47
51
 
48
52
  try:
49
53
  from selenium import webdriver
@@ -97,14 +101,6 @@ class chdir(contextlib.AbstractContextManager): # noqa: N801
97
101
  os.chdir(self._old_cwd.pop())
98
102
 
99
103
 
100
- class ReflexProcessLoggedErrorError(RuntimeError):
101
- """Exception raised when the reflex process logs contain errors."""
102
-
103
-
104
- class ReflexProcessExitNonZeroError(RuntimeError):
105
- """Exception raised when the reflex process exits with a non-zero status."""
106
-
107
-
108
104
  @dataclasses.dataclass
109
105
  class AppHarness:
110
106
  """AppHarness executes a reflex app in-process for testing."""
@@ -117,15 +113,14 @@ class AppHarness:
117
113
  app_module_path: Path
118
114
  app_module: types.ModuleType | None = None
119
115
  app_instance: reflex.App | None = None
120
- reflex_process: subprocess.Popen | None = None
121
- reflex_process_log_path: Path | None = None
122
- reflex_process_error_log_path: Path | None = None
116
+ app_asgi: ASGIApp | None = None
117
+ frontend_process: subprocess.Popen | None = None
123
118
  frontend_url: str | None = None
124
- backend_port: int | None = None
125
- 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
126
122
  state_manager: StateManager | None = None
127
123
  _frontends: list[WebDriver] = dataclasses.field(default_factory=list)
128
- _reflex_process_log_fn: TextIOWrapper | None = None
129
124
 
130
125
  @classmethod
131
126
  def create(
@@ -278,117 +273,82 @@ class AppHarness:
278
273
  # Ensure the AppHarness test does not skip State assignment due to running via pytest
279
274
  os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None)
280
275
  os.environ[reflex.constants.APP_HARNESS_FLAG] = "true"
276
+ # Ensure we actually compile the app during first initialization.
281
277
  self.app_instance, self.app_module = (
282
278
  reflex.utils.prerequisites.get_and_validate_app(
283
279
  # Do not reload the module for pre-existing apps (only apps generated from source)
284
280
  reload=self.app_source is not None
285
281
  )
286
282
  )
287
- # Have to compile to ensure all state is available.
288
- _ = self.app_instance()
289
- self.state_manager = (
290
- self.app_instance._state_manager if self.app_instance else None
291
- )
292
- if isinstance(self.state_manager, StateManagerDisk):
293
- object.__setattr__(
294
- 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
295
295
  )
296
296
 
297
297
  def _reload_state_module(self):
298
298
  """Reload the rx.State module to avoid conflict when reloading."""
299
299
  reload_state_module(module=f"{self.app_name}.{self.app_name}")
300
300
 
301
- def _start_subprocess(
302
- self, backend: bool = True, frontend: bool = True, mode: str = "dev"
303
- ):
304
- """Start the reflex app using subprocess instead of threads.
301
+ def _get_backend_shutdown_handler(self):
302
+ if self.backend is None:
303
+ msg = "Backend was not initialized."
304
+ raise RuntimeError(msg)
305
305
 
306
- Args:
307
- backend: Whether to start the backend server.
308
- frontend: Whether to start the frontend server.
309
- mode: The mode to run the app in (dev, prod, etc.).
310
- """
311
- self.reflex_process_log_path = self.app_path / "reflex.log"
312
- self.reflex_process_error_log_path = self.app_path / "reflex_error.log"
313
- self._reflex_process_log_fn = self.reflex_process_log_path.open("w")
314
- command = [
315
- sys.executable,
316
- "-u",
317
- "-m",
318
- "reflex",
319
- "run",
320
- "--env",
321
- mode,
322
- "--loglevel",
323
- "debug",
324
- ]
325
- if backend:
326
- if self.backend_port is None:
327
- self.backend_port = reflex.utils.processes.handle_port(
328
- "backend", 48000, auto_increment=True
329
- )
330
- command.extend(["--backend-port", str(self.backend_port)])
331
- if not frontend:
332
- command.append("--backend-only")
333
- if frontend:
334
- if self.frontend_port is None:
335
- self.frontend_port = reflex.utils.processes.handle_port(
336
- "frontend", 43000, auto_increment=True
337
- )
338
- command.extend(["--frontend-port", str(self.frontend_port)])
339
- if not backend:
340
- command.append("--frontend-only")
341
- self.reflex_process = subprocess.Popen(
342
- command,
343
- stdout=self._reflex_process_log_fn,
344
- stderr=self._reflex_process_log_fn,
345
- cwd=self.app_path,
346
- env={
347
- **os.environ,
348
- "REFLEX_ERROR_LOG_FILE": str(self.reflex_process_error_log_path),
349
- "PYTEST_CURRENT_TEST": "",
350
- "APP_HARNESS_FLAG": "true",
351
- },
352
- )
353
- self._wait_for_servers(backend=backend, frontend=frontend)
306
+ original_shutdown = self.backend.shutdown
354
307
 
355
- def _wait_for_servers(self, backend: bool, frontend: bool):
356
- """Wait for both frontend and backend servers to be ready by parsing console output.
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()
357
315
 
358
- Args:
359
- backend: Whether to wait for the backend server to be ready.
360
- frontend: Whether to wait for the frontend server to be ready.
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()
361
320
 
362
- Raises:
363
- RuntimeError: If servers did not start properly.
364
- """
365
- if self.reflex_process is None or self.reflex_process.pid is None:
366
- msg = "Reflex process has no pid."
367
- 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()
368
328
 
369
- frontend_ready = False
370
- backend_ready = False
371
- timeout = 30
372
- start_time = time.time()
329
+ await original_shutdown(*args, **kwargs)
373
330
 
374
- process = psutil.Process(self.reflex_process.pid)
375
- while not ((not frontend or frontend_ready) and (not backend or backend_ready)):
376
- if time.time() - start_time > timeout:
377
- msg = f"Timeout waiting for servers. Frontend ready: {frontend_ready}, Backend ready: {backend_ready}"
378
- raise RuntimeError(msg)
331
+ return _shutdown
379
332
 
380
- for proc in process.children(recursive=True):
381
- with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied):
382
- if ncs := proc.net_connections():
383
- for net_conn in ncs:
384
- if net_conn.status == psutil.CONN_LISTEN:
385
- if net_conn.laddr.port == self.frontend_port:
386
- frontend_ready = True
387
- self.frontend_url = (
388
- f"http://localhost:{self.frontend_port}/"
389
- )
390
- elif net_conn.laddr.port == self.backend_port:
391
- backend_ready = True
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
392
352
 
393
353
  async def _reset_backend_state_manager(self):
394
354
  """Reset the StateManagerRedis event loop affinity.
@@ -416,14 +376,78 @@ class AppHarness:
416
376
  msg = "Failed to reset state manager."
417
377
  raise RuntimeError(msg)
418
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
+
419
441
  def start(self) -> AppHarness:
420
- """Start the app using reflex run subprocess.
442
+ """Start the backend in a new thread and dev frontend as a separate process.
421
443
 
422
444
  Returns:
423
445
  self
424
446
  """
425
447
  self._initialize_app()
426
- self._start_subprocess()
448
+ self._start_backend()
449
+ self._start_frontend()
450
+ self._wait_frontend()
427
451
  return self
428
452
 
429
453
  @staticmethod
@@ -452,48 +476,43 @@ class AppHarness:
452
476
  return self.start()
453
477
 
454
478
  def stop(self) -> None:
455
- """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.
456
483
  for driver in self._frontends:
457
484
  driver.quit()
458
485
 
459
- self._stop_reflex()
460
486
  self._reload_state_module()
461
487
 
462
- def _stop_reflex(self):
463
- returncode = None
464
- # Check if the process exited on its own or we have to kill it.
465
- if (
466
- self.reflex_process is not None
467
- and (returncode := self.reflex_process.poll()) is None
468
- ):
469
- try:
470
- # Kill server and children recursively.
471
- reflex.utils.exec.kill(self.reflex_process.pid)
472
- except (ProcessLookupError, OSError):
473
- pass
474
- finally:
475
- self.reflex_process = None
476
- if self._reflex_process_log_fn is not None:
477
- with contextlib.suppress(Exception):
478
- self._reflex_process_log_fn.close()
479
- if self.reflex_process_log_path is not None:
480
- print(self.reflex_process_log_path.read_text()) # noqa: T201 for pytest debugging
481
- # If there are errors in the logs, raise an exception.
482
- if (
483
- self.reflex_process_error_log_path is not None
484
- and self.reflex_process_error_log_path.exists()
485
- ):
486
- error_log_content = self.reflex_process_error_log_path.read_text()
487
- if error_log_content:
488
- msg = f"Reflex process error log contains errors:\n{error_log_content}"
489
- raise ReflexProcessLoggedErrorError(msg)
490
- # When the process exits non-zero, but wasn't killed, it is a test failure.
491
- if returncode is not None and returncode != 0:
492
- msg = (
493
- f"Reflex process exited with code {returncode}. "
494
- "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,
495
494
  )
496
- 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()
497
516
 
498
517
  def __exit__(self, *excinfo) -> None:
499
518
  """Contextmanager protocol for `stop()`.
@@ -562,6 +581,39 @@ class AppHarness:
562
581
  await asyncio.sleep(step)
563
582
  return False
564
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
+
565
617
  def frontend(
566
618
  self,
567
619
  driver_clz: type[WebDriver] | None = None,
@@ -648,18 +700,71 @@ class AppHarness:
648
700
  The state instance associated with the given token
649
701
 
650
702
  Raises:
651
- AssertionError: when the state manager is not initialized
703
+ RuntimeError: when the app hasn't started running
652
704
  """
653
- assert self.state_manager is not None, "State manager is not initialized."
654
- if isinstance(self.state_manager, StateManagerDisk):
655
- 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)
656
708
  try:
657
709
  return await self.state_manager.get_state(token)
658
710
  finally:
659
- await self._reset_backend_state_manager()
660
711
  if isinstance(self.state_manager, StateManagerRedis):
661
712
  await self.state_manager.close()
662
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
+
663
768
  def poll_for_content(
664
769
  self,
665
770
  element: WebElement,
@@ -902,7 +1007,7 @@ class AppHarnessProd(AppHarness):
902
1007
  root=web_root,
903
1008
  error_page_map=error_page_map,
904
1009
  ) as self.frontend_server:
905
- self.frontend_url = "http://localhost:{1}/".format(
1010
+ self.frontend_url = "http://localhost:{1}".format(
906
1011
  *self.frontend_server.socket.getsockname()
907
1012
  )
908
1013
  self.frontend_server.serve_forever()
@@ -911,7 +1016,10 @@ class AppHarnessProd(AppHarness):
911
1016
  # Set up the frontend.
912
1017
  with chdir(self.app_path):
913
1018
  config = reflex.config.get_config()
914
- 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
+ )
915
1023
  print("Building frontend...") # for pytest diagnosis #noqa: T201
916
1024
 
917
1025
  get_config().loglevel = reflex.constants.LogLevel.INFO
@@ -940,17 +1048,32 @@ class AppHarnessProd(AppHarness):
940
1048
  msg = "Frontend did not start"
941
1049
  raise RuntimeError(msg)
942
1050
 
943
- def start(self) -> AppHarness:
944
- """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
945
1071
 
946
- Returns:
947
- self
948
- """
949
- self._initialize_app()
950
- self._start_subprocess(frontend=False, mode="prod")
951
- self._start_frontend()
952
- self._wait_frontend()
953
- 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)
954
1077
 
955
1078
  def stop(self):
956
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.3a1
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=_3RaedM2tJEmZpzEce0AixYBv7lYtCS52pcFHoVVzo0,35025
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.3a1.dist-info/METADATA,sha256=q-R6r0BXinptbZJV_2s1I5US8zOsqkEb-1_TVlHf6R4,12371
405
- reflex-0.8.3a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
406
- reflex-0.8.3a1.dist-info/entry_points.txt,sha256=Rxt4dXc7MLBNt5CSHTehVPuSe9Xqow4HLX55nD9tQQ0,45
407
- reflex-0.8.3a1.dist-info/licenses/LICENSE,sha256=dw3zLrp9f5ObD7kqS32vWfhcImfO52PMmRqvtxq_YEE,11358
408
- reflex-0.8.3a1.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,,