Skip to content

Commit

Permalink
Limit implicit show in REPL to printing 20 KiB by default (#53959)
Browse files Browse the repository at this point in the history
closes #40735

---------

Co-authored-by: Jameson Nash <vtjnash@gmail.com>
  • Loading branch information
ericphanson and vtjnash authored Oct 17, 2024
1 parent d32cc26 commit 6b95ac0
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 1 deletion.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,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
62 changes: 61 additions & 1 deletion stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -484,10 +484,70 @@ 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)

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))
n_bytes = write(io.io, v)
io.n += n_bytes
return n_bytes
end

# Semantically, we only need to override `Base.write`, but we also
# override `unsafe_write` for performance.
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
# 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
Base.unsafe_write(limiter.io, p, remaining)
end
throw(LimitIOException(limiter.maxbytes))
end

# We won't hit the limit so we'll write the full `nb` bytes
bytes_written = Base.unsafe_write(limiter.io, p, nb)
limiter.n += bytes_written
return bytes_written
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), 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)); call `show(stdout, MIME"text/plain"(), ans)` to print without truncation]"""; 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 @@ -504,7 +564,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[])
println(io)
end
return nothing
Expand Down
40 changes: 40 additions & 0 deletions stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1964,6 +1964,46 @@ 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)
hint = """call `show(stdout, MIME"text/plain"(), ans)` to print without truncation"""
suffix = "[printing stopped after displaying 1000 bytes; $hint]"
@test !endswith(output, suffix)
@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, suffix)
@test length(output) <= 1200
# 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; $hint]"
REPL.SHOW_MAXIMUM_BYTES = 0
output = sprint(REPL.show_limited, MIME"text/plain"(), 1)
@test output == "…[printing stopped after displaying 0 bytes; $hint]"
@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() * (Sys.iswindows() ? ";" : ":"), "JULIA_LOAD_PATH" => nothing) do
Expand Down

0 comments on commit 6b95ac0

Please sign in to comment.