Temporarily inject development dependencies into package

Typically for development packages, including the defaults such as InteractiveUtils, we install them into the global environment, and are able to access them from the REPL, no matter what project we are in. Sometimes rather than executing code in the REPL, it’s easier to modify some file within a project temporarily, just to get some information. In this case, I would like to modify part of my package to have using InteractiveUtils: @code_warntype and then use @code_warntype. Currently, to do so, I have to add InteractiveUtils as a package dependency, but I would quite like to be able to specify that I want certain packages to have access to the global environment in the environment stack, e.g. like setting JULIA_DEBUG='mypackage' but instead JULIA_REPLENV='mypackage' or similar.

Is there currently a better/existing way to achieve this? Would it be possible to have some startup code or a small utility package like TestEnv to do this?

4 Likes

Interested in the answer too, since it’s quite relevant for Infiltrator.jl as well

The Infiltrator documentation has a tip about this, that I find incredibly useful: the following snippet allows package code to access Infiltrator when it’s loaded in an interactive session (even though it’s not declared as a dependency of the package itself)

if isdefined(Main, :Infiltrator)
  Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__)
end

I think the technique could be extended to many similar situations. For example, it should be possible to access @code_warntype from InteractiveUtils using something like this:

if isdefined(Main, :InteractiveUtils)
    interactiveutils = getproperty(Main, :InteractiveUtils)
    @eval $(interactiveutils).@code_warntype foo(x, y)
end
6 Likes

Yeah I know the tip but I get tired of copypasting this after a while :rofl:

Can we make a macro for it?
And then put that macro in startup.jl?

2 Likes

I think the technique could be extended to many similar situations. For example, it should be possible to access @code_warntype from InteractiveUtils using something like this:

if isdefined(Main, :InteractiveUtils)
   interactiveutils = getproperty(Main, :InteractiveUtils)
   @eval $(interactiveutils).@code_warntype foo(x, y)
end

If I try this, I get an error like the following at import time

ERROR: LoadError: ArgumentError: Package FooPackageDependency not found in current path.
- Run `import Pkg; Pkg.add("FooPackageDependency")` to install the FooPackageDependency package.
Stacktrace:
 [1] macro expansion
   @ ./loading.jl:1630 [inlined]
 [2] macro expansion
   @ ./lock.jl:267 [inlined]
 [3] require(into::Module, mod::Symbol)
   @ Base ./loading.jl:1611
 [4] include(fname::String)
   @ Base.MainInclude ./client.jl:478
 [5] top-level scope
   @ none:1

Where I’m importing Package which is where I’m pasting the snippet, which has FooPackageDependency in its dependencies, but I’m in an environment with Package in the dependencies and not FooPackageDependency.

I guess this is probably because I’m trying to use a macro which makes stuff a bit more difficult.

Can we make a macro for it?
And then put that macro in startup.jl?

I think maybe we could, at least for the calling a function from a module imported into Main case, but then I think we may have the same problem, the macro is defined in Main and we want to have it in MyPackage.

Perhaps if there was some way to hack at the MyPackage’s stack before `MyPackage is loaded? – But I don’t know whether this is even possible/makes sense/where to start. Also I guess this might trigger a bunch of recompilation.

1 Like

We could put it into Base via @eval in the startup.jl
see:

Since Base is accessibly everywhere we can then use it from what ever package we like

julia> module Bar
       bar()=Base.foo(100)
       end
Main.Bar

julia> @eval Base foo(x)=x
foo (generic function with 1 method)

julia> Bar.bar()
100

It won’t cause any recompilation as it doesn’t invalidate anything, since it isn’t used anywhere.

1 Like

I just write Main.@code_warntype foo(x, y) or whatever in the package code and it works fine.

Does it always work, or only in interactive contexts (where InteractiveUtils is automatically loaded)?


I’m not sure it’s necessary to put anything in Base, because Main seems to be accessible everywhere as well. For example, something like this seems to work (from my very limited testing):

# startup.jl
macro infiltrate_from_main()
    file = String(__source__.file); @show file
    line = __source__.line; @show line
    quote
        if isdefined(Main, :Infiltrator)
            Main.infiltrate($__module__, Base.@locals, $file, $line)
        end
    end
end
# package code
module Foo
function foo()
   Main.@infiltrate_from_main
end
end #module

Could it be because precompilation is not happy with such constructs?

It seems (again, from very limited testing) that when debugging, it’s a bit more reliable to first load the package to be debugged, unaltered. And only afterwards modify its sources to incorporate calls to Infiltrator/InteractiveUtils/whatever, relying on Revise to make things work.

1 Like

Only if InteractiveUtils is loaded.