Browse Source

Merge branch 'main' into remix-over-next

Khaleel Al-Adhami 4 days ago
parent
commit
c8fa9dbd3e

+ 0 - 89
.github/workflows/integration_tests_wsl.yml

@@ -1,89 +0,0 @@
-name: integration-tests-wsl
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.event.pull_request.id }}
-  cancel-in-progress: true
-
-on:
-  push:
-    branches: ["main"]
-    paths-ignore:
-      - "**/*.md"
-  pull_request:
-    branches: ["main"]
-    paths-ignore:
-      - "**/*.md"
-
-permissions:
-  contents: read
-
-env:
-  TELEMETRY_ENABLED: false
-  NODE_OPTIONS: "--max_old_space_size=4096"
-
-jobs:
-  example-counter-wsl:
-    timeout-minutes: 20
-    runs-on: windows-latest
-    steps:
-      - uses: actions/checkout@v4
-      - name: Clone Reflex Examples Repo
-        uses: actions/checkout@v4
-        with:
-          repository: reflex-dev/reflex-examples
-          path: reflex-examples
-
-      - uses: Vampire/setup-wsl@v5
-        with:
-          distribution: Ubuntu-24.04
-
-      - name: Install packages
-        shell: wsl-bash {0}
-        run: |
-          sudo apt-get update
-          sudo apt-get install -y python3 python3-pip curl dos2unix zip unzip
-
-      - name: Install Uv
-        shell: wsl-bash {0}
-        run: |
-          curl -LsSf https://astral.sh/uv/install.sh | sh
-
-      - name: Install reflex deps
-        shell: wsl-bash {0}
-        run: |
-          /root/.local/bin/uv sync --link-mode=copy
-
-      - name: Install requirements for counter example
-        working-directory: ./reflex-examples/counter
-        shell: wsl-bash {0}
-        run: |
-          /root/.local/bin/uv pip install -r requirements.txt --link-mode=copy
-      - name: Check export --backend-only before init for counter example
-        working-directory: ./reflex-examples/counter
-        shell: wsl-bash {0}
-        run: |
-          export TELEMETRY_ENABLED=false
-          /root/.local/bin/uv run reflex export --backend-only
-      - name: Check run --backend-only before init for counter example
-        shell: wsl-bash {0}
-        run: |
-          export TELEMETRY_ENABLED=false
-          dos2unix scripts/integration.sh
-          /root/.local/bin/uv run bash scripts/integration.sh ./reflex-examples/counter dev 8001 --backend-only --backend-port 8001
-      - name: Init Website for counter example
-        working-directory: ./reflex-examples/counter
-        shell: wsl-bash {0}
-        run: |
-          export TELEMETRY_ENABLED=false
-          /root/.local/bin/uv run reflex init --loglevel debug
-      - name: Check export for counter example
-        working-directory: ./reflex-examples/counter
-        shell: wsl-bash {0}
-        run: |
-          export TELEMETRY_ENABLED=false
-          /root/.local/bin/uv run reflex export --frontend-only --loglevel debug
-      - name: Run Website and Check for errors
-        shell: wsl-bash {0}
-        run: |
-          export TELEMETRY_ENABLED=false
-          /root/.local/bin/uv run bash scripts/integration.sh ./reflex-examples/counter dev

+ 8 - 8
pyi_hashes.json

@@ -25,13 +25,13 @@
   "reflex/components/datadisplay/code.pyi": "651fc3d417b998eb1c3d072328f505d0",
   "reflex/components/datadisplay/dataeditor.pyi": "601c59f3ced6ab94fcf5527b90472a4f",
   "reflex/components/datadisplay/shiki_code_block.pyi": "ac16fd6c23eef7ce0185437ecf2d529d",
-  "reflex/components/el/__init__.pyi": "09042a2db5e0637e99b5173430600522",
+  "reflex/components/el/__init__.pyi": "f07f5957ca4dc3d95ffdc2ddb75fe2f8",
   "reflex/components/el/element.pyi": "323cfb5d67d8ccb58ac36c7cc7641dc3",
-  "reflex/components/el/elements/__init__.pyi": "280ed457675f3720e34b560a3f617739",
+  "reflex/components/el/elements/__init__.pyi": "baeddd04d4d3a82799420b2a6df368f6",
   "reflex/components/el/elements/base.pyi": "697cd6716e3b1127b35299435c3d4e69",
-  "reflex/components/el/elements/forms.pyi": "7e067419808bca05125de4967d284935",
+  "reflex/components/el/elements/forms.pyi": "21d7135b513bac72fd63b44c945932a5",
   "reflex/components/el/elements/inline.pyi": "ab31eec758f1cff8a9d51bf0935b9fca",
