From 815ebeb7afe6b6e26a2c5a3afa7ddedbd1993fde Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Tue, 14 Apr 2026 11:43:28 -0400 Subject: [PATCH 1/2] fix: handle name collisions in codegen --- .../aws/codegen/AwsAuthIntegration.java | 17 +- .../codegen/AwsQueryProtocolGenerator.java | 3 +- .../python/aws/codegen/AwsRuntimeTypes.java | 47 +++++ ...sStandardRegionalEndpointsIntegration.java | 6 +- .../python/codegen/ClientGenerator.java | 46 ++--- .../python/codegen/HttpAuthGenerator.java | 13 +- .../codegen/HttpProtocolTestGenerator.java | 76 ++++---- .../python/codegen/PythonSymbolProvider.java | 31 ++++ .../smithy/python/codegen/RuntimeTypes.java | 175 ++++++++++++++++++ .../codegen/generators/ConfigGenerator.java | 26 ++- .../codegen/generators/EnumGenerator.java | 1 + .../codegen/generators/IntEnumGenerator.java | 1 + .../codegen/generators/ListGenerator.java | 23 +-- .../codegen/generators/MapGenerator.java | 23 +-- .../generators/OperationGenerator.java | 37 ++-- .../codegen/generators/SchemaGenerator.java | 32 ++-- .../generators/ServiceErrorGenerator.java | 7 +- .../generators/StructureGenerator.java | 27 +-- .../codegen/generators/UnionGenerator.java | 44 +++-- .../codegen/integrations/HttpApiKeyAuth.java | 32 ++-- .../RestJsonProtocolGenerator.java | 4 +- .../codegen/writer/ImportDeclarations.java | 30 +++ .../python/codegen/writer/PythonWriter.java | 156 +++++++++++++++- 23 files changed, 649 insertions(+), 208 deletions(-) create mode 100644 codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java create mode 100644 codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java index 8358c54be..e4ddf3a84 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java @@ -7,7 +7,6 @@ import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.REGION; import java.util.List; -import java.util.Set; import software.amazon.smithy.aws.traits.auth.SigV4Trait; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.model.shapes.ServiceShape; @@ -16,6 +15,7 @@ import software.amazon.smithy.python.codegen.CodegenUtils; import software.amazon.smithy.python.codegen.ConfigProperty; import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.integrations.AuthScheme; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; @@ -98,21 +98,22 @@ public void customize(GenerationContext context) { // must be accounted for. context.writerDelegator().useFileWriter(resolver.getDefinitionFile(), resolver.getNamespace(), writer -> { writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); - writer.addImport("smithy_core.interfaces.auth", "AuthOption", "AuthOptionProtocol"); - writer.addImports("smithy_core.auth", Set.of("AuthOption", "AuthParams")); - writer.addImport("smithy_core.shapes", "ShapeID"); writer.pushState(); writer.write(""" - def $1L(auth_params: AuthParams[Any, Any]) -> AuthOptionProtocol | None: - return AuthOption( - scheme_id=ShapeID($2S), + def $1L(auth_params: $3T[Any, Any]) -> $4T | None: + return $5T( + scheme_id=$6T($2S), identity_properties={}, # type: ignore signer_properties={} # type: ignore ) """, SIGV4_OPTION_GENERATOR_NAME, - SigV4Trait.ID.toString()); + SigV4Trait.ID.toString(), + RuntimeTypes.AUTH_PARAMS, + RuntimeTypes.AUTH_OPTION_INTERFACE, + RuntimeTypes.AUTH_OPTION, + RuntimeTypes.SHAPE_ID); writer.popState(); }); } diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsQueryProtocolGenerator.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsQueryProtocolGenerator.java index f02c5e815..d03463b45 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsQueryProtocolGenerator.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsQueryProtocolGenerator.java @@ -47,12 +47,11 @@ public ApplicationProtocol getApplicationProtocol(GenerationContext context) { @Override public void initializeProtocol(GenerationContext context, PythonWriter writer) { writer.addDependency(AwsPythonDependency.SMITHY_AWS_CORE.withOptionalDependencies("xml")); - writer.addImport("smithy_aws_core.aio.protocols", "AwsQueryClientProtocol"); var service = context.settings().service(context.model()); var serviceSymbol = context.symbolProvider().toSymbol(service); var serviceSchema = serviceSymbol.expectProperty(SymbolProperties.SCHEMA); var version = service.getVersion(); - writer.write("AwsQueryClientProtocol($T, $S)", serviceSchema, version); + writer.write("$1T($2T, $3S)", AwsRuntimeTypes.AWS_QUERY_CLIENT_PROTOCOL, serviceSchema, version); } @Override diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java new file mode 100644 index 000000000..8d78d0809 --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.aws.codegen; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Pre-defined Symbol constants for AWS-specific framework types used across generators. + * + *

Using these symbols with the {@code $T} formatter ensures that all framework type + * references participate in the writer's collision detection system. + */ +@SmithyUnstableApi +public final class AwsRuntimeTypes { + + // smithy_aws_core.aio.protocols + public static final Symbol REST_JSON_CLIENT_PROTOCOL = createSymbol( + "aio.protocols", + "RestJsonClientProtocol"); + public static final Symbol AWS_QUERY_CLIENT_PROTOCOL = createSymbol( + "aio.protocols", + "AwsQueryClientProtocol"); + + // smithy_aws_core.identity + public static final Symbol STATIC_CREDENTIALS_RESOLVER = createSymbol( + "identity", + "StaticCredentialsResolver"); + + // smithy_aws_core.endpoints.standard_regional + public static final Symbol STANDARD_REGIONAL_ENDPOINTS_RESOLVER = createSymbol( + "endpoints.standard_regional", + "StandardRegionalEndpointsResolver"); + + private AwsRuntimeTypes() {} + + private static Symbol createSymbol(String module, String name) { + var namespace = module.isEmpty() ? "smithy_aws_core" : "smithy_aws_core." + module; + return Symbol.builder() + .name(name) + .namespace(namespace, ".") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build(); + } +} diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java index c290468b4..346f94fe3 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java @@ -59,11 +59,9 @@ public void write(PythonWriter writer, String previousText, InitDefaultEndpointR .map(ServiceTrait::getEndpointPrefix) .orElse(context.settings().service().getName()); - writer.addImport("smithy_aws_core.endpoints.standard_regional", - "StandardRegionalEndpointsResolver", - "_RegionalResolver"); writer.write( - "self.endpoint_resolver = endpoint_resolver or _RegionalResolver(endpoint_prefix=$S)", + "self.endpoint_resolver = endpoint_resolver or $1T(endpoint_prefix=$2S)", + AwsRuntimeTypes.STANDARD_REGIONAL_ENDPOINTS_RESOLVER, endpointPrefix); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java index a56aff818..6a3f65c4a 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java @@ -50,6 +50,7 @@ public void run() { private void generateService(PythonWriter writer) { var serviceSymbol = symbolProvider.toSymbol(service); + writer.addLocallyDefinedSymbol(serviceSymbol); var configSymbol = CodegenUtils.getConfigSymbol(context.settings()); var pluginSymbol = CodegenUtils.getPluginSymbol(context.settings()); writer.addLogger(); @@ -71,7 +72,6 @@ private void generateService(PythonWriter writer) { } writer.addDependency(SmithyPythonDependency.SMITHY_CORE); - writer.addImport("smithy_core.retries", "RetryStrategyResolver"); writer.write(""" def __init__(self, config: $1T | None = None, plugins: list[$2T] | None = None): $3C @@ -86,12 +86,13 @@ def __init__(self, config: $1T | None = None, plugins: list[$2T] | None = None): for plugin in client_plugins: plugin(self._config) - self._retry_strategy_resolver = RetryStrategyResolver() + self._retry_strategy_resolver = $5T() """, configSymbol, pluginSymbol, writer.consumer(w -> writeConstructorDocs(w, serviceSymbol.getName())), - writer.consumer(w -> writeDefaultPlugins(w, defaultPlugins))); + writer.consumer(w -> writeDefaultPlugins(w, defaultPlugins)), + RuntimeTypes.RETRY_STRATEGY_RESOLVER); var topDownIndex = TopDownIndex.of(model); var eventStreamIndex = EventStreamIndex.of(model); @@ -209,18 +210,11 @@ private void writeSharedOperationInit( } writer.putContext("operation", symbolProvider.toSymbol(operation)); - writer.addImport("smithy_core.aio.client", "ClientCall"); - writer.addImport("smithy_core.interceptors", "InterceptorChain"); - writer.addImport("smithy_core.types", "TypedProperties"); - writer.addImport("smithy_core.aio.client", "RequestPipeline"); - writer.addImport("smithy_core.exceptions", "ExpectationNotMetError"); - writer.addImport("smithy_core.retries", "RetryStrategyOptions"); - writer.addImport("smithy_core.interfaces.retries", "RetryStrategy"); writer.addStdlibImport("copy", "deepcopy"); writer.write(""" operation_plugins: list[Plugin] = [ - $C + $1C ] if plugins: operation_plugins.extend(plugins) @@ -228,27 +222,33 @@ private void writeSharedOperationInit( for plugin in operation_plugins: plugin(config) if config.protocol is None or config.transport is None: - raise ExpectationNotMetError("protocol and transport MUST be set on the config to make calls.") + raise $2T("protocol and transport MUST be set on the config to make calls.") retry_strategy = await self._retry_strategy_resolver.resolve_retry_strategy( retry_strategy=config.retry_strategy ) - pipeline = RequestPipeline( + pipeline = $3T( protocol=config.protocol, transport=config.transport ) - call = ClientCall( + call = $4T( input=input, operation=${operation:T}, - context=TypedProperties({"config": config}), - interceptor=InterceptorChain(config.interceptors), + context=$5T({"config": config}), + interceptor=$6T(config.interceptors), auth_scheme_resolver=config.auth_scheme_resolver, supported_auth_schemes=config.auth_schemes, endpoint_resolver=config.endpoint_resolver, retry_strategy=retry_strategy, ) - """, writer.consumer(w -> writeDefaultPlugins(w, defaultPlugins))); + """, + writer.consumer(w -> writeDefaultPlugins(w, defaultPlugins)), + RuntimeTypes.EXPECTATION_NOT_MET_ERROR, + RuntimeTypes.REQUEST_PIPELINE, + RuntimeTypes.CLIENT_CALL, + RuntimeTypes.TYPED_PROPERTIES, + RuntimeTypes.INTERCEPTOR_CHAIN); } @@ -291,14 +291,14 @@ private void generateEventStreamOperation(PythonWriter writer, OperationShape op // the type declaration so much that it's no better than Any. if (inputStreamSymbol.isPresent()) { if (outputStreamSymbol.isPresent()) { - writer.addImport("smithy_core.aio.eventstream", "DuplexEventStream"); + writer.putContext("duplexEventStream", RuntimeTypes.DUPLEX_EVENT_STREAM); var outputDocs = "A `DuplexEventStream` for bidirectional streaming."; writer.write(""" async def ${operationName:L}( self, input: ${input:T}, plugins: list[${plugin:T}] | None = None - ) -> DuplexEventStream[${inputStream:T}, ${outputStream:T}, ${output:T}]: + ) -> ${duplexEventStream:T}[${inputStream:T}, ${outputStream:T}, ${output:T}]: ${C|} return await pipeline.duplex_stream( call, @@ -309,14 +309,14 @@ private void generateEventStreamOperation(PythonWriter writer, OperationShape op """, writer.consumer(w -> writeSharedOperationInit(w, operation, input, output, outputDocs))); } else { - writer.addImport("smithy_core.aio.eventstream", "InputEventStream"); + writer.putContext("inputEventStream", RuntimeTypes.INPUT_EVENT_STREAM); var outputDocs = "An `InputEventStream` for client-to-server streaming."; writer.write(""" async def ${operationName:L}( self, input: ${input:T}, plugins: list[${plugin:T}] | None = None - ) -> InputEventStream[${inputStream:T}, ${output:T}]: + ) -> ${inputEventStream:T}[${inputStream:T}, ${output:T}]: ${C|} return await pipeline.input_stream( call, @@ -326,14 +326,14 @@ private void generateEventStreamOperation(PythonWriter writer, OperationShape op writer.consumer(w -> writeSharedOperationInit(w, operation, input, output, outputDocs))); } } else { - writer.addImport("smithy_core.aio.eventstream", "OutputEventStream"); + writer.putContext("outputEventStream", RuntimeTypes.OUTPUT_EVENT_STREAM); var outputDocs = "An `OutputEventStream` for server-to-client streaming."; writer.write(""" async def ${operationName:L}( self, input: ${input:T}, plugins: list[${plugin:T}] | None = None - ) -> OutputEventStream[${outputStream:T}, ${output:T}]: + ) -> ${outputEventStream:T}[${outputStream:T}, ${output:T}]: ${C|} return await pipeline.output_stream( call, diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java index 69780212a..ebf4269f0 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java @@ -69,21 +69,22 @@ private void generateAuthSchemeResolver( .toList(); writer.pushState(new GenerateHttpAuthSchemeResolverSection(resolvedAuthSchemes)); + writer.addLocallyDefinedSymbol(resolverSymbol); writer.addDependency(SmithyPythonDependency.SMITHY_CORE); writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); - writer.addImport("smithy_core.interfaces.auth", "AuthOption", "AuthOptionProtocol"); - writer.addImport("smithy_core.auth", "AuthParams"); writer.addStdlibImport("typing", "Any"); writer.write(""" class $1L: - def resolve_auth_scheme(self, auth_parameters: AuthParams[Any, Any]) -> list[AuthOptionProtocol]: - auth_options: list[AuthOptionProtocol] = [] + def resolve_auth_scheme(self, auth_parameters: $2T[Any, Any]) -> list[$3T]: + auth_options: list[$3T] = [] - ${2C|} - ${3C|} + ${4C|} + ${5C|} """, resolverSymbol.getName(), + RuntimeTypes.AUTH_PARAMS, + RuntimeTypes.AUTH_OPTION_INTERFACE, writer.consumer(w -> writeOperationAuthOptions(w, supportedAuthSchemes)), writer.consumer(w -> writeAuthOptions(w, resolvedAuthSchemes))); writer.popState(); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpProtocolTestGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpProtocolTestGenerator.java index c432847bb..169d2c035 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpProtocolTestGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpProtocolTestGenerator.java @@ -182,13 +182,12 @@ private void generateRequestTest(OperationShape operation, HttpRequestTestCase t } else { path = ""; } - writer.addImport("smithy_core.retries", "SimpleRetryStrategy"); writeClientBlock(context.symbolProvider().toSymbol(service), testCase, Optional.of(() -> { writer.write(""" config = $T( endpoint_uri="https://$L/$L", transport = $T(), - retry_strategy=SimpleRetryStrategy(max_attempts=1), + retry_strategy=$T(max_attempts=1), ${C|} ) """, @@ -196,6 +195,7 @@ private void generateRequestTest(OperationShape operation, HttpRequestTestCase t host, path, REQUEST_TEST_ASYNC_HTTP_CLIENT_SYMBOL, + RuntimeTypes.SIMPLE_RETRY_STRATEGY, (Runnable) this::writeSigV4TestConfig); })); @@ -403,8 +403,7 @@ private void writeRequestBodyComparison(HttpMessageTestCase testCase, PythonWrit return; } writer.addDependency(SmithyPythonDependency.SMITHY_CORE); - writer.addImport("smithy_core.aio.types", "AsyncBytesReader"); - writer.write("actual_body_content = await AsyncBytesReader(actual.body or b'').read()"); + writer.write("actual_body_content = await $T(actual.body or b'').read()", RuntimeTypes.ASYNC_BYTES_READER); writer.write("expected_body_content = b$S", testCase.getBody().get()); compareMediaBlob(testCase, writer); } @@ -563,13 +562,11 @@ private void assertResponseEqual(HttpMessageTestCase testCase, Shape operationOr var memberName = context.symbolProvider().toMemberName(member); if (member.equals(streamingMember)) { writer.addDependency(SmithyPythonDependency.SMITHY_CORE); - writer.addImport("smithy_core.aio.interfaces", "AsyncByteStream"); - writer.addImport("smithy_core.aio.types", "AsyncBytesReader"); writer.write(""" - assert isinstance(actual.$1L, AsyncByteStream) + assert isinstance(actual.$1L, $2T) actual_body_content = await actual.$1L.read() - expected_body_content = await AsyncBytesReader(expected.$1L).read() - """, memberName); + expected_body_content = await $3T(expected.$1L).read() + """, memberName, RuntimeTypes.ASYNC_BYTE_STREAM, RuntimeTypes.ASYNC_BYTES_READER); compareMediaBlob(testCase, writer); continue; } @@ -628,52 +625,46 @@ private void writeSigV4TestConfig() { if (!service.hasTrait(SigV4Trait.class)) { return; } - writer.addImport("smithy_aws_core.identity", "StaticCredentialsResolver"); writer.write(""" region="us-east-1", aws_access_key_id="test-access-key-id", aws_secret_access_key="test-secret-access-key", - aws_credentials_identity_resolver=StaticCredentialsResolver(), - """); + aws_credentials_identity_resolver=$T(), + """, RuntimeTypes.STATIC_CREDENTIALS_RESOLVER); } private void writeUtilStubs(Symbol serviceSymbol) { LOGGER.fine(String.format("Writing utility stubs for %s : %s", serviceSymbol.getName(), protocol.getName())); + writer.addLocallyDefinedSymbol(TEST_HTTP_SERVICE_ERR_SYMBOL); + writer.addLocallyDefinedSymbol(REQUEST_TEST_ASYNC_HTTP_CLIENT_SYMBOL); + writer.addLocallyDefinedSymbol(RESPONSE_TEST_ASYNC_HTTP_CLIENT_SYMBOL); writer.addDependency(SmithyPythonDependency.SMITHY_CORE); writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); - writer.addImports("smithy_http.interfaces", - Set.of( - "HTTPRequestConfiguration", - "HTTPClientConfiguration")); - writer.addImports("smithy_http.aio.interfaces", Set.of("HTTPRequest", "HTTPResponse")); - writer.addImport("smithy_http", "tuples_to_fields"); - writer.addImport("smithy_http.aio", "HTTPResponse", "_HTTPResponse"); - writer.addImport("smithy_core.aio.utils", "async_list"); writer.write(""" class $1L($2T): ""\"A test error that subclasses the service-error for protocol tests.""\" - def __init__(self, request: HTTPRequest): + def __init__(self, request: $3T): self.request = request - class $3L: + class $4L: ""\"An asynchronous HTTP client solely for testing purposes.""\" TIMEOUT_EXCEPTIONS = () - def __init__(self, *, client_config: HTTPClientConfiguration | None = None): + def __init__(self, *, client_config: $5T | None = None): self._client_config = client_config async def send( - self, request: HTTPRequest, *, request_config: HTTPRequestConfiguration | None = None - ) -> HTTPResponse: + self, request: $3T, *, request_config: $6T | None = None + ) -> $7T: # Raise the exception with the request object to bypass actual request handling raise $1T(request) - class $4L: + class $8L: ""\"An asynchronous HTTP client solely for testing purposes.""\" TIMEOUT_EXCEPTIONS = () @@ -681,30 +672,37 @@ class $4L: def __init__( self, *, - client_config: HTTPClientConfiguration | None = None, + client_config: $5T | None = None, status: int = 200, headers: list[tuple[str, str]] | None = None, body: bytes = b"", ): self._client_config = client_config self.status = status - self.fields = tuples_to_fields(headers or []) + self.fields = $9T(headers or []) self.body = body async def send( - self, request: HTTPRequest, *, request_config: HTTPRequestConfiguration | None = None - ) -> _HTTPResponse: + self, request: $3T, *, request_config: $6T | None = None + ) -> ${10T}: # Pre-construct the response from the request and return it - return _HTTPResponse( + return ${10T}( status=self.status, fields=self.fields, - body=async_list([self.body]), + body=${11T}([self.body]), ) """, TEST_HTTP_SERVICE_ERR_SYMBOL, CodegenUtils.getServiceError(context.settings()), + RuntimeTypes.HTTP_REQUEST, REQUEST_TEST_ASYNC_HTTP_CLIENT_SYMBOL, - RESPONSE_TEST_ASYNC_HTTP_CLIENT_SYMBOL); + RuntimeTypes.HTTP_CLIENT_CONFIGURATION, + RuntimeTypes.HTTP_REQUEST_CONFIGURATION, + RuntimeTypes.HTTP_RESPONSE, + RESPONSE_TEST_ASYNC_HTTP_CLIENT_SYMBOL, + RuntimeTypes.TUPLES_TO_FIELDS, + RuntimeTypes.HTTP_RESPONSE_IMPL, + RuntimeTypes.ASYNC_LIST); } /** @@ -726,8 +724,8 @@ public Void arrayNode(ArrayNode node) { if (inputShape.isListShape()) { var target = model.expectShape(inputShape.asListShape().get().getMember().getTarget()); if (target.isDocumentShape()) { - writer.addImport("smithy_core.documents", "Document"); - openList = "Document(["; + var docName = writer.format("$T", RuntimeTypes.DOCUMENT); + openList = docName + "(["; closeList = "])"; } } @@ -832,8 +830,8 @@ private Void structureMemberShapes(StructureShape container, ObjectNode node) { var formatString = "$L = $C,"; if (targetShape.isDocumentShape()) { - writer.addImport("smithy_core.documents", "Document"); - formatString = "$L = Document($C),"; + var docName = writer.format("$T", RuntimeTypes.DOCUMENT); + formatString = "$L = " + docName + "($C),"; } writer.write(formatString, @@ -860,8 +858,8 @@ private Void mapShape(MapShape shape, ObjectNode node) { var formatString = "$S: $C,"; if (targetShape.isDocumentShape()) { - writer.addImport("smithy_core.documents", "Document"); - formatString = "$S: Document($C),"; + var docName = writer.format("$T", RuntimeTypes.DOCUMENT); + formatString = "$S: " + docName + "($C),"; } writer.write(formatString, diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/PythonSymbolProvider.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/PythonSymbolProvider.java index a44f7cb91..654d005cb 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/PythonSymbolProvider.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/PythonSymbolProvider.java @@ -6,7 +6,9 @@ import static java.lang.String.format; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; import java.util.logging.Logger; import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider; import software.amazon.smithy.codegen.core.ReservedWordsBuilder; @@ -70,6 +72,7 @@ public final class PythonSymbolProvider implements SymbolProvider, ShapeVisitor< private final ReservedWordSymbolProvider.Escaper errorMemberEscaper; private final PythonSettings settings; private final ServiceShape service; + private final Set allShapeNames; public PythonSymbolProvider(Model model, PythonSettings settings) { this.model = model; @@ -99,6 +102,25 @@ public PythonSymbolProvider(Model model, PythonSettings settings) { .memberReservedWords(reservedErrorMembers.build()) .escapePredicate((shape, symbol) -> !StringUtils.isEmpty(symbol.getDefinitionFile())) .buildEscaper(); + + // Collect all shape names that will be generated as PascalCase classes in models.py. + // Used to detect collisions with synthesized names (union variants, unknown types). + this.allShapeNames = collectAllShapeNames(); + } + + /** + * Collects the PascalCase names of all shapes that will be generated as classes. + */ + private Set collectAllShapeNames() { + var names = new HashSet(); + for (Shape shape : model.toSet()) { + if (shape.isStructureShape() || shape.isUnionShape() + || shape.isEnumShape() + || shape.isIntEnumShape()) { + names.add(getDefaultShapeName(shape)); + } + } + return names; } private String escapeWord(String word) { @@ -352,6 +374,9 @@ public Symbol unionShape(UnionShape shape) { String name = getDefaultShapeName(shape); var unknownName = name + "Unknown"; + if (allShapeNames.contains(unknownName)) { + unknownName = name + "_Unknown"; + } var unknownSymbol = createGeneratedSymbolBuilder(shape, unknownName, SHAPES_FILE).build(); var builder = createGeneratedSymbolBuilder(shape, name, SHAPES_FILE) .putProperty(SymbolProperties.UNION_UNKNOWN, unknownSymbol); @@ -374,6 +399,12 @@ public Symbol memberShape(MemberShape shape) { // Union members, unlike other shape members, have types generated for them. var containerSymbol = container.accept(this); var name = containerSymbol.getName() + StringUtils.capitalize(shape.getMemberName()); + // Check if the synthesized variant name collides with any service-defined shape. + // Smithy shape names cannot contain underscores, so using "_" as separator + // guarantees the disambiguated name won't collide with any shape. + if (allShapeNames.contains(name)) { + name = containerSymbol.getName() + "_" + StringUtils.capitalize(shape.getMemberName()); + } return createGeneratedSymbolBuilder(shape, name, SHAPES_FILE, false) .putProperty(SymbolProperties.SCHEMA, containerSymbol.expectProperty(SymbolProperties.SCHEMA)) .build(); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java new file mode 100644 index 000000000..3f0173d90 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/RuntimeTypes.java @@ -0,0 +1,175 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Pre-defined Symbol constants for all framework types used across generators. + * + *

Using these symbols with the {@code $T} formatter ensures that all framework type + * references participate in the writer's collision detection system. When a service model + * defines a shape with the same name as a framework type, the writer will automatically + * alias the import to avoid shadowing. + */ +@SmithyUnstableApi +public final class RuntimeTypes { + + // smithy_core.schemas + public static final Symbol SCHEMA = createSymbol("schemas", "Schema", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol API_OPERATION = + createSymbol("schemas", "APIOperation", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.shapes + public static final Symbol SHAPE_ID = createSymbol("shapes", "ShapeID", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol SHAPE_TYPE = createSymbol("shapes", "ShapeType", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.serializers + public static final Symbol SHAPE_SERIALIZER = + createSymbol("serializers", "ShapeSerializer", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.deserializers + public static final Symbol SHAPE_DESERIALIZER = + createSymbol("deserializers", "ShapeDeserializer", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.documents + public static final Symbol DOCUMENT = createSymbol("documents", "Document", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol TYPE_REGISTRY = + createSymbol("documents", "TypeRegistry", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.exceptions + public static final Symbol MODELED_ERROR = + createSymbol("exceptions", "ModeledError", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol SERIALIZATION_ERROR = + createSymbol("exceptions", "SerializationError", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol EXPECTATION_NOT_MET_ERROR = + createSymbol("exceptions", "ExpectationNotMetError", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.traits + public static final Symbol TRAIT = createSymbol("traits", "Trait", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol API_KEY_LOCATION = + createSymbol("traits", "APIKeyLocation", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.auth + public static final Symbol AUTH_PARAMS = createSymbol("auth", "AuthParams", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol AUTH_OPTION = createSymbol("auth", "AuthOption", SmithyPythonDependency.SMITHY_CORE); + + // Note: AuthOption from smithy_core.interfaces.auth has the same simple name as + // AuthOption from smithy_core.auth. The collision resolver will automatically + // disambiguate them with module-based aliases. + public static final Symbol AUTH_OPTION_INTERFACE = + createSymbol("interfaces.auth", "AuthOption", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.retries + public static final Symbol RETRY_STRATEGY_RESOLVER = + createSymbol("retries", "RetryStrategyResolver", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol RETRY_STRATEGY_OPTIONS = + createSymbol("retries", "RetryStrategyOptions", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol SIMPLE_RETRY_STRATEGY = + createSymbol("retries", "SimpleRetryStrategy", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.interfaces.retries + public static final Symbol RETRY_STRATEGY = + createSymbol("interfaces.retries", "RetryStrategy", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.types + public static final Symbol TYPED_PROPERTIES = + createSymbol("types", "TypedProperties", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.interceptors + public static final Symbol INTERCEPTOR_CHAIN = + createSymbol("interceptors", "InterceptorChain", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol INTERCEPTOR = + createSymbol("interceptors", "Interceptor", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.aio.client + public static final Symbol CLIENT_CALL = + createSymbol("aio.client", "ClientCall", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol REQUEST_PIPELINE = + createSymbol("aio.client", "RequestPipeline", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.aio.eventstream + public static final Symbol DUPLEX_EVENT_STREAM = + createSymbol("aio.eventstream", "DuplexEventStream", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol INPUT_EVENT_STREAM = + createSymbol("aio.eventstream", "InputEventStream", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol OUTPUT_EVENT_STREAM = + createSymbol("aio.eventstream", "OutputEventStream", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.aio.interfaces + public static final Symbol ASYNC_BYTE_STREAM = + createSymbol("aio.interfaces", "AsyncByteStream", SmithyPythonDependency.SMITHY_CORE); + public static final Symbol ENDPOINT_RESOLVER = + createSymbol("aio.interfaces", "EndpointResolver", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.aio.endpoints + public static final Symbol STATIC_ENDPOINT_RESOLVER = + createSymbol("aio.endpoints", "StaticEndpointResolver", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.aio.types + public static final Symbol ASYNC_BYTES_READER = + createSymbol("aio.types", "AsyncBytesReader", SmithyPythonDependency.SMITHY_CORE); + + // smithy_core.aio.utils + public static final Symbol ASYNC_LIST = createSymbol("aio.utils", "async_list", SmithyPythonDependency.SMITHY_CORE); + + // smithy_http + public static final Symbol TUPLES_TO_FIELDS = + createSymbol("", "tuples_to_fields", SmithyPythonDependency.SMITHY_HTTP); + + // Note: HTTPResponse from smithy_http.aio has the same simple name as HTTPResponse + // from smithy_http.aio.interfaces. The collision resolver will automatically + // disambiguate them with module-based aliases. + public static final Symbol HTTP_RESPONSE_IMPL = + createSymbol("aio", "HTTPResponse", SmithyPythonDependency.SMITHY_HTTP); + + // smithy_http.interfaces + public static final Symbol HTTP_REQUEST_CONFIGURATION = + createSymbol("interfaces", "HTTPRequestConfiguration", SmithyPythonDependency.SMITHY_HTTP); + public static final Symbol HTTP_CLIENT_CONFIGURATION = + createSymbol("interfaces", "HTTPClientConfiguration", SmithyPythonDependency.SMITHY_HTTP); + + // smithy_http.aio.interfaces + public static final Symbol HTTP_REQUEST = + createSymbol("aio.interfaces", "HTTPRequest", SmithyPythonDependency.SMITHY_HTTP); + public static final Symbol HTTP_RESPONSE = + createSymbol("aio.interfaces", "HTTPResponse", SmithyPythonDependency.SMITHY_HTTP); + + // smithy_http.aio.crt + public static final Symbol AWS_CRT_HTTP_CLIENT = + createSymbol("aio.crt", "AWSCRTHTTPClient", SmithyPythonDependency.SMITHY_HTTP); + + // smithy_http.aio.aiohttp + public static final Symbol AIOHTTP_CLIENT = + createSymbol("aio.aiohttp", "AIOHTTPClient", SmithyPythonDependency.SMITHY_HTTP); + + // smithy_http.aio.identity.apikey + public static final Symbol API_KEY_IDENTITY_RESOLVER = + createSymbol("aio.identity.apikey", "APIKeyIdentityResolver", SmithyPythonDependency.SMITHY_HTTP); + + // smithy_aws_core.aio.protocols + public static final Symbol REST_JSON_CLIENT_PROTOCOL = createSymbol( + "aio.protocols", + "RestJsonClientProtocol", + SmithyPythonDependency.SMITHY_AWS_CORE); + + // smithy_aws_core.identity + public static final Symbol STATIC_CREDENTIALS_RESOLVER = createSymbol( + "identity", + "StaticCredentialsResolver", + SmithyPythonDependency.SMITHY_AWS_CORE); + + private RuntimeTypes() {} + + private static Symbol createSymbol(String module, String name, PythonDependency dependency) { + var namespace = module.isEmpty() ? dependency.packageName() : dependency.packageName() + "." + module; + return Symbol.builder() + .name(name) + .namespace(namespace, ".") + .addDependency(dependency) + .build(); + } +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java index de03d42d0..b02f0b31f 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java @@ -22,6 +22,7 @@ import software.amazon.smithy.python.codegen.ConfigProperty; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.PythonSettings; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; @@ -84,18 +85,15 @@ public final class ConfigGenerator implements Runnable { .build(), ConfigProperty.builder() .name("endpoint_resolver") - .type(Symbol.builder() - .name("_EndpointResolver") - .build()) + .type(RuntimeTypes.ENDPOINT_RESOLVER) .documentation(""" The endpoint resolver used to resolve the final endpoint per-operation based on the \ configuration.""") .nullable(false) .initialize(writer -> { - writer.addImport("smithy_core.aio.interfaces", "EndpointResolver", "_EndpointResolver"); writer.pushState(new InitDefaultEndpointResolverSection()); - writer.addImport("smithy_core.aio.endpoints", "StaticEndpointResolver"); - writer.write("self.endpoint_resolver = endpoint_resolver or StaticEndpointResolver()"); + writer.write("self.endpoint_resolver = endpoint_resolver or $T()", + RuntimeTypes.STATIC_ENDPOINT_RESOLVER); writer.popState(); }) .build()); @@ -155,8 +153,8 @@ private static List getProtocolProperties(GenerationContext cont transportBuilder .initialize(writer -> { writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("awscrt")); - writer.addImport("smithy_http.aio.crt", "AWSCRTHTTPClient"); - writer.write("self.transport = transport or AWSCRTHTTPClient()"); + writer.write("self.transport = transport or $T()", + RuntimeTypes.AWS_CRT_HTTP_CLIENT); }); } else { @@ -164,8 +162,8 @@ private static List getProtocolProperties(GenerationContext cont .initialize(writer -> { writer.addDependency( SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("aiohttp")); - writer.addImport("smithy_http.aio.aiohttp", "AIOHTTPClient"); - writer.write("self.transport = transport or AIOHTTPClient()"); + writer.write("self.transport = transport or $T()", + RuntimeTypes.AIOHTTP_CLIENT); }); } } @@ -244,12 +242,12 @@ private static void writeDefaultAuthSchemes(GenerationContext context, PythonWri var service = context.settings().service(context.model()); writer.openBlock("self.auth_schemes = auth_schemes or {"); - writer.addImport("smithy_core.shapes", "ShapeID"); for (PythonIntegration integration : context.integrations()) { for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) { if (plugin.matchesService(context.model(), service) && plugin.getAuthScheme().isPresent()) { var scheme = plugin.getAuthScheme().get(); - writer.write("ShapeID($S): ${C|},", + writer.write("$T($S): ${C|},", + RuntimeTypes.SHAPE_ID, scheme.getAuthTrait(), writer.consumer(w -> scheme.initializeScheme(context, writer, service))); } @@ -286,7 +284,6 @@ private void writeInterceptorsType(PythonWriter writer) { writer.addStdlibImport("typing", "Union"); writer.addDependency(SmithyPythonDependency.SMITHY_CORE); - writer.getImportContainer().addImport("smithy_core.interceptors", "Interceptor", "Interceptor"); writer.writeInline("_ServiceInterceptor = Union["); var iter = operationShapes.iterator(); @@ -297,7 +294,7 @@ private void writeInterceptorsType(PythonWriter writer) { // TODO: pull the transport request/response types off of the application protocol writer.addStdlibImport("typing", "Any"); - writer.writeInline("Interceptor[$T, $T, Any, Any]", input, output); + writer.writeInline("$T[$T, $T, Any, Any]", RuntimeTypes.INTERCEPTOR, input, output); if (iter.hasNext()) { writer.writeInline(", "); } else { @@ -341,6 +338,7 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { .map(ServiceTrait::getSdkId) .orElse(context.settings().service().getName()); writer.pushState(new ConfigSection(finalProperties)); + writer.addLocallyDefinedSymbol(configSymbol); writer.addStdlibImport("dataclasses", "dataclass"); writer.write(""" @dataclass(init=False) diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EnumGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EnumGenerator.java index 354054503..15fe18497 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EnumGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EnumGenerator.java @@ -30,6 +30,7 @@ public void run() { var enumSymbol = context.symbolProvider().toSymbol(shape).expectProperty(SymbolProperties.ENUM_SYMBOL); context.writerDelegator().useShapeWriter(shape, writer -> { writer.addStdlibImport("enum", "StrEnum"); + writer.addLocallyDefinedSymbol(enumSymbol); writer.openBlock("class $L(StrEnum):", "", enumSymbol.getName(), () -> { shape.getTrait(DocumentationTrait.class).ifPresent(trait -> { writer.writeDocs(trait.getValue(), context); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/IntEnumGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/IntEnumGenerator.java index 8816f9d38..12cee0e50 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/IntEnumGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/IntEnumGenerator.java @@ -27,6 +27,7 @@ public void run() { var enumSymbol = directive.symbol().expectProperty(SymbolProperties.ENUM_SYMBOL); directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> { writer.addStdlibImport("enum", "IntEnum"); + writer.addLocallyDefinedSymbol(enumSymbol); writer.openBlock("class $L(IntEnum):", "", enumSymbol.getName(), () -> { directive.shape().getTrait(DocumentationTrait.class).ifPresent(trait -> { writer.writeDocs(trait.getValue(), directive.context()); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java index 7fe3c2d6e..f5026914e 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java @@ -7,6 +7,7 @@ import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.traits.SparseTrait; import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.writer.PythonWriter; import software.amazon.smithy.utils.SmithyInternalApi; @@ -36,13 +37,11 @@ private void generateSerializer() { var listSymbol = context.symbolProvider().toSymbol(shape); var serializerSymbol = listSymbol.expectProperty(SymbolProperties.SERIALIZER); writer.pushState(); - writer.addImport("smithy_core.serializers", "ShapeSerializer"); - writer.addImport("smithy_core.schemas", "Schema"); writer.putContext("sparse", shape.hasTrait(SparseTrait.class)); writer.putContext("propertyName", "e"); var memberTarget = context.model().expectShape(shape.getMember().getTarget()); writer.write(""" - def $1L(serializer: ShapeSerializer, schema: Schema, value: $2T) -> None: + def $1L(serializer: $2T, schema: $3T, value: $4T) -> None: member_schema = schema.members["member"] with serializer.begin_list(schema, len(value)) as ls: for e in value: @@ -50,14 +49,16 @@ private void generateSerializer() { if e is None: ls.write_null(member_schema) else: - ${3C|} + ${5C|} ${/sparse} ${^sparse} - ${3C|} + ${5C|} ${/sparse} """, serializerSymbol.getName(), + RuntimeTypes.SHAPE_SERIALIZER, + RuntimeTypes.SCHEMA, listSymbol, writer.consumer(w -> memberTarget.accept( new MemberSerializerGenerator(context, w, shape.getMember(), "ls")))); @@ -70,28 +71,28 @@ private void generateDeserializer() { var memberTarget = context.model().expectShape(shape.getMember().getTarget()); writer.pushState(); - writer.addImport("smithy_core.serializers", "ShapeSerializer"); - writer.addImport("smithy_core.schemas", "Schema"); var sparse = shape.hasTrait(SparseTrait.class); writer.putContext("sparse", sparse); writer.putContext("includeSchema", sparse || (!memberTarget.isUnionShape() && !memberTarget.isStructureShape())); writer.write(""" - def $1L(deserializer: ShapeDeserializer, schema: Schema) -> $2T: - result: $2T = [] + def $1L(deserializer: $2T, schema: $3T) -> $4T: + result: $4T = [] ${?includeSchema} member_schema = schema.members["member"] ${/includeSchema} - def _read_value(d: ShapeDeserializer): + def _read_value(d: $2T): if d.is_null(): d.read_null() ${?sparse}result.append(None)${/sparse} else: - result.append(${3C|}) + result.append(${5C|}) deserializer.read_list(schema, _read_value) return result """, deserializerSymbol.getName(), + RuntimeTypes.SHAPE_DESERIALIZER, + RuntimeTypes.SCHEMA, listSymbol, writer.consumer(w -> memberTarget.accept( new MemberDeserializerGenerator(context, w, shape.getMember(), "d")))); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java index 0923d7ee7..cf65a83c7 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java @@ -7,6 +7,7 @@ import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.traits.SparseTrait; import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.writer.PythonWriter; import software.amazon.smithy.utils.SmithyInternalApi; @@ -36,8 +37,6 @@ private void generateSerializer() { var mapSymbol = context.symbolProvider().toSymbol(shape); var serializerSymbol = mapSymbol.expectProperty(SymbolProperties.SERIALIZER); writer.pushState(); - writer.addImport("smithy_core.serializers", "ShapeSerializer"); - writer.addImport("smithy_core.schemas", "Schema"); writer.putContext("sparse", shape.hasTrait(SparseTrait.class)); writer.putContext("propertyName", "v"); var valueTarget = context.model().expectShape(shape.getValue().getTarget()); @@ -45,7 +44,7 @@ private void generateSerializer() { // Note that we have to disable typing in the sparse case because pyright for some reason isn't // narrowing out the None even though there's an explicit is None check. writer.write(""" - def $1L(serializer: ShapeSerializer, schema: Schema, value: $2T) -> None: + def $1L(serializer: $2T, schema: $3T, value: $4T) -> None: with serializer.begin_map(schema, len(value)) as m: value_schema = schema.members["value"] for k, v in value.items(): @@ -53,14 +52,16 @@ private void generateSerializer() { if v is None: m.entry(k, lambda vs: vs.write_null(value_schema)) else: - m.entry(k, lambda vs: ${3C|}) # type: ignore + m.entry(k, lambda vs: ${5C|}) # type: ignore ${/sparse} ${^sparse} - m.entry(k, lambda vs: ${3C|}) + m.entry(k, lambda vs: ${5C|}) ${/sparse} """, serializerSymbol.getName(), + RuntimeTypes.SHAPE_SERIALIZER, + RuntimeTypes.SCHEMA, mapSymbol, writer.consumer(w -> valueTarget.accept( new MemberSerializerGenerator(context, w, shape.getValue(), "vs")))); @@ -73,25 +74,25 @@ private void generateDeserializer() { var valueTarget = context.model().expectShape(shape.getValue().getTarget()); writer.pushState(); - writer.addImport("smithy_core.serializers", "ShapeSerializer"); - writer.addImport("smithy_core.schemas", "Schema"); var sparse = shape.hasTrait(SparseTrait.class); writer.putContext("sparse", sparse); writer.putContext("includeSchema", sparse || (!valueTarget.isUnionShape() && !valueTarget.isStructureShape())); writer.write(""" - def $1L(deserializer: ShapeDeserializer, schema: Schema) -> $2T: - result: $2T = {} + def $1L(deserializer: $2T, schema: $3T) -> $4T: + result: $4T = {} value_schema = schema.members["value"] - def _read_value(k: str, d: ShapeDeserializer): + def _read_value(k: str, d: $2T): if d.is_null(): d.read_null() ${?sparse}result[k] = None${/sparse} else: - result[k] = ${3C|} + result[k] = ${5C|} deserializer.read_map(schema, _read_value) return result """, deserializerSymbol.getName(), + RuntimeTypes.SHAPE_DESERIALIZER, + RuntimeTypes.SCHEMA, listSymbol, writer.consumer(w -> valueTarget.accept( new MemberDeserializerGenerator(context, w, shape.getValue(), "d")))); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/OperationGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/OperationGenerator.java index 9557a93ae..65386f33f 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/OperationGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/OperationGenerator.java @@ -12,6 +12,7 @@ import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.writer.PythonWriter; @@ -38,38 +39,39 @@ public OperationGenerator(GenerationContext context, PythonWriter writer, Operat @Override public void run() { var opSymbol = symbolProvider.toSymbol(shape); + writer.addLocallyDefinedSymbol(opSymbol); var inSymbol = symbolProvider.toSymbol(model.expectShape(shape.getInputShape())); var outSymbol = symbolProvider.toSymbol(model.expectShape(shape.getOutputShape())); writer.addStdlibImport("dataclasses", "dataclass"); writer.addDependency(SmithyPythonDependency.SMITHY_CORE); - writer.addImport("smithy_core.schemas", "APIOperation"); - writer.addImport("smithy_core.documents", "TypeRegistry"); writer.write(""" - $1L = APIOperation( - input = $2T, - output = $3T, - schema = $4T, - input_schema = $5T, - output_schema = $6T, - error_registry = TypeRegistry({ - $7C + $1L = $2T( + input = $3T, + output = $4T, + schema = $5T, + input_schema = $6T, + output_schema = $7T, + error_registry = $8T({ + $9C }), effective_auth_schemes = [ - $8C + ${10C} ], error_schemas = [ - $9C + ${11C} ] ) """, opSymbol.getName(), + RuntimeTypes.API_OPERATION, inSymbol, outSymbol, opSymbol.expectProperty(SymbolProperties.SCHEMA), inSymbol.expectProperty(SymbolProperties.SCHEMA), outSymbol.expectProperty(SymbolProperties.SCHEMA), + RuntimeTypes.TYPE_REGISTRY, writer.consumer(this::writeErrorTypeRegistry), writer.consumer(this::writeAuthSchemes), writer.consumer(this::writeErrorSchemas)); @@ -77,12 +79,9 @@ public void run() { private void writeErrorTypeRegistry(PythonWriter writer) { List errors = shape.getErrors(); - if (!errors.isEmpty()) { - writer.addImport("smithy_core.shapes", "ShapeID"); - } for (var error : errors) { var errSymbol = symbolProvider.toSymbol(model.expectShape(error)); - writer.write("ShapeID($S): $T,", error, errSymbol); + writer.write("$1T($2S): $3T,", RuntimeTypes.SHAPE_ID, error, errSymbol); } } @@ -99,12 +98,8 @@ private void writeAuthSchemes(PythonWriter writer) { shape.getId(), ServiceIndex.AuthSchemeMode.NO_AUTH_AWARE); - if (!authSchemes.isEmpty()) { - writer.addImport("smithy_core.shapes", "ShapeID"); - } - for (var authSchemeId : authSchemes.keySet()) { - writer.write("ShapeID($S),", authSchemeId); + writer.write("$1T($2S),", RuntimeTypes.SHAPE_ID, authSchemeId); } } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SchemaGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SchemaGenerator.java index 4f97023ae..ac6bd4657 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SchemaGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SchemaGenerator.java @@ -27,6 +27,7 @@ import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.writer.PythonWriter; @@ -96,11 +97,10 @@ public static void generateAll(GenerationContext context, Collection shap } private void writeShapeSchema(PythonWriter writer, Shape shape) { - writer.addImport("smithy_core.schemas", "Schema"); - writer.addImports("smithy_core.shapes", Set.of("ShapeID", "ShapeType")); writer.pushState(); var symbol = context.symbolProvider().toSymbol(shape).expectProperty(SymbolProperties.SCHEMA).getSymbol(); + writer.addLocallyDefinedSymbol(symbol); var traits = filterTraits(shape); writer.putContext("isCollection", shape.isStructureShape() || !shape.members().isEmpty()); @@ -109,19 +109,22 @@ private void writeShapeSchema(PythonWriter writer, Shape shape) { writer.putContext("hasTraits", !traits.isEmpty()); writer.write(""" - $L = Schema${?isCollection}.collection${/isCollection}( - id=ShapeID($S), - ${^isStructure}shape_type=ShapeType.${shapeType:L},${/isStructure} + $1L = $2T${?isCollection}.collection${/isCollection}( + id=$3T($4S), + ${^isStructure}shape_type=$5T.${shapeType:L},${/isStructure} ${?hasTraits} traits=[ - ${C|} + ${6C|} ], ${/hasTraits} - ${C|} + ${7C|} ) """, symbol.getName(), + RuntimeTypes.SCHEMA, + RuntimeTypes.SHAPE_ID, shape.getId(), + RuntimeTypes.SHAPE_TYPE, writer.consumer(w -> writeTraits(w, traits)), writer.consumer(w -> writeSchemaMembers(w, shape))); writer.popState(); @@ -142,13 +145,14 @@ private Map> filterTraits(Shape shape) { } private void writeTraits(PythonWriter writer, Map> traits) { - writer.addImport("smithy_core.traits", "Trait"); writer.pushState(); writer.putContext("traits", traits); writer.write(""" ${#traits} - Trait.new(id=ShapeID(${key:S})${?value}, value=${value:N}${/value}), - ${/traits}"""); + $1T.new(id=$2T(${key:S})${?value}, value=${value:N}${/value}), + ${/traits}""", + RuntimeTypes.TRAIT, + RuntimeTypes.SHAPE_ID); writer.popState(); } @@ -216,9 +220,6 @@ public void finalizeRecursiveShapes() { } private void finalizeRecursiveShapes(PythonWriter writer) { - writer.addImport("smithy_core.schemas", "Schema"); - writer.addImport("smithy_core.shapes", "ShapeType"); - for (Map.Entry entry : deferredMembers.entrySet()) { var member = entry.getKey(); LOGGER.warning("Generating member: " + member.getId()); @@ -236,7 +237,7 @@ private void finalizeRecursiveShapes(PythonWriter writer) { writer.putContext("hasTraits", !traits.isEmpty()); writer.write(""" - $1T.members[$2S] = Schema.member( + $1T.members[$2S] = $6T.member( id=$1T.id.with_member($2S), target=$3T, index=$5L, @@ -252,7 +253,8 @@ private void finalizeRecursiveShapes(PythonWriter writer) { member.getMemberName(), target, writer.consumer(w -> writeTraits(w, traits)), - entry.getValue()); + entry.getValue(), + RuntimeTypes.SCHEMA); writer.popState(); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ServiceErrorGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ServiceErrorGenerator.java index a4fcf04c1..171cbfd99 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ServiceErrorGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ServiceErrorGenerator.java @@ -7,6 +7,7 @@ import software.amazon.smithy.codegen.core.WriterDelegator; import software.amazon.smithy.python.codegen.CodegenUtils; import software.amazon.smithy.python.codegen.PythonSettings; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.writer.PythonWriter; import software.amazon.smithy.utils.SmithyInternalApi; @@ -29,16 +30,16 @@ public void run() { var serviceError = CodegenUtils.getServiceError(settings); writers.useFileWriter(serviceError.getDefinitionFile(), serviceError.getNamespace(), writer -> { writer.addDependency(SmithyPythonDependency.SMITHY_CORE); - writer.addImport("smithy_core.exceptions", "ModeledError"); + writer.addLocallyDefinedSymbol(serviceError); writer.write(""" - class $L(ModeledError): + class $L($T): ""\" Base error for all errors in the service. Some exceptions do not extend from this class, including synthetic, implicit, and shared exception types. ""\" - """, serviceError.getName()); + """, serviceError.getName(), RuntimeTypes.MODELED_ERROR); }); } } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java index 55d4daf67..e4975f65e 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java @@ -29,6 +29,7 @@ import software.amazon.smithy.python.codegen.CodegenUtils; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.PythonSettings; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.writer.PythonWriter; import software.amazon.smithy.utils.SmithyInternalApi; @@ -93,6 +94,7 @@ public void run() { private void renderStructure() { writer.addStdlibImport("dataclasses", "dataclass"); var symbol = symbolProvider.toSymbol(shape); + writer.addLocallyDefinedSymbol(symbol); writer.write(""" @dataclass(kw_only=True) class $L: @@ -119,6 +121,7 @@ private void renderError() { var fault = errorTrait.getValue(); var symbol = symbolProvider.toSymbol(shape); + writer.addLocallyDefinedSymbol(symbol); var baseError = CodegenUtils.getServiceError(settings); writer.putContext("retryable", false); writer.putContext("throttling", false); @@ -275,7 +278,9 @@ private String getDefaultValue(PythonWriter writer, MemberShape member) { } if (target.isDocumentShape()) { - return String.format("lambda: Document(%s)", switch (defaultNode.getType()) { + var docSymbol = RuntimeTypes.DOCUMENT; + var docName = writer.format("$T", docSymbol); + return String.format("lambda: %s(%s)", docName, switch (defaultNode.getType()) { case NULL -> "None"; case BOOLEAN -> defaultNode.expectBooleanNode().getValue() ? "True" : "False"; case ARRAY -> "list()"; @@ -296,17 +301,17 @@ private String getDefaultValue(PythonWriter writer, MemberShape member) { private void generateSerializeMethod() { writer.pushState(); - writer.addImport("smithy_core.serializers", "ShapeSerializer"); + writer.putContext("shapeSerializer", RuntimeTypes.SHAPE_SERIALIZER); writer.putContext("schema", symbolProvider.toSymbol(shape).expectProperty(SymbolProperties.SCHEMA)); writer.write(""" - def serialize(self, serializer: ShapeSerializer): + def serialize(self, serializer: ${shapeSerializer:T}): serializer.write_struct(${schema:T}, self) """); var serializeableMembers = filterMembers(); - writer.write("def serialize_members(self, serializer: ShapeSerializer):").indent(); + writer.write("def serialize_members(self, serializer: ${shapeSerializer:T}):").indent(); if (serializeableMembers.isEmpty()) { writer.write("pass"); } else { @@ -352,8 +357,6 @@ private void generateDeserializeMethod() { writer.pushState(); writer.addLogger(); writer.addStdlibImports("typing", Set.of("Self", "Any")); - writer.addImport("smithy_core.deserializers", "ShapeDeserializer"); - writer.addImport("smithy_core.schemas", "Schema"); var schemaSymbol = symbolProvider.toSymbol(shape).expectProperty(SymbolProperties.SCHEMA); writer.putContext("schema", schemaSymbol); @@ -361,23 +364,25 @@ private void generateDeserializeMethod() { // TODO: either formalize deserialize_kwargs or remove it when http serde is converted writer.write(""" @classmethod - def deserialize(cls, deserializer: ShapeDeserializer) -> Self: + def deserialize(cls, deserializer: $1T) -> Self: return cls(**cls.deserialize_kwargs(deserializer)) @classmethod - def deserialize_kwargs(cls, deserializer: ShapeDeserializer) -> dict[str, Any]: + def deserialize_kwargs(cls, deserializer: $1T) -> dict[str, Any]: kwargs: dict[str, Any] = {} - def _consumer(schema: Schema, de: ShapeDeserializer) -> None: + def _consumer(schema: $2T, de: $1T) -> None: match schema.expect_member_index(): - ${C|} + ${3C|} case _: logger.debug("Unexpected member schema: %s", schema) - deserializer.read_struct($T, consumer=_consumer) + deserializer.read_struct($4T, consumer=_consumer) return kwargs """, + RuntimeTypes.SHAPE_DESERIALIZER, + RuntimeTypes.SCHEMA, writer.consumer(w -> deserializeMembers(shape.members())), schemaSymbol); writer.popState(); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/UnionGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/UnionGenerator.java index 4fe83d051..197a6fa16 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/UnionGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/UnionGenerator.java @@ -14,6 +14,7 @@ import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.StringTrait; import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.writer.PythonWriter; import software.amazon.smithy.utils.SmithyInternalApi; @@ -49,9 +50,13 @@ public UnionGenerator( public void run() { writer.addStdlibImports("typing", Set.of("Union")); writer.pushState(); - var parentName = symbolProvider.toSymbol(shape).getName(); + var parentSymbol = symbolProvider.toSymbol(shape); + var parentName = parentSymbol.getName(); + writer.addLocallyDefinedSymbol(parentSymbol); writer.addStdlibImport("dataclasses", "dataclass"); - writer.addImport("smithy_core.serializers", "ShapeSerializer"); + writer.putContext("shapeSerializer", RuntimeTypes.SHAPE_SERIALIZER); + writer.putContext("shapeDeserializer", RuntimeTypes.SHAPE_DESERIALIZER); + writer.putContext("serializationError", RuntimeTypes.SERIALIZATION_ERROR); var schemaSymbol = symbolProvider.toSymbol(shape).expectProperty(SymbolProperties.SCHEMA); writer.putContext("schema", schemaSymbol); @@ -59,6 +64,7 @@ public void run() { for (MemberShape member : shape.members()) { var memberSymbol = symbolProvider.toSymbol(member); memberNames.add(memberSymbol.getName()); + writer.addLocallyDefinedSymbol(memberSymbol); var target = model.expectShape(member.getTarget()); var targetSymbol = symbolProvider.toSymbol(target); @@ -69,14 +75,14 @@ class $1L: value: $3T - def serialize(self, serializer: ShapeSerializer): + def serialize(self, serializer: ${shapeSerializer:T}): serializer.write_struct($4T, self) - def serialize_members(self, serializer: ShapeSerializer): + def serialize_members(self, serializer: ${shapeSerializer:T}): ${5C|} @classmethod - def deserialize(cls, deserializer: ShapeDeserializer) -> Self: + def deserialize(cls, deserializer: ${shapeDeserializer:T}) -> Self: return cls(value=${6C|}) """, @@ -99,7 +105,7 @@ def deserialize(cls, deserializer: ShapeDeserializer) -> Self: // Since the underlying value is unknown and un-comparable, that is the only // realistic implementation. var unknownSymbol = symbolProvider.toSymbol(shape).expectProperty(SymbolProperties.UNION_UNKNOWN); - writer.addImport("smithy_core.exceptions", "SerializationError"); + writer.addLocallyDefinedSymbol(unknownSymbol); writer.write(""" @dataclass class $1L: @@ -114,14 +120,14 @@ class $1L: tag: str - def serialize(self, serializer: ShapeSerializer): - raise SerializationError("Unknown union variants may not be serialized.") + def serialize(self, serializer: ${shapeSerializer:T}): + raise ${serializationError:T}("Unknown union variants may not be serialized.") - def serialize_members(self, serializer: ShapeSerializer): - raise SerializationError("Unknown union variants may not be serialized.") + def serialize_members(self, serializer: ${shapeSerializer:T}): + raise ${serializationError:T}("Unknown union variants may not be serialized.") @classmethod - def deserialize(cls, deserializer: ShapeDeserializer) -> Self: + def deserialize(cls, deserializer: ${shapeDeserializer:T}) -> Self: raise NotImplementedError() """, unknownSymbol.getName()); @@ -141,11 +147,10 @@ raise NotImplementedError() private void generateDeserializer() { writer.addLogger(); writer.addStdlibImports("typing", Set.of("Self", "Any")); - writer.addImport("smithy_core.deserializers", "ShapeDeserializer"); - writer.addImport("smithy_core.exceptions", "SerializationError"); var symbol = symbolProvider.toSymbol(shape); var deserializerSymbol = symbol.expectProperty(SymbolProperties.DESERIALIZER); + writer.addLocallyDefinedSymbol(deserializerSymbol); var schemaSymbol = symbol.expectProperty(SymbolProperties.SCHEMA); var unknownSymbol = symbol.expectProperty(SymbolProperties.UNION_UNKNOWN); writer.putContext("schema", schemaSymbol); @@ -153,29 +158,30 @@ private void generateDeserializer() { class $1L: _result: $2T | None = None - def deserialize(self, deserializer: ShapeDeserializer) -> $2T: + def deserialize(self, deserializer: ${shapeDeserializer:T}) -> $2T: self._result = None deserializer.read_struct($3T, self._consumer) if self._result is None: - raise SerializationError("Unions must have exactly one value, but found none.") + raise ${serializationError:T}("Unions must have exactly one value, but found none.") return self._result - def _consumer(self, schema: Schema, de: ShapeDeserializer) -> None: + def _consumer(self, schema: $4T, de: ${shapeDeserializer:T}) -> None: match schema.expect_member_index(): - ${4C|} + ${5C|} case _: - self._set_result($5L(tag=schema.expect_member_name())) + self._set_result($6L(tag=schema.expect_member_name())) def _set_result(self, value: $2T) -> None: if self._result is not None: - raise SerializationError("Unions must have exactly one value, but found more than one.") + raise ${serializationError:T}("Unions must have exactly one value, but found more than one.") self._result = value """, deserializerSymbol.getName(), symbol, schemaSymbol, + RuntimeTypes.SCHEMA, writer.consumer(w -> deserializeMembers()), unknownSymbol.getName()); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java index 2c58c7ed2..754257960 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Locale; -import java.util.Set; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; @@ -16,6 +15,7 @@ import software.amazon.smithy.python.codegen.ConfigProperty; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.PythonSettings; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.writer.PythonWriter; import software.amazon.smithy.utils.SmithyInternalApi; @@ -61,12 +61,11 @@ public List getClientPlugins(GenerationContext context) { .build()) .build()) .initialize(writer -> { - writer.addImport("smithy_http.aio.identity.apikey", "APIKeyIdentityResolver"); writer.write(""" self.api_key_identity_resolver = ( - api_key_identity_resolver or APIKeyIdentityResolver() + api_key_identity_resolver or $T() ) - """); + """, RuntimeTypes.API_KEY_IDENTITY_RESOLVER); }) .build()) .authScheme(new ApiKeyAuthScheme()) @@ -86,21 +85,22 @@ public void customize(GenerationContext context) { context.writerDelegator().useFileWriter(resolver.getDefinitionFile(), resolver.getNamespace(), writer -> { writer.addDependency(SmithyPythonDependency.SMITHY_CORE); writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); - writer.addImport("smithy_core.interfaces.auth", "AuthOption", "AuthOptionProtocol"); - writer.addImports("smithy_core.auth", Set.of("AuthOption", "AuthParams")); - writer.addImport("smithy_core.shapes", "ShapeID"); writer.addStdlibImport("typing", "Any"); writer.pushState(); writer.write(""" - def $1L(auth_params: AuthParams[Any, Any]) -> AuthOptionProtocol | None: - return AuthOption( - scheme_id=ShapeID($2S), + def $1L(auth_params: $3T[Any, Any]) -> $4T | None: + return $5T( + scheme_id=$6T($2S), identity_properties={}, # type: ignore signer_properties={}, # type: ignore ) """, OPTION_GENERATOR_NAME, - HttpApiKeyAuthTrait.ID.toString()); + HttpApiKeyAuthTrait.ID.toString(), + RuntimeTypes.AUTH_PARAMS, + RuntimeTypes.AUTH_OPTION_INTERFACE, + RuntimeTypes.AUTH_OPTION, + RuntimeTypes.SHAPE_ID); writer.popState(); }); } @@ -152,11 +152,10 @@ public void initializeScheme(GenerationContext context, PythonWriter writer, Ser var trait = service.expectTrait(HttpApiKeyAuthTrait.class); writer.pushState(); writer.putContext("scheme", trait.getScheme().orElse(null)); - writer.addImport("smithy_core.traits", "APIKeyLocation"); writer.write(""" - $T( - name=$S, - location=APIKeyLocation.$L, + $1T( + name=$2S, + location=$4T.$3L, ${?scheme} scheme=${scheme:S}, ${/scheme} @@ -164,7 +163,8 @@ public void initializeScheme(GenerationContext context, PythonWriter writer, Ser """, getAuthSchemeSymbol(context), trait.getName(), - trait.getIn().name().toUpperCase(Locale.ENGLISH)); + trait.getIn().name().toUpperCase(Locale.ENGLISH), + RuntimeTypes.API_KEY_LOCATION); writer.popState(); } } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RestJsonProtocolGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RestJsonProtocolGenerator.java index 09f49690b..1a31050e7 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RestJsonProtocolGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RestJsonProtocolGenerator.java @@ -13,6 +13,7 @@ import software.amazon.smithy.python.codegen.ApplicationProtocol; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.HttpProtocolTestGenerator; +import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.SymbolProperties; import software.amazon.smithy.python.codegen.generators.ProtocolGenerator; @@ -84,10 +85,9 @@ public ApplicationProtocol getApplicationProtocol(GenerationContext context) { @Override public void initializeProtocol(GenerationContext context, PythonWriter writer) { writer.addDependency(SmithyPythonDependency.SMITHY_AWS_CORE.withOptionalDependencies("json")); - writer.addImport("smithy_aws_core.aio.protocols", "RestJsonClientProtocol"); var serviceSymbol = context.symbolProvider().toSymbol(context.settings().service(context.model())); var serviceSchema = serviceSymbol.expectProperty(SymbolProperties.SCHEMA); - writer.write("RestJsonClientProtocol($T)", serviceSchema); + writer.write("$1T($2T)", RuntimeTypes.REST_JSON_CLIENT_PROTOCOL, serviceSchema); } // This is here rather than in HttpBindingProtocolGenerator because eventually diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java index 13df49e9d..d8a753c34 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java @@ -104,6 +104,36 @@ private ImportDeclarations addImportToMap( return this; } + /** + * Retroactively aliases an existing import to avoid name collisions. + * + *

This is called by {@link PythonWriter} at {@code toString()} time when a collision + * is detected between an imported name and a locally-defined name. The import statement + * is rewritten from {@code from namespace import name} to {@code from namespace import name as alias}. + * + * @param namespace The module namespace of the import. + * @param name The original imported name. + * @param alias The alias to use instead. + */ + void aliasImport(String namespace, String name, String alias) { + aliasImportInMap(namespace, name, alias, externalImports); + aliasImportInMap(namespace, name, alias, localImports); + } + + private void aliasImportInMap( + String namespace, + String name, + String alias, + Map> importMap + ) { + var namespaceImports = importMap.get(namespace); + if (namespaceImports != null + && namespaceImports.containsKey(name) + && namespaceImports.get(name).equals(name)) { + namespaceImports.put(name, alias); + } + } + @Override public String toString() { if (externalImports.isEmpty() && stdlibImports.isEmpty() && localImports.isEmpty()) { diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java index 435b8463a..cce51c458 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java @@ -6,6 +6,8 @@ import static software.amazon.smithy.python.codegen.SymbolProperties.IMPORTABLE; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -31,7 +33,11 @@ /** * Specialized code writer for managing Python dependencies. * - *

Use the {@code $T} formatter to refer to {@link Symbol}s. + *

Use the {@code $T} formatter to refer to {@link Symbol}s. The formatter writes a + * placeholder token into the body text and records the symbol in a symbol table. At + * {@link #toString()} time, the writer detects name collisions between imported symbols + * and locally-defined names, aliases colliding imports with a {@code _} prefix, and + * replaces all placeholder tokens with the resolved (possibly aliased) names. * *

Use the {@code $N} formatter to render {@link Node}s. */ @@ -40,9 +46,25 @@ public final class PythonWriter extends SymbolWriter> symbolTable = new HashMap<>(); + + /** + * Names that are defined locally in this file (e.g., class names, top-level variables). + * These take priority over imported names during collision resolution. + */ + private final Set locallyDefinedNames = new HashSet<>(); + /** * Constructs a PythonWriter. * @@ -316,18 +338,124 @@ public PythonWriter maybeWrite(boolean shouldWrite, Object content, Object... ar return this; } + /** + * Records a symbol as locally defined in this file (e.g., a class or top-level variable). + * + *

The writer uses this information at {@link #toString()} time to detect collisions + * between locally-defined names and imported names. When a collision is detected, the + * imported name is aliased with a {@code _} prefix. + * + * @param symbol The symbol being defined locally in this file. + * @return Returns the writer. + */ + public PythonWriter addLocallyDefinedSymbol(Symbol symbol) { + locallyDefinedNames.add(symbol.getName()); + return this; + } + @Override public String toString() { String header = "# Code generated by smithy-python-codegen DO NOT EDIT.\n\n"; - String imports = getImportContainer().toString(); String logger = addLogger ? "\nlogger = logging.getLogger(__name__)\n\n" : ""; String mainContent = super.toString(); + // Resolve collisions: for each imported name that collides with a locally-defined + // name, alias the import with a "_" prefix and rewrite placeholders in the body. + Map resolvedNames = resolveCollisions(); + + // Replace all placeholder tokens in the body with resolved names. + for (Map.Entry entry : resolvedNames.entrySet()) { + var placeholder = PLACEHOLDER_PREFIX + entry.getKey() + PLACEHOLDER_SUFFIX; + mainContent = mainContent.replace(placeholder, entry.getValue()); + } + + String imports = getImportContainer().toString(); + return header + imports + logger + mainContent; } + /** + * Detects collisions between symbols and resolves them by aliasing. + * + *

Collision cases handled: + *

+ * + * @return A map from placeholder key (namespace.name) to resolved name. + */ + private Map resolveCollisions() { + Map resolvedNames = new HashMap<>(); + + for (Map.Entry> entry : symbolTable.entrySet()) { + String simpleName = entry.getKey(); + Set symbols = entry.getValue(); + + boolean collidesWithLocal = locallyDefinedNames.contains(simpleName); + boolean hasImportCollision = symbols.size() > 1; + + if (!collidesWithLocal && !hasImportCollision) { + // No collision — use the plain name. + for (Symbol symbol : symbols) { + String placeholderKey = symbol.getNamespace() + "." + symbol.getName(); + resolvedNames.put(placeholderKey, simpleName); + } + continue; + } + + if (collidesWithLocal && !hasImportCollision) { + // Single import collides with a locally-defined name — alias it as _Name. + for (Symbol symbol : symbols) { + String placeholderKey = symbol.getNamespace() + "." + symbol.getName(); + String aliasedName = "_" + simpleName; + getImportContainer().aliasImport(symbol.getNamespace(), simpleName, aliasedName); + resolvedNames.put(placeholderKey, aliasedName); + } + } else { + // Multiple imports with the same simple name (possibly also colliding with local). + // Each gets a unique alias derived from its module namespace. + for (Symbol symbol : symbols) { + String placeholderKey = symbol.getNamespace() + "." + symbol.getName(); + String aliasedName = buildModuleAlias(symbol); + getImportContainer().aliasImport(symbol.getNamespace(), simpleName, aliasedName); + resolvedNames.put(placeholderKey, aliasedName); + } + } + } + + return resolvedNames; + } + + /** + * Builds a unique alias for a symbol based on its module namespace. + * + *

For example, {@code AuthOption} from {@code smithy_core.interfaces.auth} becomes + * {@code _smithy_core_interfaces_auth_AuthOption}, and {@code AuthOption} from + * {@code smithy_core.auth} becomes {@code _smithy_core_auth_AuthOption}. + * + *

The alias is constructed by joining all namespace segments with the symbol name. + * This guarantees uniqueness since no two modules share the same full namespace. + */ + private static String buildModuleAlias(Symbol symbol) { + String namespace = symbol.getNamespace(); + String[] parts = namespace.split("\\."); + var sb = new StringBuilder("_"); + for (String part : parts) { + sb.append(part).append("_"); + } + sb.append(symbol.getName()); + return sb.toString(); + } + /** * Implements Python symbol formatting for the {@code $T} formatter. + * + *

Instead of returning the symbol's name directly, this formatter writes a placeholder + * token and registers the symbol in the symbol table. The placeholder is resolved at + * {@link PythonWriter#toString()} time, where collision detection determines the final name. */ private final class PythonSymbolFormatter implements BiFunction { @Override @@ -338,7 +466,7 @@ public String apply(Object type, String indent) { if (typeSymbol.getProperty(IMPORTABLE).orElse(true)) { addUseImports(typeSymbol); } - return typeSymbol.getName(); + return resolvePlaceholder(typeSymbol); } else if (type instanceof SymbolReference typeSymbol) { addImport(typeSymbol.getSymbol(), typeSymbol.getAlias(), SymbolReference.ContextOption.USE); return typeSymbol.getAlias(); @@ -347,6 +475,28 @@ public String apply(Object type, String indent) { "Invalid type provided to $T. Expected a Symbol, but found `" + type + "`"); } } + + /** + * Returns a placeholder token for the symbol and registers it in the symbol table. + * + *

Only symbols that are imported from another module (have a namespace but no + * definition file) get a placeholder. Symbols without a namespace (builtins like + * int, str, bool) and symbols with a definition file (locally-defined types) are + * returned as-is since they don't need collision detection. + */ + private String resolvePlaceholder(Symbol symbol) { + if (symbol.getNamespace().isEmpty() || !symbol.getDefinitionFile().isEmpty()) { + // No namespace means builtin. Has definition file means locally-defined. + // Neither case needs collision detection. + return symbol.getName(); + } + + // Register in symbol table for collision detection. + symbolTable.computeIfAbsent(symbol.getName(), k -> new HashSet<>()).add(symbol); + + // Return a placeholder that will be resolved in toString(). + return PLACEHOLDER_PREFIX + symbol.getNamespace() + "." + symbol.getName() + PLACEHOLDER_SUFFIX; + } } private final class PythonNodeFormatter implements BiFunction { From a3a227efd3bf80aea789413ebd9c971d4a94f58a Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Tue, 14 Apr 2026 15:36:00 -0400 Subject: [PATCH 2/2] Fix positional argument ordering --- .../smithy/python/aws/codegen/AwsAuthIntegration.java | 10 +++++----- .../python/codegen/integrations/HttpApiKeyAuth.java | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java index e4ddf3a84..707429954 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAuthIntegration.java @@ -101,19 +101,19 @@ public void customize(GenerationContext context) { writer.pushState(); writer.write(""" - def $1L(auth_params: $3T[Any, Any]) -> $4T | None: - return $5T( - scheme_id=$6T($2S), + def $1L(auth_params: $2T[Any, Any]) -> $3T | None: + return $4T( + scheme_id=$5T($6S), identity_properties={}, # type: ignore signer_properties={} # type: ignore ) """, SIGV4_OPTION_GENERATOR_NAME, - SigV4Trait.ID.toString(), RuntimeTypes.AUTH_PARAMS, RuntimeTypes.AUTH_OPTION_INTERFACE, RuntimeTypes.AUTH_OPTION, - RuntimeTypes.SHAPE_ID); + RuntimeTypes.SHAPE_ID, + SigV4Trait.ID.toString()); writer.popState(); }); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java index 754257960..a3d796d9c 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java @@ -88,19 +88,19 @@ public void customize(GenerationContext context) { writer.addStdlibImport("typing", "Any"); writer.pushState(); writer.write(""" - def $1L(auth_params: $3T[Any, Any]) -> $4T | None: - return $5T( - scheme_id=$6T($2S), + def $1L(auth_params: $2T[Any, Any]) -> $3T | None: + return $4T( + scheme_id=$5T($6S), identity_properties={}, # type: ignore signer_properties={}, # type: ignore ) """, OPTION_GENERATOR_NAME, - HttpApiKeyAuthTrait.ID.toString(), RuntimeTypes.AUTH_PARAMS, RuntimeTypes.AUTH_OPTION_INTERFACE, RuntimeTypes.AUTH_OPTION, - RuntimeTypes.SHAPE_ID); + RuntimeTypes.SHAPE_ID, + HttpApiKeyAuthTrait.ID.toString()); writer.popState(); }); }