-
Notifications
You must be signed in to change notification settings - Fork 19
Feature: Add an option to generate exceptions for Conjure errors #1306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wsuppiger
wants to merge
9
commits into
develop
Choose a base branch
from
wsuppiger/errors-fix
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
16f3b89
Feature: Add an option to generate exceptions for Conjure errors
27ecd72
clean up a bit
20d2a1f
example names
4bc59dc
Fix generated error class: namespaced ERROR_NAME, optional args, buil…
d241491
WIP - reply to comments
624e4ff
WIP - decouple generated errors from ConjureHTTPError; encode/decode …
af59f86
WIP - verifier generates and round-trip tests error types in CI
1a62fb6
fix tests
3710940
fix tests
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| /* | ||
| * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.palantir.conjure.python.poet; | ||
|
|
||
| import com.google.common.collect.ImmutableList; | ||
| import com.palantir.conjure.python.processors.PythonIdentifierSanitizer; | ||
| import com.palantir.conjure.python.types.ImportTypeVisitor; | ||
| import com.palantir.conjure.spec.Documentation; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import org.immutables.value.Value; | ||
|
|
||
| @Value.Immutable | ||
| public interface ErrorSnippet extends PythonSnippet { | ||
|
dtobin marked this conversation as resolved.
|
||
| ImmutableList<PythonImport> DEFAULT_IMPORTS = ImmutableList.of( | ||
| PythonImport.builder() | ||
| .moduleSpecifier(ImportTypeVisitor.CONJURE_PYTHON_CLIENT) | ||
| .addNamedImports(NamedImport.of("ConjureHTTPError")) | ||
| .build(), | ||
| PythonImport.of("builtins"), | ||
| PythonImport.builder() | ||
| .moduleSpecifier(ImportTypeVisitor.TYPING) | ||
|
wsuppiger marked this conversation as resolved.
|
||
| .addNamedImports(NamedImport.of("TypedDict")) | ||
| .build()); | ||
|
|
||
| @Override | ||
| @Value.Default | ||
| default String idForSorting() { | ||
| return className(); | ||
| } | ||
|
|
||
| String className(); | ||
|
|
||
| String definitionName(); | ||
|
|
||
| PythonPackage definitionPackage(); | ||
|
|
||
| Optional<Documentation> docs(); | ||
|
|
||
| String errorCode(); | ||
|
|
||
| String namespace(); | ||
|
|
||
| List<PythonField> safeArgs(); | ||
|
|
||
| List<PythonField> unsafeArgs(); | ||
|
|
||
| @Override | ||
| default void emit(PythonPoetWriter poetWriter) { | ||
| poetWriter.writeIndentedLine(String.format("class %s(ConjureHTTPError):", className())); | ||
| poetWriter.increaseIndent(); | ||
| docs().ifPresent(poetWriter::writeDocs); | ||
|
|
||
| poetWriter.writeLine(); | ||
|
|
||
| // Error constants. ERROR_NAME is the fully-qualified wire form (e.g. "Datasets:DatasetNotFound") and | ||
| // matches the value of ConjureHTTPError.error_name parsed from the response body. | ||
| poetWriter.writeIndentedLine(String.format("ERROR_CODE = \"%s\"", errorCode())); | ||
| poetWriter.writeIndentedLine(String.format("ERROR_NAMESPACE = \"%s\"", namespace())); | ||
| poetWriter.writeIndentedLine(String.format("ERROR_NAME = \"%s:%s\"", namespace(), definitionName())); | ||
|
|
||
| poetWriter.writeLine(); | ||
|
|
||
| // args | ||
| emitTypedDict(poetWriter, "SafeArgs", safeArgs()); | ||
| emitTypedDict(poetWriter, "UnsafeArgs", unsafeArgs()); | ||
|
|
||
| emitConstructor(poetWriter); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we define the args as properties as well? |
||
| // classmethods | ||
| emitIsInstanceMethod(poetWriter); | ||
| emitFromErrorMethod(poetWriter); | ||
|
|
||
| // end of class def | ||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeLine(); | ||
| poetWriter.writeLine(); | ||
|
|
||
| PythonClassRenamer.renameClass(poetWriter, className(), definitionPackage(), definitionName()); | ||
| } | ||
|
|
||
| default void emitTypedDict(PythonPoetWriter poetWriter, String typedDictName, List<PythonField> fields) { | ||
| if (fields.isEmpty()) { | ||
| return; | ||
| } | ||
| poetWriter.writeIndentedLine(String.format("class %s(TypedDict):", typedDictName)); | ||
| poetWriter.increaseIndent(); | ||
| for (PythonField field : fields) { | ||
| poetWriter.writeIndentedLine(String.format( | ||
| "%s: %s", PythonIdentifierSanitizer.sanitize(field.attributeName()), field.myPyType())); | ||
| } | ||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeLine(); | ||
| } | ||
|
|
||
| default void emitConstructor(PythonPoetWriter poetWriter) { | ||
| poetWriter.writeIndentedLine("def __init__(self, base_error: ConjureHTTPError) -> None:"); | ||
| poetWriter.increaseIndent(); | ||
| poetWriter.writeIndentedLine("super().__init__("); | ||
|
wsuppiger marked this conversation as resolved.
Outdated
|
||
| poetWriter.increaseIndent(); | ||
| poetWriter.writeIndentedLine("status_code=base_error.status_code,"); | ||
| // TODO(bzhang): Use enum once https://github.com/palantir/conjure-python-client/pull/171 is merged | ||
|
wsuppiger marked this conversation as resolved.
Outdated
|
||
| poetWriter.writeIndentedLine("error_code=base_error.error_code,"); | ||
| poetWriter.writeIndentedLine("error_name=base_error.error_name,"); | ||
| poetWriter.writeIndentedLine("error_instance_id=base_error.error_instance_id,"); | ||
| poetWriter.writeIndentedLine("parameters=base_error.parameters"); | ||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeIndentedLine(")"); | ||
|
|
||
| emitArgsParser(poetWriter, "safe_args", "SafeArgs", safeArgs()); | ||
|
dtobin marked this conversation as resolved.
Outdated
|
||
| emitArgsParser(poetWriter, "unsafe_args", "UnsafeArgs", unsafeArgs()); | ||
|
|
||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeLine(); | ||
| } | ||
|
|
||
| default void emitArgsParser( | ||
| PythonPoetWriter poetWriter, String fieldName, String typeName, List<PythonField> fields) { | ||
| if (fields.isEmpty()) { | ||
| return; | ||
| } | ||
| poetWriter.writeIndentedLine(String.format("self.%s: %s.%s = {", fieldName, className(), typeName)); | ||
| poetWriter.increaseIndent(); | ||
| for (int i = 0; i < fields.size(); i++) { | ||
| PythonField field = fields.get(i); | ||
| String comma = i == fields.size() - 1 ? "" : ","; | ||
| String lookup = field.isOptional() | ||
| ? String.format("base_error.parameters.get('%s')", field.jsonIdentifier()) | ||
| : String.format("base_error.parameters['%s']", field.jsonIdentifier()); | ||
| poetWriter.writeIndentedLine(String.format( | ||
| "'%s': %s%s", PythonIdentifierSanitizer.sanitize(field.attributeName()), lookup, comma)); | ||
| } | ||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeIndentedLine("}"); | ||
| } | ||
|
|
||
| default void emitIsInstanceMethod(PythonPoetWriter poetWriter) { | ||
| poetWriter.writeIndentedLine("@builtins.classmethod"); | ||
| poetWriter.writeIndentedLine("def is_instance(cls, error: ConjureHTTPError) -> bool:"); | ||
| poetWriter.increaseIndent(); | ||
| poetWriter.writeIndentedLine("return ("); | ||
| poetWriter.increaseIndent(); | ||
| poetWriter.writeIndentedLine("error.error_name == cls.ERROR_NAME and"); | ||
| poetWriter.writeIndentedLine("error.error_code == cls.ERROR_CODE"); | ||
|
wsuppiger marked this conversation as resolved.
Outdated
|
||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeIndentedLine(")"); | ||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeLine(); | ||
| } | ||
|
|
||
| default void emitFromErrorMethod(PythonPoetWriter poetWriter) { | ||
| poetWriter.writeIndentedLine("@builtins.classmethod"); | ||
| poetWriter.writeIndentedLine( | ||
| String.format("def from_error(cls, error: ConjureHTTPError) -> '%s':", className())); | ||
| poetWriter.increaseIndent(); | ||
| poetWriter.writeIndentedLine("if not cls.is_instance(error):"); | ||
| poetWriter.increaseIndent(); | ||
| poetWriter.writeIndentedLine("raise ValueError(f\"Error '{error.error_name}' is not a {cls.ERROR_NAME}\")"); | ||
| poetWriter.decreaseIndent(); | ||
| poetWriter.writeIndentedLine("return cls(error)"); | ||
| poetWriter.decreaseIndent(); | ||
| } | ||
|
|
||
| class Builder extends ImmutableErrorSnippet.Builder {} | ||
|
|
||
| static Builder builder() { | ||
| return new Builder(); | ||
| } | ||
| } | ||
102 changes: 102 additions & 0 deletions
102
...ure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /* | ||
| * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.palantir.conjure.python.types; | ||
|
|
||
| import com.palantir.conjure.CaseConverter; | ||
| import com.palantir.conjure.python.poet.ErrorSnippet; | ||
| import com.palantir.conjure.python.poet.PythonField; | ||
| import com.palantir.conjure.python.poet.PythonImport; | ||
| import com.palantir.conjure.python.poet.PythonPackage; | ||
| import com.palantir.conjure.python.processors.packagename.PackageNameProcessor; | ||
| import com.palantir.conjure.python.processors.typename.TypeNameProcessor; | ||
| import com.palantir.conjure.spec.ErrorDefinition; | ||
| import com.palantir.conjure.spec.FieldDefinition; | ||
| import com.palantir.conjure.visitor.DealiasingTypeVisitor; | ||
| import com.palantir.conjure.visitor.TypeVisitor; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| public final class PythonErrorGenerator { | ||
|
|
||
| private final PackageNameProcessor implPackageNameProcessor; | ||
| private final TypeNameProcessor implTypeNameProcessor; | ||
| private final PackageNameProcessor definitionPackageNameProcessor; | ||
| private final TypeNameProcessor definitionTypeNameProcessor; | ||
| private final DealiasingTypeVisitor dealiasingTypeVisitor; | ||
| private final PythonTypeNameVisitor pythonTypeNameVisitor; | ||
| private final MyPyTypeNameVisitor myPyTypeNameVisitor; | ||
|
|
||
| public PythonErrorGenerator( | ||
| PackageNameProcessor implPackageNameProcessor, | ||
| TypeNameProcessor implTypeNameProcessor, | ||
| PackageNameProcessor definitionPackageNameProcessor, | ||
| TypeNameProcessor definitionTypeNameProcessor, | ||
| DealiasingTypeVisitor dealiasingTypeVisitor) { | ||
| this.implPackageNameProcessor = implPackageNameProcessor; | ||
| this.implTypeNameProcessor = implTypeNameProcessor; | ||
| this.definitionPackageNameProcessor = definitionPackageNameProcessor; | ||
| this.definitionTypeNameProcessor = definitionTypeNameProcessor; | ||
| this.dealiasingTypeVisitor = dealiasingTypeVisitor; | ||
| this.pythonTypeNameVisitor = new PythonTypeNameVisitor(implTypeNameProcessor); | ||
| this.myPyTypeNameVisitor = new MyPyTypeNameVisitor(dealiasingTypeVisitor, implTypeNameProcessor); | ||
| } | ||
|
|
||
| public ErrorSnippet generateError(ErrorDefinition errorDef) { | ||
| ImportTypeVisitor importVisitor = | ||
| new ImportTypeVisitor(errorDef.getErrorName(), implTypeNameProcessor, implPackageNameProcessor); | ||
|
|
||
| Set<PythonImport> imports = Stream.concat(errorDef.getSafeArgs().stream(), errorDef.getUnsafeArgs().stream()) | ||
| .flatMap(entry -> entry.getType().accept(importVisitor).stream()) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| List<PythonField> safeArgs = toPythonFields(errorDef.getSafeArgs()); | ||
| List<PythonField> unsafeArgs = toPythonFields(errorDef.getUnsafeArgs()); | ||
|
|
||
| return ErrorSnippet.builder() | ||
| .pythonPackage(PythonPackage.of( | ||
| implPackageNameProcessor.process(errorDef.getErrorName().getPackage()))) | ||
| .className(implTypeNameProcessor.process(errorDef.getErrorName())) | ||
| .definitionPackage(PythonPackage.of(definitionPackageNameProcessor.process( | ||
| errorDef.getErrorName().getPackage()))) | ||
| .definitionName(definitionTypeNameProcessor.process(errorDef.getErrorName())) | ||
| .addAllImports(ErrorSnippet.DEFAULT_IMPORTS) | ||
| .addAllImports(imports) | ||
| .docs(errorDef.getDocs()) | ||
| .errorCode(errorDef.getCode().toString()) | ||
| .namespace(errorDef.getNamespace().toString()) | ||
| .safeArgs(safeArgs) | ||
| .unsafeArgs(unsafeArgs) | ||
| .build(); | ||
| } | ||
|
|
||
| private List<PythonField> toPythonFields(List<FieldDefinition> fields) { | ||
|
wsuppiger marked this conversation as resolved.
|
||
| return fields.stream() | ||
| .map(entry -> PythonField.builder() | ||
| .attributeName(CaseConverter.toCase(entry.getFieldName().get(), CaseConverter.Case.SNAKE_CASE)) | ||
| .jsonIdentifier(entry.getFieldName().get()) | ||
| .docs(entry.getDocs()) | ||
| .pythonType(entry.getType().accept(pythonTypeNameVisitor)) | ||
| .myPyType(entry.getType().accept(myPyTypeNameVisitor)) | ||
| .isOptional(dealiasingTypeVisitor | ||
| .dealias(entry.getType()) | ||
| .fold(_typeDefinition -> false, type -> type.accept(TypeVisitor.IS_OPTIONAL))) | ||
| .build()) | ||
| .collect(Collectors.toList()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
conjure-python-core/src/test/resources/errors/example-errors.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| types: | ||
| definitions: | ||
| default-package: com.palantir.product | ||
| objects: | ||
| Dataset: | ||
| fields: | ||
| fileSystemId: string | ||
| rid: string | ||
| errors: | ||
| DatasetNotFound: | ||
| namespace: Datasets | ||
| code: NOT_FOUND | ||
| safe-args: | ||
| datasetRid: string | ||
| availableDatasets: list<string> | ||
| docs: Thrown when the requested dataset does not exist | ||
|
|
||
| InvalidFileSystemId: | ||
| namespace: Datasets | ||
| code: INVALID_ARGUMENT | ||
| safe-args: | ||
| fileSystemId: string | ||
|
wsuppiger marked this conversation as resolved.
|
||
| reason: optional<string> | ||
| unsafe-args: | ||
| userId: string | ||
| docs: Thrown when a file system identifier is invalid | ||
|
|
||
| services: | ||
| DatasetService: | ||
| name: Dataset Service | ||
| package: com.palantir.product | ||
| base-path: /datasets | ||
| endpoints: | ||
| getDatasetsByFileSystem: | ||
| http: GET /fileSystem/{fileSystemId} | ||
| args: | ||
| fileSystemId: string | ||
| returns: list<Dataset> | ||
| docs: Get datasets by file system | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.