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")
Related issues/PRs: #622 #635, #749
After lot of discussion, I think we agreed about what we want, which is:
The general consensus is that the default
targetis "here", i.e. we want to insert a sibling of the<py-script>tag. So the code above will produce the following DOM: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-defWhat is the implicit target of
displayhere? There are at least two options:<py-script>tag in which the code is written. The target forsay_hello()ispy1and the example above displayshello worldsay_hellothe default target ispy1, but when we call it, the default target ispy2. Thus, the example above displaysworld 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-handlersI 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 displayhellonext toClick me.Also, there might be events which doesn't have a clear tag where they are originated from.
Example 3:
display-and-asyncLet'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:But then, consider this example:
I would expect this to display
A0 A1 A2 hello B0 B1 B2, but with the logic above it doesn't work, because we setcurrent_target=Bwhen the first loop is still executing. So, my naive implementation above would displayA0 hello B0 A1 B2 A2 B2which 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:
This might technically work, although it has still weird corner cases. Example
sync-and-async:It sounds very weird that the
Acase is allowed and theBcase 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
displayfor each tag, something like this:And then, for each
<py-script>tag we create alocal_displayand 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. Examplesame-global-namespace: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 alsodisplaymust be unique, i.e.:But if
displaymust be identical across tags, we can no longer have an uniquelocal_displayfor 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 couldast.parse()the code, detect the usage of a globaldisplayname 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.:
Where
parentis 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-tagparentif 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 propertiesThis 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: