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 @@ -139,13 +139,15 @@ private PythonFile getImplPythonFile(
implTypeNameProcessor,
definitionPackageNameProcessor,
definitionTypeNameProcessor,
dealiasingTypeVisitor);
dealiasingTypeVisitor,
config.forceKeywordArgs());
ClientGenerator clientGenerator = new ClientGenerator(
implPackageNameProcessor,
implTypeNameProcessor,
definitionPackageNameProcessor,
definitionTypeNameProcessor,
dealiasingTypeVisitor);
dealiasingTypeVisitor,
config.forceKeywordArgs());

List<PythonSnippet> snippets = new ArrayList<>();
snippets.addAll(conjureDefinition.getTypes().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 forceKeywordArgs() {
return false;
}

default Optional<String> pythonicPackageName() {
return packageName().map(packageName -> packageName.replace('-', '_'));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,21 @@ public final class ClientGenerator {
private final DealiasingTypeVisitor dealiasingTypeVisitor;
private final PythonTypeNameVisitor pythonTypeNameVisitor;
private final MyPyTypeNameVisitor myPyTypeNameVisitor;
private final boolean forceKeywordArgs;

public ClientGenerator(
PackageNameProcessor implPackageNameProcessor,
TypeNameProcessor implTypeNameProcessor,
PackageNameProcessor definitionPackageNameProcessor,
TypeNameProcessor definitionTypeNameProcessor,
DealiasingTypeVisitor dealiasingTypeVisitor) {
DealiasingTypeVisitor dealiasingTypeVisitor,
boolean forceKeywordArgs) {
this.implPackageNameProcessor = implPackageNameProcessor;
this.implTypeNameProcessor = implTypeNameProcessor;
this.definitionPackageNameProcessor = definitionPackageNameProcessor;
this.definitionTypeNameProcessor = definitionTypeNameProcessor;
this.dealiasingTypeVisitor = dealiasingTypeVisitor;
this.forceKeywordArgs = forceKeywordArgs;
pythonTypeNameVisitor = new PythonTypeNameVisitor(implTypeNameProcessor);
myPyTypeNameVisitor = new MyPyTypeNameVisitor(dealiasingTypeVisitor, implTypeNameProcessor);
}
Expand Down Expand Up @@ -154,6 +157,7 @@ private PythonEndpointDefinition generateEndpoint(
.dealias(rt)
.fold(_typeDefinition -> false, type -> type.accept(TypeVisitor.IS_OPTIONAL)))
.orElse(false))
.forceKeywordArgs(forceKeywordArgs)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ default String idForSorting() {

List<PythonField> fields();

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

@Override
default void emit(PythonPoetWriter poetWriter) {
poetWriter.writeIndentedLine(String.format("class %s(ConjureBeanType):", className()));
Expand Down Expand Up @@ -97,22 +102,25 @@ default void emit(PythonPoetWriter poetWriter) {

// constructor -- only if there are fields
if (!fields().isEmpty()) {
poetWriter.writeIndentedLine(String.format(
"def __init__(self, %s) -> None:",
Joiner.on(", ")
.join(fields().stream()
.sorted(new PythonField.PythonFieldComparator())
.map(field -> {
String name = String.format(
"%s: %s",
PythonIdentifierSanitizer.sanitize(field.attributeName()),
field.myPyType());
if (field.isOptional()) {
return String.format("%s = None", name);
}
return name;
})
.collect(Collectors.toList()))));
String argsString = Joiner.on(", ")
.join(fields().stream()
.sorted(new PythonField.PythonFieldComparator())
.map(field -> {
String name = String.format(
"%s: %s",
PythonIdentifierSanitizer.sanitize(field.attributeName()), field.myPyType());
if (field.isOptional()) {
return String.format("%s = None", name);
}
return name;
})
.collect(Collectors.toList()));

String initSignature = forceKeywordArgs()
? String.format("def __init__(self, *, %s) -> None:", argsString)
: String.format("def __init__(self, %s) -> None:", argsString);

poetWriter.writeIndentedLine(initSignature);
poetWriter.increaseIndent();
fields().forEach(field -> poetWriter.writeIndentedLine(String.format(
"self._%s = %s",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public interface PythonEndpointDefinition extends Emittable {

Optional<String> myPyReturnType();

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

@Value.Check
default void check() {
checkState(
Expand Down Expand Up @@ -90,22 +95,27 @@ default void emit(PythonPoetWriter poetWriter) {
.build()
: params();

poetWriter.writeIndentedLine(
"def %s(self, %s) -> %s:",
pythonMethodName(),
Joiner.on(", ")
.join(paramsWithHeader.stream()
.sorted(new PythonEndpointParamComparator())
.map(param -> {
String typedParam =
String.format("%s: %s", param.pythonParamName(), param.myPyType());
if (param.isOptional() || param.isCollection()) {
return String.format("%s = None", typedParam);
}
return typedParam;
})
.collect(Collectors.toList())),
myPyReturnType().orElse("None"));
String paramsString = Joiner.on(", ")
.join(paramsWithHeader.stream()
.sorted(new PythonEndpointParamComparator())
.map(param -> {
String typedParam = String.format("%s: %s", param.pythonParamName(), param.myPyType());
if (param.isOptional() || param.isCollection()) {
return String.format("%s = None", typedParam);
}
return typedParam;
})
.collect(Collectors.toList()));

String methodSignature = forceKeywordArgs() && !paramsWithHeader.isEmpty()
? String.format(
"def %s(self, *, %s) -> %s:",
pythonMethodName(), paramsString, myPyReturnType().orElse("None"))
: String.format(
"def %s(self, %s) -> %s:",
pythonMethodName(), paramsString, myPyReturnType().orElse("None"));

poetWriter.writeIndentedLine(methodSignature);
poetWriter.increaseIndent();
docs().ifPresent(poetWriter::writeDocs);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ default String idForSorting() {

List<PythonField> options();

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

/** The name of the option as a constructor / method parameter. */
static String parameterName(PythonField option) {
return PythonIdentifierSanitizer.sanitize(option.attributeName());
Expand Down Expand Up @@ -120,6 +125,9 @@ default void emit(PythonPoetWriter poetWriter) {
poetWriter.increaseIndent();
poetWriter.increaseIndent();
poetWriter.writeIndentedLine("self,");
if (forceKeywordArgs() && !options().isEmpty()) {
poetWriter.writeIndentedLine("*,");
}
for (int i = 0; i < options().size(); i++) {
PythonField option = options().get(i);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,21 @@ public final class PythonTypeGenerator {
private final DealiasingTypeVisitor dealiasingTypeVisitor;
private final PythonTypeNameVisitor pythonTypeNameVisitor;
private final MyPyTypeNameVisitor myPyTypeNameVisitor;
private final boolean forceKeywordArgs;

public PythonTypeGenerator(
PackageNameProcessor implPackageNameProcessor,
TypeNameProcessor implTypeNameProcessor,
PackageNameProcessor definitionPackageNameProcessor,
TypeNameProcessor definitionTypeNameProcessor,
DealiasingTypeVisitor dealiasingTypeVisitor) {
DealiasingTypeVisitor dealiasingTypeVisitor,
boolean forceKeywordArgs) {
this.implPackageNameProcessor = implPackageNameProcessor;
this.implTypeNameProcessor = implTypeNameProcessor;
this.definitionPackageNameProcessor = definitionPackageNameProcessor;
this.definitionTypeNameProcessor = definitionTypeNameProcessor;
this.dealiasingTypeVisitor = dealiasingTypeVisitor;
this.forceKeywordArgs = forceKeywordArgs;
pythonTypeNameVisitor = new PythonTypeNameVisitor(implTypeNameProcessor);
myPyTypeNameVisitor = new MyPyTypeNameVisitor(dealiasingTypeVisitor, implTypeNameProcessor);
}
Expand Down Expand Up @@ -127,6 +130,7 @@ private BeanSnippet generateBean(ObjectDefinition typeDef) {
.addAllImports(imports)
.docs(typeDef.getDocs())
.fields(fields)
.forceKeywordArgs(forceKeywordArgs)
.build();
}

Expand Down Expand Up @@ -182,6 +186,7 @@ private UnionSnippet generateUnion(UnionDefinition typeDef) {
.addAllImports(imports)
.docs(typeDef.getDocs())
.options(options)
.forceKeywordArgs(forceKeywordArgs)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* (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;

import static org.assertj.core.api.Assertions.assertThat;

import com.palantir.conjure.defs.Conjure;
import com.palantir.conjure.spec.ConjureDefinition;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;

public final class ForceKeywordArgsTest {

@Test
public void testConstructorKeywordArgsAreForced() throws IOException {
String generated = generateCode(true);

assertThat(generated).contains("def __init__(self, *, age: int, email: str, name: str) -> None:");
}

@Test
public void testConstructorPositionalArgsByDefault() throws IOException {
String generated = generateCode(false);

assertThat(generated).contains("def __init__(self, age: int, email: str, name: str) -> None:");
assertThat(generated).doesNotContain("def __init__(self, *,");
}

@Test
public void testUnionKeywordArgsAreForced() throws IOException {
String generated = generateCode(true);
// The signature spans multiple lines with specific indentation
assertThat(generated).contains(" def __init__(\n self,\n *,");
}

@Test
public void testUnionPositionalArgsByDefault() throws IOException {
String generated = generateCode(false);

// The signature spans multiple lines with specific indentation
assertThat(generated).contains(" def __init__(\n self,\n foo: Optional[str] = None,");
assertThat(generated).doesNotContain(" def __init__(\n self,\n *,");
}

@Test
public void testServiceEndpointKeywordArgsAreForced() throws IOException {
String generated = generateCode(true);
assertThat(generated).contains("def test_endpoint(self, *, param1: str, param2: int) -> str:");
}

@Test
public void testServiceEndpointPositionalArgsByDefault() throws IOException {
String generated = generateCode(false);
assertThat(generated).contains("def test_endpoint(self, param1: str, param2: int) -> str:");
assertThat(generated).doesNotContain("def test_endpoint(self, *,");
}

@SuppressWarnings("for-rollout:deprecation")
private String generateCode(boolean forceKeywordArgs) throws IOException {
ConjurePythonGenerator generator = new ConjurePythonGenerator(GeneratorConfiguration.builder()
.packageName("test")
.packageVersion("0.0.0")
.minConjureClientVersion("2.8.0")
.generatorVersion("0.0.0")
.shouldWriteCondaRecipe(false)
.generateRawSource(false)
.forceKeywordArgs(forceKeywordArgs)
.build());

Path testFolder = Path.of("src/test/resources/force-keyword-args");
List<File> files;
try (Stream<Path> walk = Files.walk(testFolder)) {
files = walk.map(Path::toFile)
.filter(file -> file.toString().endsWith(".yml"))
.collect(Collectors.toList());
}
ConjureDefinition definition = Conjure.parse(files);

InMemoryPythonFileWriter writer = new InMemoryPythonFileWriter();
generator.write(definition, writer);

return writer.getPythonFiles().values().stream().collect(Collectors.joining("\n"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# coding=utf-8
package:
name: package-name
version: 0.0.0

source:
path: ../

build:
noarch: python
script: python setup.py install --single-version-externally-managed --record=record.txt

requirements:
build:
- python
- setuptools
- requests
- conjure-python-client >=2.8.0,<4

run:
- python
- requests
- conjure-python-client >=2.8.0,<4

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading