diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SetupGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SetupGenerator.java index 6e88bae27..8551bd11b 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SetupGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SetupGenerator.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -141,6 +142,7 @@ private static void writePyproject( Optional.ofNullable(dependencies.get(PythonDependency.Type.DEPENDENCY.getType())).ifPresent(deps -> { writer.openBlock("dependencies = [", "]\n", () -> writeDependencyList(writer, deps.values())); + writeOptionalDependencies(writer, deps); }); Optional> testDeps = @@ -188,6 +190,60 @@ private static void writePyproject( }); } + /** + * Emit a {@code [project.optional-dependencies]} table so users can opt into extra features + * (e.g. {@code pip install [awscrt]}) without having to hunt down compatible dependency + * versions themselves. + * + *

The set of extras is data-driven: {@link #collectOptionalDependencies} decides which + * extras apply to this client, and this method renders whatever it returns. To add a new extra, + * contribute an entry there rather than changing the rendering here. + */ + private static void writeOptionalDependencies( + PythonWriter writer, + Map dependencies + ) { + var extras = collectOptionalDependencies(dependencies); + if (extras.isEmpty()) { + return; + } + + writer.write("[project.optional-dependencies]"); + for (var extra : extras.entrySet()) { + writer.openBlock("$L = [", "]\n", extra.getKey(), () -> { + for (var iter = extra.getValue().iterator(); iter.hasNext();) { + writer.write("$S$L", iter.next(), iter.hasNext() ? "," : ""); + } + }); + } + } + + /** + * Collect the opt-in extras to expose in {@code [project.optional-dependencies]}, keyed by extra + * name and mapped to the requirement specifiers that extra installs. Insertion order is + * preserved in the generated output. + * + *

This is the single place to register a new extra. + */ + // Package-private for testing. + static Map> collectOptionalDependencies( + Map dependencies + ) { + var extras = new LinkedHashMap>(); + + // Let users opt into the CRT transport (AWSCRTHTTPClient) without guessing a compatible + // awscrt version, by re-exporting smithy_http's awscrt extra. This keeps the version + // constraint sourced from smithy-http, the single source of truth. Skipped when the client + // already requires awscrt (e.g. an http2 service that defaults to the CRT transport), where + // there is nothing to opt into. + var smithyHttp = dependencies.get(SmithyPythonDependency.SMITHY_HTTP.packageName()); + if (smithyHttp != null && !getOptionalDependencies(smithyHttp).contains("awscrt")) { + extras.put("awscrt", List.of("smithy_http[awscrt]" + smithyHttp.getVersion())); + } + + return extras; + } + private static void writeDependencyList(PythonWriter writer, Collection dependencies) { for (var iter = dependencies.iterator(); iter.hasNext();) { writer.pushState(); @@ -208,7 +264,7 @@ private static void writeDependencyList(PythonWriter writer, Collection getOptionalDependencies(SymbolDependency dependency) { - var optionals = dependency.getProperty(SymbolProperties.OPTIONAL_DEPENDENCIES) + return dependency.getProperty(SymbolProperties.OPTIONAL_DEPENDENCIES) .filter(list -> { for (var d : list) { if (!(d instanceof String)) { @@ -218,11 +274,6 @@ private static List getOptionalDependencies(SymbolDependency dependency) return true; }) .orElse(Collections.emptyList()); - try { - return optionals; - } catch (Exception e) { - return Collections.emptyList(); - } } private static void writeReadme( diff --git a/codegen/core/src/test/java/software/amazon/smithy/python/codegen/generators/SetupGeneratorTest.java b/codegen/core/src/test/java/software/amazon/smithy/python/codegen/generators/SetupGeneratorTest.java new file mode 100644 index 000000000..a200645fc --- /dev/null +++ b/codegen/core/src/test/java/software/amazon/smithy/python/codegen/generators/SetupGeneratorTest.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.generators; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.codegen.core.SymbolDependency; +import software.amazon.smithy.python.codegen.SmithyPythonDependency; + +public class SetupGeneratorTest { + + /** + * When a client depends on smithy_http (the common case, since all transports default to aiohttp), + * an {@code awscrt} extra is exposed that re-exports smithy_http's own awscrt extra so the version + * constraint stays sourced from smithy-http. + */ + @Test + public void exposesAwscrtExtraForSmithyHttpDependency() { + var smithyHttp = SmithyPythonDependency.SMITHY_HTTP.getDependency(); + Map dependencies = Map.of(smithyHttp.getPackageName(), smithyHttp); + + var extras = SetupGenerator.collectOptionalDependencies(dependencies); + + assertEquals( + Map.of("awscrt", List.of("smithy_http[awscrt]" + smithyHttp.getVersion())), + extras); + } +}