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

Limit implicit show in REPL to printing 20 KiB by default #53959

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 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
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ Standard library changes
- the REPL will now warn if it detects a name is being accessed from a module which does not define it (nor has a submodule which defines it),
and for which the name is not public in that module. For example, `map` is defined in Base, and executing `LinearAlgebra.map`
in the REPL will now issue a warning the first time occurs. ([#54872])
- When an object is printed automatically (by being returned in the REPL), its display is now truncated after printing 20 KiB.
This does not affect manual calls to `show`, `print`, and so forth. ([#53959])

#### SuiteSparse

Expand Down
70 changes: 69 additions & 1 deletion stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -488,10 +488,78 @@ function repl_backend_loop(backend::REPLBackend, get_module::Function)
return nothing
end

SHOW_MAXIMUM_BYTES::Int = 20480

# Limit printing during REPL display
mutable struct LimitIO{IO_t <: IO} <: IO
io::IO_t
maxbytes::Int
n::Int # max bytes to write
end
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)

# Forward `ioproperties` onwards (for interop with IOContext)
Base.ioproperties(io::LimitIO) = Base.ioproperties(io.io)

struct LimitIOException <: Exception
maxbytes::Int
end

function Base.showerror(io::IO, e::LimitIOException)
print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
end

function Base.write(io::LimitIO, v::UInt8)
io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
io.n += write(io.io, v)
end

# Semantically, we only need to override `Base.write`, but we also
# override `unsafe_write` for performance.
function Base.unsafe_write(io::Base.IOContext{<:LimitIO}, p::Ptr{UInt8}, nb::UInt)
ericphanson marked this conversation as resolved.
Show resolved Hide resolved
limiter = io.io
# already exceeded? throw
limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
remaining = limiter.maxbytes - limiter.n # >= 0

# Not enough bytes left; we will print up to the limit, then throw
if remaining < nb
if remaining > 0
# note we pass on the `ioproperties` with the inner `limiter.io` object
Base.unsafe_write(IOContext(limiter.io, Base.ioproperties(io)), p, remaining)
end
throw(LimitIOException(limiter.maxbytes))
end

# We won't hit the limit so we'll write the full `nb` bytes, again passing along the `ioproperties`.
bytes_written = Base.unsafe_write(IOContext(limiter.io, Base.ioproperties(io)), p, nb)
limiter.n += bytes_written
return bytes_written::Union{UInt, Int}
end

# wrap to hit optimized method
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
return unsafe_write(IOContext(limiter), p, nb)
end

struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
repl::Repl
end

function show_limited(io::IO, mime::MIME, x)
try
# We wrap in a LimitIO to limit the amount of printing.
# We unpack `IOContext`s, since we will pass the properties on the outside.
inner = io isa IOContext ? io.io : io
wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), Base.ioproperties(io))
# `show_repl` to allow the hook with special syntax highlighting
show_repl(wrapped_limiter, mime, x)
catch e
e isa LimitIOException || rethrow()
printstyled(io, "…[printing stopped after displaying $(Base.format_bytes(e.maxbytes))]"; color=:light_yellow, bold=true)
end
end

function display(d::REPLDisplay, mime::MIME"text/plain", x)
x = Ref{Any}(x)
with_repl_linfo(d.repl) do io
Expand All @@ -508,7 +576,7 @@ function display(d::REPLDisplay, mime::MIME"text/plain", x)
# this can override the :limit property set initially
io = foldl(IOContext, d.repl.options.iocontext, init=io)
end
show_repl(io, mime, x[])
show_limited(io, mime, x[])
ericphanson marked this conversation as resolved.
Show resolved Hide resolved
println(io)
end
return nothing
Expand Down
38 changes: 38 additions & 0 deletions stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,44 @@ end
@test undoc == [:AbstractREPL, :BasicREPL, :LineEditREPL, :StreamREPL]
end

struct A40735
str::String
end

# https://github.com/JuliaLang/julia/issues/40735
@testset "Long printing" begin
previous = REPL.SHOW_MAXIMUM_BYTES
try
REPL.SHOW_MAXIMUM_BYTES = 1000
str = string(('a':'z')...)^50
@test length(str) > 1100
# For a raw string, we correctly get the standard abbreviated output
output = sprint(REPL.show_limited, MIME"text/plain"(), str; context=:limit => true)
@test !endswith(output, "[printing stopped after displaying 1000 bytes]")
@test contains(output, "bytes ⋯")
# For a struct without a custom `show` method, we don't hit the abbreviated
# 3-arg show on the inner string, so here we check that the REPL print-limiting
# feature is correctly kicking in.
a = A40735(str)
output = sprint(REPL.show_limited, MIME"text/plain"(), a; context=:limit => true)
@test endswith(output, "…[printing stopped after displaying 1000 bytes]")
@test length(output) < 1100
# We also check some extreme cases
REPL.SHOW_MAXIMUM_BYTES = 1
output = sprint(REPL.show_limited, MIME"text/plain"(), 1)
@test output == "1"
output = sprint(REPL.show_limited, MIME"text/plain"(), 12)
@test output == "1…[printing stopped after displaying 1 byte]"
REPL.SHOW_MAXIMUM_BYTES = 0
output = sprint(REPL.show_limited, MIME"text/plain"(), 1)
@test output == "…[printing stopped after displaying 0 bytes]"
@test sprint(io -> show(REPL.LimitIO(io, 5), "abc")) == "\"abc\""
@test_throws REPL.LimitIOException(1) sprint(io -> show(REPL.LimitIO(io, 1), "abc"))
finally
REPL.SHOW_MAXIMUM_BYTES = previous
end
end

@testset "Dummy Pkg prompt" begin
# do this in an empty depot to test default for new users
withenv("JULIA_DEPOT_PATH" => mktempdir(), "JULIA_LOAD_PATH" => nothing) do
Expand Down