Repository: reflex-dev/reflex

Issue:
Bug: is_backend_variable does not exclude dunder prefixed attrs
**Describe the bug**
`is_backend_variable` does not exclude attrs prefixed with dunder (`__`)

**To Reproduce**
```python
import reflex as rx


class State(rx.State):
    state_attr: int = 1
    _state_attr: int = 1
    __state_attr: int = 1

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        print(self._backend_vars)
        print(self.backend_vars)


def index() -> rx.Component:
    return rx.text("Hello, world!")


app = rx.App()
app.add_page(index)
```

Log output:
```bash
{'_state_attr': 1, '_State__state_attr': 1}
{'_state_attr': 1, '_State__state_attr': 1}
Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 9/9 0:00:00
```

**Expected behavior**
State attrs starting with dunder (`__`) are not interpreted as backend vars.

**Specifics (please complete the following information):**
 - Python Version: 3.11.6
 - Reflex Version: 0.3.8
 - OS: arch linux
 - Browser (Optional):


Discussion:
from your logs, `__state_attr` is excluded which is the expected results. vars with a single leading  underscores are considered backend vars, read more here : https://reflex.dev/docs/state/vars#backend-only-vars . 
Closing this now, feel free to report any other issues.
@ElijahAhianyo thanks for your response. Here is another example:

```python
import reflex as rx


class State(rx.State):
    state_attr: int = 1
    _state_attr: int = 1
    __dunder_attr: int = 1

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        print(self._backend_vars)
        print(self.backend_vars)


def index() -> rx.Component:
    return rx.text("Hello, world!")


app = rx.App()
app.add_page(index)
```

Log:
```log
──────────────────────────────────────────────────────────────────────────── Starting Reflex App ─────────────────────────────────────────────────────────────────────────────
{'_state_attr': 1, '_State__dunder_attr': 1}
{'_state_attr': 1, '_State__dunder_attr': 1}
Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 9/9 0:00:00
```
`_State__dunder_attr` should not be considered a backend var, but yet it is in `backend_vars`.

i think this is a real bug
I am sorry, the first example didn't make this clear enough, cause the variable names are too close together. Could you reopen this for me if you consider this as a bug? Thanks in advance.



Patch:
diff --git a/reflex/state.py b/reflex/state.py
index 4b66185bf3d..a9c5c9d8335 100644
--- a/reflex/state.py
+++ b/reflex/state.py
@@ -315,7 +315,7 @@ def __init_subclass__(cls, **kwargs):
         cls.new_backend_vars = {
             name: value
             for name, value in cls.__dict__.items()
-            if types.is_backend_variable(name)
+            if types.is_backend_variable(name, cls)
             and name not in cls.inherited_backend_vars
             and not isinstance(value, FunctionType)
         }
@@ -876,7 +876,10 @@ def __setattr__(self, name: str, value: Any):
             setattr(self.parent_state, name, value)
             return
 
-        if types.is_backend_variable(name) and name not in RESERVED_BACKEND_VAR_NAMES:
+        if (
+            types.is_backend_variable(name, self.__class__)
+            and name not in RESERVED_BACKEND_VAR_NAMES
+        ):
             self._backend_vars.__setitem__(name, value)
             self.dirty_vars.add(name)
             self._mark_dirty()
@@ -1175,7 +1178,7 @@ def get_delta(self) -> Delta:
         subdelta = {
             prop: getattr(self, prop)
             for prop in delta_vars
-            if not types.is_backend_variable(prop)
+            if not types.is_backend_variable(prop, self.__class__)
         }
         if len(subdelta) > 0:
             delta[self.get_full_name()] = subdelta
diff --git a/reflex/utils/types.py b/reflex/utils/types.py
index 6191f5de333..06237d36ad5 100644
--- a/reflex/utils/types.py
+++ b/reflex/utils/types.py
@@ -251,15 +251,18 @@ def is_valid_var_type(type_: Type) -> bool:
     return _issubclass(type_, StateVar) or serializers.has_serializer(type_)
 
 
-def is_backend_variable(name: str) -> bool:
+def is_backend_variable(name: str, cls: Type | None = None) -> bool:
     """Check if this variable name correspond to a backend variable.
 
     Args:
         name: The name of the variable to check
+        cls: The class of the variable to check
 
     Returns:
         bool: The result of the check
     """
+    if cls is not None and name.startswith(f"_{cls.__name__}__"):
+        return False
     return name.startswith("_") and not name.startswith("__")
 
 


Test patch:
diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py
index 2700cf364d2..fac3fc3b720 100644
--- a/tests/utils/test_utils.py
+++ b/tests/utils/test_utils.py
@@ -144,6 +144,18 @@ def test_setup_frontend(tmp_path, mocker):
     assert (web_public_folder / "favicon.ico").exists()
 
 
+@pytest.fixture
+def test_backend_variable_cls():
+    class TestBackendVariable:
+        """Test backend variable."""
+
+        _hidden: int = 0
+        not_hidden: int = 0
+        __dunderattr__: int = 0
+
+    return TestBackendVariable
+
+
 @pytest.mark.parametrize(
     "input, output",
     [
@@ -152,8 +164,8 @@ def test_setup_frontend(tmp_path, mocker):
         ("__dundermethod__", False),
     ],
 )
-def test_is_backend_variable(input, output):
-    assert types.is_backend_variable(input) == output
+def test_is_backend_variable(test_backend_variable_cls, input, output):
+    assert types.is_backend_variable(input, test_backend_variable_cls) == output
 
 
 @pytest.mark.parametrize(