Skip to content

Commit 233df0a

Browse files
committed
Improve type hints
1 parent e3e5bf1 commit 233df0a

File tree

16 files changed

+101
-78
lines changed

16 files changed

+101
-78
lines changed

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(uv run mypy:*)",
5+
"Bash(uv run ty check:*)",
6+
"Bash(uv run pytest:*)"
7+
]
8+
}
9+
}

modernrpc/apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ class ModernRpcConfig(AppConfig):
99
name = "modernrpc"
1010
verbose_name = "Django Modern RPC"
1111

12-
def ready(self): ...
12+
def ready(self) -> None: ...

modernrpc/compat.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
import django
66
from django.views.decorators.csrf import csrf_exempt
77

8+
try:
9+
# types.NoneType is available only with Python 3.10+
10+
from types import NoneType
11+
except ImportError:
12+
NoneType = type(None) # type: ignore[misc] # ty: ignore[unused-type-ignore-comment
13+
14+
815
if django.VERSION >= (5, 0):
916
# Django updated its decorators to support wrapping asynchronous method in release 5.0
1017
# REF: https://docs.djangoproject.com/en/5.2/releases/5.0/#decorators
@@ -21,26 +28,26 @@ def async_csrf_exempt(view):
2128

2229
if sys.version_info >= (3, 14):
2330

24-
def is_union_type(_type: type):
31+
def is_union_type(_type: typing.Any) -> bool:
2532
return isinstance(_type, types.UnionType)
2633

2734
elif sys.version_info >= (3, 10):
2835

29-
def is_union_type(_type: type):
36+
def is_union_type(_type: typing.Any) -> bool:
3037
return typing.get_origin(_type) in (types.UnionType, typing.Union)
3138

3239
else:
3340

34-
def is_union_type(_type: type):
41+
def is_union_type(_type: typing.Any) -> bool:
3542
return typing.get_origin(_type) is typing.Union
3643

3744

3845
if sys.version_info >= (3, 14):
3946

40-
def union_str_repr(obj):
47+
def union_str_repr(obj: typing.Any) -> str:
4148
return str(obj)
4249

4350
else:
4451

45-
def union_str_repr(obj: type):
52+
def union_str_repr(obj: typing.Any) -> str:
4653
return " | ".join(t.__name__ for t in typing.get_args(obj))

modernrpc/core.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from collections import OrderedDict, defaultdict
66
from dataclasses import dataclass
7-
from typing import TYPE_CHECKING, Any, Callable, Iterable
7+
from typing import TYPE_CHECKING, Any, Iterable
88

99
from asgiref.sync import async_to_sync, iscoroutinefunction, sync_to_async
1010
from django.utils.functional import cached_property
@@ -25,7 +25,7 @@
2525

2626
from modernrpc.handler import RpcHandler
2727
from modernrpc.server import RpcServer
28-
from modernrpc.types import AuthPredicateType
28+
from modernrpc.types import AuthPredicateType, FuncOrCoro
2929

3030
logger = logging.getLogger(__name__)
3131

@@ -57,7 +57,7 @@ class RpcRequestContext:
5757
class ProcedureArgDocs:
5858
docstring: str = ""
5959
doc_type: str = ""
60-
type_hint: type | None = None
60+
type_hint: Any = None
6161

6262
@property
6363
def type_hint_as_str(self) -> str:
@@ -79,7 +79,7 @@ class ProcedureWrapper:
7979

