Fail closed on namespace dunders and metadata types

This commit is contained in:
2026-07-02 18:29:47 +02:00
parent f7143e7bac
commit 3cf4a5eb52
2 changed files with 173 additions and 7 deletions
@@ -1187,6 +1187,52 @@ NODE_CLASS_MAPPINGS = {
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_input_types_with_non_string_input_name_skips_node(self):
source = '''
class NonStringInputNameNode:
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
1: ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"NonStringInputNameNode": NonStringInputNameNode,
}
'''
result = self._extract_source(source, "non-string-input-name-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_input_types_with_non_string_input_type_skips_node(self):
source = '''
class NonStringInputTypeNode:
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": (2,),
},
}
NODE_CLASS_MAPPINGS = {
"NonStringInputTypeNode": NonStringInputTypeNode,
}
'''
result = self._extract_source(source, "non-string-input-type-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_dynamic_return_types_reassignment_skips_node(self): def test_dynamic_return_types_reassignment_skips_node(self):
source = ''' source = '''
def build_outputs(): def build_outputs():
@@ -1876,6 +1922,53 @@ NODE_CLASS_MAPPINGS = {
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_non_string_return_type_entry_skips_node(self):
source = '''
class NonStringReturnTypeNode:
RETURN_TYPES = (123,)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"NonStringReturnTypeNode": NonStringReturnTypeNode,
}
'''
result = self._extract_source(source, "non-string-return-type-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_non_string_return_name_entry_skips_node(self):
source = '''
class NonStringReturnNameNode:
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = (456,)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"NonStringReturnNameNode": NonStringReturnNameNode,
}
'''
result = self._extract_source(source, "non-string-return-name-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_missing_return_names_is_allowed(self): def test_missing_return_names_is_allowed(self):
source = ''' source = '''
class MissingReturnNamesNode: class MissingReturnNamesNode:
@@ -3028,6 +3121,55 @@ G.update(NODE_CLASS_MAPPINGS={})
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_dunder_setitem_invalidates_static_node_mapping(self):
source = '''
class GlobalDunderSetitemNode:
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"GlobalDunderSetitemNode": GlobalDunderSetitemNode,
}
globals().__setitem__("NODE_CLASS_MAPPINGS", {})
'''
result = self._extract_source(source, "global-dunder-setitem-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_globals_alias_dunder_setitem_invalidates_static_node_mapping(self):
source = '''
class GlobalAliasDunderSetitemNode:
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
}
NODE_CLASS_MAPPINGS = {
"GlobalAliasDunderSetitemNode": GlobalAliasDunderSetitemNode,
}
ns = globals()
ns.__setitem__("NODE_CLASS_MAPPINGS", {})
'''
result = self._extract_source(source, "global-alias-dunder-setitem-pack")
self.assertEqual({}, result["nodes"])
self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_arbitrary_call_invalidates_static_node_mapping(self): def test_arbitrary_call_invalidates_static_node_mapping(self):
source = ''' source = '''
class ArbitraryCallMappingNode: class ArbitraryCallMappingNode:
+31 -7
View File
@@ -39,6 +39,7 @@ if hasattr(ast, "TryStar"):
_CLASS_SIGNATURE_ATTRS = {"INPUT_TYPES", "RETURN_NAMES", "RETURN_TYPES"} _CLASS_SIGNATURE_ATTRS = {"INPUT_TYPES", "RETURN_NAMES", "RETURN_TYPES"}
_DYNAMIC_NAMESPACE_MUTATION = object() _DYNAMIC_NAMESPACE_MUTATION = object()
_NAMESPACE_FUNCTIONS = {"globals", "locals", "vars"} _NAMESPACE_FUNCTIONS = {"globals", "locals", "vars"}
_NAMESPACE_DUNDER_MUTATORS = {"__delitem__", "__setitem__"}
def _literal(node, env, allow_mutable_env=True): def _literal(node, env, allow_mutable_env=True):
@@ -215,6 +216,10 @@ def _namespace_mutating_call_target_names(node):
return set() return set()
if _namespace_call_function_name(node.func.value) is None: if _namespace_call_function_name(node.func.value) is None:
return set() return set()
if node.func.attr in _NAMESPACE_DUNDER_MUTATORS:
if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
return {node.args[0].value}
return {_DYNAMIC_NAMESPACE_MUTATION}
if node.func.attr not in _MUTATING_METHODS: if node.func.attr not in _MUTATING_METHODS:
return set() return set()
if node.func.attr != "update": if node.func.attr != "update":
@@ -879,8 +884,8 @@ def _collect_module_env(tree, class_bindings=None):
def normalise_input_spec(spec): def normalise_input_spec(spec):
first = spec[0] if isinstance(spec, (list, tuple)) and spec else spec first = spec[0] if isinstance(spec, (list, tuple)) and spec else spec
if isinstance(first, list): if isinstance(first, list):
return "COMBO" return "COMBO" if all(isinstance(value, str) for value in first) else None
return str(first) return first if isinstance(first, str) else None
def _class_defs(tree): def _class_defs(tree):
@@ -1545,7 +1550,16 @@ def _namespace_alias_mutation_target_names(stmt, aliases):
def visit_Call(self, node): def visit_Call(self, node):
if isinstance(node.func, ast.Attribute): if isinstance(node.func, ast.Attribute):
if isinstance(node.func.value, ast.Name) and node.func.value.id in aliases: if isinstance(node.func.value, ast.Name) and node.func.value.id in aliases:
if node.func.attr == "update": if node.func.attr in _NAMESPACE_DUNDER_MUTATORS:
if (
node.args
and isinstance(node.args[0], ast.Constant)
and isinstance(node.args[0].value, str)
):
names.add(node.args[0].value)
else:
names.add(_DYNAMIC_NAMESPACE_MUTATION)
elif node.func.attr == "update":
for keyword in node.keywords: for keyword in node.keywords:
names.add(_DYNAMIC_NAMESPACE_MUTATION if keyword.arg is None else keyword.arg) names.add(_DYNAMIC_NAMESPACE_MUTATION if keyword.arg is None else keyword.arg)
if node.args or not node.keywords: if node.args or not node.keywords:
@@ -1772,18 +1786,28 @@ def _signature_from_class(node_type, cls, display, pack_meta, class_env, input_e
else: else:
values = {} values = {}
for name, spec in values.items(): for name, spec in values.items():
inputs[str(name)] = normalise_input_spec(spec) if not isinstance(name, str):
return None
input_type = normalise_input_spec(spec)
if input_type is None:
return None
inputs[name] = input_type
if section == "required": if section == "required":
required.append(str(name)) required.append(name)
output_names = [] output_names = []
if return_names is _MISSING: if return_names is _MISSING:
output_names = [] output_names = []
elif isinstance(return_names, (list, tuple)): elif isinstance(return_names, (list, tuple)):
output_names = [str(name) for name in return_names] if not all(isinstance(name, str) for name in return_names):
return None
output_names = list(return_names)
else: else:
return None return None
if not all(isinstance(value, str) for value in return_types):
return None
return { return {
"type": node_type, "type": node_type,
"display": display or node_type, "display": display or node_type,
@@ -1791,7 +1815,7 @@ def _signature_from_class(node_type, cls, display, pack_meta, class_env, input_e
"repository": pack_meta.get("repository", ""), "repository": pack_meta.get("repository", ""),
"inputs": inputs, "inputs": inputs,
"required": required, "required": required,
"outputs": [str(value) for value in return_types], "outputs": list(return_types),
"output_names": output_names, "output_names": output_names,
"confidence": "static_exact", "confidence": "static_exact",
} }