From 147c300feb33e43d05dc5fa0bb0100ba4c2b0196 Mon Sep 17 00:00:00 2001 From: Ashish Aggarwal Date: Wed, 1 Apr 2026 16:19:06 -0400 Subject: [PATCH 1/3] Inject code converter and handle TypeLoadException Add an optional Func codeConverter to PythonMigrationAssistantViewModel and use it in MigrateCode to allow injecting a custom migrator (useful for testing). Catch TypeLoadException thrown when initializing the migration environment (e.g. incompatible Python.Runtime/pythonnet versions) and set the diff view to an Error state instead of crashing. Includes a unit test that verifies the view model shows Error when the migrator throws a TypeLoadException. --- .../PythonMigrationAssistantViewModel.cs | 21 +++++++-- .../PythonMigrationAssistantViewModelTests.cs | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs diff --git a/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs b/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs index dacf3422e06..a4a1a950df5 100644 --- a/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs +++ b/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs @@ -24,6 +24,7 @@ internal class PythonMigrationAssistantViewModel : NotificationObject private readonly Version dynamoVersion; private PythonNode PythonNode; + private readonly Func codeConverter; private IDiffViewViewModel currentViewModel; private SideBySideDiffModel diffModel; @@ -43,7 +44,7 @@ public IDiffViewViewModel CurrentViewModel set { this.currentViewModel = value; RaisePropertyChanged(nameof(this.CurrentViewModel)); } } - public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel workspace, IPathManager pathManager, Version dynamoVersion) + public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel workspace, IPathManager pathManager, Version dynamoVersion, Func codeConverter = null) { PythonNode = pythonNode; OldCode = pythonNode.Script; @@ -51,6 +52,7 @@ public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel w this.workspace = workspace; backupDirectory = pathManager.BackupDirectory; this.dynamoVersion = dynamoVersion; + this.codeConverter = codeConverter; try { @@ -62,7 +64,19 @@ public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel w diffModel = sidebyside.BuildDiffModel(OldCode, OldCode, false); SetSideBySideViewModel(); - + + CurrentViewModel.DiffState = State.Error; + return; + } + catch (TypeLoadException) + { + // Python.Runtime assembly at runtime is incompatible (e.g. pythonnet 2.x instead of 3.x): + // PyModule type does not exist in the older assembly. Show error state instead of crashing. + var sidebyside = new SideBySideDiffBuilder(); + diffModel = sidebyside.BuildDiffModel(OldCode, OldCode, false); + + SetSideBySideViewModel(); + CurrentViewModel.DiffState = State.Error; return; } @@ -74,7 +88,8 @@ public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel w private void MigrateCode() { - NewCode = ScriptMigrator.MigrateCode(OldCode); + var converter = codeConverter ?? ScriptMigrator.MigrateCode; + NewCode = converter(OldCode); var sidebyside = new SideBySideDiffBuilder(); diffModel = sidebyside.BuildDiffModel(OldCode, NewCode, false); diff --git a/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs b/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs new file mode 100644 index 00000000000..dc919c45cd7 --- /dev/null +++ b/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs @@ -0,0 +1,43 @@ +using System; +using Dynamo; +using Dynamo.Graph.Workspaces; +using Dynamo.PythonMigration.Differ; +using Dynamo.PythonMigration.MigrationAssistant; +using NUnit.Framework; +using PythonNodeModels; + +namespace DynamoPythonTests +{ + [TestFixture] + public class PythonMigrationAssistantViewModelTests : DynamoModelTestBase + { + /// + /// Regression test: when Python.Runtime is an incompatible version (e.g. pythonnet 2.x loaded + /// instead of 3.x), Py.CreateScope() throws TypeLoadException because PyModule does not exist + /// in that assembly. The ViewModel must catch this and show an error state rather than crashing. + /// + [Test] + public void WhenMigrationThrowsTypeLoadExceptionViewModelShowsErrorState() + { + // Arrange + var pyNode = new PythonNode(); + var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel; + var pathManager = CurrentDynamoModel.PathManager; + + Func brokenMigrator = _ => + throw new TypeLoadException("Could not load type 'Python.Runtime.PyModule' from assembly 'Python.Runtime, Version=2.5.2.12086'"); + + // Act + PythonMigrationAssistantViewModel viewModel = null; + Assert.DoesNotThrow(() => + { + viewModel = new PythonMigrationAssistantViewModel( + pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator); + }); + + // Assert + Assert.IsNotNull(viewModel); + Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState); + } + } +} From ac412c149171cad86aa6b602a8f42e09a092b126 Mon Sep 17 00:00:00 2001 From: Ashish Aggarwal Date: Fri, 3 Apr 2026 15:21:35 -0400 Subject: [PATCH 2/3] Consolidate migration error handling Extract SetMigrationErrorState to centralize error-state setup and use it for migration failures. Add XML docs to the constructor (including the test-only codeConverter param), replace duplicate error-handling code with a helper, and broaden the exception handling to catch TypeLoadException, MissingMethodException, FileLoadException, and BadImageFormatException. Add unit tests to verify MissingMethodException and FileLoadException are handled gracefully and import System.IO in the test file. --- .../PythonMigrationAssistantViewModel.cs | 45 ++++++++++------ .../PythonMigrationAssistantViewModelTests.cs | 51 +++++++++++++++++++ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs b/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs index a4a1a950df5..6d6d051b5e9 100644 --- a/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs +++ b/src/PythonMigrationViewExtension/MigrationAssistant/PythonMigrationAssistantViewModel.cs @@ -44,6 +44,18 @@ public IDiffViewViewModel CurrentViewModel set { this.currentViewModel = value; RaisePropertyChanged(nameof(this.CurrentViewModel)); } } + /// + /// Initializes the view model, runs the 2-to-3 code migration, and prepares the diff model. + /// If migration fails the view model is placed in an error state rather than propagating the exception. + /// + /// The Python node whose script will be migrated. + /// The workspace that contains the node. + /// Provides backup and application paths. + /// The running Dynamo version, used to scope disclaimer-dismiss files. + /// + /// Optional override for the migration function. Defaults to . + /// Intended for unit testing only. + /// public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel workspace, IPathManager pathManager, Version dynamoVersion, Func codeConverter = null) { PythonNode = pythonNode; @@ -60,24 +72,19 @@ public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel w } catch (PythonException) { - var sidebyside = new SideBySideDiffBuilder(); - diffModel = sidebyside.BuildDiffModel(OldCode, OldCode, false); - - SetSideBySideViewModel(); - - CurrentViewModel.DiffState = State.Error; + // Python script error during migration (e.g. syntax the 2to3 tool cannot parse). + SetMigrationErrorState(); return; } - catch (TypeLoadException) + catch (Exception ex) when ( + ex is TypeLoadException || + ex is MissingMethodException || + ex is FileLoadException || + ex is BadImageFormatException) { - // Python.Runtime assembly at runtime is incompatible (e.g. pythonnet 2.x instead of 3.x): - // PyModule type does not exist in the older assembly. Show error state instead of crashing. - var sidebyside = new SideBySideDiffBuilder(); - diffModel = sidebyside.BuildDiffModel(OldCode, OldCode, false); - - SetSideBySideViewModel(); - - CurrentViewModel.DiffState = State.Error; + // Python.Runtime assembly at runtime is incompatible (e.g. pythonnet 2.x instead of 3.x). + // Types or members such as PyModule may be absent in older assemblies. + SetMigrationErrorState(); return; } SetSideBySideViewModel(); @@ -95,6 +102,14 @@ private void MigrateCode() diffModel = sidebyside.BuildDiffModel(OldCode, NewCode, false); } + private void SetMigrationErrorState() + { + var sidebyside = new SideBySideDiffBuilder(); + diffModel = sidebyside.BuildDiffModel(OldCode, OldCode, false); + SetSideBySideViewModel(); + CurrentViewModel.DiffState = State.Error; + } + /// /// Replaces the code in the Python node with the code changes made by the Migration Assistant. /// diff --git a/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs b/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs index dc919c45cd7..55909253153 100644 --- a/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs +++ b/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Dynamo; using Dynamo.Graph.Workspaces; using Dynamo.PythonMigration.Differ; @@ -39,5 +40,55 @@ public void WhenMigrationThrowsTypeLoadExceptionViewModelShowsErrorState() Assert.IsNotNull(viewModel); Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState); } + + /// + /// Verifies that other assembly-mismatch exceptions (MissingMethodException, FileLoadException, + /// BadImageFormatException) from an incompatible Python.Runtime are also handled gracefully. + /// + [Test] + public void WhenMigrationThrowsMissingMethodExceptionViewModelShowsErrorState() + { + var pyNode = new PythonNode(); + var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel; + var pathManager = CurrentDynamoModel.PathManager; + + Func brokenMigrator = _ => + throw new MissingMethodException("Python.Runtime.Py", "CreateScope"); + + PythonMigrationAssistantViewModel viewModel = null; + Assert.DoesNotThrow(() => + { + viewModel = new PythonMigrationAssistantViewModel( + pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator); + }); + + Assert.IsNotNull(viewModel); + Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState); + } + + /// + /// Verifies that a FileLoadException (e.g. assembly version conflict) during migration + /// is handled gracefully and shows an error state. + /// + [Test] + public void WhenMigrationThrowsFileLoadExceptionViewModelShowsErrorState() + { + var pyNode = new PythonNode(); + var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel; + var pathManager = CurrentDynamoModel.PathManager; + + Func brokenMigrator = _ => + throw new FileLoadException("Could not load file or assembly 'Python.Runtime'"); + + PythonMigrationAssistantViewModel viewModel = null; + Assert.DoesNotThrow(() => + { + viewModel = new PythonMigrationAssistantViewModel( + pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator); + }); + + Assert.IsNotNull(viewModel); + Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState); + } } } From 98cb58c8f21bfb61cb8d09d5ef7f3d096d0c8ff3 Mon Sep 17 00:00:00 2001 From: Ashish Aggarwal Date: Mon, 6 Apr 2026 14:52:44 -0400 Subject: [PATCH 3/3] Update PythonMigrationAssistantViewModelTests.cs --- .../PythonMigrationAssistantViewModelTests.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs b/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs index 55909253153..cf933918119 100644 --- a/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs +++ b/test/Libraries/DynamoPythonTests/PythonMigrationAssistantViewModelTests.cs @@ -42,8 +42,8 @@ public void WhenMigrationThrowsTypeLoadExceptionViewModelShowsErrorState() } /// - /// Verifies that other assembly-mismatch exceptions (MissingMethodException, FileLoadException, - /// BadImageFormatException) from an incompatible Python.Runtime are also handled gracefully. + /// Verifies that a MissingMethodException (e.g. missing member in incompatible Python.Runtime) + /// during migration is handled gracefully and shows an error state. /// [Test] public void WhenMigrationThrowsMissingMethodExceptionViewModelShowsErrorState() @@ -90,5 +90,30 @@ public void WhenMigrationThrowsFileLoadExceptionViewModelShowsErrorState() Assert.IsNotNull(viewModel); Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState); } + + /// + /// Verifies that a BadImageFormatException (e.g. 32/64-bit or .NET target mismatch in + /// Python.Runtime) during migration is handled gracefully and shows an error state. + /// + [Test] + public void WhenMigrationThrowsBadImageFormatExceptionViewModelShowsErrorState() + { + var pyNode = new PythonNode(); + var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel; + var pathManager = CurrentDynamoModel.PathManager; + + Func brokenMigrator = _ => + throw new BadImageFormatException("The format of the file 'Python.Runtime' is invalid."); + + PythonMigrationAssistantViewModel viewModel = null; + Assert.DoesNotThrow(() => + { + viewModel = new PythonMigrationAssistantViewModel( + pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator); + }); + + Assert.IsNotNull(viewModel); + Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState); + } } }