-
-
Notifications
You must be signed in to change notification settings - Fork 268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal for first class support of conditional dependencies in Pkg #1285
Comments
Thank you very much for writing this up. I have a use case in LogDensityProblems.jl which I am wondering about. Specifically, both the glue code for working with ForwardDiff and ReverseDiff relies on DiffResults to extract gradients. Currently this is handled by a code that looks like function __init__()
@require DiffResults="163ba53b-c6d8-5494-b064-1a9d43ac40c5" include("DiffResults_helpers.jl")
@require ForwardDiff="f6369f11-7733-5829-9624-2563aa707210" include("AD_ForwardDiff.jl")
@require ReverseDiff="37e2e3b7-166d-5795-8a7a-e32c996b4267" include("AD_ReverseDiff.jl")
end so, for the purposes of Would this continue to work? For the mechanism you propose, I imagine I could just provide deps information for DiffResults. Generally, how is it handled when glue code needs other modules which themselves would not trigger glue code of their own? Can we still specify eg compat bounds for them? |
If I understand your example correctly, you would just have to declare a conditional dep on DiffResults (and ForwardDiff + ReverseDiff). |
Thanks. So, if I do that, then eg it would be triggered by |
Yes. |
What if I want to define a glue module to be loaded when both CuArrays and OrdinaryDiffEq are imported? That is to say, can there be something equivalent to the following? function __init__()
@require CuArrays="..." begin
@require OrdinaryDiffEq="..." include("glue.jl")
end
end I guess a possible API would be to include (say) [conditional-deps]
CuArrays = "..."
OrdinaryDiffEq = "..."
[compat]
CuArrays = "..."
OrdinaryDiffEq = "..."
[on-import]
foo = ["CuArrays", "OrdinaryDiffEq"] which tells the loader to include |
This sounds fantastic. Is this a feature that would be available in a Julia 1.x release, e.g. Julia 1.4 or Julia 1.5? Or would it have to wait until Julia 2.0? |
I like the first one more. Listing the conditional dependencies under |
Some Julia 1.x.
Yeah, I thought about this a little bit too. A first implementation of this might not support this but we should probably make sure that adding it will not be awkward. |
Triaging to discuss what to do about multiple conditional dependencies (which kind of starts to sound like "features"). |
Multiple conditional dependencies (2, 3, or even more than 3 packages
required for the glue code to be loaded) is definitely a use case for me!
Looking forward to seeing what triage thinks!
…On Fri, Aug 16, 2019 at 10:53 Kristoffer Carlsson ***@***.***> wrote:
Triaging to discuss what to do about multiple conditional dependencies
(which kind of starts to sound like "features").
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#1285?email_source=notifications&email_token=ABK4BLJHIYIGLM2FZQR3MVDQE25OJA5CNFSM4IJEA2D2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD4O2SXY#issuecomment-522037599>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABK4BLI2CGSXV24O7MRRMLDQE25OJANCNFSM4IJEA2DQ>
.
|
Regarding to multiple deps, maybe we could borrow something similar from rust-cargo (as features), it was included in this proposal: #977 |
AFAIU, the difference between features and conditional dependencies is that a feature is something that someone opts into from the current active Project while a conditional dependency is automatically "activated" whenever it's requirements are satisfied. |
I'm going to make an alternate proposal here. First: I think we should not call these conditional dependencies. They're NOT dependencies—they are packages that glue other packages together and are loaded automatically when the set of packages that they glue together are loaded. They depend on the packages that they glue together, not the other way around! This is crucial. So instead, I propose that we call them "glue packages". Here's how we could specify them in a package's name = "P"
uuid = "<uuid>"
[deps]
# P's dependencies here
# glue with a single dependency
[glue]
A = "<uuid>" # source at `glue/A.jl`
B = "<uuid>" # source at `glue/B.jl`
# glue with multiple dependencies
[glue.CD] # source at `glue/CD.jl`
C = "<uuid>"
D = "<uuid>" There's a few ways that we can go with the module P_A
import P, A
include("glue/A.jl")
end Then inside of the file module P_CD
import P, C, D
include("glue/CD.jl")
end Now, the actual loading would work like this: when all of the packages |
I agree. I'm using I think one benefit of formalizing it as hooks is that code loading can respond to other "conditions" like feature flags like @Roger-luo is suggesting. [extras]
CuArrays = "..."
Zygote = "..."
[hook.GPUImpl]
import = ["CuArrays", "Zygote"]
feature = ["GPU"] which indicates that I'm not suggesting adding feature flag support right now. I just thought this format is more extensible. |
I very intentionally don't want this mechanism to be too flexible. I don't want anything besides loading a glue package to happen when a set of packages are loaded. Of course, that's not very restrictive since loading a package can execute arbitrary code, but it does mean that there's a module that results which can be precompiled and saved—and that not being the case is precisely what's so problematic with the current requires system. Having arbitrary import hooks are likely to have all the problems that requires currently has. I also very much do not want this to be a mechanism for changing the behavior of packages. The only liberty that a glue package should take is that it can define methods (and types, I guess) for that depend on the types the packages that it glues together. So, it would be considered type piracy for a normal package A that depends on B and C to define I don't know how we should handle package features like what Roger wants, but it must not be this, or we will completely screw up the ability of this feature to fix the current precompilation issues. |
It was not my intention to suggest introducing any events for the hooks more dynamic than code loading events. If you think the term "hook" suggest features more dynamic than what is already possible by what you are suggesting, it probably is not the best term to use. But (temporal) dynamism and flexibility are different and feature flags can be implemented in very static manner. For example, if |
Actually, let me take back my earlier comment. Feature flags can be turned into |
We need to be able to give
It's pretty ugly with all the |
Just allow anything that appears in a glue stanza in the normal |
To elaborate, I think it would be confusing to use clashing names across glue packages so I think it's sane for them to have to match and it doesn't make sense for compat bounds not to match across glue packages, so we can just put glue bounds in |
How would testing "glued packages" look? Could there be something similar for a |
I'm not sure "Don't care about supporting that use" is resolved by this. If the maintainers don't want to maintain glue code for a specific package, they won't want to do so with more first-class glue code either. |
I think the idea is they wouldn't have to be involved at all? the conditional glue can just be maintained with the lightweight base package, but without the current overheads of doing that. |
I'm not sure this is distinct from the proposals above, but timholy/SnoopCompile.jl#253 wants JET = "$UUID_JET" "if julia.VERSION >= 1.6" (SnoopCompile already loads different functionality depending on the Julia version.) |
Just to add, another place this comes up in the various scenarios Lyndon outlined in #1285 (comment) besides with plot recipes or chain rules is with serialization packages like StructTypes or ArrowTypes. Those are lightweight packages to define how to serialize Julia objects to various targets, but upstream packages may not want them for one of the reasons Lyndon mentioned. I would love it if every package that defines a struct also defined several robust serialization options like JSON or Arrow so that serialization "just worked" the way plotting sometimes does, and I think glue packages can help fill the holes there. Also, one other reason upstream may not want to add a light dependency is compat; if the light dep requires a later Julia version than the package wants to support, then it might not want to add it. That's a scenario where a glue package would work great, since if you're loading both packages then you must be on a compatible Julia version. (Though I guess this is pretty much when Tim said in #1285 (comment)). |
So this issue is now 3 years old and still has many other open issues connected. Is there any path or timeline forward on resolving this? |
Just in case anyone here missed it, there's a proposal at JuliaLang/julia#47040 |
I think this is fixed now with the new "extension" functionality in Pkg. |
This is a proposal for adding first class support in Pkg and the code loading system for conditional dependencies.
What is a conditional dependency
Desribing a conditional dependency is easiest with an example. A typical concrete example is for a plotting package to add support for plotting e.g. DataFrames (by adding some method
plot(::DataFrame)
) but not require a user to installDataFrames
to use the plotting package. The plotting package wants to run a bit of extra code (the part that defines the method) when the conditional dependencyDataFrames
are somehow "available" to the user. The extra code that the package executes when the conditional dependency is active is called "glue code".Current way of doing conditional dependencies
The way people implement conditional dependencies right now is by using Requires.jl. It works by registering a callback that evaluates some code with the package loading code in Base. The callback gets executed when the conditional dependency is loaded (by e.g. comparing UUID), the code from the callback is evaluated into the module and the functionality for the conditional dependency is provided.
As an example usage:
What is the problem with Requires.jl
There are a few reasons why the current strategy using Requires.jl to deal with this is unsatisfactory.
include
-ing some file when the conditional dependnecy is available. Requires.jl runs inside__init__
which means the code evaluated by theinclude
command does not end up in the precompile file.@require
performance JuliaPackaging/Requires.jl#39)The current proposal
How does declare a conditional dependency
One declares a conditional dependency by adding an entry to the Project.toml file as:
An alternative possibility is to just put
DataFrames
inside[deps]
and then have a list of names that are conditional.Where should the glue code be stored?
Precompilation works on a module granularity so we want a module containing the glue code for each conditional dependency. The gluecode would be stored (based on a documented convention) in a file inside the package, eg
src/DataFramesGlue.jl
insidePlots
where the exact name of the file is yet to be decieded.An example of a glue file for Plots conditionally depending on DataFrames is:
How is the glue code loaded?
When
DataFrames
gets loaded, we check all packages that declares a conditional dependency with it. If the version ofDataFrames
loaded is compatible with the compat entry for a package withDataFrames
as a conditional dependency, we load the glue code which will act like a normal package and precompile. We need to teach code loading some stuff about glue packages so it knows how to map the names inside the glue module to the UUIDs in the "main package".The fact that we are not trying to resolve a set of versions compatible with the conditional dependency avoids cases where we in general need to resolve in arbitrarily many times with potential of cycles.
The text was updated successfully, but these errors were encountered: