1# Copyright (c) 2018-2019, Neil Booth 2# 3# All rights reserved. 4# 5# The MIT License (MIT) 6# 7# Permission is hereby granted, free of charge, to any person obtaining 8# a copy of this software and associated documentation files (the 9# "Software"), to deal in the Software without restriction, including 10# without limitation the rights to use, copy, modify, merge, publish, 11# distribute, sublicense, and/or sell copies of the Software, and to 12# permit persons to whom the Software is furnished to do so, subject to 13# the following conditions: 14# 15# The above copyright notice and this permission notice shall be 16# included in all copies or substantial portions of the Software. 17# 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 26'''Classes for JSONRPC versions 1.0 and 2.0, and a loose interpretation.''' 27 28__all__ = ('JSONRPC', 'JSONRPCv1', 'JSONRPCv2', 'JSONRPCLoose', 29 'JSONRPCAutoDetect', 'Request', 'Notification', 'Batch', 30 'RPCError', 'ProtocolError', 'JSONRPCConnection', 'handler_invocation') 31 32import itertools 33import json 34from functools import partial 35from numbers import Number 36 37from asyncio import get_event_loop 38from aiorpcx.util import signature_info 39 40 41class SingleRequest: 42 __slots__ = ('method', 'args') 43 44 def __init__(self, method, args): 45 if not isinstance(method, str): 46 raise ProtocolError(JSONRPC.METHOD_NOT_FOUND, 47 'method must be a string') 48 if not isinstance(args, (list, tuple, dict)): 49 raise ProtocolError.invalid_args('request arguments must be a ' 50 'list or a dictionary') 51 self.args = args 52 self.method = method 53 54 def __repr__(self): 55 return f'{self.__class__.__name__}({self.method!r}, {self.args!r})' 56 57 def __eq__(self, other): 58 return (isinstance(other, self.__class__) and 59 self.method == other.method and self.args == other.args) 60 61 62class Request(SingleRequest): 63 def send_result(self, _response): 64 return None 65 66 67class Notification(SingleRequest): 68 pass 69 70 71class Batch: 72 __slots__ = ('items', ) 73 74 def __init__(self, items): 75 if not isinstance(items, (list, tuple)): 76 raise ProtocolError.invalid_request('items must be a list') 77 if not items: 78 raise ProtocolError.empty_batch() 79 if not (all(isinstance(item, SingleRequest) for item in items) or 80 all(isinstance(item, Response) for item in items)): 81 raise ProtocolError.invalid_request('batch must be homogeneous') 82 self.items = items 83 84 def __len__(self): 85 return len(self.items) 86 87 def __getitem__(self, item): 88 return self.items[item] 89 90 def __iter__(self): 91 return iter(self.items) 92 93 def __repr__(self): 94 return f'Batch({len(self.items)} items)' 95 96 97class Response: 98 __slots__ = ('result', ) 99 100 def __init__(self, result): 101 # Type checking happens when converting to a message 102 self.result = result 103 104 105class CodeMessageError(Exception): 106 '''Invoke as CodeMessageError(code, message)''' 107 108 @property 109 def code(self): 110 return self.args[0] 111 112 @property 113 def message(self): 114 return self.args[1] 115 116 def __eq__(self, other): 117 return (isinstance(other, self.__class__) and 118 self.code == other.code and self.message == other.message) 119 120 def __hash__(self): 121 # overridden to make the exception hashable 122 # see https://bugs.python.org/issue28603 123 return hash((self.code, self.message)) 124 125 @classmethod 126 def invalid_args(cls, message): 127 return cls(JSONRPC.INVALID_ARGS, message) 128 129 @classmethod 130 def invalid_request(cls, message): 131 return cls(JSONRPC.INVALID_REQUEST, message) 132 133 @classmethod 134 def empty_batch(cls): 135 return cls.invalid_request('batch is empty') 136 137 138class RPCError(CodeMessageError): 139 140 def __init__(self, code, message, *, cost=0.0): 141 super().__init__(code, message) 142 self.cost = cost 143 144 145class ProtocolError(CodeMessageError): 146 147 def __init__(self, code, message): 148 super().__init__(code, message) 149 # If not None send this unframed message over the network 150 self.error_message = None 151 # If the error was in a JSON response message; its message ID. 152 # Since None can be a response message ID, "id" means the 153 # error was not sent in a JSON response 154 self.response_msg_id = id 155 156 157class JSONRPC: 158 '''Abstract base class that interprets and constructs JSON RPC messages.''' 159 160 # Error codes. See http://www.jsonrpc.org/specification 161 PARSE_ERROR = -32700 162 INVALID_REQUEST = -32600 163 METHOD_NOT_FOUND = -32601 164 INVALID_ARGS = -32602 165 INTERNAL_ERROR = -32603 166 # Codes specific to this library 167 ERROR_CODE_UNAVAILABLE = -100 168 EXCESSIVE_RESOURCE_USAGE = -101 169 SERVER_BUSY = -102 170 171 # Can be overridden by derived classes 172 allow_batches = True 173 174 @classmethod 175 def _message_id(cls, message, require_id): 176 '''Validate the message is a dictionary and return its ID. 177 178 Raise an error if the message is invalid or the ID is of an 179 invalid type. If it has no ID, raise an error if require_id 180 is True, otherwise return None. 181 ''' 182 raise NotImplementedError 183 184 @classmethod 185 def _validate_message(cls, message): 186 '''Validate other parts of the message other than those 187 done in _message_id.''' 188 189 @classmethod 190 def _request_args(cls, request): 191 '''Validate the existence and type of the arguments passed 192 in the request dictionary.''' 193 raise NotImplementedError 194 195 @classmethod 196 def _process_request(cls, payload): 197 request_id = None 198 try: 199 request_id = cls._message_id(payload, False) 200 cls._validate_message(payload) 201 method = payload.get('method') 202 if request_id is None: 203 item = Notification(method, cls._request_args(payload)) 204 else: 205 item = Request(method, cls._request_args(payload)) 206 return item, request_id 207 except ProtocolError as error: 208 code, message = error.code, error.message 209 raise cls._error(code, message, True, request_id) 210 211 @classmethod 212 def _process_response(cls, payload): 213 request_id = None 214 try: 215 request_id = cls._message_id(payload, True) 216 cls._validate_message(payload) 217 return Response(cls.response_value(payload)), request_id 218 except ProtocolError as error: 219 code, message = error.code, error.message 220 raise cls._error(code, message, False, request_id) 221 222 @classmethod 223 def _message_to_payload(cls, message): 224 '''Returns a Python object or a ProtocolError.''' 225 try: 226 return json.loads(message.decode()) 227 except UnicodeDecodeError: 228 message = 'messages must be encoded in UTF-8' 229 except json.JSONDecodeError: 230 message = 'invalid JSON' 231 raise cls._error(cls.PARSE_ERROR, message, True, None) 232 233 @classmethod 234 def _error(cls, code, message, send, msg_id): 235 error = ProtocolError(code, message) 236 if send: 237 error.error_message = cls.response_message(error, msg_id) 238 else: 239 error.response_msg_id = msg_id 240 return error 241 242 # 243 # External API 244 # 245 246 @classmethod 247 def message_to_item(cls, message): 248 '''Translate an unframed received message and return an 249 (item, request_id) pair. 250 251 The item can be a Request, Notification, Response or a list. 252 253 A JSON RPC error response is returned as an RPCError inside a 254 Response object. 255 256 If a Batch is returned, request_id is an iterable of request 257 ids, one per batch member. 258 259 If the message violates the protocol in some way a 260 ProtocolError is returned, except if the message was 261 determined to be a response, in which case the ProtocolError 262 is placed inside a Response object. This is so that client 263 code can mark a request as having been responded to even if 264 the response was bad. 265 266 raises: ProtocolError 267 ''' 268 payload = cls._message_to_payload(message) 269 if isinstance(payload, dict): 270 if 'method' in payload: 271 return cls._process_request(payload) 272 else: 273 return cls._process_response(payload) 274 elif isinstance(payload, list) and cls.allow_batches: 275 if not payload: 276 raise cls._error(JSONRPC.INVALID_REQUEST, 'batch is empty', 277 True, None) 278 return payload, None 279 raise cls._error(cls.INVALID_REQUEST, 280 'request object must be a dictionary', True, None) 281 282 # Message formation 283 @classmethod 284 def request_message(cls, item, request_id): 285 '''Convert an RPCRequest item to a message.''' 286 assert isinstance(item, Request) 287 return cls.encode_payload(cls.request_payload(item, request_id)) 288 289 @classmethod 290 def notification_message(cls, item): 291 '''Convert an RPCRequest item to a message.''' 292 assert isinstance(item, Notification) 293 return cls.encode_payload(cls.request_payload(item, None)) 294 295 @classmethod 296 def response_message(cls, result, request_id): 297 '''Convert a response result (or RPCError) to a message.''' 298 if isinstance(result, CodeMessageError): 299 payload = cls.error_payload(result, request_id) 300 else: 301 payload = cls.response_payload(result, request_id) 302 return cls.encode_payload(payload) 303 304 @classmethod 305 def batch_message(cls, batch, request_ids): 306 '''Convert a request Batch to a message.''' 307 assert isinstance(batch, Batch) 308 if not cls.allow_batches: 309 raise ProtocolError.invalid_request( 310 'protocol does not permit batches') 311 id_iter = iter(request_ids) 312 rm = cls.request_message 313 nm = cls.notification_message 314 parts = (rm(request, next(id_iter)) if isinstance(request, Request) 315 else nm(request) for request in batch) 316 return cls.batch_message_from_parts(parts) 317 318 @classmethod 319 def batch_message_from_parts(cls, messages): 320 '''Convert messages, one per batch item, into a batch message. At 321 least one message must be passed. 322 ''' 323 # Comma-separate the messages and wrap the lot in square brackets 324 middle = b', '.join(messages) 325 if not middle: 326 raise ProtocolError.empty_batch() 327 return b''.join([b'[', middle, b']']) 328 329 @classmethod 330 def encode_payload(cls, payload): 331 '''Encode a Python object as JSON and convert it to bytes.''' 332 try: 333 return json.dumps(payload).encode() 334 except TypeError: 335 msg = f'JSON payload encoding error: {payload}' 336 raise ProtocolError(cls.INTERNAL_ERROR, msg) from None 337 338 339class JSONRPCv1(JSONRPC): 340 '''JSON RPC version 1.0.''' 341 342 allow_batches = False 343 344 @classmethod 345 def _message_id(cls, message, require_id): 346 # JSONv1 requires an ID always, but without constraint on its type 347 # No need to test for a dictionary here as we don't handle batches. 348 if 'id' not in message: 349 raise ProtocolError.invalid_request('request has no "id"') 350 return message['id'] 351 352 @classmethod 353 def _request_args(cls, request): 354 args = request.get('params') 355 if not isinstance(args, list): 356 raise ProtocolError.invalid_args( 357 f'invalid request arguments: {args}') 358 return args 359 360 @classmethod 361 def _best_effort_error(cls, error): 362 # Do our best to interpret the error 363 code = cls.ERROR_CODE_UNAVAILABLE 364 message = 'no error message provided' 365 if isinstance(error, str): 366 message = error 367 elif isinstance(error, int): 368 code = error 369 elif isinstance(error, dict): 370 if isinstance(error.get('message'), str): 371 message = error['message'] 372 if isinstance(error.get('code'), int): 373 code = error['code'] 374 375 return RPCError(code, message) 376 377 @classmethod 378 def response_value(cls, payload): 379 if 'result' not in payload or 'error' not in payload: 380 raise ProtocolError.invalid_request( 381 'response must contain both "result" and "error"') 382 383 result = payload['result'] 384 error = payload['error'] 385 if error is None: 386 return result # It seems None can be a valid result 387 if result is not None: 388 raise ProtocolError.invalid_request( 389 'response has a "result" and an "error"') 390 391 return cls._best_effort_error(error) 392 393 @classmethod 394 def request_payload(cls, request, request_id): 395 '''JSON v1 request (or notification) payload.''' 396 if isinstance(request.args, dict): 397 raise ProtocolError.invalid_args( 398 'JSONRPCv1 does not support named arguments') 399 return { 400 'method': request.method, 401 'params': request.args, 402 'id': request_id 403 } 404 405 @classmethod 406 def response_payload(cls, result, request_id): 407 '''JSON v1 response payload.''' 408 return { 409 'result': result, 410 'error': None, 411 'id': request_id 412 } 413 414 @classmethod 415 def error_payload(cls, error, request_id): 416 return { 417 'result': None, 418 'error': {'code': error.code, 'message': error.message}, 419 'id': request_id 420 } 421 422 423class JSONRPCv2(JSONRPC): 424 '''JSON RPC version 2.0.''' 425 426 @classmethod 427 def _message_id(cls, message, require_id): 428 if not isinstance(message, dict): 429 raise ProtocolError.invalid_request( 430 'request object must be a dictionary') 431 if 'id' in message: 432 request_id = message['id'] 433 if not isinstance(request_id, (Number, str, type(None))): 434 raise ProtocolError.invalid_request( 435 f'invalid "id": {request_id}') 436 return request_id 437 else: 438 if require_id: 439 raise ProtocolError.invalid_request('request has no "id"') 440 return None 441 442 @classmethod 443 def _validate_message(cls, message): 444 if message.get('jsonrpc') != '2.0': 445 raise ProtocolError.invalid_request('"jsonrpc" is not "2.0"') 446 447 @classmethod 448 def _request_args(cls, request): 449 args = request.get('params', []) 450 if not isinstance(args, (dict, list)): 451 raise ProtocolError.invalid_args( 452 f'invalid request arguments: {args}') 453 return args 454 455 @classmethod 456 def response_value(cls, payload): 457 if 'result' in payload: 458 if 'error' in payload: 459 raise ProtocolError.invalid_request( 460 'response contains both "result" and "error"') 461 return payload['result'] 462 463 if 'error' not in payload: 464 raise ProtocolError.invalid_request( 465 'response contains neither "result" nor "error"') 466 467 # Return an RPCError object 468 error = payload['error'] 469 if isinstance(error, dict): 470 code = error.get('code') 471 message = error.get('message') 472 if isinstance(code, int) and isinstance(message, str): 473 return RPCError(code, message) 474 475 raise ProtocolError.invalid_request( 476 f'ill-formed response error object: {error}') 477 478 @classmethod 479 def request_payload(cls, request, request_id): 480 '''JSON v2 request (or notification) payload.''' 481 payload = { 482 'jsonrpc': '2.0', 483 'method': request.method, 484 } 485 # A notification? 486 if request_id is not None: 487 payload['id'] = request_id 488 # Preserve empty dicts as missing params is read as an array 489 if request.args or request.args == {}: 490 payload['params'] = request.args 491 return payload 492 493 @classmethod 494 def response_payload(cls, result, request_id): 495 '''JSON v2 response payload.''' 496 return { 497 'jsonrpc': '2.0', 498 'result': result, 499 'id': request_id 500 } 501 502 @classmethod 503 def error_payload(cls, error, request_id): 504 return { 505 'jsonrpc': '2.0', 506 'error': {'code': error.code, 'message': error.message}, 507 'id': request_id 508 } 509 510 511class JSONRPCLoose(JSONRPC): 512 '''A relaxed versin of JSON RPC.''' 513 514 # Don't be so loose we accept any old message ID 515 _message_id = JSONRPCv2._message_id 516 _validate_message = JSONRPC._validate_message 517 _request_args = JSONRPCv2._request_args 518 # Outoing messages are JSONRPCv2 so we give the other side the 519 # best chance to assume / detect JSONRPCv2 as default protocol. 520 error_payload = JSONRPCv2.error_payload 521 request_payload = JSONRPCv2.request_payload 522 response_payload = JSONRPCv2.response_payload 523 524 @classmethod 525 def response_value(cls, payload): 526 # Return result, unless it is None and there is an error 527 if payload.get('error') is not None: 528 if payload.get('result') is not None: 529 raise ProtocolError.invalid_request( 530 'response contains both "result" and "error"') 531 return JSONRPCv1._best_effort_error(payload['error']) 532 533 if 'result' not in payload: 534 raise ProtocolError.invalid_request( 535 'response contains neither "result" nor "error"') 536 537 # Can be None 538 return payload['result'] 539 540 541class JSONRPCAutoDetect(JSONRPCv2): 542 543 @classmethod 544 def detect_protocol(cls, message): 545 '''Attempt to detect the protocol from the message.''' 546 main = cls._message_to_payload(message) 547 548 def protocol_for_payload(payload): 549 if not isinstance(payload, dict): 550 return JSONRPCLoose # Will error 551 # Obey an explicit "jsonrpc" 552 version = payload.get('jsonrpc') 553 if version == '2.0': 554 return JSONRPCv2 555 if version == '1.0': 556 return JSONRPCv1 557 558 # Now to decide between JSONRPCLoose and JSONRPCv1 if possible 559 if 'result' in payload and 'error' in payload: 560 return JSONRPCv1 561 return JSONRPCLoose 562 563 if isinstance(main, list): 564 parts = set(protocol_for_payload(payload) for payload in main) 565 # If all same protocol, return it 566 if len(parts) == 1: 567 return parts.pop() 568 # If strict protocol detected, return it, preferring JSONRPCv2. 569 # This means a batch of JSONRPCv1 will fail 570 for protocol in (JSONRPCv2, JSONRPCv1): 571 if protocol in parts: 572 return protocol 573 # Will error if no parts 574 return JSONRPCLoose 575 576 return protocol_for_payload(main) 577 578 579class JSONRPCConnection: 580 '''Maintains state of a JSON RPC connection, in particular 581 encapsulating the handling of request IDs. 582 583 protocol - the JSON RPC protocol to follow 584 max_response_size - responses over this size send an error response 585 instead. 586 ''' 587 588 def __init__(self, protocol): 589 self._protocol = protocol 590 self._id_counter = itertools.count() 591 # Sent Requests and Batches that have not received a response. 592 # The key is its request ID; for a batch it is sorted tuple 593 # of request IDs 594 self._requests = {} 595 self._create_future = get_event_loop().create_future 596 # A public attribute intended to be settable dynamically 597 self.max_response_size = 0 598 599 def _oversized_response_message(self, request_id): 600 text = f'response too large (over {self.max_response_size:,d} bytes' 601 error = RPCError.invalid_request(text) 602 return self._protocol.response_message(error, request_id) 603 604 def _receive_response(self, result, request_id): 605 if request_id not in self._requests: 606 if request_id is None and isinstance(result, RPCError): 607 message = f'diagnostic error received: {result}' 608 else: 609 message = f'response to unsent request (ID: {request_id})' 610 raise ProtocolError.invalid_request(message) from None 611 _request, future = self._requests.pop(request_id) 612 if not future.done(): 613 if isinstance(result, Exception): 614 future.set_exception(result) 615 else: 616 future.set_result(result) 617 return [] 618 619 def _receive_request_batch(self, payloads): 620 def item_send_result(request_id, result): 621 nonlocal size 622 part = protocol.response_message(result, request_id) 623 size += len(part) + 2 624 if size > self.max_response_size > 0: 625 part = self._oversized_response_message(request_id) 626 parts.append(part) 627 if len(parts) == count: 628 return protocol.batch_message_from_parts(parts) 629 return None 630 631 parts = [] 632 items = [] 633 size = 0 634 count = 0 635 protocol = self._protocol 636 for payload in payloads: 637 try: 638 item, request_id = protocol._process_request(payload) 639 items.append(item) 640 if isinstance(item, Request): 641 count += 1 642 item.send_result = partial(item_send_result, request_id) 643 except ProtocolError as error: 644 count += 1 645 parts.append(error.error_message) 646 647 if not items and parts: 648 error = ProtocolError(0, "") 649 error.error_message = protocol.batch_message_from_parts(parts) 650 raise error 651 return items 652 653 def _receive_response_batch(self, payloads): 654 request_ids = [] 655 results = [] 656 for payload in payloads: 657 # Let ProtocolError exceptions through 658 item, request_id = self._protocol._process_response(payload) 659 request_ids.append(request_id) 660 results.append(item.result) 661 662 ordered = sorted(zip(request_ids, results), key=lambda t: t[0]) 663 ordered_ids, ordered_results = zip(*ordered) 664 if ordered_ids not in self._requests: 665 raise ProtocolError.invalid_request('response to unsent batch') 666 _request_batch, future = self._requests.pop(ordered_ids) 667 if not future.done(): 668 future.set_result(ordered_results) 669 return [] 670 671 def _send_result(self, request_id, result): 672 message = self._protocol.response_message(result, request_id) 673 if len(message) > self.max_response_size > 0: 674 message = self._oversized_response_message(request_id) 675 return message 676 677 def _future(self, request, request_id): 678 future = self._create_future() 679 self._requests[request_id] = (request, future) 680 return future 681 682 # 683 # External API 684 # 685 def send_request(self, request): 686 '''Send a Request. Return a (message, event) pair. 687 688 The message is an unframed message to send over the network. 689 Wait on the event for the response; which will be in the 690 "result" attribute. 691 692 Raises: ProtocolError if the request violates the protocol 693 in some way.. 694 ''' 695 request_id = next(self._id_counter) 696 message = self._protocol.request_message(request, request_id) 697 return message, self._future(request, request_id) 698 699 def send_notification(self, notification): 700 return self._protocol.notification_message(notification) 701 702 def send_batch(self, batch): 703 ids = tuple(next(self._id_counter) 704 for request in batch if isinstance(request, Request)) 705 message = self._protocol.batch_message(batch, ids) 706 event = self._future(batch, ids) if ids else None 707 return message, event 708 709 def receive_message(self, message): 710 '''Call with an unframed message received from the network. 711 712 Raises: ProtocolError if the message violates the protocol in 713 some way. However, if it happened in a response that can be 714 paired with a request, the ProtocolError is instead set in the 715 result attribute of the send_request() that caused the error. 716 ''' 717 if self._protocol is JSONRPCAutoDetect: 718 self._protocol = JSONRPCAutoDetect.detect_protocol(message) 719 720 try: 721 item, request_id = self._protocol.message_to_item(message) 722 except ProtocolError as e: 723 if e.response_msg_id is not id: 724 return self._receive_response(e, e.response_msg_id) 725 raise 726 727 if isinstance(item, Request): 728 item.send_result = partial(self._send_result, request_id) 729 return [item] 730 if isinstance(item, Notification): 731 return [item] 732 if isinstance(item, Response): 733 return self._receive_response(item.result, request_id) 734 735 assert isinstance(item, list) 736 if all(isinstance(payload, dict) and ('result' in payload or 'error' in payload) 737 for payload in item): 738 return self._receive_response_batch(item) 739 else: 740 return self._receive_request_batch(item) 741 742 def cancel_pending_requests(self): 743 '''Cancel all pending requests.''' 744 for _request, future in self._requests.values(): 745 if not future.done(): 746 future.cancel() 747 self._requests.clear() 748 749 def pending_requests(self): 750 '''All sent requests that have not received a response.''' 751 return [request for request, event in self._requests.values()] 752 753 754def handler_invocation(handler, request): 755 method, args = request.method, request.args 756 if handler is None: 757 raise RPCError(JSONRPC.METHOD_NOT_FOUND, 758 f'unknown method "{method}"') 759 760 # We must test for too few and too many arguments. How 761 # depends on whether the arguments were passed as a list or as 762 # a dictionary. 763 info = signature_info(handler) 764 if isinstance(args, (tuple, list)): 765 if len(args) < info.min_args: 766 s = '' if len(args) == 1 else 's' 767 raise RPCError.invalid_args( 768 f'{len(args)} argument{s} passed to method ' 769 f'"{method}" but it requires {info.min_args}') 770 if info.max_args is not None and len(args) > info.max_args: 771 s = '' if len(args) == 1 else 's' 772 raise RPCError.invalid_args( 773 f'{len(args)} argument{s} passed to method ' 774 f'{method} taking at most {info.max_args}') 775 return partial(handler, *args) 776 777 # Arguments passed by name 778 if info.other_names is None: 779 raise RPCError.invalid_args(f'method "{method}" cannot ' 780 f'be called with named arguments') 781 782 missing = set(info.required_names).difference(args) 783 if missing: 784 s = '' if len(missing) == 1 else 's' 785 missing = ', '.join(sorted(f'"{name}"' for name in missing)) 786 raise RPCError.invalid_args(f'method "{method}" requires ' 787 f'parameter{s} {missing}') 788 789 if info.other_names is not any: 790 excess = set(args).difference(info.required_names) 791 excess = excess.difference(info.other_names) 792 if excess: 793 s = '' if len(excess) == 1 else 's' 794 excess = ', '.join(sorted(f'"{name}"' for name in excess)) 795 raise RPCError.invalid_args(f'method "{method}" does not ' 796 f'take parameter{s} {excess}') 797 return partial(handler, **args) 798