diff --git a/NEWS.md b/NEWS.md index c12cc3c64300c..1d26c98674320 100644 --- a/NEWS.md +++ b/NEWS.md @@ -155,6 +155,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 diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index ddf2f55d0b9f7..5a95ba2897768 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -474,10 +474,71 @@ 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(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::Union{UInt, Int} +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)); 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 @@ -494,7 +555,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 diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index f4d594b2a02e1..ec56e4de51686 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -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(), "JULIA_LOAD_PATH" => nothing) do