Skip to content

fix: correct send_message and update_event return contracts (closes #238, closes #239)#243

Merged
Olen merged 1 commit into
mainfrom
fix/send-message-update-event
May 14, 2026
Merged

fix: correct send_message and update_event return contracts (closes #238, closes #239)#243
Olen merged 1 commit into
mainfrom
fix/send-message-update-event

Conversation

@Olen
Copy link
Copy Markdown
Owner

@Olen Olen commented May 14, 2026

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:

  1. Missing `await` on `_continue_chat()`. Callers using the `chat_id` path were getting an unawaited coroutine object back instead of the API response, plus a `RuntimeWarning: coroutine '_continue_chat' was never awaited` at GC time.
  2. Sentinel-dict response when args are inconsistent. Was returning `{"error": "wrong usage, ..."}`; now raises `ValueError` so the failure is loud and the return-type annotation is finally honest.
  3. Unreachable `return False` branch. `get_person()` raises `KeyError` rather than returning falsy, so `if user_obj: ... else: return False` was dead code. Removed.

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 `await r.json()` directly — the actual API response.
  • Drop the now-unused `self.events_update` attribute entirely. It was only ever set inside this one method and never read anywhere else (grep across `spond/`, `tests/`, `examples/` confirms zero consumers).

Return annotation tightened from `list[JSONDict] | None` to `JSONDict`.

Tests added

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

  • Local `pytest` — 27 tests pass (was 24)
  • Local `ruff check` — clean
  • Local `ruff format --check` — clean
  • CI: 4-version matrix green
  • Copilot review: addressed all comments

🤖 Generated with Claude Code

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.
@Olen Olen requested a review from Copilot May 14, 2026 14:19
@Olen Olen added the updateme Automatically update PR from main label May 14, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(): add await on _continue_chat, raise ValueError for inconsistent args (instead of returning sentinel dict), drop unreachable return False branch, and refresh docstring.
  • update_event(): return await r.json() directly, drop unused self.events_update attribute, and tighten return annotation to JSONDict.
  • 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.

@Olen Olen merged commit 3a80812 into main May 14, 2026
11 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

updateme Automatically update PR from main

Projects

None yet

2 participants