using NLog; using Python.Runtime; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Python; /// /// Extracts command arguments from Python source file. /// public class ArgParser { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IPyRunner pyRunner; private string rootPath; private string moduleName; public ArgParser(IPyRunner pyRunner, string rootPath, string moduleName) { this.pyRunner = pyRunner; this.rootPath = rootPath; this.moduleName = moduleName; } /// /// Convert a PyObject to a value that can be used as a LaunchOption value. /// public static object PyObjectToOptionValue(PyObject obj) { var type = obj.GetPythonType().Name; return type switch { "bool" => obj.As(), "int" => obj.As(), "str" => obj.As(), _ => throw new ArgumentException($"Unknown option type {type}") }; } /// /// Convert a PyObject to a LaunchOptionType enum. /// public static LaunchOptionType? PyObjectToOptionType(PyObject typeObj) { var typeName = typeObj.GetAttr("__name__").As(); return typeName switch { "bool" => LaunchOptionType.Bool, "int" => LaunchOptionType.Int, "str" => LaunchOptionType.String, _ => null }; } public async Task> GetArgsAsync() { await pyRunner.Initialize(); return await pyRunner.RunInThreadWithLock(() => { using var scope = Py.CreateScope(); dynamic sys = scope.Import("sys"); dynamic argparse = scope.Import("argparse"); // Add root path to sys.path sys.path.insert(0, rootPath); // Import module var argsModule = scope.Import(moduleName); var argsDict = argsModule.GetAttr("__dict__").As(); // Find ArgumentParser object in module dynamic? argParser = null; var argParserType = argparse.ArgumentParser; foreach (var obj in argsDict.Values()) { if (obj.IsInstance(argParserType)) { argParser = obj; break; } } if (argParser == null) { throw new ArgumentException($"Could not find ArgumentParser object in module '{moduleName}'"); } // Loop through arguments var definitions = new List(); foreach (var action in argParser._actions) { var name = (action.dest as PyObject)?.As(); if (name == null) { throw new Exception("Argument option did not have a `dest` value"); } var optionStrings = ((PyObject) action.option_strings).As(); var dest = (action.dest as PyObject)?.As(); // var nArgs = (action.nargs as PyObject)?.As(); var isConst = (action.@const as PyObject)?.IsTrue() ?? false; var isRequired = (action.required as PyObject)?.IsTrue() ?? false; var type = action.type as PyObject; // Bool types will have a type of None (null) var optionType = type == null ? LaunchOptionType.Bool : PyObjectToOptionType(type); if (optionType == null) { Logger.Warn("Skipping option {Dest} with type {Name}", dest, type); continue; } // Parse default var @default = action.@default as PyObject; var defaultValue = @default != null ? PyObjectToOptionValue(@default) : null; var help = (action.help as PyObject)?.As(); definitions.Add(new LaunchOptionDefinition { Name = help ?? name, Description = help, Options = new List { optionStrings[0] }, // ReSharper disable once ConstantNullCoalescingCondition Type = optionType ?? LaunchOptionType.Bool, DefaultValue = defaultValue, MinSelectedOptions = isRequired ? 1 : 0, }); } return definitions; }); } }