diff --git a/tests/test_generate_popular_node_signatures.py b/tests/test_generate_popular_node_signatures.py index c47b183..75c8d01 100644 --- a/tests/test_generate_popular_node_signatures.py +++ b/tests/test_generate_popular_node_signatures.py @@ -260,6 +260,49 @@ NODE_CLASS_MAPPINGS = { self.assertEqual({}, result["nodes"]) self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_duplicate_node_id_with_unsupported_mapping_value_skips_static_node(self): + source_a = ''' +class StaticDupNode: + RETURN_TYPES = ("IMAGE",) + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + }, + } + + +NODE_CLASS_MAPPINGS = { + "DupNode": StaticDupNode, +} +''' + source_b = ''' +def build_node(): + return object() + + +NODE_CLASS_MAPPINGS = { + "DupNode": build_node(), +} +''' + with tempfile.TemporaryDirectory() as tmp: + Path(tmp, "a.py").write_text(textwrap.dedent(source_a), encoding="utf-8") + Path(tmp, "b.py").write_text(textwrap.dedent(source_b), encoding="utf-8") + result = extract_repo_signatures( + Path(tmp), + { + "id": "unsupported-duplicate-node-pack", + "title": "Unsupported Duplicate Node Pack", + "repository": "https://github.com/example/unsupported-duplicate-node-pack", + "rank": 1, + }, + ) + + self.assertEqual({}, result["nodes"]) + self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_unsupported_reassignment_invalidates_static_env_value(self): source = ''' def build_inputs(): @@ -767,6 +810,33 @@ NODE_CLASS_MAPPINGS = { self.assertEqual({}, result["nodes"]) self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_arbitrary_call_observing_mutable_env_value_invalidates_static_env_value(self): + source = ''' +INPUTS = { + "required": { + "image": ("IMAGE",), + }, +} +observe(INPUTS) + + +class ObservedInputEnvNode: + RETURN_TYPES = ("IMAGE",) + + @classmethod + def INPUT_TYPES(cls): + return INPUTS + + +NODE_CLASS_MAPPINGS = { + "ObservedInputEnvNode": ObservedInputEnvNode, +} +''' + result = self._extract_source(source, "observed-input-env-pack") + + self.assertEqual({}, result["nodes"]) + self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_function_default_mutation_invalidates_static_env_value(self): source = ''' INPUTS = { @@ -1685,6 +1755,30 @@ NODE_CLASS_MAPPINGS = { self.assertEqual({}, result["nodes"]) self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_return_types_used_as_callee_skips_node(self): + source = ''' +class CalleeObservedReturnTypesNode: + RETURN_TYPES = ["IMAGE"] + RETURN_TYPES() + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + }, + } + + +NODE_CLASS_MAPPINGS = { + "CalleeObservedReturnTypesNode": CalleeObservedReturnTypesNode, +} +''' + result = self._extract_source(source, "callee-observed-return-types-pack") + + self.assertEqual({}, result["nodes"]) + self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_return_types_function_default_arbitrary_call_skips_node(self): source = ''' class DefaultArbitraryCallReturnTypesNode: @@ -3735,6 +3829,31 @@ NODE_CLASS_MAPPINGS = { self.assertEqual({}, result["nodes"]) self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_input_types_used_as_callee_skips_node(self): + source = ''' +class CalleeObservedInputTypesNode: + RETURN_TYPES = ("IMAGE",) + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + }, + } + + INPUT_TYPES() + + +NODE_CLASS_MAPPINGS = { + "CalleeObservedInputTypesNode": CalleeObservedInputTypesNode, +} +''' + result = self._extract_source(source, "callee-observed-input-types-pack") + + self.assertEqual({}, result["nodes"]) + self.assertEqual("no_static_nodes", result["pack"]["status"]) + def test_input_types_alias_observed_by_arbitrary_call_skips_node(self): source = ''' class AliasObservedInputTypesNode: diff --git a/tools/generate_popular_node_signatures.py b/tools/generate_popular_node_signatures.py index b485400..a781966 100644 --- a/tools/generate_popular_node_signatures.py +++ b/tools/generate_popular_node_signatures.py @@ -635,6 +635,7 @@ def _arbitrary_call_observed_names(stmt): self.visit(node.args) def visit_Call(self, node): + names.update(_referenced_names(node.func)) if isinstance(node.func, ast.Attribute): names.update(_referenced_names(node.func.value)) for arg in node.args: @@ -779,6 +780,10 @@ def _apply_module_stmt_to_env(stmt, env, class_bindings=None): else: _invalidate_class_bindings(class_bindings, names) _invalidate_env_names(env, names) + observed_names = _arbitrary_call_observed_names(stmt) + for name in observed_names: + if name in env and _is_mutable_static_value(env[name]): + _invalidate_env_name(env, name) if isinstance(stmt, ast.ClassDef): if class_bindings is not None: if _is_trivially_safe_class_def(stmt): @@ -1782,6 +1787,19 @@ def _node_class_mappings(tree): return {node_type: binding for node_type, (_class_name, binding) in mappings.items() if node_type} +def _node_class_mapping_keys(tree): + if _has_module_wildcard_import(tree): + return set() + mappings = _final_module_dict( + tree, + "NODE_CLASS_MAPPINGS", + lambda _value, _env, _class_bindings: True, + ) + if not all(isinstance(node_type, str) for node_type in mappings): + return set() + return {node_type for node_type in mappings if node_type} + + def _display_mappings(tree): displays = _final_module_dict( tree, @@ -1880,7 +1898,15 @@ def extract_repo_signatures(repo_dir, pack_meta): continue env = _collect_module_env(tree) mappings = _node_class_mappings(tree) + mapping_node_types = _node_class_mapping_keys(tree) displays = _display_mappings(tree) + for node_type in sorted(mapping_node_types): + prior_path = node_sources.get(node_type) + if prior_path is not None and prior_path != path: + duplicate_node_types.add(node_type) + nodes.pop(node_type, None) + continue + node_sources.setdefault(node_type, path) if displays is _INVALID: continue for node_type, binding in sorted(mappings.items()):