mirror of
https://github.com/meshcore-dev/meshcore_py.git
synced 2026-06-11 11:56:18 +00:00
G2: F22 — add Event.is_error() helper and document .type-check contract
Why: wait_for_event matches a single EventType; when callers pass
[X, ERROR] to send() or wait_for_events, the return value may be an
error response whose payload is {"reason": "..."} — not the command-
specific keys the caller expects. Without a documented contract and
a convenience helper, every call site independently forgets to check
.type before accessing payload keys, leading to KeyError (F21/M01,
M04) or silent fallthrough. The is_error() helper and docstrings on
send()/wait_for_events() establish the contract that subsequent
commits in this branch rely on.
Refs: Forensics report finding F22
This commit is contained in:
@@ -90,6 +90,14 @@ class CommandHandlerBase:
|
|||||||
expected_events: Optional[Union[EventType, List[EventType]]] = None,
|
expected_events: Optional[Union[EventType, List[EventType]]] = None,
|
||||||
timeout: Optional[float] = None,
|
timeout: Optional[float] = None,
|
||||||
) -> Event:
|
) -> Event:
|
||||||
|
"""Wait for the first of *expected_events* to arrive.
|
||||||
|
|
||||||
|
Returns the first matched ``Event``. When ``EventType.ERROR`` is
|
||||||
|
among the expected types, the caller **must** check
|
||||||
|
``result.is_error()`` before accessing command-specific payload
|
||||||
|
keys — an ERROR payload is ``{"reason": "..."}`` and will
|
||||||
|
``KeyError`` on any other key.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Convert single event to list if needed
|
# Convert single event to list if needed
|
||||||
if not isinstance(expected_events, list):
|
if not isinstance(expected_events, list):
|
||||||
@@ -129,9 +137,6 @@ class CommandHandlerBase:
|
|||||||
logger.debug(f"Command error: {e}")
|
logger.debug(f"Command error: {e}")
|
||||||
return Event(EventType.ERROR, {"error": str(e)})
|
return Event(EventType.ERROR, {"error": str(e)})
|
||||||
|
|
||||||
return Event(EventType.ERROR, {})
|
|
||||||
|
|
||||||
|
|
||||||
async def send(
|
async def send(
|
||||||
self,
|
self,
|
||||||
data: bytes,
|
data: bytes,
|
||||||
@@ -151,7 +156,14 @@ class CommandHandlerBase:
|
|||||||
timeout: Timeout in seconds, or None to use default_timeout
|
timeout: Timeout in seconds, or None to use default_timeout
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Event: The full event object that was received in response to the command
|
Event: The full event object that was received in response to
|
||||||
|
the command.
|
||||||
|
|
||||||
|
Important:
|
||||||
|
When ``EventType.ERROR`` is included in *expected_events*, the
|
||||||
|
returned event may be an error response. Callers **must**
|
||||||
|
check ``result.is_error()`` before accessing command-specific
|
||||||
|
payload keys to avoid ``KeyError``.
|
||||||
"""
|
"""
|
||||||
if not self.dispatcher:
|
if not self.dispatcher:
|
||||||
raise RuntimeError("Dispatcher not set, cannot send commands")
|
raise RuntimeError("Dispatcher not set, cannot send commands")
|
||||||
@@ -266,6 +278,7 @@ class CommandHandlerBase:
|
|||||||
contact = self._get_contact_by_prefix(dst_bytes.hex()) # need a contact for return path
|
contact = self._get_contact_by_prefix(dst_bytes.hex()) # need a contact for return path
|
||||||
if contact is None:
|
if contact is None:
|
||||||
logger.error("No contact found")
|
logger.error("No contact found")
|
||||||
|
return Event(EventType.ERROR, {"reason": "contact_not_found"})
|
||||||
|
|
||||||
zero_hop = False
|
zero_hop = False
|
||||||
if contact["out_path_len"] == -1:
|
if contact["out_path_len"] == -1:
|
||||||
|
|||||||
@@ -104,6 +104,17 @@ class Event:
|
|||||||
if kwargs:
|
if kwargs:
|
||||||
self.attributes.update(kwargs)
|
self.attributes.update(kwargs)
|
||||||
|
|
||||||
|
def is_error(self) -> bool:
|
||||||
|
"""Return True if this event represents an error response.
|
||||||
|
|
||||||
|
Callers that include ``EventType.ERROR`` in their expected-events
|
||||||
|
list **must** check ``result.is_error()`` (or ``result.type ==
|
||||||
|
EventType.ERROR``) before accessing keyed payload fields, because
|
||||||
|
an ERROR payload contains ``{"reason": "..."}`` — not the
|
||||||
|
command-specific keys the caller expects on the happy path.
|
||||||
|
"""
|
||||||
|
return self.type == EventType.ERROR
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
"""
|
"""
|
||||||
Create a copy of the event.
|
Create a copy of the event.
|
||||||
|
|||||||
Reference in New Issue
Block a user