How to remove PyPlot.jl from the distribution of a package that uses it

Maybe someone can give advice how to remove PyPlot from the distribution of a Julia package in which it is used, in particular from package ModiaMath. So, this means “PyPlot” should not be within the Project.toml file of ModiaMath. The reasons for this requirement are explained here. The behaviour should be:

  • If PyPlot is present in the actual environment, ModiaMath.plot(…) makes a PyPlot plot.
  • If PyPlot is not in the actual environment, ModiaMath.plot(…) shall give the information message that PyPlot is not installed.

The following test package demonstrates a solution in Julia 1.0.0:

module TestPlot1

export myplot

const PyPlotAvailable = fill(false,1)

function __init__()
    @eval Main begin
        try
            import PyPlot
        catch
            println("... PyPlot not installed (plot commands will be ignored)")
        end
    end

    PyPlotAvailable[1] = isdefined(Main, :PyPlot)
end
   
function myplot(x,y)
    if PyPlotAvailable[1]
        Main.PyPlot.plot(x,y)  
    else
        println("no plot, because PyPlot not installed")
    end
end

Once this package is added to the actual environment (]add TestPlot1), then it can be used as:

   using TestPlot1
   t=collect(0:0.01:10); y=sin.(t)
   myplot(t,y)

and either a plot appears (if PyPlot is in the current environment) or an information message is printed.

@ChrisRackauckas adviced to not use this unsafe implementation but use one that is based on package Requires.jl. I tried it, but until now failed to come up with a solution. It should be possible, because package Plots.jl has the feature above and uses Requires.jl (but I did not manage to understand how this is done in Plots.jl).

Here is one of my trials:

module TestPlot2

using Requires

export myplot

const PyPlotAvailable = fill(false,1)


function __init__()
    @require PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" begin
        println("... PyPlot will be loaded.")
        @eval begin
            import PyPlot
            global PyPlotAvailable[1] = true
            pyplot_plot(x,y) = PyPlot.plot(x,y)
        end
    end
end
   

function myplot(x,y)
    if PyPlotAvailable[1]
        pyplot_plot(x,y)  
    else
        println("no plot, because PyPlot not installed")
    end
end

end

However, this solution requires that using PyPlot has to be explicitely given in the REPL. Once this is done a warning message appears:

julia> using PyPlot
... PyPlot will be loaded.
┌ Warning: Package TestPlot2 does not have PyPlot in its dependencies:
│ - If you have TestPlot2 checked out for development and have
│   added PyPlot as a dependency but haven't updated your primary
│   environment's manifest file, try `Pkg.resolve()`.
│ - Otherwise you may need to report an issue with TestPlot2
└ Loading PyPlot into TestPlot2 from project dependency, future warnings for TestPlot2 are suppressed.

To summarize, can someone give advice for a safer implementation and without requiring that the user has to define “using PyPlot” in the REPL and without getting this warning message.

I would recommend to use plot recipes with Plots.jl (or soon with Makie) instead:

I would recommend to use plot recipes with Plots.jl (or soon with Makie) instead:

Typical applications of ModiaMath have several plot windows. For example: The first simulation generates three plot windows. The second simulation reuses these windows (clearing their previous content and adding the new results to these windows). This cannot be defined with Plots. Makie does not support multiple windows either but plans to support it in the future. To summarize, if multiple window support is needed, Plots and Makie are (today) no option.

1 Like

In your init function you don’t need to @eval begin import PyPlot since @require will already to the importing.

You might want to use @isdefined instead of relying on a global boolean.

You can make function pyplot_plot end a stub function defined outside of the __init__() function that errors and just add dispatches to it in the @require block. Then there’s no need for myplot since the function will naturally always exist but throw the error when plotting isn’t installed.

Some of what they are relying on and need is sadly part of the “never coming to Plots” set of functionality, but is part of the “can be done with Makie” set.

New try with your hints:

module TestPlot2

using Requires
export myplot

myplot(x,y) = @info "... myplot(...) ignored, since PyPlot not defined in current environment"

