Skip to content

Commit 7531aca

Browse files
Phase G: snake_case fix-up on public dataclass fields
Mirrors PR #1357 phase B + api_review_python.md item B.9 (TypeScript SDK API review). Public Python dataclasses no longer expose camelCase attribute names; the wire JSON keys are unchanged. Renames (Python attribute → wire JSON key): * `PingResponse.protocolVersion` → `protocol_version` (wire: `protocolVersion`) * `SessionContext.gitRoot` → `git_root` (wire: `gitRoot`) * `SessionListFilter.gitRoot` → `git_root` (wire: `gitRoot`) * `SessionMetadata.sessionId` → `session_id` (wire: `sessionId`) * `SessionMetadata.startTime` → `start_time` (wire: `startTime`) * `SessionMetadata.modifiedTime` → `modified_time` (wire: `modifiedTime`) * `SessionMetadata.isRemote` → `is_remote` (wire: `isRemote`) * `SessionLifecycleEventMetadata.startTime` → `start_time` * `SessionLifecycleEventMetadata.modifiedTime` → `modified_time` * `SessionLifecycleEventBase.sessionId` → `session_id` Hook input TypedDicts (`PreToolUseHookInput`, `PreMcpToolCallHookInput`, etc.) intentionally keep camelCase keys — those are wire-format dicts delivered straight to handlers, not Python attributes. Tests + docs updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 24cafde commit 7531aca

7 files changed

Lines changed: 74 additions & 74 deletions

File tree

python/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,12 @@ await client.set_foreground_session_id("session-123")
199199

200200
# Subscribe to all lifecycle events
201201
def on_lifecycle(event):
202-
print(f"{event.type}: {event.sessionId}")
202+
print(f"{event.type}: {event.session_id}")
203203

204204
unsubscribe = client.on_lifecycle(on_lifecycle)
205205

206206
# Subscribe to specific event type
207-
unsubscribe = client.on_lifecycle("session.foreground", lambda e: print(f"Foreground: {e.sessionId}"))
207+
unsubscribe = client.on_lifecycle("session.foreground", lambda e: print(f"Foreground: {e.session_id}"))
208208

209209
# Later, to stop receiving events:
210210
unsubscribe()

python/copilot/client.py

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -351,32 +351,32 @@ class PingResponse:
351351
"""Response from ping"""
352352

353353
message: str # Echo message with "pong: " prefix
354-
timestamp: datetime # ISO 8601 timestamp when the ping was processed
355-
protocolVersion: int # Protocol version for SDK compatibility
354+
timestamp: datetime # Timestamp when the ping was processed
355+
protocol_version: int # Protocol version for SDK compatibility
356356

357357
@staticmethod
358358
def from_dict(obj: Any) -> PingResponse:
359359
assert isinstance(obj, dict)
360360
message = obj.get("message")
361361
timestamp = obj.get("timestamp")
362-
protocolVersion = obj.get("protocolVersion")
363-
if message is None or timestamp is None or protocolVersion is None:
362+
protocol_version = obj.get("protocolVersion")
363+
if message is None or timestamp is None or protocol_version is None:
364364
raise ValueError(
365365
f"Missing required fields in PingResponse: message={message}, "
366-
f"timestamp={timestamp}, protocolVersion={protocolVersion}"
366+
f"timestamp={timestamp}, protocolVersion={protocol_version}"
367367
)
368368
timestamp_value = (
369369
datetime.fromtimestamp(timestamp / 1000, tz=UTC)
370370
if isinstance(timestamp, (int, float))
371371
else from_datetime(timestamp)
372372
)
373-
return PingResponse(str(message), timestamp_value, int(protocolVersion))
373+
return PingResponse(str(message), timestamp_value, int(protocol_version))
374374

375375
def to_dict(self) -> dict:
376376
result: dict = {}
377377
result["message"] = self.message
378378
result["timestamp"] = self.timestamp.isoformat()
379-
result["protocolVersion"] = self.protocolVersion
379+
result["protocolVersion"] = self.protocol_version
380380
return result
381381

382382

@@ -757,7 +757,7 @@ class SessionContext:
757757
"""Working directory context for a session"""
758758

759759
working_directory: str # Working directory where the session was created
760-
gitRoot: str | None = None # Git repository root (if in a git repo)
760+
git_root: str | None = None # Git repository root (if in a git repo)
761761
repository: str | None = None # GitHub repository in "owner/repo" format
762762
branch: str | None = None # Current git branch
763763

@@ -769,15 +769,15 @@ def from_dict(obj: Any) -> SessionContext:
769769
raise ValueError("Missing required field 'cwd' in SessionContext")
770770
return SessionContext(
771771
working_directory=str(cwd),
772-
gitRoot=obj.get("gitRoot"),
772+
git_root=obj.get("gitRoot"),
773773
repository=obj.get("repository"),
774774
branch=obj.get("branch"),
775775
)
776776

777777
def to_dict(self) -> dict:
778778
result: dict = {"cwd": self.working_directory}
779-
if self.gitRoot is not None:
780-
result["gitRoot"] = self.gitRoot
779+
if self.git_root is not None:
780+
result["gitRoot"] = self.git_root
781781
if self.repository is not None:
782782
result["repository"] = self.repository
783783
if self.branch is not None:
@@ -790,16 +790,16 @@ class SessionListFilter:
790790
"""Filter options for listing sessions"""
791791

792792
working_directory: str | None = None # Filter by exact working directory match
793-
gitRoot: str | None = None # Filter by git root
793+
git_root: str | None = None # Filter by git root
794794
repository: str | None = None # Filter by repository (owner/repo format)
795795
branch: str | None = None # Filter by branch
796796

797797
def to_dict(self) -> dict:
798798
result: dict = {}
799799
if self.working_directory is not None:
800800
result["cwd"] = self.working_directory
801-
if self.gitRoot is not None:
802-
result["gitRoot"] = self.gitRoot
801+
if self.git_root is not None:
802+
result["gitRoot"] = self.git_root
803803
if self.repository is not None:
804804
result["repository"] = self.repository
805805
if self.branch is not None:
@@ -811,43 +811,43 @@ def to_dict(self) -> dict:
811811
class SessionMetadata:
812812
"""Metadata about a session"""
813813

814-
sessionId: str # Session identifier
815-
startTime: datetime # Timestamp when session was created
816-
modifiedTime: datetime # Timestamp when session was last modified
817-
isRemote: bool # Whether the session is remote
814+
session_id: str # Session identifier
815+
start_time: datetime # Timestamp when session was created
816+
modified_time: datetime # Timestamp when session was last modified
817+
is_remote: bool # Whether the session is remote
818818
summary: str | None = None # Optional summary of the session
819819
context: SessionContext | None = None # Working directory context
820820

821821
@staticmethod
822822
def from_dict(obj: Any) -> SessionMetadata:
823823
assert isinstance(obj, dict)
824-
sessionId = obj.get("sessionId")
825-
startTime = obj.get("startTime")
826-
modifiedTime = obj.get("modifiedTime")
827-
isRemote = obj.get("isRemote")
828-
if sessionId is None or startTime is None or modifiedTime is None or isRemote is None:
824+
session_id = obj.get("sessionId")
825+
start_time = obj.get("startTime")
826+
modified_time = obj.get("modifiedTime")
827+
is_remote = obj.get("isRemote")
828+
if session_id is None or start_time is None or modified_time is None or is_remote is None:
829829
raise ValueError(
830-
f"Missing required fields in SessionMetadata: sessionId={sessionId}, "
831-
f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}"
830+
f"Missing required fields in SessionMetadata: sessionId={session_id}, "
831+
f"startTime={start_time}, modifiedTime={modified_time}, isRemote={is_remote}"
832832
)
833833
summary = obj.get("summary")
834834
context_dict = obj.get("context")
835835
context = SessionContext.from_dict(context_dict) if context_dict else None
836836
return SessionMetadata(
837-
sessionId=str(sessionId),
838-
startTime=_parse_session_timestamp(startTime),
839-
modifiedTime=_parse_session_timestamp(modifiedTime),
840-
isRemote=bool(isRemote),
837+
session_id=str(session_id),
838+
start_time=_parse_session_timestamp(start_time),
839+
modified_time=_parse_session_timestamp(modified_time),
840+
is_remote=bool(is_remote),
841841
summary=summary,
842842
context=context,
843843
)
844844

845845
def to_dict(self) -> dict:
846846
result: dict = {}
847-
result["sessionId"] = self.sessionId
848-
result["startTime"] = self.startTime.isoformat()
849-
result["modifiedTime"] = self.modifiedTime.isoformat()
850-
result["isRemote"] = self.isRemote
847+
result["sessionId"] = self.session_id
848+
result["startTime"] = self.start_time.isoformat()
849+
result["modifiedTime"] = self.modified_time.isoformat()
850+
result["isRemote"] = self.is_remote
851851
if self.summary is not None:
852852
result["summary"] = self.summary
853853
if self.context is not None:
@@ -884,15 +884,15 @@ def _parse_session_timestamp(value: Any) -> datetime:
884884
class SessionLifecycleEventMetadata:
885885
"""Metadata for session lifecycle events."""
886886

887-
startTime: datetime
888-
modifiedTime: datetime
887+
start_time: datetime
888+
modified_time: datetime
889889
summary: str | None = None
890890

891891
@staticmethod
892892
def from_dict(data: dict) -> SessionLifecycleEventMetadata:
893893
return SessionLifecycleEventMetadata(
894-
startTime=_parse_session_timestamp(data.get("startTime", "")),
895-
modifiedTime=_parse_session_timestamp(data.get("modifiedTime", "")),
894+
start_time=_parse_session_timestamp(data.get("startTime", "")),
895+
modified_time=_parse_session_timestamp(data.get("modifiedTime", "")),
896896
summary=data.get("summary"),
897897
)
898898

@@ -906,7 +906,7 @@ class SessionLifecycleEventBase:
906906
branch on the event kind.
907907
"""
908908

909-
sessionId: str
909+
session_id: str
910910
metadata: SessionLifecycleEventMetadata | None = None
911911

912912

@@ -962,17 +962,17 @@ def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent:
962962
session_id = data.get("sessionId", "")
963963
match data.get("type"):
964964
case "session.created":
965-
return SessionCreatedEvent(sessionId=session_id, metadata=metadata)
965+
return SessionCreatedEvent(session_id=session_id, metadata=metadata)
966966
case "session.deleted":
967-
return SessionDeletedEvent(sessionId=session_id, metadata=metadata)
967+
return SessionDeletedEvent(session_id=session_id, metadata=metadata)
968968
case "session.foreground":
969-
return SessionForegroundEvent(sessionId=session_id, metadata=metadata)
969+
return SessionForegroundEvent(session_id=session_id, metadata=metadata)
970970
case "session.background":
971-
return SessionBackgroundEvent(sessionId=session_id, metadata=metadata)
971+
return SessionBackgroundEvent(session_id=session_id, metadata=metadata)
972972
case _:
973973
# Default to ``session.updated`` for unknown event types so consumers
974974
# keep working across server upgrades.
975-
return SessionUpdatedEvent(sessionId=session_id, metadata=metadata)
975+
return SessionUpdatedEvent(session_id=session_id, metadata=metadata)
976976

977977

978978
SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None]
@@ -2419,7 +2419,7 @@ async def get_session_metadata(self, session_id: str) -> SessionMetadata | None:
24192419
Example:
24202420
>>> metadata = await client.get_session_metadata("session-123")
24212421
>>> if metadata:
2422-
... print(f"Session started at: {metadata.startTime}")
2422+
... print(f"Session started at: {metadata.start_time}")
24232423
"""
24242424
if not self._client:
24252425
raise RuntimeError("Client not connected")
@@ -2573,11 +2573,11 @@ def on_lifecycle(
25732573
Example:
25742574
>>> # Subscribe to specific event type
25752575
>>> unsubscribe = client.on_lifecycle(
2576-
... "session.foreground", lambda e: print(e.sessionId)
2576+
... "session.foreground", lambda e: print(e.session_id)
25772577
... )
25782578
>>>
25792579
>>> # Subscribe to all events
2580-
>>> unsubscribe = client.on_lifecycle(lambda e: print(f"{e.type}: {e.sessionId}"))
2580+
>>> unsubscribe = client.on_lifecycle(lambda e: print(f"{e.type}: {e.session_id}"))
25812581
>>>
25822582
>>> # Later, to stop receiving events:
25832583
>>> unsubscribe()

python/e2e/test_client_e2e.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ async def test_should_get_status_with_version_and_protocol_info(self):
105105
assert hasattr(status, "version")
106106
assert isinstance(status.version, str)
107107
assert hasattr(status, "protocolVersion")
108-
assert isinstance(status.protocolVersion, int)
109-
assert status.protocolVersion >= 1
108+
assert isinstance(status.protocol_version, int)
109+
assert status.protocol_version >= 1
110110

111111
await client.stop()
112112
finally:

python/e2e/test_client_lifecycle_e2e.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def handler(event):
118118
try:
119119
event = await asyncio.wait_for(created, 10.0)
120120
assert event.type == "session.created"
121-
assert event.sessionId == session.session_id
121+
assert event.session_id == session.session_id
122122
finally:
123123
await session.disconnect()
124124
finally:
@@ -140,7 +140,7 @@ def handler(event):
140140
try:
141141
event = await asyncio.wait_for(created, 10.0)
142142
assert event.type == "session.created"
143-
assert event.sessionId == session.session_id
143+
assert event.session_id == session.session_id
144144
finally:
145145
await session.disconnect()
146146
finally:
@@ -170,7 +170,7 @@ def disposed_handler(_event):
170170
)
171171
try:
172172
event = await asyncio.wait_for(active_event, 10.0)
173-
assert event.sessionId == session.session_id
173+
assert event.session_id == session.session_id
174174
assert unsubscribed_count == 0, "Disposed handler should not have fired"
175175
finally:
176176
await session.disconnect()
@@ -206,7 +206,7 @@ async def test_should_receive_session_updated_lifecycle_event_for_non_ephemeral_
206206
def handler(event):
207207
if (
208208
event.type == "session.updated"
209-
and event.sessionId == session.session_id
209+
and event.session_id == session.session_id
210210
and not updated.done()
211211
):
212212
updated.set_result(event)
@@ -216,7 +216,7 @@ def handler(event):
216216
await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN))
217217
event = await asyncio.wait_for(updated, timeout=15.0)
218218
assert event.type == "session.updated"
219-
assert event.sessionId == session.session_id
219+
assert event.session_id == session.session_id
220220
finally:
221221
unsubscribe()
222222
await session.disconnect()
@@ -241,7 +241,7 @@ async def test_should_receive_session_deleted_lifecycle_event_when_deleted(
241241
def handler(event):
242242
if (
243243
event.type == "session.deleted"
244-
and event.sessionId == session_id
244+
and event.session_id == session_id
245245
and not deleted.done()
246246
):
247247
deleted.set_result(event)
@@ -253,6 +253,6 @@ def handler(event):
253253

254254
event = await asyncio.wait_for(deleted, timeout=15.0)
255255
assert event.type == "session.deleted"
256-
assert event.sessionId == session_id
256+
assert event.session_id == session_id
257257
finally:
258258
unsubscribe()

python/e2e/test_client_options_e2e.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def _get_available_port() -> int:
142142
return;
143143
}
144144
if (message.method === "session.create") {
145-
const sessionId = message.params?.sessionId ?? "fake-session";
145+
const sessionId = message.params?.session_id ?? "fake-session";
146146
writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null });
147147
return;
148148
}

0 commit comments

Comments
 (0)