Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.palantir.conjure.python.processors.typename.PackagePrependingTypeNameProcessor;
import com.palantir.conjure.python.processors.typename.TypeNameProcessor;
import com.palantir.conjure.python.types.DefinitionImportTypeDefinitionVisitor;
import com.palantir.conjure.python.types.PythonErrorGenerator;
import com.palantir.conjure.python.types.PythonTypeGenerator;
import com.palantir.conjure.spec.ConjureDefinition;
import com.palantir.conjure.spec.TypeName;
Expand Down Expand Up @@ -146,6 +147,12 @@ private PythonFile getImplPythonFile(
definitionPackageNameProcessor,
definitionTypeNameProcessor,
dealiasingTypeVisitor);
PythonErrorGenerator errorGenerator = new PythonErrorGenerator(
implPackageNameProcessor,
implTypeNameProcessor,
definitionPackageNameProcessor,
definitionTypeNameProcessor,
dealiasingTypeVisitor);

List<PythonSnippet> snippets = new ArrayList<>();
snippets.addAll(conjureDefinition.getTypes().stream()
Expand All @@ -154,6 +161,11 @@ private PythonFile getImplPythonFile(
snippets.addAll(conjureDefinition.getServices().stream()
.map(clientGenerator::generateClient)
.toList());
if (config.generateErrorTypes()) {
snippets.addAll(conjureDefinition.getErrors().stream()
.map(errorGenerator::generateError)
.toList());
}

Map<PythonPackage, List<PythonSnippet>> snippetsByPackage =
snippets.stream().collect(Collectors.groupingBy(PythonSnippet::pythonPackage));
Expand Down Expand Up @@ -203,6 +215,19 @@ private List<PythonFile> getInitFiles(
importsByPackage.put(pythonPackage, pythonImport);
});

if (config.generateErrorTypes()) {
conjureDefinition.getErrors().forEach(errorDefinition -> {
PythonPackage pythonPackage = PythonPackage.of(definitionPackageNameProcessor.process(
errorDefinition.getErrorName().getPackage()));
PythonImport pythonImport = PythonImport.of(
moduleSpecifier,
NamedImport.of(
implTypeNameProcessor.process(errorDefinition.getErrorName()),
definitionTypeNameProcessor.process(errorDefinition.getErrorName())));
importsByPackage.put(pythonPackage, pythonImport);
});
}

return KeyedStream.stream(importsByPackage.build().asMap())
.map((pythonPackage, imports) -> {
List<String> importNames = imports.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public interface GeneratorConfiguration {

boolean generateRawSource();

@Value.Default
default boolean generateErrorTypes() {
return false;
}

default Optional<String> pythonicPackageName() {
return packageName().map(packageName -> packageName.replace('-', '_'));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* (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 {
ImmutableList<PythonImport> DEFAULT_IMPORTS = ImmutableList.of(
PythonImport.builder()
.moduleSpecifier(ImportTypeVisitor.CONJURE_PYTHON_CLIENT)
.addNamedImports(NamedImport.of("ConjureHTTPError"))
.build(),
PythonImport.builder()
.moduleSpecifier(ImportTypeVisitor.TYPING)
.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
poetWriter.writeIndentedLine(String.format("ERROR_CODE = \"%s\"", errorCode()));
poetWriter.writeIndentedLine(String.format("ERROR_NAMESPACE = \"%s\"", namespace()));
poetWriter.writeIndentedLine(String.format("ERROR_NAME = \"%s\"", definitionName()));

poetWriter.writeLine();

// args
emitTypedDict(poetWriter, "SafeArgs", safeArgs());
emitTypedDict(poetWriter, "UnsafeArgs", unsafeArgs());

emitConstructor(poetWriter);

// 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__(");
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
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());
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 ? "" : ",";
poetWriter.writeIndentedLine(String.format(
"'%s': base_error.parameters['%s']%s",
PythonIdentifierSanitizer.sanitize(field.attributeName()), field.jsonIdentifier(), comma));
}
poetWriter.decreaseIndent();
poetWriter.writeIndentedLine("}");
}

default void emitIsInstanceMethod(PythonPoetWriter poetWriter) {
poetWriter.writeIndentedLine("@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");
poetWriter.decreaseIndent();
poetWriter.writeIndentedLine(")");
poetWriter.decreaseIndent();
poetWriter.writeLine();
}

default void emitFromErrorMethod(PythonPoetWriter poetWriter) {
poetWriter.writeIndentedLine("@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 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* (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.visitor.DealiasingTypeVisitor;
import com.palantir.conjure.visitor.TypeVisitor;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

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);

// Collect imports from all field types
Set<PythonImport> imports = errorDef.getSafeArgs().stream()
.flatMap(entry -> entry.getType().accept(importVisitor).stream())
.collect(Collectors.toSet());
imports.addAll(errorDef.getUnsafeArgs().stream()
.flatMap(entry -> entry.getType().accept(importVisitor).stream())
.collect(Collectors.toSet()));

// Convert safe args to PythonFields
List<PythonField> safeArgs = errorDef.getSafeArgs().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());

// Convert unsafe args to PythonFields
List<PythonField> unsafeArgs = errorDef.getUnsafeArgs().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());

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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public final class ConjurePythonGeneratorTest {
.generatorVersion("0.0.0")
.shouldWriteCondaRecipe(true)
.generateRawSource(false)
.generateErrorTypes(true)
.build());
private final InMemoryPythonFileWriter pythonFileWriter = new InMemoryPythonFileWriter();

Expand Down
Loading