feat: add module and code hash to component's metadata (#8737)

* feat: filter out base components path in custom component loading

* revert changes to allow testing the function

* feat: add module_name parameter to custom component template functions

* feat: include full module name in component template creation

* feat: add module metadata to various starter project JSON files

* feat: add code hash generation for custom component templates

* test: add unit tests for code hash and module metadata functionality

* feat: add code hash to metadata in various starter project JSON files

* feat: refactor code hash generation to accept source code and class name

* feat: implement loading strategy for custom components in settings service

* Update metadata for various starter project JSON files to include code hashes and module references for ChatInput and ChatOutput components. This enhances traceability and consistency across the project configurations.

* refactor: improve _generate_code_hash function for better error handling and clarity

- Simplified the logic to generate a SHA256 hash of the source code.
- Added explicit error handling for empty source code, encoding issues, and type errors.
- Updated docstring to reflect changes in argument descriptions and added information about raised exceptions.

* test: add unit tests for metadata functionality in custom utils

- Introduced tests for the _generate_code_hash function, covering basic hash generation, error handling for empty and None source, hash consistency, and uniqueness for different code.
- Added tests to verify metadata addition in template building functions, ensuring that module names and code hashes are correctly included in the metadata of custom components.

---------

Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2025-07-18 15:03:17 -03:00 • committed by GitHub
commit ca595ac7fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 863 additions and 165 deletions

View file

@ -0,0 +1,142 @@
"""Test metadata functionality in custom utils."""
from unittest.mock import Mock, patch
import pytest
from langflow.custom.utils import _generate_code_hash
class TestCodeHashGeneration:
"""Test the _generate_code_hash function."""
def test_hash_generation_basic(self):
"""Test basic hash generation."""
source = "def test(): pass"
modname = "test_module"
class_name = "TestClass"
result = _generate_code_hash(source, modname, class_name)
assert isinstance(result, str)
assert len(result) == 12
assert all(c in "0123456789abcdef" for c in result)
def test_hash_empty_source_raises(self):
"""Test that empty source raises ValueError."""
with pytest.raises(ValueError, match="Empty source code"):
_generate_code_hash("", "mod", "cls")
def test_hash_none_source_raises(self):
"""Test that None source raises ValueError."""
with pytest.raises(ValueError, match="Empty source code"):
_generate_code_hash(None, "mod", "cls")
def test_hash_consistency(self):
"""Test that same code produces same hash."""
source = "class A: pass"
hash1 = _generate_code_hash(source, "mod", "A")
hash2 = _generate_code_hash(source, "mod", "A")
assert hash1 == hash2
def test_hash_different_code(self):
"""Test that different code produces different hash."""
hash1 = _generate_code_hash("class A: pass", "mod", "A")
hash2 = _generate_code_hash("class B: pass", "mod", "B")
assert hash1 != hash2
class TestMetadataInTemplateBuilders:
"""Test metadata addition in template building functions."""
@patch("langflow.custom.utils.ComponentFrontendNode")
def test_build_from_inputs_adds_metadata_with_module(self, mock_frontend_class):
"""Test that build_custom_component_template_from_inputs adds metadata when module_name is provided."""
from langflow.custom.custom_component.component import Component
from langflow.custom.utils import build_custom_component_template_from_inputs
# Setup mock frontend node
mock_frontend = Mock()
mock_frontend.metadata = {}
mock_frontend.outputs = []
mock_frontend.to_dict = Mock(return_value={"test": "data"})
mock_frontend.validate_component = Mock()
mock_frontend.set_base_classes_from_outputs = Mock()
mock_frontend_class.from_inputs.return_value = mock_frontend
# Create test component
test_component = Mock(spec=Component)
test_component.__class__.__name__ = "TestComponent"
test_component._code = "class TestComponent: pass"
test_component.template_config = {"inputs": []}
# Mock get_component_instance to return a mock instance
with patch("langflow.custom.utils.get_component_instance") as mock_get_instance:
mock_instance = Mock()
mock_instance.get_template_config = Mock(return_value={})
mock_instance._get_field_order = Mock(return_value=[])
mock_get_instance.return_value = mock_instance
# Mock add_code_field to return the frontend node
with (
patch("langflow.custom.utils.add_code_field", return_value=mock_frontend),
patch("langflow.custom.utils.reorder_fields"),
):
# Call the function
template, _ = build_custom_component_template_from_inputs(test_component, module_name="test.module")
# Verify metadata was added
assert "module" in mock_frontend.metadata
assert mock_frontend.metadata["module"] == "test.module"
assert "code_hash" in mock_frontend.metadata
assert len(mock_frontend.metadata["code_hash"]) == 12
@patch("langflow.custom.utils.CustomComponentFrontendNode")
def test_build_template_adds_metadata_with_module(self, mock_frontend_class):
"""Test that build_custom_component_template adds metadata when module_name is provided."""
from langflow.custom.custom_component.custom_component import CustomComponent
from langflow.custom.utils import build_custom_component_template
# Setup mock frontend node
mock_frontend = Mock()
mock_frontend.metadata = {}
mock_frontend.to_dict = Mock(return_value={"test": "data"})
mock_frontend_class.return_value = mock_frontend
# Create test component
test_component = Mock(spec=CustomComponent)
test_component.__class__.__name__ = "CustomTestComponent"
test_component._code = "class CustomTestComponent: pass"
test_component.template_config = {"display_name": "Test"}
test_component.get_function_entrypoint_args = []
test_component._get_function_entrypoint_return_type = []
# Mock helper functions
with patch("langflow.custom.utils.run_build_config") as mock_run_build:
mock_instance = Mock()
mock_instance._get_field_order = Mock(return_value=[])
mock_run_build.return_value = ({}, mock_instance)
with (
patch("langflow.custom.utils.add_extra_fields"),
patch("langflow.custom.utils.add_code_field", return_value=mock_frontend),
patch("langflow.custom.utils.add_base_classes"),
patch("langflow.custom.utils.add_output_types"),
patch("langflow.custom.utils.reorder_fields"),
):
# Call the function
template, _ = build_custom_component_template(test_component, module_name="custom.test")
# Verify metadata was added
assert "module" in mock_frontend.metadata
assert mock_frontend.metadata["module"] == "custom.test"
assert "code_hash" in mock_frontend.metadata
assert len(mock_frontend.metadata["code_hash"]) == 12
def test_hash_generation_unicode(self):
"""Test hash generation with unicode characters."""
source = "# Test with unicode: 你好 🌟\nclass Component: pass"
result = _generate_code_hash(source, "unicode_mod", "Component")
assert isinstance(result, str)
assert len(result) == 12
assert all(c in "0123456789abcdef" for c in result)

View file

@ -0,0 +1,59 @@
"""Test code hash and module metadata functionality."""
import pytest
from langflow.interface.components import import_langflow_components
@pytest.mark.asyncio
async def test_component_metadata_has_code_hash():
"""Test that built-in components have valid module and code_hash metadata."""
result = await import_langflow_components()
assert result is not None
assert "components" in result
assert len(result["components"]) > 0
# Find first component to test
sample_category = None
sample_component = None
for category, components in result["components"].items():
if components:
sample_category = category
sample_component = next(iter(components.values()))
break
assert sample_component is not None, "No components found to test"
# Test metadata presence - metadata should be in the 'metadata' sub-field
assert "metadata" in sample_component, f"Metadata field missing from component in {sample_category}"
metadata = sample_component["metadata"]
assert "module" in metadata, f"Module metadata missing from component in {sample_category}"
assert "code_hash" in metadata, f"Code hash metadata missing from component in {sample_category}"
# Test that values are valid
module_name = metadata["module"]
code_hash = metadata["code_hash"]
assert isinstance(module_name, str), f"Invalid module name type: {type(module_name)}"
assert module_name, f"Invalid module name: {module_name}"
assert isinstance(code_hash, str), f"Invalid code hash type: {type(code_hash)}"
assert len(code_hash) == 12, f"Invalid code hash: {code_hash} (should be 12 chars)"
@pytest.mark.asyncio
async def test_code_hash_uniqueness():
"""Test that different built-in components have different code hashes."""
result = await import_langflow_components()
all_hashes = []
for components in result["components"].values():
for comp in components.values():
metadata = comp.get("metadata", {})
if metadata.get("code_hash"):
all_hashes.append(metadata["code_hash"])
# Check that we have some components with metadata
assert len(all_hashes) > 0, "No components with code hashes found"
# Check that we have reasonable uniqueness in hashes
unique_hashes = len(set(all_hashes))
total_hashes = len(all_hashes)
uniqueness_ratio = unique_hashes / total_hashes
# Should have high uniqueness (most components have different code)
assert uniqueness_ratio > 0.95, f"Hash uniqueness too low: {uniqueness_ratio:.1%}"