reflex 0.5.0.post1__py3-none-any.whl → 0.5.1a1__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 (41) hide show
  1. reflex/.templates/web/utils/state.js +7 -2
  2. reflex/app.py +69 -50
  3. reflex/base.py +5 -2
  4. reflex/components/component.py +49 -13
  5. reflex/components/core/__init__.py +7 -1
  6. reflex/components/core/banner.py +79 -6
  7. reflex/components/core/banner.pyi +130 -0
  8. reflex/components/core/cond.py +1 -1
  9. reflex/components/core/debounce.py +2 -4
  10. reflex/components/core/foreach.py +11 -0
  11. reflex/components/core/upload.py +9 -10
  12. reflex/components/el/elements/forms.py +12 -6
  13. reflex/components/el/elements/media.py +19 -0
  14. reflex/components/el/elements/media.pyi +3 -1
  15. reflex/components/gridjs/datatable.py +4 -2
  16. reflex/components/props.py +30 -0
  17. reflex/components/radix/themes/components/tabs.py +1 -1
  18. reflex/components/sonner/toast.py +102 -35
  19. reflex/components/sonner/toast.pyi +27 -14
  20. reflex/config.py +5 -3
  21. reflex/constants/compiler.py +3 -3
  22. reflex/constants/installer.py +1 -1
  23. reflex/event.py +38 -24
  24. reflex/experimental/__init__.py +4 -0
  25. reflex/experimental/client_state.py +198 -0
  26. reflex/state.py +61 -21
  27. reflex/style.py +3 -3
  28. reflex/testing.py +28 -9
  29. reflex/utils/exceptions.py +64 -8
  30. reflex/utils/format.py +73 -2
  31. reflex/utils/prerequisites.py +28 -18
  32. reflex/utils/processes.py +34 -4
  33. reflex/utils/telemetry.py +5 -5
  34. reflex/utils/types.py +16 -0
  35. reflex/vars.py +104 -61
  36. reflex/vars.pyi +7 -6
  37. {reflex-0.5.0.post1.dist-info → reflex-0.5.1a1.dist-info}/METADATA +1 -1
  38. {reflex-0.5.0.post1.dist-info → reflex-0.5.1a1.dist-info}/RECORD +41 -39
  39. {reflex-0.5.0.post1.dist-info → reflex-0.5.1a1.dist-info}/LICENSE +0 -0
  40. {reflex-0.5.0.post1.dist-info → reflex-0.5.1a1.dist-info}/WHEEL +0 -0
  41. {reflex-0.5.0.post1.dist-info → reflex-0.5.1a1.dist-info}/entry_points.txt +0 -0
