Generate dedicated Python session event types#1063
Conversation
Align Python session event generation with the newer Go-style dedicated per-event payload model instead of the old merged quicktype Data shape. This updates the runtime/tests for typed payloads while preserving compatibility aliases and legacy Data behavior for existing callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| const lines: string[] = []; | ||
| if (description) { | ||
| lines.push(`class ${enumName}(Enum):`); | ||
| lines.push(` """${description.replace(/"/g, '\\"')}"""`); |
| lines.push(`@dataclass`); | ||
| lines.push(`class ${typeName}:`); | ||
| if (description || schema.description) { | ||
| lines.push(` """${(description || schema.description || "").replace(/"/g, '\\"')}"""`); |
| lines.push(`@dataclass`); | ||
| lines.push(`class ${typeName}:`); | ||
| if (description) { | ||
| lines.push(` """${description.replace(/"/g, '\\"')}"""`); |
There was a problem hiding this comment.
Pull request overview
This PR updates the Python SDK’s session-events generation to emit dedicated per-event payload dataclasses (instead of a single merged Data payload), and updates the Python runtime/tests to consume these typed payloads while retaining compatibility shims.
Changes:
- Replaced Python session-events quicktype generation with a custom generator that emits per-event payload dataclasses and dispatches in
SessionEvent.from_dict(). - Updated Python session handling and tests to use typed payload classes (e.g.,
CommandExecuteData,ElicitationRequestedData) and validate legacy top-level exports +Datashim behavior. - Preserved forward compatibility for unknown event types via
SessionEventType.UNKNOWN+RawSessionEventData, plus added several legacy helper aliases.
Show a summary per file
| File | Description |
|---|---|
| scripts/codegen/python.ts | Implements custom Python session-events codegen emitting per-event payload dataclasses, compatibility shims, and typed dispatch. |
| python/test_event_forward_compatibility.py | Extends forward-compat tests to cover legacy helper exports and Data shim dict-preservation behavior. |
| python/test_commands_and_elicitation.py | Updates tests to construct typed event payloads instead of using the legacy Data container. |
| python/copilot/session.py | Updates broadcast event handling to cast event.data to the appropriate generated payload dataclass before accessing fields. |
Copilot's findings
- Files reviewed: 4/5 changed files
- Comments generated: 2
| out.push(` result: list[str] = []`); | ||
| out.push(` for index, char in enumerate(name.replace(".", "_")):`); | ||
| out.push( | ||
| ` if char.isupper() and index > 0 and (not name[index - 1].isupper() or (index + 1 < len(name) and name[index + 1].islower())):` |
There was a problem hiding this comment.
In the generated _compat_to_python_key, the loop iterates over name.replace(".", "_") but the underscore-insertion condition still indexes into the original name. For keys containing a dot followed by an uppercase letter (e.g. "foo.Bar"), this will insert an extra underscore and produce "foo__bar" instead of "foo_bar". Consider using a local normalized = name.replace(".", "_") and use normalized[...] for the neighbor checks to keep the logic consistent with the iterated string.
| out.push(` result: list[str] = []`); | |
| out.push(` for index, char in enumerate(name.replace(".", "_")):`); | |
| out.push( | |
| ` if char.isupper() and index > 0 and (not name[index - 1].isupper() or (index + 1 < len(name) and name[index + 1].islower())):` | |
| out.push(` normalized = name.replace(".", "_")`); | |
| out.push(` result: list[str] = []`); | |
| out.push(` for index, char in enumerate(normalized):`); | |
| out.push( | |
| ` if char.isupper() and index > 0 and (not normalized[index - 1].isupper() or (index + 1 < len(normalized) and normalized[index + 1].islower())):` |
| } | ||
| for (const value of values) { | ||
| lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`); | ||
| } |
There was a problem hiding this comment.
getOrCreatePyEnum generates plain Enum classes with only the known schema values. Any newly introduced enum values from the server/schema will raise ValueError in parse_enum(...), which is at odds with the forward-compat approach used for SessionEventType (UNKNOWN + _missing_). To preserve forward compatibility (especially for discriminator enums created by emitPyFlatDiscriminatedUnion), consider emitting an UNKNOWN member plus a _missing_ handler (and optionally preserving the raw string) for these generated enums as well, or avoid enums for forward-compatible discriminator fields.
| } | |
| } | |
| lines.push(` UNKNOWN = "__unknown__"`); | |
| lines.push(``); | |
| lines.push(` @classmethod`); | |
| lines.push(` def _missing_(cls, value: object) -> "${enumName}":`); | |
| lines.push(` return cls.UNKNOWN`); |
Cross-SDK Consistency Review ✅This PR improves cross-SDK consistency rather than introducing new inconsistencies. SummaryThe PR aligns Python session-event generation with the Go (and .NET) approach by replacing quicktype's flattened
Key consistency checks
Internal usage in
|
Summary
Python session events still used quicktype and collapsed payload members into one merged
Datamodel, unlike the newer Go implementation. That made the language bindings inconsistent and increased the chance of naming conflicts as the runtime schema grows.This change switches Python session-event generation to dedicated per-event payload dataclasses, matching the newer Go-style approach.
SessionEvent.from_dict()now dispatches to typed payload classes, unknown events still round-trip throughRawSessionEventData, and the Python runtime/tests were updated to handle the typed payload union.The one non-obvious part is compatibility: the generator also restores legacy top-level helper exports and keeps arbitrary nested mappings in the
Datashim as plaindicts so older callers do not break while the generated model becomes more structured.Validation
python -m pytest test_event_forward_compatibility.py test_commands_and_elicitation.pypython -m pytest --ignore=e2e