reflex 0.8.2a1__py3-none-any.whl → 0.8.3a2__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 (37) 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/recharts/recharts.py +2 -2
  20. reflex/components/sonner/toast.py +1 -1
  21. reflex/config.py +3 -3
  22. reflex/constants/installer.py +2 -1
  23. reflex/environment.py +3 -0
  24. reflex/event.py +69 -8
  25. reflex/model.py +55 -0
  26. reflex/reflex.py +33 -0
  27. reflex/state.py +9 -4
  28. reflex/testing.py +180 -288
  29. reflex/utils/console.py +17 -0
  30. reflex/utils/exec.py +22 -5
  31. reflex/utils/processes.py +28 -38
  32. reflex/utils/types.py +1 -1
  33. {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/METADATA +1 -1
  34. {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/RECORD +37 -35
  35. {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/WHEEL +0 -0
  36. {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/entry_points.txt +0 -0
  37. {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/licenses/LICENSE +0 -0
reflex/testing.py CHANGED
@@ -10,7 +10,6 @@ import inspect
10
10
  import os
11
11
  import platform
12
12
  import re
13
- import signal
14
13
  import socket
15
14
  import socketserver
16
15
  import subprocess
@@ -19,17 +18,14 @@ import textwrap
19
18
  import threading
20
19
  import time
21
20
  import types
22
- from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
21
+ from collections.abc import Callable, Coroutine, Sequence
23
22
  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
-
29
27
  import reflex
30
- import reflex.environment
31
28
  import reflex.reflex
32
- import reflex.utils.build
33
29
  import reflex.utils.exec
34
30
  import reflex.utils.format
35
31
  import reflex.utils.prerequisites
@@ -45,9 +41,7 @@ from reflex.state import (
45
41
  StateManagerRedis,
46
42
  reload_state_module,
47
43
  )
48
- from reflex.utils import console
49
44
  from reflex.utils.export import export
50
- from reflex.utils.types import ASGIApp
51
45
 
52
46
  try:
53
47
  from selenium import webdriver
@@ -101,6 +95,32 @@ class chdir(contextlib.AbstractContextManager): # noqa: N801
101
95
  os.chdir(self._old_cwd.pop())
102
96
 
103
97
 
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
+
104
124
  @dataclasses.dataclass
105
125
  class AppHarness:
106
126
  """AppHarness executes a reflex app in-process for testing."""
@@ -113,14 +133,15 @@ class AppHarness:
113
133
  app_module_path: Path
114
134
  app_module: types.ModuleType | None = None
115
135
  app_instance: reflex.App | None = None
116
- app_asgi: ASGIApp | None = None
117
- frontend_process: subprocess.Popen | 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
118
139
  frontend_url: str | None = None
119
- frontend_output_thread: threading.Thread | None = None
120
- backend_thread: threading.Thread | None = None
121
- backend: uvicorn.Server | None = None
140
+ backend_port: int | None = None
141
+ frontend_port: int | None = None
122
142
  state_manager: StateManager | None = None
123
143
  _frontends: list[WebDriver] = dataclasses.field(default_factory=list)
144
+ _reflex_process_log_fn: TextIOWrapper | None = None
124
145
 
125
146
  @classmethod
126
147
  def create(
@@ -273,82 +294,116 @@ class AppHarness:
273
294
  # Ensure the AppHarness test does not skip State assignment due to running via pytest
274
295
  os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None)
275
296
  os.environ[reflex.constants.APP_HARNESS_FLAG] = "true"
276
- # Ensure we actually compile the app during first initialization.
277
297
  self.app_instance, self.app_module = (
278
298
  reflex.utils.prerequisites.get_and_validate_app(
279
299
  # Do not reload the module for pre-existing apps (only apps generated from source)
280
300
  reload=self.app_source is not None
281
301
  )
282
302
  )
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
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"
295
311
  )
296
312
 
297
313
  def _reload_state_module(self):
298
314
  """Reload the rx.State module to avoid conflict when reloading."""
299
315
  reload_state_module(module=f"{self.app_name}.{self.app_name}")
300
316
 
301
- def _get_backend_shutdown_handler(self):
302
- if self.backend is None:
303
- msg = "Backend was not initialized."
304
- raise RuntimeError(msg)
305
-
306
- original_shutdown = self.backend.shutdown
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.
307
321
 
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()
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)
315
370
 
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()
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.
320
373
 
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()
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.
328
377
 
329
- await original_shutdown(*args, **kwargs)
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)
330
384
 
331
- return _shutdown
385
+ if backend and self.backend_port is None:
386
+ msg = "Backend port is not set."
387
+ raise RuntimeError(msg)
332
388
 
333
- def _start_backend(self, port: int = 0):
334
- if self.app_asgi is None:
335
- msg = "App was not initialized."
389
+ if frontend and self.frontend_port is None:
390
+ msg = "Frontend port is not set."
336
391
  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
+
393
+ frontend_ready = False
394
+ backend_ready = False
395
+
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)
352
407
 
353
408
  async def _reset_backend_state_manager(self):
354
409
  """Reset the StateManagerRedis event loop affinity.
@@ -376,78 +431,14 @@ class AppHarness:
376
431
  msg = "Failed to reset state manager."
377
432
  raise RuntimeError(msg)
378
433
 
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
-
441
434
  def start(self) -> AppHarness:
442
- """Start the backend in a new thread and dev frontend as a separate process.
435
+ """Start the app using reflex run subprocess.
443
436
 
444
437
  Returns:
445
438
  self
446
439
  """
447
440
  self._initialize_app()
448
- self._start_backend()
449
- self._start_frontend()
450
- self._wait_frontend()
441
+ self._start_subprocess()
451
442
  return self
452
443
 
453
444
  @staticmethod
@@ -476,43 +467,48 @@ class AppHarness:
476
467
  return self.start()
477
468
 
478
469
  def stop(self) -> None:
479
- """Stop the frontend and backend servers."""
480
- import psutil
481
-
482
- # Quit browsers first to avoid any lingering events being sent during shutdown.
470
+ """Stop the reflex subprocess."""
483
471
  for driver in self._frontends:
484
472
  driver.quit()
485
473
 
474
+ self._stop_reflex()
486
475
  self._reload_state_module()
487
476
 
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,
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."
494
510
  )
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()
511
+ raise ReflexProcessExitNonZeroError(msg)
516
512
 
517
513
  def __exit__(self, *excinfo) -> None:
518
514
  """Contextmanager protocol for `stop()`.
@@ -581,39 +577,6 @@ class AppHarness:
581
577
  await asyncio.sleep(step)
582
578
  return False
583
579
 
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
-
617
580
  def frontend(
618
581
  self,
619
582
  driver_clz: type[WebDriver] | None = None,
@@ -700,71 +663,18 @@ class AppHarness:
700
663
  The state instance associated with the given token
701
664
 
702
665
  Raises:
703
- RuntimeError: when the app hasn't started running
666
+ AssertionError: when the state manager is not initialized
704
667
  """
705
- if self.state_manager is None:
706
- msg = "state_manager is not set."
707
- raise RuntimeError(msg)
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
708
671
  try:
709
672
  return await self.state_manager.get_state(token)
710
673
  finally:
674
+ await self._reset_backend_state_manager()
711
675
  if isinstance(self.state_manager, StateManagerRedis):
712
676
  await self.state_manager.close()
713
677
 
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
-
768
678
  def poll_for_content(
769
679
  self,
770
680
  element: WebElement,
@@ -1007,7 +917,7 @@ class AppHarnessProd(AppHarness):
1007
917
  root=web_root,
1008
918
  error_page_map=error_page_map,
1009
919
  ) as self.frontend_server:
1010
- self.frontend_url = "http://localhost:{1}".format(
920
+ self.frontend_url = "http://localhost:{1}/".format(
1011
921
  *self.frontend_server.socket.getsockname()
1012
922
  )
1013
923
  self.frontend_server.serve_forever()
@@ -1016,10 +926,7 @@ class AppHarnessProd(AppHarness):
1016
926
  # Set up the frontend.
1017
927
  with chdir(self.app_path):
1018
928
  config = reflex.config.get_config()
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
- )
929
+ config.api_url = f"http://localhost:{self.backend_port}"
1023
930
  print("Building frontend...") # for pytest diagnosis #noqa: T201
1024
931
 
1025
932
  get_config().loglevel = reflex.constants.LogLevel.INFO
@@ -1048,32 +955,17 @@ class AppHarnessProd(AppHarness):
1048
955
  msg = "Frontend did not start"
1049
956
  raise RuntimeError(msg)
1050
957
 
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
958
+ def start(self) -> AppHarness:
959
+ """Start the app using reflex run subprocess.
1071
960
 
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)
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
1077
969
 
1078
970
  def stop(self):
1079
971
  """Stop the frontend python webserver."""
reflex/utils/console.py CHANGED
@@ -128,6 +128,21 @@ 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
+
131
146
  def print_to_log_file(msg: str, *, dedupe: bool = False, **kwargs):
132
147
  """Print a message to the log file.
133
148
 
@@ -328,6 +343,8 @@ def error(msg: str, *, dedupe: bool = False, **kwargs):
328
343
  print(f"[red]{msg}[/red]", **kwargs)
329
344
  if should_use_log_file_console():
330
345
  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)
331
348
 
332
349
 
333
350
  def ask(