-  "reflex/components/el/elements/media.pyi": "ac99654959ed26b7b7d0f3dafa0ea2ab",
+  "reflex/components/el/elements/media.pyi": "c191a9e00223a97e26a0d6ab99a1919b",
   "reflex/components/el/elements/metadata.pyi": "eda94a3283bae6a9b61b4cb1e20c1dbd",
   "reflex/components/el/elements/other.pyi": "960bdac0bb9bf6b3cffd241f9b66c0fc",
   "reflex/components/el/elements/scripts.pyi": "ac3b8bfbd7777f33fb2c6aa109a5c627",
@@ -110,12 +110,12 @@
   "reflex/components/react_player/audio.pyi": "bd7e024d39ac641f8279ee0f6afd7985",
   "reflex/components/react_player/react_player.pyi": "40db798bcb7fa40207d24f49722135ae",
   "reflex/components/react_player/video.pyi": "22d84a7f57be13ece90cb30536d76c7d",
-  "reflex/components/recharts/__init__.pyi": "a52c9055e37c6ee25ded15688d45e8a5",
-  "reflex/components/recharts/cartesian.pyi": "eb44b706cdb45f4b8450ef5302a981ae",
+  "reflex/components/recharts/__init__.pyi": "a060a4abcd018165bc499173e723cf9e",
+  "reflex/components/recharts/cartesian.pyi": "601e1acb0ad6bd93ce371d763220aabe",
   "reflex/components/recharts/charts.pyi": "2f0a39f9c02de83d9e2d97763b4411af",
   "reflex/components/recharts/general.pyi": "06d0e97776cc82b946fed465ab36fba4",
-  "reflex/components/recharts/polar.pyi": "7c445e98c1d0c95868411173de8fe85e",
-  "reflex/components/recharts/recharts.pyi": "f2739c20a27990a571d16133a40a0878",
+  "reflex/components/recharts/polar.pyi": "77ca6e0d992f5d5c0479de73db4f71ba",
+  "reflex/components/recharts/recharts.pyi": "bbaec232c2da035b31b5d0e3888f4801",
   "reflex/components/sonner/toast.pyi": "6dc6d5d05d9a8d7d364c0326fb2e6503",
   "reflex/components/suneditor/editor.pyi": "0a6dcab61cc2d750488601e3808080d9",
   "reflex/experimental/layout.pyi": "fb4c52b954431d9a927fbdd612b562eb"

+ 37 - 23
pyproject.toml

@@ -83,36 +83,50 @@ reportIncompatibleMethodOverride = false
 target-version = "py310"
 output-format = "concise"
 lint.isort.split-on-trailing-comma = false
-lint.select = [
-  "ANN001",
-  "B",
-  "C4",
-  "D",
-  "E",
-  "ERA",
-  "F",
-  "FURB",
-  "I",
-  "N",
-  "PERF",
-  "PGH",
-  "PTH",
-  "RUF",
-  "SIM",
-  "T",
-  "TRY",
-  "UP",
-  "W",
-]
+lint.select = ["ALL"]
 lint.ignore = [
+  "A",
+  "ANN002",
+  "ANN003",
+  "ANN2",
+  "ANN4",
+  "ARG",
+  "ASYNC",
   "B008",
+  "BLE",
+  "C901",
+  "COM",
   "D205",
+  "DTZ",
   "E501",
+  "EM",
   "F403",
-  "SIM115",
+  "FBT",
+  "FIX",
+  "FLY",
+  "G004",
+  "INP",
+  "ISC003",
+  "NPY",
+  "PD",
+  "PIE",
+  "PLC",
+  "PLR",
+  "PLW",
+  "PT",
+  "PYI",
+  "RET",
+  "RSE",
   "RUF006",
   "RUF008",
   "RUF012",
+  "S",
+  "SIM115",
+  "SLF",
+  "SLOT",
+  "TC",
+  "TD",
+  "TID",
   "TRY0",
   "UP038",
 ]
@@ -219,7 +233,7 @@ hooks = [
     "reflex",
     "tests",
   ] },
