diff --git a/polyapi/generate.py b/polyapi/generate.py index 883f0ca..a65480e 100644 --- a/polyapi/generate.py +++ b/polyapi/generate.py @@ -16,7 +16,7 @@ from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto, TableSpecDto from .api import render_api_function from .server import render_server_function -from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace +from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace, to_type_module_alias from .variables import generate_variables from .poly_tables import generate_tables from . import http_client @@ -506,8 +506,10 @@ def add_function_file( with open(init_path, "r", encoding='utf-8') as f: init_content = f.read() - # Prepare new content to append to __init__.py - new_init_content = init_content + f"\n\nfrom . import {func_namespace}\n\n{func_str}" + # Import the generated type module under a private alias so PascalCase function + # names do not shadow their own module symbol in __init__.py. + type_module_alias = to_type_module_alias(function_name) + new_init_content = init_content + f"\n\nfrom . import {func_namespace} as {type_module_alias}\n\n{func_str}" # Use temporary files for atomic writes # Write to __init__.py atomically diff --git a/polyapi/utils.py b/polyapi/utils.py index 1a6d168..d3dafcc 100644 --- a/polyapi/utils.py +++ b/polyapi/utils.py @@ -71,11 +71,17 @@ def print_red(s: str): print(Fore.RED + s + Style.RESET_ALL) +def to_type_module_alias(function_name: str) -> str: + """Return the internal alias used for a function's generated type module.""" + return f"_{to_func_namespace(function_name)}_types" + + def add_type_import_path(function_name: str, arg: str) -> str: """if not basic type, coerce to camelCase and add the import path""" # from now, we start qualifying non-basic types :)) # e.g. Callable[[EmailAddress, Dict, Dict, Dict], None] # becomes Callable[[Set_profile_email.EmailAddress, Dict, Dict, Dict], None] + type_module_alias = to_type_module_alias(function_name) if arg.startswith("Callable"): inner = arg[len("Callable["):-1] # strip outer Callable[...] @@ -84,7 +90,7 @@ def add_type_import_path(function_name: str, arg: str) -> str: for p in parts: clean = p.strip("[] ") if clean and clean not in BASIC_PYTHON_TYPES: - replacement = f"{to_func_namespace(function_name)}.{camelCase(clean)}" + replacement = f"{type_module_alias}.{camelCase(clean)}" p = p.replace(clean, replacement) qualified.append(p) return "Callable[" + ",".join(qualified) + "]" @@ -100,11 +106,11 @@ def add_type_import_path(function_name: str, arg: str) -> str: else: if '"' in sub: sub = sub.replace('"', "") - return f'List["{to_func_namespace(function_name)}.{camelCase(sub)}"]' + return f'List["{type_module_alias}.{camelCase(sub)}"]' else: - return f"List[{to_func_namespace(function_name)}.{camelCase(sub)}]" + return f"List[{type_module_alias}.{camelCase(sub)}]" - return f"{to_func_namespace(function_name)}.{camelCase(arg)}" + return f"{type_module_alias}.{camelCase(arg)}" def get_type_and_def( @@ -352,4 +358,4 @@ def return_type_already_defined_in_args(return_type_name: str, args_def: str) -> basic_match = bool(re.search(basic_pattern, args_def, re.MULTILINE)) class_pattern = rf"^class {re.escape(return_type_name)}\(TypedDict" class_match = bool(re.search(class_pattern, args_def, re.MULTILINE)) - return basic_match or class_match \ No newline at end of file + return basic_match or class_match diff --git a/pyproject.toml b/pyproject.toml index f1385c1..41023ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "polyapi-python" -version = "0.3.14.dev4" +version = "0.3.15.dev1" description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers" authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }] dependencies = [ diff --git a/tests/test_api.py b/tests/test_api.py index ceba99c..a8bf970 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,7 @@ import unittest from polyapi.api import render_api_function -from polyapi.utils import to_func_namespace +from polyapi.utils import to_type_module_alias ACCUWEATHER = { "id": "f7588018-2364-4586-b60d", @@ -236,7 +236,7 @@ def test_render_function_accuweather(self): ) self.assertIn(ACCUWEATHER["id"], func_str) self.assertIn("locationId: int,", func_str) - self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str) + self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str) def test_render_function_zillow(self): name = ZILLOW["name"] @@ -250,7 +250,7 @@ def test_render_function_zillow(self): ) self.assertIn(ZILLOW["id"], func_str) self.assertIn("locationId: int,", func_str) - self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str) + self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str) def test_render_function_twilio_api(self): name = TWILIO["name"] @@ -265,7 +265,7 @@ def test_render_function_twilio_api(self): self.assertIn(TWILIO["id"], func_str) self.assertIn("conversationSID: str", func_str) self.assertIn("authToken: str", func_str) - self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str) + self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str) def test_render_function_twilio_get_details(self): # same test but try it as a serverFunction rather than an apiFunction @@ -279,6 +279,6 @@ def test_render_function_twilio_get_details(self): TWILIO_GET_DETAILS["function"]["returnType"], ) self.assertIn(TWILIO_GET_DETAILS["id"], func_str) - self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str) + self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str) self.assertIn("class SubresourceUris", func_type_defs) # self.assertIn('Required["SubresourceUris"]', func_type_defs) diff --git a/tests/test_generate.py b/tests/test_generate.py index f6f08fa..aa3f453 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -2,9 +2,11 @@ import os import shutil import importlib.util +import tempfile from unittest.mock import patch, MagicMock -from polyapi.utils import get_type_and_def, rewrite_reserved -from polyapi.generate import render_spec, create_empty_schemas_module, generate_functions, create_function +from polyapi.typedefs import SpecificationDto +from polyapi.utils import get_type_and_def, rewrite_reserved, to_type_module_alias +from polyapi.generate import render_spec, create_empty_schemas_module, generate_functions, create_function, add_function_file from polyapi.poly_schemas import generate_schemas, create_schema from polyapi.variables import generate_variables, create_variable @@ -660,3 +662,45 @@ def failing_add_function_file(full_path, function_name, spec): # The failed function directory should not exist func_dir = os.path.join(context_dir, "failing_function") self.assertFalse(os.path.exists(func_dir)) + + def test_add_function_file_aliases_pascal_case_type_module(self): + spec: SpecificationDto = { + "id": "test-func-id", + "name": "CreateAJsonPost", + "context": "test_context", + "type": "apiFunction", + "description": "Test function", + "language": "python", + "function": { + "arguments": [], + "returnType": { + "kind": "object", + "schema": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + }, + "required": ["id"], + "title": "ReturnType", + }, + }, + }, + } + + with tempfile.TemporaryDirectory() as temp_dir: + add_function_file(temp_dir, spec["name"], spec) + + init_path = os.path.join(temp_dir, "__init__.py") + with open(init_path, "r", encoding="utf-8") as file: + init_content = file.read() + + type_module_alias = to_type_module_alias(spec["name"]) + self.assertIn( + f"from . import {spec['name']} as {type_module_alias}", + init_content, + ) + self.assertIn(f"def {spec['name']}(", init_content) + self.assertIn( + f"-> {type_module_alias}.{spec['name']}Response", + init_content, + ) diff --git a/tests/test_server.py b/tests/test_server.py index c8126be..397816f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,6 +1,6 @@ import unittest -from polyapi.utils import to_func_namespace +from polyapi.utils import to_type_module_alias from .test_api import TWILIO from polyapi.server import render_server_function @@ -81,7 +81,7 @@ def test_render_function_twilio_server(self): self.assertIn(TWILIO["id"], func_str) self.assertIn("conversationSID: str", func_str) self.assertIn("authToken: str", func_str) - self.assertIn(f"-> {to_func_namespace(name)}.ResponseType", func_str) + self.assertIn(f"-> {to_type_module_alias(name)}.ResponseType", func_str) def test_render_function_get_products_count(self): return_type = GET_PRODUCTS_COUNT["function"]["returnType"] @@ -116,4 +116,4 @@ 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)