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