From d5c3e01b844e34099fb160ceb956ad50587c8056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Mon, 21 Sep 2026 13:26:44 +0200 Subject: [PATCH 1/2] Fix extension name in the `Test` error hint `Base.get_extension` returns `nothing` for an unknown extension name, so querying `:InverseFunctionsTest` instead of `:InverseFunctionsTestExt` left the guard permanently true: "Did you forget to load Test?" was appended to every `test_inverse` `MethodError`, including those raised when `Test` is loaded and the extension method exists. Co-Authored-By: Claude Opus 5 (1M context) --- src/InverseFunctions.jl | 2 +- test/test_inverse.jl | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/InverseFunctions.jl b/src/InverseFunctions.jl index 4cffde1..0728ef2 100644 --- a/src/InverseFunctions.jl +++ b/src/InverseFunctions.jl @@ -38,7 +38,7 @@ if isdefined(Base, :get_extension) && isdefined(Base.Experimental, :register_err function __init__() Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ if exc.f === test_inverse && - (Base.get_extension(InverseFunctions, :InverseFunctionsTest) === nothing) + (Base.get_extension(InverseFunctions, :InverseFunctionsTestExt) === nothing) print(io, "\nDid you forget to load Test?") end end diff --git a/test/test_inverse.jl b/test/test_inverse.jl index 79cf725..6c01288 100644 --- a/test/test_inverse.jl +++ b/test/test_inverse.jl @@ -43,6 +43,25 @@ end InverseFunctions.test_inverse(inverse, log, compare = ===) end +@testset "test_inverse" begin + err = try + InverseFunctions.test_inverse(identity) + catch e + e + end + @test err isa MethodError + @test !occursin("Did you forget to load Test?", sprint(showerror, err)) + + # Test can't be unloaded, so the hint itself is checked in a subprocess + if isdefined(Base, :get_extension) + script = "using InverseFunctions; InverseFunctions.test_inverse(identity, 1)" + cmd = `$(Base.julia_cmd()) --startup-file=no --project=$(Base.active_project()) -e $script` + output = IOBuffer() + run(pipeline(ignorestatus(cmd); stdout=output, stderr=output)) + @test occursin("Did you forget to load Test?", String(take!(output))) + end +end + @testset "maths" begin InverseFunctions.test_inverse(!, false) From d51de21255d345e0eb2ec6da1b987db629bb0ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Mon, 21 Sep 2026 13:43:43 +0200 Subject: [PATCH 2/2] Let the `Test` extension implement an internal stub `test_inverse` is now a regular method of the package that forwards to `_test_inverse`, which `InverseFunctionsTestExt` implements. This removes `__init__` and with it the error hint: registering the hint cost 17-20 ms of load time on Julia 1.10 (~80% of it compilation), and an empty `__init__` costs the same, so the cost is having one at all. Precompilation gets cheaper too, since `__init__` also runs there. Arity errors now go through ordinary dispatch, so no internal method leaks into "Closest candidates", and the "load Test" message cannot be emitted while the extension is loaded. Unlike a `Base.get_extension` lookup, the stub needs no extension name literal -- the source of the bug fixed in the previous commit -- and stays statically resolvable, so it does not trip the `--trim` verifier. Behaviour change: a correct-arity call without `Test` now throws an `ArgumentError` instead of a `MethodError` carrying a hint, and a wrong-arity call without `Test` gets a plain `MethodError` with no hint at all. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 2 +- ext/InverseFunctionsTestExt.jl | 2 +- src/InverseFunctions.jl | 22 +++++++++------------- test/test_inverse.jl | 8 +++++--- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Project.toml b/Project.toml index b6acb6f..bd50f06 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "InverseFunctions" uuid = "3587e190-3f89-42d0-90ee-14403ec27112" -version = "0.1.17" +version = "0.1.18" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/ext/InverseFunctionsTestExt.jl b/ext/InverseFunctionsTestExt.jl index c130074..f6b4062 100644 --- a/ext/InverseFunctionsTestExt.jl +++ b/ext/InverseFunctionsTestExt.jl @@ -3,7 +3,7 @@ module InverseFunctionsTestExt using Test: @test, @testset using InverseFunctions: InverseFunctions, inverse -function InverseFunctions.test_inverse(f, x; compare=isapprox, kwargs...) +function InverseFunctions._test_inverse(f, x; compare=isapprox, kwargs...) @testset "test_inverse: $f with input $x" begin y = f(x) inverse_f = inverse(f) diff --git a/src/InverseFunctions.jl b/src/InverseFunctions.jl index 0728ef2..e84895a 100644 --- a/src/InverseFunctions.jl +++ b/src/InverseFunctions.jl @@ -26,23 +26,19 @@ The function tests (as a `Test.@testset`) if On Julia >= 1.9, you have to load the `Test` standard library to be able to use this function. """ -function test_inverse end +function test_inverse(f, x; kwargs...) + hasmethod(_test_inverse, Tuple{Any,Any}) || throw(ArgumentError( + "InverseFunctions.test_inverse requires the Test standard library: `using Test`." + )) + return _test_inverse(f, x; kwargs...) +end + +# implemented by InverseFunctionsTestExt +function _test_inverse end @static if !isdefined(Base, :get_extension) include("../ext/InverseFunctionsDatesExt.jl") include("../ext/InverseFunctionsTestExt.jl") end -# Better error message if users forget to load Test -if isdefined(Base, :get_extension) && isdefined(Base.Experimental, :register_error_hint) - function __init__() - Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ - if exc.f === test_inverse && - (Base.get_extension(InverseFunctions, :InverseFunctionsTestExt) === nothing) - print(io, "\nDid you forget to load Test?") - end - end - end -end - end # module diff --git a/test/test_inverse.jl b/test/test_inverse.jl index 6c01288..3a5b232 100644 --- a/test/test_inverse.jl +++ b/test/test_inverse.jl @@ -44,21 +44,23 @@ end end @testset "test_inverse" begin + @test !isdefined(InverseFunctions, :__init__) + err = try InverseFunctions.test_inverse(identity) catch e e end @test err isa MethodError - @test !occursin("Did you forget to load Test?", sprint(showerror, err)) + @test !occursin("requires the Test standard library", sprint(showerror, err)) - # Test can't be unloaded, so the hint itself is checked in a subprocess + # Test can't be unloaded, so the missing-Test error is checked in a subprocess if isdefined(Base, :get_extension) script = "using InverseFunctions; InverseFunctions.test_inverse(identity, 1)" cmd = `$(Base.julia_cmd()) --startup-file=no --project=$(Base.active_project()) -e $script` output = IOBuffer() run(pipeline(ignorestatus(cmd); stdout=output, stderr=output)) - @test occursin("Did you forget to load Test?", String(take!(output))) + @test occursin("ArgumentError: InverseFunctions.test_inverse requires the Test standard library", String(take!(output))) end end