-
-
Notifications
You must be signed in to change notification settings - Fork 106
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
Add initial pyodide support #218
Conversation
pygls/protocol.py
Outdated
@@ -378,6 +379,11 @@ def _send_data(self, data): | |||
if not data: | |||
return | |||
|
|||
if IS_PYODIDE: | |||
from js import post_message |
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 requires there be a JavaScript post_message
method be defined on the server's web worker.
See here
f6d1f9e
to
629e6a0
Compare
I would love to help out on this one, I want a to hook up a python LS running in pyodide to a monaco editor using Monaco-Language, and I've identified pygls as the path of least resistance. Thanks for the pull request @alcarney, I was just about to rip multiprocessing out myself when I thought to check open PRs ;) If there's anything I can do to help this land please let me know! |
@willemkokke (and everybody else! :) - @danixeee doesn't have as much time for pygls right now as we'd all like. Open Law Library is looking for someone interested in a part-time (~40hrs/mo) paid position maintaining pygls. We are also looking for volunteer community maintainers. If you know anybody who might be interested, please let me know ([email protected]). |
I'd be interested in helping out as a volunteer where I can :) |
This is such a cool PR! Thank you. We want to get this merged pretty much as is. Then we can start another issue or PR about adding some Pyodide-specific tests to make sure this keeps working. I've just added a couple of comments, if we can clear that up then LGTM. |
Hmm.. the only comment I can see is this one - could you share a link to one of the others? |
|
||
IS_WIN = os.name == 'nt' | ||
IS_PYODIDE = 'pyodide' in sys.modules |
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.
Is there a chance of a false positive here? The only way that pyodide
can be part of sys.modules
is if the parent application explicitly imports it right?
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.
@alcarney how about this? Do you see this?
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.
I see them now.... not sure what happened there.
Is there a chance of a false positive here? The only way that pyodide can be part of sys.modules is if the parent application explicitly imports it right?
I'm assuming Pyodide is doing a little bit of magic to make it available?
It is the suggested method in the project's FAQ for detecting the runtime.
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.
Ok, the suggested method must be pretty tried and tested then. Thanks
pygls/server.py
Outdated
@@ -263,7 +267,7 @@ async def connection_made(websocket, _): | |||
self.shutdown() | |||
|
|||
@property | |||
def thread_pool(self) -> ThreadPool: | |||
def thread_pool(self) -> "ThreadPool": |
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.
Do you recall the reason for the quote marks? "ThreadPool"
is not the same type as ThreadPool
right?
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.
@alcarney and this?
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.
I believe "ThreadPool"
will be resolved to ThreadPool
by tools like mypy during type checking.
If I remember rightly, the issue is that importing ThreadPool
from multiprocessing
crashed under Pyodide so this line disables the import (multiprocessing is not available under Pyodide anyway) meaning we have to switch to using quotes
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.
That makes sense. So would this work too?
if not IS_PYODIDE:
from multiprocessing.pool import ThreadPool
else:
ThreadPool = "ThreadPool"
.
.
def thread_pool(self) -> ThreadPool:
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.
Not sure…. I don’t know the specifics of typing enough to say either way.
Is there a particular reason for doing it that way? Can’t say it’s something I’ve seen before
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.
So I tried the above form out and mypy
didn't like it.
pygls/server.py:39: error: Cannot assign to a type
pygls/server.py:39: error: Incompatible types in assignment (expression has type "str", variable has type "Type[ThreadPool]")
Thanks. So what about this?: if IS_PYODIDE:
ThreadPoolType = "ThreadPool"
else:
from multiprocessing.pool import ThreadPool
ThreadPoolType = ThreadPool
.
.
def thread_pool(self) -> ThreadPoolType: I don't really know enough about mypy either. It just seems a shame to lose typing information when not using Pyodide |
Maybe you can use |
Thanks for all that info! Ok, so quoting is just another valid way of expressing a type, it doesn't mean the type is a literal. So my worry is that somebody in the future will see this break in convention (of expressing the type with quotes when all other types are without quotes) and innocently remove the quotes, breaking Pyodide support (which doesn't have tests yet). More broadly speaking, I think it's best to strive to maintain the precedent that the core code dictates to non-core features. In that sense, here Pyodide should bend to the core, not the other way around. So quoting I like the So here are the options I see:
if IS_PYODIDE:
@property
def thread_pool(self) -> ThreadPool:
...
@property
def thread_pool_executor(self) -> ThreadPoolExecutor:
... |
I like that idea, I imagine it should work and makes it a lot clearer that they can't be relied on in a Pyodide context as they simply won't exist.
Do you want to tackle that as part of this PR? After some very hasty googling I see a couple of options
|
Personally I think the tests can be worked on afterwards, once the PR is merged. We can make a separate issue for that as not having tests for a feature should be considered a bug. You've already done so much cool work here, it's high time it made it into master so others can kick its tyres. So do you want to put that guard at the function-level, then LGTM? |
This mostly involves disabling some things that are not supported by the Pyodide runtime. - Add a constant `IS_PYODIDE` that can be used to check if we're running in the Pyodide runtime. - The `multiprocessing` module is not available so we don't import `ThreadPool`. This does mean that `@server.thread()` methods will not work. - Pyodide provides its own `asyncio` event loop implementation so, setting it in the server's constructor is unecessary. Most of the heavy lifting of the server's main loop can be handled by the web platform. Message objects are serialized to/from JSON to keep the Python <-> JavaScript translation simple. Client -> server messages are delivered by calling the `_procedure_handler` on the protocol class directly. e.g. ```ts self.client_message = JSON.stringify(event.data) await pyodide.runPythonAsync(` from js import client_message message = json.loads(client_message, object_hook=deserialize_message) server.lsp._procedure_handler(message) `) ``` Server -> client messages are delivered by calling a `post_message` JavaScript function that is made available to the Pyodide runtime, where the `post_message` function could be implemented as follows. ```ts self.post_message = (json) => { let obj = JSON.parse(json) postMessage(obj) } ``` See following commits for a more complete pyodide example.
Done, I've also rebased the branch on the latest |
Merged! 🥳 I'll add an issue to add tests... |
Description
With the recent releases of github.dev and vscode.dev I wanted to see if it was possible to use Pyodide to have a
pygls
powered language server running in a web browser. - Turns out it is! 😄It's worth noting that due to the
multiprocessing
module not being available in Pyodide,@server.thread()
methods will not work in the browser, but so far everything else appears to work.This PR makes the few tweaks required to get
pygls
running in a browser context (see commit messages for details) as well as adding a new example extension that demonstrates how to get a simple language server up and running in a web version of VSCode.This isn't quite ready to be merged yet as
I should probably update the docs with some notes on Pyodide, but I thought I'd open it now so that there's an opportunity for feedback 😄mypy
isn't happy about the import from Pyodide'sjs
module andCode review checklist (for code reviewer to complete)