Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for chunked text and markdown outputs #318

Merged
merged 2 commits into from
Aug 22, 2023
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
4 changes: 2 additions & 2 deletions lib/kino.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ defmodule Kino do
def inspect(term, opts \\ []) do
label = if label = opts[:label], do: "#{label}: ", else: ""

{:text, text} = Kino.Output.inspect(term, opts)
output = {:text, label <> text}
{:terminal_text, text, info} = Kino.Output.inspect(term, opts)
output = {:terminal_text, label <> text, info}
Kino.Bridge.put_output(output)

term
Expand Down
25 changes: 17 additions & 8 deletions lib/kino/markdown.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,32 @@ defmodule Kino.Markdown do
This format may come in handy when exploring Markdown
from external sources:

content = File.read!("/path/to/README.md")
Kino.Markdown.new(content)
text = File.read!("/path/to/README.md")
Kino.Markdown.new(text)
'''

@enforce_keys [:content]
@enforce_keys [:text, :chunk]

defstruct [:content]
defstruct [:text, :chunk]

@opaque t :: %__MODULE__{
content: binary()
text: String.t(),
chunk: boolean()
}

@doc """
Creates a new kino displaying the given Markdown content.

## Options

* `:chunk` - whether this is a part of a larger text. Adjacent chunks
are merged into a single text. This is useful for streaming content.
Defaults to `false`

"""
@spec new(binary()) :: t()
def new(content) do
%__MODULE__{content: content}
@spec new(binary(), keyword()) :: t()
def new(text, opts \\ []) do
opts = Keyword.validate!(opts, chunk: false)
%__MODULE__{text: text, chunk: opts[:chunk]}
end
end
50 changes: 24 additions & 26 deletions lib/kino/output.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ defmodule Kino.Output do
"""
@type t ::
ignored()
| stdout()
| text()
| terminal_text()
| plain_text()
| markdown()
| image()
Expand All @@ -27,31 +26,30 @@ defmodule Kino.Output do
"""
@type ignored :: :ignored

@typedoc """
IO text output, adjacent such outputs are treated as a whole.
@typedoc ~S"""
Terminal text content.

Supports ANSI escape codes.
Supports ANSI escape codes and overwriting lines with `\r`.
"""
@type stdout :: {:stdout, binary()}

@typedoc """
Standalone text block visually matching `t:stdout/0`.

Supports ANSI escape codes.
"""
@type text :: {:text, binary()}
@type terminal_text :: {:terminal_text, String.t(), info :: %{chunk: boolean()}}

@typedoc """
Plain text content.

Adjacent outputs with `:chunk` set to `true` are merged and rendered
as a whole.

Similar to `t:markdown/0`, but with no special markup.
"""
@type plain_text :: {:plain_text, binary()}
@type plain_text :: {:plain_text, String.t(), info :: %{chunk: boolean()}}

@typedoc """
Markdown content.

Adjacent outputs with `:chunk` set to `true` are merged and rendered
as a whole.
"""
@type markdown :: {:markdown, binary()}
@type markdown :: {:markdown, String.t(), info :: %{chunk: boolean()}}

@typedoc """
A raw image in the given format.
Expand Down Expand Up @@ -423,27 +421,27 @@ defmodule Kino.Output do
@type ref :: String.t()

@doc """
See `t:text/0`.
See `t:terminal_text/0`.
"""
@spec text(binary()) :: t()
def text(text) when is_binary(text) do
{:text, text}
@spec terminal_text(binary(), boolean()) :: t()
def terminal_text(text, chunk \\ false) when is_binary(text) do
{:terminal_text, text, %{chunk: chunk}}
end

@doc """
See `t:plain_text/0`.
"""
@spec plain_text(binary()) :: t()
def plain_text(text) do
{:plain_text, text}
@spec plain_text(binary(), boolean()) :: t()
def plain_text(text, chunk \\ false) do
{:plain_text, text, %{chunk: chunk}}
end

@doc """
See `t:markdown/0`.
"""
@spec markdown(binary()) :: t()
def markdown(content) when is_binary(content) do
{:markdown, content}
@spec markdown(binary(), boolean()) :: t()
def markdown(content, chunk \\ false) when is_binary(content) do
{:markdown, content, %{chunk: chunk}}
end

@doc """
Expand Down Expand Up @@ -508,7 +506,7 @@ defmodule Kino.Output do
@spec inspect(term(), keyword()) :: t()
def inspect(term, opts \\ []) do
inspected = Kernel.inspect(term, inspect_opts(opts))
text(inspected)
terminal_text(inspected)
end

defp inspect_opts(opts) do
Expand Down
6 changes: 3 additions & 3 deletions lib/kino/render.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ end

defimpl Kino.Render, for: Kino.Text do
def to_livebook(%{terminal: true} = text) do
Kino.Output.text(text.content)
Kino.Output.terminal_text(text.text, text.chunk)
end

def to_livebook(text) do
Kino.Output.plain_text(text.content)
Kino.Output.plain_text(text.text, text.chunk)
end
end

defimpl Kino.Render, for: Kino.Markdown do
def to_livebook(markdown) do
Kino.Output.markdown(markdown.content)
Kino.Output.markdown(markdown.text, markdown.chunk)
end
end

Expand Down
22 changes: 14 additions & 8 deletions lib/kino/text.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ defmodule Kino.Text do

'''

@enforce_keys [:content]
@enforce_keys [:text]

defstruct [:content, terminal: false]
defstruct [:text, :terminal, :chunk]

@opaque t :: %__MODULE__{
content: binary(),
terminal: boolean()
text: String.t(),
terminal: boolean(),
chunk: boolean()
}

