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 5d3ba035..7fd492bb 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 @@ -20,5 +20,13 @@ private AwsConfiguration() {} .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_host") + .namespace("smithy_aws_core.config.validators", ".") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build()) + .defaultValue("'us-east-1'") .build(); } 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 7a8b1d67..bddbfe6d 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,6 +23,10 @@ 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. @@ -33,6 +37,10 @@ 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; } /** @@ -63,6 +71,34 @@ 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. * @@ -94,7 +130,11 @@ public SmithyBuilder toBuilder() { .type(type) .nullable(nullable) .documentation(documentation) - .initialize(initialize); + .initialize(initialize) + .validator(validator) + .customResolver(customResolver) + .useDescriptor(useDescriptor) + .defaultValue(defaultValue); } /** @@ -107,6 +147,11 @@ 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); @@ -182,5 +227,49 @@ 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 de03d42d..b92378c5 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 @@ -69,6 +69,17 @@ public final class ConfigGenerator implements Runnable { .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) + .validator(Symbol.builder() + .name("validate_retry_strategy") + .namespace("smithy_aws_core.config.validators", ".") + .build()) + .customResolver(Symbol.builder() + .name("resolve_retry_strategy") + .namespace("smithy_aws_core.config.custom_resolvers", ".") + .build()) + .defaultValue("RetryStrategyOptions(retry_mode=\"standard\", max_attempts=3)") .build(), ConfigProperty.builder() .name("endpoint_uri") @@ -322,6 +333,8 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { writer.onSection(new AddAuthHelper()); } + writer.onSection(new AddGetSourceHelper()); + var model = context.model(); var service = context.settings().service(model); @@ -335,6 +348,37 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { } var finalProperties = List.copyOf(properties); + + // Add imports for config resolution + writer.addDependency(SmithyPythonDependency.SMITHY_CORE); + writer.addImport("smithy_core.config.property", "ConfigProperty"); + writer.addImport("smithy_core.config.resolver", "ConfigResolver"); + writer.addImport("smithy_aws_core.config.sources", "EnvironmentSource"); + + // Add validator and resolver imports for properties that use descriptors + // Also add imports for types used in default values + 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(); + // Check if default value uses RetryStrategyOptions + if (defaultValue.contains("RetryStrategyOptions")) { + writer.addDependency(SmithyPythonDependency.SMITHY_CORE); + writer.addImport("smithy_core.retries", "RetryStrategyOptions"); + } + } + } + } + final String serviceId = context.settings() .service(context.model()) .getTrait(ServiceTrait.class) @@ -349,6 +393,8 @@ class $L: ${C|} + ${C|} + def __init__( self, *, @@ -358,14 +404,53 @@ def __init__( """, configSymbol.getName(), serviceId, + writer.consumer(w -> writeDescriptorDeclarations(w, finalProperties)), writer.consumer(w -> writePropertyDeclarations(w, finalProperties)), writer.consumer(w -> writeInitParams(w, finalProperties)), writer.consumer(w -> initializeProperties(w, finalProperties))); writer.popState(); } + // Write descriptor declarations for properties using ConfigProperty descriptor + private void writeDescriptorDeclarations(PythonWriter writer, Collection properties) { + boolean hasDescriptors = properties.stream().anyMatch(ConfigProperty::useDescriptor); + + if (!hasDescriptors) { + return; + } + + writer.write("# Config properties using descriptors (lazy resolution with caching)"); + 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.write(""); + } + private void writePropertyDeclarations(PythonWriter writer, Collection properties) { for (ConfigProperty property : properties) { + // Skip descriptor properties - they're declared above + if (property.useDescriptor()) { + continue; + } + var formatString = property.isNullable() ? "$L: $T | None" : "$L: $T"; @@ -373,6 +458,11 @@ private void writePropertyDeclarations(PythonWriter writer, Collection properties) { @@ -381,9 +471,48 @@ private void writeInitParams(PythonWriter writer, Collection pro } } + // Handle descriptor properties efficiently with a loop private void initializeProperties(PythonWriter writer, Collection properties) { + // First, initialize the resolver + writer.write("# Create resolver with environment source"); + writer.write("self._resolver = ConfigResolver(sources=[EnvironmentSource()])"); + writer.write(""); + + // Then, handle descriptor properties efficiently with a loop + var descriptorProperties = properties.stream() + .filter(ConfigProperty::useDescriptor) + .map(ConfigProperty::name) + .toList(); + + if (!descriptorProperties.isEmpty()) { + writer.write("# Set instance values for descriptor properties"); + + // Generate the list of descriptor property names + writer.writeInline("descriptor_keys = ["); + var iter = descriptorProperties.iterator(); + while (iter.hasNext()) { + writer.writeInline("'$L'", iter.next()); + if (iter.hasNext()) { + writer.writeInline(", "); + } + } + writer.write("]"); + writer.write("for key in descriptor_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) { - property.initialize(writer); + if (!property.useDescriptor()) { + property.initialize(writer); + } } } @@ -418,4 +547,46 @@ def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: """); } } + + // Helper to add get_source method for descriptor properties + 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; + } + + // First write the previous text + writer.write(previousText); + + // Add the get_source helper method + writer.write(""" + + def get_source(self, key: str) -> str | None: + \"""Get the source that provided a configuration value. + + Args: + key: The configuration key (e.g., 'region', 'retry_strategy') + + Returns: + The source name ('instance', 'environment', etc.), + or None if the key hasn't been resolved yet. + \""" + cached = self.__dict__.get(f'_cache_{key}') + return cached[1] if cached else None + """); + } + } }