From 820fda3bce6247484f5f1a464fe95577198949f6 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 28 Apr 2026 05:01:32 -0300 Subject: [PATCH 1/3] feat: update X-Client-Info to use structured semicolon-delimited metadata Consolidate platform and runtime metadata into the X-Client-Info header using semicolon-delimited key=value pairs instead of separate standalone headers. New format: X-Client-Info: supabase-py/ v; platform=; platform-version=; runtime=python; runtime-version= This avoids adding new headers that would be breaking changes in Edge Functions (where CORS allowed-headers must be explicitly listed), while keeping all metadata within the already-allowed X-Client-Info header. Removes: X-Supabase-Client-Platform, X-Supabase-Client-Platform-Version, X-Supabase-Client-Runtime, X-Supabase-Client-Runtime-Version Linear: SDK-904 Co-Authored-By: Claude Sonnet 4.6 --- .../src/supabase_auth/_async/gotrue_client.py | 12 +++-- .../src/supabase_auth/_sync/gotrue_client.py | 12 +++-- .../_async/functions_client.py | 12 +++-- .../_sync/functions_client.py | 12 +++-- .../tests/_async/test_function_client.py | 7 +-- .../tests/_sync/test_function_client.py | 7 +-- src/functions/tests/test_client.py | 52 +++++++++++++++++++ src/postgrest/src/postgrest/_async/client.py | 12 +++-- src/postgrest/src/postgrest/_sync/client.py | 12 +++-- src/postgrest/tests/_async/test_client.py | 20 +++++++ src/postgrest/tests/_sync/test_client.py | 18 +++++++ src/storage/src/storage3/_async/client.py | 12 +++-- src/storage/src/storage3/_sync/client.py | 12 +++-- src/storage/tests/test_client.py | 44 ++++++++++++++++ .../src/supabase/lib/client_options.py | 11 +++- 15 files changed, 208 insertions(+), 47 deletions(-) diff --git a/src/auth/src/supabase_auth/_async/gotrue_client.py b/src/auth/src/supabase_auth/_async/gotrue_client.py index 4048f93a..5ee1670b 100644 --- a/src/auth/src/supabase_auth/_async/gotrue_client.py +++ b/src/auth/src/supabase_auth/_async/gotrue_client.py @@ -113,11 +113,13 @@ def __init__( proxy: Optional[str] = None, ) -> None: extra_headers = { - "X-Client-Info": f"supabase-py/supabase_auth v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_auth v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), } if headers: extra_headers.update(headers) diff --git a/src/auth/src/supabase_auth/_sync/gotrue_client.py b/src/auth/src/supabase_auth/_sync/gotrue_client.py index 882950e7..063d1ddb 100644 --- a/src/auth/src/supabase_auth/_sync/gotrue_client.py +++ b/src/auth/src/supabase_auth/_sync/gotrue_client.py @@ -113,11 +113,13 @@ def __init__( proxy: Optional[str] = None, ) -> None: extra_headers = { - "X-Client-Info": f"supabase-py/supabase_auth v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_auth v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), } if headers: extra_headers.update(headers) diff --git a/src/functions/src/supabase_functions/_async/functions_client.py b/src/functions/src/supabase_functions/_async/functions_client.py index f7958deb..2f7529b1 100644 --- a/src/functions/src/supabase_functions/_async/functions_client.py +++ b/src/functions/src/supabase_functions/_async/functions_client.py @@ -29,11 +29,13 @@ def __init__( raise ValueError("url must be a valid HTTP URL string") self.url = URL(url) self.headers = { - "X-Client-Info": f"supabase-py/supabase_functions v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_functions v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/functions/src/supabase_functions/_sync/functions_client.py b/src/functions/src/supabase_functions/_sync/functions_client.py index 65268592..18f073d7 100644 --- a/src/functions/src/supabase_functions/_sync/functions_client.py +++ b/src/functions/src/supabase_functions/_sync/functions_client.py @@ -29,11 +29,13 @@ def __init__( raise ValueError("url must be a valid HTTP URL string") self.url = URL(url) self.headers = { - "X-Client-Info": f"supabase-py/supabase_functions v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_functions v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/functions/tests/_async/test_function_client.py b/src/functions/tests/_async/test_function_client.py index c991aed2..98b6ea9f 100644 --- a/src/functions/tests/_async/test_function_client.py +++ b/src/functions/tests/_async/test_function_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict from unittest.mock import AsyncMock, Mock, patch @@ -36,9 +37,9 @@ async def test_init_with_valid_params( ) assert str(client.url) == valid_url assert "X-Client-Info" in client.headers - assert ( - client.headers["X-Client-Info"] - == f"supabase-py/supabase_functions v{__version__}" + assert re.match( + rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + client.headers["X-Client-Info"], ) assert client._client.timeout == Timeout(10) diff --git a/src/functions/tests/_sync/test_function_client.py b/src/functions/tests/_sync/test_function_client.py index 469f7d36..e6c44c49 100644 --- a/src/functions/tests/_sync/test_function_client.py +++ b/src/functions/tests/_sync/test_function_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict from unittest.mock import Mock, patch @@ -36,9 +37,9 @@ def test_init_with_valid_params( ) assert str(client.url) == valid_url assert "X-Client-Info" in client.headers - assert ( - client.headers["X-Client-Info"] - == f"supabase-py/supabase_functions v{__version__}" + assert re.match( + rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + client.headers["X-Client-Info"], ) assert client._client.timeout == Timeout(10) diff --git a/src/functions/tests/test_client.py b/src/functions/tests/test_client.py index 80a200e4..0f00d4d8 100644 --- a/src/functions/tests/test_client.py +++ b/src/functions/tests/test_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict import pytest @@ -14,6 +15,57 @@ def valid_headers() -> Dict[str, str]: return {"Authorization": "Bearer test_token", "Content-Type": "application/json"} +_X_CLIENT_INFO_PATTERN = re.compile( + r"^supabase-py/supabase_functions v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$" +) +_SEPARATE_PLATFORM_HEADERS = [ + "x-supabase-client-platform", + "x-supabase-client-platform-version", + "x-supabase-client-runtime", + "x-supabase-client-runtime-version", +] + + +def test_async_x_client_info_structured_format( + valid_url: str, valid_headers: Dict[str, str] +) -> None: + client = AsyncFunctionsClient(url=valid_url, headers=valid_headers) + x_client_info = client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + +def test_sync_x_client_info_structured_format( + valid_url: str, valid_headers: Dict[str, str] +) -> None: + client = SyncFunctionsClient(url=valid_url, headers=valid_headers) + x_client_info = client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + +def test_async_no_separate_platform_headers( + valid_url: str, valid_headers: Dict[str, str] +) -> None: + client = AsyncFunctionsClient(url=valid_url, headers=valid_headers) + headers = {k.lower(): v for k, v in client.headers.items()} + for header in _SEPARATE_PLATFORM_HEADERS: + assert header not in headers, f"Unexpected header present: {header}" + + +def test_sync_no_separate_platform_headers( + valid_url: str, valid_headers: Dict[str, str] +) -> None: + client = SyncFunctionsClient(url=valid_url, headers=valid_headers) + headers = {k.lower(): v for k, v in client.headers.items()} + for header in _SEPARATE_PLATFORM_HEADERS: + assert header not in headers, f"Unexpected header present: {header}" + + def test_create_async_client(valid_url: str, valid_headers: Dict[str, str]) -> None: # Test creating async client with explicit verify=True client = create_client( diff --git a/src/postgrest/src/postgrest/_async/client.py b/src/postgrest/src/postgrest/_async/client.py index 00962e0b..b85e5895 100644 --- a/src/postgrest/src/postgrest/_async/client.py +++ b/src/postgrest/src/postgrest/_async/client.py @@ -38,11 +38,13 @@ def __init__( http_client: Optional[AsyncClient] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/postgrest-py v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/postgrest-py v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/postgrest/src/postgrest/_sync/client.py b/src/postgrest/src/postgrest/_sync/client.py index 14f48ea7..58b2e8b0 100644 --- a/src/postgrest/src/postgrest/_sync/client.py +++ b/src/postgrest/src/postgrest/_sync/client.py @@ -38,11 +38,13 @@ def __init__( http_client: Optional[Client] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/postgrest-py v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/postgrest-py v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/postgrest/tests/_async/test_client.py b/src/postgrest/tests/_async/test_client.py index 349a0345..3d8491f4 100644 --- a/src/postgrest/tests/_async/test_client.py +++ b/src/postgrest/tests/_async/test_client.py @@ -1,3 +1,4 @@ +import re from unittest.mock import patch import pytest @@ -22,6 +23,25 @@ async def postgrest_client(): yield client +class TestXClientInfo: + def test_structured_metadata_format(self, postgrest_client: AsyncPostgrestClient): + x_client_info = postgrest_client.session.headers.get("X-Client-Info") + assert x_client_info is not None + assert re.match( + r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + x_client_info, + ), f"X-Client-Info format is wrong: {x_client_info}" + + def test_no_separate_platform_headers( + self, postgrest_client: AsyncPostgrestClient + ): + headers = dict(postgrest_client.session.headers) + assert "x-supabase-client-platform" not in headers + assert "x-supabase-client-platform-version" not in headers + assert "x-supabase-client-runtime" not in headers + assert "x-supabase-client-runtime-version" not in headers + + class TestConstructor: def test_simple(self, postgrest_client: AsyncPostgrestClient): session = postgrest_client.session diff --git a/src/postgrest/tests/_sync/test_client.py b/src/postgrest/tests/_sync/test_client.py index 9a7f117c..b5fb9ad6 100644 --- a/src/postgrest/tests/_sync/test_client.py +++ b/src/postgrest/tests/_sync/test_client.py @@ -1,3 +1,4 @@ +import re from unittest.mock import patch import pytest @@ -22,6 +23,23 @@ def postgrest_client(): yield client +class TestXClientInfo: + def test_structured_metadata_format(self, postgrest_client: SyncPostgrestClient): + x_client_info = postgrest_client.session.headers.get("X-Client-Info") + assert x_client_info is not None + assert re.match( + r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + x_client_info, + ), f"X-Client-Info format is wrong: {x_client_info}" + + def test_no_separate_platform_headers(self, postgrest_client: SyncPostgrestClient): + headers = dict(postgrest_client.session.headers) + assert "x-supabase-client-platform" not in headers + assert "x-supabase-client-platform-version" not in headers + assert "x-supabase-client-runtime" not in headers + assert "x-supabase-client-runtime-version" not in headers + + class TestConstructor: def test_simple(self, postgrest_client: SyncPostgrestClient): session = postgrest_client.session diff --git a/src/storage/src/storage3/_async/client.py b/src/storage/src/storage3/_async/client.py index 2caec5ef..322619d7 100644 --- a/src/storage/src/storage3/_async/client.py +++ b/src/storage/src/storage3/_async/client.py @@ -34,11 +34,13 @@ def __init__( http_client: Optional[AsyncClient] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/storage3 v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/storage3 v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/storage/src/storage3/_sync/client.py b/src/storage/src/storage3/_sync/client.py index 956ede79..e262d4dd 100644 --- a/src/storage/src/storage3/_sync/client.py +++ b/src/storage/src/storage3/_sync/client.py @@ -34,11 +34,13 @@ def __init__( http_client: Optional[Client] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/storage3 v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/storage3 v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/storage/tests/test_client.py b/src/storage/tests/test_client.py index 4d926cc6..83c39d78 100644 --- a/src/storage/tests/test_client.py +++ b/src/storage/tests/test_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict import pytest @@ -16,6 +17,49 @@ def valid_headers() -> Dict[str, str]: return {"Authorization": "Bearer test_token", "apikey": "test_api_key"} +_X_CLIENT_INFO_PATTERN = re.compile( + r"^supabase-py/storage3 v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$" +) +_SEPARATE_PLATFORM_HEADERS = [ + "x-supabase-client-platform", + "x-supabase-client-platform-version", + "x-supabase-client-runtime", + "x-supabase-client-runtime-version", +] + + +def test_async_x_client_info_structured_format(valid_url, valid_headers) -> None: + client = AsyncStorageClient(url=valid_url, headers=valid_headers) + x_client_info = client._client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + +def test_sync_x_client_info_structured_format(valid_url, valid_headers) -> None: + client = SyncStorageClient(url=valid_url, headers=valid_headers) + x_client_info = client._client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + +def test_async_no_separate_platform_headers(valid_url, valid_headers) -> None: + client = AsyncStorageClient(url=valid_url, headers=valid_headers) + headers = {k.lower(): v for k, v in client._client.headers.items()} + for header in _SEPARATE_PLATFORM_HEADERS: + assert header not in headers, f"Unexpected header present: {header}" + + +def test_sync_no_separate_platform_headers(valid_url, valid_headers) -> None: + client = SyncStorageClient(url=valid_url, headers=valid_headers) + headers = {k.lower(): v for k, v in client._client.headers.items()} + for header in _SEPARATE_PLATFORM_HEADERS: + assert header not in headers, f"Unexpected header present: {header}" + + def test_create_async_client(valid_url, valid_headers) -> None: client = AsyncStorageClient(url=valid_url, headers=valid_headers) diff --git a/src/supabase/src/supabase/lib/client_options.py b/src/supabase/src/supabase/lib/client_options.py index 44450c0e..99ea1b42 100644 --- a/src/supabase/src/supabase/lib/client_options.py +++ b/src/supabase/src/supabase/lib/client_options.py @@ -1,3 +1,4 @@ +import platform from dataclasses import dataclass, field from typing import Dict, Optional, Union @@ -19,7 +20,15 @@ from ..version import __version__ -DEFAULT_HEADERS = {"X-Client-Info": f"supabase-py/{__version__}"} +DEFAULT_HEADERS = { + "X-Client-Info": ( + f"supabase-py/{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ) +} @dataclass From 66c60afafee57eeaa3f2610662857940c99e6f8c Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 28 Apr 2026 05:04:21 -0300 Subject: [PATCH 2/3] test: remove absence-of-separate-headers assertions Co-Authored-By: Claude Sonnet 4.6 --- src/functions/tests/test_client.py | 24 ----------------------- src/postgrest/tests/_async/test_client.py | 9 --------- src/postgrest/tests/_sync/test_client.py | 7 ------- src/storage/tests/test_client.py | 20 ------------------- 4 files changed, 60 deletions(-) diff --git a/src/functions/tests/test_client.py b/src/functions/tests/test_client.py index 0f00d4d8..f9ec4f0b 100644 --- a/src/functions/tests/test_client.py +++ b/src/functions/tests/test_client.py @@ -18,12 +18,6 @@ def valid_headers() -> Dict[str, str]: _X_CLIENT_INFO_PATTERN = re.compile( r"^supabase-py/supabase_functions v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$" ) -_SEPARATE_PLATFORM_HEADERS = [ - "x-supabase-client-platform", - "x-supabase-client-platform-version", - "x-supabase-client-runtime", - "x-supabase-client-runtime-version", -] def test_async_x_client_info_structured_format( @@ -48,24 +42,6 @@ def test_sync_x_client_info_structured_format( ), f"X-Client-Info format is wrong: {x_client_info}" -def test_async_no_separate_platform_headers( - valid_url: str, valid_headers: Dict[str, str] -) -> None: - client = AsyncFunctionsClient(url=valid_url, headers=valid_headers) - headers = {k.lower(): v for k, v in client.headers.items()} - for header in _SEPARATE_PLATFORM_HEADERS: - assert header not in headers, f"Unexpected header present: {header}" - - -def test_sync_no_separate_platform_headers( - valid_url: str, valid_headers: Dict[str, str] -) -> None: - client = SyncFunctionsClient(url=valid_url, headers=valid_headers) - headers = {k.lower(): v for k, v in client.headers.items()} - for header in _SEPARATE_PLATFORM_HEADERS: - assert header not in headers, f"Unexpected header present: {header}" - - def test_create_async_client(valid_url: str, valid_headers: Dict[str, str]) -> None: # Test creating async client with explicit verify=True client = create_client( diff --git a/src/postgrest/tests/_async/test_client.py b/src/postgrest/tests/_async/test_client.py index 3d8491f4..fa0b9f5a 100644 --- a/src/postgrest/tests/_async/test_client.py +++ b/src/postgrest/tests/_async/test_client.py @@ -32,15 +32,6 @@ def test_structured_metadata_format(self, postgrest_client: AsyncPostgrestClient x_client_info, ), f"X-Client-Info format is wrong: {x_client_info}" - def test_no_separate_platform_headers( - self, postgrest_client: AsyncPostgrestClient - ): - headers = dict(postgrest_client.session.headers) - assert "x-supabase-client-platform" not in headers - assert "x-supabase-client-platform-version" not in headers - assert "x-supabase-client-runtime" not in headers - assert "x-supabase-client-runtime-version" not in headers - class TestConstructor: def test_simple(self, postgrest_client: AsyncPostgrestClient): diff --git a/src/postgrest/tests/_sync/test_client.py b/src/postgrest/tests/_sync/test_client.py index b5fb9ad6..c4bc694d 100644 --- a/src/postgrest/tests/_sync/test_client.py +++ b/src/postgrest/tests/_sync/test_client.py @@ -32,13 +32,6 @@ def test_structured_metadata_format(self, postgrest_client: SyncPostgrestClient) x_client_info, ), f"X-Client-Info format is wrong: {x_client_info}" - def test_no_separate_platform_headers(self, postgrest_client: SyncPostgrestClient): - headers = dict(postgrest_client.session.headers) - assert "x-supabase-client-platform" not in headers - assert "x-supabase-client-platform-version" not in headers - assert "x-supabase-client-runtime" not in headers - assert "x-supabase-client-runtime-version" not in headers - class TestConstructor: def test_simple(self, postgrest_client: SyncPostgrestClient): diff --git a/src/storage/tests/test_client.py b/src/storage/tests/test_client.py index 83c39d78..73019b56 100644 --- a/src/storage/tests/test_client.py +++ b/src/storage/tests/test_client.py @@ -20,12 +20,6 @@ def valid_headers() -> Dict[str, str]: _X_CLIENT_INFO_PATTERN = re.compile( r"^supabase-py/storage3 v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$" ) -_SEPARATE_PLATFORM_HEADERS = [ - "x-supabase-client-platform", - "x-supabase-client-platform-version", - "x-supabase-client-runtime", - "x-supabase-client-runtime-version", -] def test_async_x_client_info_structured_format(valid_url, valid_headers) -> None: @@ -46,20 +40,6 @@ def test_sync_x_client_info_structured_format(valid_url, valid_headers) -> None: ), f"X-Client-Info format is wrong: {x_client_info}" -def test_async_no_separate_platform_headers(valid_url, valid_headers) -> None: - client = AsyncStorageClient(url=valid_url, headers=valid_headers) - headers = {k.lower(): v for k, v in client._client.headers.items()} - for header in _SEPARATE_PLATFORM_HEADERS: - assert header not in headers, f"Unexpected header present: {header}" - - -def test_sync_no_separate_platform_headers(valid_url, valid_headers) -> None: - client = SyncStorageClient(url=valid_url, headers=valid_headers) - headers = {k.lower(): v for k, v in client._client.headers.items()} - for header in _SEPARATE_PLATFORM_HEADERS: - assert header not in headers, f"Unexpected header present: {header}" - - def test_create_async_client(valid_url, valid_headers) -> None: client = AsyncStorageClient(url=valid_url, headers=valid_headers) From 3ffdfe278db94ab4ae468c869f48e784482a953d Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 28 Apr 2026 05:11:32 -0300 Subject: [PATCH 3/3] test: fix runtime-version regex to handle pre-release Python versions Python 3.14 reports version strings like '3.14.0b4', which broke [\d.]+ matches. Use \S+ to accept any non-whitespace version string. Co-Authored-By: Claude Sonnet 4.6 --- src/functions/tests/_async/test_function_client.py | 2 +- src/functions/tests/_sync/test_function_client.py | 2 +- src/functions/tests/test_client.py | 2 +- src/postgrest/tests/_async/test_client.py | 2 +- src/postgrest/tests/_sync/test_client.py | 2 +- src/storage/tests/test_client.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/functions/tests/_async/test_function_client.py b/src/functions/tests/_async/test_function_client.py index 98b6ea9f..a821deb8 100644 --- a/src/functions/tests/_async/test_function_client.py +++ b/src/functions/tests/_async/test_function_client.py @@ -38,7 +38,7 @@ async def test_init_with_valid_params( assert str(client.url) == valid_url assert "X-Client-Info" in client.headers assert re.match( - rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", client.headers["X-Client-Info"], ) assert client._client.timeout == Timeout(10) diff --git a/src/functions/tests/_sync/test_function_client.py b/src/functions/tests/_sync/test_function_client.py index e6c44c49..6be348df 100644 --- a/src/functions/tests/_sync/test_function_client.py +++ b/src/functions/tests/_sync/test_function_client.py @@ -38,7 +38,7 @@ def test_init_with_valid_params( assert str(client.url) == valid_url assert "X-Client-Info" in client.headers assert re.match( - rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", client.headers["X-Client-Info"], ) assert client._client.timeout == Timeout(10) diff --git a/src/functions/tests/test_client.py b/src/functions/tests/test_client.py index f9ec4f0b..2165929a 100644 --- a/src/functions/tests/test_client.py +++ b/src/functions/tests/test_client.py @@ -16,7 +16,7 @@ def valid_headers() -> Dict[str, str]: _X_CLIENT_INFO_PATTERN = re.compile( - r"^supabase-py/supabase_functions v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$" + r"^supabase-py/supabase_functions v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$" ) diff --git a/src/postgrest/tests/_async/test_client.py b/src/postgrest/tests/_async/test_client.py index fa0b9f5a..0940cbf1 100644 --- a/src/postgrest/tests/_async/test_client.py +++ b/src/postgrest/tests/_async/test_client.py @@ -28,7 +28,7 @@ def test_structured_metadata_format(self, postgrest_client: AsyncPostgrestClient x_client_info = postgrest_client.session.headers.get("X-Client-Info") assert x_client_info is not None assert re.match( - r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", x_client_info, ), f"X-Client-Info format is wrong: {x_client_info}" diff --git a/src/postgrest/tests/_sync/test_client.py b/src/postgrest/tests/_sync/test_client.py index c4bc694d..08955089 100644 --- a/src/postgrest/tests/_sync/test_client.py +++ b/src/postgrest/tests/_sync/test_client.py @@ -28,7 +28,7 @@ def test_structured_metadata_format(self, postgrest_client: SyncPostgrestClient) x_client_info = postgrest_client.session.headers.get("X-Client-Info") assert x_client_info is not None assert re.match( - r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$", + r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", x_client_info, ), f"X-Client-Info format is wrong: {x_client_info}" diff --git a/src/storage/tests/test_client.py b/src/storage/tests/test_client.py index 73019b56..67535f72 100644 --- a/src/storage/tests/test_client.py +++ b/src/storage/tests/test_client.py @@ -18,7 +18,7 @@ def valid_headers() -> Dict[str, str]: _X_CLIENT_INFO_PATTERN = re.compile( - r"^supabase-py/storage3 v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=[\d.]+$" + r"^supabase-py/storage3 v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$" )