-  { id = "ruff", args = [
+  { id = "ruff-check", args = [
     "--fix",
     "--exit-non-zero-on-fix",
   ], exclude = "^integration/benchmarks/" },

+ 4 - 2
reflex/app.py

@@ -1035,11 +1035,13 @@ class App(MiddlewareMixin, LifespanMixin):
         Example:
             >>> _get_frontend_packages({"react": "16.14.0", "react-dom": "16.14.0"})
         """
+        dependencies = constants.PackageJson.DEPENDENCIES
+        dev_dependencies = constants.PackageJson.DEV_DEPENDENCIES
         page_imports = {
             i
             for i, tags in imports.items()
-            if i not in constants.PackageJson.DEPENDENCIES
-            and i not in constants.PackageJson.DEV_DEPENDENCIES
+            if i not in dependencies
+            and i not in dev_dependencies
             and not any(i.startswith(prefix) for prefix in ["/", "$/", "."])
             and i != ""
             and any(tag.install for tag in tags)

+ 4 - 3
reflex/components/component.py

@@ -287,10 +287,11 @@ class BaseComponentMeta(ABCMeta):
 
         namespace["_own_fields"] = own_fields
         namespace["_inherited_fields"] = inherited_fields
-        namespace["_fields"] = inherited_fields | own_fields
+        all_fields = inherited_fields | own_fields
+        namespace["_fields"] = all_fields
         namespace["_js_fields"] = {
             key: value
-            for key, value in own_fields.items()
+            for key, value in all_fields.items()
             if value.is_javascript is True
         }
         return super().__new__(cls, name, bases, namespace)
@@ -1011,7 +1012,7 @@ class Component(BaseComponent, ABC):
         Returns:
             The unique fields.
         """
-        return set(cls.get_fields()) - set(Component.get_fields())
+        return set(cls.get_js_fields())
 
     @classmethod
     @functools.cache

+ 1 - 0
reflex/components/el/elements/__init__.py

@@ -69,6 +69,7 @@ _MAPPING = {
         "text",
         "line",
         "circle",
+        "g",
         "ellipse",
         "rect",
         "polygon",

+ 1 - 1
reflex/components/el/elements/forms.py

@@ -609,7 +609,7 @@ class Select(BaseHTML):
     required: Var[bool]
 
     # Number of visible options in a drop-down list
-    size: Var[int]
+    size: Var[str | int]
 
     # Fired when the select value changes
     on_change: EventHandler[input_event]

+ 26 - 0
reflex/components/el/elements/media.py

@@ -484,6 +484,30 @@ class Path(BaseHTML):
     d: Var[str | int | float]
 
 
+class G(BaseHTML):
+    """The SVG g component, used to group other SVG elements."""
+
+    tag = "g"
+
+    # The fill color of the group.
+    fill: Var[str | Color]
+
+    # The fill opacity of the group.
+    fill_opacity: Var[str | int | float]
+
+    # The stroke color of the group.
+    stroke: Var[str | Color]
+
+    # The stroke opacity of the group.
+    stroke_opacity: Var[str | int | float]
+
+    # The stroke width of the group.
+    stroke_width: Var[str | int | float]
+
+    # The transform applied to the group.
+    transform: Var[str]
+
+
 class SVG(ComponentNamespace):
     """SVG component namespace."""
 
@@ -498,6 +522,7 @@ class SVG(ComponentNamespace):
     linear_gradient = staticmethod(LinearGradient.create)
     radial_gradient = staticmethod(RadialGradient.create)
     defs = staticmethod(Defs.create)
+    g = staticmethod(G.create)
     __call__ = staticmethod(Svg.create)
 
 
@@ -512,6 +537,7 @@ stop = Stop.create
 linear_gradient = LinearGradient.create
 radial_gradient = RadialGradient.create
 defs = Defs.create
+g = G.create
 area = Area.create
 audio = Audio.create
 image = img = Img.create

+ 2 - 0
reflex/components/props.py

@@ -31,6 +31,7 @@ class PropsBase(Base):
         """Convert the object to a dictionary.
 
         Keys will be converted to camelCase.
+        By default, None values are excluded (exclude_none=True).
 
         Args:
             *args: Arguments to pass to the parent class.
@@ -39,6 +40,7 @@ class PropsBase(Base):
         Returns:
             The object as a dictionary.
         """
+        kwargs.setdefault("exclude_none", True)
         return {
             format.to_camel_case(key): value
             for key, value in super().dict(*args, **kwargs).items()

+ 1 - 1
reflex/components/recharts/__init__.py

@@ -90,10 +90,10 @@ _SUBMOD_ATTRS: dict = {
     ],
     "recharts": [
         "LiteralAnimationEasing",
-        "LiteralAreaType",
         "LiteralAxisType",
         "LiteralBarChartStackOffset",
         "LiteralComposedChartBaseValue",
+        "LiteralCurveType",
         "LiteralDirection",
         "LiteralGridType",
         "LiteralIconType",

+ 20 - 25
reflex/components/recharts/cartesian.py

@@ -11,8 +11,9 @@ from reflex.event import EventHandler, no_args_event_spec
 from reflex.vars.base import LiteralVar, Var
 
 from .recharts import (
+    ACTIVE_DOT_TYPE,
     LiteralAnimationEasing,
-    LiteralAreaType,
+    LiteralCurveType,
     LiteralDirection,
     LiteralIfOverflow,
     LiteralInterval,
@@ -89,7 +90,7 @@ class Axis(Recharts):
     ticks: Var[Sequence[str | int]]
 
     # If set false, no ticks will be drawn.
-    tick: Var[bool]
+    tick: Var[bool | dict]
 
     # The count of axis ticks. Not used if 'type' is 'category'. Default: 5
     tick_count: Var[int]
@@ -275,6 +276,9 @@ class Cartesian(Recharts):
     # The type of icon in legend. If set to 'none', no legend item will be rendered. 'line' | 'plainline' | 'square' | 'rect'| 'circle' | 'cross' | 'diamond' | 'star' | 'triangle' | 'wye' | 'none' optional
     legend_type: Var[LiteralLegendType]
 
+    # If false set, labels will not be drawn. If true set, labels will be drawn which have the props calculated internally. Default: False
+    label: Var[bool | dict[str, Any]]
+
     # If set false, animation of bar will be disabled. Default: True
     is_animation_active: Var[bool]
 
@@ -335,30 +339,27 @@ class Area(Cartesian):
     stroke: Var[str | Color] = LiteralVar.create(Color("accent", 9))
 
     # The width of the line stroke. Default: 1
-    stroke_width: Var[int]
+    stroke_width: Var[str | int | float]
 
     # The color of the area fill. Default: rx.color("accent", 5)
     fill: Var[str | Color] = LiteralVar.create(Color("accent", 5))
 
     # The interpolation type of area. And customized interpolation function can be set to type. 'basis' | 'basisClosed' | 'basisOpen' | 'bumpX' | 'bumpY' | 'bump' | 'linear' | 'linearClosed' | 'natural' | 'monotoneX' | 'monotoneY' | 'monotone' | 'step' | 'stepBefore' | 'stepAfter'. Default: "monotone"
-    type_: Var[LiteralAreaType] = LiteralVar.create("monotone")
+    type_: Var[LiteralCurveType] = LiteralVar.create("monotone")
 
     # If false set, dots will not be drawn. If true set, dots will be drawn which have the props calculated internally. Default: False
-    dot: Var[bool | dict[str, Any]]
+    dot: Var[ACTIVE_DOT_TYPE]
 
     # The dot is shown when user enter an area chart and this chart has tooltip. If false set, no active dot will not be drawn. If true set, active dot will be drawn which have the props calculated internally. Default: {stroke: rx.color("accent", 2), fill: rx.color("accent", 10)}
-    active_dot: Var[bool | dict[str, Any]] = LiteralVar.create(
+    active_dot: Var[ACTIVE_DOT_TYPE] = LiteralVar.create(
         {
             "stroke": Color("accent", 2),
             "fill": Color("accent", 10),
         }
     )
 
-    # If set false, labels will not be drawn. If set true, labels will be drawn which have the props calculated internally. Default: False
-    label: Var[bool]
-
     # The value which can describle the line, usually calculated internally.
-    base_line: Var[str | Sequence[dict[str, Any]]]
+    base_line: Var[int | Sequence[dict[str, Any]]]
 
     # The coordinates of all the points in the area, usually calculated internally.
     points: Var[Sequence[dict[str, Any]]]
@@ -384,7 +385,7 @@ class Bar(Cartesian):
     stroke: Var[str | Color]
 
     # The width of the line stroke.
-    stroke_width: Var[int]
+    stroke_width: Var[str | int | float]
 
     # The width of the line stroke. Default: Color("accent", 9)
     fill: Var[str | Color] = LiteralVar.create(Color("accent", 9))
@@ -392,9 +393,6 @@ class Bar(Cartesian):
     # If false set, background of bars will not be drawn. If true set, background of bars will be drawn which have the props calculated internally. Default: False
     background: Var[bool]
 
-    # If false set, labels will not be drawn. If true set, labels will be drawn which have the props calculated internally. Default: False
-    label: Var[bool]
-
     # The stack id of bar, when two bars have the same value axis and same stack_id, then the two bars are stacked in order.
     stack_id: Var[str]
 
@@ -431,16 +429,16 @@ class Line(Cartesian):
     alias = "RechartsLine"
 
     # The interpolation type of line. And customized interpolation function can be set to type. It's the same as type in Area.
-    type_: Var[LiteralAreaType]
+    type_: Var[LiteralCurveType]
 
     # The color of the line stroke. Default: rx.color("accent", 9)
     stroke: Var[str | Color] = LiteralVar.create(Color("accent", 9))
 
     # The width of the line stroke. Default: 1
-    stroke_width: Var[int]
+    stroke_width: Var[str | int | float]
 
     # The dot is shown when mouse enter a line chart and this chart has tooltip. If false set, no active dot will not be drawn. If true set, active dot will be drawn which have the props calculated internally. Default: {"stroke": rx.color("accent", 10), "fill": rx.color("accent", 4)}
-    dot: Var[bool | dict[str, Any]] = LiteralVar.create(
+    dot: Var[ACTIVE_DOT_TYPE] = LiteralVar.create(
         {
             "stroke": Color("accent", 10),
             "fill": Color("accent", 4),
@@ -448,16 +446,13 @@ class Line(Cartesian):
     )
 
     # The dot is shown when user enter an area chart and this chart has tooltip. If false set, no active dot will not be drawn. If true set, active dot will be drawn which have the props calculated internally. Default: {"stroke": rx.color("accent", 2), "fill": rx.color("accent", 10)}
-    active_dot: Var[bool | dict[str, Any]] = LiteralVar.create(
+    active_dot: Var[ACTIVE_DOT_TYPE] = LiteralVar.create(
         {
             "stroke": Color("accent", 2),
             "fill": Color("accent", 10),
         }
     )
 
-    # If false set, labels will not be drawn. If true set, labels will be drawn which have the props calculated internally. Default: False
-    label: Var[bool]
-
     # Hides the line when true, useful when toggling visibility state via legend. Default: False
     hide: Var[bool]
 
@@ -645,7 +640,7 @@ class ErrorBar(Recharts):
     stroke: Var[str | Color] = LiteralVar.create(Color("gray", 8))
 
     # The stroke width of error bar. Default: 1.5
-    stroke_width: Var[int | float]
+    stroke_width: Var[str | int | float]
 
 
 class Reference(Recharts):
@@ -684,7 +679,7 @@ class ReferenceLine(Reference):
     stroke: Var[str | Color]
 
     # The width of the stroke. Default: 1
-    stroke_width: Var[str | int]
+    stroke_width: Var[str | int | float]
 
     # Valid children components
     _valid_children: ClassVar[list[str]] = ["Label"]
@@ -849,10 +844,10 @@ class CartesianAxis(Grid):
     view_box: Var[dict[str, Any]]
 
     # If set false, no axis line will be drawn. If set a object, the option is the configuration of axis line. Default: True
-    axis_line: Var[bool]
+    axis_line: Var[bool | dict]
 
     # If set false, no ticks will be drawn.
-    tick: Var[bool]
+    tick: Var[bool | dict]
 
     # If set false, no axis tick lines will be drawn. If set a object, the option is the configuration of tick lines. Default: True
     tick_line: Var[bool]

+ 5 - 4
reflex/components/recharts/polar.py

@@ -11,6 +11,7 @@ from reflex.event import EventHandler, no_args_event_spec
 from reflex.vars.base import LiteralVar, Var
 
 from .recharts import (
+    ACTIVE_DOT_TYPE,
     LiteralAnimationEasing,
     LiteralGridType,
     LiteralLegendType,
@@ -65,10 +66,10 @@ class Pie(Recharts):
     legend_type: Var[LiteralLegendType]
 
     # If false set, labels will not be drawn. If true set, labels will be drawn which have the props calculated internally. Default: False
-    label: Var[bool] = Var.create(False)
+    label: Var[bool | dict[str, Any]] = Var.create(False)
 
     # If false set, label lines will not be drawn. If true set, label lines will be drawn which have the props calculated internally. Default: False
-    label_line: Var[bool]
+    label_line: Var[bool | dict[str, Any]]
 
     # Valid children components
     _valid_children: ClassVar[list[str]] = ["Cell", "LabelList", "Bare"]
@@ -126,7 +127,7 @@ class Radar(Recharts):
     points: Var[Sequence[dict[str, Any]]]
 
     # If false set, dots will not be drawn. Default: True
-    dot: Var[bool]
+    dot: Var[ACTIVE_DOT_TYPE]
 
     # Stoke color. Default: rx.color("accent", 9)
     stroke: Var[str | Color] = LiteralVar.create(Color("accent", 9))
@@ -141,7 +142,7 @@ class Radar(Recharts):
     legend_type: Var[LiteralLegendType]
 
     # If false set, labels will not be drawn. Default: True
-    label: Var[bool]
+    label: Var[bool | dict[str, Any]]
 
     # If set false, animation of polygon will be disabled. Default: True in CSR, and False in SSR
     is_animation_active: Var[bool]

+ 12 - 10
reflex/components/recharts/recharts.py

@@ -1,6 +1,6 @@
 """A component that wraps a recharts lib."""
 
-from typing import Literal
+from typing import Any, Literal
 
 from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent
 
@@ -73,25 +73,25 @@ LiteralPosition = Literal[
     "center",
 ]
 LiteralIconType = Literal[
-    "line",
-    "plainline",
-    "square",
-    "rect",
     "circle",
     "cross",
     "diamond",
+    "line",
+    "plainline",
+    "rect",
+    "square",
     "star",
     "triangle",
     "wye",
 ]
 LiteralLegendType = Literal[
-    "line",
-    "plainline",
-    "square",
-    "rect",
     "circle",
     "cross",
     "diamond",
+    "line",
+    "plainline",
+    "rect",
+    "square",
     "star",
     "triangle",
     "wye",
@@ -103,7 +103,7 @@ LiteralStackOffset = Literal["expand", "none", "wiggle", "silhouette"]
 LiteralBarChartStackOffset = Literal["expand", "none", "wiggle", "silhouette", "sign"]
 LiteralComposedChartBaseValue = Literal["dataMin", "dataMax", "auto"]
 LiteralAxisType = Literal["number", "category"]
-LiteralAreaType = Literal[
+LiteralCurveType = Literal[
     "basis",
     "basisClosed",
     "basisOpen",
@@ -126,3 +126,5 @@ LiteralIntervalAxis = Literal[
     "preserveStart", "preserveEnd", "preserveStartEnd", "equidistantPreserveStart"
 ]
 LiteralSyncMethod = Literal["index", "value"]
+
+ACTIVE_DOT_TYPE = bool | dict[str, Any]

+ 0 - 1
reflex/components/sonner/toast.py

@@ -147,7 +147,6 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
         Returns:
             The object as a dictionary with ToastAction fields intact.
         """
-        kwargs.setdefault("exclude_none", True)
         d = super().dict(*args, **kwargs)
         # Keep these fields as ToastAction so they can be serialized specially
         if "action" in d:

+ 22 - 4
reflex/config.py

@@ -69,9 +69,27 @@ def _load_dotenv_from_str(env_files: str) -> None:
             load_dotenv(env_file_path, override=True)
 
 
+def _load_dotenv_from_env():
+    """Load environment variables from paths specified in REFLEX_ENV_FILE."""
+    show_deprecation = False
+    env_env_file = os.environ.get("REFLEX_ENV_FILE")
+    if not env_env_file:
+        env_env_file = os.environ.get("ENV_FILE")
+        if env_env_file:
+            show_deprecation = True
+    if show_deprecation:
+        console.deprecate(
+            "Usage of deprecated ENV_FILE env var detected.",
+            reason="Prefer `REFLEX_` prefix when setting env vars.",
+            deprecation_version="0.7.13",
+            removal_version="0.8.0",
+        )
+    if env_env_file:
+        _load_dotenv_from_str(env_env_file)
+
+
 # Load the env files at import time if they are set in the ENV_FILE environment variable.
-if env_files := os.getenv("ENV_FILE"):
-    _load_dotenv_from_str(env_files)
+_load_dotenv_from_env()
 
 
 class DBConfig(Base):
@@ -979,8 +997,8 @@ If you are not using tailwind, set `tailwind` to `None` in rxconfig.py.""",
         Returns:
             The module name.
         """
-        if self.app_module is not None:
-            return self.app_module.__name__
+        if self.app_module_import is not None:
+            return self.app_module_import
         return ".".join([self.app_name, self.app_name])
 
     def update_from_env(self) -> dict[str, Any]:

+ 22 - 13
reflex/constants/installer.py

@@ -127,19 +127,28 @@ class PackageJson(SimpleNamespace):
 
     _react_router_version = _determine_react_router_version()
 
-    DEPENDENCIES = {
-        "axios": "1.9.0",
-        "json5": "2.2.3",
-        "react-router": _react_router_version,
-        "react-router-dom": _react_router_version,
-        "@react-router/node": _react_router_version,
-        "serve": "14.2.4",
-        "react": _react_version,
-        "react-dom": _react_version,
-        "isbot": "5.1.26",
-        "socket.io-client": "4.8.1",
-        "universal-cookie": "7.2.2",
-    }
+    @classproperty
+    @classmethod
+    def DEPENDENCIES(cls) -> dict[str, str]:
+        """The dependencies to include in package.json.
+
+        Returns:
+            A dictionary of dependencies with their versions.
+        """
+        return {
+            "axios": "1.9.0",
+            "json5": "2.2.3",
+            "react-router": _react_router_version,
+            "react-router-dom": _react_router_version,
+            "@react-router/node": _react_router_version,
+            "serve": "14.2.4",
+            "react": cls._react_version,
+            "react-dom": cls._react_version,
+            "isbot": "5.1.26",
+            "socket.io-client": "4.8.1",
+            "universal-cookie": "7.2.2",
+        }
+
     DEV_DEPENDENCIES = {
         "@emotion/react": "11.14.0",
         "autoprefixer": "10.4.21",

+ 3 - 1
reflex/experimental/client_state.py

@@ -158,7 +158,9 @@ class ClientStateVar(Var):
             hooks[f"{_client_state_ref(var_name)} ??= {var_name!s}"] = None
             hooks[f"{_client_state_ref_dict(var_name)} ??= {{}}"] = None
             hooks[f"{_client_state_ref_dict(setter_name)} ??= {{}}"] = None
-            hooks[f"{_client_state_ref_dict(var_name)}[{id_name}] = {var_name}"] = None
+            hooks[
+                f"{_client_state_ref_dict(var_name)}[{id_name}] = {_client_state_ref(var_name)}"
+            ] = None
             hooks[
                 f"{_client_state_ref_dict(setter_name)}[{id_name}] = {setter_name}"
             ] = None

+ 8 - 0
reflex/plugins/base.py

@@ -99,3 +99,11 @@ class Plugin:
         Args:
             context: The context for the plugin.
         """
+
+    def __repr__(self):
+        """Return a string representation of the plugin.
+
+        Returns:
+            A string representation of the plugin.
+        """
+        return f"{self.__class__.__name__}()"

+ 8 - 0
reflex/plugins/tailwind_v3.py

@@ -255,3 +255,11 @@ class Plugin(PluginBase):
             str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)),
             add_tailwind_to_css_file,
         )
+
+    def __repr__(self):
+        """Return a string representation of the plugin.
+
+        Returns:
+            A string representation of the plugin.
+        """
+        return "TailwindV3Plugin()"

+ 8 - 0
reflex/plugins/tailwind_v4.py

@@ -258,3 +258,11 @@ class Plugin(PluginBase):
             str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)),
             add_tailwind_to_css_file,
         )
+
+    def __repr__(self):
+        """Return a string representation of the plugin.
+
+        Returns:
+            A string representation of the plugin.
+        """
+        return "TailwindV4Plugin()"

+ 5 - 5
reflex/utils/exec.py

@@ -306,13 +306,12 @@ def get_reload_paths() -> Sequence[Path]:
         The reload paths for the backend.
     """
     config = get_config()
-    reload_paths = [Path(config.app_name).parent]
-    if config.app_module is not None and config.app_module.__file__:
-        module_path = Path(config.app_module.__file__).resolve().parent
+    reload_paths = [Path.cwd()]
+    if (spec := importlib.util.find_spec(config.module)) is not None and spec.origin:
+        module_path = Path(spec.origin).resolve().parent
 
         while module_path.parent.name and any(
-            sibling_file.name == "__init__.py"
-            for sibling_file in module_path.parent.iterdir()
+            sibling_file.name == "__init__.py" for sibling_file in module_path.iterdir()
         ):
             # go up a level to find dir without `__init__.py`
             module_path = module_path.parent
@@ -389,6 +388,7 @@ HOTRELOAD_IGNORE_EXTENSIONS = (
     "json",
     "sh",
     "bash",
+    "log",
 )
 
 HOTRELOAD_IGNORE_PATTERNS = (

+ 6 - 2
reflex/utils/types.py

@@ -263,8 +263,12 @@ def is_classvar(a_type: Any) -> bool:
     Returns:
         Whether the type is a ClassVar.
     """
-    return a_type is ClassVar or (
-        type(a_type) is _GenericAlias and a_type.__origin__ is ClassVar
+    return (
+        a_type is ClassVar
+        or (type(a_type) is _GenericAlias and a_type.__origin__ is ClassVar)
+        or (
+            type(a_type) is ForwardRef and a_type.__forward_arg__.startswith("ClassVar")
+        )
     )
 
 

+ 6 - 0
tests/integration/test_urls.py

@@ -2,6 +2,7 @@ from reflex.components.el.elements.media import (
     Circle,
     Defs,
     Ellipse,
+    G,
     Line,
     LinearGradient,
     Path,
@@ -72,3 +73,8 @@ def test_text():
 def test_stop():
     stop = Stop.create().render()
     assert stop["name"] == '"stop"'
+
+
+def test_g():
+    g = G.create().render()
+    assert g["name"] == '"g"'

+ 121 - 1
tests/units/components/test_props.py

@@ -1,7 +1,7 @@
 import pytest
 from pydantic.v1 import ValidationError
 
-from reflex.components.props import NoExtrasAllowedProps
+from reflex.components.props import NoExtrasAllowedProps, PropsBase
 from reflex.utils.exceptions import InvalidPropValueError
 
 
@@ -57,3 +57,123 @@ def test_no_extras_allowed_props(props_class, kwargs, should_raise):
     else:
         props_instance = props_class(**kwargs)
         assert isinstance(props_instance, props_class)
+
+
+# Test class definitions - reused across tests
+class MixedCaseProps(PropsBase):
+    """Test props with mixed naming conventions."""
+
+    # Single word (no case conversion needed)
+    name: str
+    # Already camelCase (should stay unchanged)
+    fontSize: int = 12
+    # snake_case (should convert to camelCase)
+    max_length: int = 100
+    is_active: bool = True
+
+
+class NestedProps(PropsBase):
+    """Test props for nested PropsBase testing."""
+
+    user_name: str
+    max_count: int = 10
+
+
+class ParentProps(PropsBase):
+    """Test props containing nested PropsBase objects."""
+
+    title: str
+    nested_config: NestedProps
+    is_enabled: bool = True
+
+
+class OptionalFieldProps(PropsBase):
+    """Test props with optional fields to test omission behavior."""
+
+    required_field: str
+    optional_snake_case: str | None = None
+    optionalCamelCase: int | None = None
+
+
+@pytest.mark.parametrize(
+    "props_class, props_kwargs, expected_dict",
+    [
+        # Test single word + snake_case conversion
+        (
+            MixedCaseProps,
+            {"name": "test", "max_length": 50},
+            {"name": "test", "fontSize": 12, "maxLength": 50, "isActive": True},
+        ),
+        # Test existing camelCase stays unchanged + snake_case converts
+        (
+            MixedCaseProps,
+            {"name": "demo", "fontSize": 16, "is_active": False},
+            {"name": "demo", "fontSize": 16, "maxLength": 100, "isActive": False},
+        ),
+        # Test all different case types together
+        (
+            MixedCaseProps,
+            {"name": "full", "fontSize": 20, "max_length": 200, "is_active": False},
+            {"name": "full", "fontSize": 20, "maxLength": 200, "isActive": False},
+        ),
+        # Test nested PropsBase conversion
+        (
+            ParentProps,
+            {
+                "title": "parent",
+                "nested_config": NestedProps(user_name="nested_user", max_count=5),
+            },
+            {
+                "title": "parent",
+                "nestedConfig": {"userName": "nested_user", "maxCount": 5},
+                "isEnabled": True,
+            },
+        ),
+        # Test nested with different values
+        (
+            ParentProps,
+            {
+                "title": "test",
+                "nested_config": NestedProps(user_name="test_user"),
+                "is_enabled": False,
+            },
+            {
+                "title": "test",
+                "nestedConfig": {"userName": "test_user", "maxCount": 10},
+                "isEnabled": False,
+            },
+        ),
+        # Test omitted optional fields appear with None values
+        (
+            OptionalFieldProps,
+            {"required_field": "present"},
+            {
+                "requiredField": "present",
+            },
+        ),
+        # Test explicit None values for optional fields
+        (
+            OptionalFieldProps,
+            {
+                "required_field": "test",
+                "optional_snake_case": None,
+                "optionalCamelCase": 42,
+            },
+            {
+                "requiredField": "test",
+                "optionalCamelCase": 42,
+            },
+        ),
+    ],
+)
+def test_props_base_dict_conversion(props_class, props_kwargs, expected_dict):
+    """Test that dict() handles different naming conventions correctly for both simple and nested props.
+
+    Args:
+        props_class: The PropsBase class to test.
+        props_kwargs: The keyword arguments to pass to the class constructor.
+        expected_dict: The expected dictionary output with camelCase keys.
+    """
+    props = props_class(**props_kwargs)
+    result = props.dict()
+    assert result == expected_dict

+ 8 - 2
tests/units/test_state.py

@@ -3321,7 +3321,10 @@ async def test_setvar_async_setter():
         TestState.setvar("asynctest", 42)
 
 
-@pytest.mark.skipif("REDIS_URL" not in os.environ, reason="Test requires redis")
+@pytest.mark.skipif(
+    "REDIS_URL" not in os.environ and "REFLEX_REDIS_URL" not in os.environ,
+    reason="Test requires redis",
+)
 @pytest.mark.parametrize(
     "expiration_kwargs, expected_values",
     [
@@ -3394,7 +3397,10 @@ config = rx.Config(
         assert state_manager.lock_warning_threshold == expected_values[2]  # pyright: ignore [reportAttributeAccessIssue]
 
 
-@pytest.mark.skipif("REDIS_URL" not in os.environ, reason="Test requires redis")
+@pytest.mark.skipif(
+    "REDIS_URL" not in os.environ and "REFLEX_REDIS_URL" not in os.environ,
+    reason="Test requires redis",
+)
 @pytest.mark.parametrize(
     "redis_lock_expiration, redis_lock_warning_threshold",
     [