From 8dede43faa292d10685c01b1ff3a87633dd78c54 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Fri, 5 Apr 2024 00:45:54 +0200 Subject: [PATCH 01/14] add `LimitIO` to REPL --- stdlib/REPL/src/REPL.jl | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index f75e91da67be2..e41d779779152 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -360,6 +360,32 @@ function repl_backend_loop(backend::REPLBackend, get_module::Function) return nothing end +SHOW_MAXIMUM_BYTES::Int = 100_000 + +# 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 `show` more than $(e.maxbytes) bytes of data in the REPL.") +end + +function Base.write(io::LimitIO, v::UInt8) + io.n > io.maxbytes && throw(LimitIOException(io.maxbytes)) + io.n += write(io.io, v) +end + struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay repl::Repl end @@ -380,7 +406,9 @@ 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(io, mime, x[]) + # We wrap in a LimitIO to limit the amount of printing, and in an + # IOContext to support downstream `get` queries properly. + show(IOContext(LimitIO(io, SHOW_MAXIMUM_BYTES)), mime, x[]) println(io) end return nothing From 026d439e496e705c7f82e18259abfe86a5d87525 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Fri, 5 Apr 2024 01:04:07 +0200 Subject: [PATCH 02/14] add test --- stdlib/REPL/src/REPL.jl | 8 +++++--- stdlib/REPL/test/repl.jl | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index e41d779779152..0ba0859f19b89 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -396,9 +396,11 @@ function display(d::REPLDisplay, mime::MIME"text/plain", x) io = IOContext(io, :limit => true, :module => active_module(d)::Module) if d.repl isa LineEditREPL mistate = d.repl.mistate - mode = LineEdit.mode(mistate) - if mode isa LineEdit.Prompt - LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool) + if mistate !== nothing # `nothing` can occur during testing + mode = LineEdit.mode(mistate) + if mode isa LineEdit.Prompt + LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool) + end end end get(io, :color, false)::Bool && write(io, answer_color(d.repl)) diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 09ff10e770e84..75caf24697a12 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1834,3 +1834,30 @@ end @test_broken isempty(undoc) @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 + fake_repl() do stdin_write, stdout_read, repl + str = string(('a':'z')...)^40 + # Even with the low byte limit, we can show the string + # without issue, thanks to the `show` method that truncates + # string `show`. Here we check that we have forwarded the IOContext + # correctly so that still happens: + display(REPL.REPLDisplay(repl), MIME"text/plain"(), str) + # Now, if we wrap our long string in a struct, we no longer + # get the abbreviated printing. Here we check that with our + # low limit we do trigger the`REPL.LimitIOException`. + a = A40735(str) + @test_throws REPL.LimitIOException display(REPL.REPLDisplay(repl), MIME"text/plain"(), a) + end + finally + REPL.SHOW_MAXIMUM_BYTES = previous + end +end From 4b42a420d3951a2562f8d0a6d720960dbd4fb0a1 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:54:23 +0200 Subject: [PATCH 03/14] stop printing, don't throw an error --- stdlib/REPL/src/REPL.jl | 23 +++++++++++++++-------- stdlib/REPL/test/repl.jl | 26 +++++++++++++------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 0ba0859f19b89..b4b6801b43d07 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -390,17 +390,26 @@ 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, and in an + # IOContext to support downstream `get` queries properly. + show(IOContext(LimitIO(io, SHOW_MAXIMUM_BYTES)), mime, x) + catch e + e isa LimitIOException || rethrow() + print(io, "…[printing stopped after $(e.maxbytes) displaying bytes]") + end +end + function display(d::REPLDisplay, mime::MIME"text/plain", x) x = Ref{Any}(x) with_repl_linfo(d.repl) do io io = IOContext(io, :limit => true, :module => active_module(d)::Module) if d.repl isa LineEditREPL mistate = d.repl.mistate - if mistate !== nothing # `nothing` can occur during testing - mode = LineEdit.mode(mistate) - if mode isa LineEdit.Prompt - LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool) - end + mode = LineEdit.mode(mistate) + if mode isa LineEdit.Prompt + LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool) end end get(io, :color, false)::Bool && write(io, answer_color(d.repl)) @@ -408,9 +417,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 - # We wrap in a LimitIO to limit the amount of printing, and in an - # IOContext to support downstream `get` queries properly. - show(IOContext(LimitIO(io, SHOW_MAXIMUM_BYTES)), 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 75caf24697a12..ba176df9a950e 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1844,19 +1844,19 @@ end previous = REPL.SHOW_MAXIMUM_BYTES try REPL.SHOW_MAXIMUM_BYTES = 1000 - fake_repl() do stdin_write, stdout_read, repl - str = string(('a':'z')...)^40 - # Even with the low byte limit, we can show the string - # without issue, thanks to the `show` method that truncates - # string `show`. Here we check that we have forwarded the IOContext - # correctly so that still happens: - display(REPL.REPLDisplay(repl), MIME"text/plain"(), str) - # Now, if we wrap our long string in a struct, we no longer - # get the abbreviated printing. Here we check that with our - # low limit we do trigger the`REPL.LimitIOException`. - a = A40735(str) - @test_throws REPL.LimitIOException display(REPL.REPLDisplay(repl), MIME"text/plain"(), a) - end + 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, "…") + @test contains(output, "⋯") + # 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 1000 displaying bytes]") + @test length(output) < 1100 finally REPL.SHOW_MAXIMUM_BYTES = previous end From dcaecd1d657937c737269b003deaef5eea4f2464 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Mon, 8 Apr 2024 13:06:16 +0200 Subject: [PATCH 04/14] fix word order --- stdlib/REPL/src/REPL.jl | 2 +- stdlib/REPL/test/repl.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index b4b6801b43d07..03fde8c9be048 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -397,7 +397,7 @@ function show_limited(io::IO, mime::MIME, x) show(IOContext(LimitIO(io, SHOW_MAXIMUM_BYTES)), mime, x) catch e e isa LimitIOException || rethrow() - print(io, "…[printing stopped after $(e.maxbytes) displaying bytes]") + print(io, "…[printing stopped after displaying $(e.maxbytes) bytes]") end end diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index ba176df9a950e..a7c4d4520b752 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1855,7 +1855,7 @@ end # 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 1000 displaying bytes]") + @test endswith(output, "…[printing stopped after displaying 1000 bytes]") @test length(output) < 1100 finally REPL.SHOW_MAXIMUM_BYTES = previous From 5505938ea6a8926e7a28b353c13f0cf7aae97852 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Fri, 26 Apr 2024 19:41:32 +0200 Subject: [PATCH 05/14] add color --- stdlib/REPL/src/REPL.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 03fde8c9be048..82e35b95324b4 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -397,7 +397,7 @@ function show_limited(io::IO, mime::MIME, x) show(IOContext(LimitIO(io, SHOW_MAXIMUM_BYTES)), mime, x) catch e e isa LimitIOException || rethrow() - print(io, "…[printing stopped after displaying $(e.maxbytes) bytes]") + printstyled(io, "…[printing stopped after displaying $(e.maxbytes) bytes]"; color=:light_yellow, bold=true) end end From 2fc5aa4020088710b25b7471a4c733dca8c85cef Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Wed, 12 Jun 2024 00:10:13 +0200 Subject: [PATCH 06/14] performance optimization --- stdlib/REPL/src/REPL.jl | 36 +++++++++++++++++++++++++++++++++--- stdlib/REPL/test/repl.jl | 13 +++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 82e35b95324b4..869dff07b33fd 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -386,15 +386,45 @@ function Base.write(io::LimitIO, v::UInt8) 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) + 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 +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, and in an - # IOContext to support downstream `get` queries properly. - show(IOContext(LimitIO(io, SHOW_MAXIMUM_BYTES)), mime, x) + # 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(wrapped_limiter, mime, x) catch e e isa LimitIOException || rethrow() printstyled(io, "…[printing stopped after displaying $(e.maxbytes) bytes]"; color=:light_yellow, bold=true) diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index a7c4d4520b752..faf84b10308bf 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1848,8 +1848,8 @@ end @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, "…") - @test contains(output, "⋯") + @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. @@ -1857,6 +1857,15 @@ end 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 bytes]" + REPL.SHOW_MAXIMUM_BYTES = 0 + output = sprint(REPL.show_limited, MIME"text/plain"(), 1) + @test output == "…[printing stopped after displaying 0 bytes]" finally REPL.SHOW_MAXIMUM_BYTES = previous end From c769517524155a3b540799dc2dbc9c3fe1b3e3a7 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Wed, 12 Jun 2024 00:26:02 +0200 Subject: [PATCH 07/14] reduce default limit to 20KiB, add test, format bytes --- stdlib/REPL/src/REPL.jl | 6 +++--- stdlib/REPL/test/repl.jl | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 869dff07b33fd..2dc2c90837ca7 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -360,7 +360,7 @@ function repl_backend_loop(backend::REPLBackend, get_module::Function) return nothing end -SHOW_MAXIMUM_BYTES::Int = 100_000 +SHOW_MAXIMUM_BYTES::Int = 20480 # Limit printing during REPL display mutable struct LimitIO{IO_t <: IO} <: IO @@ -378,7 +378,7 @@ struct LimitIOException <: Exception end function Base.showerror(io::IO, e::LimitIOException) - print(io, "$LimitIOException: aborted printing after attempting to `show` more than $(e.maxbytes) bytes of data in the REPL.") + 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) @@ -427,7 +427,7 @@ function show_limited(io::IO, mime::MIME, x) show(wrapped_limiter, mime, x) catch e e isa LimitIOException || rethrow() - printstyled(io, "…[printing stopped after displaying $(e.maxbytes) bytes]"; color=:light_yellow, bold=true) + printstyled(io, "…[printing stopped after displaying $(Base.format_bytes(e.maxbytes))]"; color=:light_yellow, bold=true) end end diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index faf84b10308bf..0ec22d475fc91 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1862,10 +1862,12 @@ end 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 bytes]" + @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 From dee77d654bc396aba6446e9f9f571b644a6ef5d9 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:47:09 +0200 Subject: [PATCH 08/14] fix whitespace --- stdlib/REPL/test/repl.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 237b95de2f0ff..66f008ec88acd 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1874,7 +1874,7 @@ end 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 From 4e1040f5960f6a66fe6616306d4a75ba789e125a Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:50:37 +0200 Subject: [PATCH 09/14] add NEWS --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index a056b85de2499..b69b4164ace28 100644 --- a/NEWS.md +++ b/NEWS.md @@ -120,6 +120,7 @@ Standard library changes complete names that have been explicitly `using`-ed. ([#54610]) - REPL completions can now complete input lines like `[import|using] Mod: xxx|` e.g. complete `using Base.Experimental: @op` to `using Base.Experimental: @opaque`. ([#54719]) +- 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 From 43767ef00d4241ab5d8e020b1503d6cd3f359526 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Sun, 7 Jul 2024 14:59:05 +0200 Subject: [PATCH 10/14] try to fix inference failure --- stdlib/REPL/src/REPL.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 00a6f848a969a..f0ed62bc50046 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -408,7 +408,7 @@ function Base.unsafe_write(io::Base.IOContext{<:LimitIO}, p::Ptr{UInt8}, nb::UIn # 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 + return bytes_written::Union{UInt, Int} end # wrap to hit optimized method From 2461a98bdf7f3d87cf7d6853ed09af90de81dbac Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:54:43 +0200 Subject: [PATCH 11/14] simplify implementation --- stdlib/REPL/src/REPL.jl | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index c231753bff71d..1d64e546aef84 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -516,8 +516,7 @@ 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) - limiter = io.io +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 @@ -525,23 +524,17 @@ function Base.unsafe_write(io::Base.IOContext{<:LimitIO}, p::Ptr{UInt8}, nb::UIn # 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) + 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, again passing along the `ioproperties`. - bytes_written = Base.unsafe_write(IOContext(limiter.io, Base.ioproperties(io)), p, nb) + # 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 -# 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 From 25d016bfb787b74390dfb463750d82e33f410d1c Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Wed, 21 Aug 2024 23:44:49 +0200 Subject: [PATCH 12/14] add hint to print without truncation --- stdlib/REPL/src/REPL.jl | 2 +- stdlib/REPL/test/repl.jl | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 1d64e546aef84..f4b1e5ec38f16 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -549,7 +549,7 @@ function show_limited(io::IO, mime::MIME, x) 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) + 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 diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 6c388cb984ff7..fee0134a05585 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1976,24 +1976,26 @@ end @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]") + 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, "…[printing stopped after displaying 1000 bytes]") - @test length(output) < 1100 + @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]" + @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]" + @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 From 72bb78d83f6fa570247ce01cec7d3c16f309c672 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:06:42 +0200 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Jameson Nash --- stdlib/REPL/src/REPL.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 126cec819a603..a19e78933c0eb 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -494,9 +494,6 @@ mutable struct LimitIO{IO_t <: IO} <: IO 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 @@ -528,7 +525,7 @@ function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt) # 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} + return bytes_written end struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay @@ -540,7 +537,7 @@ function show_limited(io::IO, mime::MIME, x) # 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)) + 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 From d773f3a3e7a3a37cc9b6923ebda20bc42304e8c9 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:07:28 +0200 Subject: [PATCH 14/14] correct return for `Base.write` --- stdlib/REPL/src/REPL.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index a19e78933c0eb..88458f7de4666 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -504,7 +504,9 @@ end function Base.write(io::LimitIO, v::UInt8) io.n > io.maxbytes && throw(LimitIOException(io.maxbytes)) - io.n += write(io.io, v) + 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