Skip to content

Commit 145ca22

Browse files
authored
[gunicorn] Use precise TypedDicts for ctl and dirty protocols (#16087)
1 parent 29a510f commit 145ca22

2 files changed

Lines changed: 106 additions & 28 deletions

File tree

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,45 @@
1-
from _typeshed import Incomplete
21
from asyncio import StreamReader, StreamWriter
32
from socket import socket
4-
from typing import ClassVar
3+
from typing import Any, ClassVar, Literal, TypeAlias, TypedDict, type_check_only
4+
5+
@type_check_only
6+
class _CtlRequest(TypedDict):
7+
id: int
8+
command: str
9+
args: list[str]
10+
11+
@type_check_only
12+
class _CtlSuccessResponse(TypedDict):
13+
id: int
14+
status: Literal["ok"]
15+
data: dict[str, Any]
16+
17+
@type_check_only
18+
class _CtlErrorResponse(TypedDict):
19+
id: int
20+
status: Literal["error"]
21+
error: str
22+
23+
_CtlResponse: TypeAlias = _CtlSuccessResponse | _CtlErrorResponse
24+
_CtlMessage: TypeAlias = _CtlRequest | _CtlResponse
525

626
class ProtocolError(Exception): ...
727

828
class ControlProtocol:
929
MAX_MESSAGE_SIZE: ClassVar[int]
1030
@staticmethod
11-
def encode_message(data: dict[Incomplete, Incomplete]) -> bytes: ...
31+
def encode_message(data: _CtlMessage) -> bytes: ...
1232
@staticmethod
13-
def decode_message(data: bytes) -> dict[Incomplete, Incomplete]: ...
33+
def decode_message(data: bytes) -> _CtlMessage: ...
1434
@staticmethod
15-
def read_message(sock: socket) -> dict[Incomplete, Incomplete]: ...
35+
def read_message(sock: socket) -> _CtlMessage: ...
1636
@staticmethod
17-
def write_message(sock: socket, data: dict[Incomplete, Incomplete]) -> None: ...
37+
def write_message(sock: socket, data: _CtlMessage) -> None: ...
1838
@staticmethod
19-
async def read_message_async(reader: StreamReader) -> dict[Incomplete, Incomplete]: ...
39+
async def read_message_async(reader: StreamReader) -> _CtlMessage: ...
2040
@staticmethod
21-
async def write_message_async(writer: StreamWriter, data: dict[Incomplete, Incomplete]) -> None: ...
41+
async def write_message_async(writer: StreamWriter, data: _CtlMessage) -> None: ...
2242

23-
# TODO: Use TypedDict for next return types
24-
def make_request(request_id: int, command: str, args: list[str] | None = None) -> dict[str, Incomplete]: ...
25-
def make_response(request_id: int, data: dict[Incomplete, Incomplete] | None = None) -> dict[str, Incomplete]: ...
26-
def make_error_response(request_id: int, error: str) -> dict[str, Incomplete]: ...
43+
def make_request(request_id: int, command: str, args: list[str] | None = None) -> _CtlRequest: ...
44+
def make_response(request_id: int, data: dict[str, Any] | None = None) -> _CtlSuccessResponse: ...
45+
def make_error_response(request_id: int, error: str) -> _CtlErrorResponse: ...

stubs/gunicorn/gunicorn/dirty/protocol.pyi

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import asyncio
22
import socket
33
from _typeshed import Incomplete
4-
from typing import ClassVar, Final
4+
from typing import ClassVar, Final, Literal, TypeAlias, TypedDict, type_check_only
5+
from typing_extensions import NotRequired
6+
7+
from .errors import _DirtyErrorDict
58

69
MAGIC: Final = b"GD"
710
VERSION: Final = 0x01
@@ -39,6 +42,65 @@ HEADER_FORMAT: Final = ">2sBBIQ"
3942
HEADER_SIZE: Final[int]
4043
MAX_MESSAGE_SIZE: Final = 67108864
4144

45+
@type_check_only
46+
class _DirtyRequest(TypedDict):
47+
type: Literal["request"]
48+
id: int | str
49+
app_path: str
50+
action: str
51+
args: list[Incomplete]
52+
kwargs: dict[str, Incomplete]
53+
54+
@type_check_only
55+
class _DirtyResponse(TypedDict):
56+
type: Literal["response"]
57+
id: int | str
58+
result: Incomplete
59+
60+
@type_check_only
61+
class _DirtyErrorResponse(TypedDict):
62+
type: Literal["error"]
63+
id: int | str
64+
error: _DirtyErrorDict | dict[str, Incomplete]
65+
66+
@type_check_only
67+
class _DirtyChunkMessage(TypedDict):
68+
type: Literal["chunk"]
69+
id: int | str
70+
data: Incomplete
71+
72+
@type_check_only
73+
class _DirtyEndMessage(TypedDict):
74+
type: Literal["end"]
75+
id: int | str
76+
77+
@type_check_only
78+
class _DirtyStashMessage(TypedDict):
79+
type: Literal["stash"]
80+
id: int | str
81+
op: int
82+
table: str
83+
key: NotRequired[Incomplete]
84+
value: NotRequired[Incomplete]
85+
pattern: NotRequired[Incomplete]
86+
87+
@type_check_only
88+
class _DirtyManageMessage(TypedDict):
89+
type: Literal["manage"]
90+
id: int | str
91+
op: int
92+
count: int
93+
94+
_DirtyMessage: TypeAlias = (
95+
_DirtyRequest
96+
| _DirtyResponse
97+
| _DirtyErrorResponse
98+
| _DirtyChunkMessage
99+
| _DirtyEndMessage
100+
| _DirtyStashMessage
101+
| _DirtyManageMessage
102+
)
103+
42104
class BinaryProtocol:
43105
HEADER_SIZE: ClassVar[int]
44106
MAX_MESSAGE_SIZE: ClassVar[int]
@@ -80,33 +142,30 @@ class BinaryProtocol:
80142
@staticmethod
81143
def decode_message(data: bytes) -> tuple[str, int, Incomplete]: ...
82144
@staticmethod
83-
async def read_message_async(reader: asyncio.StreamReader) -> dict[str, Incomplete]: ...
145+
async def read_message_async(reader: asyncio.StreamReader) -> _DirtyMessage: ...
84146
@staticmethod
85-
async def write_message_async(writer: asyncio.StreamWriter, message: dict[str, Incomplete]) -> None: ...
147+
async def write_message_async(writer: asyncio.StreamWriter, message: _DirtyMessage) -> None: ...
86148
@staticmethod
87149
def _recv_exactly(sock: socket.socket, n: int) -> bytes: ...
88150
@staticmethod
89-
def read_message(sock: socket.socket) -> dict[str, Incomplete]: ...
151+
def read_message(sock: socket.socket) -> _DirtyMessage: ...
90152
@staticmethod
91-
def write_message(sock: socket.socket, message: dict[str, Incomplete]) -> None: ...
153+
def write_message(sock: socket.socket, message: _DirtyMessage) -> None: ...
92154
@staticmethod
93-
def _encode_from_dict(message: dict[str, Incomplete]) -> bytes: ...
155+
def _encode_from_dict(message: _DirtyMessage) -> bytes: ...
94156

95157
DirtyProtocol = BinaryProtocol
96158

97-
# TODO: Use TypedDict for results
98159
def make_request(
99160
request_id: int | str,
100161
app_path: str,
101162
action: str,
102163
args: tuple[Incomplete, ...] | None = None,
103164
kwargs: dict[str, Incomplete] | None = None,
104-
) -> dict[str, Incomplete]: ...
105-
def make_response(request_id: int | str, result) -> dict[str, Incomplete]: ...
106-
def make_error_response(request_id: int | str, error) -> dict[str, Incomplete]: ...
107-
def make_chunk_message(request_id: int | str, data) -> dict[str, Incomplete]: ...
108-
def make_end_message(request_id: int | str) -> dict[str, Incomplete]: ...
109-
def make_stash_message(
110-
request_id: int | str, op: int, table: str, key=None, value=None, pattern=None
111-
) -> dict[str, Incomplete]: ...
112-
def make_manage_message(request_id: int | str, op: int, count: int = 1) -> dict[str, Incomplete]: ...
165+
) -> _DirtyRequest: ...
166+
def make_response(request_id: int | str, result) -> _DirtyResponse: ...
167+
def make_error_response(request_id: int | str, error) -> _DirtyErrorResponse: ...
168+
def make_chunk_message(request_id: int | str, data) -> _DirtyChunkMessage: ...
169+
def make_end_message(request_id: int | str) -> _DirtyEndMessage: ...
170+
def make_stash_message(request_id: int | str, op: int, table: str, key=None, value=None, pattern=None) -> _DirtyStashMessage: ...
171+
def make_manage_message(request_id: int | str, op: int, count: int = 1) -> _DirtyManageMessage: ...

0 commit comments

Comments
 (0)