10
$\begingroup$

I recently shared some work with a colleague and realized that his having v12 caused issues, as I used some new-to-13 functions.

How can I put a requirement for a minimum version into my package?

Here's an example Futuristic.m which Exit[]s upon realizing that I don't have v14 yet.

BeginPackage["Futuristic`"]

amazingFeature::usage="Whoa, dude.  
The functionality of this amazingFeature requires the latest Mathematica";

Begin["`Private`"]

lowestAllowedVersionNumber=14;
If[$VersionNumber < lowestAllowedVersionNumber,
    (
        Print["Loading the Futuristic package failed.  
               You need version "<>ToString[lowestAllowedVersionNumber]];
        Exit[]
    )
]

amazingFeature[x_]:=2 x

End[]
EndPackage[]

If, from a front-end kernel I do

Get["~/Desktop/Futuristic.m"]

it prints the error message and exits.

However, exiting is extremely aggressive design. If I were in the middle of a calculation with a lot of definitions already built up, I'd be upset if the kernel exited.

Upon trying to load I want one of two things to happen:

  • If the user has a recent-enough version, just load as expected.
  • Otherwise:
    • Print a message telling the user that loading the Futuristic package failed, as above.
    • Quickly exit from the package definition.
    • Do not leave the user with any definitions from inside the Futuristic package at all. That includes (in the above example) lowestAllowedVersionNumber.

Is that possible? Is there a "standard" way to do it?

I thought one way would be to add a requirement to the BeginPackage statement, such as BeginPackage["Futuristic``",{"FunctionPoles``"}]. Since FunctionPoles is new in 13, if it's available then the user has v13. Trying BeginPackage["Futuristic``",{"FancyNewv14Function``"}] does produce error messages, but doesn't prevent the package from being loaded, amazingFeature is still available. I can wrap it in Check and exit,

Check[BeginPackage["Futuristic`","FancyNewv14Context`"],Exit[]];

but this still uses the aggressive Exit[], rather than gracefully preventing the rest of the package from being processed.

$\endgroup$
5
  • 8
    $\begingroup$ Distribute your package as a paclet. Then you can include the minimum version as metadata. The paclet will not load in incompatible versions. You really should do this, but if for some reason you don't want to, you can check $VersionNumber and abort if it's too low. $\endgroup$
    – Szabolcs
    Jun 22, 2022 at 21:23
  • 2
    $\begingroup$ Example: github.com/szhorvat/IGraphM/blob/master/IGraphM/PacletInfo.m#L8 The more modern way is to use WolframVersion, but MathematicaVersion will be recognized by even very old Mathematica (including version 9), which is why I prefer it. Example of a more rudimentary check: github.com/szhorvat/IGraphM/blob/master/IGraphM/Kernel/… $\endgroup$
    – Szabolcs
    Jun 22, 2022 at 21:26
  • 1
    $\begingroup$ A sternly worded warning message might be nicer, in case someone with lower versions can still squeeze out some functionality. $\endgroup$
    – Chris K
    Jun 22, 2022 at 23:23
  • $\begingroup$ @Szabolcs from looking at the Kernel/init.m in the IGraphM package you do something similar when testing for $CloudEvaluation. I didn't know about Abort[]. Then I can do Check[BeginPackage["Futuristic``","FancyNewv14Context``"],EndPackage[];Print["Message :("]; Abort[]];. But perhaps now is a good time to learn the whole paclet business... $\endgroup$
    – evanb
    Jun 23, 2022 at 9:24
  • 1
    $\begingroup$ @ChrisK Well, it depends. With packages that use LibraryLink, there is a risk of a kernel crash in older versions. Older Mathematica versions simply crashes when trying to load a library compiled for a newer version. Luckily this was fixed, but there are other crash-risky ways of failure $\endgroup$
    – Szabolcs
    Jun 26, 2022 at 14:37

1 Answer 1

9
$\begingroup$

I second the suggestion of Szabolcs, in that paclets provide a clean declarative way of WL version - aware distribution of your package. You can have multiple paclets for the same package, with different paclet version and minimal WL version indicated in their PacletInfo file, and paclet manager will automatically pick the paclet with maximal paclet version, compatible with WL version requirements, at installation time.

OTOH, using Abort[] doesn't look like a good solution in this case, since it is too disruptive.


Should you insist on your suggested scheme, I can offer some solution, which should work when the package is loaded with Get or Needs from a file. It will not work when the package is evaluated interactively in the FrontEnd, however.

Here is a tiny package AbortPackageLoad.m, which you can place next to your own package, or anywhere else on $Path:

BeginPackage["AbortPackageLoad`"]

AbortPackageLoad::usage = 
"AbortPackageLoad[context, metadata] attempts to gracefully abort loading \
of a package from where it is called. The second optional argument may \
contain the metadata association, describing the specific reason for abort \
and associated information."

AbortPackageLoad::genfail = 
"Loading of package `1` failed for certain internal reason";

AbortPackageLoad::invldver = 
"Loading of package `1` failed. You need version `2`";


Begin["`Private`"]

$reasonMessageMap = <|
  "general" -> Function[Message[AbortPackageLoad::genfail]]
  ,
  "invalid_version" -> Function[
    {context, meta},
    Message[AbortPackageLoad::invldver, context, meta["RequiredVersion"]]
  ]
|>

removeContext[cont_String] /; MemberQ[Contexts[], cont] := 
  Quiet @ Remove[Evaluate[cont <> "*"]]

AbortPackageLoad[context_][metadata_ : <|"FailureReason" -> "general"|>] :=
  With[
    {private = context <> "Private`"}
    ,
    $reasonMessageMap[Lookup[metadata, "FailureReason", "general"]][
      context, metadata
    ];
    If[$InputFileName =!= "",
      Scan[removeContext, {context, private}];    
      Unprotect[$Packages];
      $Packages = DeleteCases[$Packages, context];
      Protect[$Packages];
      If[$Context === private, End[]];
      If[$Context === context, EndPackage[]];
      $ContextPath = DeleteCases[$ContextPath, context];
      Return[$Failed, Get]
    ];
]

End[]

EndPackage[]

Here is one way to use it in your package:

BeginPackage["Futuristic`"]

amazingFeature::usage = "Whoa, dude.  
The functionality of this amazingFeature requires the latest \
Mathematica";

`Private`abort = AbortPackageLoad`AbortPackageLoad[$Context];

Begin["`Private`"]

Needs["AbortPackageLoad`"]

lowestAllowedVersionNumber = 14;

If[$VersionNumber < lowestAllowedVersionNumber, 
  abort[<|
    "FailureReason" -> "invalid_version", 
    "RequiredVersion" -> "14+"
  |>]
];

amazingFeature[x_] := 2 x

End[]

EndPackage[]

EDIT

In response to the suggestion by Szabolcs in comments, I have modified the AbortPackageLoad function, so that it can also be used at the very top of the package file, before BeginPackage[] statement - if that's desirable / preferable. Or even inside the package, after BeginPackage[] but before Begin["`Private`"]. This is how it could be used in this fashion:

Block[{$ContextPath},
  Needs["AbortPackageLoad`"];
  If[$VersionNumber < 14, 
    AbortPackageLoad`AbortPackageLoad["Futuristic`"][<|
      "FailureReason" -> "invalid_version", 
      "RequiredVersion" -> "14+"
    |>]
  ];
]

