From ef79cbd82950f59265e3832ea3368db8ebcbd1f3 Mon Sep 17 00:00:00 2001 From: Akshita Date: Thu, 19 Sep 2019 14:21:32 -0700 Subject: [PATCH 1/3] Add writefile magic command --- .../MagicCommandTests.cs | 65 ++++++++++ .../KernelExtensions.cs | 114 +++++++++++++++--- .../WriteFileOptions.cs | 18 +++ 3 files changed, 178 insertions(+), 19 deletions(-) create mode 100644 Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs index 766c0115c..782f95a00 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs @@ -3,6 +3,7 @@ using System; using System.CommandLine; +using System.IO; using System.Linq; using System.Threading.Tasks; using FluentAssertions; @@ -220,5 +221,69 @@ await kernel.SendAsync(new SubmitCode( .Should() .Contain(v => v.Value.Equals("done!")); } + + [Fact] + public async Task WriteFile_magic_command_writes_the_code_in_a_file() + { + var kernel = new CompositeKernel + { + new CSharpKernel().UseKernelHelpers() + } + .UseDefaultMagicCommands() + .LogEventsToPocketLogger(); + + using var events = kernel.KernelEvents.ToSubscribedList(); + + var file = Path.GetTempFileName(); + var text = "This text must be written to a file"; + await kernel.SendAsync(new SubmitCode( + $@" +%%writefile {file} +{text}")); + + File.ReadAllText(file).Should().Be(text); + + events.Should() + .ContainSingle(e => e is DisplayedValueProduced) + .Which + .As() + .FormattedValues + .Should() + .ContainSingle(v => + v.MimeType == "text/plain" && + v.Value.ToString().StartsWith($"Written text to file {file}")); + + } + + [Fact] + public async Task WriteFile_magic_command_shows_error_if_filename_is_invalid() + { + var kernel = new CompositeKernel + { + new CSharpKernel().UseKernelHelpers() + } + .UseDefaultMagicCommands() + .LogEventsToPocketLogger(); + + using var events = kernel.KernelEvents.ToSubscribedList(); + + var file = "INVALID_PATH"; + var text = "This text must be not be written to a file"; + await kernel.SendAsync(new SubmitCode( + $@" +%%writefile {file} +{text}")); + + events.Should() + .ContainSingle(e => e is DisplayedValueProduced) + .Which + .As() + .FormattedValues + .Should() + .ContainSingle(v => + v.MimeType == "text/plain" && + v.Value.ToString().StartsWith($"Could not write to file")); + + } } } diff --git a/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs b/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs index bb1fb4371..e70eaebf6 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.CommandLine; using System.CommandLine.Invocation; using System.Diagnostics; @@ -14,6 +15,8 @@ using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Extensions; using Microsoft.DotNet.Interactive.Rendering; +using MLS.Agent.Tools; +using MLS.Agent.Tools.Roslyn; using static Microsoft.DotNet.Interactive.Rendering.PocketViewTags; namespace Microsoft.DotNet.Interactive.Jupyter @@ -27,7 +30,8 @@ public static T UseDefaultMagicCommands(this T kernel) .UseHtml() .UseJavaScript() .UseMarkdown() - .UseTime(); + .UseTime() + .UseWriteFile(); return kernel; } @@ -37,25 +41,25 @@ private static T UseHtml(this T kernel) { kernel.AddDirective(new Command("%%html") { - Handler = CommandHandler.Create((KernelInvocationContext context) => - { - if (context.Command is SubmitCode submitCode) - { - var htmlContent = submitCode.Code - .Replace("%%html", "") - .Trim(); - - context.Publish(new DisplayedValueProduced( - htmlContent, - context.Command, - formattedValues: new[] - { + Handler = CommandHandler.Create((KernelInvocationContext context) => + { + if (context.Command is SubmitCode submitCode) + { + var htmlContent = submitCode.Code + .Replace("%%html", "") + .Trim(); + + context.Publish(new DisplayedValueProduced( + htmlContent, + context.Command, + formattedValues: new[] + { new FormattedValue("text/html", htmlContent) - })); + })); - context.Complete(); - } - }) + context.Complete(); + } + }) }); return kernel; @@ -126,6 +130,14 @@ private static T UseTime(this T kernel) return kernel; } + private static T UseWriteFile(this T kernel) + where T : KernelBase + { + kernel.AddDirective(writefile()); + + return kernel; + } + private static T UseLsMagic(this T kernel) where T : KernelBase { @@ -236,7 +248,7 @@ await context.CurrentKernel.SendAsync( context.Publish( new DisplayedValueProduced( - elapsed, + elapsed, context.Command, new[] { @@ -248,5 +260,69 @@ await context.CurrentKernel.SendAsync( }) }; } + + private static Command writefile() + { + var command = new Command("%%writefile") + { + Handler = CommandHandler.Create(async (KernelInvocationContext context, WriteFileOptions options) => + { + if (context.Command is SubmitCode submitCode) + { + var code = submitCode.Code + .Replace("%%writefile", "") + .Replace(options.FileName.FullName, "") + .Trim(); + try + { + if (!options.FileName.Exists) + { + options.FileName.Create(); + } + + File.WriteAllText(options.FileName.FullName, code); + var formattableString = $"Written text to file {options.FileName.FullName}"; + await context.HandlingKernel.SendAsync(new DisplayValue(formattableString, new FormattedValue(PlainTextFormatter.MimeType, formattableString))); + } + catch (Exception e) + { + var formattableString = $"Could not write to file {options.FileName.FullName} due to exception {e.Message}"; + await context.HandlingKernel.SendAsync(new DisplayValue(formattableString, new FormattedValue(PlainTextFormatter.MimeType, formattableString))); + } + + context.Complete(); + } + }) + }; + + var fileArgument = new Argument() + { + Name = nameof(WriteFileOptions.FileName), + Description = "Specify the file path to write to" + }; + + fileArgument.AddValidator(symbolResult => + { + var file = symbolResult.Tokens + .Select(t => t.Value) + .FirstOrDefault(); + + if (!PathUtilities.IsAbsolute(file)) + { + return "Absolute file path expected"; + } + + if (!PathUtilities.IsValidFilePath(file)) + { + return "Invalid file path"; + } + + return null; + }); + + command.AddArgument(fileArgument); + + return command; + } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs b/Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs new file mode 100644 index 000000000..4c6a75ce3 --- /dev/null +++ b/Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.Interactive.Jupyter +{ + internal class WriteFileOptions + { + public WriteFileOptions(FileInfo fileName) + { + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + } + + public FileInfo FileName { get; set; } + } +} \ No newline at end of file From 8ce6b8a6634c762165942f9429de91d95c13b566 Mon Sep 17 00:00:00 2001 From: Akshita Date: Thu, 19 Sep 2019 17:02:26 -0700 Subject: [PATCH 2/3] Skip the fact due to blocking issue --- Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs index 782f95a00..d6d144d80 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs @@ -255,7 +255,7 @@ await kernel.SendAsync(new SubmitCode( } - [Fact] + [Fact(Skip = "Needs https://github.com/dotnet/try/issues/469 to be resolved ")] public async Task WriteFile_magic_command_shows_error_if_filename_is_invalid() { var kernel = new CompositeKernel From 14be7384c2e036fb424e07b0d07ac2bcf6f83510 Mon Sep 17 00:00:00 2001 From: Akshita Date: Thu, 19 Sep 2019 17:55:30 -0700 Subject: [PATCH 3/3] Add append functionality --- MLS.Agent.Tools/RelativePath.cs | 5 ++ .../MagicCommandTests.cs | 46 +++++++++++++++++-- .../KernelExtensions.cs | 28 +++++++---- .../WriteFileOptions.cs | 4 +- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/MLS.Agent.Tools/RelativePath.cs b/MLS.Agent.Tools/RelativePath.cs index 239fee6e6..1b6533fbe 100644 --- a/MLS.Agent.Tools/RelativePath.cs +++ b/MLS.Agent.Tools/RelativePath.cs @@ -189,4 +189,9 @@ protected static void ThrowIfContainsDisallowedFilePathChars(string filename) // return !Equals(left, right); // } } + + public static class ArrayExtensions + { + + } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs index d6d144d80..728ae8f3a 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs @@ -168,7 +168,7 @@ public async Task markdown_renders_markdown_content_as_html() await kernel.SendAsync(new SubmitCode( $"%%markdown\n\n# Topic!\nContent")); - + var formatted = events @@ -225,12 +225,12 @@ await kernel.SendAsync(new SubmitCode( [Fact] public async Task WriteFile_magic_command_writes_the_code_in_a_file() { - var kernel = new CompositeKernel + var kernel = new CompositeKernel { new CSharpKernel().UseKernelHelpers() } - .UseDefaultMagicCommands() - .LogEventsToPocketLogger(); + .UseDefaultMagicCommands() + .LogEventsToPocketLogger(); using var events = kernel.KernelEvents.ToSubscribedList(); @@ -251,7 +251,43 @@ await kernel.SendAsync(new SubmitCode( .Should() .ContainSingle(v => v.MimeType == "text/plain" && - v.Value.ToString().StartsWith($"Written text to file {file}")); + v.Value.ToString().StartsWith($"Overwriting text to file {file}")); + + } + + [Fact] + public async Task WriteFile_magic_command_appends_the_code_to_file_when_option_is_set() + { + var kernel = new CompositeKernel + { + new CSharpKernel().UseKernelHelpers() + } + .UseDefaultMagicCommands() + .LogEventsToPocketLogger(); + + using var events = kernel.KernelEvents.ToSubscribedList(); + + var file = Path.GetTempFileName(); + File.Create(file).Dispose(); + var existingText = "This text is already there"; + File.WriteAllText(file, existingText); + + var appendedText = "This text must be written to a file"; + await kernel.SendAsync(new SubmitCode( +$@"%%writefile -a {file} +{appendedText}")); + + File.ReadAllText(file).Should().Be(existingText + "\n" + appendedText); + + events.Should() + .ContainSingle(e => e is DisplayedValueProduced) + .Which + .As() + .FormattedValues + .Should() + .ContainSingle(v => + v.MimeType == "text/plain" && + v.Value.ToString().StartsWith($"Appending text to file {file}")); } diff --git a/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs b/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs index e70eaebf6..fcc2e5ce5 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; using System.Diagnostics; @@ -269,20 +270,27 @@ private static Command writefile() { if (context.Command is SubmitCode submitCode) { - var code = submitCode.Code - .Replace("%%writefile", "") - .Replace(options.FileName.FullName, "") - .Trim(); + var code = string.Join("\n", submitCode.Code.Split('\n', '\r').Where(line => !line.Contains("%%writefile"))); + try { if (!options.FileName.Exists) { - options.FileName.Create(); + options.FileName.Create().Dispose(); } - File.WriteAllText(options.FileName.FullName, code); - var formattableString = $"Written text to file {options.FileName.FullName}"; - await context.HandlingKernel.SendAsync(new DisplayValue(formattableString, new FormattedValue(PlainTextFormatter.MimeType, formattableString))); + string message; + if (options.Append) + { + File.AppendAllText(options.FileName.FullName, code); + message = $"Appending text to file {options.FileName.FullName}"; + } + else + { + File.WriteAllText(options.FileName.FullName, code); + message = $"Overwriting text to file {options.FileName.FullName}"; + } + await context.HandlingKernel.SendAsync(new DisplayValue(message, new FormattedValue(PlainTextFormatter.MimeType, message))); } catch (Exception e) { @@ -321,6 +329,10 @@ private static Command writefile() }); command.AddArgument(fileArgument); + command.AddOption(new Option(new string[] { "-a", "--append" }, "Append to file") + { + Argument = new Argument(defaultValue: () => false) + }); return command; } diff --git a/Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs b/Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs index 4c6a75ce3..1f71181fd 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/WriteFileOptions.cs @@ -8,11 +8,13 @@ namespace Microsoft.DotNet.Interactive.Jupyter { internal class WriteFileOptions { - public WriteFileOptions(FileInfo fileName) + public WriteFileOptions(FileInfo fileName, bool a) { FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Append = a; } public FileInfo FileName { get; set; } + public bool Append { get; } } } \ No newline at end of file