Skip to content

Commit 625fa86

Browse files
Update exception hierarchy and add retry info
This updates exceptions to embed necessary retry info. This will allow any layer that has access to the exception to set or utilize it without having to have additional interfaces and hooks. This does not modify the retry strategy interface or implementation, that will come in a follow-up.
1 parent 5a32380 commit 625fa86

File tree

10 files changed

+143
-149
lines changed

10 files changed

+143
-149
lines changed

codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ private void generateOperationExecutor(PythonWriter writer) {
127127

128128
var transportRequest = context.applicationProtocol().requestType();
129129
var transportResponse = context.applicationProtocol().responseType();
130-
var errorSymbol = CodegenUtils.getServiceError(context.settings());
131130
var pluginSymbol = CodegenUtils.getPluginSymbol(context.settings());
132131
var configSymbol = CodegenUtils.getConfigSymbol(context.settings());
133132

@@ -302,46 +301,54 @@ def _classify_error(
302301
}
303302
writer.addStdlibImport("typing", "Any");
304303
writer.addStdlibImport("asyncio", "iscoroutine");
304+
writer.addImports("smithy_core.exceptions", Set.of("SmithyException", "CallException"));
305+
writer.pushState();
306+
writer.putContext("request", transportRequest);
307+
writer.putContext("response", transportResponse);
308+
writer.putContext("plugin", pluginSymbol);
309+
writer.putContext("config", configSymbol);
305310
writer.write(
306311
"""
307312
async def _execute_operation[Input: SerializeableShape, Output: DeserializeableShape](
308313
self,
309314
input: Input,
310-
plugins: list[$1T],
311-
serialize: Callable[[Input, $5T], Awaitable[$2T]],
312-
deserialize: Callable[[$3T, $5T], Awaitable[Output]],
313-
config: $5T,
315+
plugins: list[${plugin:T}],
316+
serialize: Callable[[Input, ${config:T}], Awaitable[${request:T}]],
317+
deserialize: Callable[[${response:T}, ${config:T}], Awaitable[Output]],
318+
config: ${config:T},
314319
operation: APIOperation[Input, Output],
315-
request_future: Future[RequestContext[Any, $2T]] | None = None,
316-
response_future: Future[$3T] | None = None,
320+
request_future: Future[RequestContext[Any, ${request:T}]] | None = None,
321+
response_future: Future[${response:T}] | None = None,
317322
) -> Output:
318323
try:
319324
return await self._handle_execution(
320325
input, plugins, serialize, deserialize, config, operation,
321326
request_future, response_future,
322327
)
323328
except Exception as e:
329+
# Make sure every exception that we throw is an instance of SmithyException so
330+
# customers can reliably catch everything we throw.
331+
if not isinstance(e, SmithyException):
332+
wrapped = CallException(str(e))
333+
wrapped.__cause__ = e
334+
e = wrapped
335+
324336
if request_future is not None and not request_future.done():
325-
request_future.set_exception($4T(e))
337+
request_future.set_exception(e)
326338
if response_future is not None and not response_future.done():
327-
response_future.set_exception($4T(e))
328-
329-
# Make sure every exception that we throw is an instance of $4T so
330-
# customers can reliably catch everything we throw.
331-
if not isinstance(e, $4T):
332-
raise $4T(e) from e
339+
response_future.set_exception(e)
333340
raise
334341
335342
async def _handle_execution[Input: SerializeableShape, Output: DeserializeableShape](
336343
self,
337344
input: Input,
338-
plugins: list[$1T],
339-
serialize: Callable[[Input, $5T], Awaitable[$2T]],
340-
deserialize: Callable[[$3T, $5T], Awaitable[Output]],
341-
config: $5T,
345+
plugins: list[${plugin:T}],
346+
serialize: Callable[[Input, ${config:T}], Awaitable[${request:T}]],
347+
deserialize: Callable[[${response:T}, ${config:T}], Awaitable[Output]],
348+
config: ${config:T},
342349
operation: APIOperation[Input, Output],
343-
request_future: Future[RequestContext[Any, $2T]] | None,
344-
response_future: Future[$3T] | None,
350+
request_future: Future[RequestContext[Any, ${request:T}]] | None,
351+
response_future: Future[${response:T}] | None,
345352
) -> Output:
346353
operation_name = operation.schema.id.name
347354
logger.debug('Making request for operation "%s" with parameters: %s', operation_name, input)
@@ -350,11 +357,16 @@ def _classify_error(
350357
plugin(config)
351358
352359
input_context = InputContext(request=input, properties=TypedProperties({"config": config}))
353-
transport_request: $2T | None = None
354-
output_context: OutputContext[Input, Output, $2T | None, $3T | None] | None = None
360+
transport_request: ${request:T} | None = None
361+
output_context: OutputContext[
362+
Input,
363+
Output,
364+
${request:T} | None,
365+
${response:T} | None
366+
] | None = None
355367
356368
client_interceptors = cast(
357-
list[Interceptor[Input, Output, $2T, $3T]], list(config.interceptors)
369+
list[Interceptor[Input, Output, ${request:T}, ${response:T}]], list(config.interceptors)
358370
)
359371
interceptor_chain = InterceptorChain(client_interceptors)
360372
@@ -455,24 +467,20 @@ await sleep(retry_token.retry_delay)
455467
456468
async def _handle_attempt[Input: SerializeableShape, Output: DeserializeableShape](
457469
self,
458-
deserialize: Callable[[$3T, $5T], Awaitable[Output]],
459-
interceptor: Interceptor[Input, Output, $2T, $3T],
460-
context: RequestContext[Input, $2T],
461-
config: $5T,
470+
deserialize: Callable[[${response:T}, ${config:T}], Awaitable[Output]],
471+
interceptor: Interceptor[Input, Output, ${request:T}, ${response:T}],
472+
context: RequestContext[Input, ${request:T}],
473+
config: ${config:T},
462474
operation: APIOperation[Input, Output],
463-
request_future: Future[RequestContext[Input, $2T]] | None,
464-
) -> OutputContext[Input, Output, $2T, $3T | None]:
465-
transport_response: $3T | None = None
475+
request_future: Future[RequestContext[Input, ${request:T}]] | None,
476+
) -> OutputContext[Input, Output, ${request:T}, ${response:T} | None]:
477+
transport_response: ${response:T} | None = None
466478
try:
467479
# Step 7a: Invoke read_before_attempt
468480
interceptor.read_before_attempt(context)
469481
470-
""",
471-
pluginSymbol,
472-
transportRequest,
473-
transportResponse,
474-
errorSymbol,
475-
configSymbol);
482+
""");
483+
writer.popState();
476484

477485
boolean supportsAuth = !ServiceIndex.of(model).getAuthSchemes(service).isEmpty();
478486
writer.pushState(new ResolveIdentitySection());
@@ -873,8 +881,8 @@ private void writeSharedOperationInit(PythonWriter writer, OperationShape operat
873881
.orElse("The operation's input.");
874882

875883
writer.write("""
876-
$L
877-
""",docs);
884+
$L
885+
""", docs);
878886
writer.write("");
879887
writer.write(":param input: $L", inputDocs);
880888
writer.write("");