reflex/testing.py CHANGED
@@ -26,6 +26,7 @@ from typing import (
26
26
  AsyncIterator,
27
27
  Callable,
28
28
  Coroutine,
29
+ List,
29
30
  Optional,
30
31
  Type,
31
32
  TypeVar,
@@ -513,12 +514,19 @@ class AppHarness:
513
514
  raise TimeoutError("Backend is not listening.")
514
515
  return backend.servers[0].sockets[0]
515
516
 
516
- def frontend(self, driver_clz: Optional[Type["WebDriver"]] = None) -> "WebDriver":
517
+ def frontend(
518
+ self,
519
+ driver_clz: Optional[Type["WebDriver"]] = None,
520
+ driver_kwargs: dict[str, Any] | None = None,
521
+ driver_option_args: List[str] | None = None,
522
+ ) -> "WebDriver":
517
523
  """Get a selenium webdriver instance pointed at the app.
518
524
 
519
525
  Args:
520
526
  driver_clz: webdriver.Chrome (default), webdriver.Firefox, webdriver.Safari,
521
527
  webdriver.Edge, etc
528
+ driver_kwargs: additional keyword arguments to pass to the webdriver constructor
529
+ driver_option_args: additional arguments for the webdriver options
522
530
 
523
531
  Returns:
524
532
  Instance of the given webdriver navigated to the frontend url of the app.
@@ -541,19 +549,30 @@ class AppHarness:
541
549
  requested_driver = os.environ.get("APP_HARNESS_DRIVER", "Chrome")
542
550
  driver_clz = getattr(webdriver, requested_driver)
543
551
  options = getattr(webdriver, f"{requested_driver}Options")()
544
- if driver_clz is webdriver.Chrome and want_headless:
552
+ if driver_clz is webdriver.Chrome:
545
553
  options = webdriver.ChromeOptions()
546
- options.add_argument("--headless=new")
547
- elif driver_clz is webdriver.Firefox and want_headless:
554
+ options.add_argument("--class=AppHarness")
555
+ if want_headless:
556
+ options.add_argument("--headless=new")
557
+ elif driver_clz is webdriver.Firefox:
548
558
  options = webdriver.FirefoxOptions()
549
- options.add_argument("-headless")
550
- elif driver_clz is webdriver.Edge and want_headless:
559
+ if want_headless:
560
+ options.add_argument("-headless")
561
+ elif driver_clz is webdriver.Edge:
551
562
  options = webdriver.EdgeOptions()
552
- options.add_argument("headless")
553
- if options and (args := os.environ.get("APP_HARNESS_DRIVER_ARGS")):
563
+ if want_headless:
564
+ options.add_argument("headless")
565
+ if options is None:
566
+ raise RuntimeError(f"Could not determine options for {driver_clz}")
567
+ if args := os.environ.get("APP_HARNESS_DRIVER_ARGS"):
554
568
  for arg in args.split(","):
555
569
  options.add_argument(arg)
556
- driver = driver_clz(options=options) # type: ignore
570
+ if driver_option_args is not None:
571
+ for arg in driver_option_args:
572
+ options.add_argument(arg)
573
+ if driver_kwargs is None:
574
+ driver_kwargs = {}
575
+ driver = driver_clz(options=options, **driver_kwargs) # type: ignore
557
576
  driver.get(self.frontend_url)
558
577
  self._frontends.append(driver)
559
578
  return driver
@@ -1,21 +1,77 @@
1
1
  """Custom Exceptions."""
2
2
 
3
3
 
4
- class InvalidStylePropError(TypeError):
5
- """Custom Type Error when style props have invalid values."""
4
+ class ReflexError(Exception):
5
+ """Base exception for all Reflex exceptions."""
6
+
7
+
8
+ class ReflexRuntimeError(ReflexError, RuntimeError):
9
+ """Custom RuntimeError for Reflex."""
10
+
11
+
12
+ class UploadTypeError(ReflexError, TypeError):
13
+ """Custom TypeError for upload related errors."""
14
+
15
+
16
+ class EnvVarValueError(ReflexError, ValueError):
17
+ """Custom ValueError raised when unable to convert env var to expected type."""
18
+
19
+
20
+ class ComponentTypeError(ReflexError, TypeError):
21
+ """Custom TypeError for component related errors."""
22
+
23
+
24
+ class EventHandlerTypeError(ReflexError, TypeError):
25
+ """Custom TypeError for event handler related errors."""
26
+
27
+
28
+ class EventHandlerValueError(ReflexError, ValueError):
29
+ """Custom ValueError for event handler related errors."""
30
+
31
+
32
+ class StateValueError(ReflexError, ValueError):
33
+ """Custom ValueError for state related errors."""
34
+
6
35
 
7
- pass
36
+ class VarNameError(ReflexError, NameError):
37
+ """Custom NameError for when a state var has been shadowed by a substate var."""
8
38
 
9
39
 
10
- class ImmutableStateError(AttributeError):
40
+ class VarTypeError(ReflexError, TypeError):
41
+ """Custom TypeError for var related errors."""
42
+
43
+
44
+ class VarValueError(ReflexError, ValueError):
45
+ """Custom ValueError for var related errors."""
46
+
47
+
48
+ class VarAttributeError(ReflexError, AttributeError):
49
+ """Custom AttributeError for var related errors."""
50
+
51
+
52
+ class UploadValueError(ReflexError, ValueError):
53
+ """Custom ValueError for upload related errors."""
54
+
55
+
56
+ class RouteValueError(ReflexError, ValueError):
57
+ """Custom ValueError for route related errors."""
58
+
59
+
60
+ class VarOperationTypeError(ReflexError, TypeError):
61
+ """Custom TypeError for when unsupported operations are performed on vars."""
62
+
63
+
64
+ class InvalidStylePropError(ReflexError, TypeError):
65
+ """Custom Type Error when style props have invalid values."""
66
+
67
+
68
+ class ImmutableStateError(ReflexError):
11
69
  """Raised when a background task attempts to modify state outside of context."""
12
70
 
13
71
 
14
- class LockExpiredError(Exception):
72
+ class LockExpiredError(ReflexError):
15
73
  """Raised when the state lock expires while an event is being processed."""
16
74
 
17
75
 
18
- class MatchTypeError(TypeError):
76
+ class MatchTypeError(ReflexError, TypeError):
19
77
  """Raised when the return types of match cases are different."""
20
-
21
- pass
reflex/utils/format.py CHANGED
@@ -6,7 +6,7 @@ import inspect
6
6
  import json
7
7
  import os
8
8
  import re
9
- from typing import TYPE_CHECKING, Any, List, Optional, Union
9
+ from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union
10
10
 
11
11
  from reflex import constants
12
12
  from reflex.utils import exceptions, serializers, types
@@ -15,7 +15,7 @@ from reflex.vars import BaseVar, Var
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from reflex.components.component import ComponentStyle
18
- from reflex.event import EventChain, EventHandler, EventSpec
18
+ from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec
19
19
 
20
20
  WRAP_MAP = {
21
21
  "{": "}",
@@ -590,6 +590,77 @@ def format_event_chain(
590
590
  )
591
591
 
592
592
 
593
+ def format_queue_events(
594
+ events: EventSpec
595
+ | EventHandler
596
+ | Callable
597
+ | List[EventSpec | EventHandler | Callable]
598
+ | None = None,
599
+ args_spec: Optional[ArgsSpec] = None,
600
+ ) -> Var[EventChain]:
601
+ """Format a list of event handler / event spec as a javascript callback.
602
+
603
+ The resulting code can be passed to interfaces that expect a callback
604
+ function and when triggered it will directly call queueEvents.
605
+
606
+ It is intended to be executed in the rx.call_script context, where some
607
+ existing API needs a callback to trigger a backend event handler.
608
+
609
+ Args:
610
+ events: The events to queue.
611
+ args_spec: The argument spec for the callback.
612
+
613
+ Returns:
614
+ The compiled javascript callback to queue the given events on the frontend.
615
+ """
616
+ from reflex.event import (
617
+ EventChain,
618
+ EventHandler,
619
+ EventSpec,
620
+ call_event_fn,
621
+ call_event_handler,
622
+ )
623
+
624
+ if not events:
625
+ return Var.create_safe(
626
+ "() => null", _var_is_string=False, _var_is_local=False
627
+ ).to(EventChain)
628
+
629
+ # If no spec is provided, the function will take no arguments.
630
+ def _default_args_spec():
631
+ return []
632
+
633
+ # Construct the arguments that the function accepts.
634
+ sig = inspect.signature(args_spec or _default_args_spec) # type: ignore
635
+ if sig.parameters:
636
+ arg_def = ",".join(f"_{p}" for p in sig.parameters)
637
+ arg_def = f"({arg_def})"
638
+ else:
639
+ arg_def = "()"
640
+
641
+ payloads = []
642
+ if not isinstance(events, list):
643
+ events = [events]
644
+
645
+ # Process each event/spec/lambda (similar to Component._create_event_chain).
646
+ for spec in events:
647
+ specs: list[EventSpec] = []
648
+ if isinstance(spec, (EventHandler, EventSpec)):
649
+ specs = [call_event_handler(spec, args_spec or _default_args_spec)]
650
+ elif isinstance(spec, type(lambda: None)):
651
+ specs = call_event_fn(spec, args_spec or _default_args_spec)
652
+ payloads.extend(format_event(s) for s in specs)
653
+
654
+ # Return the final code snippet, expecting queueEvents, processEvent, and socket to be in scope.
655
+ # Typically this snippet will _only_ run from within an rx.call_script eval context.
656
+ return Var.create_safe(
657
+ f"{arg_def} => {{queueEvents([{','.join(payloads)}], {constants.CompileVars.SOCKET}); "
658
+ f"processEvent({constants.CompileVars.SOCKET})}}",
659
+ _var_is_string=False,
660
+ _var_is_local=False,
661
+ ).to(EventChain)
662
+
663
+
593
664
  def format_query_params(router_data: dict[str, Any]) -> dict[str, str]:
594
665
  """Convert back query params name to python-friendly case.
595
666
 
@@ -233,28 +233,35 @@ def get_app(reload: bool = False) -> ModuleType:
233
233
 
234
234
  Raises:
235
235
  RuntimeError: If the app name is not set in the config.
236
+ exceptions.ReflexError: Reflex specific errors.
236
237
  """
237
- os.environ[constants.RELOAD_CONFIG] = str(reload)
238
- config = get_config()
239
- if not config.app_name:
240
- raise RuntimeError(
241
- "Cannot get the app module because `app_name` is not set in rxconfig! "
242
- "If this error occurs in a reflex test case, ensure that `get_app` is mocked."
243
- )
244
- module = config.module
245
- sys.path.insert(0, os.getcwd())
246
- app = __import__(module, fromlist=(constants.CompileVars.APP,))
238
+ from reflex.utils import exceptions, telemetry
239
+
240
+ try:
241
+ os.environ[constants.RELOAD_CONFIG] = str(reload)
242
+ config = get_config()
243
+ if not config.app_name:
244
+ raise RuntimeError(
245
+ "Cannot get the app module because `app_name` is not set in rxconfig! "
246
+ "If this error occurs in a reflex test case, ensure that `get_app` is mocked."
247
+ )
248
+ module = config.module
249
+ sys.path.insert(0, os.getcwd())
250
+ app = __import__(module, fromlist=(constants.CompileVars.APP,))
247
251
 
248
- if reload:
249
- from reflex.state import reload_state_module
252
+ if reload:
253
+ from reflex.state import reload_state_module
250
254
 
251
- # Reset rx.State subclasses to avoid conflict when reloading.
252
- reload_state_module(module=module)
255
+ # Reset rx.State subclasses to avoid conflict when reloading.
256
+ reload_state_module(module=module)
253
257
 
254
- # Reload the app module.
255
- importlib.reload(app)
258
+ # Reload the app module.
259
+ importlib.reload(app)
256
260
 
257
- return app
261
+ return app
262
+ except exceptions.ReflexError as ex:
263
+ telemetry.send("error", context="frontend", detail=str(ex))
264
+ raise
258
265
 
259
266
 
260
267
  def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
@@ -406,7 +413,7 @@ def initialize_gitignore(
406
413
  # Write files to the .gitignore file.
407
414
  with open(gitignore_file, "w", newline="\n") as f:
408
415
  console.debug(f"Creating {gitignore_file}")
409
- f.write(f"{(path_ops.join(sorted(files_to_ignore))).lstrip()}")
416
+ f.write(f"{(path_ops.join(sorted(files_to_ignore))).lstrip()}\n")
410
417
 
411
418
 
412
419
  def initialize_requirements_txt():
@@ -879,6 +886,7 @@ def install_frontend_packages(packages: set[str], config: Config):
879
886
  processes.run_process_with_fallback(
880
887
  [get_install_package_manager(), "install"], # type: ignore
881
888
  fallback=fallback_command,
889
+ analytics_enabled=True,
882
890
  show_status_message="Installing base frontend packages",
883
891
  cwd=constants.Dirs.WEB,
884
892
  shell=constants.IS_WINDOWS,
@@ -894,6 +902,7 @@ def install_frontend_packages(packages: set[str], config: Config):
894
902
  *((config.tailwind or {}).get("plugins", [])),
895
903
  ],
896
904
  fallback=fallback_command,
905
+ analytics_enabled=True,
897
906
  show_status_message="Installing tailwind",
898
907
  cwd=constants.Dirs.WEB,
899
908
  shell=constants.IS_WINDOWS,
@@ -904,6 +913,7 @@ def install_frontend_packages(packages: set[str], config: Config):
904
913
  processes.run_process_with_fallback(
905
914
  [get_install_package_manager(), "add", *packages],
906
915
  fallback=fallback_command,
916
+ analytics_enabled=True,
907
917
  show_status_message="Installing frontend packages from config and components",
908
918
  cwd=constants.Dirs.WEB,
909
919
  shell=constants.IS_WINDOWS,
reflex/utils/processes.py CHANGED
@@ -211,6 +211,7 @@ def stream_logs(
211
211
  process: subprocess.Popen,
212
212
  progress=None,
213
213
  suppress_errors: bool = False,
214
+ analytics_enabled: bool = False,
214
215
  ):
215
216
  """Stream the logs for a process.
216
217
 
@@ -219,6 +220,7 @@ def stream_logs(
219
220
  process: The process.
220
221
  progress: The ongoing progress bar if one is being used.
221
222
  suppress_errors: If True, do not exit if errors are encountered (for fallback).
223
+ analytics_enabled: Whether analytics are enabled for this command.
222
224
 
223
225
  Yields:
224
226
  The lines of the process output.
@@ -226,6 +228,8 @@ def stream_logs(
226
228
  Raises:
227
229
  Exit: If the process failed.
228
230
  """
231
+ from reflex.utils import telemetry
232
+
229
233
  # Store the tail of the logs.
230
234
  logs = collections.deque(maxlen=512)
231
235
  with process:
@@ -246,6 +250,8 @@ def stream_logs(
246
250
  console.error(f"{message} failed with exit code {process.returncode}")
247
251
  for line in logs:
248
252
  console.error(line, end="")
253
+ if analytics_enabled:
254
+ telemetry.send("error", context=message)
249
255
  console.error("Run with [bold]--loglevel debug [/bold] for the full log.")
250
256
  raise typer.Exit(1)
251
257
 
@@ -261,16 +267,27 @@ def show_logs(message: str, process: subprocess.Popen):
261
267
  pass
262
268
 
263
269
 
264
- def show_status(message: str, process: subprocess.Popen, suppress_errors: bool = False):
270
+ def show_status(
271
+ message: str,
272
+ process: subprocess.Popen,
273
+ suppress_errors: bool = False,
274
+ analytics_enabled: bool = False,
275
+ ):
265
276
  """Show the status of a process.
266
277
 
267
278
  Args:
268
279
  message: The initial message to display.
269
280
  process: The process.
270
281
  suppress_errors: If True, do not exit if errors are encountered (for fallback).
282
+ analytics_enabled: Whether analytics are enabled for this command.
271
283
  """
272
284
  with console.status(message) as status:
273
- for line in stream_logs(message, process, suppress_errors=suppress_errors):
285
+ for line in stream_logs(
286
+ message,
287
+ process,
288
+ suppress_errors=suppress_errors,
289
+ analytics_enabled=analytics_enabled,
290
+ ):
274
291
  status.update(f"{message} {line}")
275
292
 
276
293
 
@@ -319,19 +336,31 @@ def get_command_with_loglevel(command: list[str]) -> list[str]:
319
336
  return command
320
337
 
321
338
 
322
- def run_process_with_fallback(args, *, show_status_message, fallback=None, **kwargs):
339
+ def run_process_with_fallback(
340
+ args,
341
+ *,
342
+ show_status_message,
343
+ fallback=None,
344
+ analytics_enabled: bool = False,
345
+ **kwargs,
346
+ ):
323
347
  """Run subprocess and retry using fallback command if initial command fails.
324
348
 
325
349
  Args:
326
350
  args: A string, or a sequence of program arguments.
327
351
  show_status_message: The status message to be displayed in the console.
328
352
  fallback: The fallback command to run.
353
+ analytics_enabled: Whether analytics are enabled for this command.
329
354
  kwargs: Kwargs to pass to new_process function.
330
355
  """
331
356
  process = new_process(get_command_with_loglevel(args), **kwargs)
332
357
  if fallback is None:
333
358
  # No fallback given, or this _is_ the fallback command.
334
- show_status(show_status_message, process)
359
+ show_status(
360
+ show_status_message,
361
+ process,
362
+ analytics_enabled=analytics_enabled,
363
+ )
335
364
  else:
336
365
  # Suppress errors for initial command, because we will try to fallback
337
366
  show_status(show_status_message, process, suppress_errors=True)
@@ -345,6 +374,7 @@ def run_process_with_fallback(args, *, show_status_message, fallback=None, **kwa
345
374
  fallback_args,
346
375
  show_status_message=show_status_message,
347
376
  fallback=None,
377
+ analytics_enabled=analytics_enabled,
348
378
  **kwargs,
349
379
  )
350
380
 
reflex/utils/telemetry.py CHANGED
@@ -126,6 +126,10 @@ def _prepare_event(event: str, **kwargs) -> dict:
126
126
 
127
127
  cpuinfo = get_cpu_info()
128
128
 
129
+ additional_keys = ["template", "context", "detail"]
130
+ additional_fields = {
131
+ key: value for key in additional_keys if (value := kwargs.get(key)) is not None
132
+ }
129
133
  return {
130
134
  "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
131
135
  "event": event,
@@ -139,11 +143,7 @@ def _prepare_event(event: str, **kwargs) -> dict:
139
143
  "cpu_count": get_cpu_count(),
140
144
  "memory": get_memory(),
141
145
  "cpu_info": dict(cpuinfo) if cpuinfo else {},
142
- **(
143
- {"template": template}
144
- if (template := kwargs.get("template")) is not None
145
- else {}
146
- ),
146
+ **additional_fields,
147
147
  },
148
148
  "timestamp": stamp,
149
149
  }
reflex/utils/types.py CHANGED
@@ -44,6 +44,22 @@ from reflex import constants
44
44
  from reflex.base import Base
45
45
  from reflex.utils import console, serializers
46
46
 
47
+ if sys.version_info >= (3, 12):
48
+ from typing import override
49
+ else:
50
+
51
+ def override(func: Callable) -> Callable:
52
+ """Fallback for @override decorator.
53
+
54
+ Args:
55
+ func: The function to decorate.
56
+
57
+ Returns:
58
+ The unmodified function.
59
+ """
60
+ return func
61
+
62
+
47
63
  # Potential GenericAlias types for isinstance checks.
48
64
  GenericAliasTypes = [_GenericAlias]
49
65