reflex 0.8.2__py3-none-any.whl → 0.8.3a1__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/base/meta.pyi +4 -256
- 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/metadata.pyi +2 -0
- 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/constants/installer.py +1 -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 +164 -287
- 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.2.dist-info → reflex-0.8.3a1.dist-info}/METADATA +1 -1
- {reflex-0.8.2.dist-info → reflex-0.8.3a1.dist-info}/RECORD +38 -36
- {reflex-0.8.2.dist-info → reflex-0.8.3a1.dist-info}/WHEEL +0 -0
- {reflex-0.8.2.dist-info → reflex-0.8.3a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.2.dist-info → reflex-0.8.3a1.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,16 @@ 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
|
|
27
|
+
import psutil
|
|
28
28
|
|
|
29
29
|
import reflex
|
|
30
|
-
import reflex.environment
|
|
31
30
|
import reflex.reflex
|
|
32
|
-
import reflex.utils.build
|
|
33
31
|
import reflex.utils.exec
|
|
34
32
|
import reflex.utils.format
|
|
35
33
|
import reflex.utils.prerequisites
|
|
@@ -45,9 +43,7 @@ from reflex.state import (
|
|
|
45
43
|
StateManagerRedis,
|
|
46
44
|
reload_state_module,
|
|
47
45
|
)
|
|
48
|
-
from reflex.utils import console
|
|
49
46
|
from reflex.utils.export import export
|
|
50
|
-
from reflex.utils.types import ASGIApp
|
|
51
47
|
|
|
52
48
|
try:
|
|
53
49
|
from selenium import webdriver
|
|
@@ -101,6 +97,14 @@ class chdir(contextlib.AbstractContextManager): # noqa: N801
|
|
|
101
97
|
os.chdir(self._old_cwd.pop())
|
|
102
98
|
|
|
103
99
|
|
|
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
|
+
|
|
104
108
|
@dataclasses.dataclass
|
|
105
109
|
class AppHarness:
|
|
106
110
|
"""AppHarness executes a reflex app in-process for testing."""
|
|
@@ -113,14 +117,15 @@ class AppHarness:
|
|
|
113
117
|
app_module_path: Path
|
|
114
118
|
app_module: types.ModuleType | None = None
|
|
115
119
|
app_instance: reflex.App | None = None
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
reflex_process: subprocess.Popen | None = None
|
|
121
|
+
reflex_process_log_path: Path | None = None
|
|
122
|
+
reflex_process_error_log_path: Path | None = None
|
|
118
123
|
frontend_url: str | None = None
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
backend: uvicorn.Server | None = None
|
|
124
|
+
backend_port: int | None = None
|
|
125
|
+
frontend_port: int | None = None
|
|
122
126
|
state_manager: StateManager | None = None
|
|
123
127
|
_frontends: list[WebDriver] = dataclasses.field(default_factory=list)
|
|
128
|
+
_reflex_process_log_fn: TextIOWrapper | None = None
|
|
124
129
|
|
|
125
130
|
@classmethod
|
|
126
131
|
def create(
|
|
@@ -273,82 +278,117 @@ class AppHarness:
|
|
|
273
278
|
# Ensure the AppHarness test does not skip State assignment due to running via pytest
|
|
274
279
|
os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None)
|
|
275
280
|
os.environ[reflex.constants.APP_HARNESS_FLAG] = "true"
|
|
276
|
-
# Ensure we actually compile the app during first initialization.
|
|
277
281
|
self.app_instance, self.app_module = (
|
|
278
282
|
reflex.utils.prerequisites.get_and_validate_app(
|
|
279
283
|
# Do not reload the module for pre-existing apps (only apps generated from source)
|
|
280
284
|
reload=self.app_source is not None
|
|
281
285
|
)
|
|
282
286
|
)
|
|
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
|
|
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"
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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.
|
|
305
305
|
|
|
306
|
-
|
|
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)
|
|
307
354
|
|
|
308
|
-
|
|
309
|
-
|
|
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()
|
|
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.
|
|
315
357
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
await self.app_instance.sio.shutdown()
|
|
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.
|
|
320
361
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
await async_engine.dispose()
|
|
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)
|
|
328
368
|
|
|
329
|
-
|
|
369
|
+
frontend_ready = False
|
|
370
|
+
backend_ready = False
|
|
371
|
+
timeout = 30
|
|
372
|
+
start_time = time.time()
|
|
330
373
|
|
|
331
|
-
|
|
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)
|
|
332
379
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
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
|
|
352
392
|
|
|
353
393
|
async def _reset_backend_state_manager(self):
|
|
354
394
|
"""Reset the StateManagerRedis event loop affinity.
|
|
@@ -376,78 +416,14 @@ class AppHarness:
|
|
|
376
416
|
msg = "Failed to reset state manager."
|
|
377
417
|
raise RuntimeError(msg)
|
|
378
418
|
|
|
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
419
|
def start(self) -> AppHarness:
|
|
442
|
-
"""Start the
|
|
420
|
+
"""Start the app using reflex run subprocess.
|
|
443
421
|
|
|
444
422
|
Returns:
|
|
445
423
|
self
|
|
446
424
|
"""
|
|
447
425
|
self._initialize_app()
|
|
448
|
-
self.
|
|
449
|
-
self._start_frontend()
|
|
450
|
-
self._wait_frontend()
|
|
426
|
+
self._start_subprocess()
|
|
451
427
|
return self
|
|
452
428
|
|
|
453
429
|
@staticmethod
|
|
@@ -476,43 +452,48 @@ class AppHarness:
|
|
|
476
452
|
return self.start()
|
|
477
453
|
|
|
478
454
|
def stop(self) -> None:
|
|
479
|
-
"""Stop the
|
|
480
|
-
import psutil
|
|
481
|
-
|
|
482
|
-
# Quit browsers first to avoid any lingering events being sent during shutdown.
|
|
455
|
+
"""Stop the reflex subprocess."""
|
|
483
456
|
for driver in self._frontends:
|
|
484
457
|
driver.quit()
|
|
485
458
|
|
|
459
|
+
self._stop_reflex()
|
|
486
460
|
self._reload_state_module()
|
|
487
461
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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."
|
|
494
495
|
)
|
|
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()
|
|
496
|
+
raise ReflexProcessExitNonZeroError(msg)
|
|
516
497
|
|
|
517
498
|
def __exit__(self, *excinfo) -> None:
|
|
518
499
|
"""Contextmanager protocol for `stop()`.
|
|
@@ -581,39 +562,6 @@ class AppHarness:
|
|
|
581
562
|
await asyncio.sleep(step)
|
|
582
563
|
return False
|
|
583
564
|
|
|
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
565
|
def frontend(
|
|
618
566
|
self,
|
|
619
567
|
driver_clz: type[WebDriver] | None = None,
|
|
@@ -700,71 +648,18 @@ class AppHarness:
|
|
|
700
648
|
The state instance associated with the given token
|
|
701
649
|
|
|
702
650
|
Raises:
|
|
703
|
-
|
|
651
|
+
AssertionError: when the state manager is not initialized
|
|
704
652
|
"""
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
708
656
|
try:
|
|
709
657
|
return await self.state_manager.get_state(token)
|
|
710
658
|
finally:
|
|
659
|
+
await self._reset_backend_state_manager()
|
|
711
660
|
if isinstance(self.state_manager, StateManagerRedis):
|
|
712
661
|
await self.state_manager.close()
|
|
713
662
|
|
|
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
663
|
def poll_for_content(
|
|
769
664
|
self,
|
|
770
665
|
element: WebElement,
|
|
@@ -1007,7 +902,7 @@ class AppHarnessProd(AppHarness):
|
|
|
1007
902
|
root=web_root,
|
|
1008
903
|
error_page_map=error_page_map,
|
|
1009
904
|
) as self.frontend_server:
|
|
1010
|
-
self.frontend_url = "http://localhost:{1}".format(
|
|
905
|
+
self.frontend_url = "http://localhost:{1}/".format(
|
|
1011
906
|
*self.frontend_server.socket.getsockname()
|
|
1012
907
|
)
|
|
1013
908
|
self.frontend_server.serve_forever()
|
|
@@ -1016,10 +911,7 @@ class AppHarnessProd(AppHarness):
|
|
|
1016
911
|
# Set up the frontend.
|
|
1017
912
|
with chdir(self.app_path):
|
|
1018
913
|
config = reflex.config.get_config()
|
|
1019
|
-
|
|
1020
|
-
config.api_url = "http://{}:{}".format(
|
|
1021
|
-
*self._poll_for_servers(timeout=30).getsockname(),
|
|
1022
|
-
)
|
|
914
|
+
config.api_url = f"http://localhost:{self.backend_port}"
|
|
1023
915
|
print("Building frontend...") # for pytest diagnosis #noqa: T201
|
|
1024
916
|
|
|
1025
917
|
get_config().loglevel = reflex.constants.LogLevel.INFO
|
|
@@ -1048,32 +940,17 @@ class AppHarnessProd(AppHarness):
|
|
|
1048
940
|
msg = "Frontend did not start"
|
|
1049
941
|
raise RuntimeError(msg)
|
|
1050
942
|
|
|
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
|
|
943
|
+
def start(self) -> AppHarness:
|
|
944
|
+
"""Start the app using reflex run subprocess.
|
|
1071
945
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
|
1077
954
|
|
|
1078
955
|
def stop(self):
|
|
1079
956
|
"""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(
|