Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: trigger a calculated property on mutation #496

Conversation

FabienArcellier
Copy link
Collaborator

@FabienArcellier FabienArcellier commented Jul 24, 2024

implement FabienArcellier#37

Peek 2024-08-01 08-09

Simple subscription

def _increment_counter(state):
    state['my_counter'] += 1

_state = wf.init_state({"a": 1, "my_counter": 0})
_state.subscribe_mutation('a', _increment_counter)

_state['a'] = 2 # trigger _increment_counter mutation

Subscription on state with strong type

class MyState(wf.WriterState):
  counter: int
  total: int

def cumulative_sum(state: MyState):
  state.total += state.counter

initial_state = ss.init_state({
    "counter": 0,
    "total": 0
}, schema=MyState)

initial_state.subscribe_mutation('counter', cumulative_sum)

initial_state['counter'] = 2 # trigger the function cumulative_sum

Calculated property

class MyState(wf.WriterState):
    counter: int

    @wf.property('counter')
    def counter_str(self) -> str:
        return str(self.counter)

state = wf.init_state({'counter': 0}, MyState)

state.counter = 2 # trigger mutation for +counter_str 

Subscription with an event handler

def _increment_counter(state: RootState, context: dict, payload: dict, session: dict, ui: wf.WriterUIManager):
	if context['event'] == 'mutation' and context['mutation'] == 'a':
		if payload['previous_value'] < payload['new_value']:
			state['my_counter'] += 1

state = wf.init_state({
	"a": 1, 
	"my_counter": 0,
})

state.subscribe_mutation('a', _increment_counter)

In preparation of dot support expression

def cumulative_sum(state):
    state['total'] += state['a.b']

initial_state = wf.init_state({
    "a.b": 0,
    "total": 0
})

initial_state.subscribe_mutation('a\.b', cumulative_sum)

@FabienArcellier FabienArcellier force-pushed the 37-trigger-a-calculated-property-on-mutation branch 3 times, most recently from 767c9c3 to a7fb9d4 Compare July 26, 2024 06:49
@FabienArcellier FabienArcellier self-assigned this Jul 26, 2024
src/writer/core.py Outdated Show resolved Hide resolved
@FabienArcellier FabienArcellier force-pushed the 37-trigger-a-calculated-property-on-mutation branch 2 times, most recently from e479327 to 9465a76 Compare August 1, 2024 05:02
@FabienArcellier FabienArcellier marked this pull request as ready for review August 1, 2024 06:03
@FabienArcellier FabienArcellier added the enhancement New feature or request label Aug 1, 2024
src/writer/core.py Outdated Show resolved Hide resolved
@FabienArcellier FabienArcellier force-pushed the 37-trigger-a-calculated-property-on-mutation branch from ba448f5 to 005b082 Compare August 1, 2024 06:12
for p in path_list:
state_proxy = self._state_proxy
path_parts = p.split(".")
final_handler = functools.partial(handler, self)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of doing things is too light.

I will have to check the arguments exposed on the handler here. To be consistent, it would be necessary to inject a payload with the previous value and the current value, add the ui and add a context which indicates the mutation causing the change.

@FabienArcellier
Copy link
Collaborator Author

FabienArcellier commented Aug 1, 2024

  • check that the signature of a property is valid (a single self argument), otherwise display a warning and ignore it
  • check the signature of mutation event (support of state, payload, context and ui)

@@ -659,6 +678,48 @@ def _set_state_item(self, key: str, value: Any):
self._state_proxy[key] = value


def subscribe_mutation(self, path: Union[str, List[str]], handler: Callable[['State'], None], initial_triggered: bool = False) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to support array, the path should be able to contain number since string can't be index

>>> obj = {"a": [1,2,3]}
>>> obj["a"]["0"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers or slices, not str


for p in path_list:
state_proxy = self._state_proxy
path_parts = p.split(".")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't handle key containing . .

Also, it doesn't work with Array index because the path_parts is an array of string (see my comment below)

IMO

  • it should handle a path like this one foo["bar.baz"][0] we might need a library to handle it
  • Or we should only accept array of string/number like ["foo", "bar.baz", 0]
  • Or we considerate it's OK, but it might need a comment in the doc saying this limitation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usage of . into a state key trigger errors. That's a good point, we don't manage properly the case where a state contains . in the key. Currently it raise many issue.

I would advocate to manage the character . as reserved keyword for State key. We should ignore this value and show warning when the application is loaded.

image

mutation of State are not monitored on list : the State structure on the backend side supervises mutations exclusively on dictionaries, not on lists. This is an intrinsic limitation of WF.

A developer has to reassign a list to trigger the mutation. This point is covered in the Mutation detection in doc. It is therefore not possible to subscribe to a mutation on an element of a list.

Copy link
Collaborator

@madeindjs madeindjs Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clear, thanks for the explanations.

About array, is it something we want to support ? Use case I see is to watch array elements in an array to get the sum. Something like this

class MyState(wf.WriterState):
  arr: List[int]
  total: int

def cumulative_sum(state: MyState):
  state.total = sum(state.arr)

initial_state = ss.init_state({
    "arr": [1,2,3,4],
    "total": 0
}, schema=MyState)

initial_state.subscribe_mutation('counter', cumulative_sum)

initial_state['counter'][0] = 9

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thin this question has already been debated in the past. I don't find trace about that. I think we decided that for the moment we wanted to avoid this pattern. Re-encoding a list as an observable object would have an overhead that we want to avoid.

In Python, lists are more often reconstructed rather than modified.

However, this was before the existence of typed states. Typed states allow you to specify how to behave with respect to a collection. I wonder on which usecase we would benefit from implementing this pattern.

@FabienArcellier FabienArcellier force-pushed the 37-trigger-a-calculated-property-on-mutation branch 3 times, most recently from 3ae449b to fc0e1c5 Compare August 5, 2024 06:55
@FabienArcellier FabienArcellier force-pushed the 37-trigger-a-calculated-property-on-mutation branch from fc0e1c5 to 99fbb72 Compare August 12, 2024 13:09
* feat: implement subscribe_mutation
* feat: implement fixture to isolate test of global context
* feat: implement calculated properties
* docs: improve the documentation of application state
* fix: make it works on writer framework
* docs: add documentation about calculated properties
* fix: trigger initial mutation on calculated properties
* docs: document mutation event
* feat: subscribe_mutation supports an event handler as a function
* feat: subscribe_mutation supports an event handler as a function
* feat: subscribe_mutation supports an async event handler as a function
* fix: handle dot separated expression on subscribe mutation
@FabienArcellier FabienArcellier force-pushed the 37-trigger-a-calculated-property-on-mutation branch from 19c3fa9 to c2c5822 Compare August 17, 2024 05:47
Copy link
Collaborator

@ramedina86 ramedina86 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a tough read but looks good to me. Should I merge?

@FabienArcellier
Copy link
Collaborator Author

FabienArcellier commented Aug 19, 2024

That was a tough read but looks good to me. Should I merge?

You should

@ramedina86 ramedina86 merged commit 505f6f0 into writer:dev Aug 19, 2024
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants