Packages
livebook
0.18.3
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/runtime/evaluator/formatter.ex
defmodule Livebook.Runtime.Evaluator.Formatter do
require Logger
@compile {:no_warn_undefined, {Kino.Render, :to_livebook, 1}}
@compile {:no_warn_undefined, {Kino.Render, :impl_for, 1}}
@compile {:no_warn_undefined, {Pythonx, :eval, 2}}
@compile {:no_warn_undefined, {Pythonx, :decode, 1}}
@doc """
Formats evaluation result into an output.
We format the result into one of the standardized runtime outputs.
Other than that, formatting is important to convert potentially
large result into a more compact representation. Finally, we want
to format in the runtime node, because it oftentimes relies on the
`inspect` protocol implementations from external packages.
"""
@spec format_result(
Livebook.Runtime.language(),
Livebook.Runtime.Evaluator.evaluation_result()
) :: Livebook.Runtime.output()
def format_result(language, result)
def format_result(:elixir, {:ok, :"do not show this result in output"}) do
# Functions in the `IEx.Helpers` module return this specific value
# to indicate no result should be printed in the iex shell,
# so we respect that as well.
%{type: :ignored}
end
def format_result(:elixir, {:ok, {:module, _, _, _} = value}) do
to_inspect_output(value, limit: 10)
end
def format_result(:elixir, {:ok, value}) do
to_output(value)
end
def format_result(:erlang, {:ok, value}) do
erlang_to_output(value)
end
def format_result(:erlang, {:error, kind, error, stacktrace}) do
if is_exception(error) do
format_result(:elixir, {:error, kind, error, stacktrace})
else
formatted =
:erl_error.format_exception(kind, error, stacktrace)
|> error_color
|> :erlang.list_to_binary()
%{type: :error, message: formatted, context: error_context(error)}
end
end
def format_result(:python, {:ok, nil}) do
%{type: :ignored}
end
def format_result(:python, {:ok, value}) do
if Code.ensure_loaded?(Kino.Render) do
try do
if Kino.Render.impl_for(value) == Kino.Render.Any do
to_repr_output(value)
else
Kino.Render.to_livebook(value)
end
catch
kind, error ->
formatted = format_error(kind, error, __STACKTRACE__)
Logger.error(formatted)
to_repr_output(value)
end
else
to_repr_output(value)
end
end
def format_result(:python, {:error, _kind, error, _stacktrace})
when is_struct(error, Pythonx.Error) do
formatted =
Pythonx.eval(
"""
import traceback
# For SyntaxErrors the traceback is not relevant
traceback_ = None if isinstance(value, SyntaxError) else traceback_
traceback.format_exception(type, value, traceback_)
""",
%{"type" => error.type, "value" => error.value, "traceback_" => error.traceback}
)
|> elem(0)
|> Pythonx.decode()
|> error_color()
|> IO.iodata_to_binary()
%{type: :error, message: formatted, context: nil}
end
def format_result(:"pyproject.toml", {:ok, _value}) do
%{type: :terminal_text, text: "Ok", chunk: false}
end
def format_result(_language, {:error, kind, error, stacktrace}) do
formatted = format_error(kind, error, stacktrace)
%{type: :error, message: formatted, context: error_context(error)}
end
defp to_output(value) do
# Kino is a "client side" extension for Livebook that may be
# installed into the runtime node. If it is installed we use
# its more precise output rendering rules.
if Code.ensure_loaded?(Kino.Render) do
try do
Kino.Render.to_livebook(value)
catch
kind, error ->
formatted = format_error(kind, error, __STACKTRACE__)
Logger.error(formatted)
to_inspect_output(value)
end
else
to_inspect_output(value)
end
end
defp to_repr_output(value) do
repr_string =
Pythonx.eval("repr(value)", %{"value" => value}) |> elem(0) |> Pythonx.decode()
%{type: :terminal_text, text: repr_string, chunk: false}
end
defp to_inspect_output(value, opts \\ []) do
try do
inspected = inspect(value, inspect_opts(opts))
%{type: :terminal_text, text: inspected, chunk: false}
catch
kind, error ->
formatted = format_error(kind, error, __STACKTRACE__)
%{type: :error, message: formatted, context: error_context(error)}
end
end
def inspect_opts(opts \\ []) do
default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()]
Keyword.merge(default_opts, opts)
end
def syntax_colors() do
# Note: we intentionally don't specify colors
# for `:binary`, `:list`, `:map` and `:tuple`
# and rely on these using the default text color.
# This way we avoid a bunch of HTML tags for coloring commas, etc.
[
atom: :blue,
# binary: :light_black,
boolean: :magenta,
# list: :light_black,
# map: :light_black,
number: :blue,
nil: :magenta,
regex: :red,
string: :green,
# tuple: :light_black,
reset: :reset
]
end
defp format_error(kind, error, stacktrace) do
{blamed, stacktrace} =
case error do
%CompileError{description: "cannot compile file (errors have been logged)" <> _, line: 0} ->
{%CompileError{description: "cannot compile cell (errors have been logged)"}, []}
_ ->
Exception.blame(kind, error, stacktrace)
end
banner =
case blamed do
%FunctionClauseError{} ->
banner = Exception.format_banner(kind, error, stacktrace)
blame = FunctionClauseError.blame(blamed, &inspect(&1, inspect_opts()), &blame_match/1)
[error_color(banner), pad(blame)]
_ ->
banner = Exception.format_banner(kind, blamed, stacktrace)
error_color(banner)
end
message =
if stacktrace == [] do
banner
else
stacktrace = format_stacktrace(stacktrace)
[banner, "\n", error_color(stacktrace)]
end
IO.iodata_to_binary(message)
end
defp format_stacktrace(trace) do
case trace do
[] -> "\n"
_ -> " " <> Enum.map_join(trace, "\n ", &format_stacktrace_entry(&1)) <> "\n"
end
end
@doc """
Formats a stacktrace entry keeping only the #cell: bits.
"""
def format_stacktrace_entry(entry) do
entry =
with {mod, fun, arity, info} <- entry,
[_ | _] = file <- info[:file],
[_, cell] <- :string.split(file, "#cell:") do
{mod, fun, arity, Keyword.put(info, :file, ~c"#cell:" ++ cell)}
else
_ -> entry
end
Exception.format_stacktrace_entry(entry)
end
defp blame_match(%{match?: true, node: node}), do: Macro.to_string(node)
defp blame_match(%{match?: false, node: node}) do
node
|> Macro.to_string()
|> error_color()
|> IO.iodata_to_binary()
end
defp pad(string) do
" " <> String.replace(string, "\n", "\n ")
end
defp error_color(string) do
IO.ANSI.format([:red, string], true)
end
defp error_context(%System.EnvError{env: "LB_" <> secret_name}),
do: {:missing_secret, secret_name}
defp error_context(error) when is_struct(error, Kino.InterruptError),
do: {:interrupt, error.variant, error.message}
defp error_context(error) when is_struct(error, Kino.FS.ForbiddenError),
do: {:file_entry_forbidden, error.name}
defp error_context(error) when is_struct(error, Mix.Error),
do: :dependencies
defp error_context(_), do: nil
defp erlang_to_output(value) do
text = :io_lib.format("~p", [value]) |> IO.iodata_to_binary()
%{type: :terminal_text, text: text, chunk: false}
end
end