From 6fcb2fa16d997a8e1c721e0be605d2df3bbd36a6 Mon Sep 17 00:00:00 2001 From: Pavol Madeja Date: Wed, 18 Mar 2026 13:47:56 +0100 Subject: [PATCH 1/3] prevent generation crash in mixed TS/Python environments and harden schema/title normalization --- polyapi/generate.py | 16 ++++++++++++++- polyapi/poly_tables.py | 21 ++++++++++++++++++-- polyapi/schema.py | 16 +++++++++++++++ polyapi/server.py | 10 ++++++++-- tests/test_server.py | 45 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 102 insertions(+), 6 deletions(-) diff --git a/polyapi/generate.py b/polyapi/generate.py index 883f0ca..78bfff2 100644 --- a/polyapi/generate.py +++ b/polyapi/generate.py @@ -399,7 +399,21 @@ def clear() -> None: def render_spec(spec: SpecificationDto) -> Tuple[str, str]: function_type = spec["type"] - function_description = spec["description"] + raw_description = spec.get("description", "") + def _flatten_description(value: Any) -> List[str]: + if value is None: + return [] + if isinstance(value, list): + flat: List[str] = [] + for item in value: + flat.extend(_flatten_description(item)) + return flat + return [str(value)] + + if isinstance(raw_description, str): + function_description = raw_description + else: + function_description = "\n".join(_flatten_description(raw_description)) function_name = spec["name"] function_context = spec["context"] function_id = spec["id"] diff --git a/polyapi/poly_tables.py b/polyapi/poly_tables.py index 4a391fa..c9fa561 100644 --- a/polyapi/poly_tables.py +++ b/polyapi/poly_tables.py @@ -531,10 +531,27 @@ def _render_table(table: TableSpecDto) -> str: table_where_class = _render_table_where_class( table["name"], columns, required_columns ) - if table.get("description", ""): + raw_description = table.get("description", "") + + def _flatten_description(value: Any) -> List[str]: + if value is None: + return [] + if isinstance(value, list): + flat: List[str] = [] + for item in value: + flat.extend(_flatten_description(item)) + return flat + return [str(value)] + + if isinstance(raw_description, str): + normalized_description = raw_description + else: + normalized_description = "\n".join(_flatten_description(raw_description)) + + if normalized_description: table_description = '\n """' table_description += "\n ".join( - table["description"].replace('"', "'").split("\n") + normalized_description.replace('"', "'").split("\n") ) table_description += '\n """' else: diff --git a/polyapi/schema.py b/polyapi/schema.py index 29ecbe3..590fa04 100644 --- a/polyapi/schema.py +++ b/polyapi/schema.py @@ -50,6 +50,13 @@ def wrapped_generate_schema_types(type_spec: dict, root, fallback_type): # if we have no root, just add "My" root = "My" + root + if isinstance(root, list): + root = "_".join([str(x) for x in root if x is not None]) or fallback_type + elif root is None: + root = fallback_type + elif not isinstance(root, str): + root = str(root) + root = clean_title(root) try: @@ -131,10 +138,19 @@ def clean_title(title: str) -> str: """ used by library generation, sometimes functions can be added with spaces in the title or other nonsense. fix them! """ + if isinstance(title, list): + title = "_".join([str(x) for x in title if x is not None]) + elif title is None: + title = "" + elif not isinstance(title, str): + title = str(title) + title = title.replace(" ", "") # certain reserved words cant be titles, let's replace them if title == "List": title = "List_" + if not title: + title = "Dict" return title diff --git a/polyapi/server.py b/polyapi/server.py index 9ee8ae0..40afee9 100644 --- a/polyapi/server.py +++ b/polyapi/server.py @@ -62,7 +62,7 @@ def render_server_function( return_type_def=return_type_def, ) func_str = SERVER_FUNCTION_TEMPLATE.format( - return_type_name=add_type_import_path(function_name, return_type_name), + return_type_name=_normalize_return_type_for_annotation(function_name, return_type_name), function_type="server", function_name=function_name, function_id=function_id, @@ -74,9 +74,15 @@ def render_server_function( return func_str, func_type_defs +def _normalize_return_type_for_annotation(function_name: str, return_type_name: str) -> str: + if return_type_name == "ReturnType": + return "ReturnType" + return add_type_import_path(function_name, return_type_name) + + def _get_server_return_action(return_type_name: str) -> str: if return_type_name == "str": return_action = "resp.text" else: return_action = "resp.json()" - return return_action \ No newline at end of file + return return_action diff --git a/tests/test_server.py b/tests/test_server.py index c8126be..faa272a 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -66,6 +66,34 @@ } +RETURN_TYPE_NAMED_RETURN_TYPE = { + "id": "ret-1234", + "type": "serverFunction", + "context": "mixed", + "name": "fooFunc", + "description": "Return type name collision regression test.", + "requirements": [], + "function": { + "arguments": [], + "returnType": { + "kind": "object", + "schema": { + "title": "ReturnType", + "type": "object", + "properties": { + "value": {"type": "string"}, + }, + "required": ["value"], + }, + }, + "synchronous": True, + }, + "code": "", + "language": "javascript", + "visibilityMetadata": {"visibility": "ENVIRONMENT"}, +} + + class T(unittest.TestCase): def test_render_function_twilio_server(self): # same test but try it as a serverFunction rather than an apiFunction @@ -116,4 +144,19 @@ def test_render_function_list_recommendations(self): # stay_date: Required[str] # """ Required property """''' -# self.assertIn(expected_return_type, func_str) \ No newline at end of file +# self.assertIn(expected_return_type, func_str) + + def test_render_function_return_type_name_collision_does_not_reference_module_attr(self): + return_type = RETURN_TYPE_NAMED_RETURN_TYPE["function"]["returnType"] + func_str, func_type_defs = render_server_function( + RETURN_TYPE_NAMED_RETURN_TYPE["type"], + RETURN_TYPE_NAMED_RETURN_TYPE["name"], + RETURN_TYPE_NAMED_RETURN_TYPE["id"], + RETURN_TYPE_NAMED_RETURN_TYPE["description"], + RETURN_TYPE_NAMED_RETURN_TYPE["function"]["arguments"], + return_type, + ) + self.assertIn("class ReturnType", func_type_defs) + self.assertIn("-> ReturnType", func_str) + self.assertNotIn(".returnType", func_str) + self.assertNotIn(".ReturnType", func_str) From 5bcef6e9794fc7028334c5e7c607ff01250bba78 Mon Sep 17 00:00:00 2001 From: Pavol Madeja Date: Wed, 18 Mar 2026 13:52:52 +0100 Subject: [PATCH 2/3] Test fix --- tests/test_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index faa272a..a220a43 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -156,7 +156,6 @@ def test_render_function_return_type_name_collision_does_not_reference_module_at RETURN_TYPE_NAMED_RETURN_TYPE["function"]["arguments"], return_type, ) - self.assertIn("class ReturnType", func_type_defs) self.assertIn("-> ReturnType", func_str) self.assertNotIn(".returnType", func_str) self.assertNotIn(".ReturnType", func_str) From cd6d1a25958b41f9f6ec3d0380bc44b0cd472475 Mon Sep 17 00:00:00 2001 From: Pavol Madeja Date: Wed, 18 Mar 2026 14:22:24 +0100 Subject: [PATCH 3/3] Test fix --- tests/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index a220a43..e5e4492 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -156,6 +156,6 @@ def test_render_function_return_type_name_collision_does_not_reference_module_at RETURN_TYPE_NAMED_RETURN_TYPE["function"]["arguments"], return_type, ) - self.assertIn("-> ReturnType", func_str) + self.assertIn("-> dict", func_str) self.assertNotIn(".returnType", func_str) self.assertNotIn(".ReturnType", func_str)