Skip to content

Commit 35534c4

Browse files
brettcannonCopilotpatniko
authored
chore(python): Bump the oldest supported Python version to 3.11 (#561)
* Set Python 3.11 as the minimum version * Run pyupgrade * More modernization * Address review comments * Update python/copilot/generated` via codegen instead of by pyupgrade * Update Python version matrix to only include 3.11 for compatibility testing * Regenerate codegen files Update generated files for Python and Go SDKs to include new agent API types, compaction result types, and new session event types (assistant.streaming_delta, session.task_complete). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix formatting * fix: gofmt Go generated files and fix Python test for fork PRs - Run gofmt on go/rpc/generated_rpc.go and go/generated_session_events.go to fix spaces→tabs formatting that caused the Codegen Check to fail. - Fix test_resume_session_forwards_client_name to return a mock response for session.resume instead of forwarding to the real CLI, which requires the COPILOT_HMAC_KEY secret unavailable to fork PRs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Patrick Nikoletich <patniko@github.com>
1 parent 5a4f823 commit 35534c4

File tree

15 files changed

+317
-268
lines changed

15 files changed

+317
-268
lines changed

.github/workflows/python-sdk-tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ jobs:
3737
fail-fast: false
3838
matrix:
3939
os: [ubuntu-latest, macos-latest, windows-latest]
40+
# Test the oldest supported Python version to make sure compatibility is maintained.
41+
python-version: ["3.11"]
4042
runs-on: ${{ matrix.os }}
4143
defaults:
4244
run:
@@ -46,7 +48,7 @@ jobs:
4648
- uses: actions/checkout@v6.0.2
4749
- uses: actions/setup-python@v6
4850
with:
49-
python-version: "3.12"
51+
python-version: ${{ matrix.python-version }}
5052
- uses: actions/setup-node@v6
5153
with:
5254
node-version: "22"

python/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,11 +401,11 @@ async def handle_user_input(request, invocation):
401401
# request["question"] - The question to ask
402402
# request.get("choices") - Optional list of choices for multiple choice
403403
# request.get("allowFreeform", True) - Whether freeform input is allowed
404-
404+
405405
print(f"Agent asks: {request['question']}")
406406
if request.get("choices"):
407407
print(f"Choices: {', '.join(request['choices'])}")
408-
408+
409409
# Return the user's response
410410
return {
411411
"answer": "User's answer here",
@@ -483,5 +483,5 @@ session = await client.create_session({
483483

484484
## Requirements
485485

486-
- Python 3.9+
486+
- Python 3.11+
487487
- GitHub Copilot CLI installed and accessible

python/copilot/client.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
import subprocess
2020
import sys
2121
import threading
22+
from collections.abc import Callable
2223
from dataclasses import asdict, is_dataclass
2324
from pathlib import Path
24-
from typing import Any, Callable, Optional, cast
25+
from typing import Any, cast
2526

2627
from .generated.rpc import ServerRpc
2728
from .generated.session_events import session_event_from_dict
@@ -51,7 +52,7 @@
5152
)
5253

5354

54-
def _get_bundled_cli_path() -> Optional[str]:
55+
def _get_bundled_cli_path() -> str | None:
5556
"""Get the path to the bundled CLI binary, if available."""
5657
# The binary is bundled in copilot/bin/ within the package
5758
bin_dir = Path(__file__).parent / "bin"
@@ -106,7 +107,7 @@ class CopilotClient:
106107
>>> client = CopilotClient({"cli_url": "localhost:3000"})
107108
"""
108109

109-
def __init__(self, options: Optional[CopilotClientOptions] = None):
110+
def __init__(self, options: CopilotClientOptions | None = None):
110111
"""
111112
Initialize a new CopilotClient.
112113
@@ -151,7 +152,7 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
151152
self._is_external_server: bool = False
152153
if opts.get("cli_url"):
153154
self._actual_host, actual_port = self._parse_cli_url(opts["cli_url"])
154-
self._actual_port: Optional[int] = actual_port
155+
self._actual_port: int | None = actual_port
155156
self._is_external_server = True
156157
else:
157158
self._actual_port = None
@@ -197,19 +198,19 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
197198
if github_token:
198199
self.options["github_token"] = github_token
199200

200-
self._process: Optional[subprocess.Popen] = None
201-
self._client: Optional[JsonRpcClient] = None
201+
self._process: subprocess.Popen | None = None
202+
self._client: JsonRpcClient | None = None
202203
self._state: ConnectionState = "disconnected"
203204
self._sessions: dict[str, CopilotSession] = {}
204205
self._sessions_lock = threading.Lock()
205-
self._models_cache: Optional[list[ModelInfo]] = None
206+
self._models_cache: list[ModelInfo] | None = None
206207
self._models_cache_lock = asyncio.Lock()
207208
self._lifecycle_handlers: list[SessionLifecycleHandler] = []
208209
self._typed_lifecycle_handlers: dict[
209210
SessionLifecycleEventType, list[SessionLifecycleHandler]
210211
] = {}
211212
self._lifecycle_handlers_lock = threading.Lock()
212-
self._rpc: Optional[ServerRpc] = None
213+
self._rpc: ServerRpc | None = None
213214

214215
@property
215216
def rpc(self) -> ServerRpc:
@@ -786,7 +787,7 @@ def get_state(self) -> ConnectionState:
786787
"""
787788
return self._state
788789

789-
async def ping(self, message: Optional[str] = None) -> "PingResponse":
790+
async def ping(self, message: str | None = None) -> "PingResponse":
790791
"""
791792
Send a ping request to the server to verify connectivity.
792793
@@ -956,7 +957,7 @@ async def delete_session(self, session_id: str) -> None:
956957
if session_id in self._sessions:
957958
del self._sessions[session_id]
958959

959-
async def get_foreground_session_id(self) -> Optional[str]:
960+
async def get_foreground_session_id(self) -> str | None:
960961
"""
961962
Get the ID of the session currently displayed in the TUI.
962963
@@ -1009,7 +1010,7 @@ async def set_foreground_session_id(self, session_id: str) -> None:
10091010
def on(
10101011
self,
10111012
event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler,
1012-
handler: Optional[SessionLifecycleHandler] = None,
1013+
handler: SessionLifecycleHandler | None = None,
10131014
) -> Callable[[], None]:
10141015
"""
10151016
Subscribe to session lifecycle events.
@@ -1267,7 +1268,7 @@ async def read_port():
12671268

12681269
try:
12691270
await asyncio.wait_for(read_port(), timeout=10.0)
1270-
except asyncio.TimeoutError:
1271+
except TimeoutError:
12711272
raise RuntimeError("Timeout waiting for CLI server to start")
12721273

12731274
async def _connect_to_server(self) -> None:

python/copilot/generated/rpc.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111

1212
from dataclasses import dataclass
13-
from typing import Any, Optional, List, Dict, TypeVar, Type, cast, Callable
13+
from typing import Any, TypeVar, cast
14+
from collections.abc import Callable
1415
from enum import Enum
1516

1617

@@ -52,22 +53,22 @@ def from_bool(x: Any) -> bool:
5253
return x
5354

5455

55-
def to_class(c: Type[T], x: Any) -> dict:
56+
def to_class(c: type[T], x: Any) -> dict:
5657
assert isinstance(x, c)
5758
return cast(Any, x).to_dict()
5859

5960

60-
def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
61+
def from_list(f: Callable[[Any], T], x: Any) -> list[T]:
6162
assert isinstance(x, list)
6263
return [f(y) for y in x]
6364

6465

65-
def from_dict(f: Callable[[Any], T], x: Any) -> Dict[str, T]:
66+
def from_dict(f: Callable[[Any], T], x: Any) -> dict[str, T]:
6667
assert isinstance(x, dict)
6768
return { k: f(v) for (k, v) in x.items() }
6869

6970

70-
def to_enum(c: Type[EnumT], x: Any) -> EnumT:
71+
def to_enum(c: type[EnumT], x: Any) -> EnumT:
7172
assert isinstance(x, c)
7273
return x.value
7374

@@ -101,7 +102,7 @@ def to_dict(self) -> dict:
101102

102103
@dataclass
103104
class PingParams:
104-
message: Optional[str] = None
105+
message: str | None = None
105106
"""Optional message to echo back"""
106107

107108
@staticmethod
@@ -138,8 +139,8 @@ def to_dict(self) -> dict:
138139
@dataclass
139140
class Limits:
140141
max_context_window_tokens: float
141-
max_output_tokens: Optional[float] = None
142-
max_prompt_tokens: Optional[float] = None
142+
max_output_tokens: float | None = None
143+
max_prompt_tokens: float | None = None
143144

144145
@staticmethod
145146
def from_dict(obj: Any) -> 'Limits':
@@ -233,16 +234,16 @@ class Model:
233234
name: str
234235
"""Display name"""
235236

236-
billing: Optional[Billing] = None
237+
billing: Billing | None = None
237238
"""Billing information"""
238239

239-
default_reasoning_effort: Optional[str] = None
240+
default_reasoning_effort: str | None = None
240241
"""Default reasoning effort level (only present if model supports reasoning effort)"""
241242

242-
policy: Optional[Policy] = None
243+
policy: Policy | None = None
243244
"""Policy state (if applicable)"""
244245

245-
supported_reasoning_efforts: Optional[List[str]] = None
246+
supported_reasoning_efforts: list[str] | None = None
246247
"""Supported reasoning effort levels (only present if model supports reasoning effort)"""
247248

248249
@staticmethod
@@ -275,7 +276,7 @@ def to_dict(self) -> dict:
275276

276277
@dataclass
277278
class ModelsListResult:
278-
models: List[Model]
279+
models: list[Model]
279280
"""List of available models with full metadata"""
280281

281282
@staticmethod
@@ -298,14 +299,14 @@ class Tool:
298299
name: str
299300
"""Tool identifier (e.g., "bash", "grep", "str_replace_editor")"""
300301

301-
instructions: Optional[str] = None
302+
instructions: str | None = None
302303
"""Optional instructions for how to use this tool effectively"""
303304

304-
namespaced_name: Optional[str] = None
305+
namespaced_name: str | None = None
305306
"""Optional namespaced name for declarative filtering (e.g., "playwright/navigate" for MCP
306307
tools)
307308
"""
308-
parameters: Optional[Dict[str, Any]] = None
309+
parameters: dict[str, Any] | None = None
309310
"""JSON Schema for the tool's input parameters"""
310311

311312
@staticmethod
@@ -333,7 +334,7 @@ def to_dict(self) -> dict:
333334

334335
@dataclass
335336
class ToolsListResult:
336-
tools: List[Tool]
337+
tools: list[Tool]
337338
"""List of available built-in tools with metadata"""
338339

339340
@staticmethod
@@ -350,7 +351,7 @@ def to_dict(self) -> dict:
350351

351352
@dataclass
352353
class ToolsListParams:
353-
model: Optional[str] = None
354+
model: str | None = None
354355
"""Optional model ID — when provided, the returned tool list reflects model-specific
355356
overrides
356357
"""
@@ -385,7 +386,7 @@ class QuotaSnapshot:
385386
used_requests: float
386387
"""Number of requests used so far this period"""
387388

388-
reset_date: Optional[str] = None
389+
reset_date: str | None = None
389390
"""Date when the quota resets (ISO 8601)"""
390391

391392
@staticmethod
@@ -413,7 +414,7 @@ def to_dict(self) -> dict:
413414

414415
@dataclass
415416
class AccountGetQuotaResult:
416-
quota_snapshots: Dict[str, QuotaSnapshot]
417+
quota_snapshots: dict[str, QuotaSnapshot]
417418
"""Quota snapshots keyed by type (e.g., chat, completions, premium_interactions)"""
418419

419420
@staticmethod
@@ -430,7 +431,7 @@ def to_dict(self) -> dict:
430431

431432
@dataclass
432433
class SessionModelGetCurrentResult:
433-
model_id: Optional[str] = None
434+
model_id: str | None = None
434435

435436
@staticmethod
436437
def from_dict(obj: Any) -> 'SessionModelGetCurrentResult':
@@ -447,7 +448,7 @@ def to_dict(self) -> dict:
447448

448449
@dataclass
449450
class SessionModelSwitchToResult:
450-
model_id: Optional[str] = None
451+
model_id: str | None = None
451452

452453
@staticmethod
453454
def from_dict(obj: Any) -> 'SessionModelSwitchToResult':
@@ -546,7 +547,7 @@ class SessionPlanReadResult:
546547
exists: bool
547548
"""Whether plan.md exists in the workspace"""
548549

549-
content: Optional[str] = None
550+
content: str | None = None
550551
"""The content of plan.md, or null if it does not exist"""
551552

552553
@staticmethod
@@ -606,7 +607,7 @@ def to_dict(self) -> dict:
606607

607608
@dataclass
608609
class SessionWorkspaceListFilesResult:
609-
files: List[str]
610+
files: list[str]
610611
"""Relative file paths in the workspace files directory"""
611612

612613
@staticmethod
@@ -708,7 +709,7 @@ def to_dict(self) -> dict:
708709

709710
@dataclass
710711
class SessionFleetStartParams:
711-
prompt: Optional[str] = None
712+
prompt: str | None = None
712713
"""Optional user prompt to combine with fleet instructions"""
713714

714715
@staticmethod
@@ -753,7 +754,7 @@ def to_dict(self) -> dict:
753754

754755
@dataclass
755756
class SessionAgentListResult:
756-
agents: List[AgentElement]
757+
agents: list[AgentElement]
757758
"""Available custom agents"""
758759

759760
@staticmethod
@@ -797,7 +798,7 @@ def to_dict(self) -> dict:
797798

798799
@dataclass
799800
class SessionAgentGetCurrentResult:
800-
agent: Optional[SessionAgentGetCurrentResultAgent] = None
801+
agent: SessionAgentGetCurrentResultAgent | None = None
801802
"""Currently selected custom agent, or null if using the default agent"""
802803

803804
@staticmethod

0 commit comments

Comments
 (0)