fix: Python 3.14 compatibility for OAuth device flow

Fixes weak reference error in asyncio.run() on Python 3.14.2.

Python 3.14 introduced stricter event loop lifecycle management that
causes weak reference errors when synchronous code (webbrowser.open)
is called from within async contexts managed by asyncio.run().

Changes:
- Run webbrowser.open() in ThreadPoolExecutor to isolate from event loop
- Make display_user_instructions() async and await it in login flow
- Adds Python 3.14 compatibility while maintaining backward compatibility

Fixes #480

Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
claude[bot]
2025-12-27 16:29:14 +00:00
parent 856737fe3c
commit 925d65a499
+9 -3
View File
@@ -71,7 +71,7 @@ class CLIAuth:
console.print(f"[red]Device authorization error: {e}[/red]")
return None
def display_user_instructions(self, device_response: dict) -> None:
async def display_user_instructions(self, device_response: dict) -> None:
"""Display user instructions for device authorization."""
user_code = device_response["user_code"]
verification_uri = device_response["verification_uri"]
@@ -87,9 +87,15 @@ class CLIAuth:
console.print(f"[bold green]{verification_uri_complete}[/bold green]")
# Try to open browser automatically
# Python 3.14: Run webbrowser.open in executor to avoid weak reference issues
try:
console.print("\n[dim]Opening browser automatically...[/dim]")
webbrowser.open(verification_uri_complete)
import asyncio
import concurrent.futures
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as executor:
await loop.run_in_executor(executor, webbrowser.open, verification_uri_complete)
except Exception:
pass # Silently fail if browser can't be opened
@@ -252,7 +258,7 @@ class CLIAuth:
return False
# Step 2: Display user instructions
self.display_user_instructions(device_response)
await self.display_user_instructions(device_response)
# Step 3: Poll for token
device_code = device_response["device_code"]