Protocol no longer needs to be passed when instantiating message senders/receivers

This commit is contained in:
Eric Froemling 2021-09-23 14:47:59 -05:00
parent 43788993e6
commit 91a40f1062
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
5 changed files with 169 additions and 58 deletions

View File

@ -310,6 +310,7 @@
<w>cbtn</w>
<w>cbtnoffs</w>
<w>ccfgs</w>
<w>ccind</w>
<w>ccode</w>
<w>ccompiler</w>
<w>cdrk</w>

View File

@ -159,6 +159,7 @@
<w>cbgn</w>
<w>cbtnoffs</w>
<w>ccdd</w>
<w>ccind</w>
<w>ccontext</w>
<w>ccylinder</w>
<w>cend</w>

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2021-09-22 for Ballistica version 1.6.5 build 20393</em></h4>
<h4><em>last updated on 2021-09-23 for Ballistica version 1.6.5 build 20393</em></h4>
<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>
<hr>

View File

@ -78,6 +78,10 @@ class _TResp3(Message):
class _TestMessageSender(MessageSender):
"""Protocol-specific sender."""
def __init__(self) -> None:
protocol = TEST_PROTOCOL
super().__init__(protocol)
def __get__(self,
obj: Any,
type_in: Any = None) -> _BoundTestMessageSender:
@ -129,6 +133,10 @@ class _TestSyncMessageReceiver(MessageReceiver):
is_async = False
def __init__(self) -> None:
protocol = TEST_PROTOCOL
super().__init__(protocol)
def __get__(
self,
obj: Any,
@ -180,6 +188,10 @@ class _TestAsyncMessageReceiver(MessageReceiver):
is_async = True
def __init__(self) -> None:
protocol = TEST_PROTOCOL
super().__init__(protocol)
def __get__(
self,
obj: Any,
@ -256,8 +268,15 @@ def test_protocol_creation() -> None:
def test_sender_module_embedded() -> None:
"""Test generation of protocol-specific sender modules for typing/etc."""
smod = TEST_PROTOCOL.create_sender_module('TestMessageSender',
private=True)
# NOTE: Ideally we should be testing efro.message.create_sender_module()
# here, but it requires us to pass code which imports this test module
# to get at the protocol, and that currently fails in our static mypy
# tests.
smod = TEST_PROTOCOL.do_create_sender_module(
'TestMessageSender',
'protocol = TEST_PROTOCOL',
private=True,
)
# Clip everything up to our first class declaration.
lines = smod.splitlines()
@ -279,9 +298,16 @@ def test_sender_module_embedded() -> None:
def test_receiver_module_sync_embedded() -> None:
"""Test generation of protocol-specific sender modules for typing/etc."""
smod = TEST_PROTOCOL.create_receiver_module('TestSyncMessageReceiver',
is_async=False,
private=True)
# NOTE: Ideally we should be testing efro.message.create_receiver_module()
# here, but it requires us to pass code which imports this test module
# to get at the protocol, and that currently fails in our static mypy
# tests.
smod = TEST_PROTOCOL.do_create_receiver_module(
'TestSyncMessageReceiver',
'protocol = TEST_PROTOCOL',
is_async=False,
private=True,
)
# Clip everything up to our first class declaration.
lines = smod.splitlines()
@ -304,9 +330,16 @@ def test_receiver_module_sync_embedded() -> None:
def test_receiver_module_async_embedded() -> None:
"""Test generation of protocol-specific sender modules for typing/etc."""
smod = TEST_PROTOCOL.create_receiver_module('TestAsyncMessageReceiver',
is_async=True,
private=True)
# NOTE: Ideally we should be testing efro.message.create_receiver_module()
# here, but it requires us to pass code which imports this test module
# to get at the protocol, and that currently fails in our static mypy
# tests.
smod = TEST_PROTOCOL.do_create_receiver_module(
'TestAsyncMessageReceiver',
'protocol = TEST_PROTOCOL',
is_async=True,
private=True,
)
# Clip everything up to our first class declaration.
lines = smod.splitlines()
@ -339,7 +372,7 @@ def test_receiver_creation() -> None:
class _TestClassR:
"""Test class incorporating receive functionality."""
receiver = _TestSyncMessageReceiver(TEST_PROTOCOL)
receiver = _TestSyncMessageReceiver()
@receiver.handler
def handle_test_message_2(self, msg: _TMsg2) -> _TResp2:
@ -354,7 +387,7 @@ def test_receiver_creation() -> None:
class _TestClassR2:
"""Test class incorporating receive functionality."""
receiver = _TestSyncMessageReceiver(TEST_PROTOCOL)
receiver = _TestSyncMessageReceiver()
# Checks that we've added handlers for all message types, etc.
receiver.validate()
@ -367,7 +400,7 @@ def test_full_pipeline() -> None:
class TestClassS:
"""Test class incorporating send functionality."""
msg = _TestMessageSender(TEST_PROTOCOL)
msg = _TestMessageSender()
def __init__(self, target: Union[TestClassRSync,
TestClassRAsync]) -> None:
@ -393,7 +426,7 @@ def test_full_pipeline() -> None:
class TestClassRSync:
"""Test class incorporating synchronous receive functionality."""
receiver = _TestSyncMessageReceiver(TEST_PROTOCOL)
receiver = _TestSyncMessageReceiver()
@receiver.handler
def handle_test_message_1(self, msg: _TMsg1) -> _TResp1:
@ -421,7 +454,7 @@ def test_full_pipeline() -> None:
class TestClassRAsync:
"""Test class incorporating asynchronous receive functionality."""
receiver = _TestAsyncMessageReceiver(TEST_PROTOCOL)
receiver = _TestAsyncMessageReceiver()
@receiver.handler
async def handle_test_message_1(self, msg: _TMsg1) -> _TResp1:

View File

@ -73,6 +73,16 @@ class EmptyResponse(Response):
"""The response equivalent of None."""
# TODO: could allow handlers to deal in raw values for these
# types similar to how we allow None in place of EmptyResponse.
@ioprepped
@dataclass
class BoolResponse(Response):
"""A simple bool value response."""
value: Annotated[bool, IOAttrs('v')]
class MessageProtocol:
"""Wrangles a set of message types, formats, and response types.
Both endpoints must be using a compatible Protocol for communication
@ -144,6 +154,7 @@ class MessageProtocol:
_reg_if_not(ErrorResponse, -1)
_reg_if_not(EmptyResponse, -2)
_reg_if_not(BoolResponse, -3)
# Some extra-thorough validation in debug mode.
if __debug__:
@ -303,31 +314,24 @@ class MessageProtocol:
f'\n')
return out
def create_sender_module(self,
basename: str,
private: bool = False) -> str:
""""Create a Python module defining a MessageSender subclass.
This class is primarily for type checking and will contain overrides
for the varieties of send calls for message/response types defined
in the protocol.
Class names are based on basename; a basename 'FooSender' will
result in classes FooSender and BoundFooSender.
If 'private' is True, class-names will be prefixed with an '_'.
Note that line lengths are not clipped, so output may need to be
run through a formatter to prevent lint warnings about excessive
line lengths.
"""
def do_create_sender_module(self,
basename: str,
protocol_create_code: str,
private: bool = False) -> str:
"""Used by create_sender_module(); do not call directly."""
# pylint: disable=too-many-locals
import textwrap
ppre = '_' if private else ''
out = self._get_module_header('sender')
ccind = textwrap.indent(protocol_create_code, ' ')
out += (f'class {ppre}{basename}(MessageSender):\n'
f' """Protocol-specific sender."""\n'
f'\n'
f' def __init__(self) -> None:\n'
f'{ccind}\n'
f' super().__init__(protocol)\n'
f'\n'
f' def __get__(self,\n'
f' obj: Any,\n'
f' type_in: Any = None)'
@ -384,37 +388,28 @@ class MessageProtocol:
return out
def create_receiver_module(self,
basename: str,
is_async: bool,
private: bool = False) -> str:
""""Create a Python module defining a MessageReceiver subclass.
This class is primarily for type checking and will contain overrides
for the register method for message/response types defined in
the protocol.
Class names are based on basename; a basename 'FooReceiver' will
result in FooReceiver and BoundFooReceiver.
If 'is_async' is True, handle_raw_message() will be an async method
and the @handler decorator will expect async methods.
If 'private' is True, class-names will be prefixed with an '_'.
Note that line lengths are not clipped, so output may need to be
run through a formatter to prevent lint warnings about excessive
line lengths.
"""
def do_create_receiver_module(self,
basename: str,
protocol_create_code: str,
is_async: bool,
private: bool = False) -> str:
"""Used by create_receiver_module(); do not call directly."""
# pylint: disable=too-many-locals
import textwrap
desc = 'asynchronous' if is_async else 'synchronous'
ppre = '_' if private else ''
out = self._get_module_header('receiver')
ccind = textwrap.indent(protocol_create_code, ' ')
out += (f'class {ppre}{basename}(MessageReceiver):\n'
f' """Protocol-specific {desc} receiver."""\n'
f'\n'
f' is_async = {is_async}\n'
f'\n'
f' def __init__(self) -> None:\n'
f'{ccind}\n'
f' super().__init__(protocol)\n'
f'\n'
f' def __get__(\n'
f' self,\n'
f' obj: Any,\n'
@ -773,8 +768,8 @@ class MessageReceiver:
handler = self._handlers.get(msgtype)
if handler is None:
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
response = handler(bound_obj, msg_decoded)
return self._encode_response(response, msgtype)
result = handler(bound_obj, msg_decoded)
return self._encode_response(result, msgtype)
except Exception as exc:
return self.raw_response_for_error(exc)
@ -790,8 +785,8 @@ class MessageReceiver:
handler = self._handlers.get(msgtype)
if handler is None:
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
response = await handler(bound_obj, msg_decoded)
return self._encode_response(response, msgtype)
result = await handler(bound_obj, msg_decoded)
return self._encode_response(result, msgtype)
except Exception as exc:
return self.raw_response_for_error(exc)
@ -824,3 +819,84 @@ class BoundMessageReceiver:
related error.
"""
return self._receiver.raw_response_for_error(exc)
def create_sender_module(basename: str,
protocol_create_code: str,
private: bool = False) -> str:
"""Create a Python module defining a MessageSender subclass.
This class is primarily for type checking and will contain overrides
for the varieties of send calls for message/response types defined
in the protocol.
Code passed for 'protocol_create_code' should import necessary
modules and assign an instance of the Protocol to a 'protocol'
variable.
Class names are based on basename; a basename 'FooSender' will
result in classes FooSender and BoundFooSender.
If 'private' is True, class-names will be prefixed with an '_'.
Note that line lengths are not clipped, so output may need to be
run through a formatter to prevent lint warnings about excessive
line lengths.
"""
# Exec the passed code to get a protocol which we then use to
# generate module code. The user could simply call
# MessageProtocol.do_create_sender_module() directly, but this allows
# us to verify that the create code works and yields the protocol used
# to generate the code.
protocol = _protocol_from_code(protocol_create_code)
return protocol.do_create_sender_module(
basename=basename,
protocol_create_code=protocol_create_code,
private=private)
def create_receiver_module(basename: str,
protocol_create_code: str,
is_async: bool,
private: bool = False) -> str:
""""Create a Python module defining a MessageReceiver subclass.
This class is primarily for type checking and will contain overrides
for the register method for message/response types defined in
the protocol.
Class names are based on basename; a basename 'FooReceiver' will
result in FooReceiver and BoundFooReceiver.
If 'is_async' is True, handle_raw_message() will be an async method
and the @handler decorator will expect async methods.
If 'private' is True, class-names will be prefixed with an '_'.
Note that line lengths are not clipped, so output may need to be
run through a formatter to prevent lint warnings about excessive
line lengths.
"""
# Exec the passed code to get a protocol which we then use to
# generate module code. The user could simply call
# MessageProtocol.do_create_sender_module() directly, but this allows
# us to verify that the create code works and yields the protocol used
# to generate the code.
protocol = _protocol_from_code(protocol_create_code)
return protocol.do_create_receiver_module(
basename=basename,
protocol_create_code=protocol_create_code,
is_async=is_async,
private=private)
def _protocol_from_code(protocol_create_code: str) -> MessageProtocol:
env: Dict = {}
exec(protocol_create_code, env) # pylint: disable=exec-used
protocol = env.get('protocol')
if not isinstance(protocol, MessageProtocol):
raise RuntimeError(
f'protocol_create_code yielded'
f' a {type(protocol)}; expected a MessageProtocol instance.')
return protocol