reflex 0.5.0.post1__py3-none-any.whl → 0.5.1__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 (42) hide show
  1. reflex/.templates/web/utils/state.js +7 -2
  2. reflex/app.py +68 -50
  3. reflex/app_module_for_backend.py +3 -0
  4. reflex/base.py +5 -2
  5. reflex/components/component.py +49 -13
  6. reflex/components/core/__init__.py +7 -1
  7. reflex/components/core/banner.py +79 -6
  8. reflex/components/core/banner.pyi +130 -0
  9. reflex/components/core/cond.py +10 -4
  10. reflex/components/core/debounce.py +2 -4
  11. reflex/components/core/foreach.py +11 -0
  12. reflex/components/core/upload.py +9 -10
  13. reflex/components/el/elements/forms.py +12 -6
  14. reflex/components/el/elements/media.py +19 -0
  15. reflex/components/el/elements/media.pyi +3 -1
  16. reflex/components/gridjs/datatable.py +4 -2
  17. reflex/components/props.py +30 -0
  18. reflex/components/radix/themes/components/tabs.py +1 -1
  19. reflex/components/sonner/toast.py +102 -35
  20. reflex/components/sonner/toast.pyi +27 -14
  21. reflex/config.py +5 -3
  22. reflex/constants/compiler.py +3 -3
  23. reflex/constants/installer.py +1 -1
  24. reflex/event.py +38 -24
  25. reflex/experimental/__init__.py +4 -0
  26. reflex/experimental/client_state.py +198 -0
  27. reflex/state.py +59 -21
  28. reflex/style.py +3 -3
  29. reflex/testing.py +28 -9
  30. reflex/utils/exceptions.py +64 -8
  31. reflex/utils/format.py +73 -2
  32. reflex/utils/prerequisites.py +27 -18
  33. reflex/utils/processes.py +34 -4
  34. reflex/utils/telemetry.py +42 -16
  35. reflex/utils/types.py +16 -0
  36. reflex/vars.py +104 -61
  37. reflex/vars.pyi +7 -6
  38. {reflex-0.5.0.post1.dist-info → reflex-0.5.1.dist-info}/METADATA +1 -1
  39. {reflex-0.5.0.post1.dist-info → reflex-0.5.1.dist-info}/RECORD +42 -40
  40. {reflex-0.5.0.post1.dist-info → reflex-0.5.1.dist-info}/LICENSE +0 -0
  41. {reflex-0.5.0.post1.dist-info → reflex-0.5.1.dist-info}/WHEEL +0 -0
  42. {reflex-0.5.0.post1.dist-info → reflex-0.5.1.dist-info}/entry_points.txt +0 -0
reflex/state.py CHANGED
@@ -279,11 +279,13 @@ class EventHandlerSetVar(EventHandler):
279
279
 
280
280
  Raises:
281
281
  AttributeError: If the given Var name does not exist on the state.
282
- ValueError: If the given Var name is not a str
282
+ EventHandlerValueError: If the given Var name is not a str
283
283
  """
284
+ from reflex.utils.exceptions import EventHandlerValueError
285
+
284
286
  if args:
285
287
  if not isinstance(args[0], str):
286
- raise ValueError(
288
+ raise EventHandlerValueError(
287
289
  f"Var name must be passed as a string, got {args[0]!r}"
288
290
  )
289
291
  # Check that the requested Var setter exists on the State at compile time.
@@ -357,6 +359,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
357
359
  # Whether the state has ever been touched since instantiation.
358
360
  _was_touched: bool = False
359
361
 
362
+ # Whether this state class is a mixin and should not be instantiated.
363
+ _mixin: ClassVar[bool] = False
364
+
360
365
  # A special event handler for setting base vars.
361
366
  setvar: ClassVar[EventHandler]
362
367
 
@@ -380,10 +385,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
380
385
  **kwargs: The kwargs to pass to the Pydantic init method.
381
386
 
382
387
  Raises:
383
- RuntimeError: If the state is instantiated directly by end user.
388
+ ReflexRuntimeError: If the state is instantiated directly by end user.
384
389
  """
390
+ from reflex.utils.exceptions import ReflexRuntimeError
391
+
385
392
  if not _reflex_internal_init and not is_testing_env():
