fix: correct send_message and update_event return contracts (closes #238, closes #239)#243
Merged
Merged
Conversation
Closes #238 and #239 in one PR — both bugs are in the same area (message/event chat-write paths) and the fixes are independent but small enough that one PR is reasonable. ## send_message() — #238 Three issues addressed: - Missing `await` on `_continue_chat()` (spond.py:285 in pre-fix code). Callers using the `chat_id` path were getting an unawaited coroutine object back instead of the API response, plus a RuntimeWarning at GC time. - Sentinel-dict response (`{"error": "wrong usage, ..."}`) when args were inconsistent. Replaced with `raise ValueError(...)` so the error is loud and unambiguous, and the return type annotation is finally honest. - Unreachable `return False` branch when `user_obj` is falsy. `get_person()` raises `KeyError` rather than returning falsy, so this branch was dead code. Removed; the `user_obj["profile"]["id"]` indexing is now unconditional. Docstring updated: removed the "Notes / known bugs" block (since the bugs are gone), added a `Raises` section documenting ValueError + KeyError. ## update_event() — #239 The method was assigning the POST response to `self.events_update` and then returning `self.events` (the unrelated cached events list). Two fixes: - Return `await r.json()` directly — the actual API response. - Drop the now-unused `self.events_update` attribute entirely. It was only ever set inside this method and never read elsewhere (grep across spond/, tests/, examples/ confirms zero external consumers). Return annotation tightened from `list[JSONDict] | None` to `JSONDict` (the singular updated event). ## Tests - `test_update_event__returns_api_response`: locks in that the return value is the API response, not the cached list. - `test_send_message__continues_chat_when_chat_id_given`: locks in that `_continue_chat` is properly awaited (would have caught the missing-await bug). - `test_send_message__missing_args_raises_valueerror`: locks in that the three inconsistent-arg cases raise ValueError rather than silently returning a sentinel. ## Compatibility note Both changes are technically breaking for callers depending on undocumented behaviour (the sentinel dict, the cache-list return). But the return-type annotations always claimed `JSONDict` / the correct shape, so any consumer respecting the type contract was already broken in practice. The new behaviour matches the contract.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes two bugs in Spond chat/event write paths: send_message() was missing await on _continue_chat() and had a sentinel-dict / unreachable-return False pattern violating its JSONDict annotation; update_event() returned the unrelated cached self.events list instead of the API response. Both fixes tighten the return-type contracts and add regression tests.
Changes:
send_message(): addawaiton_continue_chat, raiseValueErrorfor inconsistent args (instead of returning sentinel dict), drop unreachablereturn Falsebranch, and refresh docstring.update_event(): returnawait r.json()directly, drop unusedself.events_updateattribute, and tighten return annotation toJSONDict.- Add 3 new tests covering the fixed behaviors (chat-id continuation, missing-args validation, update_event response).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| spond/spond.py | Corrects return contracts and async usage in send_message and update_event; updates docstrings/annotations. |
| tests/test_spond.py | Adds regression tests for update_event response and send_message chat-id/await + ValueError paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
6 tasks
Olen
added a commit
that referenced
this pull request
May 14, 2026
Ten findings actioned (the eleventh — subtle _match_person behavioural nuance — was a no-op note, not a bug). DictCompatModel consistency (findings #1, #2): - __len__, __contains__, __iter__, keys, values, items now all use `_present_api_keys()` which returns the field names actually present in the source data. Pre-OO callers got `len(d) == len(list(d)) == sum(1 for _ in d)` on raw dicts; this restores that invariant. - New `_pydantic_extras()` helper surfaces fields preserved by `extra="allow"` (see below) so they participate in all dict-compat views, including __getitem__ (with its own targeted deprecation warning explaining the field is unmodelled). Event.update payload (findings #3, #4): - Added `_EVENT_READ_ONLY_FIELDS` whitelist of fields the update body should NOT carry: creator_uid, created_time, updated, expired, registered, modified_from_series, series_uid, series_ordinal, responses, recipients, comments. These are server-managed, derived, or have their own dedicated endpoints (e.g. change_response for responses). Critical: sending `responses` back risked Spond overwriting concurrent attendance changes with stale local state. - `Event.update()` now does `model_dump(exclude=_EVENT_READ_ONLY_FIELDS)` before overlaying caller updates, so the POST body matches the spirit of the pre-OO _EVENT_TEMPLATE whitelist without needing a separate template fixture. Forward compatibility with API drift (finding #7): - Switched every DictCompatModel subclass from `extra="ignore"` to `extra="allow"` (Profile, Member, Guardian, Group, Post, Subgroup, Role, Event, Responses, plus Transaction in club.py). Unknown fields Spond emits are now preserved on the instance and accessible both as attributes (Pydantic native) and via the dict-compat shim. Avoids silently dropping data on API drift. SpondClub.get_transactions atomicity (finding #6): - Build the page list before extending self.transactions, so a mid-page Pydantic validation failure can't leave the cache half-populated. Test fixes (findings #8, #9): - Removed misleading `s.events = [...]` from test_get_export — the deprecated wrapper doesn't consult the cache. - Restored the `assert result is not s.events` identity guard in test_update_event__returns_api_response — it's the explicit regression check for the original #239 bug. - Added `test_len_contains_and_iter_agree` locking in the new dict-compat consistency invariant, and `test_extra_allow_preserves_unmodelled_fields` covering the forward-compat behaviour. Documentation (findings #10, #11): - Event.update docstring tightened to make resolution bounds explicit (`bounded to Event.model_fields`). - README's "from v1.3 onwards" softened to "next minor release" since pyproject.toml still says 1.2.1. - DESIGN-oo-rewrite.md `#243` reference removed (rotting issue link). 47 tests pass; ruff clean; format clean. Live-tested end-to-end: real event has 29 present keys (matches API response), `len()` and iteration agree, the description field present in the API correctly shows in `in`/iter — the inconsistency between length and iteration is gone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #238 and closes #239 in one PR — both bugs are in adjacent message/event chat-write paths, both fixes are tightly scoped, and bundling them keeps the review under one round-trip.
#238 — `send_message()`
Three issues:
Docstring: dropped the "Notes / known bugs" block (bugs are gone); added `Raises` section.
#239 — `update_event()`
The method was assigning the POST response to `self.events_update` and then `return self.events` (the unrelated cached events list). Fix:
Return annotation tightened from `list[JSONDict] | None` to `JSONDict`.
Tests added
update_event()returns cached events list instead of the API update response #239 fix.send_message()missingawaiton_continue_chat()and can returnFalsedespiteJSONDictannotation #238 fix API documentation? #1 (the await would have triggered `TypeError: object MagicMock can't be used in 'await' expression` in tests if `_continue_chat` weren't awaited).send_message()missingawaiton_continue_chat()and can returnFalsedespiteJSONDictannotation #238 fix getGroup(uid) always returns None #2 (three parameterised cases: no chat_id at all, user-only, group_uid-only).27 tests pass total (up from 24).
Compatibility
Both changes are technically breaking for callers depending on undocumented behaviour (the sentinel dict, the cache-list return). But the return-type annotations always claimed `JSONDict` / the correct shape, so any consumer respecting the type contract was already broken in practice. The new behaviour matches the contract.
Test plan
🤖 Generated with Claude Code