8080
def __init__(
8181
self,
82-
func_or_coro: Callable,
82+
func_or_coro: FuncOrCoro,
8383
name: str | None = None,
8484
protocol: Protocol = Protocol.ALL,
8585
auth: AuthPredicateType = NOT_SET,
@@ -89,7 +89,8 @@ def __init__(
8989
self.func_or_coro = func_or_coro
9090

9191
# @decorator parameters
92-
self.name = name or func_or_coro.__name__
92+
func_name: str = getattr(func_or_coro, "__name__", None) or str(func_or_coro)
93+
self.name: str = name or func_name
9394
self.protocol = protocol
9495
self.context_target = context_target
9596

modernrpc/helpers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ def first(seq: Iterable, default=NOT_SET) -> Any:
6464
try:
6565
return next(iter(seq))
6666
except StopIteration:
67-
if default is not NOT_SET:
68-
return default
69-
raise IndexError("No element found in iterable") from None
67+
if default is NOT_SET:
68+
raise IndexError("No element found in iterable") from None
69+
return default
7070

7171

7272
def first_true(iterable: Iterable[Any], default: Any = None, pred: Callable[[Any], bool] | None = None) -> Any:

modernrpc/introspection.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88
import inspect
99
import re
1010
import typing
11-
from typing import Callable
11+
from typing import TYPE_CHECKING
1212

1313
from django.utils.functional import cached_property
1414

15+
if TYPE_CHECKING:
16+
from modernrpc.types import FuncOrCoro
17+
1518

1619
class Introspector:
1720
"""Helper class to extract the signature of a callable and the type hint of its arguments & returns"""
1821

19-
def __init__(self, function: Callable):
22+
def __init__(self, function: FuncOrCoro):
2023
self.func = function
2124

2225
@cached_property
@@ -45,7 +48,7 @@ class DocstringParser:
4548
PARAM_TYPE_REXP = re.compile(r"^[:@]type (\w+): ?(.*)$", flags=re.MULTILINE)
4649
RETURN_TYPE_REXP = re.compile(r"^[:@]rtype ?: ?(.*)$", flags=re.MULTILINE)
4750

48-
def __init__(self, function: Callable):
51+
def __init__(self, function: FuncOrCoro):
4952
self.func = function
5053

5154
@staticmethod

modernrpc/jsonrpc/backends/marshalling.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@
22
# PEP 604: use of typeA | typeB is available since Python 3.10, enable it for older versions
33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Iterable, overload
5+
from typing import TYPE_CHECKING, cast, overload
66

7+
from modernrpc.compat import NoneType
78
from modernrpc.constants import NOT_SET
89
from modernrpc.exceptions import RPCInvalidRequest
910
from modernrpc.jsonrpc.handler import JsonRpcRequest
1011
from modernrpc.types import DictStrAny, RpcErrorResult
1112

12-
try:
13-
# types.NoneType is available only with Python 3.10+
14-
from types import NoneType
15-
except ImportError:
16-
NoneType = type(None) # type: ignore[misc]
17-
1813
if TYPE_CHECKING:
1914
from modernrpc.jsonrpc.handler import JsonRpcResult
2015

@@ -75,13 +70,13 @@ class Marshaller:
7570
def result_to_dict(self, result: JsonRpcResult) -> DictStrAny | None: ...
7671

7772
@overload
78-
def result_to_dict(self, result: Iterable[JsonRpcResult]) -> list[DictStrAny | None]: ...
73+
def result_to_dict(self, result: list[JsonRpcResult]) -> list[DictStrAny | None]: ...
7974

8075
def result_to_dict(
81-
self, result: JsonRpcResult | Iterable[JsonRpcResult]
76+
self, result: JsonRpcResult | list[JsonRpcResult]
8277
) -> DictStrAny | None | list[DictStrAny | None]:
83-
if isinstance(result, Iterable):
84-
return [self.result_to_dict(r) for r in result]
78+
if isinstance(result, list):
79+
return [self.result_to_dict(cast("JsonRpcResult", r)) for r in result]
8580

8681
if result.request.is_notification:
8782
return None

modernrpc/jsonrpc/handler.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import logging
77
from dataclasses import dataclass, field
88
from http import HTTPStatus
9-
from typing import TYPE_CHECKING, Union
9+
from typing import TYPE_CHECKING, Union, cast
1010

1111
from django.utils.module_loading import import_string
1212

@@ -84,9 +84,9 @@ def process_request(self, request_body: str, context: RpcRequestContext) -> str
8484
exc = context.server.on_error(exc, context)
8585
return self.serializer.dumps(self.build_error_result(fake_request, exc.code, exc.message))
8686

87-
# Parsed request is an Iterable, we should handle it as a batch request
87+
# Parsed request is a list, we should handle it as a batch request
8888
if isinstance(parsed_request, list):
89-
return self.process_batch_request(parsed_request, context)
89+
return self.process_batch_request(cast("list[JsonRpcRequest]", parsed_request), context)
9090

9191
# By default, handle a standard single request
9292
result = self.process_single_request(parsed_request, context)
@@ -117,9 +117,9 @@ async def aprocess_request(self, request_body: str, context: RpcRequestContext)
117117
exc = context.server.on_error(exc, context)
118118
return self.serializer.dumps(self.build_error_result(fake_request, exc.code, exc.message))
119119

120-
# Parsed request is an Iterable, we should handle it as a batch request
120+
# Parsed request is a list, we should handle it as a batch request
121121
if isinstance(parsed_request, list):
122-
return await self.aprocess_batch_request(parsed_request, context)
122+
return await self.aprocess_batch_request(cast("list[JsonRpcRequest]", parsed_request), context)
123123

124124
# By default, handle a standard single request
125125
result = await self.aprocess_single_request(parsed_request, context)

modernrpc/server.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,26 @@
2525
from django.shortcuts import SupportsGetAbsoluteUrl
2626

2727
from modernrpc.handler import RpcHandler
28-
from modernrpc.types import AuthPredicateType
29-
28+
from modernrpc.types import AuthPredicateType, FuncOrCoro
3029

3130
logger = logging.getLogger(__name__)
3231

3332
RpcErrorHandler = Callable[[BaseException, RpcRequestContext], None]
3433

3534

3635
class RegistryMixin:
36+
"""
37+
Common logic for both RpcNamespace and RpcServer classes.
38+
Provide methods to register RPC procedures into an internal registry.
39+
"""
40+
3741
def __init__(self, auth: AuthPredicateType = NOT_SET) -> None:
3842
self._registry: dict[str, ProcedureWrapper] = {}
3943
self.auth = auth
4044

4145
def register_procedure(
4246
self,
43-
procedure: Callable | None = None,
47+
procedure: FuncOrCoro | None = None,
4448
name: str | None = None,
4549
protocol: Protocol = Protocol.ALL,
4650
auth: AuthPredicateType = NOT_SET,
@@ -61,7 +65,7 @@ def register_procedure(
6165
:raises ValueError: If a procedure can't be registered
6266
"""
6367

64-
def decorated(func: Callable) -> Callable:
68+
def decorated(func: FuncOrCoro) -> FuncOrCoro:
6569
if name and name.startswith("rpc."):
6670
raise ValueError(
6771
'According to JSON-RPC specs, method names starting with "rpc." are reserved for system extensions '
@@ -92,10 +96,13 @@ def procedures(self) -> dict[str, ProcedureWrapper]:
9296
return self._registry
9397

9498

95-
class RpcNamespace(RegistryMixin): ...
99+
class RpcNamespace(RegistryMixin):
100+
"""Registry for RPC procedures belonging to a given namespace."""
96101

97102

98103
class RpcServer(RegistryMixin):
104+
"""Base class to store all remote procedures for a given entry point"""
105+
99106
def __init__(
100107
self,
101108
register_system_procedures: bool = True,
@@ -122,6 +129,7 @@ def __init__(
122129
self.default_encoding = default_encoding
123130

124131
def register_namespace(self, namespace: RpcNamespace, name: str | None = None) -> None:
132+
"""Register all procedures from given namespace into the top-level server."""
125133
if name:
126134
prefix = name + "."
127135
logger.debug(
@@ -157,6 +165,7 @@ def get_procedure_wrapper(self, name: str, protocol: Protocol) -> ProcedureWrapp
157165
raise RPCMethodNotFound(name) from None
158166

159167
def get_request_handler(self, request: HttpRequest) -> RpcHandler | None:
168+
"""Return the first handler that can handle the given request, or None if no handler can handle it."""
160169
handler_klass = first_true(self.handler_klasses, pred=lambda cls: cls.can_handle(request))
161170
try:
162171
return handler_klass()
@@ -217,6 +226,7 @@ def check_request(self, request: HttpRequest) -> HttpResponse | None:
217226

218227
@staticmethod
219228
def build_response(handler: RpcHandler, result_data: str | tuple[int, str]) -> HttpResponse:
229+
"""Build an HttpResponse instance from the given handler and result data."""
220230
if isinstance(result_data, tuple) and len(result_data) == 2:
221231
status, result_data = result_data
222232
else:

modernrpc/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ class RpcErrorResult(RpcResult[RequestType]):
4242
NotSetType = object
4343
AuthPredicate = Callable[[RpcRequest], bool]
4444
AuthPredicateType = Union[NotSetType, AuthPredicate, Sequence[AuthPredicate]]
45+
46+
FuncOrCoro = Callable[..., Any]

0 commit comments

Comments
 (0)