Files
nx_post_support/server/libs/pygls/protocol/json_rpc.py
T
Christoph Brandau 53ebc5d055 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
2026-09-03 08:39:12 +02:00

736 lines
23 KiB
Python

############################################################################
# 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 contextvars
import enum
import inspect
import json
import logging
import sys
import traceback
import typing
import uuid
from concurrent.futures import Future
from functools import partial
from typing import Any, Callable, Protocol, Type, Union, runtime_checkable
import attrs
from cattrs.errors import ClassValidationError
from lsprotocol.types import (
CANCEL_REQUEST,
EXIT,
ResponseError,
ResponseErrorMessage,
)
from pygls.exceptions import (
FeatureNotificationError,
FeatureRequestError,
JsonRpcException,
JsonRpcInternalError,
JsonRpcInvalidParams,
JsonRpcMethodNotFound,
JsonRpcRequestCancelled,
)
from pygls.feature_manager import FeatureManager, is_thread_function
if typing.TYPE_CHECKING:
from collections.abc import Generator
from cattrs import Converter
from pygls.io_ import AsyncWriter, Writer
from pygls.server import JsonRPCServer
MessageHandler = Union[Callable[[Any], Any],]
MessageCallback = Callable[[Future[Any]], None]
logger = logging.getLogger(__name__)
# cattrs needs access to this type definition so we cannot include it in the
# TYPE_CHECKING block above
MsgId = Union[str, int]
@runtime_checkable
class RPCNotification(Protocol):
method: str
jsonrpc: str
params: Any
@runtime_checkable
class RPCRequest(Protocol):
id: MsgId
method: str
jsonrpc: str
params: Any
@runtime_checkable
class RPCResponse(Protocol):
id: MsgId
jsonrpc: str
result: Any
@runtime_checkable
class RPCError(Protocol):
id: MsgId
jsonrpc: str
error: Any
RPCMessage = Union[RPCNotification, RPCResponse, RPCRequest, RPCError]
@attrs.define
class JsonRPCNotification:
"""A class that represents a generic json rpc notification message.
Used as a fallback for unknown types.
"""
method: str
jsonrpc: str
params: Any
@attrs.define
class JsonRPCRequestMessage:
"""A class that represents a generic json rpc request message.
Used as a fallback for unknown types.
"""
id: MsgId
method: str
jsonrpc: str
params: Any
@attrs.define
class JsonRPCResponseMessage:
"""A class that represents a generic json rpc response message.
Used as a fallback for unknown types.
"""
id: MsgId
jsonrpc: str
result: Any
class JsonRPCProtocol:
"""Json RPC protocol implementation
Specification of the protocol can be found here:
https://www.jsonrpc.org/specification
This class provides bidirectional communication which is needed for LSP.
"""
CHARSET = "utf-8"
CONTENT_TYPE = "application/vscode-jsonrpc"
VERSION = "2.0"
def __init__(self, server: JsonRPCServer, converter: Converter):
self._server = server
self._converter = converter
self._shutdown = False
# Book keeping for in-flight requests
self._ctx_msg_id: contextvars.ContextVar[MsgId | None] = contextvars.ContextVar(
"msg_id", default=None
)
self._request_futures: dict[MsgId, Future[Any]] = {}
self._result_types: dict[MsgId, Any] = {}
self.fm = FeatureManager(server, converter)
self.writer: AsyncWriter | Writer | None = None
self._include_headers = False
def __call__(self):
return self
@property
def msg_id(self) -> MsgId | None:
"""Returns the id of the current context (if it exists)."""
ctx = contextvars.copy_context()
return ctx.get(self._ctx_msg_id)
def _execute_handler(
self,
msg_id: MsgId,
handler: MessageHandler,
callback: MessageCallback,
args: tuple[Any, ...] | None = None,
kwargs: dict[str, Any] | None = None,
):
"""Execute the given message handler.
Parameters
----------
msg_id
The id of the message being handled
handler
The request handler to call
callback
An optional callback function to call upon completion of the handler
args
Positional arguments to pass to the handler
kwargs
Keyword arguments to pass to the handler
"""
future: Future[Any]
args = args or tuple()
kwargs = kwargs or {}
if asyncio.iscoroutinefunction(handler):
future = asyncio.ensure_future(handler(*args, **kwargs))
self._request_futures[msg_id] = future
future.add_done_callback(callback)
elif is_thread_function(handler):
future = self._server.thread_pool.submit(handler, *args, **kwargs)
self._request_futures[msg_id] = future
future.add_done_callback(callback)
elif inspect.isgeneratorfunction(handler):
future = Future()
self._request_futures[msg_id] = future
future.add_done_callback(callback)
try:
self._run_generator(
future=None, gen=handler(*args, **kwargs), result_future=future
)
except Exception as exc:
future.set_exception(exc)
else:
# While a future is not necessary for a synchronous function, it allows us to use a single
# pattern across all handler types
future = Future()
future.add_done_callback(callback)
try:
result = handler(*args, **kwargs)
future.set_result(result)
except Exception as exc:
future.set_exception(exc)
def _run_generator(
self,
future: Future[Any] | None,
*,
gen: Generator[Any, Any, Any],
result_future: Future[Any],
):
"""Run the next portion of the given generator.
Generator handlers are designed to ``yield`` to other handlers that are executed
separately before their results are sent back into the generator allowing
execution to continue.
Generator handlers are primarily used in the implementation of pygls' builtin
feature handlers.
Parameters
----------
future
The future that contains the result of the previously executed handler, if any
gen
The generator to run
result_future
The future to send the final result to once the generator stops.
"""
if result_future.cancelled():
return
try:
value = future.result() if future is not None else None
handler, args, kwargs = gen.send(value)
self._execute_handler(
str(uuid.uuid4()),
handler,
args=args,
kwargs=kwargs,
callback=partial(
self._run_generator, gen=gen, result_future=result_future
),
)
except StopIteration as result:
result_future.set_result(result.value)
except Exception as exc:
result_future.set_exception(exc)
def _send_handler_result(self, future: Future[Any], *, msg_id: MsgId):
"""Callback function that sends the result of the given future to the client.
Used to respond to request messages.
"""
self._request_futures.pop(msg_id, None)
try:
if not future.cancelled():
self._send_response(msg_id, result=future.result())
else:
self._send_response(
msg_id,
error=JsonRpcRequestCancelled(
f'Request with id "{msg_id}" is canceled'
).to_response_error(),
)
except JsonRpcException as exc:
logger.exception('Exception occurred for message "%s"', msg_id)
self._send_response(msg_id, error=exc.to_response_error())
self._server._report_server_error(exc, FeatureRequestError)
except Exception:
error = JsonRpcInternalError.of(sys.exc_info())
logger.exception('Exception occurred for message "%s"', msg_id)
self._send_response(msg_id, error=error.to_response_error())
self._server._report_server_error(error, FeatureRequestError)
def _check_handler_result(self, future: Future[Any]):
"""Check the result of the future to see if an error occurred.
Used when handling notification messages
"""
if not future.cancelled() and (exc := future.exception()) is not None:
try:
raise exc
except Exception:
error = JsonRpcInternalError.of(sys.exc_info())
self._server._report_server_error(error, FeatureNotificationError)
def _get_handler(self, feature_name: str) -> MessageHandler:
"""Returns builtin or used defined feature by name if exists."""
if (handler := self.fm.builtin_features.get(feature_name)) is not None:
return handler
if (handler := self.fm.features.get(feature_name)) is not None:
return handler
raise JsonRpcMethodNotFound.of(feature_name)
def _handle_cancel_notification(self, msg_id: MsgId):
"""Handles a cancel notification from the client."""
future = self._request_futures.pop(msg_id, None)
if not future:
logger.warning('Cancel notification for unknown message id "%s"', msg_id)
return
# Will only work if the request hasn't started executing
if future.cancel():
logger.info('Cancelled request with id "%s"', msg_id)
def _handle_notification(self, method_name: str, params: Any):
"""Handles a notification from the client."""
if method_name == CANCEL_REQUEST:
self._handle_cancel_notification(params.id)
return
try:
handler = self._get_handler(method_name)
self._execute_handler(
msg_id=str(uuid.uuid4()),
handler=handler,
args=(params,),
callback=self._check_handler_result,
)
except JsonRpcMethodNotFound:
logger.warning("Ignoring notification for unknown method %r", method_name)
except Exception as error:
logger.exception(
"Failed to handle notification %r: %s",
method_name,
params,
exc_info=True,
)
self._server._report_server_error(error, FeatureNotificationError)
def _handle_request(self, msg_id: MsgId, method_name: str, params: Any):
"""Handles a request from the client."""
try:
handler = self._get_handler(method_name)
# Set the request id within the current context.
self._ctx_msg_id.set(msg_id)
self._execute_handler(
msg_id=msg_id,
handler=handler,
args=(params,),
callback=partial(self._send_handler_result, msg_id=msg_id),
)
except JsonRpcMethodNotFound as error:
logger.warning(
"Failed to handle request %r, unknown method %r",
msg_id,
method_name,
)
self._send_response(msg_id, None, error.to_response_error())
self._server._report_server_error(error, FeatureRequestError)
except JsonRpcException as error:
logger.exception(
"Failed to handle request %s %s %s",
msg_id,
method_name,
params,
exc_info=True,
)
self._send_response(msg_id, None, error.to_response_error())
self._server._report_server_error(error, FeatureRequestError)
except Exception as error:
logger.exception(
"Failed to handle request %s %s %s",
msg_id,
method_name,
params,
exc_info=True,
)
err = JsonRpcInternalError.of(sys.exc_info()).to_response_error()
self._send_response(msg_id, None, err)
self._server._report_server_error(error, FeatureRequestError)
def _handle_response(
self,
msg_id: MsgId,
result: Any | None = None,
error: ResponseError | None = None,
):
"""Handles a response from the client."""
future = self._request_futures.pop(msg_id, None)
if not future:
logger.warning('Received response to unknown message id "%s"', msg_id)
return
if error is not None:
logger.debug('Received error response to message "%s": %s', msg_id, error)
future.set_exception(JsonRpcException.from_error(error))
else:
logger.debug('Received result for message "%s": %s', msg_id, result)
future.set_result(result)
def _serialize_message(self, data: Any) -> dict[str, Any]:
"""Function used to serialize data sent to the client."""
if hasattr(data, "__attrs_attrs__"):
return self._converter.unstructure(data)
if isinstance(data, enum.Enum):
return data.value
return data.__dict__
def structure_message(self, data: dict[str, Any]):
"""Function used to deserialize data recevied from the client."""
if "jsonrpc" not in data:
return data
try:
if "id" in data:
if "error" in data:
return self._converter.structure(data, ResponseErrorMessage)
elif "method" in data:
request_type = (
self.get_message_type(data["method"]) or JsonRPCRequestMessage
)
return self._converter.structure(data, request_type)
else:
response_type = (
self._result_types.pop(data["id"], None)
or JsonRPCResponseMessage
)
return self._converter.structure(data, response_type)
else:
method = data.get("method", "")
notification_type = self.get_message_type(method) or JsonRPCNotification
return self._converter.structure(data, notification_type)
except ClassValidationError as exc:
logger.error("Unable to deserialize message\n%s", traceback.format_exc())
raise JsonRpcInvalidParams() from exc
except Exception as exc:
logger.error("Unable to deserialize message\n%s", traceback.format_exc())
raise JsonRpcInternalError() from exc
def handle_message(self, message: RPCMessage):
"""Delegates message to handlers depending on message type."""
if message.jsonrpc != JsonRPCProtocol.VERSION:
logger.warning('Unknown message "%s"', message)
return
if self._shutdown and getattr(message, "method", "") != EXIT:
logger.warning("Server shutting down. No more requests!")
return
# Run each handler within its own context.
ctx = contextvars.copy_context()
if isinstance(message, RPCRequest):
logger.debug("Request %r received", message.method)
ctx.run(self._handle_request, message.id, message.method, message.params)
elif isinstance(message, RPCNotification):
logger.debug("Notification %r received", message.method)
ctx.run(self._handle_notification, message.method, message.params)
elif isinstance(message, RPCResponse):
logger.debug("Response message received.")
ctx.run(self._handle_response, message.id, message.result)
else:
logger.debug("Error message received.")
ctx.run(self._handle_response, message.id, None, message.error)
def _send_data(self, data: Any):
"""Sends data to the client."""
if not data:
return
if self.writer is None:
logger.error("Unable to send data, no available transport!")
return
try:
body = json.dumps(data, default=self._serialize_message)
logger.info("Sending data: %s", body)
if self._include_headers:
header = (
f"Content-Length: {len(body)}\r\n"
f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
)
data = header + body
else:
data = body
res = self.writer.write(data.encode(self.CHARSET))
if inspect.isawaitable(res):
asyncio.ensure_future(res)
except BrokenPipeError:
logger.exception("Error sending data. BrokenPipeError", exc_info=True)
raise
except Exception as error:
logger.exception("Error sending data", exc_info=True)
self._server._report_server_error(error, JsonRpcInternalError)
def _send_response(
self,
msg_id: MsgId,
result: Any | None = None,
error: Union[ResponseError, None] = None,
):
"""Send a JSON-RPC response
.. important::
You should only set ``result`` OR ``error``.
If both are set, then the ``result`` value will be ignored.
Parameters
----------
msg_id
The id of the message to respond to
result
The result to send in the event of a success
error
The error to send in the event of a failure
"""
if error is not None:
response = ResponseErrorMessage(id=msg_id, error=error)
else:
response_type = self._result_types.pop(msg_id, JsonRPCResponseMessage)
response = response_type(
id=msg_id, result=result, jsonrpc=JsonRPCProtocol.VERSION
)
self._send_data(response)
def set_writer(
self,
writer: AsyncWriter | Writer,
include_headers: bool = True,
):
"""Set the writer object to use when sending data
Parameters
----------
writer
The writer object
include_headers
Flag indicating if headers like ``Content-Length`` should be included when
sending data. (Default ``True``)
"""
self.writer = writer
self._include_headers = include_headers
def get_message_type(self, method: str) -> Type[Any] | None:
"""Return the type definition of the message associated with the given method."""
return None
def get_result_type(self, method: str) -> Type[Any] | None:
"""Return the type definition of the result associated with the given method."""
return None
def notify(self, method: str, params: Any | None = None):
"""Send a JSON-RPC notification.
.. note::
Notifications are "fire-and-forget", there is no way for the recipient to
respond directly to a notification. If you expect a response to this message,
use ``send_request``.
Parameters
----------
method
The method name of the message to send
params
The payload of the message
"""
logger.debug("Sending notification: '%s' %s", method, params)
notification_type = self.get_message_type(method) or JsonRPCNotification
notification = notification_type(
method=method, params=params, jsonrpc=JsonRPCProtocol.VERSION
)
self._send_data(notification)
def send_request(
self,
method: str,
params: Any | None = None,
callback: Callable[[Any], None] | None = None,
msg_id: MsgId | None = None,
) -> Future[Any]:
"""Send a JSON-RPC request
Parameters
----------
method
The method name of the message to send
params
The payload of the message
callback
If set, the given callback will be called with the result of the future
when it resolves
msg_id
Send the request using the given id, if ``None``, an id will be automatically
generated
Returns
-------
Future[Any]
A future that will resolve once a response has been received
"""
if msg_id is None:
msg_id = str(uuid.uuid4())
request_type = self.get_message_type(method) or JsonRPCRequestMessage
logger.debug('Sending request with id "%s": %s %s', msg_id, method, params)
request = request_type(
id=msg_id,
method=method,
params=params,
jsonrpc=JsonRPCProtocol.VERSION,
)
future: Future[Any] = Future()
# If callback function is given, call it when result is received
if callback:
def wrapper(fut: Future[Any]):
result = fut.result()
logger.info("Client response for %s received: %s", params, result)
callback(result)
future.add_done_callback(wrapper)
self._request_futures[msg_id] = future
self._result_types[msg_id] = self.get_result_type(method)
self._send_data(request)
return future
def send_request_async(
self, method: str, params: Any | None = None, msg_id: MsgId | None = None
):
"""Send a JSON-RPC request, asynchronously.
This method calls `send_request`, wrapping the resulting future with
``asyncio.wrap_future`` so it can be used in an ``async def`` function and
awaited with the ``await`` keyword.
Parameters
----------
method
The method name of the message to send
params
The payload of the message
callback
If set, the given callback will be called with the result of the future
when it resolves
msg_id
Send the request using the given id, if ``None``, an id will be automatically
generated
Returns
-------
`asyncio.Future` that can be awaited
"""
return asyncio.wrap_future(
self.send_request(method, params=params, msg_id=msg_id)
)