Skip to content

Commit

Permalink
Basic command line parameterization for notebook automation (#70)
Browse files Browse the repository at this point in the history
* --input support, basic describe command for parameter discovery
  • Loading branch information
jonsequitur authored Sep 22, 2022
1 parent 7b1af76 commit 1e2d65a
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 62 deletions.
49 changes: 43 additions & 6 deletions src/dotnet-repl.Tests/Automation/NotebookRunnerTests.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
using System;
using System.Collections.Generic;
using System.CommandLine.IO;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Assent;
using Automation;
using dotnet_repl.Tests.Utility;
using FluentAssertions;
using Microsoft.DotNet.Interactive.CSharp;
using Microsoft.DotNet.Interactive.Documents;
using Microsoft.DotNet.Interactive.Documents.Jupyter;
using Microsoft.DotNet.Interactive.Formatting;
using Microsoft.DotNet.Interactive.Events;
using Pocket;
using Spectre.Console;
using TRexLib;
using Xunit;

namespace dotnet_repl.Tests.Automation;
Expand Down Expand Up @@ -82,17 +80,56 @@ public async Task Notebook_runner_produces_expected_output()

var expectedContent = await File.ReadAllTextAsync(notebookFile);

var inputDoc = Notebook.Parse(expectedContent, new(kernel.ChildKernels.Select(k => new KernelName(k.Name)).ToArray()));
var inputDoc = Notebook.Parse(expectedContent, kernel.CreateKernelInfos());

var resultDoc = await runner.RunNotebookAsync(inputDoc);

NormalizeMetadata(resultDoc);

var resultContent = resultDoc.Serialize();
var resultContent = resultDoc.SerializeToJupyter();

this.Assent(resultContent, _assentConfiguration);
}

[Fact]
public async Task Parameters_can_be_passed_to_input_fields_declared_in_the_notebook()
{
var dibContent = @"
#!value --name abc --from-value @input:""abc""
#!csharp
#!share --from value abc
abc.Display();
";
var inputs = new Dictionary<string, string>
{
["abc"] = "hello!"
};

using var kernel = KernelBuilder.CreateKernel(new StartupOptions
{
ExitAfterRun = true,
Inputs = inputs
});

var inputDoc = CodeSubmission.Parse(dibContent, kernel.CreateKernelInfos());

var runner = new NotebookRunner(kernel);

var events = kernel.KernelEvents.ToSubscribedList();

await runner.RunNotebookAsync(inputDoc, inputs);

events.Should().NotContainErrors();

events.Should()
.ContainSingle<DisplayedValueProduced>()
.Which
.Value
.Should()
.Be("hello!");
}

