Skip to content

fix(select_nodes): Safe-guard if previously selected node cannot be reselected - #331

Merged
jakubjezek001 merged 2 commits into
ynput:developfrom
straylondon:fix/select_nodes_on_deleted_nodes
Sep 1, 2026
Merged

fix(select_nodes): Safe-guard if previously selected node cannot be reselected#331
jakubjezek001 merged 2 commits into
ynput:developfrom
straylondon:fix/select_nodes_on_deleted_nodes

Conversation

@vincentullmann

Copy link
Copy Markdown
Contributor

Changelog Description

avoid crashing if a node cannot be selected

Additional review information

as mentioned in #329, there can be cases where select_nodes may get called with a list of invalid nodes. For example when using a maintained_selection context manager and one of the previous selected nodes got deleted within the block.

Testing notes:

from ayon_nuke.api.lib import maintained_selection, reset_selection, select_nodes


nuke.scriptClear(force=True)

# create some test nodes
grade = nuke.createNode("Grade")
blur = nuke.createNode("Blur")
read = nuke.createNode("Read")
write = nuke.createNode("Write")

# preprare a selection
reset_selection()
select_nodes([grade, blur, read])  # select a subset

with maintained_selection():
    select_nodes([write])  # change the selection
    nuke.delete(blur)  # delete one of the selected nodes


selected_nodes = nuke.selectedNodes()
print("selected_nodes", selected_nodes)  # should be grade + read
assert len(selected_nodes) == 2
assert grade in selected_nodes
assert read in selected_nodes
assert write not in selected_nodes

@iLLiCiTiT

iLLiCiTiT commented Jul 20, 2026

Copy link
Copy Markdown
Member

I would do explicit ValueError capture and not log it out, just add comment why the error can happen, and move it to maintain selection as it probably should fail if happens with invalid nodes during other operations.


QUESTION: Could we do something like this?

@contextlib.contextmanager
def maintained_selection(exclude_nodes=None):
    """Maintain selection during context

    Maintain selection during context and unselect
    all nodes after context is done.

    Arguments:
        exclude_nodes (list[nuke.Node]): list of nodes to be unselected
                                         before context is done

    Example:
        >>> with maintained_selection():
        ...     node["selected"].setValue(True)
        >>> print(node["selected"].value())
        False
    """
    if exclude_nodes:
        for node in exclude_nodes:
            node["selected"].setValue(False)

    selected_nodes = nuke.selectedNodes()
    node_ids = set()
    for node in selected_nodes:
        node["selected"].setValue(False)
        node_ids.add(id(node))

    try:
        yield
    finally:
        # unselect all selection in case there is some
        reset_selection()

        # and select all previously selected nodes if were not removed
        for node in nuke.allNodes()
            if not node_ids:
                break
            if id(node) in node_ids:
                node_ids.discard(id(node))
                node["selected"].setValue(True)

Not sure how slower this might be...

@iLLiCiTiT iLLiCiTiT added the type: bug Something isn't working label Jul 20, 2026
@vincentullmann
vincentullmann force-pushed the fix/select_nodes_on_deleted_nodes branch from f8849a0 to aab56a8 Compare August 28, 2026 19:09
@vincentullmann

Copy link
Copy Markdown
Contributor Author

I did a few tests today and nuke.allNodes() is surprisingly fast.

I'd still go with try/except though. It's the fastest as expected, especially for typical cases where only a handful of nodes need to be reselected, and I think it's the semantically cleanest option.

test setup
# step 1
for i in range(1_000):
    grade = nuke.createNode("Grade")
    
# step 2
# take a "few" of these nodes and copy them a couple of times

# step 3
# select a random chunk

I ended up with 4355 total nodes.

image
test script
import time

import nuke


#####################################
# Helpers

def reset_selection():
    for node in nuke.selectedNodes():
        node["selected"].setValue(False)


def time_it(func):

    def wrapped():
        start = time.time()
        func()
        end = time.time()
        ms = (end - start) * 1000
        print(f"{func.__name__} {ms:.5f}ms")

    return wrapped


def test_func(func):
    for _ in range(10):
        func()
    print("")

#####################################
# Tests

@time_it
def test__nuke_all_nodes():
    _ = nuke.allNodes()


@time_it
def test__nodes_set():
    previous_selection = set(nuke.selectedNodes())

    # try:
    #     yield
    # finally:
    reset_selection()

    for node in nuke.allNodes():
        selected = node in previous_selection
        node["selected"].setValue(selected)

@time_it
def test__ids():

    ids = {id(node) for node in nuke.selectedNodes()}

    # try:
    #     yield
    # finally:
    reset_selection()

    for node in nuke.allNodes():
        selected = id(node) in ids
        node["selected"].setValue(selected)


@time_it
def test__try_except():

    previous_selection = nuke.selectedNodes()

    # try:
    #     yield
    # finally:
    reset_selection()

    for node in previous_selection:
        node["selected"].setValue(True)



n_sel = len(nuke.selectedNodes())
n_total = len(nuke.allNodes())
print(f"{n_sel} / {n_total} selected")
print("")

test_func(test__nuke_all_nodes)
test_func(test__nodes_set)
test_func(test__ids)
test_func(test__try_except)

Results:

4355 total nodes, median of 10 runs (ms):

