feat: turn most class variables into instance variables in the Component classes (#5252)
* refactor: improve type checking and initialization in BaseComponent class - Moved UUID import to TYPE_CHECKING for better type hinting without runtime overhead. - Reorganized the initialization of class attributes within the __init__ method for clarity and consistency. - Enhanced the __setattr__ method to handle potential KeyError and AttributeError when checking immutability of _user_id. * refactor: streamline CustomComponent initialization and enhance attribute management - Introduced ClassVar constants for better clarity and organization. - Removed redundant class attributes and restructured instance attribute initialization for improved readability. - Ensured that instance-specific attributes are initialized before calling the parent class's constructor. - Enhanced the overall structure of the CustomComponent class to facilitate future maintenance and development. * refactor: enhance Component initialization and attribute management - Reorganized the __init__ method to initialize instance-specific attributes before calling the parent class constructor. - Introduced new instance attributes for better management of inputs, outputs, and internal state. - Improved clarity by processing input kwargs and ensuring unique ID assignment for components. - Streamlined the setup process for inputs and outputs, enhancing overall structure and maintainability. * test: Add unit tests for ChatInput component to verify attribute independence
This commit is contained in:
parent
fffc7fb73f
commit
0e1f1a48e2
4 changed files with 199 additions and 67 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import operator
|
||||
import re
|
||||
from typing import Any, ClassVar
|
||||
from uuid import UUID
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from cachetools import TTLCache, cachedmethod
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -12,6 +11,9 @@ from langflow.custom.code_parser import CodeParser
|
|||
from langflow.custom.eval import eval_custom_component_code
|
||||
from langflow.utils import validate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class ComponentCodeNullError(HTTPException):
|
||||
pass
|
||||
|
|
@ -25,15 +27,15 @@ class BaseComponent:
|
|||
ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided."
|
||||
ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided."
|
||||
|
||||
_code: str | None = None
|
||||
"""The code of the component. Defaults to None."""
|
||||
_function_entrypoint_name: str = "build"
|
||||
field_config: dict = {}
|
||||
_user_id: str | UUID | None = None
|
||||
_template_config: dict = {}
|
||||
|
||||
def __init__(self, **data) -> None:
|
||||
self._code: str | None = None
|
||||
self._function_entrypoint_name: str = "build"
|
||||
self.field_config: dict = {}
|
||||
self._user_id: str | UUID | None = None
|
||||
self._template_config: dict = {}
|
||||
|
||||
self.cache: TTLCache = TTLCache(maxsize=1024, ttl=60)
|
||||
|
||||
for key, value in data.items():
|
||||
if key == "user_id":
|
||||
self._user_id = value
|
||||
|
|
@ -41,8 +43,12 @@ class BaseComponent:
|
|||
setattr(self, key, value)
|
||||
|
||||
def __setattr__(self, key, value) -> None:
|
||||
if key == "_user_id" and self._user_id is not None:
|
||||
logger.warning("user_id is immutable and cannot be changed.")
|
||||
if key == "_user_id":
|
||||
try:
|
||||
if self._user_id is not None:
|
||||
logger.warning("user_id is immutable and cannot be changed.")
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
super().__setattr__(key, value)
|
||||
|
||||
@cachedmethod(cache=operator.attrgetter("cache"))
|
||||
|
|
|
|||
|
|
@ -93,16 +93,27 @@ class Component(CustomComponent):
|
|||
inputs: list[InputTypes] = []
|
||||
outputs: list[Output] = []
|
||||
code_class_base_inheritance: ClassVar[str] = "Component"
|
||||
_output_logs: dict[str, list[Log]] = {}
|
||||
_current_output: str = ""
|
||||
_metadata: dict = {}
|
||||
_ctx: dict = {}
|
||||
_code: str | None = None
|
||||
_logs: list[Log] = []
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
# if key starts with _ it is a config
|
||||
# else it is an input
|
||||
# Initialize instance-specific attributes first
|
||||
self._output_logs: dict[str, list[Log]] = {}
|
||||
self._current_output: str = ""
|
||||
self._metadata: dict = {}
|
||||
self._ctx: dict = {}
|
||||
self._code: str | None = None
|
||||
self._logs: list[Log] = []
|
||||
|
||||
# Initialize component-specific collections
|
||||
self._inputs: dict[str, InputTypes] = {}
|
||||
self._outputs_map: dict[str, Output] = {}
|
||||
self._results: dict[str, Any] = {}
|
||||
self._attributes: dict[str, Any] = {}
|
||||
self._edges: list[EdgeData] = []
|
||||
self._components: list[Component] = []
|
||||
self._event_manager: EventManager | None = None
|
||||
self._state_model = None
|
||||
|
||||
# Process input kwargs
|
||||
inputs = {}
|
||||
config = {}
|
||||
for key, value in kwargs.items():
|
||||
|
|
@ -112,34 +123,35 @@ class Component(CustomComponent):
|
|||
config[key[1:]] = value
|
||||
else:
|
||||
inputs[key] = value
|
||||
self._inputs: dict[str, InputTypes] = {}
|
||||
self._outputs_map: dict[str, Output] = {}
|
||||
self._results: dict[str, Any] = {}
|
||||
self._attributes: dict[str, Any] = {}
|
||||
|
||||
self._parameters = inputs or {}
|
||||
self._edges: list[EdgeData] = []
|
||||
self._components: list[Component] = []
|
||||
self._current_output = ""
|
||||
self._event_manager: EventManager | None = None
|
||||
self._state_model = None
|
||||
self.set_attributes(self._parameters)
|
||||
self._output_logs = {}
|
||||
config = config or {}
|
||||
if "_id" not in config:
|
||||
config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"}
|
||||
|
||||
# Store original inputs and config for reference
|
||||
self.__inputs = inputs
|
||||
self.__config = config
|
||||
self._reset_all_output_values()
|
||||
super().__init__(**config)
|
||||
self.__config = config or {}
|
||||
|
||||
# Add unique ID if not provided
|
||||
if "_id" not in self.__config:
|
||||
self.__config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"}
|
||||
|
||||
# Initialize base class
|
||||
super().__init__(**self.__config)
|
||||
|
||||
# Post-initialization setup
|
||||
if hasattr(self, "_trace_type"):
|
||||
self.trace_type = self._trace_type
|
||||
if not hasattr(self, "trace_type"):
|
||||
self.trace_type = "chain"
|
||||
|
||||
# Setup inputs and outputs
|
||||
self._reset_all_output_values()
|
||||
if self.inputs is not None:
|
||||
self.map_inputs(self.inputs)
|
||||
if self.outputs is not None:
|
||||
self.map_outputs(self.outputs)
|
||||
# Set output types
|
||||
|
||||
# Final setup
|
||||
self._set_output_types(list(self._outputs_map.values()))
|
||||
self.set_class_code()
|
||||
self._set_output_required_inputs()
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ class CustomComponent(BaseComponent):
|
|||
_tree (Optional[dict]): The code tree of the custom component.
|
||||
"""
|
||||
|
||||
# True constants that should be shared (using ClassVar)
|
||||
_code_class_base_inheritance: ClassVar[str] = "CustomComponent"
|
||||
function_entrypoint_name: ClassVar[str] = "build"
|
||||
name: str | None = None
|
||||
"""The name of the component used to styles. Defaults to None."""
|
||||
display_name: str | None = None
|
||||
|
|
@ -58,36 +61,6 @@ class CustomComponent(BaseComponent):
|
|||
"""The description of the component. Defaults to None."""
|
||||
icon: str | None = None
|
||||
"""The icon of the component. It should be an emoji. Defaults to None."""
|
||||
is_input: bool | None = None
|
||||
"""The input state of the component. Defaults to None.
|
||||
If True, the component must have a field named 'input_value'."""
|
||||
add_tool_output: bool | None = False
|
||||
"""Indicates whether the component will be treated as a tool. Defaults to False."""
|
||||
is_output: bool | None = None
|
||||
"""The output state of the component. Defaults to None.
|
||||
If True, the component must have a field named 'input_value'."""
|
||||
field_config: dict = {}
|
||||
"""The field configuration of the component. Defaults to an empty dictionary."""
|
||||
field_order: list[str] | None = None
|
||||
"""The field order of the component. Defaults to an empty list."""
|
||||
frozen: bool | None = False
|
||||
"""The default frozen state of the component. Defaults to False."""
|
||||
build_parameters: dict | None = None
|
||||
"""The build parameters of the component. Defaults to None."""
|
||||
_vertex: Vertex | None = None
|
||||
"""The edge target parameter of the component. Defaults to None."""
|
||||
_code_class_base_inheritance: ClassVar[str] = "CustomComponent"
|
||||
function_entrypoint_name: ClassVar[str] = "build"
|
||||
function: Callable | None = None
|
||||
repr_value: Any | None = ""
|
||||
status: Any | None = None
|
||||
"""The status of the component. This is displayed on the frontend. Defaults to None."""
|
||||
_flows_data: list[Data] | None = None
|
||||
_outputs: list[OutputValue] = []
|
||||
_logs: list[Log] = []
|
||||
_output_logs: dict[str, list[Log] | Log] = {}
|
||||
_tracing_service: TracingService | None = None
|
||||
_tree: dict | None = None
|
||||
|
||||
def __init__(self, **data) -> None:
|
||||
"""Initializes a new instance of the CustomComponent class.
|
||||
|
|
@ -95,10 +68,33 @@ class CustomComponent(BaseComponent):
|
|||
Args:
|
||||
**data: Additional keyword arguments to initialize the custom component.
|
||||
"""
|
||||
self.cache: TTLCache = TTLCache(maxsize=1024, ttl=60)
|
||||
# Initialize instance-specific attributes first
|
||||
self.is_input: bool | None = None
|
||||
self.is_output: bool | None = None
|
||||
self.add_tool_output: bool = False
|
||||
self.field_config: dict = {}
|
||||
self.field_order: list[str] | None = None
|
||||
self.frozen: bool = False
|
||||
self.build_parameters: dict | None = None
|
||||
self._vertex: Vertex | None = None
|
||||
self.function: Callable | None = None
|
||||
self.repr_value: Any = ""
|
||||
self.status: Any | None = None
|
||||
|
||||
# Initialize collections with empty defaults
|
||||
self._flows_data: list[Data] | None = None
|
||||
self._outputs: list[OutputValue] = []
|
||||
self._logs: list[Log] = []
|
||||
self._output_logs: dict[str, list[Log] | Log] = {}
|
||||
self._tracing_service: TracingService | None = None
|
||||
self._tree: dict | None = None
|
||||
|
||||
# Initialize additional instance state
|
||||
self.cache: TTLCache = TTLCache(maxsize=1024, ttl=60)
|
||||
self._results: dict = {}
|
||||
self._artifacts: dict = {}
|
||||
|
||||
# Call parent's init after setting up our attributes
|
||||
super().__init__(**data)
|
||||
|
||||
def set_attributes(self, parameters: dict) -> None:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import pytest
|
||||
from langflow.components.inputs.chat import ChatInput
|
||||
from langflow.schema.message import Message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_input_instances():
|
||||
"""Create two instances of ChatInput for testing."""
|
||||
chat1 = ChatInput()
|
||||
chat2 = ChatInput()
|
||||
return chat1, chat2
|
||||
|
||||
|
||||
def test_input_value_independence(chat_input_instances):
|
||||
"""Test that input_value is independent between instances."""
|
||||
chat1, chat2 = chat_input_instances
|
||||
|
||||
# Set different input values
|
||||
chat1.build(input_value="Hello from chat1")
|
||||
chat2.build(input_value="Hello from chat2")
|
||||
|
||||
# Verify values are different
|
||||
assert chat1.input_value != chat2.input_value
|
||||
assert chat1.input_value == "Hello from chat1"
|
||||
assert chat2.input_value == "Hello from chat2"
|
||||
|
||||
|
||||
def test_sender_name_independence(chat_input_instances):
|
||||
"""Test that sender_name is independent between instances."""
|
||||
chat1, chat2 = chat_input_instances
|
||||
|
||||
# Set different sender names
|
||||
chat1.build(sender_name="Alice")
|
||||
chat2.build(sender_name="Bob")
|
||||
|
||||
# Verify values are different
|
||||
assert chat1.sender_name != chat2.sender_name
|
||||
assert chat1.sender_name == "Alice"
|
||||
assert chat2.sender_name == "Bob"
|
||||
|
||||
|
||||
def test_multiple_attributes_independence(chat_input_instances):
|
||||
"""Test that multiple attributes are independent between instances."""
|
||||
chat1, chat2 = chat_input_instances
|
||||
|
||||
# Set multiple attributes for chat1
|
||||
chat1.build(input_value="Message 1", sender_name="Alice", background_color="blue", text_color="white")
|
||||
|
||||
# Set different attributes for chat2
|
||||
chat2.build(input_value="Message 2", sender_name="Bob", background_color="red", text_color="black")
|
||||
|
||||
# Verify all attributes are independent
|
||||
assert chat1.input_value != chat2.input_value
|
||||
assert chat1.sender_name != chat2.sender_name
|
||||
assert chat1.background_color != chat2.background_color
|
||||
assert chat1.text_color != chat2.text_color
|
||||
|
||||
|
||||
async def test_message_output_independence(chat_input_instances):
|
||||
"""Test that message outputs are independent between instances."""
|
||||
chat1, chat2 = chat_input_instances
|
||||
|
||||
# Configure different messages
|
||||
chat1.build(
|
||||
input_value="Hello from chat1",
|
||||
sender_name="Alice",
|
||||
should_store_message=False, # Prevent actual message storage
|
||||
)
|
||||
chat2.build(
|
||||
input_value="Hello from chat2",
|
||||
sender_name="Bob",
|
||||
should_store_message=False, # Prevent actual message storage
|
||||
)
|
||||
|
||||
# Get messages from both instances
|
||||
message1 = await chat1.message_response()
|
||||
message2 = await chat2.message_response()
|
||||
|
||||
# Verify messages are different
|
||||
assert isinstance(message1, Message)
|
||||
assert isinstance(message2, Message)
|
||||
assert message1.text != message2.text
|
||||
assert message1.sender_name != message2.sender_name
|
||||
|
||||
|
||||
async def test_status_independence(chat_input_instances):
|
||||
"""Test that status attribute is independent between instances."""
|
||||
chat1, chat2 = chat_input_instances
|
||||
|
||||
# Configure and run messages
|
||||
chat1.build(input_value="Status test 1", sender_name="Alice", should_store_message=False)
|
||||
chat2.build(input_value="Status test 2", sender_name="Bob", should_store_message=False)
|
||||
|
||||
# Generate messages to update status
|
||||
await chat1.message_response()
|
||||
await chat2.message_response()
|
||||
|
||||
# Verify status values are different
|
||||
assert chat1.status != chat2.status
|
||||
assert chat1.status.text == "Status test 1"
|
||||
assert chat2.status.text == "Status test 2"
|
||||
|
||||
|
||||
def test_files_independence(chat_input_instances):
|
||||
"""Test that files attribute is independent between instances."""
|
||||
chat1, chat2 = chat_input_instances
|
||||
|
||||
# Set different files
|
||||
files1 = ["file1.txt", "file2.txt"]
|
||||
files2 = ["file3.txt", "file4.txt"]
|
||||
|
||||
chat1.build(files=files1)
|
||||
chat2.build(files=files2)
|
||||
|
||||
# Verify files are independent
|
||||
assert chat1.files != chat2.files
|
||||
assert chat1.files == files1
|
||||
assert chat2.files == files2
|
||||
Loading…
Add table
Add a link
Reference in a new issue