Skip to content

Conversation

@ivaylo-matov
Copy link
Contributor

Purpose

Tis PR is response to DYN-9720 & DYN-9719.

This update makes Dynamo automatically create backups and upgrade Python nodes when a graph or custom node still uses CPython3.

When a graph (.dyn) with CPython3 nodes is opened, Dynamo saves a backup copy of the file in the Backup folder. It then updates all Python nodes in the file to use PythonNet3.

When the graph is saved, Dynamo also makes backups of any custom nodes that are part of it. These backups are copied to the backup folder. The original .dyf files in the package folder are updated in place so they now use PythonNet3.

DYN-9720

Check these if you believe they are true

Release Notes

Dynamo now automatically backs up and upgrades any graphs or custom nodes using CPython3 to PythonNet3.

Reviewers

@zeusongit
@DynamoDS/eidos

FYIs

@achintyabhat
@dnenov

ivaylo-matov and others added 30 commits September 3, 2025 09:42
cpython still present but does not load
Copilot AI review requested due to automatic review settings November 5, 2025 13:01
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the ticket for this pull request: https://jira.autodesk.com/browse/DYN-9720

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements automatic backup and migration of CPython3 nodes to PythonNet3 in Dynamo graphs and custom nodes. When a graph containing CPython3 nodes is opened, Dynamo creates a backup file and upgrades all Python nodes to PythonNet3, addressing the deprecation of the CPython3 engine.

  • Automatic detection and in-memory upgrade of CPython3 nodes to PythonNet3 when graphs are opened
  • Backup creation for both graphs (.dyn) and custom node definitions (.dyf) before migration
  • Toast notifications informing users about automatic upgrades and backup file locations

Reviewed Changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
PythonMigrationViewExtension.cs Core logic for detecting CPython nodes, performing temporary upgrades, managing workspace events, and displaying toast notifications
PythonEngineUpgradeService.cs New service class handling Python engine detection, in-memory node upgrades, and permanent migration commits with backup creation
Resources.resx Added localized strings for backup messages, node/definition labels, and combined upgrade notifications
Resources.en-US.resx English localization for new backup and upgrade notification strings
PythonMigrationAssistantViewModel.cs Added title parameter to backup notification message box
DynamoView.xaml.cs Added event handler cleanup for Python engine change notifications
DynamoViewModel.cs Reset CPython notification flag when clearing home workspace
Files not reviewed (1)
  • src/PythonMigrationViewExtension/Properties/Resources.Designer.cs: Language not supported
Comments suppressed due to low confidence (1)

src/PythonMigrationViewExtension/PythonMigrationViewExtension.cs:1

  • Corrected spacing in constructor parameter declaration.
using System;

ivaylo-matov and others added 4 commits November 5, 2025 13:03
fixed bug where:
- user opens graph A that contains custom node A (with cpython)
-> user sees notification and a backup file of the graph is saved
- user closes without saving
-> the graph or node are not permanently saved
- user opens graph B that contains the same custom node A
-> no notification or backup for graph B !!!
@zeusongit zeusongit requested a review from a team November 5, 2025 16:43
@zeusongit
Copy link
Contributor

@ivaylo-matov Please take a look at the updated message in the task comments. Also, this task: https://jira.autodesk.com/browse/DYN-9828 which I think can be included in this PR, but of not let me know, and you can submit another one for that and others.

@ivaylo-matov
Copy link
Contributor Author

ivaylo-matov commented Nov 7, 2025

@ivaylo-matov Please take a look at the updated message in the task comments. Also, this task: https://jira.autodesk.com/browse/DYN-9828 which I think can be included in this PR, but of not let me know, and you can submit another one for that and others.

Hi @zeusongit , I've addressed DYN-9828 in this PR.
Can you clarify which message you are referring to? Is it Jingyi's comment here: https://jira.autodesk.com/browse/DYN-9720 ? I thought we agreed to keep the backup notification only in the toast, am I wrong?

Copy link
Contributor

@benglin benglin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work as always, @ivaylo-matov! I debugged through your code halfway and added a few observations. I hope these help in some ways, I'll let the other members of the team take a closer look at it. 👍

@ivaylo-matov
Copy link
Contributor Author

@benglin , thank you very much for the review and the comments - super useful! 🙏

Copy link
Contributor

@benglin benglin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ivaylo-matov, thanks for accommodating the requested changes. I went on a short vacation right after leaving those comments, so I could only review them today. I was in a rush at that time and couldn't finish the review, but today I debugged through your latest code. I left some comments as I went through the code—if you could take a look at them, that'd be great.

While reviewing the latest changes, I found some behaviors that might be good candidates for fixing before we merge this. Generally, I think the code around notification setting/hiding could use some simplification. If we can find a way to more reliably track which workspace has shown its notifications, that would be ideal. Alternatively, we could consider always showing toast notifications when a workspace is upgraded to use PythonNet3, which might simplify the code further.

