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

Merged
merged 24 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8dede43
add `LimitIO` to REPL
ericphanson Apr 4, 2024
026d439
add test
ericphanson Apr 4, 2024
4b42a42
stop printing, don't throw an error
ericphanson Apr 5, 2024
dcaecd1
fix word order
ericphanson Apr 8, 2024
e37c05f
Merge branch 'master' into eph/limit-repl
ericphanson Apr 12, 2024
5505938
add color
ericphanson Apr 26, 2024
2fc5aa4
performance optimization
ericphanson Jun 11, 2024
c769517
reduce default limit to 20KiB, add test, format bytes
ericphanson Jun 11, 2024
7c632c4
Merge branch 'master' into eph/limit-repl
ericphanson Jun 20, 2024
5e15614
Merge branch 'master' into eph/limit-repl
ericphanson Jun 24, 2024
dee77d6
fix whitespace
ericphanson Jun 24, 2024
4e1040f
add NEWS
ericphanson Jun 24, 2024
43767ef
try to fix inference failure
ericphanson Jul 7, 2024
03f5ba9
Merge branch 'master' into eph/limit-repl
ericphanson Jul 7, 2024
0bb1298
Merge branch 'master' into eph/limit-repl
ericphanson Jul 8, 2024
1add163
Merge branch 'master' into eph/limit-repl
ericphanson Jul 24, 2024
8a48d1c
Merge remote-tracking branch 'origin/master' into eph/limit-repl
ericphanson Jul 24, 2024
2461a98
simplify implementation
ericphanson Aug 20, 2024
25d016b
add hint to print without truncation
ericphanson Aug 21, 2024
afcfae3
Merge branch 'master' into eph/limit-repl
ericphanson Aug 30, 2024
5f49559
Merge branch 'master' into eph/limit-repl
ericphanson Sep 11, 2024
415f587
Merge branch 'master' into eph/limit-repl
ericphanson Oct 14, 2024
72bb78d
Apply suggestions from code review
ericphanson Oct 16, 2024
d773f3a
correct return for `Base.write`
ericphanson Oct 16, 2024
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 @@ -165,6 +165,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[])
ericphanson marked this conversation as resolved.
Show resolved Hide resolved
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