diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1a58b9..de9fbb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: if: ${{ true }} run: | Write-Output "***Locating iron python package!***" - if (Test-Path -Path "${{ github.workspace }}\DSIronPython3\package_output\DSIronPython3\bin\python3eval.dll") { + if (Test-Path -Path "${{ github.workspace }}\DSIronPython3\package_output\DSIronPython3\extra\python3eval.dll") { Write-Output "python node dll exists!" } else { Write-Error "python node dll was not found!" diff --git a/DSIronPython3Empty/Class1.cs b/DSIronPython3Empty/Class1.cs new file mode 100644 index 0000000..cac70a0 --- /dev/null +++ b/DSIronPython3Empty/Class1.cs @@ -0,0 +1,11 @@ +namespace DSIronPython3Empty +{ + /// + /// This class/dll exists so that Dynamo knows this package is loaded, + /// even though it really only contains an extension. + /// + internal class Class1 + { + + } +} diff --git a/DSIronPython3Empty/DSIronPython3Empty.csproj b/DSIronPython3Empty/DSIronPython3Empty.csproj new file mode 100644 index 0000000..b01ab1c --- /dev/null +++ b/DSIronPython3Empty/DSIronPython3Empty.csproj @@ -0,0 +1,12 @@ + + + + false + false + $(SolutionDir)\package_output\DSIronPython3\bin\ + net8.0 + enable + enable + + + \ No newline at end of file diff --git a/IronPython3Extension.sln b/IronPython3Extension.sln index d1ec540..c230114 100644 --- a/IronPython3Extension.sln +++ b/IronPython3Extension.sln @@ -7,7 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IronPython3Extension", "Iro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "python3eval", "python3eval\python3eval.csproj", "{2B256D7E-4864-4086-A481-6E5B24CBA95E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IronPython3Tests", "IronPython3Tests\IronPython3Tests.csproj", "{CD76A408-05CF-45E6-A508-6A6BA6A95E75}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IronPython3Tests", "IronPython3Tests\IronPython3Tests.csproj", "{CD76A408-05CF-45E6-A508-6A6BA6A95E75}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSIronPython3Empty", "DSIronPython3Empty\DSIronPython3Empty.csproj", "{AC582B53-98DA-48D3-97AC-DCDA17EAAFAA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,6 +29,10 @@ Global {CD76A408-05CF-45E6-A508-6A6BA6A95E75}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD76A408-05CF-45E6-A508-6A6BA6A95E75}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD76A408-05CF-45E6-A508-6A6BA6A95E75}.Release|Any CPU.Build.0 = Release|Any CPU + {AC582B53-98DA-48D3-97AC-DCDA17EAAFAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC582B53-98DA-48D3-97AC-DCDA17EAAFAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC582B53-98DA-48D3-97AC-DCDA17EAAFAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC582B53-98DA-48D3-97AC-DCDA17EAAFAA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/IronPython3Extension/Extension.cs b/IronPython3Extension/Extension.cs index 95f435b..07e76b8 100644 --- a/IronPython3Extension/Extension.cs +++ b/IronPython3Extension/Extension.cs @@ -1,13 +1,19 @@ using System; using System.IO; +using System.Linq; using System.Reflection; +using System.Runtime.Loader; using Dynamo.Extensions; +using Dynamo.Graph.Workspaces; using Dynamo.Logging; +using Dynamo.PythonServices; namespace IronPython3Extension { public class IronPython3Extension : IExtension, ILogSource { + private const string PythonEvaluatorAssembly = "python3eval"; + #region ILogSource public event Action MessageLogged; @@ -29,9 +35,78 @@ public void Dispose() } - public void Ready(ReadyParams sp) + /// + /// Action to be invoked when the Dynamo has started up and is ready + /// for user interaction. + /// + /// + public void Ready(ReadyParams rp) + { + var extraPath = Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent.FullName, "extra"); + var alc = new IsolatedPythonContext(Path.Combine(extraPath, $"{PythonEvaluatorAssembly}.dll")); + var dsIronAssem = alc.LoadFromAssemblyName(new AssemblyName(PythonEvaluatorAssembly)); + + //load the engine into Dynamo ourselves. + LoadPythonEngine(dsIronAssem); + + //we used to do this: + //but it's not neccesary to load anything into the VM. + //instead we skip all the extra work and trigger the side effect we want + //which is re executing the graph after the dsIronPython evaluator is loaded into the PythonEngineManager. + //rp.StartupParams.LibraryLoader.LoadNodeLibrary(dsIronAssem); + + if (rp.CurrentWorkspaceModel is HomeWorkspaceModel hwm) + { + foreach (var n in hwm.Nodes) + { + n.MarkNodeAsModified(true); + } + hwm.Run(); + } + } + + private static void LoadPythonEngine(Assembly assembly) { + if (assembly == null) + { + throw new ArgumentNullException($"Error while loading python engine - assembly {PythonEvaluatorAssembly}.dll was not loaded successfully."); + } + + // Currently we are using try-catch to validate loaded assembly and Singleton Instance method exist + // but we can optimize by checking all loaded types against evaluators interface later + try + { + Type eType = null; + PropertyInfo instanceProp = null; + try + { + eType = assembly.GetTypes().FirstOrDefault(x => typeof(PythonEngine).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); + if (eType == null) return; + + instanceProp = eType?.GetProperty("Instance", BindingFlags.NonPublic | BindingFlags.Static); + if (instanceProp == null) return; + } + catch + { + // Ignore exceptions from iterating assembly types. + return; + } + PythonEngine engine = (PythonEngine)instanceProp.GetValue(null); + if (engine == null) + { + throw new Exception($"Could not get a valid PythonEngine instance by calling the {eType.Name}.Instance method"); + } + + if (PythonEngineManager.Instance.AvailableEngines.All(x => x.Name != engine.Name)) + { + PythonEngineManager.Instance.AvailableEngines.Add(engine); + } + } + catch (Exception ex) + { + throw new Exception($"Failed to add a Python engine from assembly {assembly.GetName().Name}.dll with error: {ex.Message}"); + } } public void Shutdown() @@ -44,4 +119,35 @@ public void Startup(StartupParams sp) } } + internal class IsolatedPythonContext : AssemblyLoadContext + { + private AssemblyDependencyResolver resolver; + + public IsolatedPythonContext(string libPath) + { + resolver = new AssemblyDependencyResolver(libPath); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + string assemblyPath = resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath != null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string libraryPath = resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (libraryPath != null) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + + return IntPtr.Zero; + } + } } diff --git a/IronPython3Extension/IronPython3Extension.csproj b/IronPython3Extension/IronPython3Extension.csproj index 882532d..ecef528 100644 --- a/IronPython3Extension/IronPython3Extension.csproj +++ b/IronPython3Extension/IronPython3Extension.csproj @@ -4,8 +4,8 @@ net8.0 false false - $(SolutionDir)\package_output\DSIronPython3\bin\ - 1.0.2 + $(SolutionDir)\package_output\DSIronPython3\extra\ + 1.4.0 @@ -21,4 +21,10 @@ + + + Always + + + diff --git a/IronPython3Extension/IronPython3Extension_ExtensionDefinition.xml b/IronPython3Extension/IronPython3Extension_ExtensionDefinition.xml new file mode 100644 index 0000000..e715bed --- /dev/null +++ b/IronPython3Extension/IronPython3Extension_ExtensionDefinition.xml @@ -0,0 +1,4 @@ + + IronPython3Extension.dll + IronPython3Extension.IronPython3Extension + \ No newline at end of file diff --git a/IronPython3Tests/pythonEvalTests.cs b/IronPython3Tests/pythonEvalTests.cs index bd9554f..e74ef99 100644 --- a/IronPython3Tests/pythonEvalTests.cs +++ b/IronPython3Tests/pythonEvalTests.cs @@ -1,4 +1,5 @@ using Dynamo; +using Microsoft.Extensions.Configuration; using NUnit.Framework; using System.Collections; @@ -46,6 +47,63 @@ public void BindingsWork() } } + [Test] + [Category("UnitTests")] + public void SysDiagProccess_AndOtherShimmedCLRTypesWork() + { + + + foreach (var pythonEvaluator in Evaluators) + { + var output = pythonEvaluator( + @" +import clr +from System.Reflection import Assembly +from System.Diagnostics import Process +dynamoCore = Assembly.Load(""DynamoCore"") +version_long = dynamoCore.GetName().Version.Major.ToString() +proc = Process.GetCurrentProcess().ProcessName +OUT = (version_long,proc) +", + new ArrayList(), + new ArrayList() + ); + Assert.AreEqual("3", (output as IList)[0]); + //we do this because the process name can change depending + //on where tests are running. + Assert.IsNotEmpty((string?)(output as IList)[1]); + } + } + [Test] + [Category("UnitTests")] + public void ConfigLoadShimsCanBeDisabled() + { + + var configb = new ConfigurationBuilder(); + var config = configb.AddInMemoryCollection(new Dictionary() + { + {"config:CONFIG_ENABLE_NET48SHIMCOMPAT","false" } + }).Build(); + + var evaluator = new IronPython3.Evaluator.IronPython3Evaluator(config); + + var e = Assert.Throws(() => + { + var output = evaluator.Evaluate( + @" +import clr +from System.Reflection import Assembly +from System.Diagnostics import Process +", + new ArrayList(), + new ArrayList() + ); + + }); + StringAssert.Contains("ImportError", e.Message); + } + + [Test] [Category("UnitTests")] public void DataMarshaling_Output() diff --git a/package_output/DSIronPython3/pkg.json b/package_output/DSIronPython3/pkg.json index d7566b7..7d01d95 100644 --- a/package_output/DSIronPython3/pkg.json +++ b/package_output/DSIronPython3/pkg.json @@ -2,8 +2,8 @@ "license": "", "file_hash": null, "name": "DynamoIronPython3", - "version": "1.0.0", - "description": "unofficial python package for the IronPython 3 engine + stdlib ", + "version": "1.4.0", + "description": "unofficial python package for the IronPython 3 engine + stdlib. If you are using Dynamo < 3.x use package version < 1.2 ", "group": "", "keywords": [ "python", "ironpython", "ironpython3" ], "dependencies": [], @@ -15,5 +15,5 @@ "site_url": "https://dynamobim.org/", "repository_url": "https://github.com/DynamoDS/Dynamo", "contains_binaries": true, - "node_libraries": ["python3eval"] + "node_libraries": [ "DSIronPython3Empty, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" ] } \ No newline at end of file diff --git a/python3eval/appsettings.json b/python3eval/appsettings.json new file mode 100644 index 0000000..eaeb979 --- /dev/null +++ b/python3eval/appsettings.json @@ -0,0 +1,5 @@ +{ + "config": { + "CONFIG_ENABLE_NET48SHIMCOMPAT": "true" + } +} diff --git a/python3eval/python3eval.csproj b/python3eval/python3eval.csproj index 05e8eeb..86d9b3c 100644 --- a/python3eval/python3eval.csproj +++ b/python3eval/python3eval.csproj @@ -4,9 +4,9 @@ false false net8.0 - $(SolutionDir)\package_output\DSIronPython3\bin\ + $(SolutionDir)\package_output\DSIronPython3\extra\ true - 1.0.2 + 1.4.0 @@ -25,18 +25,14 @@ + - - - - - - - - - - - + + + Always + + + diff --git a/python3eval/pythoneval.cs b/python3eval/pythoneval.cs index 1a2e612..ae17abe 100644 --- a/python3eval/pythoneval.cs +++ b/python3eval/pythoneval.cs @@ -7,18 +7,20 @@ using Autodesk.DesignScript.Runtime; using Dynamo.Events; using Dynamo.Logging; -using Dynamo.PythonServices; using Dynamo.PythonServices.EventHandlers; using Dynamo.Session; using Dynamo.Utilities; using IronPython.Hosting; using Microsoft.Scripting.Hosting; +using Microsoft.Extensions.Configuration; namespace IronPython3.Evaluator { [IsVisibleInDynamoLibrary(false)] public class IronPython3Evaluator:Dynamo.PythonServices.PythonEngine { + private bool CONFIG_ENABLE_NET48SHIMCOMPAT = false; + private const string DynamoPrintFuncName = "__dynamoprint__"; /// stores a copy of the previously executed code private static string prev_code { get; set; } @@ -63,6 +65,17 @@ public override object Evaluate(string code, IList bindingNames, IList bindingVa if (code != prev_code) { var pythonEngine = Python.CreateEngine(); + //to maintain compatability with ironPython code written in .netframework - we load the system.dll shim + //which loads many types which used to be in system.dll(mscorlib) like system.diagnostics.process + //which were moved in .Net. These types are now importable by python code without users needing to add + //clr.addreference. + //TODO consider a preference for this. + //TODO test smaller shims...netstd.dll + + if (CONFIG_ENABLE_NET48SHIMCOMPAT) + { + pythonEngine.Runtime.LoadAssembly(Assembly.Load("System")); + } if (!string.IsNullOrEmpty(stdLib)) { paths = pythonEngine.GetSearchPaths().ToList(); @@ -333,6 +346,26 @@ private static string PythonStandardLibPath() } return pythonLibDir; } + + public IronPython3Evaluator(IConfiguration config = null) + { + //either inject a config or load from appsettings.json + if (config is not null) + { + + } + else + { + var configPath = Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, "appsettings.json"); + config = new ConfigurationBuilder().AddJsonFile(configPath, optional: true).Build(); + } + var enableShimLoad = config.GetSection("config").GetChildren().FirstOrDefault(x => x.Key == nameof(CONFIG_ENABLE_NET48SHIMCOMPAT))?.Value; + if (bool.TryParse(enableShimLoad,out var parsed)) + { + CONFIG_ENABLE_NET48SHIMCOMPAT = parsed; + } + + } } }