Invalidate dynamic namespace mutations

This commit is contained in:
2026-07-02 16:55:52 +02:00
parent d1f49e7c95
commit 2ad3cd3a09
2 changed files with 121 additions and 2 deletions
@@ -2125,6 +2125,54 @@ RET.clear()
self.assertEqual({}, result["nodes"]) self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"]) self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_getattr_return_types_mutation_after_mapping_skips_node(self):
source = '''
class GetattrReturnTypesNode:
RETURN_TYPES = ["IMAGE"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"GetattrReturnTypesNode": GetattrReturnTypesNode,
}
getattr(GetattrReturnTypesNode, "RETURN_TYPES").clear()
'''
result = self._extract_source(source, "getattr-return-types-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_globals_class_return_types_mutation_after_mapping_skips_node(self):
source = '''
class GlobalsClassReturnTypesNode:
RETURN_TYPES = ["IMAGE"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"GlobalsClassReturnTypesNode": GlobalsClassReturnTypesNode,
}
globals()["GlobalsClassReturnTypesNode"].RETURN_TYPES.clear()
'''
result = self._extract_source(source, "globals-class-return-types-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_module_class_tuple_alias_patch_after_mapping_skips_node(self): def test_module_class_tuple_alias_patch_after_mapping_skips_node(self):
source = ''' source = '''
class TupleAliasPatchedNode: class TupleAliasPatchedNode:
@@ -2225,6 +2273,30 @@ NODE_CLASS_MAPPINGS.clear()
self.assertEqual({}, result["nodes"]) self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"]) self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_globals_mutation_invalidates_static_node_mapping(self):
source = '''
class GlobalMutatedMappingNode:
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"GlobalMutatedMappingNode": GlobalMutatedMappingNode,
}
globals()["NODE_CLASS_MAPPINGS"].clear()
'''
result = self._extract_source(source, "global-mutated-mapping-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_unpacked_alias_mutation_invalidates_static_node_mapping(self): def test_unpacked_alias_mutation_invalidates_static_node_mapping(self):
source = ''' source = '''
class UnpackedAliasMutatedMappingNode: class UnpackedAliasMutatedMappingNode:
+49 -2
View File
@@ -83,6 +83,20 @@ def _is_mutable_static_value(value):
return isinstance(value, (dict, list, set)) return isinstance(value, (dict, list, set))
def _namespace_subscript_name(node):
if not isinstance(node, ast.Subscript):
return None
if not isinstance(node.value, ast.Call) or not isinstance(node.value.func, ast.Name):
return None
if node.value.func.id not in {"globals", "locals", "vars"}:
return None
if node.value.args or node.value.keywords:
return None
if isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, str):
return node.slice.value
return None
def _target_names(target): def _target_names(target):
if isinstance(target, ast.Name): if isinstance(target, ast.Name):
return {target.id} return {target.id}
@@ -93,23 +107,56 @@ def _target_names(target):
return names return names
if isinstance(target, ast.Starred): if isinstance(target, ast.Starred):
return _target_names(target.value) return _target_names(target.value)
if isinstance(target, (ast.Attribute, ast.Subscript)): if isinstance(target, ast.Attribute):
return _target_names(target.value)
if isinstance(target, ast.Subscript):
name = _namespace_subscript_name(target)
if name is not None:
return {name}
return _target_names(target.value) return _target_names(target.value)
return set() return set()
def _root_name(node): def _root_name(node):
while isinstance(node, (ast.Attribute, ast.Subscript)): while True:
name = _namespace_subscript_name(node)
if name is not None:
return name
if not isinstance(node, (ast.Attribute, ast.Subscript)):
break
node = node.value node = node.value
if isinstance(node, ast.Name): if isinstance(node, ast.Name):
return node.id return node.id
return None return None
def _getattr_signature_target_names(node):
if not isinstance(node, ast.Call):
return set()
if not isinstance(node.func, ast.Name) or node.func.id != "getattr":
return set()
if len(node.args) < 2:
return set()
name = _root_name(node.args[0])
if name is None:
return set()
attr = node.args[1]
if (
isinstance(attr, ast.Constant)
and isinstance(attr.value, str)
and attr.value not in _CLASS_SIGNATURE_ATTRS
):
return set()
return {name}
def _attribute_target_base_names(target): def _attribute_target_base_names(target):
if isinstance(target, ast.Attribute): if isinstance(target, ast.Attribute):
name = _root_name(target.value) name = _root_name(target.value)
return {name} if name else set() return {name} if name else set()
names = _getattr_signature_target_names(target)
if names:
return names
if isinstance(target, ast.Subscript): if isinstance(target, ast.Subscript):
return _attribute_target_base_names(target.value) return _attribute_target_base_names(target.value)
if isinstance(target, (ast.List, ast.Tuple)): if isinstance(target, (ast.List, ast.Tuple)):