mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(telegram): instrument getUpdates request via subclass re-tag, not slotted attr
PTB's HTTPXRequest/BaseRequest use __slots__. On Python 3.13 their instances no longer carry a __dict__, so the getUpdates progress instrumentation's `request.do_request = wrapper` monkey-patch raises `AttributeError: 'HTTPXRequest' object attribute 'do_request' is read-only`, failing every Telegram connect (#64482). It only worked on Python 3.12, where the instance still had a __dict__ — the Docker image ships Python 3.13, so the release broke Telegram outright. Re-tag the request to a thin `__slots__ = ()` subclass that overrides do_request instead of mutating the instance. This preserves the exact progress-observation semantics, works on both 3.12 and 3.13, and covers the real request and the test doubles alike.
This commit is contained in:
parent
080daa3f42
commit
f5f79ff1b4
1 changed files with 53 additions and 25 deletions
|
|
@ -1969,34 +1969,62 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
self._polling_conflict_count = 0
|
||||
self._send_path_degraded = False
|
||||
|
||||
def _observe_polling_request_result(self, request, generation, result):
|
||||
"""Record getUpdates progress from an observed do_request result.
|
||||
|
||||
Purely observational: PTB still parses the untouched payload and owns
|
||||
any resulting exception. Kept as its own method so the observation
|
||||
logic is shared and independently testable.
|
||||
"""
|
||||
status_code, payload = result
|
||||
if generation is None or not (200 <= status_code < 300):
|
||||
return
|
||||
try:
|
||||
# Use the request's own parser so health observation agrees
|
||||
# exactly with PTB's authoritative response handling (e.g.
|
||||
# UTF-8 replacement decoding and BOM rejection).
|
||||
envelope = request.parse_json_payload(payload)
|
||||
except Exception:
|
||||
return
|
||||
if (
|
||||
isinstance(envelope, dict)
|
||||
and envelope.get("ok") is True
|
||||
and "result" in envelope
|
||||
):
|
||||
self._record_polling_progress(generation)
|
||||
|
||||
def _instrument_polling_request(self, request):
|
||||
"""Wrap one dedicated PTB getUpdates request with progress tracking."""
|
||||
do_request = request.do_request
|
||||
"""Instrument one dedicated PTB getUpdates request with progress tracking.
|
||||
|
||||
async def _do_request(*args, **kwargs):
|
||||
generation = _POLLING_GENERATION_CONTEXT.get()
|
||||
result = await do_request(*args, **kwargs)
|
||||
status_code, payload = result
|
||||
if generation is not None and 200 <= status_code < 300:
|
||||
try:
|
||||
# Use the request's own parser so health observation agrees
|
||||
# exactly with PTB's authoritative response handling (e.g.
|
||||
# UTF-8 replacement decoding and BOM rejection).
|
||||
envelope = request.parse_json_payload(payload)
|
||||
except Exception:
|
||||
# Instrumentation is observational: PTB still parses the
|
||||
# untouched payload and owns the resulting exception.
|
||||
pass
|
||||
else:
|
||||
if (
|
||||
isinstance(envelope, dict)
|
||||
and envelope.get("ok") is True
|
||||
and "result" in envelope
|
||||
):
|
||||
self._record_polling_progress(generation)
|
||||
return result
|
||||
PTB's request classes (``BaseRequest`` / ``HTTPXRequest``) use
|
||||
``__slots__``. On Python 3.13 their instances no longer carry a
|
||||
``__dict__`` (the ``AbstractAsyncContextManager`` MRO stopped yielding
|
||||
one), so ``request.do_request = wrapper`` raises
|
||||
``AttributeError: 'HTTPXRequest' object attribute 'do_request' is
|
||||
read-only`` and the whole Telegram connect fails (#64482). It only
|
||||
appeared to work on Python 3.12, where those instances still had a
|
||||
``__dict__``.
|
||||
|
||||
request.do_request = _do_request
|
||||
Instead of monkey-patching the instance, re-tag it to a thin subclass
|
||||
that overrides ``do_request``. This is portable across Python versions
|
||||
and works for both the real request and the test doubles. The subclass
|
||||
declares ``__slots__ = ()`` so its instance layout stays identical to
|
||||
the base, which is what makes the ``__class__`` swap legal on a slotted
|
||||
instance.
|
||||
"""
|
||||
adapter = self
|
||||
base_cls = type(request)
|
||||
|
||||
class _InstrumentedPollingRequest(base_cls):
|
||||
__slots__ = ()
|
||||
|
||||
async def do_request(self, *args, **kwargs):
|
||||
generation = _POLLING_GENERATION_CONTEXT.get()
|
||||
result = await super().do_request(*args, **kwargs)
|
||||
adapter._observe_polling_request_result(self, generation, result)
|
||||
return result
|
||||
|
||||
request.__class__ = _InstrumentedPollingRequest
|
||||
return request
|
||||
|
||||
async def _start_polling_once(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue