diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfigPropertyMetadata.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfigPropertyMetadata.java new file mode 100644 index 000000000..035d757c5 --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfigPropertyMetadata.java @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.aws.codegen; + +import java.util.Optional; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * AWS-specific config resolution metadata for a config property. + * Holds validators, custom resolvers, and default values. + */ +@SmithyInternalApi +public record AwsConfigPropertyMetadata( + Symbol validator, + Symbol customResolver, + String defaultValue +) { + public Optional validatorOpt() { + return Optional.ofNullable(validator); + } + + public Optional customResolverOpt() { + return Optional.ofNullable(customResolver); + } + + public Optional defaultValueOpt() { + return Optional.ofNullable(defaultValue); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Symbol validator; + private Symbol customResolver; + private String defaultValue; + + public Builder validator(Symbol validator) { + this.validator = validator; + return this; + } + + public Builder customResolver(Symbol customResolver) { + this.customResolver = customResolver; + return this; + } + + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public AwsConfigPropertyMetadata build() { + return new AwsConfigPropertyMetadata(validator, customResolver, defaultValue); + } + } +} diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfigResolutionIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfigResolutionIntegration.java new file mode 100644 index 000000000..45161c410 --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfigResolutionIntegration.java @@ -0,0 +1,254 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.aws.codegen; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.smithy.python.codegen.ConfigProperty; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.integrations.PythonIntegration; +import software.amazon.smithy.python.codegen.sections.ConfigSection; +import software.amazon.smithy.python.codegen.writer.PythonWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Intercepts the generated Config class to add AWS-specific descriptor-based + * config resolution, keeping the generic ConfigGenerator unchanged. + */ +@SmithyInternalApi +public class AwsConfigResolutionIntegration implements PythonIntegration { + + // Metadata for properties that use descriptors, keyed by property name. + private static final Map DESCRIPTOR_METADATA = Map.of( + "region", AwsConfiguration.REGION_METADATA, + "retry_strategy", AwsConfiguration.RETRY_STRATEGY_METADATA, + "sdk_ua_app_id", AwsUserAgentIntegration.SDK_UA_APP_ID_METADATA + ); + + @Override + public List> interceptors( + GenerationContext context + ) { + return List.of( + new PropertyDeclarationInterceptor(context), + new PropertyInitInterceptor(), + new PreDeclarationsInterceptor(), + new PreInitInterceptor(), + new ConfigTailInterceptor() + ); + } + + // Replaces plain field declarations with descriptor assignments for properties + // that have AWS metadata registered. + private static final class PropertyDeclarationInterceptor + implements CodeInterceptor { + + private final GenerationContext context; + + PropertyDeclarationInterceptor(GenerationContext context) { + this.context = context; + } + + @Override + public Class sectionType() { + return ConfigSection.PropertyDeclarationSection.class; + } + + @Override + public void write( + PythonWriter writer, + String previousText, + ConfigSection.PropertyDeclarationSection section + ) { + ConfigProperty prop = section.property(); + AwsConfigPropertyMetadata meta = DESCRIPTOR_METADATA.get(prop.name()); + + if (meta == null) { + writer.write(previousText.stripTrailing()); + return; + } + + String typeHint = prop.type().getName(); + if (prop.isNullable() && !typeHint.endsWith("| None")) { + typeHint = typeHint + " | None"; + } + writer.write("$L: $L = _descriptors['$L'] # type: ignore[assignment]", + prop.name(), typeHint, prop.name()); + writer.writeDocs(prop.documentation(), context); + } + } + + // Skips `self.X = X` initialization for descriptor properties since the + // descriptor handles resolution. + private static final class PropertyInitInterceptor + implements CodeInterceptor { + + @Override + public Class sectionType() { + return ConfigProperty.InitializeConfigPropertySection.class; + } + + @Override + public void write( + PythonWriter writer, + String previousText, + ConfigProperty.InitializeConfigPropertySection section + ) { + if (DESCRIPTOR_METADATA.containsKey(section.property().name())) { + return; + } + writer.write(previousText.stripTrailing()); + } + } + + // Injects _descriptors dict and _resolver field before property declarations. + private static final class PreDeclarationsInterceptor + implements CodeInterceptor { + + @Override + public Class sectionType() { + return ConfigSection.PrePropertyDeclarationsSection.class; + } + + @Override + public void write( + PythonWriter writer, + String previousText, + ConfigSection.PrePropertyDeclarationsSection section + ) { + List descriptorProps = section.properties().stream() + .filter(p -> DESCRIPTOR_METADATA.containsKey(p.name())) + .collect(Collectors.toList()); + + if (descriptorProps.isEmpty()) { + return; + } + + addImports(writer, descriptorProps); + + writer.write("# Config properties using descriptors"); + writer.openBlock("_descriptors = {"); + for (ConfigProperty prop : descriptorProps) { + AwsConfigPropertyMetadata meta = DESCRIPTOR_METADATA.get(prop.name()); + StringBuilder sb = new StringBuilder(); + sb.append("'").append(prop.name()).append("': ConfigProperty('") + .append(prop.name()).append("'"); + if (meta != null) { + meta.validatorOpt().ifPresent(sym -> + sb.append(", validator=").append(sym.getName())); + meta.customResolverOpt().ifPresent(sym -> + sb.append(", resolver_func=").append(sym.getName())); + meta.defaultValueOpt().ifPresent(val -> + sb.append(", default_value=").append(val)); + } + sb.append("),"); + writer.write(sb.toString()); + } + writer.closeBlock("}"); + writer.write(""); + writer.write("_resolver: ConfigResolver"); + } + + private void addImports(PythonWriter writer, List descriptorProps) { + writer.addImport("smithy_aws_core.config.property", "ConfigProperty"); + writer.addImport("smithy_aws_core.config.resolver", "ConfigResolver"); + writer.addImport("smithy_aws_core.config.sources", "EnvironmentSource"); + writer.addImport("smithy_aws_core.config.source_info", "SourceInfo"); + + for (ConfigProperty prop : descriptorProps) { + AwsConfigPropertyMetadata meta = DESCRIPTOR_METADATA.get(prop.name()); + if (meta == null) { + continue; + } + meta.validatorOpt().ifPresent(sym -> + writer.addImport(sym.getNamespace(), sym.getName())); + meta.customResolverOpt().ifPresent(sym -> + writer.addImport(sym.getNamespace(), sym.getName())); + meta.defaultValueOpt().ifPresent(val -> { + if (val.contains("RetryStrategyOptions")) { + writer.addImport("smithy_core.retries", "RetryStrategyOptions"); + } + }); + } + } + } + + // Injects resolver initialization at the start of __init__. + private static final class PreInitInterceptor + implements CodeInterceptor { + + @Override + public Class sectionType() { + return ConfigSection.PreInitializePropertiesSection.class; + } + + @Override + public void write( + PythonWriter writer, + String previousText, + ConfigSection.PreInitializePropertiesSection section + ) { + boolean hasDescriptors = section.properties().stream() + .anyMatch(p -> DESCRIPTOR_METADATA.containsKey(p.name())); + + if (!hasDescriptors) { + return; + } + + writer.write("self._resolver = ConfigResolver(sources=[EnvironmentSource()])"); + writer.write(""); + writer.write("# Only set if provided (not None) to allow resolution from sources"); + writer.write("for key in self.__class__._descriptors.keys():"); + writer.indent(); + writer.write("value = locals().get(key)"); + writer.write("if value is not None:"); + writer.indent(); + writer.write("setattr(self, key, value)"); + writer.dedent(); + writer.dedent(); + } + } + + // Appends get_source() method to the Config class. + private static final class ConfigTailInterceptor + implements CodeInterceptor { + + @Override + public Class sectionType() { + return ConfigSection.class; + } + + @Override + public void write(PythonWriter writer, String previousText, ConfigSection section) { + boolean hasDescriptors = section.properties().stream() + .anyMatch(p -> DESCRIPTOR_METADATA.containsKey(p.name())); + + writer.write(previousText.stripTrailing()); + + if (!hasDescriptors) { + return; + } + + writer.write(""" + + def get_source(self, key: str) -> SourceInfo | None: + \"""Get the source that provided a configuration value. + + Args: + key: The configuration key (e.g., 'region', 'retry_strategy') + + Returns: + The source info (SimpleSource or ComplexSource), + or None if the key hasn't been resolved yet. + \""" + cached = self.__dict__.get(f'_cache_{key}') + return cached[1] if cached else None + """); + } + } +} diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java index 01529a4ff..361a23301 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java @@ -17,22 +17,16 @@ private AwsConfiguration() {} public static final ConfigProperty REGION = ConfigProperty.builder() .name("region") - .type(Symbol.builder().name("str | None").build()) - .documentation(" The AWS region to connect to. The configured region is used to " + .type(Symbol.builder().name("str").build()) + .documentation("The AWS region to connect to. The configured region is used to " + "determine the service endpoint.") .nullable(false) - .useDescriptor(true) - .validator(Symbol.builder() - .name("validate_region") - .namespace("smithy_aws_core.config.validators", ".") - .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) - .build()) .build(); public static final ConfigProperty RETRY_STRATEGY = ConfigProperty.builder() .name("retry_strategy") .type(Symbol.builder() - .name("RetryStrategy | RetryStrategyOptions | None") + .name("RetryStrategy | RetryStrategyOptions") .addReference(Symbol.builder() .name("RetryStrategy") .namespace("smithy_core.interfaces.retries", ".") @@ -46,8 +40,22 @@ private AwsConfiguration() {} .build()) .documentation( "The retry strategy or options for configuring retry behavior. Can be either a configured RetryStrategy or RetryStrategyOptions to create one.") - .nullable(false) - .useDescriptor(true) + .build(); + + /** + * AWS-specific metadata for descriptor-based config properties. + */ + public static final AwsConfigPropertyMetadata REGION_METADATA = AwsConfigPropertyMetadata.builder() + .validator(Symbol.builder() + .name("validate_region") + .namespace("smithy_aws_core.config.validators", ".") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build()) + .build(); + /** + * AWS-specific metadata for descriptor-based config properties. + */ + public static final AwsConfigPropertyMetadata RETRY_STRATEGY_METADATA = AwsConfigPropertyMetadata.builder() .validator(Symbol.builder() .name("validate_retry_strategy") .namespace("smithy_aws_core.config.validators", ".") diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index df703730a..ade4a5740 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -21,6 +21,14 @@ @SmithyInternalApi public class AwsUserAgentIntegration implements PythonIntegration { + public static final AwsConfigPropertyMetadata SDK_UA_APP_ID_METADATA = AwsConfigPropertyMetadata.builder() + .validator(Symbol.builder() + .name("validate_ua_string") + .namespace("smithy_aws_core.config.validators", ".") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build()) + .build(); + public static final String USER_AGENT_PLUGIN = """ def aws_user_agent_plugin(config: $1T): config.interceptors.append( @@ -49,12 +57,6 @@ public List getClientPlugins(GenerationContext context) { "A unique and opaque application ID that is appended to the User-Agent header.") .type(Symbol.builder().name("str").build()) .nullable(true) - .useDescriptor(true) - .validator(Symbol.builder() - .name("validate_ua_string") - .namespace("smithy_aws_core.config.validators", ".") - .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) - .build()) .build(); final String user_agent_plugin_file = "user_agent"; diff --git a/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration b/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration index a338df30c..4780e099b 100644 --- a/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration +++ b/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration @@ -8,3 +8,4 @@ software.amazon.smithy.python.aws.codegen.AwsProtocolsIntegration software.amazon.smithy.python.aws.codegen.AwsServiceIdIntegration software.amazon.smithy.python.aws.codegen.AwsUserAgentIntegration software.amazon.smithy.python.aws.codegen.AwsStandardRegionalEndpointsIntegration +software.amazon.smithy.python.aws.codegen.AwsConfigResolutionIntegration diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ConfigProperty.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ConfigProperty.java index bddbfe6d5..7a8b1d674 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ConfigProperty.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ConfigProperty.java @@ -23,10 +23,6 @@ public final class ConfigProperty implements ToSmithyBuilder { private final boolean nullable; private final String documentation; private final Consumer initialize; - private final Symbol validator; - private final Symbol customResolver; - private final boolean useDescriptor; - private final String defaultValue; /** * Constructor. @@ -37,10 +33,6 @@ private ConfigProperty(Builder builder) { this.nullable = builder.nullable; this.documentation = Objects.requireNonNull(builder.documentation); this.initialize = Objects.requireNonNull(builder.initialize); - this.validator = builder.validator; - this.customResolver = builder.customResolver; - this.useDescriptor = builder.useDescriptor; - this.defaultValue = builder.defaultValue; } /** @@ -71,34 +63,6 @@ public String documentation() { return documentation; } - /** - * @return Returns the validator symbol for this property, if any. - */ - public java.util.Optional validator() { - return java.util.Optional.ofNullable(validator); - } - - /** - * @return Returns the custom resolver symbol for this property, if any. - */ - public java.util.Optional customResolver() { - return java.util.Optional.ofNullable(customResolver); - } - - /** - * @return Returns whether this property uses the ConfigProperty descriptor. - */ - public boolean useDescriptor() { - return useDescriptor; - } - - /** - * @return Returns the default value for this property, if any. - */ - public java.util.Optional defaultValue() { - return java.util.Optional.ofNullable(defaultValue); - } - /** * Initializes the config field on the config object. * @@ -130,11 +94,7 @@ public SmithyBuilder toBuilder() { .type(type) .nullable(nullable) .documentation(documentation) - .initialize(initialize) - .validator(validator) - .customResolver(customResolver) - .useDescriptor(useDescriptor) - .defaultValue(defaultValue); + .initialize(initialize); } /** @@ -147,11 +107,6 @@ public static final class Builder implements SmithyBuilder { private String documentation; private Consumer initialize = writer -> writer.write("self.$1L = $1L", name); - private Symbol validator; - private Symbol customResolver; - private boolean useDescriptor = false; - private String defaultValue; - @Override public ConfigProperty build() { return new ConfigProperty(this); @@ -227,49 +182,5 @@ public Builder initialize(Consumer initialize) { this.initialize = initialize; return this; } - - /** - * Sets the validator symbol for the config property. - * - * @param validator The validator function symbol. - * @return Returns the builder. - */ - public Builder validator(Symbol validator) { - this.validator = validator; - return this; - } - - /** - * Sets the custom resolver symbol for the config property. - * - * @param customResolver The custom resolver function symbol. - * @return Returns the builder. - */ - public Builder customResolver(Symbol customResolver) { - this.customResolver = customResolver; - return this; - } - - /** - * Sets whether the config property uses the ConfigProperty descriptor. - * - * @param useDescriptor Whether to use the descriptor pattern. - * @return Returns the builder. - */ - public Builder useDescriptor(boolean useDescriptor) { - this.useDescriptor = useDescriptor; - return this; - } - - /** - * Sets the default value for the config property. - * - * @param defaultValue The default value as a Python expression string. - * @return Returns the builder. - */ - public Builder defaultValue(String defaultValue) { - this.defaultValue = defaultValue; - return this; - } } } 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 92aa857e5..dbcbdd88a 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 @@ -52,6 +52,24 @@ public final class ConfigGenerator implements Runnable { .nullable(false) .initialize(writer -> writer.write("self.interceptors = interceptors or []")) .build(), + ConfigProperty.builder() + .name("retry_strategy") + .type(Symbol.builder() + .name("RetryStrategy | RetryStrategyOptions") + .addReference(Symbol.builder() + .name("RetryStrategy") + .namespace("smithy_core.interfaces.retries", ".") + .addDependency(SmithyPythonDependency.SMITHY_CORE) + .build()) + .addReference(Symbol.builder() + .name("RetryStrategyOptions") + .namespace("smithy_core.retries", ".") + .addDependency(SmithyPythonDependency.SMITHY_CORE) + .build()) + .build()) + .documentation( + "The retry strategy or options for configuring retry behavior. Can be either a configured RetryStrategy or RetryStrategyOptions to create one.") + .build(), ConfigProperty.builder() .name("endpoint_uri") .type(Symbol.builder() @@ -80,23 +98,6 @@ public final class ConfigGenerator implements Runnable { writer.write("self.endpoint_resolver = endpoint_resolver or StaticEndpointResolver()"); writer.popState(); }) - .build(), - ConfigProperty.builder() - .name("retry_strategy") - .type(Symbol.builder() - .name("RetryStrategy | RetryStrategyOptions | None") - .addReference(Symbol.builder() - .name("RetryStrategy") - .namespace("smithy_core.interfaces.retries", ".") - .addDependency(SmithyPythonDependency.SMITHY_CORE) - .build()) - .addReference(Symbol.builder() - .name("RetryStrategyOptions") - .namespace("smithy_core.retries", ".") - .addDependency(SmithyPythonDependency.SMITHY_CORE) - .build()) - .build()) - .documentation("The retry strategy or options for configuring retry behavior. Can be either a configured RetryStrategy or RetryStrategyOptions to create one.") .build()); // This list contains any properties that must be added to any http-based @@ -309,21 +310,8 @@ private void writeInterceptorsType(PythonWriter writer) { private void generateConfig(GenerationContext context, PythonWriter writer) { var configSymbol = CodegenUtils.getConfigSymbol(context.settings()); - // Initialize a set of config properties. + // Initialize a set of config properties with our base properties. var properties = new TreeSet<>(Comparator.comparing(ConfigProperty::name)); - - var model = context.model(); - var service = context.settings().service(model); - - // Add plugin properties first so they can override base properties with same name. - for (PythonIntegration integration : context.integrations()) { - for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) { - if (plugin.matchesService(model, service)) { - properties.addAll(plugin.getConfigProperties()); - } - } - } - properties.addAll(BASE_PROPERTIES); properties.addAll(getProtocolProperties(context)); @@ -334,43 +322,19 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { writer.onSection(new AddAuthHelper()); } - writer.onSection(new AddGetSourceHelper()); - - var finalProperties = List.copyOf(properties); + var model = context.model(); + var service = context.settings().service(model); - // Check if any properties use descriptors - boolean hasDescriptors = finalProperties.stream().anyMatch(ConfigProperty::useDescriptor); - - // Only add config resolution imports if there are descriptor properties - if (hasDescriptors) { - writer.addDependency(SmithyPythonDependency.SMITHY_AWS_CORE); - writer.addImport("smithy_aws_core.config.property", "ConfigProperty"); - writer.addImport("smithy_aws_core.config.resolver", "ConfigResolver"); - writer.addImport("smithy_aws_core.config.sources", "EnvironmentSource"); - - // Add validator and resolver imports for properties that use descriptors - for (ConfigProperty property : finalProperties) { - if (property.useDescriptor()) { - if (property.validator().isPresent()) { - var validatorSymbol = property.validator().get(); - writer.addImport(validatorSymbol.getNamespace(), validatorSymbol.getName()); - } - if (property.customResolver().isPresent()) { - var resolverSymbol = property.customResolver().get(); - writer.addImport(resolverSymbol.getNamespace(), resolverSymbol.getName()); - } - // Add imports for types referenced in default values - if (property.defaultValue().isPresent()) { - var defaultValue = property.defaultValue().get(); - if (defaultValue.contains("RetryStrategyOptions")) { - writer.addDependency(SmithyPythonDependency.SMITHY_CORE); - writer.addImport("smithy_core.retries", "RetryStrategyOptions"); - } - } + // Add any relevant config properties from plugins. + for (PythonIntegration integration : context.integrations()) { + for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) { + if (plugin.matchesService(model, service)) { + properties.addAll(plugin.getConfigProperties()); } } } + var finalProperties = List.copyOf(properties); final String serviceId = context.settings() .service(context.model()) .getTrait(ServiceTrait.class) @@ -393,138 +357,46 @@ def __init__( ${C|} ): ${C|} + ${C|} """, configSymbol.getName(), serviceId, + writer.consumer(w -> { + w.pushState(new ConfigSection.PrePropertyDeclarationsSection(finalProperties)); + w.popState(); + }), writer.consumer(w -> writePropertyDeclarations(w, finalProperties)), - writer.consumer(w -> writeDescriptorDeclarations(w, finalProperties, context)), writer.consumer(w -> writeInitParams(w, finalProperties)), + writer.consumer(w -> { + w.pushState(new ConfigSection.PreInitializePropertiesSection(finalProperties)); + w.popState(); + }), writer.consumer(w -> initializeProperties(w, finalProperties))); writer.popState(); } - // Write descriptor declarations for properties using ConfigProperty descriptor - private void writeDescriptorDeclarations(PythonWriter writer, Collection properties, GenerationContext context) { - boolean hasDescriptors = properties.stream().anyMatch(ConfigProperty::useDescriptor); - - if (!hasDescriptors) { - return; - } - - writer.write("# Config properties using descriptors"); - writer.write("_descriptors = {"); - writer.indent(); - - for (ConfigProperty property : properties) { - if (property.useDescriptor()) { - writer.writeInline("'$L': ConfigProperty('$L'", - property.name(), - property.name()); - - if (property.validator().isPresent()) { - writer.writeInline(", validator=$L", property.validator().get().getName()); - } - - if (property.customResolver().isPresent()) { - writer.writeInline(", resolver_func=$L", property.customResolver().get().getName()); - } - - if (property.defaultValue().isPresent()) { - writer.writeInline(", default_value=$L", property.defaultValue().get()); - } - - writer.write("),"); - } - } - - writer.dedent(); - writer.write("}"); - writer.write(""); - - for (ConfigProperty property : properties) { - if (property.useDescriptor()) { - var typeHint = property.isNullable() - ? "$T | None" - : "$T"; - writer.write("$L: " + typeHint + " = _descriptors['$L'] # type: ignore[assignment]", - property.name(), - property.type(), - property.name()); - - if (!property.documentation().isEmpty()) { - writer.writeDocs(property.documentation(), context); - } - writer.write(""); - } - } - writer.write(""); - } - private void writePropertyDeclarations(PythonWriter writer, Collection properties) { for (ConfigProperty property : properties) { - // Skip descriptor properties - they're declared above - if (property.useDescriptor()) { - continue; - } - - String typeName = property.type().getName(); - String formatString; - if (property.isNullable() && !typeName.endsWith("| None")) { - formatString = "$L: $T | None"; - } else { - formatString = "$L: $T"; - } + writer.pushState(new ConfigSection.PropertyDeclarationSection(property)); + var formatString = property.isNullable() + ? "$L: $T | None" + : "$L: $T"; writer.write(formatString, property.name(), property.type()); writer.writeDocs(property.documentation(), context); - writer.write(""); - } - - // Add _resolver declaration only if there are descriptor properties - boolean hasDescriptors = properties.stream().anyMatch(ConfigProperty::useDescriptor); - if (hasDescriptors) { - writer.write("_resolver: ConfigResolver"); + writer.popState(); writer.write(""); } } private void writeInitParams(PythonWriter writer, Collection properties) { for (ConfigProperty property : properties) { - String typeName = property.type().getName(); - if (typeName.endsWith("| None")) { - writer.write("$L: $T = None,", property.name(), property.type()); - } else { - writer.write("$L: $T | None = None,", property.name(), property.type()); - } + writer.write("$L: $T | None = None,", property.name(), property.type()); } } private void initializeProperties(PythonWriter writer, Collection properties) { - var descriptorProperties = properties.stream() - .filter(ConfigProperty::useDescriptor) - .toList(); - - if (!descriptorProperties.isEmpty()) { - writer.write("# Set instance values for descriptor properties"); - writer.write("self._resolver = ConfigResolver(sources=[EnvironmentSource()])"); - writer.write(""); - - writer.write("# Only set if provided (not None) to allow resolution from sources"); - writer.write("for key in self.__class__._descriptors.keys():"); - writer.indent(); - writer.write("value = locals().get(key)"); - writer.write("if value is not None:"); - writer.indent(); - writer.write("setattr(self, key, value)"); - writer.dedent(); - writer.dedent(); - writer.write(""); - } - - // Finally, initialize non-descriptor properties normally for (ConfigProperty property : properties) { - if (!property.useDescriptor()) { - property.initialize(writer); - } + property.initialize(writer); } } @@ -559,45 +431,4 @@ def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: """); } } - - private static final class AddGetSourceHelper implements CodeInterceptor { - @Override - public Class sectionType() { - return ConfigSection.class; - } - - @Override - public void write(PythonWriter writer, String previousText, ConfigSection section) { - // Check if there are any descriptor properties - boolean hasDescriptors = section.properties() - .stream() - .anyMatch(ConfigProperty::useDescriptor); - - if (!hasDescriptors) { - // No descriptor properties, just write previous text - writer.write(previousText); - return; - } - - writer.write(previousText); - writer.addImport("smithy_aws_core.config.source_info", "SourceInfo"); - - writer.write(""" - - def get_source(self, key: str) -> SourceInfo | None: - \"""Get the source that provided a configuration value. - - Args: - key: The configuration key (e.g., 'region', 'retry_strategy') - - Returns: - The source info (SimpleSource('source_name') or - ComplexSource({"retry_mode": "source1", "max_attempts": "source2"})), - or None if the key hasn't been resolved yet. - \""" - cached = self.__dict__.get(f'_cache_{key}') - return cached[1] if cached else None - """); - } - } } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ConfigSection.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ConfigSection.java index 13ae6110d..ccf9dc80a 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ConfigSection.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ConfigSection.java @@ -15,4 +15,14 @@ * @param properties The list of properties that need to be present on the config. */ @SmithyInternalApi -public record ConfigSection(List properties) implements CodeSection {} +public record ConfigSection(List properties) implements CodeSection { + + /** Section for a single config property's class-level field declaration. */ + public record PropertyDeclarationSection(ConfigProperty property) implements CodeSection {} + + /** Section before property declarations, for injecting class-level fields. */ + public record PrePropertyDeclarationsSection(List properties) implements CodeSection {} + + /** Section before property initializations in __init__, for injecting setup code. */ + public record PreInitializePropertiesSection(List properties) implements CodeSection {} +} diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/resolver.py b/packages/smithy-aws-core/src/smithy_aws_core/config/resolver.py index a17a36b93..3444e987c 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/resolver.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/resolver.py @@ -3,9 +3,8 @@ from collections.abc import Sequence from typing import Any -from smithy_core.interfaces.config import ConfigSource - from smithy_aws_core.config.source_info import SimpleSource +from smithy_aws_core.interfaces.config import ConfigSource class ConfigResolver: diff --git a/packages/smithy-core/src/smithy_core/interfaces/config.py b/packages/smithy-aws-core/src/smithy_aws_core/interfaces/config.py similarity index 100% rename from packages/smithy-core/src/smithy_core/interfaces/config.py rename to packages/smithy-aws-core/src/smithy_aws_core/interfaces/config.py