function __init__()
    @require PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" begin
        myplot(x,y) = PyPlot.plot(x,y)
    end
end

end

O.k. this code is nicer and works better. Example

   using TestPlot2
   using PyPlot

   t=collect(0:0.01:10); y=sin.(t)
   myplot(t,y)

gives a PyPlot plot. If using PyPlot is not present, the information message is printed that PyPlot is missing.

Is it possible to get rid of the using PyPlot command? “Somehow” this should be possible, because with Plots a using PyPlot needs not to be given, only using Plots.

Yet another try, by combining the two codes:

module TestPlot2

using Requires
export myplot

myplot(x,y) = @info "... myplot(...) ignored, since PyPlot not defined in current environment"

function __init__()
    @eval Main begin
        try
            import PyPlot
        catch
            println("... PyPlot not installed (plot commands will be ignored)")
        end
    end

    @require PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" begin
        myplot(x,y) = PyPlot.plot(x,y)
    end
end

end

This works now as desired.

1 Like

That solution should be good. There is probably something better than the try-catch though, probably some kind of isdefined use.

Unfortunately, the above approach leads to a warning when TestPlot2 is a package that is imported in another package TestPlot3:

module TestPlot3
    import TestPlot2
end

When starting Juila (1.0.1) and giving the command import TestPlot3 a warning occurs:

julia> import TestPlot3
[ Info: Recompiling stale cache file HOME\.julia\compiled\v1.0\TestPlot3\VW81c.ji for TestPlot3 [a2f0c1e0-c87b-11e8-286b-c5f3dd565246]
WARNING: eval from module Main to TestPlot3:
Expr(:block, #= Symbol("HOME\.julia\dev\TestPlot2\src\TestPlot2.jl"):10 =#, Expr(:try, Expr(:block, #= Symbol("HOME\.julia\dev\TestPlot2\src\TestPlot2.jl"):11 =#, Expr(:import, Expr(
:., :PyPlot))), false, Expr(:block, #= Symbol("HOME\.julia\dev\TestPlot2\src\TestPlot2.jl"):13 =#, Expr(:call, :println, "... PyPlot not installed (plot commands will be ignored)"))))
  ** incremental compilation may be broken for this module **

How to get rid of this warning message?

Note, when leaving Julia and entering again and providing the command import TestPlot3, then no info message about rescompiling stale cache file is given, so the cached compiled version from the previous session is used; so incremental compilation seems to be o.k. and the warning message seems to be misleading.

I tried:

module TestPlot2

using Requires
export myplot

myplot(x,y) = println("... myplot(...) ignored, since PyPlot not defined in current environment")

function __init__()
    stdout_old = stdout
    stderr_old = stderr
    redirect_stdout()
    redirect_stderr()

    @eval Main begin
        import PyPlot
    end
    redirect_stdout(stdout_old)
    redirect_stderr(stderr_old)

    
    @require PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" begin
        myplot(x,y) = PyPlot.plot(x,y)
    end
end
end

When making import TestPlot3 (TestPlot3 contains an import TestPlot2), the output is:

julia> import TestPlot3
[ Info: Recompiling stale cache file HOME\.julia\compiled\v1.0\TestPlot3\VW81c.ji for TestPlot3 [a2f0c1e0-c87b-11e8-286b-c5f3dd565246]
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE
jl_uv_writecb() ERROR: broken pipe EPIPE

Besides these errors, it seems that plotting works, because the following command is successful:

TestPlot3.Testplot2.myplot(...)

Is there any other way to redirect the output to devnull?

O.k., finally found a solution that seems to work and not producing warning messages:

module TestPlot2

using Requires
export myplot

myplot(x,y) = println("... myplot(...) ignored, since PyPlot not available")

function __init__()
    if !Requires.isprecompiling()  
        @eval Main begin
            try
                import PyPlot
            catch
                println("... PyPlot not available (plot commands will be ignored)")
            end
        end
    end
    
    @require PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" begin
        myplot(x,y) = PyPlot.plot(x,y)
    end
end

end