@doc """
Expand All @@ -31,10 +32,15 @@ defmodule Kino.Text do

* `:terminal` - whether to render the text as if it were printed to
standard output, supporting ANSI escape codes. Defaults to `false`

* `:chunk` - whether this is a part of a larger text. Adjacent chunks
are merged into a single text. This is useful for streaming content.
Defaults to `false`

"""
@spec new(String.t(), opts) :: t() when opts: [terminal: boolean()]
def new(content, opts \\ []) when is_binary(content) do
opts = Keyword.validate!(opts, terminal: false)
%__MODULE__{content: content, terminal: opts[:terminal]}
@spec new(String.t(), opts) :: t() when opts: [terminal: boolean(), chunk: boolean()]
def new(text, opts \\ []) when is_binary(text) do
opts = Keyword.validate!(opts, terminal: false, chunk: false)
%__MODULE__{text: text, terminal: opts[:terminal], chunk: opts[:chunk]}
end
end
17 changes: 12 additions & 5 deletions test/kino/debug_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ defmodule Kino.Debug.Test do

{kino, output, _frame_ref} = assert_dbg_pipeline_render()

assert output == {:text, "\e[34m13\e[0m"}
assert output == {:terminal_text, "\e[34m13\e[0m", %{chunk: false}}

%{
dbg_line: dbg_line,
Expand Down Expand Up @@ -76,7 +76,10 @@ defmodule Kino.Debug.Test do
"changed" => true
})

assert_output({:frame, [{:text, "\e[34m31\e[0m"}], %{ref: ^frame_ref, type: :replace}})
assert_output(
{:frame, [{:terminal_text, "\e[34m31\e[0m", %{chunk: false}}],
%{ref: ^frame_ref, type: :replace}}
)
end

test "updates result when a pipeline step is moved" do
Expand All @@ -100,7 +103,10 @@ defmodule Kino.Debug.Test do
"changed" => true
})

assert_output({:frame, [{:text, "\e[34m31\e[0m"}], %{ref: ^frame_ref, type: :replace}})
assert_output(
{:frame, [{:terminal_text, "\e[34m31\e[0m", %{chunk: false}}],
%{ref: ^frame_ref, type: :replace}}
)
end

test "handles evaluation error" do
Expand All @@ -124,7 +130,8 @@ defmodule Kino.Debug.Test do
})

assert_output(
{:frame, [{:text, "\e[34m1\e[0m..\e[34m5\e[0m"}], %{ref: ^frame_ref, type: :replace}}
{:frame, [{:terminal_text, "\e[34m1\e[0m..\e[34m5\e[0m", %{chunk: false}}],
%{ref: ^frame_ref, type: :replace}}
)
end

Expand Down Expand Up @@ -163,7 +170,7 @@ defmodule Kino.Debug.Test do

{kino, output} = assert_dbg_default_render()

assert output == {:text, "\e[34m15\e[0m"}
assert output == {:terminal_text, "\e[34m15\e[0m", %{chunk: false}}

%{
dbg_line: dbg_line,
Expand Down
20 changes: 15 additions & 5 deletions test/kino/frame_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ defmodule Kino.FrameTest do
frame = Kino.Frame.new()

Kino.Frame.render(frame, 1)
assert_output({:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})

assert_output(
{:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}}
)

Kino.Frame.render(frame, Kino.Markdown.new("_hey_"))
assert_output({:frame, [{:markdown, "_hey_"}], %{type: :replace}})
assert_output({:frame, [{:markdown, "_hey_", %{chunk: false}}], %{type: :replace}})
end

test "render/2 sends output to a specific client when the :to is given" do
frame = Kino.Frame.new()

Kino.Frame.render(frame, 1, to: "client1")
assert_output_to("client1", {:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})

assert_output_to(
"client1",
{:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}}
)

assert Kino.Frame.get_outputs(frame) == []
end
Expand All @@ -24,7 +31,10 @@ defmodule Kino.FrameTest do
frame = Kino.Frame.new()

Kino.Frame.render(frame, 1, temporary: true)
assert_output_to_clients({:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})

assert_output_to_clients(
{:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}}
)

assert Kino.Frame.get_outputs(frame) == []
end
Expand Down Expand Up @@ -65,6 +75,6 @@ defmodule Kino.FrameTest do
frame = Kino.Frame.new()

Kino.Frame.append(frame, 1)
assert_output({:frame, [{:text, "\e[34m1\e[0m"}], %{type: :append}})
assert_output({:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :append}})
end
end
2 changes: 1 addition & 1 deletion test/kino/output_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Kino.OutputTest do
Kino.Config.configure(inspect: [limit: 1, syntax_colors: []])

list = Enum.to_list(1..100)
assert Kino.Output.inspect(list) == {:text, "[1, ...]"}
assert Kino.Output.inspect(list) == {:terminal_text, "[1, ...]", %{chunk: false}}

Application.delete_env(:kino, :inspect)
end
Expand Down
6 changes: 3 additions & 3 deletions test/kino/text_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ defmodule Kino.TextTest do
describe "new/1" do
test "outputs plain text" do
"Hello!" |> Kino.Text.new() |> Kino.render()
assert_output({:plain_text, "Hello!"})
assert_output({:plain_text, "Hello!", %{chunk: false}})

"Hello!" |> Kino.Text.new(terminal: false) |> Kino.render()
assert_output({:plain_text, "Hello!"})
assert_output({:plain_text, "Hello!", %{chunk: false}})
end

test "outputs terminal text" do
"Hello!" |> Kino.Text.new(terminal: true) |> Kino.render()
assert_output({:text, "Hello!"})
assert_output({:terminal_text, "Hello!", %{chunk: false}})
end
end
end
Loading
Loading