writer / writer-framework

No-code in the front, Python in the back. An open-source framework for creating data apps.
https://dev.writer.com/framework/introduction
Apache License 2.0
1.31k stars 76 forks source link

Mutations for StateProxy-contained lists may not be tracked #205

Closed mmikita95 closed 2 months ago

mmikita95 commented 8 months ago

After working on #204, I've noticed that there are some unexpected behaviors happening in relation to lists that are stored in StateProxy. Consider the following example:

from streamsync.core import StateProxy

sp = StateProxy(
            {
                'name': 'Robert',
                'age': 1,
                'interests': ["lamps", "cars"]
            }
        )

mutations = sp.get_mutations_as_dict()
assert(len(mutations) == 3)  # True, because 3 initial keys were added to a previously-empty state

# The following modification of the state invokes __setitem__, and will be tracked as a mutation
sp['interests'] = ["lamps", "cars", "dogs"]
mutations = sp.get_mutations_as_dict()
assert(len(mutations) == 1)  
# True, because sp['interests'] were modified

But:

# The following modification of the state does not invoke __setitem__
sp['interests'].append("pigeons")
mutations = sp.get_mutations_as_dict()

assert(len(mutations) == 1)  
# False, because despite sp['interests'] was modified, 
# the mutation wasn't tracked due to the __setitem__ not being invoked

In certain configurations, this leads to lists not being properly updated on the frontend.

We'll probably have to introduce a solution similar to how dictionaries are currently processed inside the StateProxy, for example by providing a wrapper class that will override all the list modification methods and message the updates of its containing list to the related StateProxy. I'll greatly appreciate any feedback and contributions while I'll be looking into it.

ramedina86 commented 8 months ago

Hi Mikita, as discussed earlier let's treat this as a potential enhancement. As discussed here, mutation detection happens by assignment and List is no exception.

ramedina86 commented 7 months ago

@raaymax wants to fix this :)

FabienArcellier commented 6 months ago

@mmikita95 I think this one has been delivered in 0.4.0. Could you confirm ?

mmikita95 commented 6 months ago

@FabienArcellier sadly, can't confirm. Good news is that the state keeps this change – I think I remember that it was not the case, but the change still doesn't get propagated to the frontend instantly – it has to "wait" on another, "trackable" change.

This is the setup I used to test if you're interested: main.py

import streamsync as ss

def implicit_list_modification(state):
    state["list_to_be_modified"].append("test")

def explicit_list_modification(state):
    state["list_to_be_modified"] += ["test"]

initial_state = ss.init_state({"list_to_be_modified": []})

ui.json

{
    "metadata": {
        "streamsync_version": "0.4.0"
    },
    "components": {
        "root": {
            "id": "root",
            "type": "root",
            "content": {
                "appName": "List modification test"
            },
            "isCodeManaged": false,
            "position": 0
        },
        "main-page": {
            "id": "main-page",
            "type": "page",
            "content": {
                "pageMode": "",
                "key": "main"
            },
            "isCodeManaged": false,
            "position": 0,
            "parentId": "root"
        },
        "d2w3obvhbri2sbut": {
            "id": "d2w3obvhbri2sbut",
            "type": "columns",
            "content": {},
            "isCodeManaged": false,
            "position": 0,
            "parentId": "f2qst53hgdfydwyw",
            "handlers": {},
            "visible": true
        },
        "437w7haqfmf98fx7": {
            "id": "437w7haqfmf98fx7",
            "type": "column",
            "content": {
                "width": "1"
            },
            "isCodeManaged": false,
            "position": 0,
            "parentId": "d2w3obvhbri2sbut",
            "handlers": {},
            "visible": true
        },
        "3qqlvgpygldtm3zu": {
            "id": "3qqlvgpygldtm3zu",
            "type": "column",
            "content": {
                "width": "1"
            },
            "isCodeManaged": false,
            "position": 1,
            "parentId": "d2w3obvhbri2sbut",
            "handlers": {},
            "visible": true
        },
        "f2kq09lfygpmx8ot": {
            "id": "f2kq09lfygpmx8ot",
            "type": "button",
            "content": {
                "text": "Test implicit list modification"
            },
            "isCodeManaged": false,
            "position": 0,
            "parentId": "437w7haqfmf98fx7",
            "handlers": {
                "ss-click": "implicit_list_modification"
            },
            "visible": true
        },
        "pz9w7yngw9o4iqxz": {
            "id": "pz9w7yngw9o4iqxz",
            "type": "text",
            "content": {
                "text": "@{list_to_be_modified}"
            },
            "isCodeManaged": false,
            "position": 0,
            "parentId": "3qqlvgpygldtm3zu",
            "handlers": {},
            "visible": true
        },
        "uzp2y2hlnsfyck32": {
            "id": "uzp2y2hlnsfyck32",
            "type": "button",
            "content": {
                "text": "Test explicit list modification"
            },
            "isCodeManaged": false,
            "position": 1,
            "parentId": "437w7haqfmf98fx7",
            "handlers": {
                "ss-click": "explicit_list_modification"
            },
            "visible": true
        },
        "f2qst53hgdfydwyw": {
            "id": "f2qst53hgdfydwyw",
            "type": "section",
            "content": {
                "title": ""
            },
            "isCodeManaged": false,
            "position": 0,
            "parentId": "main-page",
            "handlers": {},
            "visible": true
        }
    }
}

Expected effect is that "test" appears in the list on button press for both cases – but, in fact, the "test"s appended by the click(s) of the first button only appear when the second, "explicit" button is pressed.