From b5ccaadfd12fdd8696ea2c84390442352a5471c0 Mon Sep 17 00:00:00 2001 From: Simon Wydooghe Date: Mon, 30 Mar 2026 17:52:19 +0200 Subject: [PATCH 1/2] Support finding type annotated resolver --- aws_lambda_powertools/event_handler/openapi/merge.py | 12 ++++++++---- .../unit/event_handler/openapi/test_openapi_merge.py | 9 +++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/openapi/merge.py b/aws_lambda_powertools/event_handler/openapi/merge.py index 38b80914df3..1742fc465a1 100644 --- a/aws_lambda_powertools/event_handler/openapi/merge.py +++ b/aws_lambda_powertools/event_handler/openapi/merge.py @@ -67,11 +67,15 @@ def _file_has_resolver(file_path: Path, resolver_name: str) -> bool: return False for node in ast.walk(tree): + targets = [] if isinstance(node, ast.Assign): - for target in node.targets: - if isinstance(target, ast.Name) and target.id == resolver_name: - if _is_resolver_call(node.value): - return True + targets = node.targets + elif isinstance(node, ast.AnnAssign): + targets = [node.target] + for target in targets: + if isinstance(target, ast.Name) and target.id == resolver_name: + if _is_resolver_call(node.value): + return True return False diff --git a/tests/unit/event_handler/openapi/test_openapi_merge.py b/tests/unit/event_handler/openapi/test_openapi_merge.py index 21500145b35..9d95dd0c01c 100644 --- a/tests/unit/event_handler/openapi/test_openapi_merge.py +++ b/tests/unit/event_handler/openapi/test_openapi_merge.py @@ -57,6 +57,15 @@ def test_file_has_resolver_found(tmp_path: Path): assert _file_has_resolver(handler_file, "app") is True +def test_file_has_resolver_found_with_type_annotation(tmp_path: Path): + handler_file = tmp_path / "handler.py" + handler_file.write_text(""" +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +app: APIGatewayRestResolver = APIGatewayRestResolver() +""") + assert _file_has_resolver(handler_file, "app") is True + + def test_is_excluded_with_directory_pattern(): root = Path("/project") assert _is_excluded(Path("/project/tests/handler.py"), root, ["**/tests/**"]) is True From 6a94d20b314eacee5dd3e0488b50248e4a6dec1a Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 2 Apr 2026 15:57:22 +0100 Subject: [PATCH 2/2] Fix tests --- .../event_handler/openapi/merge.py | 7 ++++-- .../_pydantic/merge_handlers/typed_handler.py | 24 +++++++++++++++++++ .../_pydantic/test_openapi_merge.py | 16 +++++++++++++ .../openapi/test_openapi_merge.py | 9 ------- 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 tests/functional/event_handler/_pydantic/merge_handlers/typed_handler.py diff --git a/aws_lambda_powertools/event_handler/openapi/merge.py b/aws_lambda_powertools/event_handler/openapi/merge.py index 1742fc465a1..9db9c0daa5c 100644 --- a/aws_lambda_powertools/event_handler/openapi/merge.py +++ b/aws_lambda_powertools/event_handler/openapi/merge.py @@ -67,14 +67,17 @@ def _file_has_resolver(file_path: Path, resolver_name: str) -> bool: return False for node in ast.walk(tree): - targets = [] + targets: list[ast.expr] = [] + value: ast.expr | None = None if isinstance(node, ast.Assign): targets = node.targets + value = node.value elif isinstance(node, ast.AnnAssign): targets = [node.target] + value = node.value for target in targets: if isinstance(target, ast.Name) and target.id == resolver_name: - if _is_resolver_call(node.value): + if value is not None and _is_resolver_call(value): return True return False diff --git a/tests/functional/event_handler/_pydantic/merge_handlers/typed_handler.py b/tests/functional/event_handler/_pydantic/merge_handlers/typed_handler.py new file mode 100644 index 00000000000..53c2b4e0a12 --- /dev/null +++ b/tests/functional/event_handler/_pydantic/merge_handlers/typed_handler.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from pydantic import BaseModel + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver + +app: APIGatewayRestResolver = APIGatewayRestResolver(enable_validation=True) + + +class Product(BaseModel): + id: int + name: str + price: float + + +@app.get("/products") +def get_products() -> list[Product]: + return [ + Product(id=1, name="Widget", price=9.99), + ] + + +def handler(event, context): + return app.resolve(event, context) diff --git a/tests/functional/event_handler/_pydantic/test_openapi_merge.py b/tests/functional/event_handler/_pydantic/test_openapi_merge.py index b4dc1d70232..12d41566e32 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_merge.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_merge.py @@ -367,3 +367,19 @@ def test_openapi_merge_schema_is_cached(): # AND paths should not be duplicated assert len([p for p in schema1["paths"] if p == "/users"]) == 1 + + +def test_openapi_merge_discover_type_annotated_resolver(): + # GIVEN an OpenAPIMerge instance + merge = OpenAPIMerge(title="Typed API", version="1.0.0") + + # WHEN discovering a handler with a type-annotated resolver (app: Resolver = Resolver()) + merge.discover( + path=MERGE_HANDLERS_PATH, + pattern="**/typed_handler.py", + resolver_name="app", + ) + + # THEN it should find the resolver and include its routes in the schema + schema = merge.get_openapi_schema() + assert "/products" in schema["paths"] diff --git a/tests/unit/event_handler/openapi/test_openapi_merge.py b/tests/unit/event_handler/openapi/test_openapi_merge.py index 9d95dd0c01c..21500145b35 100644 --- a/tests/unit/event_handler/openapi/test_openapi_merge.py +++ b/tests/unit/event_handler/openapi/test_openapi_merge.py @@ -57,15 +57,6 @@ def test_file_has_resolver_found(tmp_path: Path): assert _file_has_resolver(handler_file, "app") is True -def test_file_has_resolver_found_with_type_annotation(tmp_path: Path): - handler_file = tmp_path / "handler.py" - handler_file.write_text(""" -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -app: APIGatewayRestResolver = APIGatewayRestResolver() -""") - assert _file_has_resolver(handler_file, "app") is True - - def test_is_excluded_with_directory_pattern(): root = Path("/project") assert _is_excluded(Path("/project/tests/handler.py"), root, ["**/tests/**"]) is True