Skip to content
Merged
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
4 changes: 2 additions & 2 deletions mellea/stdlib/components/genstub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}

Expand All @@ -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
Expand Down
29 changes: 29 additions & 0 deletions test/stdlib/components/_pep563_fixtures.py
Original file line number Diff line number Diff line change
@@ -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."""
33 changes: 33 additions & 0 deletions test/stdlib/components/test_genstub_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand Down Expand Up @@ -64,6 +65,26 @@ def bare():
assert result["docstring"] is None


def test_describe_function_resolves_postponed_annotations():
# 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.
# 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"]
assert "product_description: str" in result["signature"]
assert (
"list[test.stdlib.components._pep563_fixtures.Requirement]"
in result["signature"]
)


# --- get_argument ---


Expand All @@ -85,6 +106,18 @@ 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: 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.
# Guard the precondition: same reasoning as above.
assert greet.__annotations__["name"] == "str"

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:
Expand Down
Loading