Skip to content

Commit 0526f4d

Browse files
committed
Interfaces for jsonrpcclient, lspclient and serviceregistry
1 parent e14cf43 commit 0526f4d

4 files changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import collections.abc
2+
from pathlib import Path
3+
from types import TracebackType
4+
from typing import Any, Protocol, Self
5+
6+
7+
class IJsonRpcSession(Protocol):
8+
"""An active JSON-RPC connection session.
9+
10+
Use as an async context manager: ``__aenter__`` starts the subprocess,
11+
``__aexit__`` stops it.
12+
"""
13+
14+
async def __aenter__(self) -> Self: ...
15+
16+
async def __aexit__(
17+
self,
18+
exc_type: type[BaseException] | None,
19+
exc_val: BaseException | None,
20+
exc_tb: TracebackType | None,
21+
) -> None: ...
22+
23+
# -- Async API (for use from async extension handlers) -----------------
24+
25+
async def send_request(
26+
self,
27+
method: str,
28+
params: dict[str, Any] | None = None,
29+
timeout: float | None = None,
30+
) -> Any:
31+
"""Send a JSON-RPC request and wait for the response.
32+
33+
Args:
34+
method: The JSON-RPC method name.
35+
params: Optional parameters for the request.
36+
timeout: Optional timeout in seconds.
37+
38+
Returns:
39+
The ``result`` field from the JSON-RPC response.
40+
"""
41+
...
42+
43+
async def send_notification(
44+
self,
45+
method: str,
46+
params: dict[str, Any] | None = None,
47+
) -> None:
48+
"""Send a JSON-RPC notification (no response expected)."""
49+
...
50+
51+
# -- Sync API (blocks caller thread, IO thread resolves) ---------------
52+
53+
def send_request_sync(
54+
self,
55+
method: str,
56+
params: dict[str, Any] | None = None,
57+
timeout: float | None = None,
58+
) -> Any:
59+
"""Send a JSON-RPC request synchronously.
60+
61+
Blocks the calling thread until the IO thread receives the response.
62+
"""
63+
...
64+
65+
def send_notification_sync(
66+
self,
67+
method: str,
68+
params: dict[str, Any] | None = None,
69+
) -> None:
70+
"""Send a JSON-RPC notification synchronously."""
71+
...
72+
73+
# -- Server-initiated messages -----------------------------------------
74+
75+
def on_notification(
76+
self,
77+
method: str,
78+
handler: collections.abc.Callable[
79+
[dict[str, Any] | None], collections.abc.Awaitable[None]
80+
],
81+
) -> None:
82+
"""Register a handler for incoming notifications from the server.
83+
84+
Args:
85+
method: The notification method name to handle.
86+
handler: Async callable that receives the notification params.
87+
"""
88+
...
89+
90+
def on_request(
91+
self,
92+
method: str,
93+
handler: collections.abc.Callable[
94+
[dict[str, Any] | None], collections.abc.Awaitable[Any]
95+
],
96+
) -> None:
97+
"""Register a handler for incoming requests from the server.
98+
99+
Args:
100+
method: The request method name.
101+
handler: Async callable that receives params and returns the result.
102+
"""
103+
...
104+
105+
106+
class IJsonRpcClient(Protocol):
107+
"""Factory for creating JSON-RPC sessions."""
108+
109+
def session(
110+
self,
111+
cmd: str,
112+
cwd: Path | None = None,
113+
env: dict[str, str] | None = None,
114+
readable_id: str = "",
115+
) -> IJsonRpcSession:
116+
"""Create a new JSON-RPC session that launches a subprocess.
117+
118+
Usage::
119+
120+
async with json_rpc_client.session("some-server --stdio") as session:
121+
result = await session.send_request("method", {"key": "value"})
122+
123+
Args:
124+
cmd: Shell command to start the JSON-RPC server process.
125+
cwd: Working directory for the subprocess.
126+
env: Environment variables for the subprocess.
127+
readable_id: Human-readable identifier for logging.
128+
129+
Returns:
130+
An async context manager yielding IJsonRpcSession.
131+
"""
132+
...
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import collections.abc
2+
from pathlib import Path
3+
from types import TracebackType
4+
from typing import Any, Protocol, Self
5+
6+
7+
class ILspSession(Protocol):
8+
"""An active LSP session with a language server.
9+
10+
Use as an async context manager:
11+
12+
- ``__aenter__`` starts the process, sends ``initialize`` request,
13+
sends ``initialized`` notification.
14+
- ``__aexit__`` sends ``shutdown`` request, sends ``exit`` notification,
15+
stops the process.
16+
"""
17+
18+
async def __aenter__(self) -> Self: ...
19+
20+
async def __aexit__(
21+
self,
22+
exc_type: type[BaseException] | None,
23+
exc_val: BaseException | None,
24+
exc_tb: TracebackType | None,
25+
) -> None: ...
26+
27+
# -- Async API ---------------------------------------------------------
28+
29+
async def send_request(
30+
self,
31+
method: str,
32+
params: dict[str, Any] | None = None,
33+
timeout: float | None = None,
34+
) -> Any:
35+
"""Send an LSP request and return the result."""
36+
...
37+
38+
async def send_notification(
39+
self,
40+
method: str,
41+
params: dict[str, Any] | None = None,
42+
) -> None:
43+
"""Send an LSP notification."""
44+
...
45+
46+
# -- Sync API ----------------------------------------------------------
47+
48+
def send_request_sync(
49+
self,
50+
method: str,
51+
params: dict[str, Any] | None = None,
52+
timeout: float | None = None,
53+
) -> Any:
54+
"""Send an LSP request synchronously (blocks caller thread)."""
55+
...
56+
57+
def send_notification_sync(
58+
self,
59+
method: str,
60+
params: dict[str, Any] | None = None,
61+
) -> None:
62+
"""Send an LSP notification synchronously."""
63+
...
64+
65+
# -- Server-initiated messages -----------------------------------------
66+
67+
def on_notification(
68+
self,
69+
method: str,
70+
handler: collections.abc.Callable[
71+
[dict[str, Any] | None], collections.abc.Awaitable[None]
72+
],
73+
) -> None:
74+
"""Register handler for server notifications."""
75+
...
76+
77+
def on_request(
78+
self,
79+
method: str,
80+
handler: collections.abc.Callable[
81+
[dict[str, Any] | None], collections.abc.Awaitable[Any]
82+
],
83+
) -> None:
84+
"""Register handler for server-to-client requests."""
85+
...
86+
87+
# -- Server info -------------------------------------------------------
88+
89+
@property
90+
def server_capabilities(self) -> dict[str, Any]:
91+
"""Capabilities returned by the server in the initialize response."""
92+
...
93+
94+
@property
95+
def server_info(self) -> dict[str, Any] | None:
96+
"""Server info returned in the initialize response, if any."""
97+
...
98+
99+
100+
class ILspClient(Protocol):
101+
"""Factory for creating LSP sessions with language servers."""
102+
103+
def session(
104+
self,
105+
cmd: str,
106+
root_uri: str,
107+
workspace_folders: list[dict[str, str]] | None = None,
108+
initialization_options: dict[str, Any] | None = None,
109+
client_capabilities: dict[str, Any] | None = None,
110+
cwd: Path | None = None,
111+
env: dict[str, str] | None = None,
112+
readable_id: str = "",
113+
) -> ILspSession:
114+
"""Create a new LSP session that launches a language server.
115+
116+
The session automatically performs the LSP initialization handshake.
117+
118+
Usage::
119+
120+
async with lsp_client.session(
121+
cmd="pyright-langserver --stdio",
122+
root_uri="file:///path/to/project",
123+
) as session:
124+
result = await session.send_request(
125+
"textDocument/completion",
126+
{"textDocument": {"uri": "file:///file.py"}, "position": {"line": 0, "character": 0}},
127+
)
128+
129+
Args:
130+
cmd: Shell command to start the language server.
131+
root_uri: The root URI of the workspace.
132+
workspace_folders: Optional workspace folders (each with 'uri' and 'name' keys).
133+
initialization_options: Optional server-specific initialization options.
134+
client_capabilities: Optional client capabilities override.
135+
cwd: Working directory for the subprocess.
136+
env: Environment variables for the subprocess.
137+
readable_id: Human-readable identifier for logging.
138+
139+
Returns:
140+
An async context manager yielding ILspSession.
141+
"""
142+
...
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import typing
2+
3+
T = typing.TypeVar("T")
4+
5+
6+
class IServiceRegistry(typing.Protocol):
7+
def register_impl(
8+
self, interface: type[T], impl: type[T], singleton: bool = False
9+
) -> None: ...

finecode_jsonrpc/src/finecode_jsonrpc/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
RunnerFailedToStart,
77
RequestCancelledError,
88
)
9+
from .transports import StdioTransport
910

1011

1112
__all__ = [
@@ -15,4 +16,5 @@
1516
"ResponseTimeout",
1617
"RunnerFailedToStart",
1718
"RequestCancelledError",
19+
"StdioTransport",
1820
]

0 commit comments

Comments
 (0)