Skip to content

Commit 23b12c8

Browse files
ctruedenclaude
andcommitted
Write inline pyproject.toml with table headers
Jackson's TomlMapper writes nested tables as dotted keys, e.g. tool.pixi.workspace.channels = [...]. That is valid TOML, but pixi 0.58 (the version Appose pins) does not then recognize the file as a pixi manifest, failing with "Missing table in manifest pyproject.toml". Newer pixi versions accept either form. So we now emit an explicit [header] per table, which all pixi versions understand, still letting TomlMapper serialize the leaf values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e65dad5 commit 23b12c8

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

‎src/main/java/org/scijava/plugins/scripting/appose/python/_internal/InlineMetadata.java‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.regex.Matcher;
4141
import java.util.regex.Pattern;
4242

43+
import com.fasterxml.jackson.core.io.JsonStringEncoder;
4344
import com.fasterxml.jackson.databind.JsonNode;
4445
import com.fasterxml.jackson.databind.node.ObjectNode;
4546
import com.fasterxml.jackson.dataformat.toml.TomlMapper;
@@ -175,7 +176,9 @@ else if ("tool".equals(key)) {
175176
}
176177

177178
try {
178-
return mapper.writeValueAsString(pyproject);
179+
final StringBuilder sb = new StringBuilder();
180+
writeTable(mapper, pyproject, null, sb);
181+
return sb.toString();
179182
}
180183
catch (final IOException e) {
181184
throw new IllegalArgumentException(
@@ -249,6 +252,40 @@ private static void unsupported(final JsonNode table, final String prefix,
249252
});
250253
}
251254

255+
/**
256+
* Writes the given table as TOML, using a {@code [header]} per table rather
257+
* than the dotted keys that {@link TomlMapper} would produce, since some
258+
* pixi versions (e.g. 0.58) do not recognize {@code tool.pixi.workspace}
259+
* written in dotted form.
260+
*/
261+
private static void writeTable(final TomlMapper mapper, final JsonNode table,
262+
final String path, final StringBuilder sb) throws IOException
263+
{
264+
final List<Map.Entry<String, JsonNode>> subtables = new ArrayList<>();
265+
final ObjectNode values = mapper.createObjectNode();
266+
table.fields().forEachRemaining(field -> {
267+
if (field.getValue().isObject()) subtables.add(field);
268+
else values.set(field.getKey(), field.getValue());
269+
});
270+
if (path != null && (!values.isEmpty() || subtables.isEmpty())) {
271+
if (sb.length() > 0) sb.append('\n');
272+
sb.append('[').append(path).append("]\n");
273+
}
274+
if (!values.isEmpty()) sb.append(mapper.writeValueAsString(values));
275+
for (final Map.Entry<String, JsonNode> subtable : subtables) {
276+
final String key = tomlKey(subtable.getKey());
277+
writeTable(mapper, subtable.getValue(),
278+
path == null ? key : path + "." + key, sb);
279+
}
280+
}
281+
282+
/** Quotes the given key if it is not a valid TOML bare key. */
283+
private static String tomlKey(final String key) {
284+
if (key.matches("[A-Za-z0-9_-]+")) return key;
285+
return '"' + new String(JsonStringEncoder.getInstance().quoteAsString(
286+
key)) + '"';
287+
}
288+
252289
/** Removes the comment prefix from each line, as the specification says. */
253290
private static String uncomment(final String content) {
254291
final StringBuilder sb = new StringBuilder();

‎src/test/java/org/scijava/plugins/scripting/appose/python/_internal/InlineMetadataTest.java‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
import static org.junit.Assert.assertThrows;
3737
import static org.junit.Assert.assertTrue;
3838

39+
import java.util.ArrayList;
40+
import java.util.Arrays;
41+
import java.util.List;
42+
3943
import com.fasterxml.jackson.databind.JsonNode;
4044
import com.fasterxml.jackson.dataformat.toml.TomlMapper;
4145

@@ -125,6 +129,26 @@ public void testToPyProject() throws Exception {
125129
pixi.get("workspace").get("platforms").get(0).asText());
126130
}
127131

132+
@Test
133+
public void testToPyProjectUsesTableHeaders() {
134+
// Older pixi versions (e.g. 0.58) do not recognize a pixi manifest
135+
// written with dotted keys, such as tool.pixi.workspace.channels.
136+
final String toml = InlineMetadata.toPyProject(String.join("\n", //
137+
"[tool.pixi.dependencies]", //
138+
"numpy = \"*\"", //
139+
"[tool.pixi.pypi-dependencies.\"my.pkg\"]", //
140+
"path = \".\"", //
141+
""), "my-env");
142+
final List<String> headers = new ArrayList<>();
143+
for (final String line : toml.split("\n")) {
144+
if (line.startsWith("[")) headers.add(line);
145+
else if (!line.isEmpty()) assertFalse(line, line.split(" = ")[0].contains("."));
146+
}
147+
assertEquals(Arrays.asList("[project]", "[tool.pixi.dependencies]",
148+
"[tool.pixi.pypi-dependencies.\"my.pkg\"]", "[tool.pixi.workspace]"),
149+
headers);
150+
}
151+
128152
@Test
129153
public void testToPyProjectKeepsDeclaredWorkspace() throws Exception {
130154
final JsonNode pyproject = pyproject(String.join("\n", //

0 commit comments

Comments
 (0)