Repository: reflex-dev/reflex

Issue:
[REF-3160] [VarOps] It is way too easy to pass incorrect kwargs to `rx.Var._replace`
**Describe the bug**
This `_replace` func accepts arbitrary kwargs, pulls out the ones it recognizes, and passes those off to BaseVar constructor. Meanwhile, any remainin unrecognized kwargs are just jettisoned without any errors.

This means if I pass `_merge_var_data` instead of `merge_var_data`, it silently does the wrong thing.

**To Reproduce**
```python
rx.Var.create("foo")._replace(_this_should_fail=True)
```

**Expected behavior**
The line of code above should raise some TypeError or similar. Instead it returns a new Var with nothing changed.

<sub>[REF-3160](https://linear.app/reflex-dev/issue/REF-3160/[varops]-it-is-way-too-easy-to-pass-incorrect-kwargs-to-rxvar-replace)</sub>


Discussion:
Hey @masenf 

I noticed in [vars.py](https://github.com/reflex-dev/reflex/blob/main/reflex/vars.py) that the _replace function typically takes class variables of BaseVars, Vars, or ComputedVar as kwargs. I propose a solution to create a custom decorator for verifying kwargs.

In this decorator, we can either pass the class and it will automatically consider the class variables, or we can pass a set of expected kwargs, if the values passes is outside the set it will throw and TypeError. This decorator might be useful at multiple places too .

 Let me know your thoughts on this !



Patch:
diff --git a/reflex/vars.py b/reflex/vars.py
index afe3e60c336..f226d88bd96 100644
--- a/reflex/vars.py
+++ b/reflex/vars.py
@@ -496,6 +496,9 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> BaseVar:
 
         Returns:
             A new BaseVar with the updated fields overwriting the corresponding fields in this Var.
+
+        Raises:
+            TypeError: If kwargs contains keys that are not allowed.
         """
         field_values = dict(
             _var_name=kwargs.pop("_var_name", self._var_name),
@@ -507,9 +510,14 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> BaseVar:
                 self._var_full_name_needs_state_prefix,
             ),
             _var_data=VarData.merge(
-                kwargs.get("_var_data", self._var_data), merge_var_data
+                kwargs.pop("_var_data", self._var_data), merge_var_data
             ),
         )
+
+        if kwargs:
+            unexpected_kwargs = ", ".join(kwargs.keys())
+            raise TypeError(f"Unexpected keyword arguments: {unexpected_kwargs}")
+
         return BaseVar(**field_values)
 
     def _decode(self) -> Any:
@@ -2024,26 +2032,35 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> ComputedVar:
 
         Returns:
             The new ComputedVar instance.
+
+        Raises:
+            TypeError: If kwargs contains keys that are not allowed.
         """
-        return ComputedVar(
-            fget=kwargs.get("fget", self.fget),
-            initial_value=kwargs.get("initial_value", self._initial_value),
-            cache=kwargs.get("cache", self._cache),
-            deps=kwargs.get("deps", self._static_deps),
-            auto_deps=kwargs.get("auto_deps", self._auto_deps),
-            interval=kwargs.get("interval", self._update_interval),
-            backend=kwargs.get("backend", self._backend),
-            _var_name=kwargs.get("_var_name", self._var_name),
-            _var_type=kwargs.get("_var_type", self._var_type),
-            _var_is_local=kwargs.get("_var_is_local", self._var_is_local),
-            _var_is_string=kwargs.get("_var_is_string", self._var_is_string),
-            _var_full_name_needs_state_prefix=kwargs.get(
+        field_values = dict(
+            fget=kwargs.pop("fget", self.fget),
+            initial_value=kwargs.pop("initial_value", self._initial_value),
+            cache=kwargs.pop("cache", self._cache),
+            deps=kwargs.pop("deps", self._static_deps),
+            auto_deps=kwargs.pop("auto_deps", self._auto_deps),
+            interval=kwargs.pop("interval", self._update_interval),
+            backend=kwargs.pop("backend", self._backend),
+            _var_name=kwargs.pop("_var_name", self._var_name),
+            _var_type=kwargs.pop("_var_type", self._var_type),
+            _var_is_local=kwargs.pop("_var_is_local", self._var_is_local),
+            _var_is_string=kwargs.pop("_var_is_string", self._var_is_string),
+            _var_full_name_needs_state_prefix=kwargs.pop(
                 "_var_full_name_needs_state_prefix",
                 self._var_full_name_needs_state_prefix,
             ),
             _var_data=VarData.merge(self._var_data, merge_var_data),
         )
 
+        if kwargs:
+            unexpected_kwargs = ", ".join(kwargs.keys())
+            raise TypeError(f"Unexpected keyword arguments: {unexpected_kwargs}")
+
+        return ComputedVar(**field_values)
+
     @property
     def _cache_attr(self) -> str:
         """Get the attribute used to cache the value on the instance.


Test patch:
diff --git a/tests/test_var.py b/tests/test_var.py
index e58daaa035e..918d3a177cb 100644
--- a/tests/test_var.py
+++ b/tests/test_var.py
@@ -539,6 +539,29 @@ def test_var_indexing_str():
     assert str(str_var[-1]) == "{str.at(-1)}"
 
 
+@pytest.mark.parametrize(
+    "var",
+    [
+        (BaseVar(_var_name="foo", _var_type=int)),
+        (BaseVar(_var_name="bar", _var_type=float)),
+    ],
+)
+def test_var_replace_with_invalid_kwargs(var):
+    with pytest.raises(TypeError) as excinfo:
+        var._replace(_this_should_fail=True)
+    assert "Unexpected keyword arguments" in str(excinfo.value)
+
+
+def test_computed_var_replace_with_invalid_kwargs():
+    @computed_var(initial_value=1)
+    def test_var(state) -> int:
+        return 1
+
+    with pytest.raises(TypeError) as excinfo:
+        test_var._replace(_random_kwarg=True)
+    assert "Unexpected keyword arguments" in str(excinfo.value)
+
+
 @pytest.mark.parametrize(
     "var, index",
     [