diff --git a/README.md b/README.md
index 4ea55c548..06646ff3c 100644
--- a/README.md
+++ b/README.md
@@ -16,12 +16,13 @@ The recommended way to use conjure-python is via a build tool like [gradle-conju
Usage: conjure-python generate [...options]
- --packageName package name that will appear in setup.py
- --packageVersion version number that will appear in setup.py
- --packageDescription description that will appear in setup.py
- --packageUrl url that will appear in setup.py
- --packageAuthor author that will appear in setup.py
- --writeCondaRecipe use this boolean option to generate a `conda_recipe/meta.yaml`
+ --packageName package name that will appear in setup.py or pyproject.toml
+ --packageVersion version number that will appear in setup.py or pyproject.toml
+ --packageDescription description that will appear in setup.py or pyproject.toml
+ --packageUrl url that will appear in setup.py or pyproject.toml
+ --packageAuthor author that will appear in setup.py or pyproject.toml
+ --writeCondaRecipe use this boolean option to generate a `conda_recipe/meta.yaml`
+ --writePyprojectToml use this boolean option to generate a `pyproject.toml` with Hatch instead of `setup.py` (default: false)
## Example generated objects
diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java
index a6d386d92..aef5b95b9 100644
--- a/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java
+++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java
@@ -30,6 +30,7 @@
import com.palantir.conjure.python.poet.PythonLine;
import com.palantir.conjure.python.poet.PythonMetaYaml;
import com.palantir.conjure.python.poet.PythonPackage;
+import com.palantir.conjure.python.poet.PythonPyprojectToml;
import com.palantir.conjure.python.poet.PythonSetup;
import com.palantir.conjure.python.poet.PythonSnippet;
import com.palantir.conjure.python.processors.packagename.CompoundPackageNameProcessor;
@@ -78,7 +79,11 @@ public void write(ConjureDefinition conjureDefinition, PythonFileWriter writer)
PythonPackage rootPackage = PythonPackage.of(buildPackageNameProcessor().process(""));
if (!config.generateRawSource()) {
- writer.writePythonFile(buildPythonSetupFile(rootPackage));
+ if (config.shouldWritePyprojectToml()) {
+ writer.writePythonFile(buildPyprojectTomlFile(rootPackage));
+ } else {
+ writer.writePythonFile(buildPythonSetupFile(rootPackage));
+ }
writer.writePythonFile(buildPyTypedFile());
}
if (config.shouldWriteCondaRecipe()) {
@@ -274,6 +279,26 @@ private PythonFile buildPythonSetupFile(PythonPackage rootPackage) {
.build();
}
+ private PythonFile buildPyprojectTomlFile(PythonPackage rootPackage) {
+ PythonPyprojectToml.Builder builder = PythonPyprojectToml.builder()
+ .pythonPackage(rootPackage)
+ .putOptions("name", config.packageName().get())
+ .putOptions("version", config.packageVersion().get())
+ .putOptions("requires-python", ">=3.8")
+ .addInstallDependencies("requests")
+ .addInstallDependencies(String.format(
+ "conjure-python-client>=%s,<%s",
+ config.minConjureClientVersion(), config.maxConjureClientVersion()));
+ config.packageDescription().ifPresent(value -> builder.putOptions("description", value));
+ config.packageUrl().ifPresent(url -> builder.putUrlOptions("Homepage", url));
+
+ return PythonFile.builder()
+ .pythonPackage(PythonPackage.of("."))
+ .fileName("pyproject.toml")
+ .addContents(builder.build())
+ .build();
+ }
+
/**
* Mark a package as containing type annotations.
*/
diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java
index a6ee31169..e8f261ac0 100644
--- a/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java
+++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java
@@ -16,6 +16,7 @@
package com.palantir.conjure.python;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.base.Splitter;
import com.palantir.tokens.auth.ImmutablesStyle;
import java.util.Optional;
@@ -23,6 +24,7 @@
@Value.Immutable
@ImmutablesStyle
+@JsonDeserialize(as = ImmutableGeneratorConfiguration.class)
public interface GeneratorConfiguration {
Optional packageName();
@@ -41,6 +43,8 @@ public interface GeneratorConfiguration {
boolean shouldWriteCondaRecipe();
+ boolean shouldWritePyprojectToml();
+
boolean generateRawSource();
default Optional pythonicPackageName() {
diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/PythonPyprojectToml.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/PythonPyprojectToml.java
new file mode 100644
index 000000000..ce21fc640
--- /dev/null
+++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/PythonPyprojectToml.java
@@ -0,0 +1,106 @@
+/*
+ * (c) Copyright 2018 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.ImmutableSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.immutables.value.Value;
+
+@Value.Immutable
+public interface PythonPyprojectToml extends PythonSnippet {
+
+ @Override
+ @Value.Default
+ default String idForSorting() {
+ return "pyproject";
+ }
+
+ @Override
+ @Value.Default
+ default Set imports() {
+ return ImmutableSet.of();
+ }
+
+ Map options();
+
+ Map urlOptions();
+
+ List installDependencies();
+
+ @Override
+ default void emit(PythonPoetWriter poetWriter) {
+ poetWriter.maintainingIndent(() -> {
+ // [build-system]
+ poetWriter.writeIndentedLine("[build-system]");
+ poetWriter.writeIndentedLine("requires = [\"hatchling\"]");
+ poetWriter.writeIndentedLine("build-backend = \"hatchling.build\"");
+ poetWriter.writeIndentedLine("");
+
+ // [project]
+ poetWriter.writeIndentedLine("[project]");
+
+ // Write string options
+ options().forEach((key, value) -> {
+ poetWriter.writeIndentedLine("%s = \"%s\"", key, escapeTomlString(value));
+ });
+
+ // Write dependencies
+ if (!installDependencies().isEmpty()) {
+ poetWriter.writeIndentedLine("dependencies = [");
+ poetWriter.increaseIndent();
+ installDependencies().forEach(dependency -> {
+ poetWriter.writeIndentedLine("\"%s\",", escapeTomlString(dependency));
+ });
+ poetWriter.decreaseIndent();
+ poetWriter.writeIndentedLine("]");
+ }
+
+ // Write [project.urls] section if present
+ if (!urlOptions().isEmpty()) {
+ poetWriter.writeIndentedLine("");
+ poetWriter.writeIndentedLine("[project.urls]");
+ urlOptions().forEach((key, value) -> {
+ poetWriter.writeIndentedLine("%s = \"%s\"", key, escapeTomlString(value));
+ });
+ }
+
+ // [tool.hatch.build.targets.wheel]
+ poetWriter.writeIndentedLine("");
+ poetWriter.writeIndentedLine("[tool.hatch.build.targets.wheel]");
+ poetWriter.writeIndentedLine("packages = [\".\"]");
+ });
+ }
+
+ /**
+ * Escapes special characters in TOML strings.
+ */
+ static String escapeTomlString(String value) {
+ return value.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ }
+
+ class Builder extends ImmutablePythonPyprojectToml.Builder {}
+
+ static Builder builder() {
+ return new Builder();
+ }
+}
diff --git a/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java b/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java
index bd307bf23..ee69b433e 100644
--- a/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java
+++ b/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java
@@ -18,6 +18,8 @@
import static org.assertj.core.api.Assertions.assertThat;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.palantir.conjure.defs.Conjure;
import com.palantir.conjure.spec.ConjureDefinition;
import java.io.File;
@@ -35,23 +37,19 @@
@RunWith(ConjureSubfolderRunner.class)
public final class ConjurePythonGeneratorTest {
- private final ConjurePythonGenerator generator = new ConjurePythonGenerator(GeneratorConfiguration.builder()
- .packageName("package-name")
- .packageVersion("0.0.0")
- .packageDescription("project description")
- .minConjureClientVersion("2.8.0")
- .maxConjureClientVersion("4")
- .generatorVersion("0.0.0")
- .shouldWriteCondaRecipe(true)
- .generateRawSource(false)
- .build());
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new Jdk8Module());
+
private final InMemoryPythonFileWriter pythonFileWriter = new InMemoryPythonFileWriter();
@ConjureSubfolderRunner.Test
public void assertThatFilesRenderAsExpected(Path folder) throws IOException {
+ Path configPath = folder.resolve("generator-config.json");
+ GeneratorConfiguration config = OBJECT_MAPPER.readValue(configPath.toFile(), GeneratorConfiguration.class);
+ ConjurePythonGenerator generator = new ConjurePythonGenerator(config);
+
Path expected = folder.resolve("expected");
ConjureDefinition definition = getInputDefinitions(folder);
- maybeResetExpectedDirectory(expected, definition);
+ maybeResetExpectedDirectory(expected, definition, generator);
generator.write(definition, pythonFileWriter);
assertFoldersEqual(expected);
@@ -76,7 +74,8 @@ private void assertFoldersEqual(Path expected) throws IOException {
System.out.println(count + " files checked");
}
- private void maybeResetExpectedDirectory(Path expected, ConjureDefinition definition) throws IOException {
+ private void maybeResetExpectedDirectory(
+ Path expected, ConjureDefinition definition, ConjurePythonGenerator generator) throws IOException {
if (Boolean.parseBoolean(System.getProperty("recreate", "false"))
|| !expected.toFile().isDirectory()) {
Files.createDirectories(expected);
diff --git a/conjure-python-core/src/test/resources/services/expected/conda_recipe/meta.yaml b/conjure-python-core/src/test/resources/services/expected/conda_recipe/meta.yaml
deleted file mode 100644
index 398009c87..000000000
--- a/conjure-python-core/src/test/resources/services/expected/conda_recipe/meta.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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
diff --git a/conjure-python-core/src/test/resources/services/expected/pyproject.toml b/conjure-python-core/src/test/resources/services/expected/pyproject.toml
new file mode 100644
index 000000000..025759ab8
--- /dev/null
+++ b/conjure-python-core/src/test/resources/services/expected/pyproject.toml
@@ -0,0 +1,17 @@
+# coding=utf-8
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "package-name"
+version = "0.0.0"
+requires-python = ">=3.8"
+description = "project description"
+dependencies = [
+ "requests",
+ "conjure-python-client>=2.8.0,<4",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["."]
diff --git a/conjure-python-core/src/test/resources/services/expected/setup.py b/conjure-python-core/src/test/resources/services/expected/setup.py
deleted file mode 100644
index 42ad2060a..000000000
--- a/conjure-python-core/src/test/resources/services/expected/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# coding=utf-8
-from setuptools import (
- find_packages,
- setup,
-)
-
-setup(
- name='package-name',
- version='0.0.0',
- python_requires='>=3.8',
- description='project description',
- package_data={"": ["py.typed"]},
- packages=find_packages(),
- install_requires=[
- 'requests',
- 'conjure-python-client>=2.8.0,<4',
- ],
-)
diff --git a/conjure-python-core/src/test/resources/services/generator-config.json b/conjure-python-core/src/test/resources/services/generator-config.json
new file mode 100644
index 000000000..d7e59f2a2
--- /dev/null
+++ b/conjure-python-core/src/test/resources/services/generator-config.json
@@ -0,0 +1,11 @@
+{
+ "packageName": "package-name",
+ "packageVersion": "0.0.0",
+ "packageDescription": "project description",
+ "minConjureClientVersion": "2.8.0",
+ "maxConjureClientVersion": "4",
+ "generatorVersion": "0.0.0",
+ "shouldWriteCondaRecipe": false,
+ "shouldWritePyprojectToml": true,
+ "generateRawSource": false
+}
diff --git a/conjure-python-core/src/test/resources/types/generator-config.json b/conjure-python-core/src/test/resources/types/generator-config.json
new file mode 100644
index 000000000..4d5ba83e8
--- /dev/null
+++ b/conjure-python-core/src/test/resources/types/generator-config.json
@@ -0,0 +1,11 @@
+{
+ "packageName": "package-name",
+ "packageVersion": "0.0.0",
+ "packageDescription": "project description",
+ "minConjureClientVersion": "2.8.0",
+ "maxConjureClientVersion": "4",
+ "generatorVersion": "0.0.0",
+ "shouldWriteCondaRecipe": true,
+ "shouldWritePyprojectToml": false,
+ "generateRawSource": false
+}
diff --git a/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java b/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java
index ad0294b27..500d4a4cf 100644
--- a/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java
+++ b/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java
@@ -51,6 +51,12 @@ boolean shouldWriteCondaRecipe() {
return false;
}
+ @Value.Default
+ @SuppressWarnings("DesignForExtension")
+ boolean shouldWritePyprojectToml() {
+ return false;
+ }
+
@Value.Check
final void check() {
Preconditions.checkArgument(input().isFile(), "Target must exist and be a file");
diff --git a/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java b/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java
index f46d25921..4c5b7aecd 100644
--- a/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java
+++ b/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java
@@ -89,6 +89,12 @@ public static final class GenerateCommand implements Runnable {
description = "Generate a `conda_recipe/meta.yaml`")
private boolean writeCondaRecipe;
+ @CommandLine.Option(
+ names = "--writePyprojectToml",
+ defaultValue = "false",
+ description = "Generate a `pyproject.toml` file using Hatch instead of `setup.py`")
+ private boolean writePyprojectToml;
+
@CommandLine.Unmatched
@SuppressWarnings("StrictUnusedVariable")
private List unmatchedOptions;
@@ -118,6 +124,7 @@ CliConfiguration getConfiguration() {
.packageUrl(Optional.ofNullable(packageUrl))
.generateRawSource(rawSource)
.shouldWriteCondaRecipe(writeCondaRecipe)
+ .shouldWritePyprojectToml(writePyprojectToml)
.build();
}
@@ -133,6 +140,7 @@ static GeneratorConfiguration resolveGeneratorConfiguration(
.packageVersion(cliConfig.packageVersion())
.packageUrl(cliConfig.packageUrl())
.shouldWriteCondaRecipe(cliConfig.shouldWriteCondaRecipe())
+ .shouldWritePyprojectToml(cliConfig.shouldWritePyprojectToml())
.generateRawSource(cliConfig.generateRawSource())
.build();
}