Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add web sockets support #128

Merged
merged 30 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cd300a8
Added support for Web sockets based server over PYLSP.
Nov 30, 2021
565aa72
Updated readme with ws configuration usage
Nov 30, 2021
3cb9d24
Update README.md
npradeep357 Nov 30, 2021
268cb34
Updated pylint checks
Dec 1, 2021
eef3471
Fixed pylint issues
npradeep357 Dec 1, 2021
93b731a
Updated trailing spaces issues
npradeep357 Dec 1, 2021
315c8d6
Fixed pycodestyle checks
Dec 2, 2021
e650c90
fixed issue with on close misisng parameters
Dec 3, 2021
25e3fab
- moved websockets information to end of configuration
Dec 4, 2021
8b1cb59
removed unnecessary comments, updated comment
Dec 5, 2021
9371c86
Merge branch 'python-lsp:develop' into develop
npradeep357 Dec 7, 2021
40b98ac
Merge branch 'python-lsp:develop' into develop
npradeep357 Dec 25, 2021
ef5e8a5
Merge branch 'develop' of https://github.com/python-lsp/python-lsp-se…
npradeep357 Jan 24, 2022
07a9dfb
Merge branch 'python-lsp-develop' into develop
npradeep357 Jan 24, 2022
0702f2b
Merge branch 'python-lsp:develop' into develop
npradeep357 Feb 15, 2022
a43e4a1
Updated review comments
npradeep357 Feb 15, 2022
55fa4e7
Merge branch 'python-lsp:develop' into develop
npradeep357 Mar 21, 2022
7fa70f9
Merge branch 'python-lsp:develop' into develop
npradeep357 Apr 2, 2022
3bae1c5
Merge branch 'python-lsp:develop' into develop
npradeep357 Apr 14, 2022
a977e33
Websockets (#4)
npradeep357 Apr 21, 2022
1de7296
Update README.md
npradeep357 Apr 21, 2022
330bab6
removed triling whitespaces
npradeep357 Apr 21, 2022
0cb47b5
removed space between parameters
npradeep357 Apr 21, 2022
297ab9e
Updated debug log
npradeep357 Apr 21, 2022
294e267
changed logic to use single threadpool excutor
npradeep357 Apr 22, 2022
50524d4
Merge branch 'develop' of https://github.com/python-lsp/python-lsp-se…
npradeep357 May 17, 2022
5b192df
Merge branch 'python-lsp-develop' into develop
npradeep357 May 17, 2022
36c07be
updated websockets version to 10.3
npradeep357 May 17, 2022
d54361d
Merge branch 'python-lsp:develop' into develop
npradeep357 May 30, 2022
4f64665
Addressing comments
npradeep357 May 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ As an example, to change the list of errors that pycodestyle will ignore, assumi
3. Same as 1, but add to `setup.cfg` file in the root of the project.


Python LSP Server can communicate over WebSockets when configured as follows:

```
pylsp --ws --port [port]
```

The following libraries are required for Web Sockets support:
- [websockets](https://websockets.readthedocs.io/en/stable/) for Python LSP Server Web sockets using websockets library. refer [Websockets installation](https://websockets.readthedocs.io/en/stable/intro/index.html#installation) for more details

you can install these dependencies with below command:
npradeep357 marked this conversation as resolved.
Show resolved Hide resolved

```
pip install 'python-lsp-server[websockets]'
```

## LSP Server Features

* Auto Completion
Expand Down
9 changes: 8 additions & 1 deletion pylsp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import json

from .python_lsp import (PythonLSPServer, start_io_lang_server,
start_tcp_lang_server)
start_tcp_lang_server, start_ws_lang_server)
from ._version import __version__

LOG_FORMAT = "%(asctime)s {0} - %(levelname)s - %(name)s - %(message)s".format(
Expand All @@ -27,6 +27,10 @@ def add_arguments(parser):
"--tcp", action="store_true",
help="Use TCP server instead of stdio"
)
parser.add_argument(
"--ws", action="store_true",
help="Use Web Sockets server instead of stdio"
)
parser.add_argument(
"--host", default="127.0.0.1",
help="Bind to this address"
Expand Down Expand Up @@ -72,6 +76,9 @@ def main():
if args.tcp:
start_tcp_lang_server(args.host, args.port, args.check_parent_process,
PythonLSPServer)
elif args.ws:
start_ws_lang_server(args.port, args.check_parent_process,
PythonLSPServer)
else:
stdin, stdout = _binary_stdio()
start_io_lang_server(stdin, stdout, args.check_parent_process,
Expand Down
85 changes: 79 additions & 6 deletions pylsp/python_lsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import socketserver
import threading
import ujson as json

from pylsp_jsonrpc.dispatchers import MethodDispatcher
from pylsp_jsonrpc.endpoint import Endpoint
Expand Down Expand Up @@ -91,32 +92,102 @@ def start_io_lang_server(rfile, wfile, check_parent_process, handler_class):
server.start()


def start_ws_lang_server(port, check_parent_process, handler_class):
if not issubclass(handler_class, PythonLSPServer):
raise ValueError('Handler class must be an instance of PythonLSPServer')

# pylint: disable=import-outside-toplevel

# imports needed only for websockets based server
try:
import asyncio
from concurrent.futures import ThreadPoolExecutor
import websockets
except ImportError as e:
raise ImportError("websocket modules missing. Please run pip install 'python-lsp-server[websockets]") from e

with ThreadPoolExecutor(max_workers=10) as tpool:
async def pylsp_ws(websocket):
log.debug("Creating LSP object")

# creating a partial function and suppling the websocket connection
response_handler = partial(send_message, websocket=websocket)

# Not using default stream reader and writer.
# Instead using a consumer based approach to handle processed requests
pylsp_handler = handler_class(rx=None, tx=None, consumer=response_handler,
check_parent_process=check_parent_process)

async for message in websocket:
try:
log.debug("consuming payload and feeding it to LSP handler")
request = json.loads(message)
loop = asyncio.get_running_loop()
await loop.run_in_executor(tpool, pylsp_handler.consume, request)
except Exception as e: # pylint: disable=broad-except
log.exception("Failed to process request %s, %s", message, str(e))

def send_message(message, websocket):
"""Handler to send responses of processed requests to respective web socket clients"""
try:
payload = json.dumps(message, ensure_ascii=False)
asyncio.run(websocket.send(payload))
except Exception as e: # pylint: disable=broad-except
log.exception("Failed to write message %s, %s", message, str(e))

async def run_server():
async with websockets.serve(pylsp_ws, port=port):
# runs forever
await asyncio.Future()

asyncio.run(run_server())


class PythonLSPServer(MethodDispatcher):
""" Implementation of the Microsoft VSCode Language Server Protocol
https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md
"""

# pylint: disable=too-many-public-methods,redefined-builtin

def __init__(self, rx, tx, check_parent_process=False):
def __init__(self, rx, tx, check_parent_process=False, consumer=None):
self.workspace = None
self.config = None
self.root_uri = None
self.watching_thread = None
self.workspaces = {}
self.uri_workspace_mapper = {}

self._jsonrpc_stream_reader = JsonRpcStreamReader(rx)
self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx)
self._check_parent_process = check_parent_process
self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS)

if rx is not None:
self._jsonrpc_stream_reader = JsonRpcStreamReader(rx)
else:
self._jsonrpc_stream_reader = None

if tx is not None:
self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx)
else:
self._jsonrpc_stream_writer = None

# if consumer is None, it is assumed that the default streams-based approach is being used
if consumer is None:
self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS)
else:
self._endpoint = Endpoint(self, consumer, max_workers=MAX_WORKERS)

self._dispatchers = []
self._shutdown = False

def start(self):
"""Entry point for the server."""
self._jsonrpc_stream_reader.listen(self._endpoint.consume)

def consume(self, message):
"""Entry point for consumer based server. Alternate to stream listeners"""
npradeep357 marked this conversation as resolved.
Show resolved Hide resolved
# assuming message will be JSON
self._endpoint.consume(message)

def __getitem__(self, item):
"""Override getitem to fallback through multiple dispatchers."""
if self._shutdown and item != 'exit':
Expand All @@ -141,8 +212,10 @@ def m_shutdown(self, **_kwargs):

def m_exit(self, **_kwargs):
self._endpoint.shutdown()
self._jsonrpc_stream_reader.close()
self._jsonrpc_stream_writer.close()
if self._jsonrpc_stream_reader is not None:
self._jsonrpc_stream_reader.close()
if self._jsonrpc_stream_writer is not None:
self._jsonrpc_stream_writer.close()

def _match_uri_to_workspace(self, uri):
workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pyflakes = ["pyflakes>=2.4.0,<2.5.0"]
pylint = ["pylint>=2.5.0"]
rope = ["rope>0.10.5"]
yapf = ["yapf"]
websockets = ["websockets>=10.3"]
test = [
"pylint>=2.5.0",
"pytest",
Expand Down