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.
- reflex/.templates/web/utils/state.js +7 -2
- reflex/__init__.py +1 -0
- reflex/__init__.pyi +2 -0
- reflex/app.py +8 -11
- reflex/compiler/compiler.py +10 -38
- reflex/components/base/error_boundary.py +6 -5
- reflex/components/core/__init__.py +1 -0
- reflex/components/core/__init__.pyi +3 -0
- reflex/components/core/window_events.py +104 -0
- reflex/components/core/window_events.pyi +84 -0
- reflex/components/el/__init__.pyi +4 -0
- reflex/components/el/elements/__init__.py +1 -0
- reflex/components/el/elements/__init__.pyi +5 -0
- reflex/components/el/elements/forms.py +4 -2
- reflex/components/el/elements/typography.py +7 -0
- reflex/components/el/elements/typography.pyi +246 -0
- reflex/components/lucide/icon.py +303 -292
- reflex/components/lucide/icon.pyi +303 -292
- reflex/components/recharts/recharts.py +2 -2
- reflex/components/sonner/toast.py +1 -1
- reflex/config.py +3 -3
- reflex/constants/installer.py +2 -1
- reflex/environment.py +3 -0
- reflex/event.py +69 -8
- reflex/model.py +55 -0
- reflex/reflex.py +33 -0
- reflex/state.py +9 -4
- reflex/testing.py +180 -288
- reflex/utils/console.py +17 -0
- reflex/utils/exec.py +22 -5
- reflex/utils/processes.py +28 -38
- reflex/utils/types.py +1 -1
- {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/METADATA +1 -1
- {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/RECORD +37 -35
- {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/WHEEL +0 -0
- {reflex-0.8.2a1.dist-info → reflex-0.8.3a2.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
+
if backend and self.backend_port is None:
|
|
386
|
+
msg = "Backend port is not set."
|
|
387
|
+
raise RuntimeError(msg)
|
|
332
388
|
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
if
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
+
AssertionError: when the state manager is not initialized
|
|
704
667
|
"""
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
|
1052
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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(
|