Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f40ee84

Browse files
committed
Fix IPC communication for mypy 1.20.0
They changed the way IPC messages are sent and this broke things.
1 parent b08ea6d commit f40ee84

2 files changed

Lines changed: 86 additions & 36 deletions

File tree

src/idlemypyextension/client.py

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@
4040
__title__ = "Mypy Daemon Client"
4141
__license__ = "MIT"
4242

43-
import base64
4443
import contextlib
44+
import struct
4545
import sys
4646
from collections import ChainMap
47-
from typing import TYPE_CHECKING, Literal, TypedDict, cast
47+
from typing import TYPE_CHECKING, Final, Literal, TypedDict, cast
4848

4949
import orjson
5050

@@ -59,7 +59,11 @@
5959
daemonize as _daemonize,
6060
process_start_options as _process_start_options,
6161
)
62-
from mypy.ipc import IPCClient as _IPCClient, IPCException as _IPCException
62+
from mypy.ipc import (
63+
HEADER_SIZE,
64+
IPCClient as _IPCClient,
65+
IPCException as _IPCException,
66+
)
6367
from mypy.version import __version__
6468

6569
if TYPE_CHECKING:
@@ -101,6 +105,14 @@ class Stats(TypedDict):
101105
sccs_left: int
102106
nodes_left: int
103107
cache_commit_time: float
108+
type_expression_parse_count: int
109+
type_expression_full_parse_success_count: int
110+
type_expression_full_parse_failure_count: int
111+
load_missing_time: float
112+
order_scc_time: float
113+
semanal_time: float
114+
type_check_time: float
115+
flush_and_cache_time: float
104116

105117

106118
class Response(TypedDict):
@@ -256,6 +268,11 @@ def _read_request_response_json(request_response: str | bytes) -> Response:
256268
return cast("Response", data)
257269

258270

271+
IPC_CONNECTION_CLOSED_ERROR: Final = Response(
272+
{"error": "No data received, IPC socket connection closed."},
273+
)
274+
275+
259276
async def _request_win32(
260277
name: str,
261278
request_arguments: bytes,
@@ -273,7 +290,7 @@ async def _receive(
273290
"""
274291
bdata: str = await async_connection.read()
275292
if not bdata:
276-
return {"error": "No data received, IPC socket connection closed."}
293+
return IPC_CONNECTION_CLOSED_ERROR
277294
return _read_request_response_json(bdata)
278295

279296
try:
@@ -295,19 +312,24 @@ async def _receive(
295312
return {"error": str(err)}
296313

297314

298-
def find_frame_in_buffer(
315+
def frame_from_buffer(
299316
buffer: bytearray,
300-
) -> tuple[bytearray, bytearray | None]:
317+
message_size: int | None = None,
318+
) -> tuple[bytearray, bytes | None, int | None]:
301319
"""Return a full frame from the bytes we have in the buffer.
302320
303-
Returns (next frame keep data, complete frame | None)
321+
Returns (next frame keep data, complete frame | None, complete frame size | None)
304322
"""
305-
space_pos = buffer.find(b" ")
306-
if space_pos == -1:
307-
# Incomplete frame
308-
return buffer, None
309-
# We have a full frame
310-
return buffer[space_pos + 1 :], buffer[:space_pos]
323+
size = len(buffer)
324+
if size < HEADER_SIZE:
325+
return buffer, None, None
326+
if message_size is None:
327+
message_size = struct.unpack("!L", buffer[:HEADER_SIZE])[0]
328+
if size < message_size + HEADER_SIZE:
329+
return buffer, None, message_size
330+
# We have a full frame, avoid extra copy in case we get a large frame.
331+
bdata = memoryview(buffer)[HEADER_SIZE : HEADER_SIZE + message_size]
332+
return buffer[HEADER_SIZE + message_size :], bytes(bdata), message_size
311333

312334

313335
async def _request_linux(
@@ -317,38 +339,56 @@ async def _request_linux(
317339
) -> Response:
318340
"""Request from daemon on linux/unix."""
319341
buffer = bytearray()
320-
frame: bytearray | None = None
342+
frame: bytes | None = None
321343
all_responses: list[Response] = []
322344
async with await trio.open_unix_socket(filename) as connection:
323-
# Frame the data by urlencoding it and separating by space.
324-
request_frame = base64.encodebytes(request_arguments) + b" "
345+
# Frame the request and send.
346+
request_frame = (
347+
struct.pack("!L", len(request_arguments)) + request_arguments
348+
)
325349
await connection.send_all(request_frame)
326350

327351
is_not_done = True
328352
while is_not_done:
329-
# Receive more data into the buffer.
330-
try:
331-
if timeout is None:
332-
more = await connection.receive_some()
333-
else:
334-
with trio.fail_after(timeout):
335-
more = await connection.receive_some()
336-
except trio.TooSlowError:
337-
return {"error": "IPC socket connection timed out"}
338-
if not more:
339-
# Connection closed
340-
# Socket was empty and we didn't get any frame.
341-
return {
342-
"error": "No data received, IPC socket connection closed.",
343-
}
344-
buffer.extend(more)
345-
346-
buffer, frame = find_frame_in_buffer(buffer)
353+
# check if we have read entire frame yet
354+
buffer, frame, complete_frame_size = frame_from_buffer(buffer)
355+
356+
max_read_bytes: int | None
357+
if complete_frame_size is None:
358+
# if have not read frame size header, do so.
359+
max_read_bytes = HEADER_SIZE
360+
else:
361+
# number of bytes to read next is the complete frame
362+
# size - the size header - current bytes count
363+
max_read_bytes = max(
364+
HEADER_SIZE,
365+
complete_frame_size - len(buffer) + HEADER_SIZE,
366+
)
367+
347368
if frame is None:
369+
# Have not read entire frame yet, so receive more data
370+
# into the buffer.
371+
try:
372+
if timeout is None:
373+
more = await connection.receive_some(max_read_bytes)
374+
else:
375+
with trio.fail_after(timeout):
376+
more = await connection.receive_some(
377+
max_read_bytes,
378+
)
379+
except trio.TooSlowError:
380+
return {
381+
"error": f"IPC socket connection timed out ({timeout = })",
382+
}
383+
if not more:
384+
# Connection closed
385+
# Socket was empty and we didn't get any frame.
386+
return IPC_CONNECTION_CLOSED_ERROR
387+
buffer.extend(more)
348388
continue
389+
349390
# Frame is not None, we read a full frame
350-
response_text = base64.decodebytes(frame)
351-
response = _read_request_response_json(response_text)
391+
response = _read_request_response_json(frame)
352392

353393
is_not_done = not bool(response.pop("final", False))
354394
all_responses.append(response)

src/idlemypyextension/extension.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class idlemypyextension(utils.BaseExtension): # noqa: N801
191191
"timeout_mins": "30",
192192
"action_max_sec": "None",
193193
"should_restart_always": "False",
194+
"force_base_ipc_request": "False",
194195
}
195196
# Default key binds for configuration file
196197
bind_defaults: ClassVar[dict[str, str | None]] = {
@@ -211,6 +212,7 @@ class idlemypyextension(utils.BaseExtension): # noqa: N801
211212
timeout_mins = "30"
212213
action_max_sec = "None"
213214
should_restart_always = "False"
215+
force_base_ipc_request = "False"
214216

215217
# Class attributes
216218
idlerc_folder = Path(idleConf.userdir).expanduser().absolute()
@@ -258,6 +260,14 @@ def register_rightclick_items(self) -> None:
258260
),
259261
)
260262

263+
@classmethod
264+
def reload(cls) -> None:
265+
"""Load class variables from configuration."""
266+
super().reload()
267+
268+
# Set client global var based on extension config
269+
client.FORCE_BASE_REQUEST = cls.force_base_ipc_request == "True"
270+
261271
def __getattr__(self, attr_name: str) -> object:
262272
"""Transform event async sync calls to sync wrappers."""
263273
if attr_name.endswith("_event"):

0 commit comments

Comments
 (0)