Repository: plotly/dash

Issue:
[BUG] Patch += operation not working on the patch object.
If you have a patch object for a number prop and try to do `patched += 1`,  the callback will fail with an error, `returned non serializable object`.

Example:
```
app = Dash()
app.layout = [dcc.Store(data=0, store="store"), html.Button("click", id="click")]
@app.callback(Output("store", "data"), Input("click", "n_clicks"))
def on_click(_):
    patched = Patch()
    patched += 1
    return patched
```

Clicking on the button result in the error, it should instead update the store data prop to +1.


Discussion:

Resolving patch:
diff --git a/dash/_patch.py b/dash/_patch.py
--- a/dash/_patch.py
+++ b/dash/_patch.py
@@ -36,18 +36,18 @@ def __getstate__(self):
     def __setstate__(self, state):
         vars(self).update(state)
 
-    def __getitem__(self, item):
+    def __getitem__(self, item) -> "Patch":
         validate_slice(item)
         return Patch(location=self._location + [item], parent=self)
 
-    def __getattr__(self, item):
+    def __getattr__(self, item) -> "Patch":
         if item == "tolist":
             # to_json fix
             raise AttributeError
         if item == "_location":
-            return self._location
+            return self._location  # type: ignore
         if item == "_operations":
-            return self._operations
+            return self._operations  # type: ignore
         return self.__getitem__(item)
 
     def __setattr__(self, key, value):
@@ -81,22 +81,32 @@ def __iadd__(self, other):
             self.extend(other)
         else:
             self._operations.append(_operation("Add", self._location, value=other))
+        if not self._location:
+            return self
         return _noop
 
     def __isub__(self, other):
         self._operations.append(_operation("Sub", self._location, value=other))
+        if not self._location:
+            return self
         return _noop
 
     def __imul__(self, other):
         self._operations.append(_operation("Mul", self._location, value=other))
+        if not self._location:
+            return self
         return _noop
 
     def __itruediv__(self, other):
         self._operations.append(_operation("Div", self._location, value=other))
+        if not self._location:
+            return self
         return _noop
 
     def __ior__(self, other):
         self.update(E=other)
+        if not self._location:
+            return self
         return _noop
 
     def __iter__(self):


Test patch:
diff --git a/tests/integration/test_patch.py b/tests/integration/test_patch.py
--- a/tests/integration/test_patch.py
+++ b/tests/integration/test_patch.py
@@ -452,3 +452,95 @@ def test_pch005_clientside_duplicate(dash_duo):
 
     dash_duo.find_element("#click2").click()
     dash_duo.wait_for_text_to_equal("#output", "click2")
+
+
+def test_pch006_base_operators(dash_duo):
+    app = Dash()
+
+    app.layout = [
+        dcc.Store(data=2, id="store"),
+        html.Button("add-one", id="add-one"),
+        html.Button("remove-one", id="remove-one"),
+        html.Button("mul-two", id="mul-two"),
+        html.Button("div-two", id="div-two"),
+        html.Button("merge", id="merge"),
+        dcc.Store(data={"initial": "initial"}, id="dict-store"),
+        html.Div(id="store-output"),
+        html.Div(id="dict-store-output"),
+    ]
+
+    @app.callback(
+        Output("store", "data"), Input("add-one", "n_clicks"), prevent_initial_call=True
+    )
+    def on_add(_):
+        patched = Patch()
+        patched += 1
+        return patched
+
+    @app.callback(
+        Output("store", "data", allow_duplicate=True),
+        Input("remove-one", "n_clicks"),
+        prevent_initial_call=True,
+    )
+    def on_remove(_):
+        patched = Patch()
+        patched -= 1
+        return patched
+
+    @app.callback(
+        Output("store", "data", allow_duplicate=True),
+        Input("mul-two", "n_clicks"),
+        prevent_initial_call=True,
+    )
+    def on_mul(_):
+        patched = Patch()
+        patched *= 2
+        return patched
+
+    @app.callback(
+        Output("store", "data", allow_duplicate=True),
+        Input("div-two", "n_clicks"),
+        prevent_initial_call=True,
+    )
+    def on_div(_):
+        patched = Patch()
+        patched /= 2
+        return patched
+
+    @app.callback(
+        Output("dict-store", "data", allow_duplicate=True),
+        Input("merge", "n_clicks"),
+        prevent_initial_call=True,
+    )
+    def on_merge(_):
+        patched = Patch()
+        patched |= {"merged": "merged"}
+        return patched
+
+    app.clientside_callback(
+        "data => data", Output("store-output", "children"), Input("store", "data")
+    )
+    app.clientside_callback(
+        "data => JSON.stringify(data)",
+        Output("dict-store-output", "children"),
+        Input("dict-store", "data"),
+    )
+
+    dash_duo.start_server(app)
+
+    dash_duo.find_element("#add-one").click()
+    dash_duo.wait_for_text_to_equal("#store-output", "3")
+
+    dash_duo.find_element("#remove-one").click()
+    dash_duo.wait_for_text_to_equal("#store-output", "2")
+
+    dash_duo.find_element("#mul-two").click()
+    dash_duo.wait_for_text_to_equal("#store-output", "4")
+
+    dash_duo.find_element("#div-two").click()
+    dash_duo.wait_for_text_to_equal("#store-output", "2")
+
+    dash_duo.find_element("#merge").click()
+    dash_duo.wait_for_text_to_equal(
+        "#dict-store-output", '{"initial":"initial","merged":"merged"}'
+    )