From 77ea872bc8f9688cf9866d8cb24c0bfb0df58507 Mon Sep 17 00:00:00 2001 From: Luke Kingland Date: Mon, 10 Feb 2025 12:24:55 +0900 Subject: [PATCH] Improved cross-language homogeneity Use an explicit "handle" method on Function instances, as this is the most similar method signature approach between different language; a primary goal of the project. This is in contrast to a more strict adherence to the ASGI spec which would see the Function object itself be callable. See PR for further exploration of this choice. --- CHANGELOG.md | 5 +++++ cmd/fhttp/main.py | 9 ++++++--- src/func_python/http.py | 13 +++++++------ tests/test_http.py | 6 ++++-- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f307e032..412a636f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added ### Changed + +- Simplified instanced functions by expecting a named 'handle' method. + ### Deprecated ### Removed ### Fixed @@ -19,6 +22,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [0.2.0] - 2024-11-06 ### Added + - optional message returns from lifeycle methods "alive" and "ready" - expanded development and release documentation @@ -26,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [0.1.0] - 2024-10-28 ### Added + - Initial Implementation of the Python HTTP Functions Middleware diff --git a/cmd/fhttp/main.py b/cmd/fhttp/main.py index 44e46b38..deb101b3 100644 --- a/cmd/fhttp/main.py +++ b/cmd/fhttp/main.py @@ -38,9 +38,11 @@ async def handle(scope, receive, send): # Example instanced handler # This is the default expected by this test. -# The class can be named anything. See "new" below. +# The class can be named anything, but there must be a constructor named "new" +# which returns an object with an async method "handle" conforming to the ASGI +# callable's method signature. class MyFunction: - async def __call__(self, scope, receive, send): + async def handle(self, scope, receive, send): logging.info("OK") await send({ @@ -68,7 +70,8 @@ def stop(self): # Function instance constructor # expected to be named exactly "new" -# Must return a callable which conforms to the ASGI spec. +# Must return a object which exposes a method "handle" which conforms to the +# ASGI callable spec. def new(): """ new is the factory function (or constructor) which will create a new function instance when invoked. This must be named "new", and the diff --git a/src/func_python/http.py b/src/func_python/http.py index 4a7120c6..9394b2b9 100644 --- a/src/func_python/http.py +++ b/src/func_python/http.py @@ -11,6 +11,7 @@ logging.basicConfig(level=DEFAULT_LOG_LEVEL) + def serve(f): """serve a function f by wrapping it in an ASGI web application and starting. The function can be either a constructor for a functon @@ -28,7 +29,7 @@ def serve(f): raise else: raise ValueError("function must be either be a constructor 'new' or a " - "handler 'handle'.") + "handler function 'handle'.") class DefaultFunction: @@ -38,7 +39,7 @@ class DefaultFunction: def __init__(self, handler): self.handle = handler - async def __call__(self, scope, receive, send): + async def handle(self, scope, receive, send): # delegate to the handler implementation provided during construction. await self.handle(scope, receive, send) @@ -47,6 +48,9 @@ class ASGIApplication(): def __init__(self, f): self.f = f self.stop_event = asyncio.Event() + if hasattr(self.f, "handle") is not True: + raise AttributeError( "Function must implement a 'handle' method.") + # Inform the user via logs that defaults will be used for health # endpoints if no matchin methods were provided. if hasattr(self.f, "alive") is not True: @@ -125,10 +129,7 @@ async def __call__(self, scope, receive, send): elif scope['path'] == '/health/readiness': await self.handle_readiness(scope, receive, send) else: - if callable(self.f): - await self.f(scope, receive, send) - else: - raise Exception("function does not implement handle") + await self.f.handle(scope, receive, send) except Exception as e: await send_exception(send, 500, f"Error: {e}") diff --git a/tests/test_http.py b/tests/test_http.py index 11031353..45a39914 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -10,16 +10,18 @@ # Set a dynamic test URL using an environment variable os.environ["LISTEN_ADDRESS"] = os.getenv("LISTEN_ADDRESS", "127.0.0.1:8081") + # Retrieve the LISTEN_ADDRESS for use in the tests LISTEN_ADDRESS = os.getenv("LISTEN_ADDRESS") + def test_static(): """ ensures that a user function developed using the default "static" style (method signature) is served by the middleware. """ - # Functoin + # Function # An example minimal "static" user function which will be # exposed on the network as an ASGI service by the middleware. async def handle(scope, receive, send): @@ -81,7 +83,7 @@ def test_instanced(): # An example standard "instanced" function (user's Function) which is # exposed on the network as an ASGI service by the middleware. class MyFunction: - async def __call__(self, scope, receive, send): + async def handle(self, scope, receive, send): await send({ 'type': 'http.response.start', 'status': 200,