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:
parent
d9f3ced8b0
commit
ca595ac7fb
35 changed files with 863 additions and 165 deletions
142
src/backend/tests/unit/custom/test_utils_metadata.py
Normal file
142
src/backend/tests/unit/custom/test_utils_metadata.py
Normal 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)
|
||||
59
src/backend/tests/unit/test_code_hash.py
Normal file
59
src/backend/tests/unit/test_code_hash.py
Normal 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%}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue