From cd43c41fe00954f368a6a85fb4602538bf691fd6 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Thu, 6 Aug 2026 00:42:50 +0100 Subject: [PATCH 1/3] fix(stdlib): resolve postponed annotations in generative stub signature inspect.signature(func) returns literal annotation strings when the caller's module has `from __future__ import annotations` (PEP 563), corrupting both the rendered function signature and the argument value-quoting check in describe_function()/get_argument(). Pass eval_str=True so annotations resolve the same way typing.get_type_hints() already resolves them elsewhere in the codebase. Fixes #1503 Assisted-by: Claude Code Signed-off-by: Nigel Jones --- mellea/stdlib/components/genstub.py | 4 +-- test/stdlib/components/_pep563_fixtures.py | 29 +++++++++++++++++++++ test/stdlib/components/test_genstub_unit.py | 28 ++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 test/stdlib/components/_pep563_fixtures.py diff --git a/mellea/stdlib/components/genstub.py b/mellea/stdlib/components/genstub.py index a071c910a..e82c40804 100644 --- a/mellea/stdlib/components/genstub.py +++ b/mellea/stdlib/components/genstub.py @@ -215,7 +215,7 @@ def describe_function(func: Callable) -> FunctionDict: """ return { "name": func.__name__, - "signature": str(inspect.signature(func)), + "signature": str(inspect.signature(func, eval_str=True)), "docstring": inspect.getdoc(func), } @@ -233,7 +233,7 @@ def get_argument(func: Callable, key: str, val: Any) -> Argument: Returns: Argument: an argument object representing the given parameter. """ - sig = inspect.signature(func) + sig = inspect.signature(func, eval_str=True) param = sig.parameters.get(key) if param and param.annotation is not inspect.Parameter.empty: param_type = param.annotation diff --git a/test/stdlib/components/_pep563_fixtures.py b/test/stdlib/components/_pep563_fixtures.py new file mode 100644 index 000000000..549ad8cb3 --- /dev/null +++ b/test/stdlib/components/_pep563_fixtures.py @@ -0,0 +1,29 @@ +# Copyright IBM Corp. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Fixtures with postponed annotations (PEP 563), used by test_genstub_unit.py. + +Kept in a separate module because `from __future__ import annotations` is a +module-level switch — isolating it here keeps the rest of the test suite on +normal (resolved) annotations. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Requirement: + """A single extracted requirement.""" + + id: str + text: str + + +def extract_requirements(product_description: str) -> list[Requirement]: + """Extract requirements from a product description.""" + + +def greet(name: str) -> str: + """Say hello.""" diff --git a/test/stdlib/components/test_genstub_unit.py b/test/stdlib/components/test_genstub_unit.py index 718a35412..4cb4e7ce9 100644 --- a/test/stdlib/components/test_genstub_unit.py +++ b/test/stdlib/components/test_genstub_unit.py @@ -64,6 +64,23 @@ def bare(): assert result["docstring"] is None +def test_describe_function_resolves_postponed_annotations(): + # Regression test for issue #1476: `from __future__ import annotations` + # made `describe_function` render literal annotation strings (e.g. + # "(product_description: 'str') -> 'list[Requirement]'") instead of the + # resolved types, corrupting the prompt sent to the model. + from test.stdlib.components._pep563_fixtures import extract_requirements + + result = describe_function(extract_requirements) + assert "'str'" not in result["signature"] + assert "'list[Requirement]'" not in result["signature"] + assert "product_description: str" in result["signature"] + assert ( + "list[test.stdlib.components._pep563_fixtures.Requirement]" + in result["signature"] + ) + + # --- get_argument --- @@ -85,6 +102,17 @@ def fn(count: int) -> None: assert "int" in str(arg._argument_dict["annotation"]) +def test_get_argument_string_value_quoted_under_postponed_annotations(): + # Regression test for issue #1476: under `from __future__ import + # annotations`, `param.annotation` was the literal string "str" rather + # than the `str` type, so the `is str` check failed and string arguments + # were rendered unquoted in the prompt. + from test.stdlib.components._pep563_fixtures import greet + + arg = get_argument(greet, "name", "Alice") + assert arg._argument_dict["value"] == '"Alice"' + + def test_get_argument_no_annotation_falls_back_to_runtime_type(): # No annotation on kwargs — should fall back to type(val) def fn(**kwargs) -> None: From 80cd1cfe237e4b3fb13630d850516ed70c0efde9 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Thu, 6 Aug 2026 01:31:50 +0100 Subject: [PATCH 2/3] fix(stdlib): drop stale issue ref and guard PEP 563 test preconditions Regression test comments cited #1476 (the original, unrelated report) instead of #1503 (this fix); per project convention, drop the numeric ref rather than just correct it. Also pin each test's precondition via __annotations__ so it can't pass vacuously if the fixture module ever drops `from __future__ import annotations`. Assisted-by: Claude Code Signed-off-by: Nigel Jones --- test/stdlib/components/test_genstub_unit.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/stdlib/components/test_genstub_unit.py b/test/stdlib/components/test_genstub_unit.py index 4cb4e7ce9..61d6402b5 100644 --- a/test/stdlib/components/test_genstub_unit.py +++ b/test/stdlib/components/test_genstub_unit.py @@ -65,12 +65,17 @@ def bare(): def test_describe_function_resolves_postponed_annotations(): - # Regression test for issue #1476: `from __future__ import annotations` - # made `describe_function` render literal annotation strings (e.g. + # Regression test: `from __future__ import annotations` made + # `describe_function` render literal annotation strings (e.g. # "(product_description: 'str') -> 'list[Requirement]'") instead of the # resolved types, corrupting the prompt sent to the model. from test.stdlib.components._pep563_fixtures import extract_requirements + # Guard the precondition: if the fixture module ever drops its + # `from __future__ import annotations`, this test would otherwise keep + # passing without exercising postponed annotations at all. + assert extract_requirements.__annotations__["return"] == "list[Requirement]" + result = describe_function(extract_requirements) assert "'str'" not in result["signature"] assert "'list[Requirement]'" not in result["signature"] @@ -103,12 +108,15 @@ def fn(count: int) -> None: def test_get_argument_string_value_quoted_under_postponed_annotations(): - # Regression test for issue #1476: under `from __future__ import - # annotations`, `param.annotation` was the literal string "str" rather - # than the `str` type, so the `is str` check failed and string arguments - # were rendered unquoted in the prompt. + # Regression test: under `from __future__ import annotations`, + # `param.annotation` was the literal string "str" rather than the `str` + # type, so the `is str` check failed and string arguments were rendered + # unquoted in the prompt. from test.stdlib.components._pep563_fixtures import greet + # Guard the precondition: same reasoning as above. + assert greet.__annotations__["name"] == "str" + arg = get_argument(greet, "name", "Alice") assert arg._argument_dict["value"] == '"Alice"' From 68c10af68efb6056ce4275ec2cf8aeb08725f387 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Thu, 6 Aug 2026 10:13:43 +0100 Subject: [PATCH 3/3] test(stdlib): move PEP 563 fixture imports to module level Match the module-top-level import style used in PR #1509's test/backends/test_tools_pep563.py for the same fixture pattern. Assisted-by: Claude Code Signed-off-by: Nigel Jones --- test/stdlib/components/test_genstub_unit.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/stdlib/components/test_genstub_unit.py b/test/stdlib/components/test_genstub_unit.py index 61d6402b5..335f44444 100644 --- a/test/stdlib/components/test_genstub_unit.py +++ b/test/stdlib/components/test_genstub_unit.py @@ -26,6 +26,7 @@ get_argument, ) from mellea.stdlib.requirements.requirement import reqify +from test.stdlib.components._pep563_fixtures import extract_requirements, greet # --- describe_function --- @@ -69,8 +70,6 @@ def test_describe_function_resolves_postponed_annotations(): # `describe_function` render literal annotation strings (e.g. # "(product_description: 'str') -> 'list[Requirement]'") instead of the # resolved types, corrupting the prompt sent to the model. - from test.stdlib.components._pep563_fixtures import extract_requirements - # Guard the precondition: if the fixture module ever drops its # `from __future__ import annotations`, this test would otherwise keep # passing without exercising postponed annotations at all. @@ -112,8 +111,6 @@ def test_get_argument_string_value_quoted_under_postponed_annotations(): # `param.annotation` was the literal string "str" rather than the `str` # type, so the `is str` check failed and string arguments were rendered # unquoted in the prompt. - from test.stdlib.components._pep563_fixtures import greet - # Guard the precondition: same reasoning as above. assert greet.__annotations__["name"] == "str"