Skip to content

Commit 07090b2

Browse files
fix: let copied credentials replace static auth
1 parent 9aa85c8 commit 07090b2

2 files changed

Lines changed: 71 additions & 5 deletions

File tree

src/anthropic/_client.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def __init__(
172172
_token_cache: TokenCache | None | NotGiven = not_given,
173173
) -> None:
174174
"""Construct a new synchronous Anthropic client instance.
175-
175+
176176
Credentials are resolved in the following order (first match wins):
177177
178178
1. Explicit constructor arguments — ``api_key=``, ``auth_token=``,
@@ -456,6 +456,19 @@ def copy(
456456

457457
http_client = http_client or self._client
458458
# --- credentials support (hand-written, upstream to Stainless) ---
459+
replaces_static_auth = (
460+
(not isinstance(credentials, NotGiven) and credentials is not None)
461+
or config is not None
462+
or profile is not None
463+
)
464+
copied_api_key = api_key if api_key is not None else self.api_key
465+
copied_auth_token = auth_token if auth_token is not None else self.auth_token
466+
if replaces_static_auth:
467+
if api_key is None:
468+
copied_api_key = None
469+
if auth_token is None:
470+
copied_auth_token = None
471+
459472
if config is not None:
460473
if not isinstance(credentials, NotGiven) or profile is not None:
461474
raise TypeError("Pass at most one of `credentials=`, `config=`, or `profile=`.")
@@ -475,8 +488,8 @@ def copy(
475488
_extra_kwargs = {"_token_cache": self._token_cache, **_extra_kwargs}
476489
# --- end credentials support ---
477490
return self.__class__(
478-
api_key=api_key or self.api_key,
479-
auth_token=auth_token or self.auth_token,
491+
api_key=copied_api_key,
492+
auth_token=copied_auth_token,
480493
webhook_key=webhook_key or self.webhook_key,
481494
base_url=base_url or self.base_url,
482495
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
@@ -854,6 +867,19 @@ def copy(
854867

855868
http_client = http_client or self._client
856869
# --- credentials support (hand-written, upstream to Stainless) ---
870+
replaces_static_auth = (
871+
(not isinstance(credentials, NotGiven) and credentials is not None)
872+
or config is not None
873+
or profile is not None
874+
)
875+
copied_api_key = api_key if api_key is not None else self.api_key
876+
copied_auth_token = auth_token if auth_token is not None else self.auth_token
877+
if replaces_static_auth:
878+
if api_key is None:
879+
copied_api_key = None
880+
if auth_token is None:
881+
copied_auth_token = None
882+
857883
if config is not None:
858884
if not isinstance(credentials, NotGiven) or profile is not None:
859885
raise TypeError("Pass at most one of `credentials=`, `config=`, or `profile=`.")
@@ -873,8 +899,8 @@ def copy(
873899
_extra_kwargs = {"_token_cache": self._token_cache, **_extra_kwargs}
874900
# --- end credentials support ---
875901
return self.__class__(
876-
api_key=api_key or self.api_key,
877-
auth_token=auth_token or self.auth_token,
902+
api_key=copied_api_key,
903+
auth_token=copied_auth_token,
878904
webhook_key=webhook_key or self.webhook_key,
879905
base_url=base_url or self.base_url,
880906
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,

tests/lib/test_credentials.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2948,6 +2948,18 @@ def _walk_sync_auth(client: Anthropic) -> httpx.Request:
29482948
pass
29492949
return modified
29502950

2951+
@staticmethod
2952+
async def _walk_async_auth(client: AsyncAnthropic) -> httpx.Request:
2953+
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
2954+
auth = client.custom_auth
2955+
if auth is None:
2956+
return request
2957+
2958+
async for modified in auth.async_auth_flow(request):
2959+
return modified
2960+
2961+
raise AssertionError("async auth flow did not yield a request")
2962+
29512963
# -- step 1 beats step 2: explicit credentials= beats env static --------
29522964

29532965
def test_explicit_credentials_beats_env_api_key(
@@ -3044,6 +3056,34 @@ def test_copy_with_explicit_api_key_shadows_inherited_credentials(self, caplog:
30443056
assert req.headers.get("Authorization") is None
30453057
assert any("`api_key=`" in r.message for r in caplog.records)
30463058

3059+
def test_copy_with_credentials_replaces_inherited_api_key(self, caplog: pytest.LogCaptureFixture) -> None:
3060+
parent = Anthropic(api_key="sk-parent")
3061+
with caplog.at_level(logging.WARNING, logger="anthropic.lib.credentials._auth"):
3062+
copied = parent.copy(credentials=StaticToken("bearer-child"))
3063+
3064+
assert copied.api_key is None
3065+
assert copied.credentials is not None
3066+
req = self._walk_sync_auth(copied)
3067+
assert req.headers.get("X-Api-Key") is None
3068+
assert req.headers.get("Authorization") == "Bearer bearer-child"
3069+
assert not any("takes precedence" in r.message for r in caplog.records)
3070+
3071+
async def test_async_copy_with_credentials_replaces_inherited_api_key(
3072+
self, caplog: pytest.LogCaptureFixture
3073+
) -> None:
3074+
parent = AsyncAnthropic(api_key="sk-parent")
3075+
with caplog.at_level(logging.WARNING, logger="anthropic.lib.credentials._auth"):
3076+
copied = parent.copy(credentials=StaticToken("bearer-child"))
3077+
3078+
assert copied.api_key is None
3079+
assert copied.credentials is not None
3080+
req = await self._walk_async_auth(copied)
3081+
assert req.headers.get("X-Api-Key") is None
3082+
assert req.headers.get("Authorization") == "Bearer bearer-child"
3083+
assert not any("takes precedence" in r.message for r in caplog.records)
3084+
await parent.close()
3085+
await copied.close()
3086+
30473087
# -- step 2 shadows steps 3-5: env static shadows auto-discovery ---------
30483088

30493089
def test_env_api_key_shadows_env_federation_trio_with_warning(

0 commit comments

Comments
 (0)