From 761c4ffa96ae2bb99e8eab81ed0fdb39f3757820 Mon Sep 17 00:00:00 2001 From: kimikage Date: Thu, 22 Oct 2020 18:04:58 +0900 Subject: [PATCH 1/3] Add `color` keyword argument This adds `color` keyword argument to `iocapture()`. Currently, this argument does not work on Julia v1.5 or earlier, but to avoid version-based conditional branching on the user side, it does not throw an error. --- README.md | 13 +++++++++++++ src/IOCapture.jl | 21 ++++++++++++++++----- test/runtests.jl | 25 ++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 70114b1..9deb3c4 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ See the docstring for full documentation. ## Known limitations +### Separately stored `stdout` or `stderr` objects + The capturing does not work properly if `f` prints to the `stdout` object that has been stored in a separate variable or object, e.g.: @@ -56,6 +58,17 @@ Stacktrace: This is because `stdout` and `stderr` within an `iocapture` actually refer to the temporary redirect streams which get cleaned up at the end of the `iocapture` call. +### ANSI color/escape code + +On Julia v1.6 and later, the captured output of `iocapture` inherits the `:color` property +of the `stdout` or `stderr` by default. The colorization can be disabled by setting the +`color` keyword argument of `iocapture` to `false`. + +On the other hand, on Julia v1.5 or earlier, even if the `color` keyword argument is set to +`true`, no coloring will be applied. However, this limitation might be removed in the +future, so you should specify `color=false` if you want to avoid coloring. + + ## Similar packages * [Suppressor.jl](https://github.com/JuliaIO/Suppressor.jl) provides similar functionality, diff --git a/src/IOCapture.jl b/src/IOCapture.jl index d5d2ed4..32bb0f0 100644 --- a/src/IOCapture.jl +++ b/src/IOCapture.jl @@ -4,7 +4,7 @@ using Logging export iocapture """ - iocapture(f; throwerrors=true) + iocapture(f; throwerrors=true, color=true) Runs the function `f` and captures the `stdout` and `stderr` outputs without printing them in the terminal. Returns an object with the following fields: @@ -21,6 +21,10 @@ The behaviour can be customized with the following keyword arguments: via the `.value` field (with also `.error` and `.backtrace` set accordingly). If set to `:interrupt`, only `InterruptException`s are rethrown. +* `color`: if set to `true` (default), `iocapture` inherits the `:color` property of + `stdout` and `stderr`, which specifies whether ANSI color/escape codes are expected. This + argument is only effective on Julia v1.6 and later. + # Extended help `iocapture` works by temporarily redirecting the standard output and error streams @@ -49,7 +53,7 @@ It is also possible to set `throwerrors = :interrupt`, which will make `iocaptur only `InterruptException`s. This is useful when you want to capture all the exceptions, but allow the user to interrupt the running code with `Ctrl+C`. """ -function iocapture(f; throwerrors::Union{Bool,Symbol}=true) +function iocapture(f; throwerrors::Union{Bool,Symbol}=true, color::Bool=true) # Currently, :interrupt is the only valid Symbol value for throwerrors if isa(throwerrors, Symbol) && throwerrors !== :interrupt throw(DomainError(throwerrors, "Invalid value passed for throwerrors")) @@ -62,10 +66,17 @@ function iocapture(f; throwerrors::Union{Bool,Symbol}=true) # Redirect both the `stdout` and `stderr` streams to a single `Pipe` object. pipe = Pipe() Base.link_pipe!(pipe; reader_supports_async = true, writer_supports_async = true) - redirect_stdout(pipe.in) - redirect_stderr(pipe.in) + if VERSION >= v"1.6.0-DEV.481" # https://github.com/JuliaLang/julia/pull/36688 + pe_stdout = IOContext(pipe.in, :color => get(stdout, :color, false) & color) + pe_stderr = IOContext(pipe.in, :color => get(stdout, :color, false) & color) + else + pe_stdout = pipe.in + pe_stderr = pipe.in + end + redirect_stdout(pe_stdout) + redirect_stderr(pe_stderr) # Also redirect logging stream to the same pipe - logger = ConsoleLogger(pipe.in) + logger = ConsoleLogger(pe_stderr) # Bytes written to the `pipe` are captured in `output` and converted to a `String`. output = UInt8[] diff --git a/test/runtests.jl b/test/runtests.jl index 6042aee..b393cdf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,6 +5,10 @@ using Test # Note: this can not be inside the testset (VERSION < v"1.2.0-DEV.272") && (hasfield(::Type{T}, name::Symbol) where T = Base.fieldindex(T, name, false) > 0) +hascolor(io) = VERSION >= v"1.6.0-DEV.481" && get(io, :color, false) +has_escapecodes(s) = occursin(r"\e\[[^m]*m", s) +strip_escapecodes(s) = replace(s, r"\e\[[^m]*m" => "") + # Callable object for testing struct Foo x @@ -61,13 +65,25 @@ end @test c.value === nothing # Colors get discarded - c = iocapture() do + c = iocapture(color=false) do printstyled("foo", color=:red) end @test !c.error @test c.output == "foo" @test c.value === nothing + # Colors are preserved if it's supported + c = iocapture() do + printstyled("foo", color=:red) + end + @test !c.error + if hascolor(stdout) + @test c.output == "\e[31mfoo\e[39m" + else + @test c.output == "foo" + end + @test c.value === nothing + # This test checks that deprecation warnings are captured correctly c = iocapture() do println("println") @@ -83,9 +99,12 @@ end @test isdefined(Base, :JLOptions) @test hasfield(Base.JLOptions, :depwarn) if Base.JLOptions().depwarn == 0 # --depwarn=no, default on Julia >= 1.5 - @test c.output == "println\n[ Info: @info\n" + @test has_escapecodes(c.output) === hascolor(stderr) + @test strip_escapecodes(c.output) == "println\n[ Info: @info\n" else # --depwarn=yes - @test startswith(c.output, "println\n[ Info: @info\n┌ Warning: depwarn\n") + @test has_escapecodes(c.output) === hascolor(stderr) + output_nocol = strip_escapecodes(c.output) + @test startswith(output_nocol, "println\n[ Info: @info\n┌ Warning: depwarn\n") end # Exceptions -- normally rethrown From 076f427b5e00d2cea42ab6c5e7de9045e942299f Mon Sep 17 00:00:00 2001 From: kimikage Date: Sun, 25 Oct 2020 20:36:52 +0900 Subject: [PATCH 2/3] Change the default value of `color` kwarg to `false` This also fixes a typo (stdout --> stderr). --- README.md | 7 +++---- src/IOCapture.jl | 14 +++++++------- test/runtests.jl | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9deb3c4..d906aa1 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,12 @@ redirect streams which get cleaned up at the end of the `iocapture` call. ### ANSI color/escape code -On Julia v1.6 and later, the captured output of `iocapture` inherits the `:color` property -of the `stdout` or `stderr` by default. The colorization can be disabled by setting the -`color` keyword argument of `iocapture` to `false`. +On Julia v1.6 and later, if the `color` keyword argument is set to `true`, the captured +output of `iocapture` inherits the `:color` property of the `stdout` or `stderr`. On the other hand, on Julia v1.5 or earlier, even if the `color` keyword argument is set to `true`, no coloring will be applied. However, this limitation might be removed in the -future, so you should specify `color=false` if you want to avoid coloring. +future. ## Similar packages diff --git a/src/IOCapture.jl b/src/IOCapture.jl index 32bb0f0..1b2ae3e 100644 --- a/src/IOCapture.jl +++ b/src/IOCapture.jl @@ -4,7 +4,7 @@ using Logging export iocapture """ - iocapture(f; throwerrors=true, color=true) + iocapture(f; throwerrors=true, color=false) Runs the function `f` and captures the `stdout` and `stderr` outputs without printing them in the terminal. Returns an object with the following fields: @@ -21,9 +21,9 @@ The behaviour can be customized with the following keyword arguments: via the `.value` field (with also `.error` and `.backtrace` set accordingly). If set to `:interrupt`, only `InterruptException`s are rethrown. -* `color`: if set to `true` (default), `iocapture` inherits the `:color` property of - `stdout` and `stderr`, which specifies whether ANSI color/escape codes are expected. This - argument is only effective on Julia v1.6 and later. +* `color`: if set to `true`, `iocapture` inherits the `:color` property of `stdout` and + `stderr`, which specifies whether ANSI color/escape codes are expected. This argument is + only effective on Julia v1.6 and later. # Extended help @@ -53,7 +53,7 @@ It is also possible to set `throwerrors = :interrupt`, which will make `iocaptur only `InterruptException`s. This is useful when you want to capture all the exceptions, but allow the user to interrupt the running code with `Ctrl+C`. """ -function iocapture(f; throwerrors::Union{Bool,Symbol}=true, color::Bool=true) +function iocapture(f; throwerrors::Union{Bool,Symbol}=true, color::Bool=false) # Currently, :interrupt is the only valid Symbol value for throwerrors if isa(throwerrors, Symbol) && throwerrors !== :interrupt throw(DomainError(throwerrors, "Invalid value passed for throwerrors")) @@ -66,9 +66,9 @@ function iocapture(f; throwerrors::Union{Bool,Symbol}=true, color::Bool=true) # Redirect both the `stdout` and `stderr` streams to a single `Pipe` object. pipe = Pipe() Base.link_pipe!(pipe; reader_supports_async = true, writer_supports_async = true) - if VERSION >= v"1.6.0-DEV.481" # https://github.com/JuliaLang/julia/pull/36688 + @static if VERSION >= v"1.6.0-DEV.481" # https://github.com/JuliaLang/julia/pull/36688 pe_stdout = IOContext(pipe.in, :color => get(stdout, :color, false) & color) - pe_stderr = IOContext(pipe.in, :color => get(stdout, :color, false) & color) + pe_stderr = IOContext(pipe.in, :color => get(stderr, :color, false) & color) else pe_stdout = pipe.in pe_stderr = pipe.in diff --git a/test/runtests.jl b/test/runtests.jl index b393cdf..40fa1c4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -65,7 +65,7 @@ end @test c.value === nothing # Colors get discarded - c = iocapture(color=false) do + c = iocapture() do printstyled("foo", color=:red) end @test !c.error @@ -73,7 +73,7 @@ end @test c.value === nothing # Colors are preserved if it's supported - c = iocapture() do + c = iocapture(color=true) do printstyled("foo", color=:red) end @test !c.error @@ -85,7 +85,7 @@ end @test c.value === nothing # This test checks that deprecation warnings are captured correctly - c = iocapture() do + c = iocapture(color=true) do println("println") @info "@info" f() = (Base.depwarn("depwarn", :f); nothing) From eff09b83aa94c17cec27fb7aafc80a676652460f Mon Sep 17 00:00:00 2001 From: kimikage Date: Tue, 27 Oct 2020 18:59:59 +0900 Subject: [PATCH 3/3] Update README.md Co-authored-by: Morten Piibeleht --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d906aa1..f9e3d5e 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,9 @@ redirect streams which get cleaned up at the end of the `iocapture` call. ### ANSI color/escape code -On Julia v1.6 and later, if the `color` keyword argument is set to `true`, the captured -output of `iocapture` inherits the `:color` property of the `stdout` or `stderr`. - -On the other hand, on Julia v1.5 or earlier, even if the `color` keyword argument is set to -`true`, no coloring will be applied. However, this limitation might be removed in the -future. +On Julia 1.5 and earlier, setting `color` to `true` has no effect, because the [ability to set `IOContext` attributes on +redirected streams was added in 1.6](https://github.com/JuliaLang/julia/pull/36688). I.e. on those older Julia versions +the captured output will generally not contain ANSI color escape sequences. ## Similar packages