Files
Eduard van Valkenburg 6e95517659 Python: Split type checkers by target (pyright source, 5 checkers on tests/samples) (#6443)
* Python: Split type checkers by target (pyright source, 5 checkers on tests/samples)

Rework the typing setup along the lines of the 'too many type checkers'
approach:

- Pyright (strict) is now the sole source-code type checker; mypy is
  removed from source and its [tool.mypy] block becomes a relaxed profile
  used only for tests/samples.
- Tests are checked by all five checkers (pyright relaxed, mypy, pyrefly,
  ty, zuban); samples by pyright, pyrefly, and ty. All run in a relaxed/
  basic profile so authors aren't forced into over-annotation.
- Add pyrightconfig.tests.json and bump sample pyright configs to basic.
- Unify test/sample typing onto the same parallel fan-out used by source
  pyright via run_command_items in task_runner.py.
- Make version-conditional imports symmetric: keep or drop the
  '# type: ignore' on both branches so results match across interpreter
  versions (local vs CI).
- Update SKILL.md, DEV_SETUP.md, and CODING_STANDARD.md for the five
  gating checkers and pyright on source+tests+samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix merge regressions from main (typing + runtime)

Merging main into the type-checker split branch surfaced regressions that
the new five-checker test suite and unit tests caught:

Runtime fixes:
- anthropic: restore the dropped `cache_read_input_token_count` mapping in
  _parse_usage_from_anthropic (lost during merge conflict resolution).
- gemini: _get_function_calling_mode test helper returned str(enum)
  ('FunctionCallingConfigMode.AUTO') instead of the enum value ('AUTO').
- openai: _response_id_from_token test helper was an infinite self-recursion;
  return token['response_id'].
- orchestrations: reset output_events per approval iteration so the terminal
  output assertion counts only the final run.
- core: drop a stale duplicate harness test whose message ('non-negative')
  contradicted the source ('positive').
- purview: import PolicyLocation/PolicyScope/ProtectionScopeActivities/
  ExecutionMode used by the processor tests.

Type-checker fixes (tests, relaxed profile):
- core: pyright/mypy/pyrefly/ty/zuban green-ups across the harness, MCP,
  observability and types tests.
- anthropic/openai: route provider-namespaced UsageDetails keys through a
  dict cast (extra_items TypedDict unsupported by mypy/ty).
- purview: typed model constructors and cache-mock casts.
- ag-ui: annotate WorkflowContext[Any, Any] so yield_output accepts test
  payloads, guard Optional forwarded_props, and ty-ignore intentional bad args.

Source pyright (sole source checker) flagged unnecessary ignores newly
introduced by merged code in core _tools.py and declarative _declarative_base.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Isolate per-package mypy cache in test-typing fan-out

The parallel test-typing fan-out runs many mypy processes concurrently,
all defaulting to a single shared ./.mypy_cache. Concurrent writes corrupt
the cache and mypy aborts with INTERNAL ERROR (intermittently, depending on
worker timing) -- which is why CI's Test Typing job failed on a shifting set
of packages while a single-package run was fine.

Give each mypy invocation an isolated cache dir keyed by its target paths so
incremental caching still works per package without races. Other checkers
(zuban/pyrefly/ty/pyright) maintain their own caches and are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Make lab pyright-only on source (drop source mypy)

Lab was the last package still running mypy on its source code, requiring
mypy-only `# type: ignore` comments that pyright (the sole source checker
everywhere else) flags as unnecessary. Align lab with the rest of the
monorepo:

- Remove the lab source mypy poe tasks (mypy-gaia/lightning/tau2) and the
  now-dead strict [tool.mypy] config block.
- Drop the 'Run lab mypy' CI step; lab source is type-checked by pyright only.

Lab tests remain covered by the workspace test-typing fan-out (mypy, pyrefly,
ty, zuban, pyright over tests using the relaxed root config).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix test-typing regressions from latest main merge

A fresh merge from main brought in new test code never run under the
five-checker test-typing suite. Green up across the affected packages:

- core: narrow Optional span.attributes with 'and' guards in span filters
  and assert+cast the json.loads(...attributes[...]) reads (test_observability);
  match the existing as_agent ignore on the protocol-typed fixture (test_clients).
- openai: align new streaming tests with the established chat_options dict
  pattern (ChatOptions TypedDict isn't assignable to dict), route Optional
  .annotations[0] access through a small _first_annotation helper (mirrors the
  file's assert-not-None convention), and annotate a mapped ResponseStream.
- foundry_hosting: annotate error: dict[str, Any] = body.get(...) or {}
  (zuban needs the annotation).
- foundry: narrow ignores for the live AIProjectClient credential arg (pyrefly)
  and connections.get_default (zuban) SDK type gaps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated pyright version

* pyright fix

* Python: Fix source typing for pyright 1.1.410

Pyright 1.1.410 tightened several checks. Apply the same source fixes as
upstream PR #6275:

- anthropic: import AsyncAnthropicBedrock from anthropic.lib.bedrock and
  AsyncAnthropicVertex from anthropic.lib.vertex (no longer re-exported from
  the anthropic top-level package -> reportPrivateImportUsage).
- core _types.py: cast the transform-hook result to UpdateT (reportAssignmentType).
- core _workflows/_events.py: annotate the @contextmanager helper as
  Generator[None] instead of Iterator[None] (reportDeprecated).
- redis: build the combined filter expression with an explicit loop instead of
  reduce(and_, ...), which pyright could no longer fully type (drops the now
  unused functools.reduce / operator.and_ imports).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Accept plain-text body in Azure Functions workflow/run endpoint

The workflow_orchestrator already accepts plain strings as well as JSON
objects via context.get_input(), but the start_workflow_orchestration HTTP
handler only accepted JSON and returned 400 for any non-JSON body. This made
the functions integration tests that POST text/plain to /api/workflow/run
(e.g. test_09_workflow_shared_state) fail consistently with 400 != 202.

Fall back to the raw request body (decoded as UTF-8) when the body is not
JSON, rejecting only a truly empty body. The JSON path is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 15:06:20 +00:00

683 lines
21 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Tests for custom PowerFx-like functions."""
from typing import cast
from agent_framework_declarative._workflows._powerfx_functions import (
CUSTOM_FUNCTIONS,
assistant_message,
concat_text,
count_rows,
find,
first,
is_blank,
last,
lower,
message_text,
search_table,
system_message,
upper,
user_message,
)
class TestMessageText:
"""Tests for MessageText function."""
def test_message_text_from_string(self):
"""Test extracting text from a plain string."""
assert message_text("Hello") == "Hello"
def test_message_text_from_single_dict(self):
"""Test extracting text from a single message dict."""
msg = {"role": "assistant", "content": "Hello world"}
assert message_text(msg) == "Hello world"
def test_message_text_from_list(self):
"""Test extracting text from a list of messages."""
msgs = [
{"role": "user", "content": "Hi"},
{"role": "assistant", "content": "Hello"},
]
assert message_text(msgs) == "Hi Hello"
def test_message_text_from_none(self):
"""Test that None returns empty string."""
assert message_text(None) == ""
def test_message_text_empty_list(self):
"""Test that empty list returns empty string."""
assert message_text([]) == ""
class TestUserMessage:
"""Tests for UserMessage function."""
def test_user_message_creates_dict(self):
"""Test that UserMessage creates correct dict."""
msg = user_message("Hello")
assert msg == {"role": "user", "content": "Hello"}
def test_user_message_with_none(self):
"""Test UserMessage with None."""
msg = user_message(cast("str", None))
assert msg == {"role": "user", "content": ""}
class TestAssistantMessage:
"""Tests for AssistantMessage function."""
def test_assistant_message_creates_dict(self):
"""Test that AssistantMessage creates correct dict."""
msg = assistant_message("Hello")
assert msg == {"role": "assistant", "content": "Hello"}
class TestSystemMessage:
"""Tests for SystemMessage function."""
def test_system_message_creates_dict(self):
"""Test that SystemMessage creates correct dict."""
msg = system_message("You are helpful")
assert msg == {"role": "system", "content": "You are helpful"}
class TestIsBlank:
"""Tests for IsBlank function."""
def test_is_blank_none(self):
"""Test that None is blank."""
assert is_blank(None) is True
def test_is_blank_empty_string(self):
"""Test that empty string is blank."""
assert is_blank("") is True
def test_is_blank_whitespace(self):
"""Test that whitespace-only string is blank."""
assert is_blank(" ") is True
def test_is_blank_empty_list(self):
"""Test that empty list is blank."""
assert is_blank([]) is True
def test_is_blank_non_empty(self):
"""Test that non-empty values are not blank."""
assert is_blank("hello") is False
assert is_blank([1, 2, 3]) is False
assert is_blank(0) is False
class TestCountRows:
"""Tests for CountRows function."""
def test_count_rows_list(self):
"""Test counting list items."""
assert count_rows([1, 2, 3]) == 3
def test_count_rows_empty(self):
"""Test counting empty list."""
assert count_rows([]) == 0
def test_count_rows_none(self):
"""Test counting None."""
assert count_rows(None) == 0
class TestFirstLast:
"""Tests for First and Last functions."""
def test_first_returns_first_item(self):
"""Test that First returns first item."""
assert first([1, 2, 3]) == 1
def test_last_returns_last_item(self):
"""Test that Last returns last item."""
assert last([1, 2, 3]) == 3
def test_first_empty_returns_none(self):
"""Test that First returns None for empty list."""
assert first([]) is None
def test_last_empty_returns_none(self):
"""Test that Last returns None for empty list."""
assert last([]) is None
class TestFind:
"""Tests for Find function."""
def test_find_substring(self):
"""Test finding a substring."""
result = find("world", "Hello world")
assert result == 7 # 1-based index
def test_find_not_found(self):
"""Test when substring not found - returns Blank (None) per PowerFx semantics."""
result = find("xyz", "Hello world")
assert result is None
def test_find_at_start(self):
"""Test finding at start of string."""
result = find("Hello", "Hello world")
assert result == 1
class TestUpperLower:
"""Tests for Upper and Lower functions."""
def test_upper(self):
"""Test uppercase conversion."""
assert upper("hello") == "HELLO"
def test_lower(self):
"""Test lowercase conversion."""
assert lower("HELLO") == "hello"
def test_upper_none(self):
"""Test upper with None."""
assert upper(None) == ""
class TestConcatText:
"""Tests for Concat function."""
def test_concat_simple_list(self):
"""Test concatenating simple list."""
assert concat_text(["a", "b", "c"], separator=", ") == "a, b, c"
def test_concat_with_field(self):
"""Test concatenating with field extraction."""
items = [{"name": "Alice"}, {"name": "Bob"}]
assert concat_text(items, field="name", separator=", ") == "Alice, Bob"
class TestSearchTable:
"""Tests for Search function."""
def test_search_finds_matching(self):
"""Test search finds matching items."""
items = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35},
]
result = search_table(items, "Bob", "name")
assert len(result) == 1
assert result[0]["name"] == "Bob"
def test_search_case_insensitive(self):
"""Test search is case insensitive."""
items = [{"name": "Alice"}]
result = search_table(items, "alice", "name")
assert len(result) == 1
def test_search_partial_match(self):
"""Test search finds partial matches."""
items = [{"name": "Alice Smith"}, {"name": "Bob Jones"}]
result = search_table(items, "Smith", "name")
assert len(result) == 1
class TestCustomFunctionsRegistry:
"""Tests for the CUSTOM_FUNCTIONS registry."""
def test_all_functions_registered(self):
"""Test that all functions are in the registry."""
expected = [
"MessageText",
"UserMessage",
"AssistantMessage",
"SystemMessage",
"IsBlank",
"CountRows",
"First",
"Last",
"Find",
"Upper",
"Lower",
"Concat",
"Search",
"If",
"Or",
"And",
"Not",
"AgentMessage",
"ForAll",
]
for name in expected:
assert name in CUSTOM_FUNCTIONS
class TestMessageTextEdgeCases:
"""Additional tests for message_text edge cases."""
def test_message_text_dict_with_text_attr_content(self):
"""Test message with content that has text attribute."""
class ContentWithText: # noqa: B903
def __init__(self, text: str):
self.text = text
msg = {"role": "assistant", "content": ContentWithText("Hello from text attr")}
assert message_text(msg) == "Hello from text attr"
def test_message_text_dict_content_non_string(self):
"""Test message with non-string content."""
msg = {"role": "assistant", "content": 42}
assert message_text(msg) == "42"
def test_message_text_list_with_string_items(self):
"""Test message_text with list of strings."""
result = message_text(["Hello", "World"])
assert result == "Hello World"
def test_message_text_list_with_content_objects(self):
"""Test message_text with list items having content attribute."""
class MessageObj: # noqa: B903
def __init__(self, content: str):
self.content = content
msgs = [MessageObj("Hello"), MessageObj("World")]
result = message_text(msgs)
assert result == "Hello World"
def test_message_text_list_with_content_text_attr(self):
"""Test message_text with content having text attribute."""
class ContentWithText: # noqa: B903
def __init__(self, text: str):
self.text = text
class MessageObj:
def __init__(self, content):
self.content = content
msgs = [MessageObj(ContentWithText("Part1")), MessageObj(ContentWithText("Part2"))]
result = message_text(msgs)
assert result == "Part1 Part2"
def test_message_text_list_with_non_string_content(self):
"""Test message_text with non-string content in dicts."""
msgs = [{"content": 123}, {"content": 456}]
result = message_text(msgs)
assert result == "123 456"
def test_message_text_object_with_text_attr(self):
"""Test message_text with object having text attribute."""
class ObjWithText:
text = "Direct text"
result = message_text(ObjWithText())
assert result == "Direct text"
def test_message_text_object_with_content_attr(self):
"""Test message_text with object having content attribute."""
class ObjWithContent:
content = "Direct content"
result = message_text(ObjWithContent())
assert result == "Direct content"
def test_message_text_object_with_non_string_content(self):
"""Test message_text with object having non-string content."""
class ObjWithContent:
content = None
result = message_text(ObjWithContent())
assert result == ""
def test_message_text_list_with_empty_content_object(self):
"""Test message with content object that evaluates to empty."""
class MessageObj:
content = None
result = message_text([MessageObj()])
assert result == ""
class TestAgentMessage:
"""Tests for agent_message function."""
def test_agent_message_creates_dict(self):
"""Test that AgentMessage creates correct dict."""
from agent_framework_declarative._workflows._powerfx_functions import agent_message
msg = agent_message("Hello")
assert msg == {"role": "assistant", "content": "Hello"}
def test_agent_message_with_none(self):
"""Test AgentMessage with None."""
from agent_framework_declarative._workflows._powerfx_functions import agent_message
msg = agent_message(cast("str", None))
assert msg == {"role": "assistant", "content": ""}
class TestIfFunc:
"""Tests for if_func conditional function."""
def test_if_true_condition(self):
"""Test If with true condition."""
from agent_framework_declarative._workflows._powerfx_functions import if_func
assert if_func(True, "yes", "no") == "yes"
def test_if_false_condition(self):
"""Test If with false condition."""
from agent_framework_declarative._workflows._powerfx_functions import if_func
assert if_func(False, "yes", "no") == "no"
def test_if_truthy_value(self):
"""Test If with truthy value."""
from agent_framework_declarative._workflows._powerfx_functions import if_func
assert if_func(1, "yes", "no") == "yes"
assert if_func("non-empty", "yes", "no") == "yes"
def test_if_falsy_value(self):
"""Test If with falsy value."""
from agent_framework_declarative._workflows._powerfx_functions import if_func
assert if_func(0, "yes", "no") == "no"
assert if_func("", "yes", "no") == "no"
assert if_func(None, "yes", "no") == "no"
def test_if_no_false_value(self):
"""Test If with no false value defaults to None."""
from agent_framework_declarative._workflows._powerfx_functions import if_func
assert if_func(False, "yes") is None
class TestOrFunc:
"""Tests for or_func function."""
def test_or_all_false(self):
"""Test Or with all false values."""
from agent_framework_declarative._workflows._powerfx_functions import or_func
assert or_func(False, False, False) is False
def test_or_one_true(self):
"""Test Or with one true value."""
from agent_framework_declarative._workflows._powerfx_functions import or_func
assert or_func(False, True, False) is True
def test_or_all_true(self):
"""Test Or with all true values."""
from agent_framework_declarative._workflows._powerfx_functions import or_func
assert or_func(True, True, True) is True
def test_or_empty(self):
"""Test Or with no arguments."""
from agent_framework_declarative._workflows._powerfx_functions import or_func
assert or_func() is False
class TestAndFunc:
"""Tests for and_func function."""
def test_and_all_true(self):
"""Test And with all true values."""
from agent_framework_declarative._workflows._powerfx_functions import and_func
assert and_func(True, True, True) is True
def test_and_one_false(self):
"""Test And with one false value."""
from agent_framework_declarative._workflows._powerfx_functions import and_func
assert and_func(True, False, True) is False
def test_and_all_false(self):
"""Test And with all false values."""
from agent_framework_declarative._workflows._powerfx_functions import and_func
assert and_func(False, False, False) is False
def test_and_empty(self):
"""Test And with no arguments."""
from agent_framework_declarative._workflows._powerfx_functions import and_func
assert and_func() is True
class TestNotFunc:
"""Tests for not_func function."""
def test_not_true(self):
"""Test Not with true."""
from agent_framework_declarative._workflows._powerfx_functions import not_func
assert not_func(True) is False
def test_not_false(self):
"""Test Not with false."""
from agent_framework_declarative._workflows._powerfx_functions import not_func
assert not_func(False) is True
def test_not_truthy(self):
"""Test Not with truthy values."""
from agent_framework_declarative._workflows._powerfx_functions import not_func
assert not_func(1) is False
assert not_func("text") is False
def test_not_falsy(self):
"""Test Not with falsy values."""
from agent_framework_declarative._workflows._powerfx_functions import not_func
assert not_func(0) is True
assert not_func("") is True
assert not_func(None) is True
class TestIsBlankEdgeCases:
"""Additional tests for is_blank edge cases."""
def test_is_blank_empty_dict(self):
"""Test that empty dict is blank."""
assert is_blank({}) is True
def test_is_blank_non_empty_dict(self):
"""Test that non-empty dict is not blank."""
assert is_blank({"key": "value"}) is False
class TestCountRowsEdgeCases:
"""Additional tests for count_rows edge cases."""
def test_count_rows_dict(self):
"""Test counting dict items."""
assert count_rows({"a": 1, "b": 2, "c": 3}) == 3
def test_count_rows_tuple(self):
"""Test counting tuple items."""
assert count_rows((1, 2, 3, 4)) == 4
def test_count_rows_non_iterable(self):
"""Test counting non-iterable returns 0."""
assert count_rows(42) == 0
assert count_rows("string") == 0
class TestFirstLastEdgeCases:
"""Additional tests for first/last edge cases."""
def test_first_none(self):
"""Test first with None."""
assert first(None) is None
def test_last_none(self):
"""Test last with None."""
assert last(None) is None
def test_first_tuple(self):
"""Test first with tuple."""
assert first((1, 2, 3)) == 1
def test_last_tuple(self):
"""Test last with tuple."""
assert last((1, 2, 3)) == 3
class TestFindEdgeCases:
"""Additional tests for find edge cases."""
def test_find_none_substring(self):
"""Test find with None substring."""
assert find(None, "text") is None
def test_find_none_text(self):
"""Test find with None text."""
assert find("sub", None) is None
def test_find_both_none(self):
"""Test find with both None."""
assert find(None, None) is None
class TestLowerEdgeCases:
"""Additional tests for lower edge cases."""
def test_lower_none(self):
"""Test lower with None."""
assert lower(None) == ""
class TestConcatStrings:
"""Tests for concat_strings function."""
def test_concat_strings_basic(self):
"""Test basic string concatenation."""
from agent_framework_declarative._workflows._powerfx_functions import concat_strings
assert concat_strings("Hello", " ", "World") == "Hello World"
def test_concat_strings_with_none(self):
"""Test concat with None values."""
from agent_framework_declarative._workflows._powerfx_functions import concat_strings
assert concat_strings("Hello", None, "World") == "HelloWorld"
def test_concat_strings_empty(self):
"""Test concat with no arguments."""
from agent_framework_declarative._workflows._powerfx_functions import concat_strings
assert concat_strings() == ""
class TestConcatTextEdgeCases:
"""Additional tests for concat_text edge cases."""
def test_concat_text_none(self):
"""Test concat_text with None."""
assert concat_text(None) == ""
def test_concat_text_non_list(self):
"""Test concat_text with non-list."""
assert concat_text("single value") == "single value"
def test_concat_text_with_field_attr(self):
"""Test concat_text with field as object attribute."""
class Item: # noqa: B903
def __init__(self, name: str):
self.name = name
items = [Item("Alice"), Item("Bob")]
assert concat_text(items, field="name", separator=", ") == "Alice, Bob"
def test_concat_text_with_none_values(self):
"""Test concat_text with None values in list."""
items = [{"name": "Alice"}, {"name": None}, {"name": "Bob"}]
result = concat_text(items, field="name", separator=", ")
assert result == "Alice, , Bob"
class TestForAll:
"""Tests for for_all function."""
def test_for_all_with_list_of_dicts(self):
"""Test ForAll with list of dictionaries."""
from agent_framework_declarative._workflows._powerfx_functions import for_all
items = [{"name": "Alice"}, {"name": "Bob"}]
result = for_all(items, "expression")
assert result == items
def test_for_all_with_non_dict_items(self):
"""Test ForAll with non-dict items."""
from agent_framework_declarative._workflows._powerfx_functions import for_all
items = [1, 2, 3]
result = for_all(items, "expression")
assert result == [1, 2, 3]
def test_for_all_with_none(self):
"""Test ForAll with None."""
from agent_framework_declarative._workflows._powerfx_functions import for_all
assert for_all(None, "expression") == []
def test_for_all_with_non_list(self):
"""Test ForAll with non-list."""
from agent_framework_declarative._workflows._powerfx_functions import for_all
assert for_all("not a list", "expression") == []
def test_for_all_empty_list(self):
"""Test ForAll with empty list."""
from agent_framework_declarative._workflows._powerfx_functions import for_all
assert for_all([], "expression") == []
class TestSearchTableEdgeCases:
"""Additional tests for search_table edge cases."""
def test_search_table_none(self):
"""Test search_table with None."""
assert search_table(None, "value", "column") == []
def test_search_table_non_list(self):
"""Test search_table with non-list."""
assert search_table("not a list", "value", "column") == []
def test_search_table_with_object_attr(self):
"""Test search_table with object attributes."""
class Item: # noqa: B903
def __init__(self, name: str):
self.name = name
items = [Item("Alice"), Item("Bob"), Item("Charlie")]
result = search_table(items, "Bob", "name")
assert len(result) == 1
assert result[0].name == "Bob"
def test_search_table_no_matching_column(self):
"""Test search_table when items don't have the column."""
items = [{"other": "value"}]
result = search_table(items, "value", "name")
assert result == []
def test_search_table_empty_value(self):
"""Test search_table with empty search value."""
items = [{"name": "Alice"}, {"name": "Bob"}]
result = search_table(items, "", "name")
# Empty string matches everything
assert len(result) == 2