-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Behavior of display() targets #769
Comments
Thanks for the great writeup, overall, despite the complexity, I still think the dynamic scoping behavior is what I'd intuitively expect. If we decide the complexity is too much my second vote would go for being fully explicit. Everything else is, imo, a lot of mental overhead for users or simply non-intuitive.
I think this should be possible using ContextVar. I've recently been dealing with a similar issue and ended up rolling my own solution by adding an |
that's interesting because it's exactly what I would not expect :).
uhm, this is interesting.
I'm still not fully convinced that there are no weird cases though. |
It feels like display() has overlap with the Element api. Element("#foo").write("<h1>My Grand Title</h1>")
#vs
display( "<h1>My Grand Title</h1>", "#foo" ) It also feels like the output target here should be configurable.
<py-script stderr="console.error" stdout="#mydiv">
print('hello') # to #mydiv
</py-script> <py-script stderr="console.error" stdout="console.log">
print('hello') # to console.log
import foo&bar # import error to console.error
</py-script>
<py-config>
stderr="console.error"
stdout="console.log"
</py-config>
<py-script>
print('hello') # to console.log
</py-script> I do not believe we need to wrap text in display(string) as TextNode is a valid DOM type and is generated purely from a string in the DOM api. There are some important aspects to writing to the DOM and these "modes" need to be taken into account:
See https://github.com/patrick-steele-idem/morphdom for a Diff/Patch API without Virtual DOM |
Thank you @antocuni ! This is a great summary!
I'll say dynamic scoping behavior is exactly what I'd expect as well. I will risk here and say that @pzwang vote goes there too. In general I think this is very familiar to anyone used to Notebooks. You get your output in the context of the cell that is executing code (not defining!). In that sense, actually most examples above don't really seem an edge case (at least to me). I do like @philippjfr idea of using ContextVar. or something along these lines where the target lives within that execution block. I'll give you that this write you actually adds one use case that I really consider edge, and that is example 2. In that case I think there's no good In addition to that default, IF WE CAN, I think we should also allow users to set targets explicitly at different levels. For instance:
|
this is something which came up many times already, and I don't really understand it. From my POV notebooks cells and
I'm still not 100% convinced that it works in all cases, especially in combination with the JS async loop. I need to think more.
ok, it's good that we are converging somewhere :).
class BaseEvalElementWithImplicitTarger extends HTMLElement {
async evaluate(): Promise<void> {
magically_set_current_target(this);
try {
... execute the python code ...
} finally {
magically_set_current_target(undefined);
}
}
+1
-0. It probably doesn't hurt, but what is the use case for that? I mean: <py-script display-target="foo">
display(obj)
</py-script>
<py-script>
display(obj, target="foo")
</py-script> The two examples seem to have the same level of complexity to me: is it worth to introduce yet-another-construct which does the same work as one that we already have? I think it adds cognitive overhead for a very little gain. For example, as soon as you introduce The only case in which it saves typing is if you have multiple calls to
+1 |
The current behavior is: we only display by default on
You'll get the
That's just building on top of previous behavior... And I imagine if we want to change this we'd have to undergo deeper refactoring of the code. If I understand correctly that's your compromise design idea?
Example 3 would be nicely fixed by @philippjfr's idea, I guess? I'm not familiar with this var but that was Fabio's opinion yesterday.
This session seems so overly complicated. I like these:
I think this is a bad solution in an user perspective. |
Sorry my page wasn't updated here are some more cents.
I hear you!
I will run some tests trying this once we merge the current PR.
You're saying we should put rules in place that enforce this behavior (as in implemented in the code and throwing warnings or errors? Or just somewhere in the docs as what you should do?)
+1 This is already implemented. |
I hate to add more cooks to this great kitchen, but: PrecedenceI'm hoping to clarify the precedence of the various function-arguments and tag-attributes as they've been discussed. I agree with @antocuni's desire not to overcomplicate what users need to learn, but I think the following order (from most-explicit to most-generic) is reasonable and not overcomplicated.
Here's what I propose as the logic for how the DOM-placement-location is determined. They work from most-explicit to most-general, with "output as a sibling to the current tag" as a fallback: if the call to `display()` has a 'target' argument?
if the value of 'target' is the id of a tag on the current page:
write output to that tag; return
else: <Error?>
if the current tag (py-script, py-repl, tag with py-on* event, etc) has a 'display-target' attribute:
''' Per antocuni/fpliger, this may or may not be an option we want to have. JGlass wants it, see below'''
if the value of 'display-target' is the id of a tag on the current page:
write output to that tag; return
else: <Error?>
if the current page has a 'default-display-target' set in Py-Config:
if the value of 'default-display-target' is the id of a tag on the page:
write output to that tag; return
else: <Error?>
if we`re using lexical-scoping:
*somehow* identify the context any nested calls to display() were defined in
Repeat the above logic for that scope
If the above-logic does not terminate in a result - error?
Or if we`re doing dynamic scoping:
create new tag as sibling of calling tag;
write output to that tag; return The To highlight: the reference to the 'current-tag' on line 6 is either the evaluatable tag( Which brings us to... Lexical vs Dynamic Binding (Opinion)Personally, I'd advocate for 'dynamic scoping'. Having In my experience, when creating projects with PyScript, this design pattern (with dynamic scoping) is what I'd like to use almost all of the time: <py-script>
def nice_output(raw_value):
display("Some additional formatting around " + raw_value + " is nice")
</py-script>
...
<py-script output="example-a">
result = some_action()
display("We did something")
nice_output(result) #results near the action on the page
</py-script>
...
<py-script output="example-b">
another_result = some_entirely_different_thing()
display("We did something else")
nice_output(another_result) #results near the other action on the page
</py-script> And sure, we could pass a target as an argument to It also brings the answer to "Where is my output going to go when I call If a user-defined function is always supposed to output to a specific location, the place to specify that is as a argument to the Per-tag Targetting (Opinion)
+3. It allows writing code that can be 'portable' between PyScript tags with different output destinations, or the changing of the output of all calls to |
Wow, lots of very valuable discussion here, thanks to everyone who is contributing! As usual, let me try to answer some comments individually.
Do you mean that
we should actively check for this and throw errors in case the "current target" is not defined.
The proposed logic looks reasonable to me, but the hard part is to define what is "the current tag" and under which conditions it's well defined.
agreed, errors should not pass silently. We should raise a big red error in that case.
I am +1 to define
ok, with this example you convinced me. +1 for dynamic scoping.
I don't understand what you mean. Could you please provide an example? |
exactly. In that case, it should be an error but that's precisely what we are trying to decide with this discussion :) |
I think perhaps I confused things by using the term 'current tag' above, which I realize in #749 has special meaning as a global variable. A more accurate term for me, above, would have been "most-recently evaluate()'d py-script/py-repl/etc tag or the source tag of the most-recently trigged py-on* event". Essentially, we can use ContextVars to create this behavior, and do away with the global CURRENT_PY_SCRIPT_TAG entirely. When PyScript Evaluate()s a tag, or triggers a py-on* event from a tag, if that tag has an I'm cleaning up a demo of this which I think will clarify my proposed approach and demonstrates how it works with various tags (and async). I was in the process of also describing it in more detail here, but honestly I think it will be easier to read the code than the Psuedocode.
I think this is again a case where I've confused things. I'll try to clear up my meaning presently.
For sure, that wasn't super clear of me. To copy-paste from a current project, with a little reformatting to use the proposed <py-script>
import asyncio
for i in range(1, 20+1):
display(f"Counted to {i}")
await asyncio.sleep(1)
display("I counted to 20!", append=True)
asyncio.sleep(2)
for i in range(20, 0+1):
display(f"Counted down to {i}")
await asnycio.sleep(1)
display("And we're done!", append=True)
</py-script> When I'm writing a script like this, I know I want all of the output to go to the same place on the page, but it's not like its behavior is specifically tied to anything special about that place. And that place/tag is might change many times as the page itself evolves, for reasons that have nothing to do with the behavior of the Python code. If we allow display(f"Counted to {i}", output-target="some-div")
...
display("I counted to 20!", append=True, output-target="some-div")
...
etc And we would have to change the argument of each of those calls whenever a change in page organization requires changing the output target. And yes, we could use a variable to store the value To return to the 'portable' comment of mine above - I just meant that if you wanted to have multiple identical (or similar) scripts running on the same page, being able to configure the output target a the tag level means the code in each of them looks more similar, and it's easier to reason about them together. (Or copy-paste between them.) Not that this is what folks would universally want to do; I think it would be good practice to say that if your code is coupled to the behavior/layout/placement of the output on the page, then being explicit about the output location wherever possible is good! But for situations where the behavior of the code doesn't really depend (or care) about the output location, being able to configure that at the tag level, I think, is nice. |
Ok. Remove the interaction part of it for a second and abstract it. From an execution standpoint, both are the same thing, just the execution of a block of code.
+1 on that
+0 for right no but really think we should explore solutions right away
That's where we disagree. Typing and verbosity does matter imho. I pretty strongly think that it matters and makes things more accessible. In general, glancing over this, I think I'm pretty much aligned with @JeffersGlass on this. I might be wrong since I'm on a rush but I think it's the case. |
(Apologies for the delay on this, I had hoped to have more time over the weekend.) As promised, I've build a little demo of how this could work using ContextVars, over at JeffersGlass/output-contextvar. It's built on top of @marimeireles work on #749 (much good stuff! And so much code removed!) If you're interested, the easiest thing to do is check out that branch, build it, and load the EDIT: Here are online demos of outputtargeting.html and outputtargeting-async.html as well. Each section of
(*) I wasn't actually sure the best way to check Py-Config from Python, so there's a hard-coded option at the top of PyScript.js: appConfig_default_output_location = "default-location-div"
# appConfig_default_output_location = None #uncomment to simulate no default location set in Py-Config The Like I say, this is more of a demo than a ready-to-merge anything, just to explore what using ContextVars gets us. I'm not wedded to variable names, syntax, arguments, the conditional logic is functional but ugly and unoptimized, and there's definitely untested edge cases (especially with async...) but the bones of the functionality is there. No thought was put into changing/wrapping the content when it gets added, just to where on the page it goes. In any case, hoping this is a helpful touchpoint for conversation, what aspects feel natural and what don't, what's missing and what's too much. |
+2
+2 @JeffersGlass thank you for putting this together!
Should throw an error as it's not allowed (as we've discussed in this issue. I think we converged to that?). Why use ContextVars: At least in my opinion the main reason for using
They were supposed to hold the information of "which tag invoked the |
HI @marimeireles!
The ContextVar is holding "the ID of the tag from the currently running script should go, if the call to display() has no I've updated the demo to highlight the behavior better, and added radio buttons that allow you to simulate:
With a default set in Py-config: (**) Like I noted above, I didn't actually implement the logic to grab that data from <py-config> - it's just hardcoded at at the top of pyscript.py currently. The new toggle button in the demo just updates the value of the global variable created there. The async example also works, but to see it, it's best to build the examples locally and change the value of
I think this is answered by the clarified answer above - if there's no Or at least, that's what I'm proposing. But happy to cut out options from what's presented here. |
I thought this was a good solution too. But I remember having a discussion with @antocuni where he disagreed. Other than that I think this is great :) |
I'm closing this issue as things were mostly resolved here and seems like we're moving away from the |
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
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: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
What is the implicit target of
display
here? There are at least two options:<py-script>
tag in which the code is written. The target forsay_hello()
ispy1
and the example above displayshello world
say_hello
the 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-handlers
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 displayhello
next toClick 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: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=B
when the first loop is still executing. So, my naive implementation above would displayA0 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:
This might technically work, although it has still weird corner cases. Example
sync-and-async
:It sounds very weird that the
A
case is allowed and theB
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:And then, for each
<py-script>
tag we create alocal_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. 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 alsodisplay
must be unique, i.e.:But if
display
must be identical across tags, we can no longer have an uniquelocal_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 couldast.parse()
the code, detect the usage of a globaldisplay
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.:
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-tagparent
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 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:
The text was updated successfully, but these errors were encountered: