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:
Gabriel Luiz Freitas Almeida 2024-12-18 15:59:17 -03:00 • committed by GitHub
commit 0e1f1a48e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 199 additions and 67 deletions

View file

@ -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"))

View file

@ -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()

View file

@ -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: