Skip to content

Behavior of display() targets #769

@antocuni

Description

@antocuni

Related issues/PRs: #622 #635, #749

After lot of discussion, I think we agreed about what we want, which is:

<div id="mydiv"></div>
...
<py-script>
    print('this goes to stdout')
    display('this goes to the DOM')
    display('this goes to the DOM with an explicit target', target='mydiv')
</py-script>

Sidenote 1: here I'm using strings but display() will be able to render rich objects as well, e.g. images, tables, charts, etc. -- but this is not pertinent to this particular discussion.

Sidenote 2: it's unclear whether display('somestring') should wrap the string into an HTML tag or not. For these examples, I'm wrapping it into a <div> but please don't comment on that: again, this belongs to a different discussion.

The general consensus is that the default target is "here", i.e. we want to insert a sibling of the <py-script> tag. So the code above will produce the following DOM:

<div id="mydiv">
    <div>this goes to the DOM with an explicit target</div>
</div>
...
<py-script>...</py-script>
<div id="automatically-generated">
   <div>this goes to the DOM</div>
</div>

I think that in this example the semantics is clear, straightforward and intuitive. However, things become less clear in more complicated examples. I'll try to add names to each example so that it's easier to refer to them in the further discussion.


Example 1: display-inside-def

<py-script id="py1">
def say_hello():
    display('hello')
</py-script>
<div>world</div>
<py-script id="py2">
say_hello()
</py-script>

What is the implicit target of display here? There are at least two options:

  • lexical scoping: the target is determined by <py-script> tag in which the code is written. The target for say_hello() is py1 and the example above displays hello world
  • dynamic scoping: we have the notion of "current target" which varies during the execution of the page. During the definition of say_hello the default target is py1, but when we call it, the default target is py2. Thus, the example above displays world hello.

The concept of "current target" might seem tempting at first, but I think it introduces a lot of complexity and corner cases, as shown by the next examples.


Example 2: display-from-event-handlers

<py-script id=`py1`>
def say_hello():
    display('hello')
</py-script>
<div>world</div>
<button py-onclick="say_hello()">Click me</button>

I think that the concept of "current target" is ill-defined in a case like this. You could say that it's button, but it seems very counter-intuitive to me to display hello next to Click me.
Also, there might be events which doesn't have a clear tag where they are originated from.


Example 3: display-and-async
Let's forget about events for a while and let's stay focused on <py-script> only. Even in this case, there are cases in which the concept of "current target" is vague, ill-defined or simply unexpected. I can imagine to implement it like this:

for(s of list_of_pyscript_tags) {
    set_global_current_target(s);
    s.evaluate();
}

But then, consider this example:

<py-script id="A">
for i in range(3):
    display(f'A{i}')
    asyncio.sleep(0.1)
</py-script>
<div>hello</div>
<py-script id="B">
for i in range(3):
    display(f'B{i}')
    asyncio.sleep(0.1)
</py-script>

I would expect this to display A0 A1 A2 hello B0 B1 B2, but with the logic above it doesn't work, because we set current_target=B when the first loop is still executing. So, my naive implementation above would display A0 hello B0 A1 B2 A2 B2 which is clearly wrong.
The only way to make the "current target" working is to keep track of it at each async swtich. I don't even know if this is technically possible.


A middle ground?

One possible compromise could be to declare this rule:

display() without a target can be called only during the execution of <py-script> tags. Inside event handlers and async blocks, you must always provide an explicit target

This might technically work, although it has still weird corner cases. Example sync-and-async:

<py-script id="A">
for i in range(3):
    display(f'A{i}')  # this is allowrd
</py-script>
<div>hello</div>
<py-script id="B">
for i in range(3):
    display(f'B{i}')  # this is forbidden because of the async sleep?
    asyncio.sleep(0.1)
</py-script>

It sounds very weird that the A case is allowed and the B case is not.


Implementation problems

From the wall of text above, it should be clear that I am more favorable to use "lexical scoping" and declare that the default target of dsiplay() depends on the tag where the code is defined, not where it's run.
However, this poses another problem: I don't know how to implement it :)

The naive approach is create a different display for each tag, something like this:

def global_display(obj, target):
    ...

def make_display_with_a_default_target(target):
    def local_display(obj, target=target):
        return global_display(obj, target)
    return local_display

And then, for each <py-script> tag we create a local_display and inject it into its namespace.
The biggest problem is that it mixes very badly with the fact that all <py-script> tags share the same global namespace. Example same-global-namespace:

<py-script>
a = 42
</py-script>
<py-script>
print(a)
</py-script>

In order to make this working, the only reasonable approach is to ensure that all the tags share the same __globals__. But then it means that also display must be unique, i.e.:

<py-script>
a = display
</py-script>
<py-script>
assert a is display
</py-script>

But if display must be identical across tags, we can no longer have an unique local_display for each of them.
I don't really know how to solve this cleanly


Horror story: a non-clean solution

I'm writing it here just for the sake of completeness, but please don't even consider to use it :).
For each <py-script> tag, we could ast.parse() the code, detect the usage of a global display name and substitute it with something else which is unique per each block.


Always require explicit targets

One possibility is to decide that implicit targets are too hard and that we always require an explicit one; e.g.:

<py-script>
display('hello', target=parent)
# or: parent.display('hello')
</py-script>

Where parent is automagically "the current tag". But this has the very same problems that I explained above in the section "Implementation problems": we cannot have a per-tag parent if we share the same __globals__.


Kill the global namespace

Another "obvious" solution is to declare that each <py-script> tag has its own local namespace, and you have to be explicit to access their properties

<py-script id="py1">
a = 42
</py-script>
<py-script>
print(document.py1.a)
</py-script>

This solves all the problems explained above. I don't know if we want to go in that direction though.


Wrap up

I think this is a fundamental issue in our desired semantics. We need to consider it very seriously because a mistake here have probably big consequences on the usability of pyscript, especially if it leads to weird corner cases.
Somehow, my gut feeling is that the following three properties don't mix well together:

  • python semantics
  • implicit global namespace
  • implicit per-tag local state ("the default target")

Metadata

Metadata

Assignees

No one assigned

    Labels

    backlogissue has been triaged but has not been earmarked for any upcoming releasesprintissue has been pulled into current sprint and is actively being worked on

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Closed

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions