Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions docs/extending-dotnet-interactive.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,20 @@ public class ClockKernelExtension : IKernelExtension
{
// ...

var hourOption = new Option<int>(new[] { "-o", "--hour" },
"The position of the hour hand");
var minuteOption = new Option<int>(new[] { "-m", "--minute" },
"The position of the minute hand");
var secondOption = new Option<int>(new[] { "-s", "--second" },
"The position of the second hand");

var clockCommand = new Command("#!clock", "Displays a clock showing the current or specified time.")
{
new Option<int>(new[]{"-o","--hour"},
"The position of the hour hand"),
new Option<int>(new[]{"-m","--minute"},
"The position of the minute hand"),
new Option<int>(new[]{"-s","--second"},
"The position of the second hand")
hourOption,
minuteOption,
secondOption
};

//...

kernel.AddDirective(clockCommand);
Expand Down Expand Up @@ -80,10 +84,12 @@ The `Formatter` API can be used to customize the output for a given .NET type (`

## Script-based extensions

Extensions can also be script-based. This enables a NuGet package to not have any direct dependency on the
As an alternative to implementing `IKernelExtension`, extensions can also be script-based. This enables a NuGet package to not have any direct dependency on the
`Microsoft.DotNet.Interactive` libraries or tools which means that any other projects that normally reference that
NuGet package also will not have a dependency on .NET Interactive.
NuGet package also will not have a dependency on .NET Interactive.

NuGet packages that are loaded via `#r "nuget:..."` are probed for a well-known file, `interactive-extensions/dotnet/extension.dib`.
If that file is found, then it is read in its entirety and executed and that code can add new commands, register
formatters, etc. See the [RandomNumber](../samples/extensions/RandomNumber/README.md) extension to see this in action.
If that file is found, then it is read in its entirety and executed. The script can add new commands, register
formatters, load packages, and run code in multiple languages using kernel chooser magic commands (e.g. `#!csharp`, `#!fsharp`, `#!pwsh`, `#!javascript`, and so on).

See the [RandomNumber](../samples/extensions/RandomNumber/README.md) extension to see this in action.
396 changes: 373 additions & 23 deletions samples/extensions/ClockExtension.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions samples/extensions/ClockExtension/ClockExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="microsoft.dotnet.interactive" Version="1.0.0-beta.21357.1" />
<PackageReference Include="microsoft.dotnet.interactive.csharp" Version="1.0.0-beta.21357.1" />
<PackageReference Include="microsoft.dotnet.interactive" Version="1.0.0-beta.22256.1" />
<PackageReference Include="microsoft.dotnet.interactive.csharp" Version="1.0.0-beta.22256.1" />
</ItemGroup>

<ItemGroup>
Expand Down
94 changes: 48 additions & 46 deletions samples/extensions/ClockExtension/ClockKernelExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,69 @@

using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading.Tasks;

using Microsoft.DotNet.Interactive;
using Microsoft.DotNet.Interactive.Formatting;

using static Microsoft.DotNet.Interactive.Formatting.PocketViewTags;

namespace ClockExtension
namespace ClockExtension;

public class ClockKernelExtension : IKernelExtension
{
public class ClockKernelExtension : IKernelExtension
public Task OnLoadAsync(Kernel kernel)
{
public Task OnLoadAsync(Kernel kernel)
Formatter.Register<DateTime>((date, writer) =>
{
Formatter.Register<DateTime>((date, writer) =>
{
writer.Write(date.DrawSvgClock());
}, "text/html");

Formatter.Register<DateTimeOffset>((date, writer) =>
{
writer.Write(date.DrawSvgClock());
}, "text/html");
writer.Write(date.DrawSvgClock());
}, "text/html");

Formatter.Register<DateTimeOffset>((date, writer) =>
{
writer.Write(date.DrawSvgClock());
}, "text/html");

var clockCommand = new Command("#!clock", "Displays a clock showing the current or specified time.")
{
new Option<int>(new[]{"-o","--hour"},
"The position of the hour hand"),
new Option<int>(new[]{"-m","--minute"},
"The position of the minute hand"),
new Option<int>(new[]{"-s","--second"},
"The position of the second hand")
};

clockCommand.Handler = CommandHandler.Create(
(int hour, int minute, int second, KernelInvocationContext invocationContext) =>
{
invocationContext.Display(SvgClock.DrawSvgClock(hour, minute, second));
});
var hourOption = new Option<int>(new[] { "-o", "--hour" },
"The position of the hour hand");
var minuteOption = new Option<int>(new[] { "-m", "--minute" },
"The position of the minute hand");
var secondOption = new Option<int>(new[] { "-s", "--second" },
"The position of the second hand");

kernel.AddDirective(clockCommand);
var clockCommand = new Command("#!clock", "Displays a clock showing the current or specified time.")
{
hourOption,
minuteOption,
secondOption
};

if (KernelInvocationContext.Current is { } context)
clockCommand.SetHandler(
(int hour, int minute, int second) =>
{
PocketView view = div(
code(nameof(ClockExtension)),
" is loaded. It adds visualizations for ",
code(typeof(System.DateTime)),
" and ",
code(typeof(System.DateTimeOffset)),
". Try it by running: ",
code("DateTime.Now"),
" or ",
code("#!clock -h")
);
KernelInvocationContext.Current.Display(SvgClock.DrawSvgClock(hour, minute, second));
},
hourOption,
minuteOption,
secondOption);

context.Display(view);
}
kernel.AddDirective(clockCommand);

return Task.CompletedTask;
if (KernelInvocationContext.Current is { } context)
{
PocketView view = div(
code(nameof(ClockExtension)),
" is loaded. It adds visualizations for ",
code(typeof(DateTime)),
" and ",
code(typeof(DateTimeOffset)),
". Try it by running: ",
code("DateTime.Now"),
" or ",
code("#!clock -h")
);

context.Display(view);
}

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="microsoft.dotnet.interactive.csharp" Version="1.0.0-beta.21357.1" />
<PackageReference Include="microsoft.dotnet.interactive.fsharp" Version="1.0.0-beta.21357.1" />
<PackageReference Include="microsoft.dotnet.interactive.csharp" Version="1.0.0-beta.22256.1" />
<PackageReference Include="microsoft.dotnet.interactive.fsharp" Version="1.0.0-beta.22256.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,6 @@ Microsoft.DotNet.Interactive.Formatting
.ctor()
public System.Void Add(System.String selector, System.ValueTuple<System.String,System.String> properties)
public System.Collections.IEnumerator GetEnumerator()
public System.Collections.Generic.IDictionary<System.String,System.Object> LinkAndApplyClass(System.String class)
public System.Void SetContent(System.Object[] args)
public abstract class TypeFormatter<T>, ITypeFormatter<T>, ITypeFormatter
public System.String MimeType { get;}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// 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.Collections.Generic;
using System.Data;
using System.Reflection;
using FluentAssertions;
using Xunit;

namespace Microsoft.DotNet.Interactive.Formatting.Tests;

public sealed partial class FormatterTests
{
public class Defaults : FormatterTestBase
{
[Fact]
public void Default_formatter_for_Type_displays_generic_parameter_name_for_single_parameter_generic_type()
{
typeof(List<string>).ToDisplayString()
.Should().Be("System.Collections.Generic.List<System.String>");
new List<string>().GetType().ToDisplayString()
.Should().Be("System.Collections.Generic.List<System.String>");
}

[Fact]
public void Default_formatter_for_Type_displays_generic_parameter_name_for_multiple_parameter_generic_type()
{
typeof(Dictionary<string, IEnumerable<int>>).ToDisplayString()
.Should().Be(
"System.Collections.Generic.Dictionary<System.String,System.Collections.Generic.IEnumerable<System.Int32>>");
}

[Fact]
public void Default_formatter_for_Type_displays_generic_parameter_names_for_open_generic_types()
{
typeof(IList<>).ToDisplayString()
.Should().Be("System.Collections.Generic.IList<T>");
typeof(IDictionary<,>).ToDisplayString()
.Should().Be("System.Collections.Generic.IDictionary<TKey,TValue>");
}

[Fact]
public void Custom_formatter_for_Type_can_be_registered()
{
Formatter.Register<Type>(t => t.GUID.ToString());

GetType().ToDisplayString()
.Should().Be(GetType().GUID.ToString());
}

[Fact]
public void Default_formatter_for_null_Nullable_indicates_null()
{
int? nullable = null;

var output = nullable.ToDisplayString();

output.Should().Be(((object) null).ToDisplayString());
}

[Fact]
public void Exceptions_always_get_properties_formatters()
{
var exception = new ReflectionTypeLoadException(
new[]
{
typeof(FileStyleUriParser),
typeof(AssemblyKeyFileAttribute)
},
new Exception[]
{
new DataMisalignedException()
});

var message = exception.ToDisplayString();

message.Should().Contain(nameof(DataMisalignedException.Data));
message.Should().Contain(nameof(DataMisalignedException.HResult));
message.Should().Contain(nameof(DataMisalignedException.StackTrace));
}

[Fact]
public void Exception_Data_is_included_by_default()
{
var ex = new InvalidOperationException("oh noes!", new NullReferenceException());
var key = "a very important int";
ex.Data[key] = 123456;

var msg = ex.ToDisplayString();

msg.Should().Contain(key);
msg.Should().Contain("123456");
}

[Fact]
public void Exception_StackTrace_is_included_by_default()
{
string msg;
var ex = new InvalidOperationException("oh noes!", new NullReferenceException());

try
{
throw ex;
}
catch (Exception thrownException)
{
msg = thrownException.ToDisplayString();
}

msg.Should()
.Contain($"StackTrace: at {typeof(FormatterTests)}.{nameof(Defaults)}.{MethodInfo.GetCurrentMethod().Name}");
}

[Fact]
public void Exception_Type_is_included_by_default()
{
var ex = new InvalidOperationException("oh noes!", new NullReferenceException());

var msg = ex.ToDisplayString();

msg.Should().Contain("InvalidOperationException");
}

[Fact]
public void Exception_Message_is_included_by_default()
{
var ex = new InvalidOperationException("oh noes!", new NullReferenceException());

var msg = ex.ToDisplayString();

msg.Should().Contain("oh noes!");
}

[Fact]
public void Exception_InnerExceptions_are_included_by_default()
{
var ex = new InvalidOperationException("oh noes!", new NullReferenceException("oh my.", new DataException("oops!")));

ex.ToDisplayString()
.Should()
.Contain("NullReferenceException");
ex.ToDisplayString()
.Should()
.Contain("DataException");
}

[Fact]
public void When_ResetToDefault_is_called_then_default_formatters_are_immediately_reregistered()
{
var widget = new Widget { Name = "hola!" };

var defaultValue = widget.ToDisplayString();

Formatter.Register<Widget>(e => "hello!");

widget.ToDisplayString().Should().NotBe(defaultValue);

Formatter.ResetToDefault();

widget.ToDisplayString().Should().Be(defaultValue);
}
}
}
Loading