mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-02-07 08:03:30 +08:00
more work on messaging system
This commit is contained in:
parent
9733751716
commit
14c1a20ad0
2
.idea/dictionaries/ericf.xml
generated
2
.idea/dictionaries/ericf.xml
generated
@ -715,6 +715,7 @@
|
|||||||
<w>existables</w>
|
<w>existables</w>
|
||||||
<w>expatbuilder</w>
|
<w>expatbuilder</w>
|
||||||
<w>expatreader</w>
|
<w>expatreader</w>
|
||||||
|
<w>expectedsig</w>
|
||||||
<w>explodable</w>
|
<w>explodable</w>
|
||||||
<w>explodey</w>
|
<w>explodey</w>
|
||||||
<w>exportoptions</w>
|
<w>exportoptions</w>
|
||||||
@ -1403,6 +1404,7 @@
|
|||||||
<w>mrmaxmeier</w>
|
<w>mrmaxmeier</w>
|
||||||
<w>msbuild</w>
|
<w>msbuild</w>
|
||||||
<w>msgdict</w>
|
<w>msgdict</w>
|
||||||
|
<w>msgfull</w>
|
||||||
<w>msgtype</w>
|
<w>msgtype</w>
|
||||||
<w>mshell</w>
|
<w>mshell</w>
|
||||||
<w>msvccompiler</w>
|
<w>msvccompiler</w>
|
||||||
|
|||||||
@ -332,6 +332,7 @@
|
|||||||
<w>exhash</w>
|
<w>exhash</w>
|
||||||
<w>exhashstr</w>
|
<w>exhashstr</w>
|
||||||
<w>expbool</w>
|
<w>expbool</w>
|
||||||
|
<w>expectedsig</w>
|
||||||
<w>expl</w>
|
<w>expl</w>
|
||||||
<w>extrahash</w>
|
<w>extrahash</w>
|
||||||
<w>extrascale</w>
|
<w>extrascale</w>
|
||||||
@ -640,6 +641,7 @@
|
|||||||
<w>mqrspec</w>
|
<w>mqrspec</w>
|
||||||
<w>msaa</w>
|
<w>msaa</w>
|
||||||
<w>msgdict</w>
|
<w>msgdict</w>
|
||||||
|
<w>msgfull</w>
|
||||||
<w>msgtype</w>
|
<w>msgtype</w>
|
||||||
<w>mult</w>
|
<w>mult</w>
|
||||||
<w>multing</w>
|
<w>multing</w>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||||
<h4><em>last updated on 2021-09-05 for Ballistica version 1.6.5 build 20391</em></h4>
|
<h4><em>last updated on 2021-09-07 for Ballistica version 1.6.5 build 20391</em></h4>
|
||||||
<p>This page documents the Python classes and functions in the 'ba' module,
|
<p>This page documents the Python classes and functions in the 'ba' module,
|
||||||
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@ -9,10 +9,11 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from efro.error import CleanError, RemoteError
|
||||||
from efro.dataclassio import ioprepped
|
from efro.dataclassio import ioprepped
|
||||||
from efro.message import (Message, MessageProtocol, MessageSender,
|
from efro.message import (Message, MessageProtocol, MessageSender,
|
||||||
MessageReceiver)
|
MessageReceiver)
|
||||||
# from efrotools.statictest import static_type_equals
|
from efrotools.statictest import static_type_equals
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import List, Type, Any, Callable, Union
|
from typing import List, Type, Any, Callable, Union
|
||||||
@ -63,6 +64,7 @@ class _TestMessageR3(Message):
|
|||||||
|
|
||||||
class _TestMessageSender(MessageSender):
|
class _TestMessageSender(MessageSender):
|
||||||
"""Testing type overrides for message sending.
|
"""Testing type overrides for message sending.
|
||||||
|
|
||||||
Normally this would be auto-generated based on the protocol.
|
Normally this would be auto-generated based on the protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -74,6 +76,7 @@ class _TestMessageSender(MessageSender):
|
|||||||
|
|
||||||
class _BoundTestMessageSender:
|
class _BoundTestMessageSender:
|
||||||
"""Testing type overrides for message sending.
|
"""Testing type overrides for message sending.
|
||||||
|
|
||||||
Normally this would be auto-generated based on the protocol.
|
Normally this would be auto-generated based on the protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -91,13 +94,14 @@ class _BoundTestMessageSender:
|
|||||||
message: _TestMessage2) -> Union[_TestMessageR1, _TestMessageR2]:
|
message: _TestMessage2) -> Union[_TestMessageR1, _TestMessageR2]:
|
||||||
...
|
...
|
||||||
|
|
||||||
def send(self, message: Any) -> Any:
|
def send(self, message: Message) -> Message:
|
||||||
"""Send a particular message type."""
|
"""Send a particular message type."""
|
||||||
return self._sender.send(self._obj, message)
|
return self._sender.send(self._obj, message)
|
||||||
|
|
||||||
|
|
||||||
class _TestMessageReceiver(MessageReceiver):
|
class _TestMessageReceiver(MessageReceiver):
|
||||||
"""Testing type overrides for message receiving.
|
"""Testing type overrides for message receiving.
|
||||||
|
|
||||||
Normally this would be auto-generated based on the protocol.
|
Normally this would be auto-generated based on the protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -127,6 +131,7 @@ class _TestMessageReceiver(MessageReceiver):
|
|||||||
|
|
||||||
class _BoundTestMessageReceiver:
|
class _BoundTestMessageReceiver:
|
||||||
"""Testing type overrides for message receiving.
|
"""Testing type overrides for message receiving.
|
||||||
|
|
||||||
Normally this would be auto-generated based on the protocol.
|
Normally this would be auto-generated based on the protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -135,13 +140,21 @@ class _BoundTestMessageReceiver:
|
|||||||
self._obj = obj
|
self._obj = obj
|
||||||
self._receiver = receiver
|
self._receiver = receiver
|
||||||
|
|
||||||
|
def handle_raw_message(self, message: bytes) -> bytes:
|
||||||
|
"""Handle a raw incoming message."""
|
||||||
|
return self._receiver.handle_raw_message(self._obj, message)
|
||||||
|
|
||||||
TEST_PROTOCOL = MessageProtocol(message_types={
|
|
||||||
1: _TestMessage1,
|
TEST_PROTOCOL = MessageProtocol(
|
||||||
2: _TestMessage2,
|
message_types={
|
||||||
3: _TestMessageR1,
|
1: _TestMessage1,
|
||||||
4: _TestMessageR2,
|
2: _TestMessage2,
|
||||||
})
|
3: _TestMessageR1,
|
||||||
|
4: _TestMessageR2,
|
||||||
|
},
|
||||||
|
trusted_client=True,
|
||||||
|
log_remote_exceptions=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_protocol_creation() -> None:
|
def test_protocol_creation() -> None:
|
||||||
@ -159,6 +172,37 @@ def test_protocol_creation() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_creation() -> None:
|
||||||
|
"""Test receiver creation"""
|
||||||
|
|
||||||
|
# This should fail due to the registered handler only specifying
|
||||||
|
# one response message type while the message type itself
|
||||||
|
# specifies two.
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
|
||||||
|
class _TestClassR:
|
||||||
|
"""Test class incorporating receive functionality."""
|
||||||
|
|
||||||
|
receiver = _TestMessageReceiver(TEST_PROTOCOL)
|
||||||
|
|
||||||
|
@receiver.handler
|
||||||
|
def handle_test_message_2(self,
|
||||||
|
msg: _TestMessage2) -> _TestMessageR2:
|
||||||
|
"""Test."""
|
||||||
|
del msg # Unused
|
||||||
|
print('Hello from test message 1 handler!')
|
||||||
|
return _TestMessageR2(fval=1.2)
|
||||||
|
|
||||||
|
# Should fail because not all message types in the protocol are handled.
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
|
||||||
|
class _TestClassR2:
|
||||||
|
"""Test class incorporating receive functionality."""
|
||||||
|
|
||||||
|
receiver = _TestMessageReceiver(TEST_PROTOCOL)
|
||||||
|
receiver.validate_handler_completeness()
|
||||||
|
|
||||||
|
|
||||||
def test_message_sending() -> None:
|
def test_message_sending() -> None:
|
||||||
"""Test simple message sending."""
|
"""Test simple message sending."""
|
||||||
|
|
||||||
@ -168,14 +212,13 @@ def test_message_sending() -> None:
|
|||||||
|
|
||||||
msg = _TestMessageSender(TEST_PROTOCOL)
|
msg = _TestMessageSender(TEST_PROTOCOL)
|
||||||
|
|
||||||
def __init__(self, receiver: TestClassR) -> None:
|
def __init__(self, target: TestClassR) -> None:
|
||||||
self._receiver = receiver
|
self._target = target
|
||||||
|
|
||||||
@msg.send_raw_handler
|
@msg.send_raw_handler
|
||||||
def _send_raw_message(self, data: bytes) -> bytes:
|
def _send_raw_message(self, data: bytes) -> bytes:
|
||||||
"""Test."""
|
"""Test."""
|
||||||
print(f'WOULD SEND RAW MSG OF SIZE: {len(data)}')
|
return self._target.receiver.handle_raw_message(data)
|
||||||
return b''
|
|
||||||
|
|
||||||
class TestClassR:
|
class TestClassR:
|
||||||
"""Test class incorporating receive functionality."""
|
"""Test class incorporating receive functionality."""
|
||||||
@ -185,8 +228,11 @@ def test_message_sending() -> None:
|
|||||||
@receiver.handler
|
@receiver.handler
|
||||||
def handle_test_message_1(self, msg: _TestMessage1) -> _TestMessageR1:
|
def handle_test_message_1(self, msg: _TestMessage1) -> _TestMessageR1:
|
||||||
"""Test."""
|
"""Test."""
|
||||||
del msg # Unused
|
|
||||||
print('Hello from test message 1 handler!')
|
print('Hello from test message 1 handler!')
|
||||||
|
if msg.ival == 1:
|
||||||
|
raise CleanError('Testing Clean Error')
|
||||||
|
if msg.ival == 2:
|
||||||
|
raise RuntimeError('Testing Runtime Error')
|
||||||
return _TestMessageR1(bval=True)
|
return _TestMessageR1(bval=True)
|
||||||
|
|
||||||
@receiver.handler
|
@receiver.handler
|
||||||
@ -195,14 +241,27 @@ def test_message_sending() -> None:
|
|||||||
msg: _TestMessage2) -> Union[_TestMessageR1, _TestMessageR2]:
|
msg: _TestMessage2) -> Union[_TestMessageR1, _TestMessageR2]:
|
||||||
"""Test."""
|
"""Test."""
|
||||||
del msg # Unused
|
del msg # Unused
|
||||||
print('Hello from test message 1 handler!')
|
print('Hello from test message 2 handler!')
|
||||||
return _TestMessageR2(fval=1.2)
|
return _TestMessageR2(fval=1.2)
|
||||||
|
|
||||||
obj_r = TestClassR()
|
receiver.validate_handler_completeness()
|
||||||
obj_s = TestClassS(receiver=obj_r)
|
|
||||||
|
|
||||||
_result = obj_s.msg.send(_TestMessage1(ival=0))
|
obj_r = TestClassR()
|
||||||
_result2 = obj_s.msg.send(_TestMessage2(sval='rah'))
|
obj_s = TestClassS(target=obj_r)
|
||||||
print('SKIPPING STATIC CHECK')
|
|
||||||
# assert static_type_equals(result, _TestMessageR1)
|
response = obj_s.msg.send(_TestMessage1(ival=0))
|
||||||
# assert isinstance(result, _TestMessageR1)
|
response2 = obj_s.msg.send(_TestMessage2(sval='rah'))
|
||||||
|
assert static_type_equals(response, _TestMessageR1)
|
||||||
|
assert isinstance(response, _TestMessageR1)
|
||||||
|
assert isinstance(response2, (_TestMessageR1, _TestMessageR2))
|
||||||
|
|
||||||
|
# Remote CleanErrors should come across locally as the same.
|
||||||
|
try:
|
||||||
|
_response3 = obj_s.msg.send(_TestMessage1(ival=1))
|
||||||
|
except Exception as exc:
|
||||||
|
assert isinstance(exc, CleanError)
|
||||||
|
assert str(exc) == 'Testing Clean Error'
|
||||||
|
|
||||||
|
# Other remote errors should come across as RemoteError.
|
||||||
|
with pytest.raises(RemoteError):
|
||||||
|
_response4 = obj_s.msg.send(_TestMessage1(ival=2))
|
||||||
|
|||||||
@ -9,12 +9,16 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from efro.error import CleanError, RemoteError
|
||||||
from efro.dataclassio import (ioprepped, is_ioprepped_dataclass, IOAttrs,
|
from efro.dataclassio import (ioprepped, is_ioprepped_dataclass, IOAttrs,
|
||||||
dataclass_to_dict)
|
dataclass_to_dict, dataclass_from_dict)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import (Dict, Type, Tuple, List, Any, Callable, Optional, Set,
|
from typing import (Dict, Type, Tuple, List, Any, Callable, Optional, Set,
|
||||||
@ -63,7 +67,8 @@ class MessageProtocol:
|
|||||||
message_types: Dict[int, Type[Message]],
|
message_types: Dict[int, Type[Message]],
|
||||||
type_key: Optional[str] = None,
|
type_key: Optional[str] = None,
|
||||||
preserve_clean_errors: bool = True,
|
preserve_clean_errors: bool = True,
|
||||||
remote_stack_traces: bool = False) -> None:
|
log_remote_exceptions: bool = True,
|
||||||
|
trusted_client: bool = False) -> None:
|
||||||
"""Create a protocol with a given configuration.
|
"""Create a protocol with a given configuration.
|
||||||
Each entry for message_types should contain an ID, a message type,
|
Each entry for message_types should contain an ID, a message type,
|
||||||
and all possible response types.
|
and all possible response types.
|
||||||
@ -77,23 +82,24 @@ class MessageProtocol:
|
|||||||
on the remote end will result in the same error raised locally.
|
on the remote end will result in the same error raised locally.
|
||||||
All other Exception types come across as efro.error.RemoteError.
|
All other Exception types come across as efro.error.RemoteError.
|
||||||
|
|
||||||
If 'remote_stack_traces' is True, stringified remote stack traces will
|
If 'trusted_client' is True, stringified remote stack traces will
|
||||||
be included in the RemoteError. This should only be enabled in cases
|
be included in the RemoteError. This should only be enabled in cases
|
||||||
where the client is trusted.
|
where the client is trusted.
|
||||||
"""
|
"""
|
||||||
self._message_types_by_id: Dict[int, Type[Message]] = {}
|
self.message_types_by_id: Dict[int, Type[Message]] = {}
|
||||||
self._message_ids_by_type: Dict[Type[Message], int] = {}
|
self.message_ids_by_type: Dict[Type[Message], int] = {}
|
||||||
for m_id, m_type in message_types.items():
|
for m_id, m_type in message_types.items():
|
||||||
|
|
||||||
# Make sure only valid message types were passed and each
|
# Make sure only valid message types were passed and each
|
||||||
# id was assigned only once.
|
# id was assigned only once.
|
||||||
assert isinstance(m_id, int)
|
assert isinstance(m_id, int)
|
||||||
|
assert m_id >= 0
|
||||||
assert (is_ioprepped_dataclass(m_type)
|
assert (is_ioprepped_dataclass(m_type)
|
||||||
and issubclass(m_type, Message))
|
and issubclass(m_type, Message))
|
||||||
assert self._message_types_by_id.get(m_id) is None
|
assert self.message_types_by_id.get(m_id) is None
|
||||||
|
|
||||||
self._message_types_by_id[m_id] = m_type
|
self.message_types_by_id[m_id] = m_type
|
||||||
self._message_ids_by_type[m_type] = m_id
|
self.message_ids_by_type[m_type] = m_id
|
||||||
|
|
||||||
# Make sure all return types are valid and have been assigned
|
# Make sure all return types are valid and have been assigned
|
||||||
# an ID as well.
|
# an ID as well.
|
||||||
@ -106,21 +112,28 @@ class MessageProtocol:
|
|||||||
all_response_types.update(m_rtypes)
|
all_response_types.update(m_rtypes)
|
||||||
for cls in all_response_types:
|
for cls in all_response_types:
|
||||||
assert is_ioprepped_dataclass(cls) and issubclass(cls, Message)
|
assert is_ioprepped_dataclass(cls) and issubclass(cls, Message)
|
||||||
if cls not in self._message_ids_by_type:
|
if cls not in self.message_ids_by_type:
|
||||||
raise ValueError(f'Possible response type {cls}'
|
raise ValueError(f'Possible response type {cls}'
|
||||||
f' was not included in message_types.')
|
f' was not included in message_types.')
|
||||||
|
|
||||||
self._type_key = type_key
|
self._type_key = type_key
|
||||||
self._preserve_clean_errors = preserve_clean_errors
|
self.preserve_clean_errors = preserve_clean_errors
|
||||||
self._remote_stack_traces = remote_stack_traces
|
self.log_remote_exceptions = log_remote_exceptions
|
||||||
|
self.trusted_client = trusted_client
|
||||||
|
|
||||||
def message_encode(self, message: Message) -> bytes:
|
def message_encode(self,
|
||||||
"""Encode a message to bytes for sending."""
|
message: Message,
|
||||||
|
is_error: bool = False) -> bytes:
|
||||||
|
"""Encode a message to bytes for transport."""
|
||||||
|
|
||||||
m_id = self._message_ids_by_type.get(type(message))
|
m_id: Optional[int]
|
||||||
if m_id is None:
|
if is_error:
|
||||||
raise TypeError(f'Message type is not registered in Protocol:'
|
m_id = -1
|
||||||
f' {type(message)}')
|
else:
|
||||||
|
m_id = self.message_ids_by_type.get(type(message))
|
||||||
|
if m_id is None:
|
||||||
|
raise TypeError(f'Message type is not registered in Protocol:'
|
||||||
|
f' {type(message)}')
|
||||||
msgdict = dataclass_to_dict(message)
|
msgdict = dataclass_to_dict(message)
|
||||||
|
|
||||||
# Encode type as part of the message dict if desired
|
# Encode type as part of the message dict if desired
|
||||||
@ -136,9 +149,38 @@ class MessageProtocol:
|
|||||||
return json.dumps(out, separators=(',', ':')).encode()
|
return json.dumps(out, separators=(',', ':')).encode()
|
||||||
|
|
||||||
def message_decode(self, data: bytes) -> Message:
|
def message_decode(self, data: bytes) -> Message:
|
||||||
"""Decode a message from bytes."""
|
"""Decode a message from bytes.
|
||||||
print(f'WOULD DECODE MSG FROM RAW: {str(data)}')
|
|
||||||
return Message()
|
If the message represents a remote error, an Exception will
|
||||||
|
be raised.
|
||||||
|
"""
|
||||||
|
msgfull = json.loads(data.decode())
|
||||||
|
assert isinstance(msgfull, dict)
|
||||||
|
msgdict: Optional[dict]
|
||||||
|
if self._type_key is not None:
|
||||||
|
m_id = msgfull.pop(self._type_key)
|
||||||
|
msgdict = msgfull
|
||||||
|
assert isinstance(m_id, int)
|
||||||
|
else:
|
||||||
|
m_id = msgfull.get('t')
|
||||||
|
msgdict = msgfull.get('m')
|
||||||
|
assert isinstance(m_id, int)
|
||||||
|
assert isinstance(msgdict, dict)
|
||||||
|
|
||||||
|
# Special case: a remote error occurred. Raise a local Exception.
|
||||||
|
if m_id == -1:
|
||||||
|
err = dataclass_from_dict(RemoteErrorMessage, msgdict)
|
||||||
|
if (self.preserve_clean_errors
|
||||||
|
and err.error_type is RemoteErrorType.CLEAN):
|
||||||
|
raise CleanError(err.error_message)
|
||||||
|
raise RemoteError(err.error_message)
|
||||||
|
|
||||||
|
# Decode this particular type and make sure its valid.
|
||||||
|
msgtype = self.message_types_by_id.get(m_id)
|
||||||
|
if msgtype is None:
|
||||||
|
raise TypeError(f'Got unregistered message type id of {m_id}.')
|
||||||
|
|
||||||
|
return dataclass_from_dict(msgtype, msgdict)
|
||||||
|
|
||||||
def create_sender_module(self, classname: str) -> str:
|
def create_sender_module(self, classname: str) -> str:
|
||||||
""""Create a Python module defining a MessageSender subclass.
|
""""Create a Python module defining a MessageSender subclass.
|
||||||
@ -156,23 +198,6 @@ class MessageProtocol:
|
|||||||
the protocol.
|
the protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def validate_message_type(self, msgtype: Type,
|
|
||||||
responsetypes: Sequence[Type]) -> None:
|
|
||||||
"""Ensure message type associated response types are valid.
|
|
||||||
Raises an exception if not.
|
|
||||||
"""
|
|
||||||
if msgtype not in self._message_ids_by_type:
|
|
||||||
raise TypeError(f'Message type {msgtype} is not registered'
|
|
||||||
f' in this Protocol.')
|
|
||||||
|
|
||||||
# Make sure the responses exactly matches what the message expects.
|
|
||||||
assert len(set(responsetypes)) == len(responsetypes)
|
|
||||||
|
|
||||||
for responsetype in responsetypes:
|
|
||||||
if responsetype not in self._message_ids_by_type:
|
|
||||||
raise TypeError(f'Response message type {responsetype} is'
|
|
||||||
f' not registered in this Protocol.')
|
|
||||||
|
|
||||||
|
|
||||||
class MessageSender:
|
class MessageSender:
|
||||||
"""Facilitates sending messages to a target and receiving responses.
|
"""Facilitates sending messages to a target and receiving responses.
|
||||||
@ -207,17 +232,27 @@ class MessageSender:
|
|||||||
self._send_raw_message_call = call
|
self._send_raw_message_call = call
|
||||||
return call
|
return call
|
||||||
|
|
||||||
def send(self, bound_obj: Any, message: Message) -> Any:
|
def send(self, bound_obj: Any, message: Message) -> Message:
|
||||||
"""Send a message and receive a response.
|
"""Send a message and receive a response.
|
||||||
|
|
||||||
Will encode the message for transport and call dispatch_raw_message()
|
Will encode the message for transport and call dispatch_raw_message()
|
||||||
"""
|
"""
|
||||||
if self._send_raw_message_call is None:
|
if self._send_raw_message_call is None:
|
||||||
raise RuntimeError('send() is unimplemented for this type.')
|
raise RuntimeError('send() is unimplemented for this type.')
|
||||||
encoded = self._protocol.message_encode(message)
|
|
||||||
return self._send_raw_message_call(bound_obj, encoded)
|
# Only types with possible response types should ever be sent.
|
||||||
|
assert type(message).get_response_types()
|
||||||
|
|
||||||
|
msg_encoded = self._protocol.message_encode(message)
|
||||||
|
response_encoded = self._send_raw_message_call(bound_obj, msg_encoded)
|
||||||
|
response = self._protocol.message_decode(response_encoded)
|
||||||
|
assert isinstance(response, Message)
|
||||||
|
assert type(response) in type(message).get_response_types()
|
||||||
|
return response
|
||||||
|
|
||||||
def send_bg(self, bound_obj: Any, message: Message) -> Message:
|
def send_bg(self, bound_obj: Any, message: Message) -> Message:
|
||||||
"""Send a message asynchronously and receive a future.
|
"""Send a message asynchronously and receive a future.
|
||||||
|
|
||||||
The message will be encoded for transport and passed to
|
The message will be encoded for transport and passed to
|
||||||
dispatch_raw_message from a background thread.
|
dispatch_raw_message from a background thread.
|
||||||
"""
|
"""
|
||||||
@ -225,6 +260,7 @@ class MessageSender:
|
|||||||
|
|
||||||
def send_async(self, bound_obj: Any, message: Message) -> Message:
|
def send_async(self, bound_obj: Any, message: Message) -> Message:
|
||||||
"""Send a message asynchronously using asyncio.
|
"""Send a message asynchronously using asyncio.
|
||||||
|
|
||||||
The message will be encoded for transport and passed to
|
The message will be encoded for transport and passed to
|
||||||
dispatch_raw_message_async.
|
dispatch_raw_message_async.
|
||||||
"""
|
"""
|
||||||
@ -233,6 +269,7 @@ class MessageSender:
|
|||||||
|
|
||||||
class MessageReceiver:
|
class MessageReceiver:
|
||||||
"""Facilitates receiving & responding to messages from a remote source.
|
"""Facilitates receiving & responding to messages from a remote source.
|
||||||
|
|
||||||
This is instantiated at the class level with unbound methods registered
|
This is instantiated at the class level with unbound methods registered
|
||||||
as handlers for different message types in the protocol.
|
as handlers for different message types in the protocol.
|
||||||
|
|
||||||
@ -257,10 +294,13 @@ class MessageReceiver:
|
|||||||
|
|
||||||
def __init__(self, protocol: MessageProtocol) -> None:
|
def __init__(self, protocol: MessageProtocol) -> None:
|
||||||
self._protocol = protocol
|
self._protocol = protocol
|
||||||
|
self._handlers: Dict[Type[Message], Callable] = {}
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
def register_handler(self, call: Callable) -> None:
|
def register_handler(self, call: Callable[[Any, Message],
|
||||||
|
Message]) -> None:
|
||||||
"""Register a handler call.
|
"""Register a handler call.
|
||||||
|
|
||||||
The message type handled by the call is determined by its
|
The message type handled by the call is determined by its
|
||||||
type annotation.
|
type annotation.
|
||||||
"""
|
"""
|
||||||
@ -268,15 +308,26 @@ class MessageReceiver:
|
|||||||
from typing import _GenericAlias # type: ignore
|
from typing import _GenericAlias # type: ignore
|
||||||
from typing import Union, get_type_hints, get_args
|
from typing import Union, get_type_hints, get_args
|
||||||
|
|
||||||
|
sig = inspect.getfullargspec(call)
|
||||||
|
|
||||||
|
# The provided callable should be a method taking one 'msg' arg.
|
||||||
|
expectedsig = ['self', 'msg']
|
||||||
|
if sig.args != expectedsig:
|
||||||
|
raise ValueError(f'Expected callable signature of {expectedsig};'
|
||||||
|
f' got {sig.args}')
|
||||||
|
|
||||||
|
# Check annotation types to determine what message types we handle.
|
||||||
# Return-type annotation can be a Union, but we probably don't
|
# Return-type annotation can be a Union, but we probably don't
|
||||||
# have it available at runtime. Explicitly pull it in.
|
# have it available at runtime. Explicitly pull it in.
|
||||||
anns = get_type_hints(call, localns={'Union': Union})
|
anns = get_type_hints(call, localns={'Union': Union})
|
||||||
msg = anns.get('msg')
|
msgtype = anns.get('msg')
|
||||||
if not isinstance(msg, type):
|
if not isinstance(msgtype, type):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f'expected a type for "msg" annotation; got {type(msg)}.')
|
f'expected a type for "msg" annotation; got {type(msgtype)}.')
|
||||||
|
assert issubclass(msgtype, Message)
|
||||||
|
|
||||||
ret = anns.get('return')
|
ret = anns.get('return')
|
||||||
rets: Tuple[Type, ...]
|
responsetypes: Tuple[Type, ...]
|
||||||
|
|
||||||
# Return types can be a single type or a union of types.
|
# Return types can be a single type or a union of types.
|
||||||
if isinstance(ret, _GenericAlias):
|
if isinstance(ret, _GenericAlias):
|
||||||
@ -284,27 +335,93 @@ class MessageReceiver:
|
|||||||
if not all(isinstance(a, type) for a in targs):
|
if not all(isinstance(a, type) for a in targs):
|
||||||
raise TypeError(f'expected only types for "return" annotation;'
|
raise TypeError(f'expected only types for "return" annotation;'
|
||||||
f' got {targs}.')
|
f' got {targs}.')
|
||||||
rets = targs
|
responsetypes = targs
|
||||||
|
|
||||||
print(f'LOOKED AT GENERIC ALIAS {targs}')
|
|
||||||
else:
|
else:
|
||||||
if not isinstance(ret, type):
|
if not isinstance(ret, type):
|
||||||
raise TypeError(f'expected one or more types for'
|
raise TypeError(f'expected one or more types for'
|
||||||
f' "return" annotation; got a {type(ret)}.')
|
f' "return" annotation; got a {type(ret)}.')
|
||||||
rets = (ret, )
|
responsetypes = (ret, )
|
||||||
|
|
||||||
print(f'WOULD REGISTER HANDLER! (got {msg} and {rets})')
|
# Make sure our protocol has this message type registered and our
|
||||||
|
# return types exactly match. (Technically we could return a subset
|
||||||
|
# of the supported types; can allow this in the future if it makes
|
||||||
|
# sense).
|
||||||
|
registered_types = self._protocol.message_ids_by_type.keys()
|
||||||
|
|
||||||
def handle_raw_message(self, msg: bytes) -> bytes:
|
if msgtype not in registered_types:
|
||||||
"""Should be called when the receiver gets a message.
|
raise TypeError(f'Message type {msgtype} is not registered'
|
||||||
The return value is the raw response to the message.
|
f' in this Protocol.')
|
||||||
|
|
||||||
|
if msgtype in self._handlers:
|
||||||
|
raise TypeError(f'Message type {msgtype} already has a registered'
|
||||||
|
f' handler.')
|
||||||
|
|
||||||
|
# Make sure the responses exactly matches what the message expects.
|
||||||
|
if set(responsetypes) != set(msgtype.get_response_types()):
|
||||||
|
raise TypeError(
|
||||||
|
f'Provided response types {responsetypes} do not'
|
||||||
|
f' match the set expected by message type {msgtype}: '
|
||||||
|
f'({msgtype.get_response_types()})')
|
||||||
|
|
||||||
|
# Ok; we're good!
|
||||||
|
self._handlers[msgtype] = call
|
||||||
|
|
||||||
|
def validate_handler_completeness(self, warn_only: bool = False) -> None:
|
||||||
|
"""Return whether this receiver handles all protocol messages.
|
||||||
|
|
||||||
|
Only messages having possible response types are considered, as
|
||||||
|
those are the only ones that can be sent to a receiver.
|
||||||
"""
|
"""
|
||||||
print('RECEIVER WOULD HANDLE RAW MESSAGE')
|
for msgtype in self._protocol.message_ids_by_type.keys():
|
||||||
del msg # Unused
|
if not msgtype.get_response_types():
|
||||||
return b''
|
continue
|
||||||
|
if msgtype not in self._handlers:
|
||||||
|
msg = (f'Protocol message {msgtype} not handled'
|
||||||
|
f' by receiver.')
|
||||||
|
if warn_only:
|
||||||
|
logging.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
def handle_raw_message(self, bound_obj: Any, msg: bytes) -> bytes:
|
||||||
|
"""Decode, handle, and return encoded response for a message."""
|
||||||
|
try:
|
||||||
|
# Decode the incoming message.
|
||||||
|
msg_decoded = self._protocol.message_decode(msg)
|
||||||
|
msgtype = type(msg_decoded)
|
||||||
|
|
||||||
|
# Call the proper handler.
|
||||||
|
handler = self._handlers.get(msgtype)
|
||||||
|
if handler is None:
|
||||||
|
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
||||||
|
response = handler(bound_obj, msg_decoded)
|
||||||
|
|
||||||
|
# Re-encode the response.
|
||||||
|
assert isinstance(response, Message)
|
||||||
|
assert type(response) in msgtype.get_response_types()
|
||||||
|
return self._protocol.message_encode(response)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
|
||||||
|
if self._protocol.log_remote_exceptions:
|
||||||
|
logging.exception('Error handling message.')
|
||||||
|
|
||||||
|
# If anything goes wrong, return a RemoteErrorMessage instead.
|
||||||
|
if (isinstance(exc, CleanError)
|
||||||
|
and self._protocol.preserve_clean_errors):
|
||||||
|
response = RemoteErrorMessage(error_message=str(exc),
|
||||||
|
error_type=RemoteErrorType.CLEAN)
|
||||||
|
else:
|
||||||
|
|
||||||
|
response = RemoteErrorMessage(
|
||||||
|
error_message=(traceback.format_exc()
|
||||||
|
if self._protocol.trusted_client else
|
||||||
|
'An unknown error has occurred.'),
|
||||||
|
error_type=RemoteErrorType.OTHER)
|
||||||
|
return self._protocol.message_encode(response, is_error=True)
|
||||||
|
|
||||||
async def handle_raw_message_async(self, msg: bytes) -> bytes:
|
async def handle_raw_message_async(self, msg: bytes) -> bytes:
|
||||||
"""Should be called when the receiver gets a message.
|
"""Should be called when the receiver gets a message.
|
||||||
|
|
||||||
The return value is the raw response to the message.
|
The return value is the raw response to the message.
|
||||||
"""
|
"""
|
||||||
raise RuntimeError('Unimplemented!')
|
raise RuntimeError('Unimplemented!')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user