approach 0% selected 50% selected 100% selected
nuke.allNodes() 0.69 0.68 0.69
set(node) + allNodes loop 3.18 26.21 41.92
id() + allNodes loop 2.90 19.78 41.94
loop previous_selection 0.001 14.09 39.53
Details
Result: 0 / 4355 selected

test__nuke_all_nodes 1.25933ms
test__nuke_all_nodes 0.86236ms
test__nuke_all_nodes 0.69761ms
test__nuke_all_nodes 0.69523ms
test__nuke_all_nodes 0.69308ms
test__nuke_all_nodes 0.67854ms
test__nuke_all_nodes 0.69427ms
test__nuke_all_nodes 0.68688ms
test__nuke_all_nodes 0.52595ms
test__nuke_all_nodes 0.52500ms

test__nodes_set 8.08764ms
test__nodes_set 7.09105ms
test__nodes_set 4.69208ms
test__nodes_set 3.83544ms
test__nodes_set 3.47733ms
test__nodes_set 2.87557ms
test__nodes_set 2.79403ms
test__nodes_set 2.88057ms
test__nodes_set 2.68102ms
test__nodes_set 2.66290ms

test__ids 2.97022ms
test__ids 3.09610ms
test__ids 2.96855ms
test__ids 2.84815ms
test__ids 2.87533ms
test__ids 2.87867ms
test__ids 2.95973ms
test__ids 2.87986ms
test__ids 2.92563ms
test__ids 2.86126ms

test__try_except 0.00429ms
test__try_except 0.00143ms
test__try_except 0.00143ms
test__try_except 0.00143ms
test__try_except 0.00143ms
test__try_except 0.00143ms
test__try_except 0.00119ms
test__try_except 0.00143ms
test__try_except 0.00143ms
test__try_except 0.00167ms

Result: 2264 / 4355 selected


test__nuke_all_nodes 1.00017ms
test__nuke_all_nodes 0.77224ms
test__nuke_all_nodes 0.69404ms
test__nuke_all_nodes 0.66829ms
test__nuke_all_nodes 0.68307ms
test__nuke_all_nodes 0.50688ms
test__nuke_all_nodes 0.51379ms
test__nuke_all_nodes 0.51141ms
test__nuke_all_nodes 0.55337ms
test__nuke_all_nodes 0.74148ms

test__nodes_set 45.74990ms
test__nodes_set 43.91646ms
test__nodes_set 23.02408ms
test__nodes_set 20.89643ms
test__nodes_set 19.17315ms
test__nodes_set 20.98203ms
test__nodes_set 26.79372ms
test__nodes_set 25.66433ms
test__nodes_set 26.75629ms
test__nodes_set 29.43468ms

test__ids 24.05000ms
test__ids 21.37780ms
test__ids 19.53745ms
test__ids 19.19174ms
test__ids 19.81926ms
test__ids 19.73963ms
test__ids 18.91208ms
test__ids 20.12467ms
test__ids 20.03026ms
test__ids 19.32573ms

test__try_except 14.93526ms
test__try_except 18.09669ms
test__try_except 15.07854ms
test__try_except 14.29415ms
test__try_except 13.93318ms
test__try_except 13.86905ms
test__try_except 14.07838ms
test__try_except 14.10818ms
test__try_except 13.81016ms
test__try_except 13.61728ms
Result: 4352 / 4355 selected

test__nuke_all_nodes 0.75006ms
test__nuke_all_nodes 0.68235ms
test__nuke_all_nodes 0.68808ms
test__nuke_all_nodes 0.69427ms
test__nuke_all_nodes 0.68784ms
test__nuke_all_nodes 0.68688ms
test__nuke_all_nodes 0.67759ms
test__nuke_all_nodes 0.67735ms
test__nuke_all_nodes 0.69094ms
test__nuke_all_nodes 0.54765ms

test__nodes_set 73.40360ms
test__nodes_set 43.45727ms
test__nodes_set 41.72492ms
test__nodes_set 60.95576ms
test__nodes_set 40.45868ms
test__nodes_set 39.40439ms
test__nodes_set 42.03486ms
test__nodes_set 44.71254ms
test__nodes_set 41.79978ms
test__nodes_set 40.28702ms

test__ids 40.23409ms
test__ids 41.74185ms
test__ids 41.29648ms
test__ids 42.97447ms
test__ids 42.14120ms
test__ids 65.00244ms
test__ids 84.08213ms
test__ids 61.31721ms
test__ids 40.89141ms
test__ids 40.21668ms

test__try_except 39.81209ms
test__try_except 59.71694ms
test__try_except 57.03187ms
test__try_except 39.25776ms
test__try_except 38.07545ms
test__try_except 38.09094ms
test__try_except 37.14252ms
test__try_except 37.44555ms
test__try_except 42.92536ms
test__try_except 40.49134ms

@BigRoy BigRoy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Prevents maintained_selection from crashing when restoring a deleted node.

Changes:

  • Restores previously selected nodes individually.
  • Ignores ValueError for invalidated nodes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread client/ayon_nuke/api/lib.py
@iLLiCiTiT iLLiCiTiT changed the title fix(select_nodes): log error when a previously selected node cannot be reselected fix(select_nodes): Safe-guard if previously selected node cannot be reselected Aug 31, 2026
@iLLiCiTiT

Copy link
Copy Markdown
Member

@vincentullmann please update the branch?

@jakubjezek001
jakubjezek001 merged commit db18301 into ynput:develop Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants