chore(lsprotocol): migrate to 2025.0.0 and cleanup artifacts
The changes align the project with the 2025.0.0 lsprotocol release, removing the old backport and updating type hints in the protocol hooks to use Sequence where appropriate. The dist-info and packaging metadata for older lsprotocol versions are replaced with the new 2025.0.0 artifacts. - Remove exceptiongroup backport used on Python <3.11 - Use Sequence instead of List in LS protocol hooks - Replace old dist-info with 2025.0.0 metadata
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
############################################################################
|
||||
# Copyright(c) Open Law Library. All rights reserved. #
|
||||
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License") #
|
||||
# you may not use this file except in compliance with the License. #
|
||||
# You may obtain a copy of the License at #
|
||||
# #
|
||||
# http: // www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
|
||||
from pygls.exceptions import JsonRpcException
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Awaitable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, BinaryIO, Callable, Protocol
|
||||
|
||||
from websockets.asyncio.client import ClientConnection
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
|
||||
from pygls.protocol import JsonRPCProtocol
|
||||
|
||||
class Reader(Protocol):
|
||||
"""An synchronous reader."""
|
||||
|
||||
def readline(self) -> bytes: ...
|
||||
|
||||
def read(self, n: int) -> bytes: ...
|
||||
|
||||
class Writer(Protocol):
|
||||
"""An synchronous writer."""
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
def write(self, data: bytes) -> None: ...
|
||||
|
||||
class AsyncReader(typing.Protocol):
|
||||
"""An asynchronous reader."""
|
||||
|
||||
def readline(self) -> Awaitable[bytes]: ...
|
||||
|
||||
def readexactly(self, n: int) -> Awaitable[bytes]: ...
|
||||
|
||||
class AsyncWriter(typing.Protocol):
|
||||
"""An asynchronous writer."""
|
||||
|
||||
def close(self) -> Awaitable[None]: ...
|
||||
|
||||
def write(self, data: bytes) -> Awaitable[None]: ...
|
||||
|
||||
|
||||
class StdinAsyncReader:
|
||||
"""Read from stdin asynchronously."""
|
||||
|
||||
def __init__(self, stdin: BinaryIO, executor: ThreadPoolExecutor | None = None):
|
||||
self.stdin = stdin
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
self.executor = executor
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
if self._loop is None:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
|
||||
return self._loop
|
||||
|
||||
def readline(self) -> Awaitable[bytes]:
|
||||
return self.loop.run_in_executor(self.executor, self.stdin.readline)
|
||||
|
||||
def readexactly(self, n: int) -> Awaitable[bytes]:
|
||||
return self.loop.run_in_executor(self.executor, self.stdin.read, n)
|
||||
|
||||
|
||||
class StdoutWriter:
|
||||
"""Align a stdout stream with pygls' writer interface."""
|
||||
|
||||
def __init__(self, stdout: BinaryIO):
|
||||
self._stdout = stdout
|
||||
|
||||
def close(self):
|
||||
self._stdout.close()
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
self._stdout.write(data)
|
||||
self._stdout.flush()
|
||||
|
||||
|
||||
class WebSocketWriter:
|
||||
"""Align a websocket connection with pygls' writer interface"""
|
||||
|
||||
def __init__(self, ws: ServerConnection | ClientConnection):
|
||||
self._ws = ws
|
||||
|
||||
def close(self) -> Awaitable[None]:
|
||||
return self._ws.close()
|
||||
|
||||
def write(self, data: bytes) -> Awaitable[None]:
|
||||
return self._ws.send(data)
|
||||
|
||||
|
||||
async def run_async(
|
||||
stop_event: threading.Event,
|
||||
reader: AsyncReader,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run a main message processing loop, asynchronously
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
reader
|
||||
The reader to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
"""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
content_length = 0
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = await reader.readline()
|
||||
if not header:
|
||||
break
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = await reader.readexactly(content_length)
|
||||
if not body:
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(body, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
finally:
|
||||
# Reset
|
||||
content_length = 0
|
||||
|
||||
|
||||
def run(
|
||||
stop_event: threading.Event,
|
||||
reader: Reader,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run a main message processing loop, synchronously
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
reader
|
||||
The reader to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
|
||||
error_handler
|
||||
Function to call when an error is encountered.
|
||||
"""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
content_length = 0
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = reader.readline()
|
||||
if not header:
|
||||
break
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = reader.read(content_length)
|
||||
if not body:
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(body, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
finally:
|
||||
# Reset
|
||||
content_length = 0
|
||||
|
||||
|
||||
async def run_websocket(
|
||||
websocket: ClientConnection | ServerConnection,
|
||||
stop_event: threading.Event,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run the main message processing loop, over websockets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
websocket
|
||||
The websocket to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
|
||||
error_handler
|
||||
Function to call when an error is encountered.
|
||||
"""
|
||||
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
protocol.set_writer(WebSocketWriter(websocket), include_headers=False)
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
||||
)
|
||||
return
|
||||
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
logger.debug("waiting for a message...")
|
||||
data = await websocket.recv(decode=False)
|
||||
except ConnectionClosed:
|
||||
logger.debug("Websocket connection closed.")
|
||||
stop_event.set()
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(data, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
|
||||
logger.debug("Exiting main loop")
|
||||
await websocket.close()
|
||||
Reference in New Issue
Block a user