Skip to content

Commit

Permalink
efro.message tidying and language updates
Browse files Browse the repository at this point in the history
  • Loading branch information
efroemling committed Sep 27, 2021
1 parent 638e428 commit f368cbf
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 43 deletions.
10 changes: 5 additions & 5 deletions .efrocachemap
Expand Up @@ -420,7 +420,7 @@
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/14/f1/4f2995d78fc20dd79dfb39c5d554",
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/57/ac/6ed0caecd25dc23688debed24c45",
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/32/08/38dac4a79ab2acee76a75d32a310",
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/1f/21/0f8b5de13f6bd5fb1e13564d7c58",
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/e4/48/6648ea4178c12d6e25ed3a53f628",
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/0f/0e/7184059414320d32104463e41038",
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/e2/58/c2c5964370df118c51528dc4bfa2",
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/28/96/397e5c164a595c2b6c2d3eb2d4f1",
Expand All @@ -436,11 +436,11 @@
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/0a/ec/f6665a696238275c806e7a0b1d0d",
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/ff/08/0d32d1babc60fdebd39def8b51da",
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/63/f0/cc8dd75a100f7d58000a361ca160",
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/cf/2b/23acc62ab35c4763a9cfe23495dc",
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/2d/e5/3737c6c3979cf381321c5472bea5",
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/87/e5/a10ddd73cfb7996bbd576032db6a",
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/dd/97/42d117db366ad4584eb8c58d191e",
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/26/8d/bf9cc8db2cc71b69e789898e1093",
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/d8/b7/9098f0cb25088d233541490e3e68",
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/2d/d5/661c050696d5a2e70e678054b9e7",
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/2e/17/fb3e7ed77fa54427b434b1791793",
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/fe/6d/751277bc6b704d4f2a54cf1a9cfa",
"assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/82/12/57bf144e12be229a9b70da9c45cb",
Expand All @@ -451,8 +451,8 @@
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/50/9f/be006ba19be6a69a57837eb6dca0",
"assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/dd/de/c197fa9aff42e4422bc66b95ad88",
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/65/e4/b9308f15437972209b4d3fce7abd",
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/c3/61/d5bcf2bcad50104b26d22d3365a4",
"assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/4b/da/7e444f86c768aee70779a0f7a28f",
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/8f/42/56f3ebcc6005f382449c1c2422fd",
"assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/4f/cc/285443e3e8e65a318df338bbc7f7",
"assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/8b/65/adfbe450da3f61677bd909fff707",
"assets/build/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/47/0a/a617cc85d927b576c4e6fc1091ed",
"assets/build/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/03/4b/57ee9b42854b26f23f81bd8c58ef",
Expand Down
2 changes: 2 additions & 0 deletions .idea/dictionaries/ericf.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ballisticacore-cmake/.idea/dictionaries/ericf.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/ba_module.md
@@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2021-09-24 for Ballistica version 1.6.5 build 20393</em></h4>
<h4><em>last updated on 2021-09-27 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>
Expand Down
4 changes: 2 additions & 2 deletions tests/test_efro/test_message.py
Expand Up @@ -6,7 +6,7 @@

import os
import asyncio
from typing import TYPE_CHECKING, overload
from typing import TYPE_CHECKING, overload, Union
from dataclasses import dataclass

import pytest
Expand All @@ -19,7 +19,7 @@
BoundMessageReceiver)

if TYPE_CHECKING:
from typing import List, Type, Any, Callable, Union, Optional, Awaitable
from typing import List, Type, Any, Callable, Optional, Awaitable


@ioprepped
Expand Down
104 changes: 73 additions & 31 deletions tools/efro/message.py
Expand Up @@ -75,6 +75,8 @@ class EmptyResponse(Response):

# TODO: could allow handlers to deal in raw values for these
# types similar to how we allow None in place of EmptyResponse.
# Though not sure if they are widely used enough to warrant the
# extra code complexity.
@ioprepped
@dataclass
class BoolResponse(Response):
Expand All @@ -83,6 +85,14 @@ class BoolResponse(Response):
value: Annotated[bool, IOAttrs('v')]


@ioprepped
@dataclass
class StringResponse(Response):
"""A simple string value response."""

value: Annotated[str, IOAttrs('v')]


class MessageProtocol:
"""Wrangles a set of message types, formats, and response types.
Both endpoints must be using a compatible Protocol for communication
Expand Down Expand Up @@ -154,7 +164,7 @@ def _reg_if_not(reg_tp: Type[Response], reg_id: int) -> None:

_reg_if_not(ErrorResponse, -1)
_reg_if_not(EmptyResponse, -2)
_reg_if_not(BoolResponse, -3)
# _reg_if_not(BoolResponse, -3)

# Some extra-thorough validation in debug mode.
if __debug__:
Expand All @@ -173,7 +183,8 @@ def _reg_if_not(reg_tp: Type[Response], reg_id: int) -> None:
assert issubclass(cls, Response)
if cls not in self.response_ids_by_type:
raise ValueError(f'Possible response type {cls}'
f' was not included in response_types.')
f' needs to be included in response_types'
f' for this protocol.')

# Make sure all registered types have unique base names.
# We can take advantage of this to generate cleaner looking
Expand Down Expand Up @@ -270,46 +281,78 @@ def _decode(self, data: str, types_by_id: Dict[int, Type],

def _get_module_header(self, part: str) -> str:
"""Return common parts of generated modules."""
# pylint: disable=too-many-locals, too-many-branches
import textwrap
tpimports: Dict[str, List[str]] = {}
imports: Dict[str, List[str]] = {}

single_message_type = len(self.message_ids_by_type) == 1

# Always import messages
for msgtype in list(self.message_ids_by_type) + [Message]:
imports.setdefault(msgtype.__module__, []).append(msgtype.__name__)
tpimports.setdefault(msgtype.__module__,
[]).append(msgtype.__name__)
for rsp_tp in list(self.response_ids_by_type) + [Response]:
# Skip these as they don't actually show up in code.
if rsp_tp is EmptyResponse or rsp_tp is ErrorResponse:
continue
imports.setdefault(rsp_tp.__module__, []).append(rsp_tp.__name__)
importlines2 = ''
if (single_message_type and part == 'sender'
and rsp_tp is not Response):
# We need to cast to the single supported response type
# in this case so need response types at runtime.
imports.setdefault(rsp_tp.__module__,
[]).append(rsp_tp.__name__)
else:
tpimports.setdefault(rsp_tp.__module__,
[]).append(rsp_tp.__name__)

import_lines = ''
tpimport_lines = ''

for module, names in sorted(imports.items()):
jnames = ', '.join(names)
line = f'from {module} import {jnames}'
if len(line) > 79:
# Recreate in a wrapping-friendly form.
line = f'from {module} import ({jnames})'
importlines2 += f' {line}\n'
import_lines += f'{line}\n'
for module, names in sorted(tpimports.items()):
jnames = ', '.join(names)
line = f'from {module} import {jnames}'
if len(line) > 75: # Account for indent
# Recreate in a wrapping-friendly form.
line = f'from {module} import ({jnames})'
tpimport_lines += f'{line}\n'

if part == 'sender':
importlines1 = (
'from efro.message import MessageSender, BoundMessageSender')
tpimportex = ''
import_lines += ('from efro.message import MessageSender,'
' BoundMessageSender')
tpimport_typing_extras = ''
else:
importlines1 = ('from efro.message import MessageReceiver,'
' BoundMessageReceiver')
tpimportex = ', Awaitable'
if single_message_type:
import_lines += ('from efro.message import (MessageReceiver,'
' BoundMessageReceiver, Message, Response)')
else:
import_lines += ('from efro.message import MessageReceiver,'
' BoundMessageReceiver')
tpimport_typing_extras = ', Awaitable'

ovld = ', overload' if not single_message_type else ''
tpimport_lines = textwrap.indent(tpimport_lines, ' ')
out = ('# Released under the MIT License. See LICENSE for details.\n'
f'#\n'
f'"""Auto-generated {part} module. Do not edit by hand."""\n'
f'\n'
f'from __future__ import annotations\n'
f'\n'
f'from typing import TYPE_CHECKING, overload\n'
f'from typing import TYPE_CHECKING{ovld}\n'
f'\n'
f'{importlines1}\n'
f'{import_lines}\n'
f'\n'
f'if TYPE_CHECKING:\n'
f' from typing import Union, Any, Optional, Callable'
f'{tpimportex}\n'
f'{importlines2}'
f'{tpimport_typing_extras}\n'
f'{tpimport_lines}'
f'\n'
f'\n')
return out
Expand All @@ -324,6 +367,8 @@ def do_create_sender_module(self,
# pylint: disable=too-many-locals
import textwrap

msgtypes = list(self.message_ids_by_type.keys())

ppre = '_' if private else ''
out = self._get_module_header('sender')
ccind = textwrap.indent(protocol_create_code, ' ')
Expand All @@ -345,16 +390,12 @@ def do_create_sender_module(self,
f'class {ppre}Bound{basename}(BoundMessageSender):\n'
f' """Protocol-specific bound sender."""\n')

# Define handler() overloads for all registered message types.
msgtypes = [
t for t in self.message_ids_by_type if issubclass(t, Message)
]

def _filt_tp_name(rtype: Type[Response]) -> str:
# We accept None to equal EmptyResponse so reflect that
# in the type annotation.
return 'None' if rtype is EmptyResponse else rtype.__name__

# Define handler() overloads for all registered message types.
if msgtypes:
for async_pass in False, True:
if async_pass and not enable_async_sends:
Expand Down Expand Up @@ -422,6 +463,7 @@ def do_create_receiver_module(self,

desc = 'asynchronous' if is_async else 'synchronous'
ppre = '_' if private else ''
msgtypes = list(self.message_ids_by_type.keys())
out = self._get_module_header('receiver')
ccind = textwrap.indent(protocol_create_code, ' ')
out += (f'class {ppre}{basename}(MessageReceiver):\n'
Expand All @@ -442,9 +484,6 @@ def do_create_receiver_module(self,
f'obj, self)\n')

# Define handler() overloads for all registered message types.
msgtypes = [
t for t in self.message_ids_by_type if issubclass(t, Message)
]

def _filt_tp_name(rtype: Type[Response]) -> str:
# We accept None to equal EmptyResponse so reflect that
Expand Down Expand Up @@ -539,7 +578,7 @@ class methods to handle raw message sending.
class MyClass:
msg = MyMessageSender(some_protocol)
@msg.sendmethod
@msg.send_method
def send_raw_message(self, message: str) -> str:
# Actually send the message here.
Expand Down Expand Up @@ -676,10 +715,9 @@ def register_handler(
The message type handled by the call is determined by its
type annotation.
"""
# pylint: disable=too-many-locals
# TODO: can use types.GenericAlias in 3.9.
from typing import _GenericAlias # type: ignore
from typing import Union, get_type_hints, get_args
from typing import get_type_hints, get_args