private void NormalizeMetadata(InteractiveDocument document)
{
foreach (var element in document.Elements)
Expand Down
3 changes: 3 additions & 0 deletions src/dotnet-repl.Tests/dotnet-repl.Tests.v3.ncrunchproject
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<NamedTestSelector>
<TestName>dotnet_repl.Tests.Automation.NotebookAutomationTests.Notebook_runner_produces_expected_output</TestName>
</NamedTestSelector>
<NamedTestSelector>
<TestName>dotnet_repl.Tests.Automation.NotebookRunnerTests.Notebook_runner_produces_expected_output</TestName>
</NamedTestSelector>
</IgnoredTests>
</Settings>
</ProjectConfiguration>
30 changes: 26 additions & 4 deletions src/dotnet-repl/Automation/NotebookRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,50 @@ namespace Automation;

public class NotebookRunner
{
private readonly Kernel _kernel;
private readonly CompositeKernel _kernel;

public NotebookRunner(Kernel kernel)
public NotebookRunner(CompositeKernel kernel)
{
_kernel = kernel;
}

public async Task<InteractiveDocument> RunNotebookAsync(
InteractiveDocument notebook,
IDictionary<string, string>? parameters = null,
CancellationToken cancellationToken = default)
{
var documentElements = new List<InteractiveDocumentElement>();

if (parameters is not null)
{
var inputKernel = _kernel.ChildKernels.OfType<InputKernel>().FirstOrDefault();

if (inputKernel is not null)
{
inputKernel.GetInputValueAsync = key =>
{
if (parameters.TryGetValue(key, out var value))
{
return Task.FromResult<string?>(value);
}
else
{
return Task.FromResult<string?>(null);
}
};
}
}

foreach (var element in notebook.Elements)
{
var command = new SubmitCode(element.Contents, element.Language);
var command = new SubmitCode(element.Contents, element.KernelName);

var events = _kernel.KernelEvents.Replay();

using var connect = events.Connect();

var startTime = DateTimeOffset.Now;

var result = _kernel.SendAsync(command, cancellationToken);

var tcs = new TaskCompletionSource();
Expand Down Expand Up @@ -126,7 +148,7 @@ public async Task<InteractiveDocument> RunNotebookAsync(

await tcs.Task;

var resultElement = new InteractiveDocumentElement(element.Contents, element.Language, outputs.ToArray());
var resultElement = new InteractiveDocumentElement(element.Contents, element.KernelName, outputs.ToArray());
resultElement.Metadata ??= new Dictionary<string, object>();
resultElement.Metadata.Add("dotnet_repl_cellExecutionStartTime", startTime);
resultElement.Metadata.Add("dotnet_repl_cellExecutionEndTime", DateTimeOffset.Now);
Expand Down
118 changes: 108 additions & 10 deletions src/dotnet-repl/CommandLineParser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
Expand Down Expand Up @@ -31,6 +32,8 @@ public static class CommandLineParser
"csharp",
"fsharp",
"pwsh",
"javascript",
"html",
"sql");

public static Option<FileInfo> NotebookOption = new Option<FileInfo>(
Expand All @@ -51,10 +54,31 @@ public static class CommandLineParser
"Working directory to which to change after launching the kernel")
.ExistingOnly();

public static Option<FileInfo> OutputPathOption = new(
"--output-path",
public static Option<IDictionary<string, string>> InputsOption = new(
"--input",
description:
"Run the file specified by --notebook and writes the output to the file specified by --output-path");
"Specifies in a value for @input tokens in magic commands in the notebook, using the format --input <key>=<value>",
parseArgument: result =>
{
var dict = new Dictionary<string, string>();

foreach (var token in result.Tokens.Select(t => t.Value))
{
var keyAndValue = token.Split("=");
dict[keyAndValue[0]] = keyAndValue[1];
}

return dict;
})
{
Arity = ArgumentArity.ZeroOrMore
};

public static Option<FileInfo> OutputPathOption = new Option<FileInfo>(
"--output-path",
description:
"Run the file specified by --notebook and writes the output to the file specified by --output-path")
.LegalFilePathsOnly();

public static Option<OutputFormat> OutputFormatOption = new(
"--output-format",
Expand All @@ -74,7 +98,10 @@ public static Parser Create(
WorkingDirOption,
ExitAfterRunOption,
OutputFormatOption,
OutputPathOption
OutputPathOption,
InputsOption,
ConvertCommand(),
DescribeCommand(),
};

startRepl ??= StartAsync;
Expand All @@ -92,16 +119,86 @@ public static Parser Create(
LogPathOption,
ExitAfterRunOption,
OutputFormatOption,
OutputPathOption),
OutputPathOption,
InputsOption),
Bind.FromServiceProvider<InvocationContext>());

return new CommandLineBuilder(rootCommand)
.UseDefaults()
.UseHelpBuilder(_ => new SpectreHelpBuilder(LocalizationResources.Instance))
.Build();

Command ConvertCommand()
{
// FIX: (ConvertCommand)

var notebookOption = new Option<FileInfo>("--notebook", "The notebook file to convert")
.ExistingOnly();

var outputPathOption = new Option<FileInfo>("--output-path")
.LegalFilePathsOnly();

var outputFormatOption = new Option<OutputFormat>(
"--output-format",
description: $"The output format to be used when running a notebook with the {NotebookOption.Aliases.First()} and {ExitAfterRunOption.Aliases.First()} options",
getDefaultValue: () => OutputFormat.ipynb);

var command = new Command("convert")
{
notebookOption,
outputPathOption,
outputFormatOption
};

return command;
}

Command DescribeCommand()
{
// FIX: (DescribeCommand)

var notebookArgument = new Argument<FileInfo>("notebook")
.ExistingOnly();

var command = new Command("describe")
{
notebookArgument
};

command.SetHandler(async context =>
{
var doc = await DocumentParser.LoadInteractiveDocumentAsync(
context.ParseResult.GetValueForArgument(notebookArgument),
KernelBuilder.CreateKernel());

var console = ansiConsole ?? AnsiConsole.Console;

var inputFields = doc.GetInputFields();

if (inputFields.Any())
{
console.WriteLine("Parameters", Theme.Default.AnnouncementTextStyle);

var table = new Table();
table.BorderStyle = Theme.Default.AnnouncementBorderStyle;
table.AddColumn(new TableColumn("Name"));
table.AddColumn(new TableColumn("Type"));
table.AddColumn(new TableColumn("Example"));

foreach (var inputField in inputFields)
{
table.AddRow(inputField.Prompt, inputField.TypeHint, $"--input {inputField.Prompt}=\"parameter value\"");
}

console.Write(table);
}
});

return command;
}
}

public static async Task<IDisposable> StartAsync(
private static async Task<IDisposable> StartAsync(
StartupOptions options,
IAnsiConsole ansiConsole,
InvocationContext context)
Expand All @@ -122,10 +219,10 @@ public static async Task<IDisposable> StartAsync(

if (options.Notebook is { } file)
{
notebook = await DocumentParser.ReadFileAsInteractiveDocument(file, kernel);
notebook = await DocumentParser.LoadInteractiveDocumentAsync(file, kernel);
}

if (notebook is { } && notebook.Elements.Any())
if (notebook is { Elements.Count: > 0 })
{
if (isTerminal)
{
Expand Down Expand Up @@ -162,13 +259,14 @@ await repl.RunAsync(
var resultNotebook = await new NotebookRunner(kernel)
.RunNotebookAsync(
notebook,
context.GetCancellationToken());
options.Inputs,
cancellationToken: context.GetCancellationToken());

switch (options.OutputFormat)
{
case OutputFormat.ipynb:
{
var outputNotebook = resultNotebook.Serialize();
var outputNotebook = resultNotebook.SerializeToJupyter();
if (options.OutputPath is null)
{
ansiConsole.Write(outputNotebook);
Expand Down
Loading

0 comments on commit 1e2d65a

Please sign in to comment.