Skip to content

Commit 6b95ac0

Browse files
ericphansonvtjnash
andauthored
Limit implicit show in REPL to printing 20 KiB by default (#53959)
closes #40735 --------- Co-authored-by: Jameson Nash <vtjnash@gmail.com>
1 parent d32cc26 commit 6b95ac0

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ Standard library changes
167167
- 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),
168168
and for which the name is not public in that module. For example, `map` is defined in Base, and executing `LinearAlgebra.map`
169169
in the REPL will now issue a warning the first time occurs. ([#54872])
170+
- When an object is printed automatically (by being returned in the REPL), its display is now truncated after printing 20 KiB.
171+
This does not affect manual calls to `show`, `print`, and so forth. ([#53959])
170172

171173
#### SuiteSparse
172174

stdlib/REPL/src/REPL.jl

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,10 +484,70 @@ function repl_backend_loop(backend::REPLBackend, get_module::Function)
484484
return nothing
485485
end
486486

487+
SHOW_MAXIMUM_BYTES::Int = 20480
488+
489+
# Limit printing during REPL display
490+
mutable struct LimitIO{IO_t <: IO} <: IO
491+
io::IO_t
492+
maxbytes::Int
493+
n::Int # max bytes to write
494+
end
495+
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
496+
497+
struct LimitIOException <: Exception
498+
maxbytes::Int
499+
end
500+
501+
function Base.showerror(io::IO, e::LimitIOException)
502+
print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
503+
end
504+
505+
function Base.write(io::LimitIO, v::UInt8)
506+
io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
507+
n_bytes = write(io.io, v)
508+
io.n += n_bytes
509+
return n_bytes
510+
end
511+
512+
# Semantically, we only need to override `Base.write`, but we also
513+
# override `unsafe_write` for performance.
514+
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
515+
# already exceeded? throw
516+
limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
517+
remaining = limiter.maxbytes - limiter.n # >= 0
518+
519+
# Not enough bytes left; we will print up to the limit, then throw
520+
if remaining < nb
521+
if remaining > 0
522+
Base.unsafe_write(limiter.io, p, remaining)
523+
end
524+
throw(LimitIOException(limiter.maxbytes))
525+
end
526+
527+
# We won't hit the limit so we'll write the full `nb` bytes
528+
bytes_written = Base.unsafe_write(limiter.io, p, nb)
529+
limiter.n += bytes_written
530+
return bytes_written
531+
end
532+
487533
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
488534
repl::Repl
489535
end
490536

537+
function show_limited(io::IO, mime::MIME, x)
538+
try
539+
# We wrap in a LimitIO to limit the amount of printing.
540+
# We unpack `IOContext`s, since we will pass the properties on the outside.
541+
inner = io isa IOContext ? io.io : io
542+
wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
543+
# `show_repl` to allow the hook with special syntax highlighting
544+
show_repl(wrapped_limiter, mime, x)
545+
catch e
546+
e isa LimitIOException || rethrow()
547+
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)
548+
end
549+
end
550+
491551
function display(d::REPLDisplay, mime::MIME"text/plain", x)
492552
x = Ref{Any}(x)
493553
with_repl_linfo(d.repl) do io
@@ -504,7 +564,7 @@ function display(d::REPLDisplay, mime::MIME"text/plain", x)
504564
# this can override the :limit property set initially
505565
io = foldl(IOContext, d.repl.options.iocontext, init=io)
506566
end
507-
show_repl(io, mime, x[])
567+
show_limited(io, mime, x[])
508568
println(io)
509569
end
510570
return nothing

stdlib/REPL/test/repl.jl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,46 @@ end
19641964
@test undoc == [:AbstractREPL, :BasicREPL, :LineEditREPL, :StreamREPL]
19651965
end
19661966

1967+
struct A40735
1968+
str::String
1969+
end
1970+
1971+
# https://github.com/JuliaLang/julia/issues/40735
1972+
@testset "Long printing" begin
1973+
previous = REPL.SHOW_MAXIMUM_BYTES
1974+
try
1975+
REPL.SHOW_MAXIMUM_BYTES = 1000
1976+
str = string(('a':'z')...)^50
1977+
@test length(str) > 1100
1978+
# For a raw string, we correctly get the standard abbreviated output
1979+
output = sprint(REPL.show_limited, MIME"text/plain"(), str; context=:limit => true)
1980+
hint = """call `show(stdout, MIME"text/plain"(), ans)` to print without truncation"""
1981+
suffix = "[printing stopped after displaying 1000 bytes; $hint]"
1982+
@test !endswith(output, suffix)
1983+
@test contains(output, "bytes ⋯")
1984+
# For a struct without a custom `show` method, we don't hit the abbreviated
1985+
# 3-arg show on the inner string, so here we check that the REPL print-limiting
1986+
# feature is correctly kicking in.
1987+
a = A40735(str)
1988+
output = sprint(REPL.show_limited, MIME"text/plain"(), a; context=:limit => true)
1989+
@test endswith(output, suffix)
1990+
@test length(output) <= 1200
1991+
# We also check some extreme cases
1992+
REPL.SHOW_MAXIMUM_BYTES = 1
1993+
output = sprint(REPL.show_limited, MIME"text/plain"(), 1)
1994+
@test output == "1"
1995+
output = sprint(REPL.show_limited, MIME"text/plain"(), 12)
1996+
@test output == "1…[printing stopped after displaying 1 byte; $hint]"
1997+
REPL.SHOW_MAXIMUM_BYTES = 0
1998+
output = sprint(REPL.show_limited, MIME"text/plain"(), 1)
1999+
@test output == "…[printing stopped after displaying 0 bytes; $hint]"
2000+
@test sprint(io -> show(REPL.LimitIO(io, 5), "abc")) == "\"abc\""
2001+
@test_throws REPL.LimitIOException(1) sprint(io -> show(REPL.LimitIO(io, 1), "abc"))
2002+
finally
2003+
REPL.SHOW_MAXIMUM_BYTES = previous
2004+
end
2005+
end
2006+
19672007
@testset "Dummy Pkg prompt" begin
19682008
# do this in an empty depot to test default for new users
19692009
withenv("JULIA_DEPOT_PATH" => mktempdir() * (Sys.iswindows() ? ";" : ":"), "JULIA_LOAD_PATH" => nothing) do

0 commit comments

Comments
 (0)