BeginPackage["Futuristic`"]

...

EndPackage[]

END EDIT


The way AbortPackageLoad works is to clean and remove the public and private contexts of the package being loaded, then clean up global variables $Package and $ContextPath, ensure that package contexts are properly closed (so that $Context will point back to whatever context was there before loading your package), and finally, crucially, uses two-argument Return to return from Get early. Since Needs calls Get internally, this will work for Needs too.

When package is loaded from file, this mechanism works, because the file is parsed line by line, and complete expressions further in the file are not sent to evaluation early. When you execute the package from a FrontEnd cell, this will not work, because expressions further in the cell will still be parsed and send for evaluation. There are some ways to cancel the rest of evaluation queue, but I wasn't convinced that they are robust enough for this use case, so I excluded FrontEnd evaluation use case with the check If[$InputFileName =!= "", ...].

I don't see obvious cases where the above solution will break for non-MX packages loaded with Get / Needs, but OTOH I would still hesitate to use it in production code.

$\endgroup$
2
  • 3
    $\begingroup$ Wouldn't it be better to do the version check before the BeginPackage? Then it wouldn't be necessary to run End[] and EndPackage[] within the abort function. I had an abort function that did this in the past, but then I just switched to doing the check before the BeginPackage, most conveniently before Kernel/init.m. The tricky part is that such a check must only use built-in symbols, otherwise it will create new symbols in the Global` context. Even With[{x = ...}, x^2] or similar would create x in Global` . $\endgroup$
    – Szabolcs
    Jun 26, 2022 at 14:35
  • 1
    $\begingroup$ @Szabolcs In this particular case, you are probably right. And of course also in general, your suggestion about using paclets is probably the best solution here. I was trying to match the request of the OP as close as possible, and was assuming that the version information for the check might only become available at some point during the loading of the package. I have slightly modified the implementation of AbortPackageLoad, so that it can be also used in the fashion you suggested. $\endgroup$ Jun 26, 2022 at 16:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.