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

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
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import contextlib
6
+ import datetime
6
7
  import inspect
7
8
  import os
8
9
  import shutil
@@ -31,7 +32,7 @@ _EMITTED_DEPRECATION_WARNINGS = set()
31
32
  _EMITTED_INFO = set()
32
33
 
33
34
  # Warnings which have been printed.
34
- _EMIITED_WARNINGS = set()
35
+ _EMITTED_WARNINGS = set()
35
36
 
36
37
  # Errors which have been printed.
37
38
  _EMITTED_ERRORS = set()
@@ -128,21 +129,6 @@ def should_use_log_file_console() -> bool:
128
129
  return environment.REFLEX_ENABLE_FULL_LOGGING.get()
129
130
 
130
131
 
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
132
  def print_to_log_file(msg: str, *, dedupe: bool = False, **kwargs):
147
133
  """Print a message to the log file.
148
134
 
@@ -151,7 +137,7 @@ def print_to_log_file(msg: str, *, dedupe: bool = False, **kwargs):
151
137
  dedupe: If True, suppress multiple console logs of print message.
152
138
  kwargs: Keyword arguments to pass to the print function.
153
139
  """
154
- log_file_console().print(msg, **kwargs)
140
+ log_file_console().print(f"[{datetime.datetime.now()}] {msg}", **kwargs)
155
141
 
156
142
 
157
143
  def debug(msg: str, *, dedupe: bool = False, **kwargs):
@@ -250,9 +236,9 @@ def warn(msg: str, *, dedupe: bool = False, **kwargs):
250
236
  """
251
237
  if _LOG_LEVEL <= LogLevel.WARNING:
252
238
  if dedupe:
253
- if msg in _EMIITED_WARNINGS:
239
+ if msg in _EMITTED_WARNINGS:
254
240
  return
255
- _EMIITED_WARNINGS.add(msg)
241
+ _EMITTED_WARNINGS.add(msg)
256
242
  print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
257
243
  if should_use_log_file_console():
258
244
  print_to_log_file(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
@@ -343,8 +329,6 @@ def error(msg: str, *, dedupe: bool = False, **kwargs):
343
329
  print(f"[red]{msg}[/red]", **kwargs)
344
330
  if should_use_log_file_console():
345
331
  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
332
 
349
333
 
350
334
  def ask(