Avaiga / taipy

Turns Data and AI algorithms into production-ready web applications in no time.
https://www.taipy.io
Apache License 2.0
14.23k stars 1.7k forks source link

Make Python expression in Viz elements more Pythonic in Python API #1379

Closed FlorianJacta closed 3 months ago

FlorianJacta commented 4 months ago

Description

Make Python expressions inside the Python API more Pythonic.

Markdown text is hard to debug, with no autocompletion, syntax coloring, etc. It is error-prone and is not easy to use when using for loops (see below).

for key in list_of_keys:
    tgb.text("{str(message['"+key+"'])}")
    # don't forget the ' otherwise this will not work, and Python autocompletion, correction won't help you
    # you cannot also use the " as it is already used; this is really error-prone and this is a simple expression

Solution Proposed

Use lambda function instead of Python inside Markdown text.

from taipy.gui import Gui 
import taipy.gui.builder as tgb
from math import cos, exp
import pandas as pd

value: int = 10
data: pd.DataFrame = compute_data(value)

def compute_data(decay:int)->pd.DataFrame:
    return pd.DataFrame({"Cos":[cos(i/6) * exp(-i*decay/600) for i in range(100)]})

def on_slider(state):
    state.data: pd.DataFrame = compute_data(state.value)

with tgb.Page() as page:
    tgb.text(value="Python")
    tgb.text(value="Taipy Demo", class_name="h1")
    tgb.text(value=lambda value: f"Value: {value}")
    tgb.slider(value="{value}", on_change=on_slider)
    tgb.chart(data=lambda data: data[:50], y="Cos") 

    with tgb.part(render=lambda value: value>=50) as page:
        tgb.table(lambda data: data.drop_na())

Gui(page=page).run(title="Frontend Demo")

A vast improvement is that you can tell Taipy which variables are interactive inside the lambda function, making creating this kind of code easy.

Current version:

for key in list_of_keys:
    tgb.text("{str(message['"+key+"'])}")
    # don't forget the ' otherwise this will not work, and Python autocompletion, correction won't help you
    # you cannot also use the " as it is already used; this is really error-prone

This is much better:

for key in list_of_keys:
    tgb.text(lambda message: str(message[key]))

message here is the bound variable of the Taipy expression, key is not and can be present in the state without a problem.

Impact of Solution

This could impact the product, but we should keep Python expressions inside Markdown as they are done now.

Code of Conduct

FredLL-Avaiga commented 4 months ago

we don't support function as variable we do support function calls How is this supposed to work ?

FlorianJacta commented 4 months ago

You can introspect the variable names of the lambda function so you know data is a bound variable then call the function with the state variables bound to it.

I don't say it is possible now obviously.

FlorianJacta commented 4 months ago

A vast improvement is that you can tell Taipy which variables are interactive inside the lambda function, making it easy to create this kind of code.

Current version:

for key in list_of_keys:
    tgb.text("{str(message['"+key+"'])}")
    # don't forget the ' otherwise this will not work, and Python autocompletion, correction won't help you
    # you cannot also use the " as it is already used; this is really error-prone

This is much better:

for key in list_of_keys:
    tgb.text(lambda message: str(message[key]))

message here is the bound variable of the Taipy expression, key is not and can be present in the state without a problem.

This is a huge improvement in Taipy with the Python API, as I and users have encountered it frequently.

jrobinAV commented 3 months ago

I like the idea. It is less error-prone than coding into strings.

FredLL-Avaiga commented 3 months ago

From what I understand, one would need to generate intermediate code that would look like:

message = {"aa": 1, "bb": 2}

for key in message:
    l_fn = lambda message: str(message[key])
    tgb.text(l_fn(message))
FredLL-Avaiga commented 3 months ago

good news: we can support lambdas

`tgb.text(lambda message: str(message))`

Which will translate into the equivalent of

state.fn_x = lambda message: str(message))
<{fn_x(message)}|text|>

bad news: I don"t think there's a way that we can support using external variables in lambda (it works but it is always the last value that is used)

message = {"a": "value A", "b": "value B"}

for key in message:
    tgb.text(lambda message: str(message[key]))

Will render as

value B
value B

This because the render is not synchronous with the declaration ie at render time key's value is the last value it got.

In that case, I wonder if the lambda support is still interesting ? @FabienLelaquais @FlorianJacta @jrobinAV

FredLL-Avaiga commented 3 months ago

PS: I can push the code in a branch if someone wants to play with it

FabienLelaquais commented 3 months ago

That's wonderful @FredLL-Avaiga.

The limitation is pretty bad though. An investigation path would be to provide the generation function with the variable scope then lookup the variables in the locals... at build time. That sounds a bit fuzzy at this point. The property value, defined as a function where are parameters would be state variables, would be partially bound at creation time...

FredLL-Avaiga commented 3 months ago

I'm afraid we cannot render at build time :-(

jrobinAV commented 3 months ago

I am not sure to understand the problem. Would it work if we pass a copy of key instead of key directly?

FabienLelaquais commented 3 months ago

I am not sure to understand the problem. Would it work if we pass a copy of key instead of key directly?

The point is to evaluate, at build time, variables that are not related to the state. So the short answer is yes.