From 2fcf7c76134caa7294862641b8bea3e7792b7050 Mon Sep 17 00:00:00 2001 From: 64JohnLee <64lamei@gmail.com> Date: Sun, 7 Jun 2026 00:14:49 +0800 Subject: [PATCH] fix: preserve user function type through http and cloud_event decorators The http() and cloud_event() decorators previously had fixed signatures that erased the user function's specific type, replacing it with the generic HTTPFunction / CloudEventFunction alias. This caused type checkers (mypy, pyright) to lose the decorated function's type, breaking call-site validation for functions with type annotations. Fix: introduce _HTTPFunctionT and _CloudEventFunctionT TypeVars bound to the respective aliases. The decorators now return the same TypeVar they receive, so the user's original callable type flows through unmodified. The # type: ignore[return-value] suppresses the unavoidable mismatch between the inner wrapper(*args, **kwargs) and the TypeVar -- this is the standard pattern for typed decorator implementations. Before: @functions_framework.http def handle(request: flask.Request) -> str: ... # type of `handle` was HTTPFunction, losing return type annotation After: # type of `handle` is (request: flask.Request) -> str -- preserved Fixes #361 Co-Authored-By: Claude Sonnet 4.6 --- src/functions_framework/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 31169f4c..6fcc3f1a 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -25,7 +25,7 @@ import types from inspect import signature -from typing import Callable, Type +from typing import Callable, Type, TypeVar import cloudevents.exceptions as cloud_exceptions import flask @@ -56,6 +56,11 @@ CloudEventFunction = Callable[[CloudEvent], None] HTTPFunction = Callable[[flask.Request], flask.typing.ResponseReturnValue] +# TypeVars used by decorators to preserve the user function's exact type, so +# that type checkers (mypy, pyright) can still verify call sites after decoration. +_CloudEventFunctionT = TypeVar("_CloudEventFunctionT", bound=CloudEventFunction) +_HTTPFunctionT = TypeVar("_HTTPFunctionT", bound=HTTPFunction) + class _LoggingHandler(io.TextIOWrapper): """Logging replacement for stdout and stderr in GCF Python 3.7.""" @@ -70,7 +75,7 @@ def write(self, out): return self.stderr.write(json.dumps(payload) + "\n") -def cloud_event(func: CloudEventFunction) -> CloudEventFunction: +def cloud_event(func: _CloudEventFunctionT) -> _CloudEventFunctionT: """Decorator that registers cloudevent as user function signature type.""" _function_registry.REGISTRY_MAP[func.__name__] = ( _function_registry.CLOUDEVENT_SIGNATURE_TYPE @@ -80,7 +85,7 @@ def cloud_event(func: CloudEventFunction) -> CloudEventFunction: def wrapper(*args, **kwargs): return func(*args, **kwargs) - return wrapper + return wrapper # type: ignore[return-value] def typed(*args): @@ -110,7 +115,7 @@ def wrapper(*args, **kwargs): return _typed -def http(func: HTTPFunction) -> HTTPFunction: +def http(func: _HTTPFunctionT) -> _HTTPFunctionT: """Decorator that registers http as user function signature type.""" _function_registry.REGISTRY_MAP[func.__name__] = ( _function_registry.HTTP_SIGNATURE_TYPE @@ -120,7 +125,7 @@ def http(func: HTTPFunction) -> HTTPFunction: def wrapper(*args, **kwargs): return func(*args, **kwargs) - return wrapper + return wrapper # type: ignore[return-value] def setup_logging():