-
-
Notifications
You must be signed in to change notification settings - Fork 981
Make to_js of a dictionary return a LiteralMap
#4576
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
Conversation
This acts like both a Map and an Object. Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
5aec054 to
0c3c5db
Compare
| has: (map, k) => map.has(k) || k in map, | ||
| ownKeys: (map) => | ||
| [...map.keys(), ...ownKeys(map)].filter((x) => | ||
| ["string", "symbol"].includes(typeof x), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like this is a good thing to do but I wonder why this file is mostly a copy/paste of the library as opposite of being the library. It's harder for me to track possible discrepancies if this gets adopted by other interpreters too.
As this and the constructor check seems to be the only difference, and both make sense to me, would you consider using the library if I reflect those changes in there? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, this might break methods explicitly defined (via defineProperty/ies) and I am not super sure why the filter is needed ... I'd love to understand more, if possible, thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This filter is needed because the ownKeys trap raises if the list returned contains anything that is not a string or symbol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I’ve mistaken the key with its value, although is not clear to me when a literal key wouldn’t be a string or symbol but I see this playing a role in the dictionary key case.
would you agree this should rather be upstream in the source lib? 🤔
I will implement this ASAP, with or without your consent 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI this change (only this one + the constructor one as I have concerns about the retro-shadowing) landed and I think it should now work out of the box without surprises.
| get(map, k, proxy) { | ||
| if (k === _) return map; | ||
| let v = map[k]; | ||
| if (typeof v === "function" && k !== "constructor") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is shadowing explicit get and set methods on the literal ... there are not many use cases for these or known APIs but properties descriptors have both get and set possible.
I have explained in the README why the module instead prefers own properties over inherited methods, also because that's how it works in general in JS via prototypal inheritance.
a {get: 123} will also fully break here expectations.
Please re-consider this change as I am not sure it should land in my module too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI MicroPython is also considering to switch to this module to make the Python/JS story more portable across interpreters micropython/micropython#13583 (comment) but if this interpreter decides that {"get": py_method} should break, when explicit, in favor of the underlying Map.prototype.get I think my proposal would fail if not used as-is and prefer other interpreters to just convert by default as object literals, differently from Pyodide ... let's please try to find out the best of both worlds we can offer, considering the current de-facto standard in JS APIs is that config options and parameters are expected to be object literals and that a destructured get in a Map would hardly fail no matter what if expected as value or non-context dependent method due new or foreign JS API design, thank you!
ryanking13
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this!
I agree with WebReflection that it's better to use the literal-map library directly if possible, rather than copying an implementation. If we need to make our implementations different for some reason, let's figure out how to make it easy to tell the difference.
|
FYI today, Tufts in Boston, we had another weird (to explain) use case: a JS module used as configuration (data and/or options) that gets converted into a Python dictionary and then it cannot be used out of the box to a chart.js module that expects object literals as both config and data because the JS to Py and back got lost in translation as data. We had to use 2 workarounds there:
None of this would've been needed if LiteralMap as proposed would've been used instead and, most importantly, no users' expectations would've required me explaining that almost everyone using JS APIs has the same issue and we're trying to work on it in the best possible way. I am telling this simply because I see this is still a Draft and nothing happened since my latest comment ... but we really would like for this to happen, in a way or another, thank you 🙏 |
|
I am willing to add tests and merge this as long as |
|
@hoodmane I am OK doing that and deal with edge cases such as JS descriptors with an explicit I don't think this is the right decision though, but it's definitively better than the current state as we just keep having reported on our side the same issue over and over ... I won't port that to my module because there won't ever be a way to retrieve an explicit |
|
I think it's a good idea overall, but I don't have anything relevant to say on the details of the implementation. The .js file should at least have clear attribution if you decide not to have that library as a dependency. You were probably going to do that anyway I imagine. Thanks for working on this subject! |
I think we all agree on the need for LiteralMap, so I think we can merge it in a backward-compatible way for now and discuss the get descriptor separately. When it comes to the behavior of get descriptors, I can see both sides of the argument, and I don't think there is a 100% right answer. However, if backward incompatible changes are an issue, I think it's better to make the necessary changes quickly. In any case, I think issues with get descriptors will be rare, and the introduction of LiteralMap solves a huge pain point in itself, so I'm very excited about having this change. |
|
@hoodmane hey hey matey! I'm confused (as usual - bear with me). 😉
Surely the behaviour of It feels strange to disallow this behaviour since we're taking such a decision away from the coder. Ergo (unless I'm missing some subtle technical reason - which is likely), I don't think it breaks previous behaviour unless the user is brave/daft enough to override I'm concerned our "by fiat" decision about Of course, and as always, just remember I have a very small brain and likely miss some subtlety in the technical situation, in which case (and only if you have time), I'd love to know more and explore this further. Put simply, I'm not sure this is a technical problem to solve, but a user defined reality of their code, for them to experience... (to misquote Kierkegaard). 😛 |
I agree with both points. My only one is that with current LiteralMap as intended by my module there is an explicit escape hook to be sure the Map get is used, but that's not necessarily reflected in Python code from the JS module, so I see Hood argument too. Mine is that whoever really needs or expect a Map in the JS side and is paranoid enough about the rare
This would be wonderful because it will eventually expose the issue, if ever encountered, unlocking the current state. Last, but not least, I don't care much about attribution if it's not exactly my module, some "thanks to the PyScript team for the idea" in the release notes would be surely appreciated but also not strictly necessary if you don't think that should be there ... it's OK, we just can't wait to stop explaining to our users that when a dictionary is passed to any JS API that's a Map and a Map only by default. Thank you 🙏 |
ryanking13
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for a productive discussion, everyone.
I'll approve this so this can go forward. Codewise looks good to me. Thanks to @WebReflection again for implementing LiteralMap.
Let's merge this as-is and revisit get descriptor issue when we actually encounter such issues from the users. If this issue is being raised frequently by multiple users, there's a good reason to reconsider it, and I think we'll get more real-world feedback from users who have actually experienced the issue.
|
Hurrah, thank you everyone for the work done on this. Lots of people looking forward to this landing..! 🎉 |
|
@ryanking13 thank you a lot for your final thoughts! Some thing to maybe think about later on:
Again, I can't wait wait to see this landing, but I felt like worth it to "plant the seed" about what LiteralMap traps allow to this, and others, project to achieve, in the name of UX. You all have a lovely weekend 👋 |
Yeah, I think this PR still needs documentation updates and some more tests if possible. My approval was to express that I agree with this PR overall, so let's not let it stuck for long (and let's land this at least before the 0.26.0 release :) ).
Yes, I like the phrase "planting the seed". Someday we may change the type translation behavior so that the Dict=>Object conversion should always be the default (or maybe not). However, I think it's very positive to be able to experiment with it in a backward-compatible way before making the big changes. |
|
There are a lot of dictionaries that have non-string keys, and converting dictionaries to Objects does not have a good way to convert them because an Object can only have a string (or symbol) as a key. I don't think we're likely to get the ffi much better than it is by having a completely generic rule that works well for everything. (Though it would be good to add a JavaScript equivalent of class SomeOtherThing(TypedDictCopyToObject):
key1: str
key2: int
class GlobalThis:
@staticmethod
def fetch(url, *, method: str, headers: StringDictCopyToObject[str], some_other_thing: SomeOtherThing):
pass
import js
sys.modules["js_convenient_to_use"] = js.bind_ffi_signature(GlobalThis)This should also give us type hints at the same time. I've started prototyping a system like this, hopefully I'll be able to get something working in the next few months. |
|
@hoodmane I honestly didn't fully follow your concern but nobody in JS APIs expect Python things as keys and, most importantly, both integers and strings are handled exactly the same from a Proxy point of view, even with proxied Arrays in the JS world itself, so I am not sure that example is representative of the issue you are trying to solve. To clarify, in JS const array = ref => new Proxy(ref, {
get(ref, key) {
console.log(typeof key);
// it's always "string" / "symbol"
// in JS proxies everything to get is a string/symbol
return ref[key];
}
});
const proxy = array([1, 2, 3]);
const result = proxy[0];
console.assert(result === 1);
// no Assertion failure here, it's 1edit bear in mind this is true for every single Proxy trap so I understand why |
Yes, I agree that there are challenges to directly converting Python dictionaries to JS objects, including non-string keys. And I think that's why we're introducing LiteralMap. We still use JS Maps to hold non-string keys from Python dictionaries, but we're providing support to use Python dictionaries as JS Object without extra conversion as well. |
I don't think this PR does this, it only changes the behavior of |
|
@hoodmane that’s already a starting point to me … give you time to realize if that’s welcomed and desired, then think bigger (make it part of your default PyProxy logic) imho … or go big all at once, your choice |
Oh, yeah. That's what I meant. |
|
Hi Folks, Good to see discussion going on here...
Tech is all about compromises. The trick is knowing which compromises to make. 😉 So, in this spirit of of trying to figure out the right way forward... the work in the PR is a great first step, and can we perhaps ask folks in our wider communities to reflect and feed back on their unique situations so at least we have a good impression of what either Pyodide or PyScript "do" given certain situations as we move forward...? I can't help but feel we're flying a bit blind without such context and we collectively are at our best when we're open and share such information. We lift each other up, and our worlds mutually grow (which I always appreciate). Speaking personally, there's not a week goes by where I have to explain import js
js.some_module_or_other(config={
"foo": "bar",
})...and it simply doesn't work. We've got this... and I think it'd be wonderful to chat about ways-of-working in this regard at the web assembly summit at PyCon. I'm looking forward to catching up over dinner! 🍻 Have a great weekend folks! 👍 🤗 |
|
So... This PR doesn't affect the behavior of PyProxy itself, but I think it's good enough that it changes the behavior of converting PyProxy to Map with
I am very interested in your idea, but maybe it can go separate from this PR? I mean, I think there are two things we're talking about right now:
This is what this PR does. So people will still need to call
This is what people mostly expect, so that js.some_module_or_other(config={
"foo": "bar",
})just work. So I am open to further discussion on (2), either in another issue or at the PyCon, but I think this PR is sufficient for (1), what do you think? |
Definitely, this pr just needs tests and is ready to go. |
for more information, see https://pre-commit.ci
|
🚀🚀🚀 Awesome! Go @hoodmane Go! 🚀🚀🚀 |
|
Hurrah! |
This acts like both a Map and an Object. Resolves #4030.