Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions python/.github/skills/python-package-management/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,27 @@ Every new package starts as `alpha`.
8. Add the package to `python/PACKAGE_STATUS.md` and keep that file updated when packages are added,
removed, renamed, or promoted. If the package exposes individually staged APIs, keep the feature list
there current too.
9. Keep the `Typing :: Typed` classifier and the `py.typed` marker in step. The classifier alone is a
promise PEP 561 keeps through the `py.typed` file inside the package directory, and that file is
the signal checkers act on: a package that declares `Typing :: Typed` without shipping it is not
treated as typed, so its API falls back to whatever a checker does with an untyped import
(`Any`, `Unknown`, or a missing-stub diagnostic) instead of the inline annotations. Check both,
not one:

```bash
# every package that claims to be typed must ship the marker
cd python/packages
for d in */; do
d=${d%/}
[ -f "$d/pyproject.toml" ] || continue
if grep -q "Typing :: Typed" "$d/pyproject.toml" && ! ls "$d"/agent_framework*/py.typed >/dev/null 2>&1; then
echo "missing py.typed: $d"
fi
done
```

Note that `py.typed` only makes annotations that are already there visible; it adds none, so a
package that has not been type-checked may surface further errors once the marker lands.

Recommended dependency workflow during connector implementation:

Expand Down
Empty file.
Empty file.
10 changes: 5 additions & 5 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,7 @@ async def create(**kwargs: Any) -> BetaMessage:

agent = Agent(
client=AnthropicClient(anthropic_client=transport, model="claude-3-5-sonnet-20241022"),
default_options=cast(
default_options=cast( # type: ignore[arg-type]
AnthropicChatOptions,
{"model": "claude-3-5-sonnet-20241022", "max_tokens": 64, "instructions": system_blocks},
),
Expand Down Expand Up @@ -1961,7 +1961,7 @@ async def mock_stream():
chat_options = ChatOptions(max_tokens=10)

chunks: list[ChatResponseUpdate] = []
async for chunk in client._inner_get_response( # type: ignore[attr-defined] # ty: ignore[not-iterable]
async for chunk in client._inner_get_response( # type: ignore[attr-defined, union-attr] # ty: ignore[not-iterable]
messages=messages, options=chat_options, stream=True
):
if chunk:
Expand All @@ -1987,7 +1987,7 @@ async def mock_stream():
messages = [Message(role="user", contents=["Hi"])]
options: dict[str, Any] = {"max_tokens": 10, "stream": False}

async for _ in client._inner_get_response( # type: ignore[attr-defined] # ty: ignore[not-iterable]
async for _ in client._inner_get_response( # type: ignore[attr-defined, union-attr] # ty: ignore[not-iterable]
messages=messages,
options=options,
stream=True,
Expand Down Expand Up @@ -2051,7 +2051,7 @@ async def test_inner_get_response_streaming_wraps_sdk_errors(mock_anthropic_clie
anthropic_sdk.AuthenticationError, 401, "invalid api key"
)
with pytest.raises(ChatClientInvalidAuthException, match="Anthropic"):
async for _ in client._inner_get_response( # type: ignore[attr-defined] # ty: ignore[not-iterable]
async for _ in client._inner_get_response( # type: ignore[attr-defined, union-attr] # ty: ignore[not-iterable]
messages=messages, options=chat_options, stream=True
):
pass
Expand All @@ -2066,7 +2066,7 @@ async def _raise_after_first_event() -> Any:
mock_anthropic_client.beta.messages.create.side_effect = None
mock_anthropic_client.beta.messages.create.return_value = _raise_after_first_event()
with pytest.raises(ChatClientInvalidAuthException, match="Anthropic"):
async for _ in client._inner_get_response( # type: ignore[attr-defined] # ty: ignore[not-iterable]
async for _ in client._inner_get_response( # type: ignore[attr-defined, union-attr] # ty: ignore[not-iterable]
messages=messages, options=chat_options, stream=True
):
pass
Expand Down
Empty file.
8 changes: 6 additions & 2 deletions python/packages/bedrock/tests/test_bedrock_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
from boto3.session import Session as Boto3Session
from botocore.client import BaseClient

from agent_framework_bedrock import BedrockChatClient, BedrockChatOptions, BedrockEmbeddingClient
from agent_framework_bedrock._chat_client import BedrockSettings
from agent_framework_bedrock import (
BedrockChatClient,
BedrockChatOptions,
BedrockEmbeddingClient,
BedrockSettings,
)
from agent_framework_bedrock._feature_usage import FeatureIndex


Expand Down
2 changes: 1 addition & 1 deletion python/packages/bedrock/tests/test_bedrock_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from agent_framework._settings import load_settings
from pydantic import BaseModel

from agent_framework_bedrock._chat_client import BedrockChatClient, BedrockSettings
from agent_framework_bedrock import BedrockChatClient, BedrockSettings


class _WeatherArgs(BaseModel):
Expand Down
Empty file.
Empty file.
Empty file.
26 changes: 9 additions & 17 deletions python/packages/declarative/tests/test_graph_executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1771,7 +1771,7 @@ def test_import_guard_exists(self):
engine = base_mod.Engine
assert engine is None or callable(engine)

def test_eval_raises_when_engine_unavailable(self):
def test_eval_raises_when_engine_unavailable(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""eval() should raise RuntimeError when Engine is None."""
import agent_framework_declarative._workflows._declarative_base as base_mod

Expand All @@ -1784,15 +1784,11 @@ def test_eval_raises_when_engine_unavailable(self):
state = DeclarativeWorkflowState(mock_state)
state.initialize({"name": "test"})

original_engine = base_mod.Engine
try:
base_mod.Engine = cast(Any, None)
with pytest.raises(RuntimeError, match="PowerFx is not available"):
state.eval("=Local.counter + 1")
finally:
base_mod.Engine = original_engine
monkeypatch.setattr(base_mod, "Engine", None)
with pytest.raises(RuntimeError, match="PowerFx is not available"):
state.eval("=Local.counter + 1")

def test_eval_passes_through_plain_strings_without_engine(self):
def test_eval_passes_through_plain_strings_without_engine(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Non-PowerFx strings (no leading '=') should work without Engine."""
import agent_framework_declarative._workflows._declarative_base as base_mod

Expand All @@ -1805,14 +1801,10 @@ def test_eval_passes_through_plain_strings_without_engine(self):
state = DeclarativeWorkflowState(mock_state)
state.initialize()

original_engine = base_mod.Engine
try:
base_mod.Engine = cast(Any, None)
assert state.eval("hello world") == "hello world"
assert state.eval("") == ""
assert state.eval(cast("str", 42)) == 42
finally:
base_mod.Engine = original_engine
monkeypatch.setattr(base_mod, "Engine", None)
assert state.eval("hello world") == "hello world"
assert state.eval("") == ""
assert state.eval(cast("str", 42)) == 42


class TestExecutorKwargsForwarding:
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ async def foundry_responses_boundary(request: Any) -> Any:
"Keep the final answer to one short sentence."
),
tools=[learn_mcp],
default_options={ # pyrefly: ignore[bad-argument-type]
default_options={ # type: ignore[arg-type] # pyrefly: ignore[bad-argument-type]
"store": False,
"reasoning": {"effort": "low", "summary": "auto"},
"include": ["reasoning.encrypted_content"],
Expand Down
8 changes: 4 additions & 4 deletions python/packages/foundry_hosting/tests/test_state_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,19 +291,19 @@ async def test_delete_reports_whether_checkpoint_existed(deleted_id: str | None,
store.delete_item.assert_awaited_once_with("checkpoint-1", call_id="call-1")


async def test_get_latest_uses_timestamp_and_list_ids_filters() -> None:
async def test_get_latest_uses_timestamp_and_list_ids_filters(monkeypatch: pytest.MonkeyPatch) -> None:
storage = FoundryCheckpointStore("context-1", _platform_context())
older = _checkpoint("older", timestamp="2026-01-01T00:00:00+00:00")
newer = _checkpoint("newer", timestamp="2026-01-02T00:00:00+00:00")
storage.list_checkpoints = AsyncMock(return_value=[newer, older]) # zuban:ignore
monkeypatch.setattr(storage, "list_checkpoints", AsyncMock(return_value=[newer, older]))

assert await storage.get_latest(workflow_name="workflow") == newer
assert await storage.list_checkpoint_ids(workflow_name="workflow") == ["newer", "older"]


async def test_get_latest_returns_none_when_no_checkpoints_exist() -> None:
async def test_get_latest_returns_none_when_no_checkpoints_exist(monkeypatch: pytest.MonkeyPatch) -> None:
storage = FoundryCheckpointStore("context-1", _platform_context())
storage.list_checkpoints = AsyncMock(return_value=[]) # zuban:ignore
monkeypatch.setattr(storage, "list_checkpoints", AsyncMock(return_value=[]))

assert await storage.get_latest(workflow_name="workflow") is None

Expand Down
16 changes: 9 additions & 7 deletions python/packages/foundry_hosting/tests/test_toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,21 +285,22 @@ def test_sync_auth_flow_removes_call_id_from_reused_request_when_context_absent(
assert "x-agent-foundry-call-id" not in prepared.headers


async def test_close_closes_owned_http_client() -> None:
async def test_close_closes_owned_http_client(monkeypatch: pytest.MonkeyPatch) -> None:
toolbox = FoundryToolbox(
_FakeCredential(), # type: ignore
url="https://h/toolboxes/tb/mcp",
)
client = toolbox._httpx_client
assert client is not None
client.aclose = AsyncMock() # zuban: ignore
aclose = AsyncMock()
monkeypatch.setattr(client, "aclose", aclose)

await toolbox.close()

client.aclose.assert_awaited_once()
aclose.assert_awaited_once()
# Idempotent: a second close does not re-close the client.
await toolbox.close()
client.aclose.assert_awaited_once()
aclose.assert_awaited_once()


def test_as_skills_provider_returns_provider() -> None:
Expand Down Expand Up @@ -540,7 +541,7 @@ def test_as_skills_provider_forwards_only_set_archive_options() -> None:


class TestFoundryToolboxReconnection:
async def test_close_preserves_credential_for_reconnection(self) -> None:
async def test_close_preserves_credential_for_reconnection(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""After close(), get_mcp_client() should recreate an authenticated client."""
cred = _FakeCredential("reconnect-token")
toolbox = FoundryToolbox(
Expand All @@ -558,10 +559,11 @@ async def test_close_preserves_credential_for_reconnection(self) -> None:
original_auth = toolbox._httpx_client.auth

client = toolbox._httpx_client
client.aclose = AsyncMock() # zuban: ignore
aclose = AsyncMock()
monkeypatch.setattr(client, "aclose", aclose)
await toolbox.close()

client.aclose.assert_awaited_once()
aclose.assert_awaited_once()
assert toolbox._httpx_client is None

assert toolbox._credential is cred
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,20 @@ async def _redis_result(value: Awaitable[_T] | _T) -> _T:
"""Await a redis-py command result that is annotated as the sync/async union.

Several redis-py commands are annotated as returning ``Awaitable[T] | T`` even on the asyncio
client, so awaiting them directly does not type-check. Newer redis-py releases narrow those
annotations to the awaitable alone, which makes a bare ``# type: ignore`` *required* on the
older annotations and *unnecessary* on the newer ones: no single ignore comment satisfies the
whole supported range. Normalising through this helper type-checks on every supported version
without an ignore comment.
client -- ``llen`` as ``int | Awaitable[int]``, ``ltrim`` as ``bool | Awaitable[bool]`` -- so
awaiting them directly does not type-check. Normalising through this helper does.

This takes one signature rather than overloads on purpose. An overload pair of
``Awaitable[_T]`` and ``_T`` makes a checker bind ``_T`` to the whole union for an
``Awaitable[T] | T`` argument: the ``Awaitable[_T]`` arm matches, and nothing consumes the
other one. The awaited value then types as the union, so a ``int`` annotation on the result is
rejected even though the runtime value is an ``int``. Listing the union arm first does not help
-- it then shadows the bare-awaitable arm. The single union parameter resolves to the awaited
type, which is what every call site here wants.

The one shape it does not cover is a parameter annotated as a bare ``Awaitable[T]`` with no
``| T``. redis-py does not use that on the asyncio client as of 8.1, so no call site needs it;
if one appears, widen this signature rather than reaching for an overload pair.
"""
if isawaitable(value):
return cast("_T", await value)
Expand Down
Empty file.
Loading