mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2026-08-15 13:04:30 +00:00
updating core files 1.7.10
This commit is contained in:
parent
daff57f1e9
commit
b110f8a12a
163 changed files with 4836 additions and 1960 deletions
9
dist/ba_data/python/efro/error.py
vendored
9
dist/ba_data/python/efro/error.py
vendored
|
|
@ -188,7 +188,14 @@ def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
|
|||
# Let's still complain, however, if we get any SSL errors besides
|
||||
# this one. https://bugs.python.org/issue39951
|
||||
if isinstance(exc, ssl.SSLError):
|
||||
if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in str(exc):
|
||||
excstr = str(exc)
|
||||
if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
|
||||
return True
|
||||
|
||||
# Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
|
||||
# Assuming this just means client is attempting to connect from some
|
||||
# outdated browser or whatnot.
|
||||
if 'SSL: WRONG_VERSION_NUMBER' in excstr:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
|||
438
dist/ba_data/python/efro/log.py
vendored
Normal file
438
dist/ba_data/python/efro/log.py
vendored
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Logging functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
from threading import Thread, current_thread, Lock
|
||||
|
||||
from efro.util import utc_now
|
||||
from efro.call import tpartial
|
||||
from efro.terminal import TerminalColor
|
||||
from efro.dataclassio import ioprepped, IOAttrs, dataclass_to_json
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, TextIO
|
||||
|
||||
|
||||
class LogLevel(Enum):
|
||||
"""Severity level for a log entry.
|
||||
|
||||
These enums have numeric values so they can be compared in severity.
|
||||
Note that these values are not currently interchangeable with the
|
||||
logging.ERROR, logging.DEBUG, etc. values.
|
||||
"""
|
||||
DEBUG = 0
|
||||
INFO = 1
|
||||
WARNING = 2
|
||||
ERROR = 3
|
||||
CRITICAL = 4
|
||||
|
||||
|
||||
LEVELNO_LOG_LEVELS = {
|
||||
logging.DEBUG: LogLevel.DEBUG,
|
||||
logging.INFO: LogLevel.INFO,
|
||||
logging.WARNING: LogLevel.WARNING,
|
||||
logging.ERROR: LogLevel.ERROR,
|
||||
logging.CRITICAL: LogLevel.CRITICAL
|
||||
}
|
||||
|
||||
LEVELNO_COLOR_CODES: dict[int, tuple[str, str]] = {
|
||||
logging.DEBUG: (TerminalColor.CYAN.value, TerminalColor.RESET.value),
|
||||
logging.INFO: ('', ''),
|
||||
logging.WARNING: (TerminalColor.YELLOW.value, TerminalColor.RESET.value),
|
||||
logging.ERROR: (TerminalColor.RED.value, TerminalColor.RESET.value),
|
||||
logging.CRITICAL:
|
||||
(TerminalColor.STRONG_MAGENTA.value + TerminalColor.BOLD.value +
|
||||
TerminalColor.BG_BLACK.value, TerminalColor.RESET.value),
|
||||
}
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class LogEntry:
|
||||
"""Single logged message."""
|
||||
name: Annotated[str,
|
||||
IOAttrs('n', soft_default='root', store_default=False)]
|
||||
message: Annotated[str, IOAttrs('m')]
|
||||
level: Annotated[LogLevel, IOAttrs('l')]
|
||||
time: Annotated[datetime.datetime, IOAttrs('t')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class LogArchive:
|
||||
"""Info and data for a log."""
|
||||
|
||||
# Total number of entries submitted to the log.
|
||||
log_size: Annotated[int, IOAttrs('t')]
|
||||
|
||||
# Offset for the entries contained here.
|
||||
# (10 means our first entry is the 10th in the log, etc.)
|
||||
start_index: Annotated[int, IOAttrs('c')]
|
||||
|
||||
entries: Annotated[list[LogEntry], IOAttrs('e')]
|
||||
|
||||
|
||||
class LogHandler(logging.Handler):
|
||||
"""Fancy-pants handler for logging output.
|
||||
|
||||
Writes logs to disk in structured json format and echoes them
|
||||
to stdout/stderr with pretty colors.
|
||||
"""
|
||||
|
||||
_event_loop: asyncio.AbstractEventLoop
|
||||
|
||||
# IMPORTANT: Any debug prints we do here should ONLY go to echofile.
|
||||
# Otherwise we can get infinite loops as those prints come back to us
|
||||
# as new log entries.
|
||||
|
||||
def __init__(self,
|
||||
path: str | Path | None,
|
||||
echofile: TextIO | None,
|
||||
suppress_non_root_debug: bool = False,
|
||||
cache_size_limit: int = 0):
|
||||
super().__init__()
|
||||
# pylint: disable=consider-using-with
|
||||
self._file = (None
|
||||
if path is None else open(path, 'w', encoding='utf-8'))
|
||||
self._echofile = echofile
|
||||
self._callbacks_lock = Lock()
|
||||
self._callbacks: list[Callable[[LogEntry], None]] = []
|
||||
self._suppress_non_root_debug = suppress_non_root_debug
|
||||
self._file_chunks: dict[str, list[str]] = {'stdout': [], 'stderr': []}
|
||||
self._file_chunk_ship_task: dict[str, asyncio.Task | None] = {
|
||||
'stdout': None,
|
||||
'stderr': None
|
||||
}
|
||||
self._cache_size = 0
|
||||
assert cache_size_limit >= 0
|
||||
self._cache_size_limit = cache_size_limit
|
||||
self._cache: list[tuple[int, LogEntry]] = []
|
||||
self._cache_index_offset = 0
|
||||
self._cache_lock = Lock()
|
||||
self._printed_callback_error = False
|
||||
self._thread_bootstrapped = False
|
||||
self._thread = Thread(target=self._thread_main, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
# Spin until our thread is up and running; otherwise we could
|
||||
# wind up trying to push stuff to our event loop before the
|
||||
# loop exists.
|
||||
while not self._thread_bootstrapped:
|
||||
time.sleep(0.001)
|
||||
|
||||
def add_callback(self, call: Callable[[LogEntry], None]) -> None:
|
||||
"""Add a callback to be run for each LogEntry.
|
||||
|
||||
Note that this callback will always run in a background thread.
|
||||
"""
|
||||
with self._callbacks_lock:
|
||||
self._callbacks.append(call)
|
||||
|
||||
def _thread_main(self) -> None:
|
||||
self._event_loop = asyncio.new_event_loop()
|
||||
# NOTE: if we ever use default threadpool at all we should allow
|
||||
# setting it for our loop.
|
||||
asyncio.set_event_loop(self._event_loop)
|
||||
self._thread_bootstrapped = True
|
||||
try:
|
||||
self._event_loop.run_forever()
|
||||
except BaseException:
|
||||
# If this ever goes down we're in trouble.
|
||||
# We won't be able to log about it though...
|
||||
# Try to make some noise however we can.
|
||||
print('LogHandler died!!!', file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
def get_cached(self,
|
||||
start_index: int = 0,
|
||||
max_entries: int | None = None) -> LogArchive:
|
||||
"""Build and return an archive of cached log entries.
|
||||
|
||||
This will only include entries that have been processed by the
|
||||
background thread, so may not include just-submitted logs or
|
||||
entries for partially written stdout/stderr lines.
|
||||
Entries from the range [start_index:start_index+max_entries]
|
||||
which are still present in the cache will be returned.
|
||||
"""
|
||||
|
||||
assert start_index >= 0
|
||||
if max_entries is not None:
|
||||
assert max_entries >= 0
|
||||
with self._cache_lock:
|
||||
# Transform start_index to our present cache space.
|
||||
start_index -= self._cache_index_offset
|
||||
# Calc end-index in our present cache space.
|
||||
end_index = (len(self._cache)
|
||||
if max_entries is None else start_index + max_entries)
|
||||
|
||||
# Clamp both indexes to both ends of our present space.
|
||||
start_index = max(0, min(start_index, len(self._cache)))
|
||||
end_index = max(0, min(end_index, len(self._cache)))
|
||||
|
||||
return LogArchive(
|
||||
log_size=self._cache_index_offset + len(self._cache),
|
||||
start_index=start_index + self._cache_index_offset,
|
||||
entries=[e[1] for e in self._cache[start_index:end_index]])
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
# Called by logging to send us records.
|
||||
# We simply package them up and ship them to our thread.
|
||||
# UPDATE: turns out we CAN get log messages from this thread
|
||||
# (the C++ layer can spit out some performance metrics when
|
||||
# calls take too long/etc.)
|
||||
# assert current_thread() is not self._thread
|
||||
|
||||
# Special case - filter out this common extra-chatty category.
|
||||
# TODO - should use a standard logging.Filter for this.
|
||||
if (self._suppress_non_root_debug and record.name != 'root'
|
||||
and record.levelname == 'DEBUG'):
|
||||
return
|
||||
|
||||
# We want to forward as much as we can along without processing it
|
||||
# (better to do so in a bg thread).
|
||||
# However its probably best to flatten the message string here since
|
||||
# it could cause problems stringifying things in threads where they
|
||||
# didn't expect to be stringified.
|
||||
msg = self.format(record)
|
||||
|
||||
# Also immediately print pretty colored output to our echo file
|
||||
# (generally stderr). We do this part here instead of in our bg
|
||||
# thread because the delay can throw off command line prompts or
|
||||
# make tight debugging harder.
|
||||
if self._echofile is not None:
|
||||
ends = LEVELNO_COLOR_CODES.get(record.levelno)
|
||||
if ends is not None:
|
||||
self._echofile.write(f'{ends[0]}{msg}{ends[1]}\n')
|
||||
else:
|
||||
self._echofile.write(f'{msg}\n')
|
||||
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(self._emit_in_thread, record.name, record.levelno,
|
||||
record.created, msg))
|
||||
|
||||
def _emit_in_thread(self, name: str, levelno: int, created: float,
|
||||
message: str) -> None:
|
||||
try:
|
||||
self._emit_entry(
|
||||
LogEntry(name=name,
|
||||
message=message,
|
||||
level=LEVELNO_LOG_LEVELS.get(levelno, LogLevel.INFO),
|
||||
time=datetime.datetime.fromtimestamp(
|
||||
created, datetime.timezone.utc)))
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc(file=self._echofile)
|
||||
|
||||
def file_write(self, name: str, output: str) -> None:
|
||||
"""Send raw stdout/stderr output to the logger to be collated."""
|
||||
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(self._file_write_in_thread, name, output))
|
||||
|
||||
def _file_write_in_thread(self, name: str, output: str) -> None:
|
||||
try:
|
||||
assert name in ('stdout', 'stderr')
|
||||
|
||||
# Here we try to be somewhat smart about breaking arbitrary
|
||||
# print output into discrete log entries.
|
||||
|
||||
self._file_chunks[name].append(output)
|
||||
|
||||
# Individual parts of a print come across as separate writes,
|
||||
# and the end of a print will be a standalone '\n' by default.
|
||||
# Let's use that as a hint that we're likely at the end of
|
||||
# a full print statement and ship what we've got.
|
||||
if output == '\n':
|
||||
self._ship_file_chunks(name, cancel_ship_task=True)
|
||||
else:
|
||||
# By default just keep adding chunks.
|
||||
# However we keep a timer running anytime we've got
|
||||
# unshipped chunks so that we can ship what we've got
|
||||
# after a short bit if we never get a newline.
|
||||
ship_task = self._file_chunk_ship_task[name]
|
||||
if ship_task is None:
|
||||
self._file_chunk_ship_task[name] = (
|
||||
self._event_loop.create_task(
|
||||
self._ship_chunks_task(name)))
|
||||
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc(file=self._echofile)
|
||||
|
||||
def file_flush(self, name: str) -> None:
|
||||
"""Send raw stdout/stderr flush to the logger to be collated."""
|
||||
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(self._file_flush_in_thread, name))
|
||||
|
||||
def _file_flush_in_thread(self, name: str) -> None:
|
||||
try:
|
||||
assert name in ('stdout', 'stderr')
|
||||
|
||||
# Immediately ship whatever chunks we've got.
|
||||
if self._file_chunks[name]:
|
||||
self._ship_file_chunks(name, cancel_ship_task=True)
|
||||
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc(file=self._echofile)
|
||||
|
||||
async def _ship_chunks_task(self, name: str) -> None:
|
||||
await asyncio.sleep(0.1)
|
||||
self._ship_file_chunks(name, cancel_ship_task=False)
|
||||
|
||||
def _ship_file_chunks(self, name: str, cancel_ship_task: bool) -> None:
|
||||
# Note: Raw print input generally ends in a newline, but that is
|
||||
# redundant when we break things into log entries and results
|
||||
# in extra empty lines. So strip off a single trailing newline.
|
||||
text = ''.join(self._file_chunks[name]).removesuffix('\n')
|
||||
|
||||
self._emit_entry(
|
||||
LogEntry(name=name,
|
||||
message=text,
|
||||
level=LogLevel.INFO,
|
||||
time=utc_now()))
|
||||
self._file_chunks[name] = []
|
||||
ship_task = self._file_chunk_ship_task[name]
|
||||
if cancel_ship_task and ship_task is not None:
|
||||
ship_task.cancel()
|
||||
self._file_chunk_ship_task[name] = None
|
||||
|
||||
def _emit_entry(self, entry: LogEntry) -> None:
|
||||
assert current_thread() is self._thread
|
||||
|
||||
# Store to our cache.
|
||||
if self._cache_size_limit > 0:
|
||||
with self._cache_lock:
|
||||
# Do a rough calc of how many bytes this entry consumes.
|
||||
entry_size = sum(
|
||||
sys.getsizeof(x)
|
||||
for x in (entry, entry.name, entry.message, entry.level,
|
||||
entry.time))
|
||||
self._cache.append((entry_size, entry))
|
||||
self._cache_size += entry_size
|
||||
|
||||
# Prune old until we are back at or under our limit.
|
||||
while self._cache_size > self._cache_size_limit:
|
||||
popped = self._cache.pop(0)
|
||||
self._cache_size -= popped[0]
|
||||
self._cache_index_offset += 1
|
||||
|
||||
# Pass to callbacks.
|
||||
with self._callbacks_lock:
|
||||
for call in self._callbacks:
|
||||
try:
|
||||
call(entry)
|
||||
except Exception:
|
||||
# Only print one callback error to avoid insanity.
|
||||
if not self._printed_callback_error:
|
||||
import traceback
|
||||
traceback.print_exc(file=self._echofile)
|
||||
self._printed_callback_error = True
|
||||
|
||||
# Dump to our structured log file.
|
||||
# TODO: set a timer for flushing; don't flush every line.
|
||||
if self._file is not None:
|
||||
entry_s = dataclass_to_json(entry)
|
||||
assert '\n' not in entry_s # Make sure its a single line.
|
||||
print(entry_s, file=self._file, flush=True)
|
||||
|
||||
|
||||
class FileLogEcho:
|
||||
"""A file-like object for forwarding stdout/stderr to a LogHandler."""
|
||||
|
||||
def __init__(self, original: TextIO, name: str,
|
||||
handler: LogHandler) -> None:
|
||||
assert name in ('stdout', 'stderr')
|
||||
self._original = original
|
||||
self._name = name
|
||||
self._handler = handler
|
||||
|
||||
def write(self, output: Any) -> None:
|
||||
"""Override standard write call."""
|
||||
self._original.write(output)
|
||||
self._handler.file_write(self._name, output)
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush the file."""
|
||||
self._original.flush()
|
||||
|
||||
# We also use this as a hint to ship whatever file chunks
|
||||
# we've accumulated (we have to try and be smart about breaking
|
||||
# our arbitrary file output into discrete entries).
|
||||
self._handler.file_flush(self._name)
|
||||
|
||||
def isatty(self) -> bool:
|
||||
"""Are we a terminal?"""
|
||||
return self._original.isatty()
|
||||
|
||||
|
||||
def setup_logging(log_path: str | Path | None,
|
||||
level: LogLevel,
|
||||
suppress_non_root_debug: bool = False,
|
||||
log_stdout_stderr: bool = False,
|
||||
cache_size_limit: int = 0) -> LogHandler:
|
||||
"""Set up our logging environment.
|
||||
|
||||
Returns the custom handler which can be used to fetch information
|
||||
about logs that have passed through it. (worst log-levels, caches, etc.).
|
||||
"""
|
||||
|
||||
lmap = {
|
||||
LogLevel.DEBUG: logging.DEBUG,
|
||||
LogLevel.INFO: logging.INFO,
|
||||
LogLevel.WARNING: logging.WARNING,
|
||||
LogLevel.ERROR: logging.ERROR,
|
||||
LogLevel.CRITICAL: logging.CRITICAL,
|
||||
}
|
||||
|
||||
# Wire logger output to go to a structured log file.
|
||||
# Also echo it to stderr IF we're running in a terminal.
|
||||
# UPDATE: Actually gonna always go to stderr. Is there a
|
||||
# reason we shouldn't? This makes debugging possible if all
|
||||
# we have is access to a non-interactive terminal or file dump.
|
||||
# We could add a '--quiet' arg or whatnot to change this behavior.
|
||||
|
||||
# Note: by passing in the *original* stderr here before we
|
||||
# (potentially) replace it, we ensure that our log echos
|
||||
# won't themselves be intercepted and sent to the logger
|
||||
# which would create an infinite loop.
|
||||
loghandler = LogHandler(
|
||||
log_path,
|
||||
# echofile=sys.stderr if sys.stderr.isatty() else None,
|
||||
echofile=sys.stderr,
|
||||
suppress_non_root_debug=suppress_non_root_debug,
|
||||
cache_size_limit=cache_size_limit)
|
||||
|
||||
# Note: going ahead with force=True here so that we replace any
|
||||
# existing logger. Though we warn if it looks like we are doing
|
||||
# that so we can try to avoid creating the first one.
|
||||
had_previous_handlers = bool(logging.root.handlers)
|
||||
logging.basicConfig(level=lmap[level],
|
||||
format='%(message)s',
|
||||
handlers=[loghandler],
|
||||
force=True)
|
||||
if had_previous_handlers:
|
||||
logging.warning('setup_logging: force-replacing previous handlers.')
|
||||
|
||||
# Optionally intercept Python's stdout/stderr output and generate
|
||||
# log entries from it.
|
||||
if log_stdout_stderr:
|
||||
sys.stdout = FileLogEcho( # type: ignore
|
||||
sys.stdout, 'stdout', loghandler)
|
||||
sys.stderr = FileLogEcho( # type: ignore
|
||||
sys.stderr, 'stderr', loghandler)
|
||||
|
||||
return loghandler
|
||||
14
dist/ba_data/python/efro/message/__init__.py
vendored
14
dist/ba_data/python/efro/message/__init__.py
vendored
|
|
@ -11,15 +11,17 @@ from efro.message._protocol import MessageProtocol
|
|||
from efro.message._sender import (MessageSender, BoundMessageSender)
|
||||
from efro.message._receiver import (MessageReceiver, BoundMessageReceiver)
|
||||
from efro.message._module import (create_sender_module, create_receiver_module)
|
||||
from efro.message._message import (Message, Response, EmptyResponse,
|
||||
ErrorResponse, StringResponse, BoolResponse,
|
||||
from efro.message._message import (Message, Response, SysResponse,
|
||||
EmptySysResponse, ErrorSysResponse,
|
||||
StringResponse, BoolResponse,
|
||||
UnregisteredMessageIDError)
|
||||
|
||||
__all__ = [
|
||||
'Message', 'Response', 'EmptyResponse', 'ErrorResponse', 'StringResponse',
|
||||
'BoolResponse', 'MessageProtocol', 'MessageSender', 'BoundMessageSender',
|
||||
'MessageReceiver', 'BoundMessageReceiver', 'create_sender_module',
|
||||
'create_receiver_module', 'UnregisteredMessageIDError'
|
||||
'Message', 'Response', 'SysResponse', 'EmptySysResponse',
|
||||
'ErrorSysResponse', 'StringResponse', 'BoolResponse', 'MessageProtocol',
|
||||
'MessageSender', 'BoundMessageSender', 'MessageReceiver',
|
||||
'BoundMessageReceiver', 'create_sender_module', 'create_receiver_module',
|
||||
'UnregisteredMessageIDError'
|
||||
]
|
||||
|
||||
# Have these things present themselves cleanly as 'thismodule.SomeClass'
|
||||
|
|
|
|||
38
dist/ba_data/python/efro/message/_message.py
vendored
38
dist/ba_data/python/efro/message/_message.py
vendored
|
|
@ -24,52 +24,56 @@ class Message:
|
|||
"""Base class for messages."""
|
||||
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response]]:
|
||||
"""Return all message types this Message can result in when sent.
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
"""Return all Response types this Message can return when sent.
|
||||
|
||||
The default implementation specifies EmptyResponse, so messages with
|
||||
no particular response needs can leave this untouched.
|
||||
Note that ErrorMessage is handled as a special case and does not
|
||||
need to be specified here.
|
||||
The default implementation specifies a None return type.
|
||||
"""
|
||||
return [EmptyResponse]
|
||||
return [None]
|
||||
|
||||
|
||||
class Response:
|
||||
"""Base class for responses to messages."""
|
||||
|
||||
|
||||
class SysResponse:
|
||||
"""Base class for system-responses to messages.
|
||||
|
||||
These are only sent/handled by the messaging system itself;
|
||||
users of the api never see them.
|
||||
"""
|
||||
|
||||
|
||||
# Some standard response types:
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ErrorResponse(Response):
|
||||
"""Message saying some error has occurred on the other end.
|
||||
class ErrorSysResponse(SysResponse):
|
||||
"""SysResponse saying some error has occurred for the send.
|
||||
|
||||
This type is unique in that it is not returned to the user; it
|
||||
instead results in a local exception being raised.
|
||||
This generally results in an Exception being raised for the caller.
|
||||
"""
|
||||
|
||||
class ErrorType(Enum):
|
||||
"""Type of error that occurred in remote message handling."""
|
||||
OTHER = 0
|
||||
CLEAN = 1
|
||||
"""Type of error that occurred while sending a message."""
|
||||
REMOTE = 0
|
||||
REMOTE_CLEAN = 1
|
||||
LOCAL = 2
|
||||
COMMUNICATION = 3
|
||||
|
||||
error_message: Annotated[str, IOAttrs('m')]
|
||||
error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.OTHER
|
||||
error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.REMOTE
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class EmptyResponse(Response):
|
||||
class EmptySysResponse(SysResponse):
|
||||
"""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.
|
||||
# types similar to how we allow None in place of EmptySysResponse.
|
||||
# Though not sure if they are widely used enough to warrant the
|
||||
# extra code complexity.
|
||||
@ioprepped
|
||||
|
|
|
|||
99
dist/ba_data/python/efro/message/_protocol.py
vendored
99
dist/ba_data/python/efro/message/_protocol.py
vendored
|
|
@ -14,8 +14,9 @@ import json
|
|||
from efro.error import CleanError
|
||||
from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict,
|
||||
dataclass_from_dict)
|
||||
from efro.message._message import (Message, Response, ErrorResponse,
|
||||
EmptyResponse, UnregisteredMessageIDError)
|
||||
from efro.message._message import (Message, Response, SysResponse,
|
||||
ErrorSysResponse, EmptySysResponse,
|
||||
UnregisteredMessageIDError)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Literal
|
||||
|
|
@ -33,37 +34,30 @@ class MessageProtocol:
|
|||
def __init__(self,
|
||||
message_types: dict[int, type[Message]],
|
||||
response_types: dict[int, type[Response]],
|
||||
preserve_clean_errors: bool = True,
|
||||
receiver_logs_exceptions: bool = True,
|
||||
receiver_returns_stack_traces: bool = False) -> None:
|
||||
forward_clean_errors: bool = False,
|
||||
remote_errors_include_stack_traces: bool = False) -> None:
|
||||
"""Create a protocol with a given configuration.
|
||||
|
||||
Note that common response types are automatically registered
|
||||
with (unchanging negative ids) so they don't need to be passed
|
||||
explicitly (but can be if a different id is desired).
|
||||
|
||||
If 'preserve_clean_errors' is True, efro.error.CleanError
|
||||
If 'forward_clean_errors' is True, efro.error.CleanError
|
||||
exceptions raised on the receiver end will result in a matching
|
||||
CleanError raised back on the sender. All other Exception types
|
||||
come across as efro.error.RemoteError.
|
||||
|
||||
When 'receiver_logs_exceptions' is True, any uncaught Exceptions
|
||||
on the receiver end will be logged there via logging.exception()
|
||||
(in addition to the usual behavior of returning an ErrorResponse
|
||||
to the sender). This is good to leave enabled if your
|
||||
intention is to never return ErrorResponses. Looser setups
|
||||
making routine use of CleanErrors or whatnot may want to
|
||||
disable this, however.
|
||||
|
||||
If 'receiver_returns_stack_traces' is True, stringified stack
|
||||
If 'remote_errors_include_stack_traces' is True, stringified stack
|
||||
traces will be returned to the sender for exceptions occurring
|
||||
on the receiver end. This can make debugging easier but should
|
||||
only be used when the client is trusted to see such info.
|
||||
"""
|
||||
self.message_types_by_id: dict[int, type[Message]] = {}
|
||||
self.message_ids_by_type: dict[type[Message], int] = {}
|
||||
self.response_types_by_id: dict[int, type[Response]] = {}
|
||||
self.response_ids_by_type: dict[type[Response], int] = {}
|
||||
self.response_types_by_id: dict[int, type[Response]
|
||||
| type[SysResponse]] = {}
|
||||
self.response_ids_by_type: dict[type[Response] | type[SysResponse],
|
||||
int] = {}
|
||||
for m_id, m_type in message_types.items():
|
||||
|
||||
# Make sure only valid message types were passed and each
|
||||
|
|
@ -85,32 +79,33 @@ class MessageProtocol:
|
|||
self.response_types_by_id[r_id] = r_type
|
||||
self.response_ids_by_type[r_type] = r_id
|
||||
|
||||
# Go ahead and auto-register a few common response types
|
||||
# if the user has not done so explicitly. Use unique negative
|
||||
# IDs which will never change or overlap with user ids.
|
||||
def _reg_if_not(reg_tp: type[Response], reg_id: int) -> None:
|
||||
if reg_tp in self.response_ids_by_type:
|
||||
return
|
||||
# Register our SysResponse types. These use negative
|
||||
# IDs so as to never overlap with user Response types.
|
||||
def _reg_sys(reg_tp: type[SysResponse], reg_id: int) -> None:
|
||||
assert self.response_types_by_id.get(reg_id) is None
|
||||
self.response_types_by_id[reg_id] = reg_tp
|
||||
self.response_ids_by_type[reg_tp] = reg_id
|
||||
|
||||
_reg_if_not(ErrorResponse, -1)
|
||||
_reg_if_not(EmptyResponse, -2)
|
||||
_reg_sys(ErrorSysResponse, -1)
|
||||
_reg_sys(EmptySysResponse, -2)
|
||||
|
||||
# Some extra-thorough validation in debug mode.
|
||||
if __debug__:
|
||||
# Make sure all Message types' return types are valid
|
||||
# and have been assigned an ID as well.
|
||||
all_response_types: set[type[Response]] = set()
|
||||
all_response_types: set[type[Response] | None] = set()
|
||||
for m_id, m_type in message_types.items():
|
||||
m_rtypes = m_type.get_response_types()
|
||||
|
||||
assert isinstance(m_rtypes, list)
|
||||
assert m_rtypes, (
|
||||
f'Message type {m_type} specifies no return types.')
|
||||
assert len(set(m_rtypes)) == len(m_rtypes) # check dups
|
||||
all_response_types.update(m_rtypes)
|
||||
for m_rtype in m_rtypes:
|
||||
all_response_types.add(m_rtype)
|
||||
for cls in all_response_types:
|
||||
if cls is None:
|
||||
continue
|
||||
assert is_ioprepped_dataclass(cls)
|
||||
assert issubclass(cls, Response)
|
||||
if cls not in self.response_ids_by_type:
|
||||
|
|
@ -127,9 +122,9 @@ class MessageProtocol:
|
|||
'message_types contains duplicate __name__s;'
|
||||
' all types are required to have unique names.')
|
||||
|
||||
self.preserve_clean_errors = preserve_clean_errors
|
||||
self.receiver_logs_exceptions = receiver_logs_exceptions
|
||||
self.receiver_returns_stack_traces = receiver_returns_stack_traces
|
||||
self.forward_clean_errors = forward_clean_errors
|
||||
self.remote_errors_include_stack_traces = (
|
||||
remote_errors_include_stack_traces)
|
||||
|
||||
@staticmethod
|
||||
def encode_dict(obj: dict) -> str:
|
||||
|
|
@ -140,26 +135,27 @@ class MessageProtocol:
|
|||
"""Encode a message to a json ready dict."""
|
||||
return self._to_dict(message, self.message_ids_by_type, 'message')
|
||||
|
||||
def response_to_dict(self, response: Response) -> dict:
|
||||
def response_to_dict(self, response: Response | SysResponse) -> dict:
|
||||
"""Encode a response to a json ready dict."""
|
||||
return self._to_dict(response, self.response_ids_by_type, 'response')
|
||||
|
||||
def error_to_response(self, exc: Exception) -> Response:
|
||||
def error_to_response(self, exc: Exception) -> SysResponse:
|
||||
"""Translate an error to a response."""
|
||||
|
||||
# Log any errors we got during handling if so desired.
|
||||
if self.receiver_logs_exceptions:
|
||||
logging.exception('Error handling message.')
|
||||
# Log any errors we got during handling.
|
||||
logging.exception('Error in efro.message handling.')
|
||||
|
||||
# If anything goes wrong, return a ErrorResponse instead.
|
||||
if isinstance(exc, CleanError) and self.preserve_clean_errors:
|
||||
return ErrorResponse(error_message=str(exc),
|
||||
error_type=ErrorResponse.ErrorType.CLEAN)
|
||||
return ErrorResponse(
|
||||
# If anything goes wrong, return a ErrorSysResponse instead.
|
||||
# (either CLEAN or generic REMOTE)
|
||||
if isinstance(exc, CleanError) and self.forward_clean_errors:
|
||||
return ErrorSysResponse(
|
||||
error_message=str(exc),
|
||||
error_type=ErrorSysResponse.ErrorType.REMOTE_CLEAN)
|
||||
return ErrorSysResponse(
|
||||
error_message=(traceback.format_exc()
|
||||
if self.receiver_returns_stack_traces else
|
||||
if self.remote_errors_include_stack_traces else
|
||||
'An internal error has occurred.'),
|
||||
error_type=ErrorResponse.ErrorType.OTHER)
|
||||
error_type=ErrorSysResponse.ErrorType.REMOTE)
|
||||
|
||||
def _to_dict(self, message: Any, ids_by_type: dict[type, int],
|
||||
opname: str) -> dict:
|
||||
|
|
@ -185,10 +181,10 @@ class MessageProtocol:
|
|||
assert isinstance(out, Message)
|
||||
return out
|
||||
|
||||
def response_from_dict(self, data: dict) -> Response:
|
||||
def response_from_dict(self, data: dict) -> Response | SysResponse:
|
||||
"""Decode a response from a json string."""
|
||||
out = self._from_dict(data, self.response_types_by_id, 'response')
|
||||
assert isinstance(out, Response)
|
||||
assert isinstance(out, Response | SysResponse)
|
||||
return out
|
||||
|
||||
# Weeeird; we get mypy errors returning dict[int, type] but
|
||||
|
|
@ -234,7 +230,7 @@ class MessageProtocol:
|
|||
rsptypes.append(Response)
|
||||
for rsp_tp in rsptypes:
|
||||
# Skip these as they don't actually show up in code.
|
||||
if rsp_tp is EmptyResponse or rsp_tp is ErrorResponse:
|
||||
if rsp_tp is EmptySysResponse or rsp_tp is ErrorSysResponse:
|
||||
continue
|
||||
if (single_message_type and part == 'sender'
|
||||
and rsp_tp is not Response):
|
||||
|
|
@ -341,10 +337,8 @@ class MessageProtocol:
|
|||
f'class {ppre}Bound{basename}(BoundMessageSender):\n'
|
||||
f' """Protocol-specific bound sender."""\n')
|
||||
|
||||
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__
|
||||
def _filt_tp_name(rtype: type[Response] | None) -> str:
|
||||
return 'None' if rtype is None else rtype.__name__
|
||||
|
||||
# Define handler() overloads for all registered message types.
|
||||
if msgtypes:
|
||||
|
|
@ -381,6 +375,7 @@ class MessageProtocol:
|
|||
|
||||
for msgtype in msgtypes:
|
||||
msgtypevar = msgtype.__name__
|
||||
# rtypes = msgtype.get_response_types()
|
||||
rtypes = msgtype.get_response_types()
|
||||
if len(rtypes) > 1:
|
||||
rtypevar = ' | '.join(
|
||||
|
|
@ -438,10 +433,8 @@ class MessageProtocol:
|
|||
|
||||
# Define handler() overloads for all registered message types.
|
||||
|
||||
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__
|
||||
def _filt_tp_name(rtype: type[Response] | None) -> str:
|
||||
return 'None' if rtype is None else rtype.__name__
|
||||
|
||||
if msgtypes:
|
||||
cbgn = 'Awaitable[' if is_async else ''
|
||||
|
|
|
|||
53
dist/ba_data/python/efro/message/_receiver.py
vendored
53
dist/ba_data/python/efro/message/_receiver.py
vendored
|
|
@ -11,13 +11,14 @@ import inspect
|
|||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.message._message import (Message, Response, EmptyResponse,
|
||||
ErrorResponse, UnregisteredMessageIDError)
|
||||
from efro.message._message import (Message, Response, EmptySysResponse,
|
||||
UnregisteredMessageIDError)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable, Awaitable
|
||||
|
||||
from efro.message._protocol import MessageProtocol
|
||||
from efro.message._message import SysResponse
|
||||
|
||||
|
||||
class MessageReceiver:
|
||||
|
|
@ -53,7 +54,8 @@ class MessageReceiver:
|
|||
self._decode_filter_call: Callable[[Any, dict, Message],
|
||||
None] | None = None
|
||||
self._encode_filter_call: Callable[
|
||||
[Any, Message | None, Response, dict], None] | None = None
|
||||
[Any, Message | None, Response | SysResponse, dict],
|
||||
None] | None = None
|
||||
|
||||
# TODO: don't currently have async encode equivalent
|
||||
# or either for sender; can add as needed.
|
||||
|
|
@ -106,26 +108,27 @@ class MessageReceiver:
|
|||
assert issubclass(msgtype, Message)
|
||||
|
||||
ret = anns.get('return')
|
||||
responsetypes: tuple[type[Any] | type[None], ...]
|
||||
responsetypes: tuple[type[Any] | None, ...]
|
||||
|
||||
# Return types can be a single type or a union of types.
|
||||
if isinstance(ret, (_GenericAlias, types.UnionType)):
|
||||
targs = get_args(ret)
|
||||
if not all(isinstance(a, type) for a in targs):
|
||||
if not all(isinstance(a, (type, type(None))) for a in targs):
|
||||
raise TypeError(f'expected only types for "return" annotation;'
|
||||
f' got {targs}.')
|
||||
responsetypes = targs
|
||||
else:
|
||||
if not isinstance(ret, type):
|
||||
if not isinstance(ret, (type, type(None))):
|
||||
raise TypeError(f'expected one or more types for'
|
||||
f' "return" annotation; got a {type(ret)}.')
|
||||
# This seems like maybe a mypy bug. Appeared after adding
|
||||
# types.UnionType above.
|
||||
responsetypes = (ret, ) # type: ignore
|
||||
responsetypes = (ret, )
|
||||
|
||||
# Return type of None translates to EmptyResponse.
|
||||
responsetypes = tuple(EmptyResponse if r is type(None) else r
|
||||
for r in responsetypes) # noqa
|
||||
# This will contain NoneType for empty return cases, but
|
||||
# we expect it to be None.
|
||||
responsetypes = tuple(None if r is type(None) else r
|
||||
for r in responsetypes)
|
||||
|
||||
# Make sure our protocol has this message type registered and our
|
||||
# return types exactly match. (Technically we could return a subset
|
||||
|
|
@ -178,7 +181,9 @@ class MessageReceiver:
|
|||
return call
|
||||
|
||||
def encode_filter_method(
|
||||
self, call: Callable[[Any, Message | None, Response, dict], None]
|
||||
self,
|
||||
call: Callable[[Any, Message | None, Response | SysResponse, dict],
|
||||
None]
|
||||
) -> Callable[[Any, Message | None, Response, dict], None]:
|
||||
"""Function decorator for defining an encode filter.
|
||||
|
||||
|
|
@ -236,17 +241,21 @@ class MessageReceiver:
|
|||
response: Response | None) -> str:
|
||||
"""Encode a response provided by the user for sending."""
|
||||
|
||||
# A return value of None equals EmptyResponse.
|
||||
if response is None:
|
||||
response = EmptyResponse()
|
||||
|
||||
assert isinstance(response, Response)
|
||||
assert isinstance(response, Response | None)
|
||||
# (user should never explicitly return error-responses)
|
||||
assert not isinstance(response, ErrorResponse)
|
||||
assert type(response) in message.get_response_types()
|
||||
response_dict = self.protocol.response_to_dict(response)
|
||||
assert (response is None
|
||||
or type(response) in message.get_response_types())
|
||||
|
||||
# A return value of None equals EmptySysResponse.
|
||||
out_response: Response | SysResponse
|
||||
if response is None:
|
||||
out_response = EmptySysResponse()
|
||||
else:
|
||||
out_response = response
|
||||
|
||||
response_dict = self.protocol.response_to_dict(out_response)
|
||||
if self._encode_filter_call is not None:
|
||||
self._encode_filter_call(bound_obj, message, response,
|
||||
self._encode_filter_call(bound_obj, message, out_response,
|
||||
response_dict)
|
||||
return self.protocol.encode_dict(response_dict)
|
||||
|
||||
|
|
@ -280,7 +289,7 @@ class MessageReceiver:
|
|||
if handler is None:
|
||||
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
||||
response = handler(bound_obj, msg_decoded)
|
||||
assert isinstance(response, (Response, type(None)))
|
||||
assert isinstance(response, Response | None)
|
||||
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||
|
||||
except Exception as exc:
|
||||
|
|
@ -308,7 +317,7 @@ class MessageReceiver:
|
|||
if handler is None:
|
||||
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
||||
response = await handler(bound_obj, msg_decoded)
|
||||
assert isinstance(response, (Response, type(None)))
|
||||
assert isinstance(response, Response | None)
|
||||
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||
|
||||
except Exception as exc:
|
||||
|
|
|
|||
96
dist/ba_data/python/efro/message/_sender.py
vendored
96
dist/ba_data/python/efro/message/_sender.py
vendored
|
|
@ -7,19 +7,17 @@ Supports static typing for message types and possible return types.
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.error import CleanError, RemoteError, CommunicationError
|
||||
from efro.message._message import EmptyResponse, ErrorResponse
|
||||
from efro.message._message import EmptySysResponse, ErrorSysResponse, Response
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable, Awaitable
|
||||
|
||||
from efro.message._message import Message, Response
|
||||
from efro.message._message import Message, SysResponse
|
||||
from efro.message._protocol import MessageProtocol
|
||||
|
||||
TM = TypeVar('TM', bound='MessageSender')
|
||||
|
||||
|
||||
class MessageSender:
|
||||
"""Facilitates sending messages to a target and receiving responses.
|
||||
|
|
@ -48,8 +46,8 @@ class MessageSender:
|
|||
[Any, str], Awaitable[str]] | None = None
|
||||
self._encode_filter_call: Callable[[Any, Message, dict],
|
||||
None] | None = None
|
||||
self._decode_filter_call: Callable[[Any, Message, dict, Response],
|
||||
None] | None = None
|
||||
self._decode_filter_call: Callable[
|
||||
[Any, Message, dict, Response | SysResponse], None] | None = None
|
||||
|
||||
def send_method(
|
||||
self, call: Callable[[Any, str],
|
||||
|
|
@ -57,8 +55,9 @@ class MessageSender:
|
|||
"""Function decorator for setting raw send method.
|
||||
|
||||
Send methods take strings and should return strings.
|
||||
Any Exception raised during the send_method manifests as
|
||||
a CommunicationError for the message sender.
|
||||
CommunicationErrors raised here will be returned to the sender
|
||||
as such; all other exceptions will result in a RuntimeError for
|
||||
the sender.
|
||||
"""
|
||||
assert self._send_raw_message_call is None
|
||||
self._send_raw_message_call = call
|
||||
|
|
@ -70,8 +69,9 @@ class MessageSender:
|
|||
"""Function decorator for setting raw send-async method.
|
||||
|
||||
Send methods take strings and should return strings.
|
||||
Any Exception raised during the send_method manifests as
|
||||
a CommunicationError for the message sender.
|
||||
CommunicationErrors raised here will be returned to the sender
|
||||
as such; all other exceptions will result in a RuntimeError for
|
||||
the sender.
|
||||
"""
|
||||
assert self._send_async_raw_message_call is None
|
||||
self._send_async_raw_message_call = call
|
||||
|
|
@ -90,7 +90,8 @@ class MessageSender:
|
|||
return call
|
||||
|
||||
def decode_filter_method(
|
||||
self, call: Callable[[Any, Message, dict, Response], None]
|
||||
self, call: Callable[[Any, Message, dict, Response | SysResponse],
|
||||
None]
|
||||
) -> Callable[[Any, Message, dict, Response], None]:
|
||||
"""Function decorator for defining a decode filter.
|
||||
|
||||
|
|
@ -122,7 +123,8 @@ class MessageSender:
|
|||
),
|
||||
)
|
||||
|
||||
def send_split_part_1(self, bound_obj: Any, message: Message) -> Response:
|
||||
def send_split_part_1(self, bound_obj: Any,
|
||||
message: Message) -> Response | SysResponse:
|
||||
"""Send a message synchronously.
|
||||
|
||||
Generally you can just call send(); these split versions are
|
||||
|
|
@ -139,15 +141,16 @@ class MessageSender:
|
|||
except Exception as exc:
|
||||
# Any error in the raw send call gets recorded as either
|
||||
# a local or communication error.
|
||||
return ErrorResponse(
|
||||
error_message=f'Error in send async method: {exc}',
|
||||
error_type=(ErrorResponse.ErrorType.COMMUNICATION
|
||||
return ErrorSysResponse(
|
||||
error_message=
|
||||
f'Error in MessageSender @send_method ({type(exc)}): {exc}',
|
||||
error_type=(ErrorSysResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError) else
|
||||
ErrorResponse.ErrorType.LOCAL))
|
||||
ErrorSysResponse.ErrorType.LOCAL))
|
||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||
|
||||
async def send_split_part_1_async(self, bound_obj: Any,
|
||||
message: Message) -> Response:
|
||||
async def send_split_part_1_async(
|
||||
self, bound_obj: Any, message: Message) -> Response | SysResponse:
|
||||
"""Send a message asynchronously.
|
||||
|
||||
Generally you can just call send(); these split versions are
|
||||
|
|
@ -165,15 +168,18 @@ class MessageSender:
|
|||
except Exception as exc:
|
||||
# Any error in the raw send call gets recorded as either
|
||||
# a local or communication error.
|
||||
return ErrorResponse(
|
||||
error_message=f'Error in send async method: {exc}',
|
||||
error_type=(ErrorResponse.ErrorType.COMMUNICATION
|
||||
return ErrorSysResponse(
|
||||
error_message=
|
||||
f'Error in MessageSender @send_async_method ({type(exc)}):'
|
||||
f' {exc}',
|
||||
error_type=(ErrorSysResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError) else
|
||||
ErrorResponse.ErrorType.LOCAL))
|
||||
ErrorSysResponse.ErrorType.LOCAL))
|
||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||
|
||||
def send_split_part_2(self, message: Message,
|
||||
raw_response: Response) -> Response | None:
|
||||
def send_split_part_2(
|
||||
self, message: Message,
|
||||
raw_response: Response | SysResponse) -> Response | None:
|
||||
"""Complete message sending (both sync and async).
|
||||
|
||||
Generally you can just call send(); these split versions are
|
||||
|
|
@ -193,7 +199,7 @@ class MessageSender:
|
|||
return self.protocol.encode_dict(msg_dict)
|
||||
|
||||
def _decode_raw_response(self, bound_obj: Any, message: Message,
|
||||
response_encoded: str) -> Response:
|
||||
response_encoded: str) -> Response | SysResponse:
|
||||
"""Create a Response from returned data.
|
||||
|
||||
These Responses may encapsulate things like remote errors and
|
||||
|
|
@ -201,6 +207,7 @@ class MessageSender:
|
|||
should be used to translate to special values like None or raise
|
||||
Exceptions. This function itself should never raise Exceptions.
|
||||
"""
|
||||
response: Response | SysResponse
|
||||
try:
|
||||
response_dict = self.protocol.decode_dict(response_encoded)
|
||||
response = self.protocol.response_from_dict(response_dict)
|
||||
|
|
@ -211,16 +218,17 @@ class MessageSender:
|
|||
# If we got to this point, we successfully communicated
|
||||
# with the other end so errors represent protocol mismatches
|
||||
# or other invalid data. For now let's just log it but perhaps
|
||||
# we'd want to somehow embed it in the ErrorResponse to be
|
||||
# we'd want to somehow embed it in the ErrorSysResponse to be
|
||||
# available directly to the user later.
|
||||
logging.exception('Error decoding raw response')
|
||||
response = ErrorResponse(
|
||||
response = ErrorSysResponse(
|
||||
error_message=
|
||||
'Error decoding raw response; see log for details.',
|
||||
error_type=ErrorResponse.ErrorType.LOCAL)
|
||||
error_type=ErrorSysResponse.ErrorType.LOCAL)
|
||||
return response
|
||||
|
||||
def _unpack_raw_response(self, raw_response: Response) -> Response | None:
|
||||
def _unpack_raw_response(
|
||||
self, raw_response: Response | SysResponse) -> Response | None:
|
||||
"""Given a raw Response, unpacks to special values or Exceptions.
|
||||
|
||||
The result of this call is what should be passed to users.
|
||||
|
|
@ -229,30 +237,31 @@ class MessageSender:
|
|||
run such that any raised Exception is active when the callback
|
||||
fires; not on the thread where the message was sent.
|
||||
"""
|
||||
# EmptyResponse translates to None
|
||||
if isinstance(raw_response, EmptyResponse):
|
||||
# EmptySysResponse translates to None
|
||||
if isinstance(raw_response, EmptySysResponse):
|
||||
return None
|
||||
|
||||
# Some error occurred. Raise a local Exception for it.
|
||||
if isinstance(raw_response, ErrorResponse):
|
||||
if isinstance(raw_response, ErrorSysResponse):
|
||||
|
||||
if (raw_response.error_type is
|
||||
ErrorResponse.ErrorType.COMMUNICATION):
|
||||
ErrorSysResponse.ErrorType.COMMUNICATION):
|
||||
raise CommunicationError(raw_response.error_message)
|
||||
|
||||
# If something went wrong on our end of the connection,
|
||||
# If something went wrong on *our* end of the connection,
|
||||
# don't say it was a remote error.
|
||||
if raw_response.error_type is ErrorResponse.ErrorType.LOCAL:
|
||||
if raw_response.error_type is ErrorSysResponse.ErrorType.LOCAL:
|
||||
raise RuntimeError(raw_response.error_message)
|
||||
|
||||
# If they want to support clean errors, do those.
|
||||
if (self.protocol.preserve_clean_errors and
|
||||
raw_response.error_type is ErrorResponse.ErrorType.CLEAN):
|
||||
if (self.protocol.forward_clean_errors and raw_response.error_type
|
||||
is ErrorSysResponse.ErrorType.REMOTE_CLEAN):
|
||||
raise CleanError(raw_response.error_message)
|
||||
|
||||
# In all other cases, just say something went wrong 'out there'.
|
||||
# Everything else gets lumped in as a remote error.
|
||||
raise RemoteError(raw_response.error_message)
|
||||
|
||||
assert isinstance(raw_response, Response)
|
||||
return raw_response
|
||||
|
||||
|
||||
|
|
@ -289,15 +298,16 @@ class BoundMessageSender:
|
|||
return await self._sender.send_async(bound_obj=self._obj,
|
||||
message=message)
|
||||
|
||||
async def send_split_part_1_async_untyped(self,
|
||||
message: Message) -> Response:
|
||||
async def send_split_part_1_async_untyped(
|
||||
self, message: Message) -> Response | SysResponse:
|
||||
"""Split send (part 1 of 2)."""
|
||||
assert self._obj is not None
|
||||
return await self._sender.send_split_part_1_async(bound_obj=self._obj,
|
||||
message=message)
|
||||
|
||||
def send_split_part_2_untyped(self, message: Message,
|
||||
raw_response: Response) -> Response | None:
|
||||
def send_split_part_2_untyped(
|
||||
self, message: Message,
|
||||
raw_response: Response | SysResponse) -> Response | None:
|
||||
"""Split send (part 2 of 2)."""
|
||||
return self._sender.send_split_part_2(message=message,
|
||||
raw_response=raw_response)
|
||||
|
|
|
|||
7
dist/ba_data/python/efro/rpc.py
vendored
7
dist/ba_data/python/efro/rpc.py
vendored
|
|
@ -441,8 +441,9 @@ class RPCEndpoint:
|
|||
weakref.ref(
|
||||
asyncio.create_task(
|
||||
self._handle_raw_message(message_id=msgid, message=msg))))
|
||||
self._debug_print_call(
|
||||
f'{self._label}: done handling message at {self._tm()}.')
|
||||
if self._debug_print:
|
||||
self._debug_print_call(
|
||||
f'{self._label}: done handling message at {self._tm()}.')
|
||||
|
||||
async def _handle_response_packet(self, big: bool) -> None:
|
||||
assert self._peer_info is not None
|
||||
|
|
@ -558,7 +559,7 @@ class RPCEndpoint:
|
|||
# If that doesn't happen, make a fuss so we know to fix it.
|
||||
# The other end will simply never get a response to this
|
||||
# message.
|
||||
logging.exception('Error handling message')
|
||||
logging.exception('Error handling raw rpc message')
|
||||
return
|
||||
|
||||
assert self._peer_info is not None
|
||||
|
|
|
|||
80
dist/ba_data/python/efro/util.py
vendored
80
dist/ba_data/python/efro/util.py
vendored
|
|
@ -27,11 +27,11 @@ if TYPE_CHECKING:
|
|||
from typing import Any, Callable, NoReturn
|
||||
|
||||
T = TypeVar('T')
|
||||
TVAL = TypeVar('TVAL')
|
||||
TARG = TypeVar('TARG')
|
||||
TSELF = TypeVar('TSELF')
|
||||
TRET = TypeVar('TRET')
|
||||
TENUM = TypeVar('TENUM', bound=Enum)
|
||||
ValT = TypeVar('ValT')
|
||||
ArgT = TypeVar('ArgT')
|
||||
SelfT = TypeVar('SelfT')
|
||||
RetT = TypeVar('RetT')
|
||||
EnumT = TypeVar('EnumT', bound=Enum)
|
||||
|
||||
|
||||
class _EmptyObj:
|
||||
|
|
@ -44,7 +44,7 @@ else:
|
|||
Call = functools.partial
|
||||
|
||||
|
||||
def enum_by_value(cls: type[TENUM], value: Any) -> TENUM:
|
||||
def enum_by_value(cls: type[EnumT], value: Any) -> EnumT:
|
||||
"""Create an enum from a value.
|
||||
|
||||
This is basically the same as doing 'obj = EnumType(value)' except
|
||||
|
|
@ -251,15 +251,15 @@ class DirtyBit:
|
|||
return False
|
||||
|
||||
|
||||
class DispatchMethodWrapper(Generic[TARG, TRET]):
|
||||
class DispatchMethodWrapper(Generic[ArgT, RetT]):
|
||||
"""Type-aware standin for the dispatch func returned by dispatchmethod."""
|
||||
|
||||
def __call__(self, arg: TARG) -> TRET:
|
||||
def __call__(self, arg: ArgT) -> RetT:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def register(
|
||||
func: Callable[[Any, Any], TRET]) -> Callable[[Any, Any], TRET]:
|
||||
func: Callable[[Any, Any], RetT]) -> Callable[[Any, Any], RetT]:
|
||||
"""Register a new dispatch handler for this dispatch-method."""
|
||||
|
||||
registry: dict[Any, Callable]
|
||||
|
|
@ -267,8 +267,8 @@ class DispatchMethodWrapper(Generic[TARG, TRET]):
|
|||
|
||||
# noinspection PyProtectedMember,PyTypeHints
|
||||
def dispatchmethod(
|
||||
func: Callable[[Any, TARG],
|
||||
TRET]) -> DispatchMethodWrapper[TARG, TRET]:
|
||||
func: Callable[[Any, ArgT],
|
||||
RetT]) -> DispatchMethodWrapper[ArgT, RetT]:
|
||||
"""A variation of functools.singledispatch for methods.
|
||||
|
||||
Note: as of Python 3.9 there is now functools.singledispatchmethod,
|
||||
|
|
@ -307,7 +307,7 @@ def dispatchmethod(
|
|||
return cast(DispatchMethodWrapper, wrapper)
|
||||
|
||||
|
||||
def valuedispatch(call: Callable[[TVAL], TRET]) -> ValueDispatcher[TVAL, TRET]:
|
||||
def valuedispatch(call: Callable[[ValT], RetT]) -> ValueDispatcher[ValT, RetT]:
|
||||
"""Decorator for functions to allow dispatching based on a value.
|
||||
|
||||
This differs from functools.singledispatch in that it dispatches based
|
||||
|
|
@ -318,21 +318,21 @@ def valuedispatch(call: Callable[[TVAL], TRET]) -> ValueDispatcher[TVAL, TRET]:
|
|||
return ValueDispatcher(call)
|
||||
|
||||
|
||||
class ValueDispatcher(Generic[TVAL, TRET]):
|
||||
class ValueDispatcher(Generic[ValT, RetT]):
|
||||
"""Used by the valuedispatch decorator"""
|
||||
|
||||
def __init__(self, call: Callable[[TVAL], TRET]) -> None:
|
||||
def __init__(self, call: Callable[[ValT], RetT]) -> None:
|
||||
self._base_call = call
|
||||
self._handlers: dict[TVAL, Callable[[], TRET]] = {}
|
||||
self._handlers: dict[ValT, Callable[[], RetT]] = {}
|
||||
|
||||
def __call__(self, value: TVAL) -> TRET:
|
||||
def __call__(self, value: ValT) -> RetT:
|
||||
handler = self._handlers.get(value)
|
||||
if handler is not None:
|
||||
return handler()
|
||||
return self._base_call(value)
|
||||
|
||||
def _add_handler(self, value: TVAL,
|
||||
call: Callable[[], TRET]) -> Callable[[], TRET]:
|
||||
def _add_handler(self, value: ValT,
|
||||
call: Callable[[], RetT]) -> Callable[[], RetT]:
|
||||
if value in self._handlers:
|
||||
raise RuntimeError(f'Duplicate handlers added for {value}')
|
||||
self._handlers[value] = call
|
||||
|
|
@ -340,42 +340,42 @@ class ValueDispatcher(Generic[TVAL, TRET]):
|
|||
|
||||
def register(
|
||||
self,
|
||||
value: TVAL) -> Callable[[Callable[[], TRET]], Callable[[], TRET]]:
|
||||
value: ValT) -> Callable[[Callable[[], RetT]], Callable[[], RetT]]:
|
||||
"""Add a handler to the dispatcher."""
|
||||
from functools import partial
|
||||
return partial(self._add_handler, value)
|
||||
|
||||
|
||||
def valuedispatch1arg(
|
||||
call: Callable[[TVAL, TARG],
|
||||
TRET]) -> ValueDispatcher1Arg[TVAL, TARG, TRET]:
|
||||
call: Callable[[ValT, ArgT],
|
||||
RetT]) -> ValueDispatcher1Arg[ValT, ArgT, RetT]:
|
||||
"""Like valuedispatch but for functions taking an extra argument."""
|
||||
return ValueDispatcher1Arg(call)
|
||||
|
||||
|
||||
class ValueDispatcher1Arg(Generic[TVAL, TARG, TRET]):
|
||||
class ValueDispatcher1Arg(Generic[ValT, ArgT, RetT]):
|
||||
"""Used by the valuedispatch1arg decorator"""
|
||||
|
||||
def __init__(self, call: Callable[[TVAL, TARG], TRET]) -> None:
|
||||
def __init__(self, call: Callable[[ValT, ArgT], RetT]) -> None:
|
||||
self._base_call = call
|
||||
self._handlers: dict[TVAL, Callable[[TARG], TRET]] = {}
|
||||
self._handlers: dict[ValT, Callable[[ArgT], RetT]] = {}
|
||||
|
||||
def __call__(self, value: TVAL, arg: TARG) -> TRET:
|
||||
def __call__(self, value: ValT, arg: ArgT) -> RetT:
|
||||
handler = self._handlers.get(value)
|
||||
if handler is not None:
|
||||
return handler(arg)
|
||||
return self._base_call(value, arg)
|
||||
|
||||
def _add_handler(self, value: TVAL,
|
||||
call: Callable[[TARG], TRET]) -> Callable[[TARG], TRET]:
|
||||
def _add_handler(self, value: ValT,
|
||||
call: Callable[[ArgT], RetT]) -> Callable[[ArgT], RetT]:
|
||||
if value in self._handlers:
|
||||
raise RuntimeError(f'Duplicate handlers added for {value}')
|
||||
self._handlers[value] = call
|
||||
return call
|
||||
|
||||
def register(
|
||||
self, value: TVAL
|
||||
) -> Callable[[Callable[[TARG], TRET]], Callable[[TARG], TRET]]:
|
||||
self, value: ValT
|
||||
) -> Callable[[Callable[[ArgT], RetT]], Callable[[ArgT], RetT]]:
|
||||
"""Add a handler to the dispatcher."""
|
||||
from functools import partial
|
||||
return partial(self._add_handler, value)
|
||||
|
|
@ -383,22 +383,22 @@ class ValueDispatcher1Arg(Generic[TVAL, TARG, TRET]):
|
|||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class ValueDispatcherMethod(Generic[TVAL, TRET]):
|
||||
class ValueDispatcherMethod(Generic[ValT, RetT]):
|
||||
"""Used by the valuedispatchmethod decorator."""
|
||||
|
||||
def __call__(self, value: TVAL) -> TRET:
|
||||
def __call__(self, value: ValT) -> RetT:
|
||||
...
|
||||
|
||||
def register(
|
||||
self, value: TVAL
|
||||
) -> Callable[[Callable[[TSELF], TRET]], Callable[[TSELF], TRET]]:
|
||||
self, value: ValT
|
||||
) -> Callable[[Callable[[SelfT], RetT]], Callable[[SelfT], RetT]]:
|
||||
"""Add a handler to the dispatcher."""
|
||||
...
|
||||
|
||||
|
||||
def valuedispatchmethod(
|
||||
call: Callable[[TSELF, TVAL],
|
||||
TRET]) -> ValueDispatcherMethod[TVAL, TRET]:
|
||||
call: Callable[[SelfT, ValT],
|
||||
RetT]) -> ValueDispatcherMethod[ValT, RetT]:
|
||||
"""Like valuedispatch but works with methods instead of functions."""
|
||||
|
||||
# NOTE: It seems that to wrap a method with a decorator and have self
|
||||
|
|
@ -407,18 +407,18 @@ def valuedispatchmethod(
|
|||
# in the function call dict and simply return a call.
|
||||
|
||||
_base_call = call
|
||||
_handlers: dict[TVAL, Callable[[TSELF], TRET]] = {}
|
||||
_handlers: dict[ValT, Callable[[SelfT], RetT]] = {}
|
||||
|
||||
def _add_handler(value: TVAL, addcall: Callable[[TSELF], TRET]) -> None:
|
||||
def _add_handler(value: ValT, addcall: Callable[[SelfT], RetT]) -> None:
|
||||
if value in _handlers:
|
||||
raise RuntimeError(f'Duplicate handlers added for {value}')
|
||||
_handlers[value] = addcall
|
||||
|
||||
def _register(value: TVAL) -> Callable[[Callable[[TSELF], TRET]], None]:
|
||||
def _register(value: ValT) -> Callable[[Callable[[SelfT], RetT]], None]:
|
||||
from functools import partial
|
||||
return partial(_add_handler, value)
|
||||
|
||||
def _call_wrapper(self: TSELF, value: TVAL) -> TRET:
|
||||
def _call_wrapper(self: SelfT, value: ValT) -> RetT:
|
||||
handler = _handlers.get(value)
|
||||
if handler is not None:
|
||||
return handler(self)
|
||||
|
|
@ -433,7 +433,7 @@ def valuedispatchmethod(
|
|||
# In reality we just return a raw function call (for reasons listed above).
|
||||
# pylint: disable=undefined-variable, no-else-return
|
||||
if TYPE_CHECKING:
|
||||
return ValueDispatcherMethod[TVAL, TRET]()
|
||||
return ValueDispatcherMethod[ValT, RetT]()
|
||||
else:
|
||||
return _call_wrapper
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue