From d1e62e300cfe867952dc49967fa661e6f5a784e3 Mon Sep 17 00:00:00 2001 From: Ethan Werner Date: Sat, 10 Oct 2026 20:57:20 +0000 Subject: Add typed functions API Introduce an in-memory API for registering typed Python functions and invoking them with JSON. Generate Draft 2020-12 schemas from annotations and document the request formats and trust boundary. --- test_server.py | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 test_server.py (limited to 'test_server.py') diff --git a/test_server.py b/test_server.py new file mode 100644 index 0000000..42a666f --- /dev/null +++ b/test_server.py @@ -0,0 +1,159 @@ +import asyncio +import json +import unittest + +from server import JSON_SCHEMA_DRAFT, Server + + +class FunctionTests(unittest.TestCase): + def setUp(self): + self.app = Server() + + def request(self, method, path, body=b""): + async def call(): + messages = [] + + async def receive(): + return {"type": "http.request", "body": body, "more_body": False} + + async def send(message): + messages.append(message) + + await self.app( + {"type": "http", "method": method, "path": path}, receive, send + ) + return messages[0]["status"], messages[1]["body"] + + return asyncio.run(call()) + + def test_reflection_validates_calls_and_describes_function(self): + source = b"def double(x: int) -> int:\n return x * 2\n" + self.assertEqual(self.request("PATCH", "/double", source)[0], 200) + status, body = self.request("GET", "/double") + self.assertEqual(status, 200) + entry = json.loads(self.request("GET", "/")[1])["functions"][0] + self.assertEqual(json.loads(body), entry) + self.assertEqual(entry["name"], "double") + self.assertEqual(entry["source"], source.decode()) + input_schema = entry["schema"]["input"] + self.assertEqual(input_schema["$schema"], JSON_SCHEMA_DRAFT) + self.assertNotIn("$defs", input_schema) + self.assertEqual(input_schema["anyOf"][1], {"type": "integer"}) + self.assertEqual(input_schema["anyOf"][0]["required"], ["x"]) + self.assertEqual( + entry["schema"]["output"], + {"$schema": JSON_SCHEMA_DRAFT, "type": "integer"}, + ) + self.assertEqual(self.request("POST", "/double", b'{"x": 4}'), (200, b"8")) + self.assertEqual(self.request("POST", "/double", b"4"), (200, b"8")) + for invalid in (b'{"x": "4"}', b'{"x": 4.5}', b"{}", b"[]", b"NaN"): + self.assertEqual(self.request("POST", "/double", invalid)[0], 400) + self.assertEqual(self.request("GET", "/double/schema")[0], 404) + self.assertEqual(self.request("PUT", "/double/schema", b"{}")[0], 404) + + self.assertEqual( + self.request( + "PATCH", "/double", b"def double(x: int) -> int:\n return str(x)" + )[0], + 200, + ) + self.assertEqual(self.request("POST", "/double", b'{"x": 4}')[0], 500) + + def test_invalid_update_keeps_previous_function(self): + original = b"def double(x: int) -> int:\n return x * 2" + self.request("PATCH", "/double", original) + for invalid in ( + b"return x * 2", + b"def other(x: int) -> int:\n return x", + b"def double(x):\n return x", + b"async def double(x: int) -> int:\n return x", + ): + self.assertEqual(self.request("PATCH", "/double", invalid)[0], 400) + status, body = self.request("GET", "/double") + self.assertEqual(status, 200) + self.assertEqual(json.loads(body)["source"], original.decode()) + self.assertEqual(self.request("POST", "/double", b"3"), (200, b"6")) + + def test_multiple_parameters_union_and_default(self): + source = ( + b'def label(x: int | str, prefix: str = "item") -> str:\n' + b' return f"{prefix}:{x}"' + ) + self.assertEqual(self.request("PATCH", "/label", source)[0], 200) + self.assertEqual( + self.request("POST", "/label", b'{"x":4,"prefix":"order"}'), + (200, b'"order:4"'), + ) + self.assertEqual( + self.request("POST", "/label", b'{"x":"abc"}'), + (200, b'"item:abc"'), + ) + for invalid in (b'{"x":4.5}', b'{"prefix":"order"}', b'{"x":4,"extra":1}'): + self.assertEqual(self.request("POST", "/label", invalid)[0], 400) + schema = json.loads(self.request("GET", "/")[1])["functions"][0]["schema"] + self.assertEqual(schema["input"]["$schema"], JSON_SCHEMA_DRAFT) + self.assertEqual(schema["input"]["required"], ["x"]) + self.assertEqual( + {item["type"] for item in schema["input"]["properties"]["x"]["anyOf"]}, + {"integer", "string"}, + ) + self.assertEqual(schema["input"]["properties"]["prefix"]["default"], "item") + + def test_model_input_and_output(self): + source = b"""from pydantic import BaseModel +class Order(BaseModel): + quantity: int + unit_price: float +def total(x: Order) -> Order: + return x +""" + self.assertEqual(self.request("PATCH", "/total", source)[0], 200) + self.assertEqual( + json.loads( + self.request( + "POST", "/total", b'{"x":{"quantity":2,"unit_price":3.5}}' + )[1] + ), + {"quantity": 2, "unit_price": 3.5}, + ) + self.assertEqual( + json.loads( + self.request("POST", "/total", b'{"quantity":2,"unit_price":3.5}')[1] + ), + {"quantity": 2, "unit_price": 3.5}, + ) + self.assertEqual( + self.request("POST", "/total", b'{"x":{"quantity":"2","unit_price":3.5}}')[ + 0 + ], + 400, + ) + entry = json.loads(self.request("GET", "/")[1])["functions"][0] + self.assertEqual(entry["schema"]["input"]["$schema"], JSON_SCHEMA_DRAFT) + self.assertEqual( + entry["schema"]["input"]["$defs"]["Order"]["properties"]["quantity"][ + "type" + ], + "integer", + ) + + def test_single_parameter_accepts_every_json_value(self): + source = b"from typing import Any\ndef identity(x: Any) -> Any:\n return x" + self.assertEqual(self.request("PATCH", "/identity", source)[0], 200) + for value in ( + b"4", + b'"hello"', + b"true", + b"false", + b"null", + b"[1,2]", + b'{"item":1}', + ): + status, response = self.request("POST", "/identity", value) + self.assertEqual(status, 200, value) + self.assertEqual(json.loads(response), json.loads(value)) + self.assertEqual(self.request("POST", "/identity", b'{"x":4}'), (200, b"4")) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.3.1