What I found with a new DYN file is shown in the video below. The notifications do show sometimes for the home workspace but are then hidden (maybe that's intentional). The notifications themselves are secondary -- what the video highlights is that the custom node workspace is not marked as "modified" when it's opened from the new workspace. This likely needs fixing before merging. As I mentioned, we could always show the notification toasts to simplify the code (please check with Achintya), but the custom workspace not being marked as modified is a higher priority.

cpython-upgrade-notifications.mp4

Thanks again for the great work, as always! Looking forward to the updated code—please let me know.

P/S: I have also attached the files I used, just in case it helps.

dyn-and-dyf-files.zip

</data>
<data name="CombinedUpgradeToastSuffix" xml:space="preserve">
<value>have been automatically converted to PythonNet3 to run in Dynamo 4.0. CPython is no longer supported.</value>
</data>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AndConjunction, CombinedUpgradeToastSuffix, CPythonUpgradeToastMessagePlural and CPythonUpgradeToastMessageSingular can now be removed.

private void TryShowPythonEngineUpgradeToast(int cpythonNodeCount, int customDefCount, string backupPath = "")
{
ShowPythonEngineUpgradeToast(cpythonNodeCount, customDefCount, backupPath);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we can remove TryShowPythonEngineUpgradeToast and use ShowPythonEngineUpgradeToast since both methods have exactly the same signature and the additional redirection does not make any actual changes?

Dispatcher.BeginInvoke(
DispatcherPriority.Background,
new Action(() => DynamoViewModel.ShowPythonEngineUpgradeCanvasToast(msg, stayOpen: true)));
CurrentWorkspace.ShowCPythonNotifications = !preferenceSettings.HideCPython3Notifications;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to keep this PR focused, you don’t need to make this change now. I’ve logged a follow-up task so we can address it later. If we name this ShowCPython3Notifications, it will read more naturally and avoid a double-negative 🙂

{
CurrentWorkspace.ShowCPythonNotifications = false;
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the code above, relying on RecomputeEngineFlags being called before this line to update hasCPython3Engine (which is defined elsewhere) feels fragile -- it’s possible that RecomputeEngineFlags could be skipped due to future changes. Since RecomputeEngineFlags is lightweight, let’s introduce a HasEngine() method and use that here instead:

if (preferenceSettings == null)
{
  if (PythonEngineManager.Instance.HasEngine(PythonEngineManager.CPython3EngineName))
  {
    CurrentWorkspace.ShowCPythonNotifications = false;
    return;
  }
}

{
upgradeService.SaveGraphBackup(CurrentWorkspace, PythonServices.PythonEngineManager.CPython3EngineName);
saveBackup = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of relying on a class-level saveBackup field that's only set when direct/first-level Python nodes are detected in RecomputeCPython3NotificationForWorkspace, we can return a boolean from that method and use it locally. This improves readability and avoids issues if saveBackup is inadvertently modified elsewhere:

private void OnCurrentWorkspaceChanged(IWorkspaceModel workspace)
{
    bool workspaceModified = false;
    if (CurrentWorkspace is HomeWorkspaceModel hws)
    {
        // Home workspace related logic...

        workspaceModified = RecomputeCPython3NotificationForWorkspace();
    }
    else if (CurrentWorkspace is ICustomNodeWorkspaceModel cws)
    {
        // Custom node workspace related logic...

        workspaceModified = RecomputeCPython3NotificationForWorkspace();
    }

    if (workspaceModified)
    {
        upgradeService.SaveGraphBackup(CurrentWorkspace, ...);
    }
}

As a side note, Cursor suggest MigrateCPythonNodesForWorkspace based on what is done in RecomputeCPython3NotificationForWorkspace method 🙂

/// <summary>
/// Custom node workspaces touched during this session
/// </summary>
public readonly HashSet<WorkspaceModel> TouchedCustomWorkspaces = new HashSet<WorkspaceModel>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both TouchedCustomWorkspaces and TempMigratedCustomDefs maintain identical information (just in different forms), which makes keeping them in sync challenging and error-prone. For example, it's unclear whether we need to remove TouchedCustomWorkspaces entries in CommitCustomNodeMigrationsOnSave after the loop, when entries are removed from TempMigratedCustomDefs.

Here's what I'd suggest to simplify their usage:

  1. Keep only the lightweight TempMigratedCustomDefs hash set, which stores only Guid values.

  2. When CommitCustomNodeMigrationsOnSave needs the corresponding WorkspaceModel, it can retrieve the heavyweight WorkspaceModel from CustomNodeManager using the Guid.

This way we only need to maintain one HashSet.

// Remove tracking if this was the last instance of that custom node
var anyRemaining = CurrentWorkspace?.Nodes
.OfType<Dynamo.Graph.Nodes.CustomNodes.Function>()
.Any(f => (f.Definition?.FunctionId ?? Guid.Empty) == defId) == true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any already returns bool so we can simplify it to:

var anyRemaining = CurrentWorkspace?.Nodes
    .OfType<Dynamo.Graph.Nodes.CustomNodes.Function>()
    .Any(f => f.Definition?.FunctionId == defId);

}

return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see TryGetFunctionWorkspace calls being used in various places. For consistency and avoid repetition, what do you think of this approach?

// New method other components can use
internal ICustomNodeWorkspaceModel TryGetFunctionWorkspace(DynamoModel dynamoModel, Guid guid)
{
    ICustomNodeWorkspaceModel ws;
    var cnm = dynamoModel?.CustomNodeManager;

    if (cnm != null && cnm.TryGetFunctionWorkspace(guid, dynamoModel.IsTestMode, out ws))
    {
        return ws;
    }

    return null;
}

Then we can update CustomNodeHasPython to be this way:

private bool CustomNodeHasPython(Guid defId, Func<NodeModel, bool> isPythonNode)
{
    var cws = this.TryGetFunctionWorkspace(DynamoModel, defId) as CustomNodeWorkspaceModel;
    return cws?.Nodes != null && cws.Nodes.Any(isPythonNode);
}

Other callers can also be simplified, too.

workspace,
SetEngine);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new upgrade service method TryGetFunctionWorkspace, we can now do this:

var workspace = upgradeService.TryGetFunctionWorkspace(Models.DynamoModel, defId) as WorkspaceModel;
if (workspace != null)
{
    // Existing logic goes here...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants