Accessing non-exported package extension functions

If module A has an extension B with a function b_fun(), how can I call this function? I was assuming I could do:

using A
using A.B: b_fun

b_fun()

The namespacing for package extensions has me a little confused.

1 Like

Ahh, I guess the answer is I need to do

ext = Base.get_extension(A, :B)
ext.b_fun()
1 Like

Key takeaways:

  • extensions are not submodules of their dependency packages
  • extensions are intended to extend names in packages, not to be imported and expose new names. If you want that, make a new package with the same dependency packages.
  • get_extension and assigning an extension’s properties to variables do not behave like imports and are not intended for routine use. Reflection and select testing seems fine.
3 Likes

Is it that package extensions aren’t intended to or can’t export new names?

They can’t

3 Likes

Thanks, a workaround I found was to do something like this where A is the base package and B is the extension:

module A
  newfunction() = error("This function is available
   after importing B and calling `newfunction` with 
  arguments x/y/z...")

export newfunction
end

And in B:

module B
using A
A.newfunction(x,y,z) = ...
end

That’s not a workaround, that’s exactly how package extensions are supposed to work :wink:

3 Likes

To extend Michaels answer – that is how packages are supposed to work – you could also leave the error-part to Julia itself (leading to a nicer Method not found error including suggestions in case you just misspelled it or missed a signature)

module A
    @doc """
    newfunction()

    This function is meant to provide functionality when both A and (package-from-extension) are loaded and then provides the functionality that... 
    """ 
    newfunction()

export newfunction
end

in my experience the error messages you get this way are more helpful.

1 Like

Shouldn’t that rather be:

function newfunction end

?

1 Like

I think both should work, but yours is indeed nicer.

I came to my solution, since usually I document a certain signature of a function.

2 Likes

I’d suggest not doing this. It’s not unusual for another package or extension to add methods to a public function originating in a package, but it’s not great for the original package alone to expose a public function that uses up the zero-argument method only to advise the user to import something else. Note that a call with more arguments will only throw a MethodError, so a user would have to guess at an atypical practice of interactively calling the listed zero-argument method for the advice. The name and implementation seem more appropriate in a separate package that depends on the packages that the extension depended on. The intention wasn’t irrational; it is cleaner to limit the number of importable packages when many methods depend on various mixes of packages, and extensions accomplish that smoothly in addition to loading them only when needed. But going too far can erode the principle of module independence (at the extreme, you merge everything into one package), and no feature can substitute careful module design.

newfunction(args...; kwargs...) as a universal fallback might not be an absolute no-go, though. It all depends on the API of your package, how the error message gets displayed to the user, and whether you can come up with something more informative than a MethodError in the specific context of your package.

I’ve done something similar in my own package:

1 Like

The important difference there is your package does provide implementation in addition to the erroring fallback.

1 Like

I think it would make sense to document how extensions interact with namespacing and the practical implications (ie that they can’t export new symbols into their parent module, so different strategies are needed, as outlined in this thread).

I don’t see an open issue for this, I will wait for feedback before opening one though.

The correct way to think about them is like they are a separate package that gets automatically imported once all the “triggers” of it has been loaded and that you can get a handle to with get_extension.

So if you want you can do

BExt = Base.get_extension(A, :B)
using .BExt

if you want to get the exported things from the extension. But (just like with a separate package) it is not like its exported symbols will just appear somewhere else (like in the parent package)

I was under the impression that this was discouraged because the extension name (:B in this example) was not meant to be part of the public API of the “extended” package (A in the example). Am I mistaken?

Yes, you are “allowed” do this from the same places as you are allowed to use the internals of A.

1 Like