fix(python): fan out only on JSON-array output, not any IEnumerable
The old code iterated every IEnumerable, so `output = "hello"` emitted one
item per character (a string is IEnumerable-of-char) and `output = {"a":1}`
emitted the keys as items. Now the script's output is serialized first and
the ValueKind decides: JSON array → per-element items; anything else →
single item. `output = None` remains an empty emission.
New tests cover string, dict, number, and None shapes.
This commit is contained in:
@@ -59,12 +59,19 @@ public sealed class PythonTransformNode : IActionNode
|
||||
string.IsNullOrEmpty(formatted) ? ex.Message : formatted);
|
||||
}
|
||||
|
||||
if (!scope.TryGetVariable("output", out object? outputObj))
|
||||
if (!scope.TryGetVariable("output", out object? outputObj) || outputObj is null)
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<Item>>>(new[] { Array.Empty<Item>() });
|
||||
|
||||
// Fan out only when `output` serializes to a JSON array. A naive
|
||||
// `IEnumerable` check would treat a bare string as an iterable of chars
|
||||
// (`output = "hello"` → 5 items) and a bare dict as an iterable of keys.
|
||||
// Serializing first and dispatching on ValueKind gives one predictable rule.
|
||||
var element = JsonToPython.ToJson(outputObj);
|
||||
var outItems = new List<Item>();
|
||||
if (outputObj is System.Collections.IEnumerable ie)
|
||||
foreach (var v in ie) outItems.Add(new Item(JsonToPython.ToJson(v)));
|
||||
if (element.ValueKind == JsonValueKind.Array)
|
||||
foreach (var el in element.EnumerateArray()) outItems.Add(new Item(el.Clone()));
|
||||
else
|
||||
outItems.Add(new Item(element));
|
||||
|
||||
return Task.FromResult<IReadOnlyList<IReadOnlyList<Item>>>(new[] { outItems });
|
||||
}
|
||||
|
||||
@@ -73,6 +73,51 @@ public class PythonTransformNodeTests
|
||||
Assert.Contains("line 2", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bare_string_output_is_a_single_item_not_one_char_per_item()
|
||||
{
|
||||
var node = new PythonTransformNode();
|
||||
var outs = await node.ExecuteAsync(Ctx("output = 'hello'"),
|
||||
new IReadOnlyList<Item>[] { Array.Empty<Item>() }, CancellationToken.None);
|
||||
Assert.Single(outs);
|
||||
Assert.Single(outs[0]);
|
||||
Assert.Equal("hello", outs[0][0].Json.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bare_dict_output_is_a_single_item_not_one_key_per_item()
|
||||
{
|
||||
var node = new PythonTransformNode();
|
||||
var outs = await node.ExecuteAsync(Ctx("output = {'test': 'hi', 'n': 3}"),
|
||||
new IReadOnlyList<Item>[] { Array.Empty<Item>() }, CancellationToken.None);
|
||||
Assert.Single(outs);
|
||||
Assert.Single(outs[0]);
|
||||
Assert.Equal(JsonValueKind.Object, outs[0][0].Json.ValueKind);
|
||||
Assert.Equal("hi", outs[0][0].Json.GetProperty("test").GetString());
|
||||
Assert.Equal(3, outs[0][0].Json.GetProperty("n").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bare_number_output_is_a_single_item()
|
||||
{
|
||||
var node = new PythonTransformNode();
|
||||
var outs = await node.ExecuteAsync(Ctx("output = 42"),
|
||||
new IReadOnlyList<Item>[] { Array.Empty<Item>() }, CancellationToken.None);
|
||||
Assert.Single(outs);
|
||||
Assert.Single(outs[0]);
|
||||
Assert.Equal(42, outs[0][0].Json.GetInt64());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task None_output_yields_empty_list()
|
||||
{
|
||||
var node = new PythonTransformNode();
|
||||
var outs = await node.ExecuteAsync(Ctx("output = None"),
|
||||
new IReadOnlyList<Item>[] { Array.Empty<Item>() }, CancellationToken.None);
|
||||
Assert.Single(outs);
|
||||
Assert.Empty(outs[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Syntax_error_message_contains_python_traceback()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user