sig = inspect.getfullargspec(call)

Expand All @@ -700,7 +738,10 @@ def register_handler(
# Check annotation types to determine what message types we handle.
# Return-type annotation can be a Union, but we probably don't
# have it available at runtime. Explicitly pull it in.
anns = get_type_hints(call, localns={'Union': Union})
# UPDATE: we've updated our pylint filter to where we should
# have all annotations available.
# anns = get_type_hints(call, localns={'Union': Union})
anns = get_type_hints(call)

msgtype = anns.get('msg')
if not isinstance(msgtype, type):
Expand Down Expand Up @@ -758,11 +799,12 @@ def validate(self, warn_only: bool = False) -> None:
if issubclass(msgtype, Response):
continue
if msgtype not in self._handlers:
msg = (f'Protocol message {msgtype} not handled'
f' by receiver.')
msg = (f'Protocol message type {msgtype} is not handled'
f' by receiver type {type(self)}.')
if warn_only:
logging.warning(msg)
raise TypeError(msg)
else:
raise TypeError(msg)

def _decode_incoming_message(self,
msg: str) -> Tuple[Message, Type[Message]]:
Expand Down
11 changes: 7 additions & 4 deletions tools/efrotools/pylintplugins.py
Expand Up @@ -123,17 +123,20 @@ def func_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:

# Wipe out argument annotations.

# Special-case: functools.singledispatch and ba.dispatchmethod *do*
# Special-case: certain function decorators *do*
# evaluate annotations at runtime so we want to leave theirs intact.
# Lets just look for a @XXX.register decorator used by both I guess.
# This includes functools.singledispatch, ba.dispatchmethod, and
# efro.MessageReceiver.
# Lets just look for a @XXX.register or @XXX.handler decorators for
# now; can get more specific if we get false positives.
if node.decorators is not None:
for dnode in node.decorators.nodes:
if (isinstance(dnode, astroid.nodes.Name)
and dnode.name in ('dispatchmethod', 'singledispatch')):
and dnode.name in {'dispatchmethod', 'singledispatch'}):
return node # Leave annotations intact.

if (isinstance(dnode, astroid.nodes.Attribute)
and dnode.attrname == 'register'):
and dnode.attrname in {'register', 'handler'}):
return node # Leave annotations intact.

node.args.annotations = [None for _ in node.args.args]
Expand Down

0 comments on commit f368cbf

Please sign in to comment.