codegen/core/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ public static Symbol getPluginSymbol(PythonSettings settings) {
8989
/**
9090
* Gets the service error symbol.
9191
*
92-
* <p>This error is the top-level error for the client. Every error surfaced by
93-
* the client MUST be a subclass of this so that customers can reliably catch all
94-
* exceptions it raises. The client implementation will wrap any errors that aren't
95-
* already subclasses.
92+
* <p>This error is the top-level error for the client. Errors surfaced by the client
93+
* MUST be a subclass of this or SmithyException so that customers can reliably catch
94+
* all the exceptions a client throws. The request pipeline will wrap exceptions of
95+
* other types.
9696
*
9797
* @param settings The client settings, used to account for module configuration.
9898
* @return Returns the symbol for the client's error class.
@@ -105,40 +105,6 @@ public static Symbol getServiceError(PythonSettings settings) {
105105
.build();
106106
}
107107

108-
/**
109-
* Gets the service API error symbol.
110-
*
111-
* <p>This error is the parent class for all errors returned over the wire by the
112-
* service, including unknown errors.
113-
*
114-
* @param settings The client settings, used to account for module configuration.
115-
* @return Returns the symbol for the client's API error class.
116-
*/
117-
public static Symbol getApiError(PythonSettings settings) {
118-
return Symbol.builder()
119-
.name("ApiError")
120-
.namespace(String.format("%s.models", settings.moduleName()), ".")
121-
.definitionFile(String.format("./src/%s/models.py", settings.moduleName()))
122-
.build();
123-
}
124-
125-
/**
126-
* Gets the unknown API error symbol.
127-
*
128-
* <p> This error is the parent class for all errors returned over the wire by
129-
* the service which aren't in the model.
130-
*
131-
* @param settings The client settings, used to account for module configuration.
132-
* @return Returns the symbol for unknown API errors.
133-
*/
134-
public static Symbol getUnknownApiError(PythonSettings settings) {
135-
return Symbol.builder()
136-
.name("UnknownApiError")
137-
.namespace(String.format("%s.models", settings.moduleName()), ".")
138-
.definitionFile(String.format("./src/%s/models.py", settings.moduleName()))
139-
.build();
140-
}
141-
142108
/**
143109
* Gets the symbol for the http auth params.
144110
*

codegen/core/src/main/java/software/amazon/smithy/python/codegen/PythonSymbolProvider.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.util.Locale;
1010
import java.util.logging.Logger;
1111
import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider;
12-
import software.amazon.smithy.codegen.core.ReservedWords;
1312
import software.amazon.smithy.codegen.core.ReservedWordsBuilder;
1413
import software.amazon.smithy.codegen.core.Symbol;
1514
import software.amazon.smithy.codegen.core.SymbolProvider;
@@ -84,6 +83,10 @@ public PythonSymbolProvider(Model model, PythonSettings settings) {
8483
var reservedMemberNamesBuilder = new ReservedWordsBuilder()
8584
.loadWords(PythonSymbolProvider.class.getResource("reserved-member-names.txt"), this::escapeWord);
8685

86+
// Reserved words that only apply to error members.
87+
var reservedErrorMembers = new ReservedWordsBuilder()
88+
.loadWords(PythonSymbolProvider.class.getResource("reserved-error-member-names.txt"), this::escapeWord);
89+
8790
escaper = ReservedWordSymbolProvider.builder()
8891
.nameReservedWords(reservedClassNames)
8992
.memberReservedWords(reservedMemberNamesBuilder.build())
@@ -92,13 +95,8 @@ public PythonSymbolProvider(Model model, PythonSettings settings) {
9295
.escapePredicate((shape, symbol) -> !StringUtils.isEmpty(symbol.getDefinitionFile()))
9396
.buildEscaper();
9497

95-
// Reserved words that only apply to error members.
96-
ReservedWords reservedErrorMembers = reservedMemberNamesBuilder
97-
.put("code", "code_")
98-
.build();
99-
10098
errorMemberEscaper = ReservedWordSymbolProvider.builder()
101-
.memberReservedWords(reservedErrorMembers)
99+
.memberReservedWords(reservedErrorMembers.build())
102100
.escapePredicate((shape, symbol) -> !StringUtils.isEmpty(symbol.getDefinitionFile()))
103101
.buildEscaper();
104102
}

codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ServiceErrorGenerator.java

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55
package software.amazon.smithy.python.codegen.generators;
66

7-
import java.util.Set;
87
import software.amazon.smithy.codegen.core.WriterDelegator;
98
import software.amazon.smithy.python.codegen.CodegenUtils;
109
import software.amazon.smithy.python.codegen.PythonSettings;
@@ -30,38 +29,15 @@ public void run() {
3029
var serviceError = CodegenUtils.getServiceError(settings);
3130
writers.useFileWriter(serviceError.getDefinitionFile(), serviceError.getNamespace(), writer -> {
3231
writer.addDependency(SmithyPythonDependency.SMITHY_CORE);
33-
writer.addImport("smithy_core.exceptions", "SmithyException");
32+
writer.addImport("smithy_core.exceptions", "ModeledException");
3433
writer.write("""
35-
class $L(SmithyException):
36-
""\"Base error for all errors in the service.""\"
37-
pass
38-
""", serviceError.getName());
39-
});
40-
41-
var apiError = CodegenUtils.getApiError(settings);
42-
writers.useFileWriter(apiError.getDefinitionFile(), apiError.getNamespace(), writer -> {
43-
writer.addStdlibImports("typing", Set.of("Literal", "ClassVar"));
44-
var unknownApiError = CodegenUtils.getUnknownApiError(settings);
45-
46-
writer.write("""
47-
@dataclass
48-
class $1L($2T):
49-
""\"Base error for all API errors in the service.""\"
50-
code: ClassVar[str]
51-
fault: ClassVar[Literal["client", "server"]]
34+
class $L(ModeledException):
35+
""\"Base error for all errors in the service.
5236
53-
message: str
54-
55-
def __post_init__(self) -> None:
56-
super().__init__(self.message)
57-
58-
59-
@dataclass
60-
class $3L($1L):
61-
""\"Error representing any unknown api errors.""\"
62-
code: ClassVar[str] = 'Unknown'
63-
fault: ClassVar[Literal["client", "server"]] = "client"
64-
""", apiError.getName(), serviceError, unknownApiError.getName());
37+
Some exceptions do not extend from this class, including
38+
synthetic, implicit, and shared exception types.
39+
""\"
40+
""", serviceError.getName());
6541
});
6642
}
6743
}

codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/SetupGenerator.java

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,6 @@ private static void writeIndexes(GenerationContext context, String projectName)
451451
writeIndexFile(context, "docs/models/index.rst", "Models");
452452
}
453453

454-
455454
/**
456455
* Write the readme in the docs folder describing instructions for generation
457456
*
@@ -461,18 +460,18 @@ private static void writeDocsReadme(
461460
GenerationContext context
462461
) {
463462
context.writerDelegator().useFileWriter("docs/README.md", writer -> {
464-
writer.write("""
465-
## Generating Documentation
466-
467-
Sphinx is used for documentation. You can generate HTML locally with the
468-
following:
469-
470-
```
471-
$$ uv pip install ".[docs]"
472-
$$ cd docs
473-
$$ make html
474-
```
475-
""");
463+
writer.write("""
464+
## Generating Documentation
465+
466+
Sphinx is used for documentation. You can generate HTML locally with the
467+
following:
468+
469+
```
470+
$$ uv pip install ".[docs]"
471+
$$ cd docs
472+
$$ make html
473+
```
474+
""");
476475
});
477476
}
478477

codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,31 +130,26 @@ private void renderError() {
130130
writer.addStdlibImports("typing", Set.of("Literal", "ClassVar"));
131131
writer.addStdlibImport("dataclasses", "dataclass");
132132

133-
// TODO: Implement protocol-level customization of the error code
134133
var fault = errorTrait.getValue();
135-
var code = shape.getId().getName();
136134
var symbol = symbolProvider.toSymbol(shape);
137-
var apiError = CodegenUtils.getApiError(settings);
135+
var baseError = CodegenUtils.getServiceError(settings);
138136
writer.pushState(new ErrorSection(symbol));
139137
writer.write("""
140138
@dataclass(kw_only=True)
141139
class $1L($2T):
142-
${5C|}
140+
${4C|}
141+
142+
fault: Literal["client", "server"] | None = $3S
143143
144-
code: ClassVar[str] = $3S
145-
fault: ClassVar[Literal["client", "server"]] = $4S
144+
${5C|}
146145
147-
message: str
148146
${6C|}
149147
150148
${7C|}
151149
152-
${8C|}
153-
154150
""",
155151
symbol.getName(),
156-
apiError,
157-
code,
152+
baseError,
158153
fault,
159154
writer.consumer(w -> writeClassDocs(true)),
160155
writer.consumer(w -> writeProperties()),
@@ -325,7 +320,9 @@ private void writeMemberDocs(MemberShape member) {
325320

326321
String memberName = symbolProvider.toMemberName(member);
327322
String docs = writer.formatDocs(String.format(":param %s: %s%s",
328-
memberName, descriptionPrefix, trait.getValue()));
323+
memberName,
324+
descriptionPrefix,
325+
trait.getValue()));
329326
writer.write(docs);
330327
});
331328
}

0 commit comments

Comments
 (0)