386
- raise RuntimeError(
393
+ raise ReflexRuntimeError(
387
394
  "State classes should not be instantiated directly in a Reflex app. "
388
395
  "See https://reflex.dev/docs/state/ for further information."
389
396
  )
@@ -424,23 +431,30 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
424
431
  """
425
432
  return [
426
433
  v
427
- for mixin in cls.__mro__
428
- if mixin is cls or not issubclass(mixin, (BaseState, ABC))
434
+ for mixin in cls._mixins() + [cls]
429
435
  for v in mixin.__dict__.values()
430
436
  if isinstance(v, ComputedVar)
431
437
  ]
432
438
 
433
439
  @classmethod
434
- def __init_subclass__(cls, **kwargs):
440
+ def __init_subclass__(cls, mixin: bool = False, **kwargs):
435
441
  """Do some magic for the subclass initialization.
436
442
 
437
443
  Args:
444
+ mixin: Whether the subclass is a mixin and should not be initialized.
438
445
  **kwargs: The kwargs to pass to the pydantic init_subclass method.
439
446
 
440
447
  Raises:
441
- ValueError: If a substate class shadows another.
448
+ StateValueError: If a substate class shadows another.
442
449
  """
450
+ from reflex.utils.exceptions import StateValueError
451
+
443
452
  super().__init_subclass__(**kwargs)
453
+
454
+ cls._mixin = mixin
455
+ if mixin:
456
+ return
457
+
444
458
  # Event handlers should not shadow builtin state methods.
445
459
  cls._check_overridden_methods()
446
460
  # Computed vars should not shadow builtin state props.
@@ -471,7 +485,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
471
485
  else:
472
486
  # During normal operation, subclasses cannot have the same name, even if they are
473
487
  # defined in different modules.
474
- raise ValueError(
488
+ raise StateValueError(
475
489
  f"The substate class '{cls.__name__}' has been defined multiple times. "
476
490
  "Shadowing substate classes is not allowed."
477
491
  )
@@ -536,7 +550,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
536
550
  for name, value in mixin.__dict__.items():
537
551
  if isinstance(value, ComputedVar):
538
552
  fget = cls._copy_fn(value.fget)
539
- newcv = ComputedVar(fget=fget, _var_name=value._var_name)
553
+ newcv = value._replace(fget=fget)
554
+ # cleanup refs to mixin cls in var_data
555
+ newcv._var_data = None
540
556
  newcv._var_set_state(cls)
541
557
  setattr(cls, name, newcv)
542
558
  cls.computed_vars[newcv._var_name] = newcv
@@ -612,8 +628,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
612
628
  return [
613
629
  mixin
614
630
  for mixin in cls.__mro__
615
- if not issubclass(mixin, (BaseState, ABC))
616
- and mixin not in [pydantic.BaseModel, Base]
631
+ if (
632
+ mixin not in [pydantic.BaseModel, Base, cls]
633
+ and issubclass(mixin, BaseState)
634
+ and mixin._mixin is True
635
+ )
617
636
  ]
618
637
 
619
638
  @classmethod
@@ -736,7 +755,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
736
755
  parent_states = [
737
756
  base
738
757
  for base in cls.__bases__
739
- if types._issubclass(base, BaseState) and base is not BaseState
758
+ if issubclass(base, BaseState) and base is not BaseState and not base._mixin
740
759
  ]
741
760
  assert len(parent_states) < 2, "Only one parent state is allowed."
742
761
  return parent_states[0] if len(parent_states) == 1 else None # type: ignore
@@ -829,10 +848,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
829
848
  prop: The variable to initialize
830
849
 
831
850
  Raises:
832
- TypeError: if the variable has an incorrect type
851
+ VarTypeError: if the variable has an incorrect type
833
852
  """
853
+ from reflex.utils.exceptions import VarTypeError
854
+
834
855
  if not types.is_valid_var_type(prop._var_type):
835
- raise TypeError(
856
+ raise VarTypeError(
836
857
  "State vars must be primitive Python types, "
837
858
  "Plotly figures, Pandas dataframes, "
838
859
  "or subclasses of rx.Base. "
@@ -1454,6 +1475,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1454
1475
  Yields:
1455
1476
  StateUpdate object
1456
1477
  """
1478
+ from reflex.utils import telemetry
1479
+
1457
1480
  # Get the function to process the event.
1458
1481
  fn = functools.partial(handler.fn, state)
1459
1482
 
@@ -1489,9 +1512,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1489
1512
  yield state._as_state_update(handler, events, final=True)
1490
1513
 
1491
1514
  # If an error occurs, throw a window alert.
1492
- except Exception:
1515
+ except Exception as ex:
1493
1516
  error = traceback.format_exc()
1494
1517
  print(error)
1518
+ telemetry.send_error(ex, context="backend")
1495
1519
  yield state._as_state_update(
1496
1520
  handler,
1497
1521
  window_alert("An error occurred. See logs for details."),
@@ -1688,10 +1712,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1688
1712
  if initial:
1689
1713
  computed_vars = {
1690
1714
  # Include initial computed vars.
1691
- prop_name: cv._initial_value
1692
- if isinstance(cv, ComputedVar)
1693
- and not isinstance(cv._initial_value, types.Unset)
1694
- else self.get_value(getattr(self, prop_name))
1715
+ prop_name: (
1716
+ cv._initial_value
1717
+ if isinstance(cv, ComputedVar)
1718
+ and not isinstance(cv._initial_value, types.Unset)
1719
+ else self.get_value(getattr(self, prop_name))
1720
+ )
1695
1721
  for prop_name, cv in self.computed_vars.items()
1696
1722
  }
1697
1723
  elif include_computed:
@@ -1818,7 +1844,7 @@ class OnLoadInternalState(State):
1818
1844
  ]
1819
1845
 
1820
1846
 
1821
- class ComponentState(Base):
1847
+ class ComponentState(State, mixin=True):
1822
1848
  """Base class to allow for the creation of a state instance per component.
1823
1849
 
1824
1850
  This allows for the bundling of UI and state logic into a single class,
@@ -1860,6 +1886,18 @@ class ComponentState(Base):
1860
1886
  # The number of components created from this class.
1861
1887
  _per_component_state_instance_count: ClassVar[int] = 0
1862
1888
 
1889
+ @classmethod
1890
+ def __init_subclass__(cls, mixin: bool = False, **kwargs):
1891
+ """Overwrite mixin default to True.
1892
+
1893
+ Args:
1894
+ mixin: Whether the subclass is a mixin and should not be initialized.
1895
+ **kwargs: The kwargs to pass to the pydantic init_subclass method.
1896
+ """
1897
+ if ComponentState in cls.__bases__:
1898
+ mixin = True
1899
+ super().__init_subclass__(mixin=mixin, **kwargs)
1900
+
1863
1901
  @classmethod
1864
1902
  def get_component(cls, *children, **props) -> "Component":
1865
1903
  """Get the component instance.
reflex/style.py CHANGED
@@ -16,10 +16,10 @@ LIGHT_COLOR_MODE: str = "light"
16
16
  DARK_COLOR_MODE: str = "dark"
17
17
 
18
18
  # Reference the global ColorModeContext
19
- color_mode_var_data = VarData( # type: ignore
19
+ color_mode_var_data = VarData(
20
20
  imports={
21
- f"/{constants.Dirs.CONTEXTS_PATH}": {ImportVar(tag="ColorModeContext")},
22
- "react": {ImportVar(tag="useContext")},
21
+ f"/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")],
22
+ "react": [ImportVar(tag="useContext")],
23
23
  },
24
24
  hooks={
25
25
  f"const [ {constants.ColorMode.NAME}, {constants.ColorMode.TOGGLE} ] = useContext(ColorModeContext)": None,
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
 
@@ -234,27 +234,33 @@ def get_app(reload: bool = False) -> ModuleType:
234
234
  Raises:
235
235
  RuntimeError: If the app name is not set in the config.
236
236
  """
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,))
237
+ from reflex.utils import telemetry
238
+
239
+ try:
240
+ os.environ[constants.RELOAD_CONFIG] = str(reload)
241
+ config = get_config()
242
+ if not config.app_name:
243
+ raise RuntimeError(
244
+ "Cannot get the app module because `app_name` is not set in rxconfig! "
245
+ "If this error occurs in a reflex test case, ensure that `get_app` is mocked."
246
+ )
247
+ module = config.module
248
+ sys.path.insert(0, os.getcwd())
249
+ app = __import__(module, fromlist=(constants.CompileVars.APP,))
247
250
 
248
- if reload:
249
- from reflex.state import reload_state_module
251
+ if reload:
252
+ from reflex.state import reload_state_module
250
253
 
251
- # Reset rx.State subclasses to avoid conflict when reloading.
252
- reload_state_module(module=module)
254
+ # Reset rx.State subclasses to avoid conflict when reloading.
255
+ reload_state_module(module=module)
253
256
 
254
- # Reload the app module.
255
- importlib.reload(app)
257
+ # Reload the app module.
258
+ importlib.reload(app)
256
259
 
257
- return app
260
+ return app
261
+ except Exception as ex:
262
+ telemetry.send_error(ex, context="frontend")
263
+ raise
258
264
 
259
265
 
260
266
  def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
@@ -406,7 +412,7 @@ def initialize_gitignore(
406
412
  # Write files to the .gitignore file.
407
413
  with open(gitignore_file, "w", newline="\n") as f:
408
414
  console.debug(f"Creating {gitignore_file}")
409
- f.write(f"{(path_ops.join(sorted(files_to_ignore))).lstrip()}")
415
+ f.write(f"{(path_ops.join(sorted(files_to_ignore))).lstrip()}\n")
410
416
 
411
417
 
412
418
  def initialize_requirements_txt():
@@ -879,6 +885,7 @@ def install_frontend_packages(packages: set[str], config: Config):
879
885
  processes.run_process_with_fallback(
880
886
  [get_install_package_manager(), "install"], # type: ignore
881
887
  fallback=fallback_command,
888
+ analytics_enabled=True,
882
889
  show_status_message="Installing base frontend packages",
883
890
  cwd=constants.Dirs.WEB,
884
891
  shell=constants.IS_WINDOWS,
@@ -894,6 +901,7 @@ def install_frontend_packages(packages: set[str], config: Config):
894
901
  *((config.tailwind or {}).get("plugins", [])),
895
902
  ],
896
903
  fallback=fallback_command,
904
+ analytics_enabled=True,
897
905
  show_status_message="Installing tailwind",
898
906
  cwd=constants.Dirs.WEB,
899
907
  shell=constants.IS_WINDOWS,
@@ -904,6 +912,7 @@ def install_frontend_packages(packages: set[str], config: Config):
904
912
  processes.run_process_with_fallback(
905
913
  [get_install_package_manager(), "add", *packages],
906
914
  fallback=fallback_command,
915
+ analytics_enabled=True,
907
916
  show_status_message="Installing frontend packages from config and components",
908
917
  cwd=constants.Dirs.WEB,
909
918
  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