From 250817a6147697740ca4986838326400dbd140be Mon Sep 17 00:00:00 2001 From: Olmo Maldonado Date: Fri, 26 Dec 2025 17:54:42 -0800 Subject: [PATCH 1/2] build for python 3.10 --- bindings/python/pyproject.toml | 4 +- bindings/python/src/lingua/__init__.py | 34 +++++++------ bindings/python/src/lingua/__init__.pyi | 63 +++++++++++++++++++------ bindings/python/tests/test_roundtrip.py | 43 +++++++++++------ 4 files changed, 97 insertions(+), 47 deletions(-) diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index b8fc0808..824217ea 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "maturin" name = "braintrust-lingua" version = "0.1.0" description = "Lingua - Universal message format for LLM APIs" -requires-python = ">=3.8" +requires-python = ">=3.10" license = { text = "MIT OR Apache-2.0" } keywords = ["llm", "ai", "openai", "anthropic", "api", "lingua"] classifiers = [ @@ -15,8 +15,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/bindings/python/src/lingua/__init__.py b/bindings/python/src/lingua/__init__.py index 5486b539..1c8d32f5 100644 --- a/bindings/python/src/lingua/__init__.py +++ b/bindings/python/src/lingua/__init__.py @@ -11,7 +11,7 @@ Note: Python uses exceptions while TypeScript uses Zod-style result objects. """ -from typing import Any, Dict +from typing import Any # Import the native conversion functions from lingua._lingua import ( @@ -65,7 +65,9 @@ def chat_completions_messages_to_lingua(messages: list) -> list: try: return _chat_completions_messages_to_lingua(messages) except Exception as e: - raise ConversionError(f"Failed to convert chat completions messages to Lingua: {e}") from e + raise ConversionError( + f"Failed to convert chat completions messages to Lingua: {e}" + ) from e def lingua_to_chat_completions_messages(messages: list) -> list: @@ -84,7 +86,9 @@ def lingua_to_chat_completions_messages(messages: list) -> list: try: return _lingua_to_chat_completions_messages(messages) except Exception as e: - raise ConversionError(f"Failed to convert Lingua to chat completions messages: {e}") from e + raise ConversionError( + f"Failed to convert Lingua to chat completions messages: {e}" + ) from e # ============================================================================ @@ -107,7 +111,9 @@ def responses_messages_to_lingua(messages: list) -> list: try: return _responses_messages_to_lingua(messages) except Exception as e: - raise ConversionError(f"Failed to convert responses messages to Lingua: {e}") from e + raise ConversionError( + f"Failed to convert responses messages to Lingua: {e}" + ) from e def lingua_to_responses_messages(messages: list) -> list: @@ -126,7 +132,9 @@ def lingua_to_responses_messages(messages: list) -> list: try: return _lingua_to_responses_messages(messages) except Exception as e: - raise ConversionError(f"Failed to convert Lingua to responses messages: {e}") from e + raise ConversionError( + f"Failed to convert Lingua to responses messages: {e}" + ) from e # ============================================================================ @@ -149,7 +157,9 @@ def anthropic_messages_to_lingua(messages: list) -> list: try: return _anthropic_messages_to_lingua(messages) except Exception as e: - raise ConversionError(f"Failed to convert Anthropic messages to Lingua: {e}") from e + raise ConversionError( + f"Failed to convert Anthropic messages to Lingua: {e}" + ) from e def lingua_to_anthropic_messages(messages: list) -> list: @@ -168,7 +178,9 @@ def lingua_to_anthropic_messages(messages: list) -> list: try: return _lingua_to_anthropic_messages(messages) except Exception as e: - raise ConversionError(f"Failed to convert Lingua to Anthropic messages: {e}") from e + raise ConversionError( + f"Failed to convert Lingua to Anthropic messages: {e}" + ) from e # ============================================================================ @@ -400,36 +412,28 @@ def validate_anthropic_response(json_str: str) -> Any: __all__ = [ # Error handling "ConversionError", - # Chat Completions API conversions "chat_completions_messages_to_lingua", "lingua_to_chat_completions_messages", - # Responses API conversions "responses_messages_to_lingua", "lingua_to_responses_messages", - # Anthropic conversions "anthropic_messages_to_lingua", "lingua_to_anthropic_messages", - # Processing functions "deduplicate_messages", "import_messages_from_spans", "import_and_deduplicate_messages", - # Chat Completions validation "validate_chat_completions_request", "validate_chat_completions_response", - # Responses API validation "validate_responses_request", "validate_responses_response", - # OpenAI validation (deprecated - use Chat Completions or Responses instead) "validate_openai_request", "validate_openai_response", - # Anthropic validation "validate_anthropic_request", "validate_anthropic_response", diff --git a/bindings/python/src/lingua/__init__.pyi b/bindings/python/src/lingua/__init__.pyi index adb80fd0..62712c5d 100644 --- a/bindings/python/src/lingua/__init__.pyi +++ b/bindings/python/src/lingua/__init__.pyi @@ -1,6 +1,7 @@ """Type stubs for Lingua Python bindings""" -from typing import Any, Dict +from typing import Any + # ============================================================================ # Error types @@ -10,48 +11,82 @@ class ConversionError(Exception): """Error during format conversion""" ... + # ============================================================================ # Chat Completions API conversions # ============================================================================ -def chat_completions_messages_to_lingua(messages: list) -> list: ... -def lingua_to_chat_completions_messages(messages: list) -> list: ... +def chat_completions_messages_to_lingua(messages: list) -> list: + ... + + +def lingua_to_chat_completions_messages(messages: list) -> list: + ... + # ============================================================================ # Responses API conversions # ============================================================================ -def responses_messages_to_lingua(messages: list) -> list: ... -def lingua_to_responses_messages(messages: list) -> list: ... +def responses_messages_to_lingua(messages: list) -> list: + ... + + +def lingua_to_responses_messages(messages: list) -> list: + ... + # ============================================================================ # Anthropic conversions # ============================================================================ -def anthropic_messages_to_lingua(messages: list) -> list: ... -def lingua_to_anthropic_messages(messages: list) -> list: ... +def anthropic_messages_to_lingua(messages: list) -> list: + ... + + +def lingua_to_anthropic_messages(messages: list) -> list: + ... + # ============================================================================ # Processing functions # ============================================================================ -def deduplicate_messages(messages: list) -> list: ... -def import_messages_from_spans(spans: list) -> list: ... -def import_and_deduplicate_messages(spans: list) -> list: ... +def deduplicate_messages(messages: list) -> list: + ... + + +def import_messages_from_spans(spans: list) -> list: + ... + + +def import_and_deduplicate_messages(spans: list) -> list: + ... + # ============================================================================ # OpenAI validation # ============================================================================ -def validate_openai_request(json_str: str) -> Any: ... -def validate_openai_response(json_str: str) -> Any: ... +def validate_openai_request(json_str: str) -> Any: + ... + + +def validate_openai_response(json_str: str) -> Any: + ... + # ============================================================================ # Anthropic validation # ============================================================================ -def validate_anthropic_request(json_str: str) -> Any: ... -def validate_anthropic_response(json_str: str) -> Any: ... +def validate_anthropic_request(json_str: str) -> Any: + ... + + +def validate_anthropic_response(json_str: str) -> Any: + ... + # ============================================================================ # Exports diff --git a/bindings/python/tests/test_roundtrip.py b/bindings/python/tests/test_roundtrip.py index dab32b0d..c88ee3e7 100644 --- a/bindings/python/tests/test_roundtrip.py +++ b/bindings/python/tests/test_roundtrip.py @@ -31,9 +31,9 @@ def __init__( name: str, provider: str, turn: str, - request: Optional[Dict] = None, - response: Optional[Dict] = None, - streaming_response: Optional[List] = None, + request: dict | None = None, + response: dict | None = None, + streaming_response: list | None = None, ): self.name = name self.provider = provider @@ -43,12 +43,17 @@ def __init__( self.streaming_response = streaming_response -def load_test_snapshots(test_case_name: str) -> List[Snapshot]: +def load_test_snapshots(test_case_name: str) -> list[Snapshot]: """Load all snapshots for a given test case""" - snapshots: List[Snapshot] = [] + snapshots: list[Snapshot] = [] # Snapshots are in the payloads directory (3 levels up from tests/) - snapshots_dir = Path(__file__).parent.parent.parent.parent / "payloads" / "snapshots" / test_case_name + snapshots_dir = ( + Path(__file__).parent.parent.parent.parent + / "payloads" + / "snapshots" + / test_case_name + ) if not snapshots_dir.exists(): return snapshots @@ -95,8 +100,7 @@ def load_test_snapshots(test_case_name: str) -> List[Snapshot]: # Try newline-delimited JSON lines = [ json.loads(line) - for line in content.split("\n") - if line.strip() + for line in content.split("\n") if line.strip() ] snapshot_data["streaming_response"] = lines @@ -119,7 +123,9 @@ def normalize_for_comparison(obj: Any) -> Any: if isinstance(obj, list): # Remove None from arrays and recursively normalize - normalized = [normalize_for_comparison(item) for item in obj if item is not None] + normalized = [ + normalize_for_comparison(item) for item in obj if item is not None + ] # Return None for empty arrays to remove them return normalized if normalized else None @@ -138,7 +144,7 @@ def normalize_for_comparison(obj: Any) -> Any: return obj -def perform_openai_roundtrip(openai_message: Dict) -> Dict[str, Any]: +def perform_openai_roundtrip(openai_message: dict) -> dict[str, Any]: """ Perform roundtrip conversion: Chat Completions -> Lingua -> Chat Completions @@ -154,10 +160,14 @@ def perform_openai_roundtrip(openai_message: Dict) -> Dict[str, Any]: lingua_msg = chat_completions_messages_to_lingua([openai_message])[0] roundtripped = lingua_to_chat_completions_messages([lingua_msg])[0] - return {"original": openai_message, "lingua": lingua_msg, "roundtripped": roundtripped} + return { + "original": openai_message, + "lingua": lingua_msg, + "roundtripped": roundtripped, + } -def perform_anthropic_roundtrip(anthropic_message: Dict) -> Dict[str, Any]: +def perform_anthropic_roundtrip(anthropic_message: dict) -> dict[str, Any]: """ Perform roundtrip conversion: Anthropic -> Lingua -> Anthropic @@ -217,7 +227,10 @@ def test_openai_roundtrips(self, test_cases): snapshots = load_test_snapshots(test_case) for snapshot in snapshots: - if snapshot.provider != "openai-chat-completions" or not snapshot.request: + if ( + snapshot.provider != "openai-chat-completions" + or not snapshot.request + ): continue messages = snapshot.request.get("messages", []) @@ -305,8 +318,8 @@ def test_coverage(self, test_cases): for test_case in test_cases: snapshots = load_test_snapshots(test_case) - providers = list(set(s.provider for s in snapshots)) - turns = list(set(s.turn for s in snapshots)) + providers = list({s.provider for s in snapshots}) + turns = list({s.turn for s in snapshots}) coverage[test_case] = {"providers": providers, "turns": turns} From dfa866fa844217307e16a5a8ad45c0b14de8ed23 Mon Sep 17 00:00:00 2001 From: Olmo Maldonado Date: Fri, 26 Dec 2025 18:16:47 -0800 Subject: [PATCH 2/2] Modernize Python 3.10+ syntax - clean up imports - Removed unused Dict, List, Optional from imports - Type annotations already modernized by pyupgrade --- bindings/python/tests/test_roundtrip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/python/tests/test_roundtrip.py b/bindings/python/tests/test_roundtrip.py index c88ee3e7..37710f7e 100644 --- a/bindings/python/tests/test_roundtrip.py +++ b/bindings/python/tests/test_roundtrip.py @@ -10,7 +10,7 @@ import json import os from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any import pytest