From 1cff096c443d8998c4721263fc9173be3da7df34 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 27 Oct 2024 19:23:25 +0000 Subject: [PATCH 01/11] chore: delete pyodide specific code --- pygls/server.py | 25 ---------------------- tests/conftest.py | 33 +++-------------------------- tests/ls_setup.py | 54 +---------------------------------------------- 3 files changed, 4 insertions(+), 108 deletions(-) diff --git a/pygls/server.py b/pygls/server.py index 12df776c..60ede030 100644 --- a/pygls/server.py +++ b/pygls/server.py @@ -42,23 +42,6 @@ logger = logging.getLogger(__name__) -class PyodideTransportAdapter: - """Protocol adapter which overrides write method. - - Write method sends data to stdout. - """ - - def __init__(self, wfile): - self.wfile = wfile - - def close(self): - self.wfile.close() - - def write(self, data): - self.wfile.write(data) - self.wfile.flush() - - class JsonRPCServer: """Base server class @@ -147,14 +130,6 @@ def start_io( finally: self.shutdown() - def start_pyodide(self): - logger.info("Starting Pyodide server") - - # Note: We don't actually start anything running as the main event - # loop will be handled by the web platform. - transport = PyodideTransportAdapter(sys.stdout) - self.protocol.set_writer(transport) # type: ignore[arg-type] - def start_tcp(self, host: str, port: int) -> None: """Starts TCP server.""" logger.info("Starting TCP server on %s:%s", host, port) diff --git a/tests/conftest.py b/tests/conftest.py index 3cdf2e87..974e31ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,19 +24,14 @@ from typing import Optional import pytest -from lsprotocol import types, converters +from lsprotocol import converters, types -from pygls import uris, IS_PYODIDE +from pygls import uris from pygls.feature_manager import FeatureManager from pygls.lsp.client import BaseLanguageClient from pygls.workspace import Workspace -from .ls_setup import ( - NativeClientServer, - PyodideClientServer, - setup_ls_features, -) - +from .ls_setup import ClientServer, setup_ls_features DOC = """document for @@ -50,11 +45,6 @@ WORKSPACE_DIR = REPO_DIR / "examples" / "servers" / "workspace" -ClientServer = NativeClientServer -if IS_PYODIDE: - ClientServer = PyodideClientServer - - @pytest.fixture(autouse=False) def client_server(request): if hasattr(request, "param"): @@ -72,23 +62,6 @@ def client_server(request): client_server.stop() -@pytest.fixture() -def event_loop(): - """Redefine `pytest-asyncio's default event_loop fixture to match the scope - of our client fixture.""" - - policy = asyncio.get_event_loop_policy() - - loop = policy.new_event_loop() - yield loop - - try: - # Not implemented on pyodide - loop.close() - except NotImplementedError: - pass - - @pytest.fixture def feature_manager(): """Return a feature manager""" diff --git a/tests/ls_setup.py b/tests/ls_setup.py index b86f3289..43812b76 100644 --- a/tests/ls_setup.py +++ b/tests/ls_setup.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # # limitations under the License. # ############################################################################ -import json import os import threading @@ -51,58 +50,7 @@ def cmd_test2(ls, *args): # pylint: disable=unused-variable return True, threading.get_ident() -class PyodideTestTransportAdapter: - """Transort adapter that's only useful for tests in a pyodide environment.""" - - def __init__(self, dest: LanguageServer): - self.dest = dest - - def close(self): ... - - def write(self, data): - object_hook = self.dest.protocol._deserialize_message - self.dest.protocol._procedure_handler(json.loads(data, object_hook=object_hook)) - - -class PyodideClientServer: - """Implementation of the `client_server` fixture for use in a pyodide - environment.""" - - def __init__(self, LS=LanguageServer): - self.server = LS("pygls-server", "v1") - self.client = LS("pygls-client", "v1") - - self.server.protocol.set_writer(PyodideTestTransportAdapter(self.client)) - self.server.protocol._include_headers = True - - self.client.protocol.set_writer(PyodideTestTransportAdapter(self.server)) - self.client.protocol._include_headers = True - - def start(self): - self.initialize() - - def stop(self): ... - - @classmethod - def decorate(cls): - return pytest.mark.parametrize("client_server", [cls], indirect=True) - - def initialize(self): - response = self.client.protocol.send_request( - INITIALIZE, - InitializeParams( - process_id=12345, root_uri="file://", capabilities=ClientCapabilities() - ), - ).result(timeout=CALL_TIMEOUT) - - assert response.capabilities is not None - - def __iter__(self): - yield self.client - yield self.server - - -class NativeClientServer: +class ClientServer: def __init__(self, LS=LanguageServer): # Client to Server pipe csr, csw = os.pipe() From 92ec2d65f53f1be93a926cc9bc04c336d34a1b49 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 27 Oct 2024 19:28:51 +0000 Subject: [PATCH 02/11] feat: fallback to a synchronous main loop on WASM platforms This should be all we need to add basic support for both pyodide and wasi WASM runtimes --- pygls/__init__.py | 2 ++ pygls/server.py | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/pygls/__init__.py b/pygls/__init__.py index 147cd9e1..4cd0dc8f 100644 --- a/pygls/__init__.py +++ b/pygls/__init__.py @@ -21,5 +21,7 @@ IS_WIN = os.name == "nt" IS_PYODIDE = "pyodide" in sys.modules +IS_WASI = sys.platform == "wasi" +IS_WASM = IS_PYODIDE or IS_WASI pygls = "pygls" diff --git a/pygls/server.py b/pygls/server.py index 60ede030..6c143a28 100644 --- a/pygls/server.py +++ b/pygls/server.py @@ -25,8 +25,9 @@ import cattrs +from pygls import IS_WASM from pygls.exceptions import JsonRpcException, PyglsError -from pygls.io_ import StdinAsyncReader, StdoutWriter, run_async, run_websocket +from pygls.io_ import StdinAsyncReader, StdoutWriter, run, run_async, run_websocket from pygls.protocol import JsonRPCProtocol if typing.TYPE_CHECKING: @@ -105,8 +106,18 @@ def report_server_error(self, error: Exception, source: ServerErrors): def start_io( self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None ): - """Starts IO server.""" - logger.info("Starting IO server") + """Starts an IO server.""" + + if IS_WASM: + self._start_io_sync(stdin, stdout) + else: + self._start_io_async(stdin, stdout) + + def _start_io_async( + self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None + ): + """Starts an asynchronous IO server.""" + logger.info("Starting async IO server") self._stop_event = Event() reader = StdinAsyncReader(stdin or sys.stdin.buffer, self.thread_pool) @@ -130,6 +141,33 @@ def start_io( finally: self.shutdown() + def _start_io_sync( + self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None + ): + """Starts an synchronous IO server.""" + logger.info("Starting sync IO server") + + self._stop_event = Event() + writer = StdoutWriter(stdout or sys.stdout.buffer) + self.protocol.set_writer(writer) + + try: + asyncio.run( + run( + stop_event=self._stop_event, + reader=stdin or sys.stdin.buffer, + protocol=self.protocol, + logger=logger, + error_handler=self.report_server_error, + ) + ) + except BrokenPipeError: + logger.error("Connection to the client is lost! Shutting down the server.") + except (KeyboardInterrupt, SystemExit): + pass + finally: + self.shutdown() + def start_tcp(self, host: str, port: int) -> None: """Starts TCP server.""" logger.info("Starting TCP server on %s:%s", host, port) From b14e509df0b8f67ee6b8cc6d08a2bf296c544e21 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 27 Oct 2024 19:31:06 +0000 Subject: [PATCH 03/11] test: delete the old pyodide test suite --- poetry.lock | 241 +----------------------- pyproject.toml | 3 - tests/pyodide_testrunner/.gitignore | 1 - tests/pyodide_testrunner/index.html | 56 ------ tests/pyodide_testrunner/run.py | 131 ------------- tests/pyodide_testrunner/test-runner.js | 38 ---- 6 files changed, 2 insertions(+), 468 deletions(-) delete mode 100644 tests/pyodide_testrunner/.gitignore delete mode 100644 tests/pyodide_testrunner/index.html delete mode 100644 tests/pyodide_testrunner/run.py delete mode 100644 tests/pyodide_testrunner/test-runner.js diff --git a/poetry.lock b/poetry.lock index 7a00afed..648aef60 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "alabaster" @@ -127,85 +127,6 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - [[package]] name = "charset-normalizer" version = "3.4.0" @@ -447,17 +368,6 @@ files = [ [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - [[package]] name = "idna" version = "3.10" @@ -762,20 +672,6 @@ rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-bo testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] -[[package]] -name = "outcome" -version = "1.3.0.post0" -description = "Capture the outcome of Python function calls." -optional = false -python-versions = ">=3.7" -files = [ - {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, - {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, -] - -[package.dependencies] -attrs = ">=19.2.0" - [[package]] name = "packaging" version = "24.1" @@ -859,17 +755,6 @@ tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} [package.extras] poetry-plugin = ["poetry (>=1.0,<2.0)"] -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - [[package]] name = "pygments" version = "2.18.0" @@ -884,18 +769,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pysocks" -version = "1.7.1" -description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, - {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, - {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, -] - [[package]] name = "pytest" version = "8.3.3" @@ -1064,36 +937,6 @@ files = [ {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] -[[package]] -name = "selenium" -version = "4.25.0" -description = "Official Python bindings for Selenium WebDriver" -optional = false -python-versions = ">=3.8" -files = [ - {file = "selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33"}, - {file = "selenium-4.25.0.tar.gz", hash = "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921"}, -] - -[package.dependencies] -certifi = ">=2021.10.8" -trio = ">=0.17,<1.0" -trio-websocket = ">=0.9,<1.0" -typing_extensions = ">=4.9,<5.0" -urllib3 = {version = ">=1.26,<3", extras = ["socks"]} -websocket-client = ">=1.8,<2.0" - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "snowballstemmer" version = "2.2.0" @@ -1105,17 +948,6 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -[[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -optional = false -python-versions = "*" -files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] - [[package]] name = "sphinx" version = "7.4.7" @@ -1315,42 +1147,6 @@ files = [ {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] -[[package]] -name = "trio" -version = "0.26.2" -description = "A friendly Python library for async concurrency and I/O" -optional = false -python-versions = ">=3.8" -files = [ - {file = "trio-0.26.2-py3-none-any.whl", hash = "sha256:c5237e8133eb0a1d72f09a971a55c28ebe69e351c783fc64bc37db8db8bbe1d0"}, - {file = "trio-0.26.2.tar.gz", hash = "sha256:0346c3852c15e5c7d40ea15972c4805689ef2cb8b5206f794c9c19450119f3a4"}, -] - -[package.dependencies] -attrs = ">=23.2.0" -cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -idna = "*" -outcome = "*" -sniffio = ">=1.3.0" -sortedcontainers = "*" - -[[package]] -name = "trio-websocket" -version = "0.11.1" -description = "WebSocket library for Trio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, - {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, -] - -[package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -trio = ">=0.11" -wsproto = ">=0.14" - [[package]] name = "typing-extensions" version = "4.12.2" @@ -1373,31 +1169,12 @@ files = [ {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] -[package.dependencies] -pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} - [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - [[package]] name = "websockets" version = "13.1" @@ -1493,20 +1270,6 @@ files = [ {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, ] -[[package]] -name = "wsproto" -version = "1.2.0" -description = "WebSockets state-machine based protocol implementation" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, - {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, -] - -[package.dependencies] -h11 = ">=0.9.0,<1" - [[package]] name = "zipp" version = "3.20.2" @@ -1532,4 +1295,4 @@ ws = ["websockets"] [metadata] lock-version = "2.0" python-versions = ">=3.9" -content-hash = "54ff2807d35912aab4210e984ed466cd22424d226500ea6e696ff7f5ce21d00b" +content-hash = "f928cbd6731950f2d75cbe8057ec2b0ab787713ea4ed47a18f1077d177dc6217" diff --git a/pyproject.toml b/pyproject.toml index 9545afb3..9433ce8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,6 @@ sphinx = ">=7.1.2" sphinx-design = ">=0.5.0" sphinx-rtd-theme = ">=1.3.0" -[tool.poetry.group.pyodide.dependencies] -selenium = "^4.15.2" - [tool.pytest.ini_options] asyncio_mode = "auto" diff --git a/tests/pyodide_testrunner/.gitignore b/tests/pyodide_testrunner/.gitignore deleted file mode 100644 index 704d3075..00000000 --- a/tests/pyodide_testrunner/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.whl diff --git a/tests/pyodide_testrunner/index.html b/tests/pyodide_testrunner/index.html deleted file mode 100644 index b3e1b8b4..00000000 --- a/tests/pyodide_testrunner/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - Pygls Testsuite - - - - - -
-

-    
- - - - - diff --git a/tests/pyodide_testrunner/run.py b/tests/pyodide_testrunner/run.py deleted file mode 100644 index 7b563745..00000000 --- a/tests/pyodide_testrunner/run.py +++ /dev/null @@ -1,131 +0,0 @@ -import os -import pathlib -import shutil -import subprocess -import sys -import tempfile - -from functools import partial -from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer -from multiprocessing import Process, Queue - -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.support import expected_conditions as EC - - -# Path to the root of the repo. -REPO = pathlib.Path(__file__).parent.parent.parent -BROWSERS = { - "chrome": (webdriver.Chrome, webdriver.ChromeOptions), - "firefox": (webdriver.Firefox, webdriver.FirefoxOptions), -} - - -def build_wheel() -> str: - """Build a wheel package of ``pygls`` and its testsuite. - - In order to test pygls under pyodide, we need to load the code for both pygls and its - testsuite. This is done by building a wheel. - - To avoid messing with the repo this is all done under a temp directory. - """ - - with tempfile.TemporaryDirectory() as tmpdir: - # Copy all required files. - dest = pathlib.Path(tmpdir) - - # So that we don't have to fuss with packaging, copy the test suite into `pygls` - # as a sub module. - directories = [("pygls", "pygls"), ("tests", "pygls/tests")] - - for src, target in directories: - shutil.copytree(REPO / src, dest / target) - - files = ["pyproject.toml", "poetry.lock", "README.md", "ThirdPartyNotices.txt"] - - for src in files: - shutil.copy(REPO / src, dest) - - # Convert the lock file to requirements.txt. - # Ensures reproducible behavour for testing. - subprocess.run( - [ - "poetry", - "export", - "-f", - "requirements.txt", - "--output", - "requirements.txt", - ], - cwd=dest, - ) - subprocess.run( - ["poetry", "run", "pip", "install", "-r", "requirements.txt"], cwd=dest - ) - # Build the wheel - subprocess.run(["poetry", "build", "--format", "wheel"], cwd=dest) - whl = list((dest / "dist").glob("*.whl"))[0] - shutil.copy(whl, REPO / "tests/pyodide_testrunner") - - return whl.name - - -def spawn_http_server(q: Queue, directory: str): - """A http server is needed to serve the files to the browser.""" - - handler_class = partial(SimpleHTTPRequestHandler, directory=directory) - server = ThreadingHTTPServer(("localhost", 0), handler_class) - q.put(server.server_port) - - server.serve_forever() - - -def main(): - exit_code = 1 - whl = build_wheel() - - q = Queue() - server_process = Process( - target=spawn_http_server, - args=(q, REPO / "tests/pyodide_testrunner"), - daemon=True, - ) - server_process.start() - port = q.get() - - print("Running tests...") - try: - driver_cls, options_cls = BROWSERS[os.environ.get("BROWSER", "chrome")] - - options = options_cls() - if "CI" in os.environ: - options.binary_location = "/usr/bin/google-chrome" - options.add_argument("--headless") - - driver = driver_cls(options=options) - driver.get(f"http://localhost:{port}?whl={whl}") - - wait = WebDriverWait(driver, 120) - try: - button = wait.until(EC.element_to_be_clickable((By.ID, "exit-code"))) - exit_code = int(button.text) - except WebDriverException as e: - print(f"Error while running test: {e!r}") - exit_code = 1 - - console = driver.find_element(By.ID, "console") - print(console.text) - finally: - if hasattr(server_process, "kill"): - server_process.kill() - else: - server_process.terminate() - - return exit_code - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/pyodide_testrunner/test-runner.js b/tests/pyodide_testrunner/test-runner.js deleted file mode 100644 index dbbc01fd..00000000 --- a/tests/pyodide_testrunner/test-runner.js +++ /dev/null @@ -1,38 +0,0 @@ -importScripts("https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js") - -// Used to redirect pyodide's stdout to the webpage. -function patchedStdout(...args) { - postMessage(args[0]) -} - -async function runTests(whl) { - console.log("Loading pyodide") - let pyodide = await loadPyodide({ - indexURL: "https://cdn.jsdelivr.net/pyodide/v0.21.3/full/" - }) - - console.log("Installing dependencies") - await pyodide.loadPackage("micropip") - await pyodide.runPythonAsync(` - import sys - import micropip - - await micropip.install('pytest') - await micropip.install('pytest-asyncio') - await micropip.install('${whl}') - `) - - console.log('Running testsuite') - - // Patch stdout to redirect the output. - pyodide.globals.get('sys').stdout.write = patchedStdout - await pyodide.runPythonAsync(` - import pytest - exit_code = pytest.main(['--color', 'no', '--pyargs', 'pygls.tests']) - `) - - postMessage({ exitCode: pyodide.globals.get('exit_code') }) -} - -let queryParams = new URLSearchParams(self.location.search) -runTests(queryParams.get('whl')) From 4090fa5a05460e15edeffe69f6caa644f63041a8 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 27 Oct 2024 20:32:35 +0000 Subject: [PATCH 04/11] test: add option to run end-to-end tests under pyodide This adds a new `pyodide` value for the option `--lsp-runtime` argument to the test suite. When given pytest will attempt to use a (pre-setup) pyodide environment to run the end-to-end tests in. This should resolve all the issues we had with the old pyodide test suite as we are not trying to shove pytest, pytest-asyncio etc and all of their complexity into pyodide. --- pyproject.toml | 2 +- tests/conftest.py | 53 ++++++++++++++- tests/e2e/test_threaded_handlers.py | 15 ++++- tests/pyodide/.gitignore | 2 + tests/pyodide/package-lock.json | 48 ++++++++++++++ tests/pyodide/package.json | 10 +++ tests/pyodide/run_server.js | 99 +++++++++++++++++++++++++++++ 7 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 tests/pyodide/.gitignore create mode 100644 tests/pyodide/package-lock.json create mode 100644 tests/pyodide/package.json create mode 100644 tests/pyodide/run_server.js diff --git a/pyproject.toml b/pyproject.toml index 9433ce8b..90d7681c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ sphinx-rtd-theme = ">=1.3.0" asyncio_mode = "auto" [tool.poe.tasks] -test-pyodide = "python tests/pyodide_testrunner/run.py" +test-pyodide = "pytest tests/e2e --lsp-runtime pyodide" ruff = "ruff check ." mypy = "mypy -p pygls" check_generated_code = "python scripts/check_generated_code_is_uptodate.py" diff --git a/tests/conftest.py b/tests/conftest.py index 974e31ea..ef84c9d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -97,7 +97,7 @@ def pytest_addoption(parser): dest="lsp_runtime", action="store", default="cpython", - choices=("cpython",), + choices=("cpython", "pyodide"), help="Choose the runtime in which to run servers under test.", ) @@ -156,7 +156,15 @@ def uri_for(runtime, path_for): def fn(*args): fpath = path_for(*args) - uri = uris.from_fs_path(str(fpath)) + + if runtime == "pyodide": + # Pyodide cannot see the whole file system, so this needs to be made relative to + # the workspace's parent folder + path = str(fpath).replace(str(WORKSPACE_DIR.parent), "") + uri = uris.from_fs_path(path) + + else: + uri = uris.from_fs_path(str(fpath)) assert uri is not None return uri @@ -225,13 +233,52 @@ async def fn( return fn +def get_client_for_pyodide_server(transport, uri_fixture): + """Return a client configured to communicate with a server running under Pyodide. + + This assumes that the pyodide environment has already been bootstrapped. + """ + + if transport != "stdio": + pytest.skip("only STDIO is supported on pyodide") + + async def fn( + server_name: str, capabilities: Optional[types.ClientCapabilities] = None + ): + client = LanguageClient("pygls-test-suite", "v1") + + PYODIDE_DIR = REPO_DIR / "tests" / "pyodide" + server_py = str(SERVER_DIR / server_name) + + await client.start_io("node", str(PYODIDE_DIR / "run_server.js"), server_py) + + response = await client.initialize_async( + types.InitializeParams( + capabilities=capabilities or types.ClientCapabilities(), + root_uri=uri_fixture(""), + ) + ) + assert response is not None + yield client, response + + await client.shutdown_async(None) + client.exit(None) + + await client.stop() + + return fn + + @pytest.fixture(scope="session") def get_client_for(runtime, transport, uri_for): """Return a client configured to communicate with the specified server. Takes into account the current runtime and transport. """ - if runtime not in {"cpython"}: + if runtime not in {"cpython", "pyodide"}: raise NotImplementedError(f"get_client_for: {runtime=}") + if runtime == "pyodide": + return get_client_for_pyodide_server(transport, uri_for) + return get_client_for_cpython_server(transport, uri_for) diff --git a/tests/e2e/test_threaded_handlers.py b/tests/e2e/test_threaded_handlers.py index a33974a5..3615e485 100644 --- a/tests/e2e/test_threaded_handlers.py +++ b/tests/e2e/test_threaded_handlers.py @@ -24,6 +24,7 @@ import pytest import pytest_asyncio from lsprotocol import types + from pygls import IS_WIN from pygls.exceptions import JsonRpcInternalError @@ -126,10 +127,14 @@ async def test_countdown_blocking( async def test_countdown_threaded( threaded_handlers: Tuple[BaseLanguageClient, types.InitializeResult], uri_for, - transport, + runtime: str, + transport: str, ): """Ensure that the countdown threaded command is working as expected.""" + if runtime == "pyodide": + pytest.skip("threads not supported in pyodide") + if (IS_WIN and transport == "tcp") or transport == "websockets": pytest.skip("see https://github.com/openlawlibrary/pygls/issues/502") @@ -198,9 +203,15 @@ async def test_countdown_threaded( @pytest.mark.asyncio(scope="function") async def test_countdown_error( - threaded_handlers: Tuple[BaseLanguageClient, types.InitializeResult], uri_for + threaded_handlers: Tuple[BaseLanguageClient, types.InitializeResult], + uri_for, + runtime: str, ): """Ensure that errors raised in threaded handlers are still handled correctly.""" + + if runtime == "pyodide": + pytest.skip("threads not supported in pyodide") + client, initialize_result = threaded_handlers completion_options = initialize_result.capabilities.completion_provider diff --git a/tests/pyodide/.gitignore b/tests/pyodide/.gitignore new file mode 100644 index 00000000..32ec2fff --- /dev/null +++ b/tests/pyodide/.gitignore @@ -0,0 +1,2 @@ +*.log +node_modules/ diff --git a/tests/pyodide/package-lock.json b/tests/pyodide/package-lock.json new file mode 100644 index 00000000..51e9b5f0 --- /dev/null +++ b/tests/pyodide/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "pyodide_tests", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pyodide_tests", + "version": "0.0.0", + "dependencies": { + "pyodide": "^0.26" + } + }, + "node_modules/pyodide": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.3.tgz", + "integrity": "sha512-cRe9CswiTNTIypOr/RtTb+aR8SxA6KG8aEf3i0OK0nzINLzF4jfVpV5DK2BwP3808ipxoL+ZrnUIjwN5wC8tCw==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/tests/pyodide/package.json b/tests/pyodide/package.json new file mode 100644 index 00000000..9109e3d9 --- /dev/null +++ b/tests/pyodide/package.json @@ -0,0 +1,10 @@ +{ + "name": "pyodide_tests", + "version": "0.0.0", + "description": "Simple wrapper that executes pygls servers in Pyodide", + "main": "run_server.js", + "author": "openlawlibrary", + "dependencies": { + "pyodide": "^0.26" + } +} diff --git a/tests/pyodide/run_server.js b/tests/pyodide/run_server.js new file mode 100644 index 00000000..000793da --- /dev/null +++ b/tests/pyodide/run_server.js @@ -0,0 +1,99 @@ +const fs = require('fs'); +const path = require('path') +const { loadPyodide } = require('pyodide'); + +const consoleLog = console.log + +const WORKSPACE = path.join(__dirname, "..", "..", "examples", "servers", "workspace") +const DIST = path.join(__dirname, "..", "..", "dist") + +// Create a file to log pyodide output to. +const logFile = fs.createWriteStream("pyodide.log") + +function writeToFile(...args) { + logFile.write(args[0] + `\n`); +} + +// Load the workspace into the pyodide runtime. +// +// Unlike WASI, there is no "just works" solution for exposing the workspace/ folder +// to the runtime - it's up to us to manually copy it into pyodide's in-memory filesystem. +function loadWorkspace(pyodide) { + const FS = pyodide.FS + + // Create a folder for the workspace to be copied into. + FS.mkdir('/workspace') + + const workspace = fs.readdirSync(WORKSPACE) + workspace.forEach((file) => { + try { + const filename = "/" + path.join("workspace", file) + // consoleLog(`${file} -> ${filename}`) + + const stream = FS.open(filename, 'w+') + const data = fs.readFileSync(path.join(WORKSPACE, file)) + + FS.write(stream, data, 0, data.length, 0) + FS.close(stream) + } catch (err) { + consoleLog(err) + } + }) +} + +// Find the *.whl file containing the build of pygls to test. +function findWhl() { + const files = fs.readdirSync(DIST); + const whlFile = files.find(file => /pygls-.*\.whl/.test(file)); + + if (whlFile) { + return path.join(DIST, whlFile); + } else { + consoleLog("Unable to find whl file.") + throw new Error("Unable to find whl file."); + } +} + +async function runServer(serverCode) { + // Annoyingly, while we can redirect stderr/stdout to a file during this setup stage + // it doesn't prevent `micropip.install` from indirectly writing to console.log. + // + // Internally, `micropip.install` calls `pyodide.loadPackage` and doesn't expose loadPacakge's + // options for redirecting output i.e. messageCallback. + // + // So instead, we override console.log globally. + console.log = writeToFile + const pyodide = await loadPyodide({ + // stdin: + stderr: writeToFile, + }) + + loadWorkspace(pyodide) + + await pyodide.loadPackage("micropip") + const micropip = pyodide.pyimport("micropip") + await micropip.install(`file://${findWhl()}`) + + // Restore the original console.log + console.log = consoleLog + await pyodide.runPythonAsync(serverCode) +} + +if (process.argv.length < 3) { + console.error("Missing server.py file") + process.exit(1) +} + + +const serverCode = fs.readFileSync(process.argv[2], 'utf8') + +logFile.once('open', (fd) => { + runServer(serverCode).then(() => { + logFile.end(); + process.exit(0) + }).catch(err => { + logFile.write(`Error in server process\n${err}`) + logFile.end(); + process.exit(1); + }) +}) From a8bc5ffeaf102e9b3123c7081f3a8415f05b802c Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 27 Oct 2024 20:35:48 +0000 Subject: [PATCH 05/11] ci: run the new pyodide test suite in CI --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7cf64ac..ed30f55d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,31 +66,44 @@ jobs: poe test test-pyodide: - needs: pre_job + needs: [pre_job, build] if: needs.pre_job.outputs.should_skip != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Use Python "3.10" - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + + - uses: 'actions/setup-node@v4' with: - python-version: "3.10" + node-version: 20.x + cache: 'npm' + cache-dependency-path: 'tests/pyodide/package-lock.json' + + - id: setup-python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install Poetry uses: snok/install-poetry@v1 with: virtualenvs-in-project: true + + - uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: 'dist' + - name: Install Dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: | - poetry install --with pyodide - - name: Run Testsuite - uses: nick-fields/retry@v2 - with: - timeout_minutes: 10 - max_attempts: 6 - command: | - source $VENV - poe test-pyodide || true + poetry install --with test + + cd tests/pyodide + npm ci + + - name: Run tests + run: | + source $VENV + poe test-pyodide lint: needs: pre_job From 7eda10a33affaca7a8600d3ee667c7ef32c5fcec Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 28 Oct 2024 18:53:30 +0000 Subject: [PATCH 06/11] ci: bump action versions This should resolve all the deprecation warnings --- .github/workflows/ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed30f55d..544d033b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,9 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 id: setup-python with: python-version: ${{ matrix.python-version }} @@ -53,7 +53,7 @@ jobs: - name: Load cached venv id: cached-poetry-dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: .venv key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} @@ -110,12 +110,12 @@ jobs: if: needs.pre_job.outputs.should_skip != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Use Python id: use-python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install Poetry @@ -126,7 +126,7 @@ jobs: # I wonder if we can replace this whole step with with the "Use Python" step above's `cache` field? # See: https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#outputs-and-environment-variables id: cached-poetry-dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: .venv key: venv-${{ hashFiles('**/poetry.lock') }}-${{ steps.use-python.outputs.python-version }} @@ -143,11 +143,11 @@ jobs: if: needs.pre_job.outputs.should_skip != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Use Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install Poetry @@ -156,7 +156,7 @@ jobs: virtualenvs-in-project: true - name: Load cached venv id: cached-poetry-dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: .venv key: venv-${{ hashFiles('**/poetry.lock') }} @@ -168,7 +168,7 @@ jobs: git describe --tags --abbrev=0 poetry build - name: Upload builds - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-artifacts path: "dist/*" From 1cf57ff697f39d691758101c6a94e10b5c4e97d7 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 28 Oct 2024 20:24:35 +0000 Subject: [PATCH 07/11] docs: fix intersphinx reference --- docs/source/howto/migrate-to-v2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/howto/migrate-to-v2.rst b/docs/source/howto/migrate-to-v2.rst index 04cecffc..9e7f360e 100644 --- a/docs/source/howto/migrate-to-v2.rst +++ b/docs/source/howto/migrate-to-v2.rst @@ -403,7 +403,7 @@ Removed ``loop`` argument from ``pygls.server.JsonRPCServer`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Servers and clients in pygls v2 now both use the high level asyncio API, removing the need for an explicit ``loop`` argument to be passed in. -If you need control over the event loop used by pygls you can use functions like :external:py:function:`asyncio.set_event_loop` before starting the server/client. +If you need control over the event loop used by pygls you can use functions like :external:py:func:`asyncio.set_event_loop` before starting the server/client. Removed ``multiprocessing.pool.ThreadPool`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From e208844173000c85319e70cabe7b71f60ab436f5 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 28 Oct 2024 20:26:17 +0000 Subject: [PATCH 08/11] docs: add guide on running the pyodide test suite --- docs/source/contributing/howto.rst | 7 ++ .../howto/run-pyodide-test-suite.rst | 95 +++++++++++++++++++ docs/source/index.rst | 8 +- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 docs/source/contributing/howto.rst create mode 100644 docs/source/contributing/howto/run-pyodide-test-suite.rst diff --git a/docs/source/contributing/howto.rst b/docs/source/contributing/howto.rst new file mode 100644 index 00000000..ac595aa0 --- /dev/null +++ b/docs/source/contributing/howto.rst @@ -0,0 +1,7 @@ +How To +====== + +.. toctree:: + :glob: + + Run the Pyodide Tests diff --git a/docs/source/contributing/howto/run-pyodide-test-suite.rst b/docs/source/contributing/howto/run-pyodide-test-suite.rst new file mode 100644 index 00000000..850ad2b3 --- /dev/null +++ b/docs/source/contributing/howto/run-pyodide-test-suite.rst @@ -0,0 +1,95 @@ +How To Run the Pyodide Test Suite +================================= + +.. highlight:: none + +This guide outlines how to setup the environment needed to run the `Pyodide `__ test suite. + +#. If you haven't done so already, install ``pytest`` and the other testing dependencies using poetry:: + + $ poetry install --with test + +#. In order to run Pyodide outside of the browser you will need `NodeJs `__ installed. + +#. Additionaly you will need to install the required node dependencies (which are specified in ``tests/pyodide/package.json``):: + + $ cd tests/pyodide + tests/pyodide $ npm ci + +#. To bootstrap the Python environment within the Pyodide runtime the test suite needs to install ``pygls`` from its wheel archive. + From the repository root, use poetry to package the current development version:: + + $ poetry build + + This will place the required ``*.whl`` file in the ``dist/`` folder + +#. Finally, to run the end-to-end tests against Pyodide, pass the ``--lsp-runtime pyodide`` option to ``pytest``:: + + $ poetry run pytest --lsp-runtime pyodide + ============================================ test session starts ============================================ + platform linux -- Python 3.13.0, pytest-8.3.3, pluggy-1.5.0 + rootdir: /var/home/alex/Projects/openlawlibrary/pygls/main + configfile: pyproject.toml + plugins: cov-5.0.0, asyncio-0.24.0 + asyncio: mode=Mode.AUTO, default_loop_scope=None + pygls: runtime='pyodide', transport='stdio' + collected 216 items + + tests/e2e/test_code_action.py . [ 0%] + tests/e2e/test_code_lens.py ... [ 1%] + tests/e2e/test_colors.py .. [ 2%] + tests/e2e/test_completion.py . [ 3%] + tests/e2e/test_declaration.py . [ 3%] + tests/e2e/test_definition.py . [ 4%] + tests/e2e/test_formatting.py .... [ 6%] + tests/e2e/test_hover.py ... [ 7%] + tests/e2e/test_implementation.py . [ 7%] + tests/e2e/test_inlay_hints.py . [ 8%] + tests/e2e/test_links.py .. [ 9%] + tests/e2e/test_publish_diagnostics.py . [ 9%] + tests/e2e/test_pull_diagnostics.py ... [ 11%] + tests/e2e/test_references.py . [ 11%] + tests/e2e/test_rename.py .... [ 13%] + tests/e2e/test_semantic_tokens.py ...... [ 16%] + tests/e2e/test_symbols.py ... [ 17%] + tests/e2e/test_threaded_handlers.py .ss [ 18%] + tests/e2e/test_type_definition.py . [ 19%] + tests/lsp/test_call_hierarchy.py ..... [ 21%] + tests/lsp/test_document_highlight.py ... [ 23%] + tests/lsp/test_errors.py ..... [ 25%] + tests/lsp/test_folding_range.py ... [ 26%] + tests/lsp/test_linked_editing_range.py ... [ 28%] + tests/lsp/test_moniker.py ... [ 29%] + tests/lsp/test_progress.py ....... [ 32%] + tests/lsp/test_selection_range.py ... [ 34%] + tests/lsp/test_signature_help.py .ss [ 35%] + tests/lsp/test_type_hierarchy.py ..... [ 37%] + tests/test_client.py ... [ 39%] + tests/test_document.py ....................... [ 50%] + tests/test_feature_manager.py ....................................... [ 70%] + tests/test_language_server.py ....... [ 73%] + tests/test_protocol.py .................... [ 82%] + tests/test_server_connection.py ... [ 84%] + tests/test_types.py ... [ 85%] + tests/test_uris.py .......sssss [ 91%] + tests/test_workspace.py ................... [100%] + ================================ 207 passed, 9 skipped in 102.04s (0:01:42) ================================= + + +.. tip:: + + You can find logs from the Pyodide environment in a file called ``pyodide.log`` in the repository root:: + + $ tail -f + Loading micropip, packaging + Loaded micropip, packaging + Loading attrs, six + Loaded attrs, six + Starting sync IO server + Language server initialized InitializeParams(capabilities=ClientCapabilities(workspace=None, text_document=None, notebook_document=None, window=None, general=None, experimental=None), process_id=None, client_info=None, locale=None, root_path=None, root_uri='file:///workspace', initialization_options=None, trace=None, work_done_token=None, workspace_folders=None) + Sending data: {"id": "5f4b70c6-fd2f-4806-985c-ce059c6a1c38", "jsonrpc": "2.0", "result": {"capabilities": {"positionEncoding": "utf-16", "textDocumentSync": {"openClose": true, "change": 2, "save": false}, "declarationProvider": true, "definitionProvider": true, "typeDefinitionProvider": true, "implementationProvider": true, "referencesProvider": true, "executeCommandProvider": {"commands": []}, "workspace": {"workspaceFolders": {"supported": true, "changeNotifications": true}, "fileOperations": {}}}, "serverInfo": {"name": "goto-server", "version": "v1"}}} + Index: {'file:///workspace/code.txt': {'types': {'Rectangle': 0:5-0:14, 'Square': 1:5-1:11}, 'functions': {'area': 3:3-3:7, 'volume': 5:3-5:9}}} + Sending data: {"id": "f61d6b6a-9dab-4c56-b2c4-f751bfbb52da", "jsonrpc": "2.0", "result": null} + Sending data: {"id": "db8b8009-adca-4b48-86a5-b04b622d6426", "jsonrpc": "2.0", "result": {"uri": "file:///workspace/code.txt", "range": {"start": {"line": 0, "character": 5}, "end": {"line": 0, "character": 14}}}} + Sending data: {"id": "e6de9938-5af2-45bf-a730-19e29e6a0465", "jsonrpc": "2.0", "result": null} + Shutting down the server diff --git a/docs/source/index.rst b/docs/source/index.rst index 9560f6a5..08db13f2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,7 +10,7 @@ allows you to write your own `language server`_ in just a few lines of code *pygls* supports -- Python 3.8+ on Windows, MacOS and Linux +- Python 3.9+ on Windows, MacOS and Linux - STDIO, TCP/IP and WEBSOCKET communication - Both sync and async styles of programming - Running code in background threads @@ -25,6 +25,12 @@ allows you to write your own `language server`_ in just a few lines of code How To reference +.. toctree:: + :hidden: + :caption: Contributing + + contributing/howto + .. toctree:: :hidden: :caption: About From 16533a84a39148b0403251d8e40e62242c47495b Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 28 Oct 2024 21:38:51 +0000 Subject: [PATCH 09/11] docs: restructure the documentation... again This commit attempts to add a bit more structure to the documentation, hopefully making it easier to navigate as the number of articles slowly grows --- docs/source/clients/index.rst | 2 + docs/source/index.rst | 70 ++++++++++++++----- docs/source/protocol/howto.rst | 8 +++ .../howto/interpret-semantic-tokens.rst} | 20 +++--- .../howto/tokens/modifiers.html | 0 .../howto/tokens/positions.html | 0 .../{ => protocol}/howto/tokens/types.html | 0 docs/source/pygls/howto.rst | 9 +++ .../{ => pygls}/howto/migrate-to-v1.rst | 0 .../{ => pygls}/howto/migrate-to-v2.rst | 0 docs/source/pygls/reference.rst | 63 +++++++++++++++++ docs/source/{ => pygls}/reference/clients.rst | 0 docs/source/pygls/reference/io.rst | 6 ++ .../source/{ => pygls}/reference/protocol.rst | 0 docs/source/{ => pygls}/reference/servers.rst | 0 docs/source/{ => pygls}/reference/types.rst | 0 docs/source/{ => pygls}/reference/uris.rst | 0 .../{ => pygls}/reference/workspace.rst | 0 docs/source/reference.rst | 8 --- .../{ => servers}/examples/code-actions.rst | 0 .../{ => servers}/examples/code-lens.rst | 0 docs/source/{ => servers}/examples/colors.rst | 0 .../{ => servers}/examples/formatting.rst | 0 docs/source/{ => servers}/examples/goto.rst | 0 docs/source/{ => servers}/examples/hover.rst | 0 .../{ => servers}/examples/inlay-hints.rst | 0 .../{ => servers}/examples/json-server.rst | 0 docs/source/{ => servers}/examples/links.rst | 0 .../examples/publish-diagnostics.rst | 0 .../examples/pull-diagnostics.rst | 0 docs/source/{ => servers}/examples/rename.rst | 0 .../examples/semantic-tokens.rst | 0 .../source/{ => servers}/examples/symbols.rst | 0 .../examples/threaded-handlers.rst | 0 docs/source/{ => servers}/getting-started.rst | 30 ++++---- docs/source/{ => servers}/howto.rst | 3 - .../howto/handle-invalid-data.rst | 0 .../howto/use-the-pygls-playground.rst | 0 docs/source/{ => servers}/tutorial.rst | 0 .../source/{ => servers}/tutorial/0-setup.rst | 0 .../{ => servers}/tutorial/y-testing.rst | 0 .../{ => servers}/tutorial/z-next-steps.rst | 0 docs/source/{ => servers}/user-guide.rst | 0 examples/servers/symbols.py | 2 +- 44 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 docs/source/clients/index.rst create mode 100644 docs/source/protocol/howto.rst rename docs/source/{howto/implement-semantic-tokens.rst => protocol/howto/interpret-semantic-tokens.rst} (97%) rename docs/source/{ => protocol}/howto/tokens/modifiers.html (100%) rename docs/source/{ => protocol}/howto/tokens/positions.html (100%) rename docs/source/{ => protocol}/howto/tokens/types.html (100%) create mode 100644 docs/source/pygls/howto.rst rename docs/source/{ => pygls}/howto/migrate-to-v1.rst (100%) rename docs/source/{ => pygls}/howto/migrate-to-v2.rst (100%) create mode 100644 docs/source/pygls/reference.rst rename docs/source/{ => pygls}/reference/clients.rst (100%) create mode 100644 docs/source/pygls/reference/io.rst rename docs/source/{ => pygls}/reference/protocol.rst (100%) rename docs/source/{ => pygls}/reference/servers.rst (100%) rename docs/source/{ => pygls}/reference/types.rst (100%) rename docs/source/{ => pygls}/reference/uris.rst (100%) rename docs/source/{ => pygls}/reference/workspace.rst (100%) delete mode 100644 docs/source/reference.rst rename docs/source/{ => servers}/examples/code-actions.rst (100%) rename docs/source/{ => servers}/examples/code-lens.rst (100%) rename docs/source/{ => servers}/examples/colors.rst (100%) rename docs/source/{ => servers}/examples/formatting.rst (100%) rename docs/source/{ => servers}/examples/goto.rst (100%) rename docs/source/{ => servers}/examples/hover.rst (100%) rename docs/source/{ => servers}/examples/inlay-hints.rst (100%) rename docs/source/{ => servers}/examples/json-server.rst (100%) rename docs/source/{ => servers}/examples/links.rst (100%) rename docs/source/{ => servers}/examples/publish-diagnostics.rst (100%) rename docs/source/{ => servers}/examples/pull-diagnostics.rst (100%) rename docs/source/{ => servers}/examples/rename.rst (100%) rename docs/source/{ => servers}/examples/semantic-tokens.rst (100%) rename docs/source/{ => servers}/examples/symbols.rst (100%) rename docs/source/{ => servers}/examples/threaded-handlers.rst (100%) rename docs/source/{ => servers}/getting-started.rst (82%) rename docs/source/{ => servers}/howto.rst (54%) rename docs/source/{ => servers}/howto/handle-invalid-data.rst (100%) rename docs/source/{ => servers}/howto/use-the-pygls-playground.rst (100%) rename docs/source/{ => servers}/tutorial.rst (100%) rename docs/source/{ => servers}/tutorial/0-setup.rst (100%) rename docs/source/{ => servers}/tutorial/y-testing.rst (100%) rename docs/source/{ => servers}/tutorial/z-next-steps.rst (100%) rename docs/source/{ => servers}/user-guide.rst (100%) diff --git a/docs/source/clients/index.rst b/docs/source/clients/index.rst new file mode 100644 index 00000000..e2a2993d --- /dev/null +++ b/docs/source/clients/index.rst @@ -0,0 +1,2 @@ +Coming Soon\ :sup:`TM` +====================== diff --git a/docs/source/index.rst b/docs/source/index.rst index 08db13f2..01e79516 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,12 +18,30 @@ allows you to write your own `language server`_ in just a few lines of code .. toctree:: :hidden: - :caption: User Guide + :caption: Language Servers - getting-started - user-guide - How To - reference + servers/getting-started + servers/user-guide + How To + +.. toctree:: + :hidden: + :caption: Language Clients + + clients/index + +.. toctree:: + :hidden: + :caption: The Protocol + + protocol/howto + +.. toctree:: + :hidden: + :caption: The Library + + pygls/howto + pygls/reference .. toctree:: :hidden: @@ -39,34 +57,54 @@ allows you to write your own `language server`_ in just a few lines of code history changelog +Navigation +---------- + +*The pygls documentation tries to (with varying degrees of success!) follow the* `Diátaxis `__ *approach to writing documentation* -The documentation is divided up into the following sections +This documentation site is divided up into the following sections .. grid:: 1 2 2 2 :gutter: 2 - .. grid-item-card:: Getting Started - :link: /getting-started + .. grid-item-card:: Language Servers + :link: servers/getting-started :link-type: doc :text-align: center - First steps with *pygls*. + Documentation specific to implementing Language Servers using *pygls*. + + .. grid-item-card:: Language Clients + :text-align: center + + Documentation specific to implementing Language Clients using *pygls*. + Coming Soon\ :sup:`TM`! - .. grid-item-card:: How To Guides - :link: /howto + .. grid-item-card:: The Protocol + :link: protocol/howto :link-type: doc :text-align: center - Short, focused articles on how to acheive a particular outcome + Additional articles that explain some aspect of the Language Server Protocol in general. - .. grid-item-card:: API Reference - :link: /reference + .. grid-item-card:: The Library + :link: pygls/howto :link-type: doc - :columns: 12 :text-align: center - Comprehensive, detailed documentation on all of the features provided by *pygls*. + Documentation that applies to the *pygls* library itself e.g. migration guides. + + .. grid-item-card:: Contributing + :link: contributing/howto + :link-type: doc + :text-align: center + + Guides on how to contribute to *pygls*. + + .. grid-item-card:: About + :text-align: center + Additional context on the *pygls* project. .. _Language Server Protocol: https://microsoft.github.io/language-server-protocol/specification .. _Language server: https://langserver.org/ diff --git a/docs/source/protocol/howto.rst b/docs/source/protocol/howto.rst new file mode 100644 index 00000000..4059416e --- /dev/null +++ b/docs/source/protocol/howto.rst @@ -0,0 +1,8 @@ +How To +====== + +.. toctree:: + :maxdepth: 1 + :glob: + + Interpret Semantic Tokens diff --git a/docs/source/howto/implement-semantic-tokens.rst b/docs/source/protocol/howto/interpret-semantic-tokens.rst similarity index 97% rename from docs/source/howto/implement-semantic-tokens.rst rename to docs/source/protocol/howto/interpret-semantic-tokens.rst index 12ab467c..a6bd6202 100644 --- a/docs/source/howto/implement-semantic-tokens.rst +++ b/docs/source/protocol/howto/interpret-semantic-tokens.rst @@ -1,8 +1,16 @@ .. _howto-semantic-tokens: -How To Implement Semantic Tokens +How To Interpret Semantic Tokens ================================ +.. seealso:: + + :ref:`Example Server ` + An example implementation of semantic tokens + + :lsp:`textDocument/semanticTokens` + Semantic tokens in the LSP Specification + Semantic Tokens can be thought of as "Syntax Highlighting++". Traditional syntax highlighting is usually implemented as a large collection of :mod:`regular expressions ` and can use the language's grammar rules to tell the difference between say a string, variable or function. @@ -98,15 +106,7 @@ To quote the specification: Finally! We have managed to construct the values we need to apply semantic tokens to the snippet of code we considered at the start -.. figure:: ../../assets/semantic-tokens-example.png +.. figure:: ../../../assets/semantic-tokens-example.png :align: center Our semantic tokens example implemented in VSCode - -.. seealso:: - - :ref:`Example Server ` - An example implementation of semantic tokens - - :lsp:`textDocument/semanticTokens` - Semantic tokens in the LSP Specification diff --git a/docs/source/howto/tokens/modifiers.html b/docs/source/protocol/howto/tokens/modifiers.html similarity index 100% rename from docs/source/howto/tokens/modifiers.html rename to docs/source/protocol/howto/tokens/modifiers.html diff --git a/docs/source/howto/tokens/positions.html b/docs/source/protocol/howto/tokens/positions.html similarity index 100% rename from docs/source/howto/tokens/positions.html rename to docs/source/protocol/howto/tokens/positions.html diff --git a/docs/source/howto/tokens/types.html b/docs/source/protocol/howto/tokens/types.html similarity index 100% rename from docs/source/howto/tokens/types.html rename to docs/source/protocol/howto/tokens/types.html diff --git a/docs/source/pygls/howto.rst b/docs/source/pygls/howto.rst new file mode 100644 index 00000000..123d3139 --- /dev/null +++ b/docs/source/pygls/howto.rst @@ -0,0 +1,9 @@ +How To +====== + +.. toctree:: + :maxdepth: 1 + :glob: + + Migrate to v1 + Migrate to v2 diff --git a/docs/source/howto/migrate-to-v1.rst b/docs/source/pygls/howto/migrate-to-v1.rst similarity index 100% rename from docs/source/howto/migrate-to-v1.rst rename to docs/source/pygls/howto/migrate-to-v1.rst diff --git a/docs/source/howto/migrate-to-v2.rst b/docs/source/pygls/howto/migrate-to-v2.rst similarity index 100% rename from docs/source/howto/migrate-to-v2.rst rename to docs/source/pygls/howto/migrate-to-v2.rst diff --git a/docs/source/pygls/reference.rst b/docs/source/pygls/reference.rst new file mode 100644 index 00000000..4ac7f02a --- /dev/null +++ b/docs/source/pygls/reference.rst @@ -0,0 +1,63 @@ +Python API +========== + +In this section you will find reference documentation on pygls' Python API + +.. toctree:: + :hidden: + :glob: + + reference/* + + +.. grid:: 1 2 2 2 + :gutter: 2 + + .. grid-item-card:: Clients + :link: reference/clients + :link-type: doc + :text-align: center + + pygls' Language Client APIs. + + .. grid-item-card:: Servers + :link: reference/servers + :link-type: doc + :text-align: center + + pygls' Language Server APIs + + .. grid-item-card:: LSP Types + :link: reference/types + :link-type: doc + :text-align: center + + LSP type definitions, as provided by the *lsprotocol* library + + .. grid-item-card:: URIs + :link: reference/types + :link-type: doc + :text-align: center + + Helper functions for working with URIs + + .. grid-item-card:: Workspace + :link: reference/workspace + :link-type: doc + :text-align: center + + pygls' workspace API + + .. grid-item-card:: Protocol + :link: reference/protocol + :link-type: doc + :text-align: center + + pygls' low-level protocol APIs + + .. grid-item-card:: IO + :link: reference/io + :link-type: doc + :text-align: center + + pygls' low-level input/output APIs diff --git a/docs/source/reference/clients.rst b/docs/source/pygls/reference/clients.rst similarity index 100% rename from docs/source/reference/clients.rst rename to docs/source/pygls/reference/clients.rst diff --git a/docs/source/pygls/reference/io.rst b/docs/source/pygls/reference/io.rst new file mode 100644 index 00000000..3bf37bd5 --- /dev/null +++ b/docs/source/pygls/reference/io.rst @@ -0,0 +1,6 @@ +IO +== + + +.. automodule:: pygls.io_ + :members: diff --git a/docs/source/reference/protocol.rst b/docs/source/pygls/reference/protocol.rst similarity index 100% rename from docs/source/reference/protocol.rst rename to docs/source/pygls/reference/protocol.rst diff --git a/docs/source/reference/servers.rst b/docs/source/pygls/reference/servers.rst similarity index 100% rename from docs/source/reference/servers.rst rename to docs/source/pygls/reference/servers.rst diff --git a/docs/source/reference/types.rst b/docs/source/pygls/reference/types.rst similarity index 100% rename from docs/source/reference/types.rst rename to docs/source/pygls/reference/types.rst diff --git a/docs/source/reference/uris.rst b/docs/source/pygls/reference/uris.rst similarity index 100% rename from docs/source/reference/uris.rst rename to docs/source/pygls/reference/uris.rst diff --git a/docs/source/reference/workspace.rst b/docs/source/pygls/reference/workspace.rst similarity index 100% rename from docs/source/reference/workspace.rst rename to docs/source/pygls/reference/workspace.rst diff --git a/docs/source/reference.rst b/docs/source/reference.rst deleted file mode 100644 index 04312347..00000000 --- a/docs/source/reference.rst +++ /dev/null @@ -1,8 +0,0 @@ -API Reference -============= - -.. toctree:: - :maxdepth: 1 - :glob: - - reference/* diff --git a/docs/source/examples/code-actions.rst b/docs/source/servers/examples/code-actions.rst similarity index 100% rename from docs/source/examples/code-actions.rst rename to docs/source/servers/examples/code-actions.rst diff --git a/docs/source/examples/code-lens.rst b/docs/source/servers/examples/code-lens.rst similarity index 100% rename from docs/source/examples/code-lens.rst rename to docs/source/servers/examples/code-lens.rst diff --git a/docs/source/examples/colors.rst b/docs/source/servers/examples/colors.rst similarity index 100% rename from docs/source/examples/colors.rst rename to docs/source/servers/examples/colors.rst diff --git a/docs/source/examples/formatting.rst b/docs/source/servers/examples/formatting.rst similarity index 100% rename from docs/source/examples/formatting.rst rename to docs/source/servers/examples/formatting.rst diff --git a/docs/source/examples/goto.rst b/docs/source/servers/examples/goto.rst similarity index 100% rename from docs/source/examples/goto.rst rename to docs/source/servers/examples/goto.rst diff --git a/docs/source/examples/hover.rst b/docs/source/servers/examples/hover.rst similarity index 100% rename from docs/source/examples/hover.rst rename to docs/source/servers/examples/hover.rst diff --git a/docs/source/examples/inlay-hints.rst b/docs/source/servers/examples/inlay-hints.rst similarity index 100% rename from docs/source/examples/inlay-hints.rst rename to docs/source/servers/examples/inlay-hints.rst diff --git a/docs/source/examples/json-server.rst b/docs/source/servers/examples/json-server.rst similarity index 100% rename from docs/source/examples/json-server.rst rename to docs/source/servers/examples/json-server.rst diff --git a/docs/source/examples/links.rst b/docs/source/servers/examples/links.rst similarity index 100% rename from docs/source/examples/links.rst rename to docs/source/servers/examples/links.rst diff --git a/docs/source/examples/publish-diagnostics.rst b/docs/source/servers/examples/publish-diagnostics.rst similarity index 100% rename from docs/source/examples/publish-diagnostics.rst rename to docs/source/servers/examples/publish-diagnostics.rst diff --git a/docs/source/examples/pull-diagnostics.rst b/docs/source/servers/examples/pull-diagnostics.rst similarity index 100% rename from docs/source/examples/pull-diagnostics.rst rename to docs/source/servers/examples/pull-diagnostics.rst diff --git a/docs/source/examples/rename.rst b/docs/source/servers/examples/rename.rst similarity index 100% rename from docs/source/examples/rename.rst rename to docs/source/servers/examples/rename.rst diff --git a/docs/source/examples/semantic-tokens.rst b/docs/source/servers/examples/semantic-tokens.rst similarity index 100% rename from docs/source/examples/semantic-tokens.rst rename to docs/source/servers/examples/semantic-tokens.rst diff --git a/docs/source/examples/symbols.rst b/docs/source/servers/examples/symbols.rst similarity index 100% rename from docs/source/examples/symbols.rst rename to docs/source/servers/examples/symbols.rst diff --git a/docs/source/examples/threaded-handlers.rst b/docs/source/servers/examples/threaded-handlers.rst similarity index 100% rename from docs/source/examples/threaded-handlers.rst rename to docs/source/servers/examples/threaded-handlers.rst diff --git a/docs/source/getting-started.rst b/docs/source/servers/getting-started.rst similarity index 82% rename from docs/source/getting-started.rst rename to docs/source/servers/getting-started.rst index bdfba7cb..4851c24d 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/servers/getting-started.rst @@ -22,91 +22,91 @@ Each of the following example servers are focused on implementing a particular s :gutter: 2 .. grid-item-card:: Code Actions - :link: /examples/code-actions + :link: examples/code-actions :link-type: doc :text-align: center :octicon:`light-bulb` .. grid-item-card:: Code Lens - :link: /examples/code-lens + :link: examples/code-lens :link-type: doc :text-align: center :octicon:`eye` .. grid-item-card:: Colors - :link: /examples/colors + :link: examples/colors :link-type: doc :text-align: center :octicon:`paintbrush` .. grid-item-card:: Formatting - :link: /examples/formatting + :link: examples/formatting :link-type: doc :text-align: center :octicon:`typography` .. grid-item-card:: Goto "X" - :link: /examples/goto + :link: examples/goto :link-type: doc :text-align: center :octicon:`search` .. grid-item-card:: Hover - :link: /examples/hover + :link: examples/hover :link-type: doc :text-align: center :octicon:`book` .. grid-item-card:: Inlay Hints - :link: /examples/inlay-hints + :link: examples/inlay-hints :link-type: doc :text-align: center :octicon:`info` .. grid-item-card:: Links - :link: /examples/links + :link: examples/links :link-type: doc :text-align: center :octicon:`link` .. grid-item-card:: Publish Diagnostics - :link: /examples/publish-diagnostics + :link: examples/publish-diagnostics :link-type: doc :text-align: center :octicon:`alert` .. grid-item-card:: Pull Diagnostics - :link: /examples/pull-diagnostics + :link: examples/pull-diagnostics :link-type: doc :text-align: center :octicon:`alert` .. grid-item-card:: Rename - :link: /examples/rename + :link: examples/rename :link-type: doc :text-align: center :octicon:`pencil` .. grid-item-card:: Semantic Tokens - :link: /examples/semantic-tokens + :link: examples/semantic-tokens :link-type: doc :text-align: center :octicon:`file-binary` .. grid-item-card:: Symbols - :link: /examples/symbols + :link: examples/symbols :link-type: doc :text-align: center @@ -118,14 +118,14 @@ These servers are dedicated to demonstrating features of *pygls* itself :gutter: 2 .. grid-item-card:: JSON Server - :link: /examples/json-server + :link: examples/json-server :link-type: doc :text-align: center :octicon:`code` .. grid-item-card:: Threaded Handlers - :link: /examples/threaded-handlers + :link: examples/threaded-handlers :link-type: doc :text-align: center diff --git a/docs/source/howto.rst b/docs/source/servers/howto.rst similarity index 54% rename from docs/source/howto.rst rename to docs/source/servers/howto.rst index 71d32960..13cb2555 100644 --- a/docs/source/howto.rst +++ b/docs/source/servers/howto.rst @@ -5,7 +5,4 @@ How To Guides :maxdepth: 1 Handle Invalid Data - Implement Semantic Tokens - Migrate to v1 - Migrate to v2 Use the pygls-playground diff --git a/docs/source/howto/handle-invalid-data.rst b/docs/source/servers/howto/handle-invalid-data.rst similarity index 100% rename from docs/source/howto/handle-invalid-data.rst rename to docs/source/servers/howto/handle-invalid-data.rst diff --git a/docs/source/howto/use-the-pygls-playground.rst b/docs/source/servers/howto/use-the-pygls-playground.rst similarity index 100% rename from docs/source/howto/use-the-pygls-playground.rst rename to docs/source/servers/howto/use-the-pygls-playground.rst diff --git a/docs/source/tutorial.rst b/docs/source/servers/tutorial.rst similarity index 100% rename from docs/source/tutorial.rst rename to docs/source/servers/tutorial.rst diff --git a/docs/source/tutorial/0-setup.rst b/docs/source/servers/tutorial/0-setup.rst similarity index 100% rename from docs/source/tutorial/0-setup.rst rename to docs/source/servers/tutorial/0-setup.rst diff --git a/docs/source/tutorial/y-testing.rst b/docs/source/servers/tutorial/y-testing.rst similarity index 100% rename from docs/source/tutorial/y-testing.rst rename to docs/source/servers/tutorial/y-testing.rst diff --git a/docs/source/tutorial/z-next-steps.rst b/docs/source/servers/tutorial/z-next-steps.rst similarity index 100% rename from docs/source/tutorial/z-next-steps.rst rename to docs/source/servers/tutorial/z-next-steps.rst diff --git a/docs/source/user-guide.rst b/docs/source/servers/user-guide.rst similarity index 100% rename from docs/source/user-guide.rst rename to docs/source/servers/user-guide.rst diff --git a/examples/servers/symbols.py b/examples/servers/symbols.py index 1e4c4a24..3a565d75 100644 --- a/examples/servers/symbols.py +++ b/examples/servers/symbols.py @@ -30,7 +30,7 @@ This server implements these requests for the pretend programming language featured in the ``code.txt`` in the example workspace in the *pygls* repository. -.. literalinclude:: ../../../examples/servers/workspace/code.txt +.. literalinclude:: ../../../../examples/servers/workspace/code.txt :language: none """ From 901a24d692a6ad2a3844c29708ff107730749236 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Wed, 30 Oct 2024 19:54:30 +0000 Subject: [PATCH 10/11] docs: add guide on running a pyodide server on nodejs --- docs/source/conf.py | 1 + docs/source/index.rst | 1 + docs/source/servers/howto.rst | 1 + .../servers/howto/run-a-server-in-pyodide.rst | 199 ++++++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 docs/source/servers/howto/run-a-server-in-pyodide.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 0429f31c..e259f7c1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,6 +69,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), + "pyodide": ("https://pyodide.org/en/stable", None), } # Add any paths that contain templates here, relative to this directory. diff --git a/docs/source/index.rst b/docs/source/index.rst index 01e79516..1525d0f7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,7 @@ allows you to write your own `language server`_ in just a few lines of code *pygls* supports - Python 3.9+ on Windows, MacOS and Linux +- **Experimental** support for Pyodide - STDIO, TCP/IP and WEBSOCKET communication - Both sync and async styles of programming - Running code in background threads diff --git a/docs/source/servers/howto.rst b/docs/source/servers/howto.rst index 13cb2555..1232c968 100644 --- a/docs/source/servers/howto.rst +++ b/docs/source/servers/howto.rst @@ -5,4 +5,5 @@ How To Guides :maxdepth: 1 Handle Invalid Data + Run a Server in Pyodide Use the pygls-playground diff --git a/docs/source/servers/howto/run-a-server-in-pyodide.rst b/docs/source/servers/howto/run-a-server-in-pyodide.rst new file mode 100644 index 00000000..4ffeda8c --- /dev/null +++ b/docs/source/servers/howto/run-a-server-in-pyodide.rst @@ -0,0 +1,199 @@ +.. _howto-use-pyodide: + +How To Run a Server in Pyodide +============================== + +.. highlight:: none + +`Pyodide `__ provides a version of the CPython interpreter compiled for WebAssembly, allowing you to execute Python programs either in a web browser or in NodeJS. + +This guide outlines how to run your *pygls* server in such an environment. + +.. important:: + + This environment imposes some `restrictions and limitations `__ to consider. + The most obvious restrictions are: + + - only the STDIO method of communication is supported + - threads are unavailable, so your server cannot use the :meth:`@server.thread() ` decorator + - while it *is* possible to use async-await syntax in Pyodide, *pygls* does not currently enable it by default. + +The setup is slightly different depending on if you are running your server via the :ref:`Browser ` or :ref:`NodeJs ` + +.. _howto-use-pyodide-in-node: + +Using NodeJS +------------ + +The most likely use case for using NodeJS is testing that your server works in Pyodide without requiring the use of a browser testing tool like `Selenium `__. +In fact, this is how we test that *pygls* works correctly when running under Pyodide. + +To help illustrate the steps required, we will use pygls' test suite as an example. + +.. tip:: + + You can find the complete setup in the `tests/pyodide `__ folder of the pygls repository. + +Writing our Python code as normal, each server is executed with the help of a wrapper script:: + + $ node run_server.js /path/to/server.py + +The simplest wrapper script might look something like the following + +.. code-block:: javascript + + const fs = require('fs'); + const { loadPyodide } = require('pyodide'); + + async function runServer(serverCode) { + // Initialize pyodide. + const pyodide = await loadPyodide() + + // Install dependencies + await pyodide.loadPackage("micropip") + const micropip = pyodide.pyimport("micropip") + await micropip.install("pygls") + + // Run the server + await pyodide.runPythonAsync(serverCode) + } + + if (process.argv.length < 3) { + console.error("Missing server.py file") + process.exit(1) + } + + // Read the contents of the given `server.py` file. + const serverCode = fs.readFileSync(process.argv[2], 'utf8') + + runServer(serverCode).then(() => { + process.exit(0) + }).catch(err => { + process.exit(1); + }) + +The above code is assuming that the given Python script ends with a call to your server's :meth:`~pygls.server.JsonRPCServer.start_io` method. + +Redirecting Output +^^^^^^^^^^^^^^^^^^ + +Unfortunately, if you tried the above script you will find that your language client wouldn't be able to establish a connection with the server. +This is due to fact Pyodide will print some log messages to ``stdout`` interfering with the client's communication with the server:: + + Loading micropip, packaging + Loaded micropip, packaging + Loading attrs, six + Loaded attrs, six + ... + +To work around this in ``run_server.js`` we create a function that will write to a log file. + +.. code-block:: javascript + + const consoleLog = console.log + const logFile = fs.createWriteStream("pyodide.log") + + function writeToFile(...args) { + logFile.write(args[0] + `\n`); + } + +And we use it to temporarily override ``console.log`` during startup + +.. code-block:: javascript + + async function runServer(serverCode) { + // Annoyingly, while we can redirect stderr/stdout to a file during this setup stage + // it doesn't prevent `micropip.install` from indirectly writing to console.log. + // + // Internally, `micropip.install` calls `pyodide.loadPackage` and doesn't expose loadPackage's + // options for redirecting output i.e. messageCallback. + // + // So instead, we override console.log globally. + console.log = writeToFile + const pyodide = await loadPyodide({ + // stdin: + stderr: writeToFile, + }) + + await pyodide.loadPackage("micropip") + const micropip = pyodide.pyimport("micropip") + await micropip.install("pygls") + + // Restore the original console.log + console.log = consoleLog + await pyodide.runPythonAsync(serverCode) + } + +While we're redirecting output, we may as well also pass the ``writeToFile`` function to pyodide's ``stderr`` channel. +That way we're also able to see the server's logging output while it's running! + +.. important:: + + Since node's ``fs`` API is asynchronous, don't forget to only start the server once the log file has been opened! + + .. code-block:: javascript + + logFile.once('open', (fd) => { + runServer(serverCode).then(() => { + logFile.end(); + process.exit(0) + }).catch(err => { + logFile.write(`Error in server process\n${err}`) + logFile.end(); + process.exit(1); + }) + }) + +Workspace Access +^^^^^^^^^^^^^^^^ + +.. seealso:: + + - :external+pyodide:std:doc:`usage/file-system` + - :external+pyodide:std:ref:`accessing_files_quickref` + +At this point we're able to get a server up and running however, it wouldn't be able to access any files! +There are many ways to approach exposing your files to the server (see the above resources), but for the pygls test suite we copy them into Pyodide's in-memory filesystem before starting the server. + +.. code-block:: javascript + + const path = require('path') + const WORKSPACE = path.join(__dirname, "..", "..", "examples", "servers", "workspace") + + function loadWorkspace(pyodide) { + const FS = pyodide.FS + + // Create a folder for the workspace to be copied into. + FS.mkdir('/workspace') + + const workspace = fs.readdirSync(WORKSPACE) + workspace.forEach((file) => { + try { + const filename = "/" + path.join("workspace", file) + // consoleLog(`${file} -> ${filename}`) + + const stream = FS.open(filename, 'w+') + const data = fs.readFileSync(path.join(WORKSPACE, file)) + + FS.write(stream, data, 0, data.length, 0) + FS.close(stream) + } catch (err) { + consoleLog(err) + } + }) + } + + async function runServer() { + // ... + loadWorkspace(pyodide) + // ... + } + +It's important to note that this **WILL NOT** synchronise any changes made within the Pyodide runtime back to the source filesystem, but for the purpose of pygls' test suite it is sufficient. + +It's also important to note that your language client will need to send URIs that make sense to server's environment i.e. ``file:///workspace/sums.txt`` and not ``file:///home/username/Projects/pygls/examples/servers/workspace/sums.txt``. + +.. _howto-use-pyodide-in-browser: + +Using the Browser +----------------- From 89bc11acec6acc219ca0f42d4992d2f733a2e571 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 2 Nov 2024 18:04:30 +0000 Subject: [PATCH 11/11] docs: add guide on running a pyodide server in the browser --- .../servers/howto/run-a-server-in-pyodide.rst | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/docs/source/servers/howto/run-a-server-in-pyodide.rst b/docs/source/servers/howto/run-a-server-in-pyodide.rst index 4ffeda8c..8430d683 100644 --- a/docs/source/servers/howto/run-a-server-in-pyodide.rst +++ b/docs/source/servers/howto/run-a-server-in-pyodide.rst @@ -197,3 +197,202 @@ It's also important to note that your language client will need to send URIs tha Using the Browser ----------------- + +.. seealso:: + + `monaco-languageclient `__ GitHub repository + For plenty of examples on how to build an in-browser client on top of the `monaco editor `__ + + `This commit `__ + For an (outdated!) example on building a simple language client for pygls servers in the browser. + +Getting your pygls server to run in a web browser using Pyodide as the runtime *is possible*. +Unfortunately, it is not necessarily *easy* - mostly because you will most likely have to build your own language client at the same time! + +While building an in-browser language client is beyond the scope of this article, we can provide some suggestions to get you started - and if you figure out a nicer way please let us know! + +WebWorkers +^^^^^^^^^^ + +Running your language server in the browser's main thread is not a great idea since any time your server is processing some message it will block the UI. +Instead we can run the server in a `WebWorker `__, which we can think of as the browser's version of a background thread. + +Using the `monaco-editor-wrapper `__ project, connecting your server to the client can be as simple as a few lines of configuration + +.. code-block:: typescript + + import '@codingame/monaco-vscode-python-default-extension'; + import { MonacoEditorLanguageClientWrapper, UserConfig } from 'monaco-editor-wrapper' + + export async function run(containerId: string) { + const wrapper = new MonacoEditorLanguageClientWrapper() + const userConfig: UserConfig = { + wrapperConfig: { + editorAppConfig: { + $type: 'extended', + codeResources: { + main: { + text: '1 + 1 =', + uri: '/workspace/sums.txt', + enforceLanguageId: 'plaintext' + } + } + } + }, + languageClientConfig: { + languageId: 'plaintext', + options: { + $type: 'WorkerDirect', + worker: new Worker('/run_server.js') + }, + } + } + + const container = document.getElementById(containerId) + await wrapper.initAndStart(userConfig, container) + } + +Where ``run_server.js`` is a slightly different version of the wrapper script we used for the NodeJS section above. + +Overview +^^^^^^^^ + +.. seealso:: + + :external+pyodide:std:doc:`usage/webworker` + + +Unlike all the other ways you will have run a pygls server up until now, the client and server will not be communicating by reading/writing bytes to/from each other. +Intead they will be passing JSON objects directly using the ``onmessage`` event and ``postMessage`` functions. +As a result, we will not be calling one of the server's ``start_xx`` methods either, instead we will rely on the events we receive from the client "drive" the server. + +.. raw:: html + + + + + Client + + + Server + + + onmessage + + + postMessage + + + + + + + + + + +Also note that since our server code is running in a WebWorker, we will need to use the `importScripts `__ function to pull in the Pyodide library. + +.. code-block:: typescript + + importScripts("https://cdn.jsdelivr.net/pyodide//full/pyodide.js"); + + async function initPyodide() { + // TODO + } + + const pyodidePromise = initPyodide() + + onmessage = async (event) => { + let pyodide = await pyodidePromise + // TODO + } + +By awaiting ``pyodidePromise`` in the ``onmessage``, we ensure that Pyodide and all our server code is ready before attempting to handle any messages. + +Initializing Pyodide +^^^^^^^^^^^^^^^^^^^^ + +The ``initPyodide`` function is fairly similar to the ``runServer`` function from the NodeJS example above. +The main differences are + +- We are now redirecting ``stderr`` to ``console.log`` rather than a file +- We are now also redirecting ``stdout``, parsing the JSON objects being written out and passing them to the ``postMessage`` function to send them onto the client. +- We **are not** calling ``server.start_io`` in our server init code. + +.. code-block:: typescript + + async function initPyodide() { + console.log("Initializing pyodide.") + + /* @ts-ignore */ + let pyodide = await loadPyodide({ + stderr: console.log + }) + + console.log("Installing dependencies.") + await pyodide.loadPackage(["micropip"]) + await pyodide.runPythonAsync(` + import micropip + await micropip.install('pygls') + `) + + // See https://pyodide.org/en/stable/usage/api/js-api.html#pyodide.setStdout + pyodide.setStdout({ batched: (msg) => postMessage(JSON.parse(msg)) }) + + console.log("Loading server.") + await pyodide.runPythonAsync(`<>`) + return pyodide + } + +Initializing the Server +^^^^^^^^^^^^^^^^^^^^^^^ + +Since we are not calling the server's ``start_io`` method, we need to configure the server to tell it where to write its messages. +Ideally, this would be done by calling the :meth:`~pygls.protocol.JsonRPCProtocol.set_writer` method on the server's ``protocol`` object. + +However, at the time of writing there is `a bug `__ in Pyodide where output is not flushed correctly, even if you call a method like ``sys.stdout.flush()`` + +To work around this, we will instead override one of the ``protocol`` object's methods to output the server's messages as a sequence of newline separated JSON strings. + +.. code-block:: python + + # Hack to workaround https://github.com/pyodide/pyodide/issues/4139 + def send_data(data): + body = json.dumps(data, default=server.protocol._serialize_message) + sys.stdout.write(f"{body}\n") + sys.stdout.flush() + + server.protocol._send_data = send_data + +The above code snippet should be included along with your server's init code. + +Handling Messages +^^^^^^^^^^^^^^^^^ + +Finally, with the server prepped to send messages, the only thing left to do is to implement the ``onmessage`` handler. + +.. code-block:: typescript + + const pyodidePromise = initPyodide() + + onmessage = async (event) => { + let pyodide = await pyodidePromise + console.log(event.data) + + /* @ts-ignore */ + self.client_message = JSON.stringify(event.data) + + // Run Python synchronously to ensure that messages are processed in the correct order. + pyodide.runPython(` + from js import client_message + message = json.loads(client_message, object_hook=server.protocol.structure_message) + server.protocol.handle_message(message) + `) + } + +The above handler + +- Converts incoming JSON objects to a string and stores them in the ``client_message`` attribute on the WebWorker itself +- Our server code is then able to access the ``client_message`` via the ``js`` module provided by Pyodide +- The server parses and handles the given message.