mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 00:39:35 +08:00
fix(tests): repoint lfx tests off moved providers; complete provider fallback map
CI fail-fast had been masking these: the LFX test job runs in an engine-only env where openai/anthropic/chroma are now bundle-package shims, so every test that used them as the example category failed on all Python versions (3.14 just reported first), and the backend Group 2 leg failed collecting test_lfx_bundles_extras.py on Python 3.10. - flow_requirements: complete _PROVIDER_PACKAGE_FALLBACKS for the nine moved model providers (OpenAI, Anthropic, Amazon Bedrock, Groq, Google Generative AI, SambaNova, IBM watsonx.ai, Ollama + existing Azure OpenAI). In engine-only installs MODEL_PROVIDERS_DICT registers only in-tree providers, so the dynamic source-inspection path cannot resolve moved providers; the static fallbacks keep lfx run/serve requirements inference working. A dict hit still wins. - test_dynamic_imports / test_import_utils: use composio (still-in-tree lazy category with its SDK absent in the bare env) as the example category instead of openai/anthropic/chroma; patch import_module at its canonical home lfx.utils.lazy_import. - flow-builder tests (build_flow_from_spec, flow_builder_tools, propose_field_edit): specs use LanguageModelComponent (in-tree) instead of OpenAIModel. - test_lfx_bundles_extras: tomli fallback for Python 3.10 (tomllib is stdlib 3.11+; pytest guarantees tomli on <3.11). Full lfx unit suite: 4550 passed. Backend extras+pin tests: 24 passed.
This commit is contained in:
@ -19,7 +19,10 @@ from __future__ import annotations
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # Python 3.10: tomllib is stdlib only on 3.11+
|
||||
import tomli as tomllib # pytest guarantees tomli on <3.11
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
METAPACKAGE_DIR = REPO_ROOT / "src" / "bundles" / "lfx-bundles"
|
||||
|
||||
@ -86,10 +86,22 @@ _INTERNAL_IMPORT_NAMES: frozenset[str] = frozenset({"lfx", "langflow", "langflow
|
||||
_MODEL_FIELDS = {"model", "agent_llm", "embeddings_model", "embedding_model"}
|
||||
|
||||
# Fallback provider → package mapping for providers whose component class may
|
||||
# not be importable in every environment (e.g. Azure OpenAI shares
|
||||
# langchain-openai with the regular OpenAI provider).
|
||||
# not be importable in every environment. Since the bundle split, provider
|
||||
# model components live in bundle distributions (lfx-openai, lfx-bundles, ...)
|
||||
# that an engine-only install does not carry, so MODEL_PROVIDERS_DICT registers
|
||||
# only in-tree providers there and the dynamic source-inspection path cannot
|
||||
# run. These static entries keep requirements inference working for flows
|
||||
# configured with those providers; a MODEL_PROVIDERS_DICT hit still wins.
|
||||
_PROVIDER_PACKAGE_FALLBACKS: dict[str, set[str]] = {
|
||||
"Amazon Bedrock": {"langchain-aws"},
|
||||
"Anthropic": {"langchain-anthropic"},
|
||||
"Azure OpenAI": {"langchain-openai"},
|
||||
"Google Generative AI": {"langchain-google-genai"},
|
||||
"Groq": {"langchain-groq"},
|
||||
"IBM watsonx.ai": {"langchain-ibm"},
|
||||
"Ollama": {"langchain-ollama"},
|
||||
"OpenAI": {"langchain-openai"},
|
||||
"SambaNova": {"langchain-sambanova"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -20,9 +20,11 @@ class TestImportUtils:
|
||||
|
||||
def test_import_mod_with_module_name(self):
|
||||
"""Test importing specific attribute from a module with missing dependencies."""
|
||||
# Test importing a class that has missing dependencies - should raise ModuleNotFoundError
|
||||
# composio: a still-in-tree category whose SDK is absent in the bare
|
||||
# lfx test env (openai/anthropic/chroma graduated to bundle packages,
|
||||
# so their lfx.components paths are shims now, not lazy categories).
|
||||
with pytest.raises(ModuleNotFoundError, match="No module named"):
|
||||
import_mod("OpenAIModelComponent", "openai_chat_model", "lfx.components.openai")
|
||||
import_mod("ComposioAPIComponent", "composio_api", "lfx.components.composio")
|
||||
|
||||
def test_import_mod_without_module_name(self):
|
||||
"""Test importing entire module when module_name is None."""
|
||||
@ -34,13 +36,13 @@ class TestImportUtils:
|
||||
def test_import_mod_module_not_found(self):
|
||||
"""Test error handling when module doesn't exist."""
|
||||
with pytest.raises(ImportError, match="not found"):
|
||||
import_mod("NonExistentComponent", "nonexistent_module", "lfx.components.openai")
|
||||
import_mod("NonExistentComponent", "nonexistent_module", "lfx.components.composio")
|
||||
|
||||
def test_import_mod_attribute_not_found(self):
|
||||
"""Test error handling when module has missing dependencies."""
|
||||
# The openai_chat_model module can't be imported due to missing dependencies
|
||||
# The composio_api module can't be imported due to missing dependencies
|
||||
with pytest.raises(ModuleNotFoundError, match="No module named"):
|
||||
import_mod("NonExistentComponent", "openai_chat_model", "lfx.components.openai")
|
||||
import_mod("NonExistentComponent", "composio_api", "lfx.components.composio")
|
||||
|
||||
|
||||
class TestComponentDynamicImports:
|
||||
@ -87,48 +89,51 @@ class TestComponentDynamicImports:
|
||||
_ = components.nonexistent_category
|
||||
|
||||
def test_category_module_dynamic_import(self):
|
||||
"""Test dynamic import behavior in category modules like openai."""
|
||||
import lfx.components.openai as openai_components
|
||||
"""Test dynamic import behavior in category modules like composio."""
|
||||
# composio: still-in-tree lazy category whose SDK is absent in the
|
||||
# bare lfx test env (openai graduated to the lfx-openai package, so
|
||||
# lfx.components.openai is a shim now, not a lazy category).
|
||||
import lfx.components.composio as composio_components
|
||||
|
||||
# Test that components are in __all__
|
||||
assert "OpenAIModelComponent" in openai_components.__all__
|
||||
assert "OpenAIEmbeddingsComponent" in openai_components.__all__
|
||||
assert "ComposioAPIComponent" in composio_components.__all__
|
||||
assert "ComposioGmailAPIComponent" in composio_components.__all__
|
||||
|
||||
# Access component - this should raise AttributeError due to missing langchain-openai
|
||||
with pytest.raises(AttributeError, match="Could not import 'OpenAIModelComponent'"):
|
||||
_ = openai_components.OpenAIModelComponent
|
||||
# Access component - this should raise AttributeError due to missing composio SDK
|
||||
with pytest.raises(AttributeError, match="Could not import 'ComposioAPIComponent'"):
|
||||
_ = composio_components.ComposioAPIComponent
|
||||
|
||||
# Test that the error is properly cached - second access should also fail
|
||||
with pytest.raises(AttributeError, match="Could not import 'OpenAIModelComponent'"):
|
||||
_ = openai_components.OpenAIModelComponent
|
||||
with pytest.raises(AttributeError, match="Could not import 'ComposioAPIComponent'"):
|
||||
_ = composio_components.ComposioAPIComponent
|
||||
|
||||
def test_category_module_dir(self):
|
||||
"""Test __dir__ functionality for category modules."""
|
||||
import lfx.components.openai as openai_components
|
||||
import lfx.components.composio as composio_components
|
||||
|
||||
dir_result = dir(openai_components)
|
||||
assert "OpenAIModelComponent" in dir_result
|
||||
assert "OpenAIEmbeddingsComponent" in dir_result
|
||||
dir_result = dir(composio_components)
|
||||
assert "ComposioAPIComponent" in dir_result
|
||||
assert "ComposioGmailAPIComponent" in dir_result
|
||||
|
||||
def test_category_module_missing_component(self):
|
||||
"""Test error handling for non-existent component in category."""
|
||||
import lfx.components.openai as openai_components
|
||||
import lfx.components.composio as composio_components
|
||||
|
||||
with pytest.raises(AttributeError, match="has no attribute 'NonExistentComponent'"):
|
||||
_ = openai_components.NonExistentComponent
|
||||
_ = composio_components.NonExistentComponent
|
||||
|
||||
def test_multiple_category_modules(self):
|
||||
"""Test dynamic imports work across multiple category modules."""
|
||||
import lfx.components.anthropic as anthropic_components
|
||||
import lfx.components.data as data_components
|
||||
import lfx.components.huggingface as huggingface_components
|
||||
|
||||
# Test different categories work independently
|
||||
# AnthropicModelComponent should work if anthropic library is available
|
||||
# HuggingFaceEndpointsComponent should work if its deps are available
|
||||
try:
|
||||
anthropic_component = anthropic_components.AnthropicModelComponent
|
||||
hf_component = huggingface_components.HuggingFaceEndpointsComponent
|
||||
# If it succeeds, just check it's a valid component
|
||||
assert anthropic_component is not None
|
||||
assert hasattr(anthropic_component, "__name__")
|
||||
assert hf_component is not None
|
||||
assert hasattr(hf_component, "__name__")
|
||||
except AttributeError:
|
||||
# If it fails due to missing dependencies, that's also expected
|
||||
pass
|
||||
@ -139,7 +144,7 @@ class TestComponentDynamicImports:
|
||||
assert hasattr(api_component, "__name__")
|
||||
|
||||
# Test that __all__ still works correctly despite import failures
|
||||
assert "AnthropicModelComponent" in anthropic_components.__all__
|
||||
assert "HuggingFaceEndpointsComponent" in huggingface_components.__all__
|
||||
assert "APIRequestComponent" in data_components.__all__
|
||||
|
||||
def test_backward_compatibility(self):
|
||||
@ -195,15 +200,15 @@ class TestComponentDynamicImports:
|
||||
|
||||
def test_consistency_check(self):
|
||||
"""Test that __all__ and _dynamic_imports are consistent."""
|
||||
import lfx.components.openai as openai_components
|
||||
import lfx.components.composio as composio_components
|
||||
|
||||
# All items in __all__ should have corresponding entries in _dynamic_imports
|
||||
for component_name in openai_components.__all__:
|
||||
assert component_name in openai_components._dynamic_imports
|
||||
for component_name in composio_components.__all__:
|
||||
assert component_name in composio_components._dynamic_imports
|
||||
|
||||
# All keys in _dynamic_imports should be in __all__
|
||||
for component_name in openai_components._dynamic_imports:
|
||||
assert component_name in openai_components.__all__
|
||||
for component_name in composio_components._dynamic_imports:
|
||||
assert component_name in composio_components.__all__
|
||||
|
||||
def test_type_checking_imports(self):
|
||||
"""Test that TYPE_CHECKING imports work correctly with dynamic loading."""
|
||||
@ -235,11 +240,13 @@ class TestPerformanceCharacteristics:
|
||||
|
||||
def test_lazy_loading_performance(self):
|
||||
"""Test that components can be accessed and cached properly."""
|
||||
from lfx.components import chroma as chromamodules
|
||||
# composio: still-in-tree lazy category with its SDK absent in the
|
||||
# bare lfx test env (chroma graduated to the lfx-bundles metapackage).
|
||||
from lfx.components import composio as composio_modules
|
||||
|
||||
# Test that we can access a component
|
||||
with pytest.raises(AttributeError, match=r"Could not import.*ChromaVectorStoreComponent"):
|
||||
chromamodules.ChromaVectorStoreComponent # noqa: B018
|
||||
with pytest.raises(AttributeError, match=r"Could not import.*ComposioAPIComponent"):
|
||||
composio_modules.ComposioAPIComponent # noqa: B018
|
||||
|
||||
def test_memory_usage_multiple_accesses(self):
|
||||
"""Test memory behavior with multiple component accesses."""
|
||||
@ -293,16 +300,16 @@ class TestSpecialCases:
|
||||
from lfx import components
|
||||
|
||||
# Test that we can access nested components through the hierarchy
|
||||
# OpenAI component requires langchain_openai which isn't installed
|
||||
with pytest.raises(AttributeError, match=r"Could not import.*OpenAIModelComponent"):
|
||||
_ = components.openai.OpenAIModelComponent
|
||||
# Composio components require the composio SDK which isn't installed
|
||||
with pytest.raises(AttributeError, match=r"Could not import.*ComposioAPIComponent"):
|
||||
_ = components.composio.ComposioAPIComponent
|
||||
|
||||
# APIRequestComponent should work now that validators is installed
|
||||
api_component = components.data.APIRequestComponent
|
||||
assert api_component is not None
|
||||
|
||||
# Test that both main module and submodules are properly cached
|
||||
assert "openai" in components.__dict__
|
||||
assert "composio" in components.__dict__
|
||||
assert "data" in components.__dict__
|
||||
|
||||
|
||||
|
||||
@ -29,12 +29,14 @@ edges:
|
||||
assert "B" in result["node_id_map"]
|
||||
|
||||
def test_three_node_flow(self):
|
||||
# LanguageModelComponent: a model type that stays in-tree (provider
|
||||
# model components live in bundle packages not installed in this env).
|
||||
spec = """\
|
||||
name: Chat with Model
|
||||
|
||||
nodes:
|
||||
A: ChatInput
|
||||
B: OpenAIModel
|
||||
B: LanguageModelComponent
|
||||
C: ChatOutput
|
||||
|
||||
edges:
|
||||
@ -262,7 +264,7 @@ name: Chat Flow
|
||||
|
||||
nodes:
|
||||
A: ChatInput
|
||||
B: OpenAIModel
|
||||
B: LanguageModelComponent
|
||||
C: ChatOutput
|
||||
|
||||
edges:
|
||||
@ -273,7 +275,7 @@ edges:
|
||||
summary = flow_to_spec_summary(result["flow"])
|
||||
assert "Chat Flow" in summary
|
||||
assert "ChatInput" in summary
|
||||
assert "OpenAIModel" in summary
|
||||
assert "LanguageModelComponent" in summary
|
||||
assert "ChatOutput" in summary
|
||||
assert "connections:" in summary
|
||||
|
||||
|
||||
@ -299,20 +299,22 @@ class TestBuildFlowFromSpec:
|
||||
assert len(events) == 0
|
||||
|
||||
# Regression: the agent has been observed to build flows with components
|
||||
# that have no edges (e.g. an OpenAIModel sitting next to an Agent with
|
||||
# that have no edges (e.g. a model component sitting next to an Agent with
|
||||
# nothing wired to it). Reject those so the LLM auto-corrects via retry.
|
||||
def test_build_should_reject_orphan_components(self):
|
||||
reset_working_flow()
|
||||
comp = BuildFlowFromSpec()
|
||||
# ChatInput connects to ChatOutput, but OpenAIModel is added without
|
||||
# any edge. This must fail validation, not silently produce a flow.
|
||||
# ChatInput connects to ChatOutput, but LanguageModelComponent is added
|
||||
# without any edge. This must fail validation, not silently produce a
|
||||
# flow. (In-tree model type; provider models live in bundle packages
|
||||
# not installed in this env.)
|
||||
comp.set(
|
||||
spec=(
|
||||
"name: Has Orphan\n"
|
||||
"nodes:\n"
|
||||
" A: ChatInput\n"
|
||||
" B: ChatOutput\n"
|
||||
" X: OpenAIModel\n"
|
||||
" X: LanguageModelComponent\n"
|
||||
"edges:\n"
|
||||
" A.message -> B.input_value\n"
|
||||
),
|
||||
@ -837,9 +839,11 @@ class TestConnectComponents:
|
||||
|
||||
reset_working_flow()
|
||||
|
||||
add_openai = AddComponent()
|
||||
add_openai.set(component_type="OpenAIModel")
|
||||
openai_id = add_openai.add_component().data["id"]
|
||||
# In-tree model type; provider models live in bundle packages not
|
||||
# installed in this env.
|
||||
add_model = AddComponent()
|
||||
add_model.set(component_type="LanguageModelComponent")
|
||||
model_id = add_model.add_component().data["id"]
|
||||
|
||||
add_agent = AddComponent()
|
||||
add_agent.set(component_type="Agent")
|
||||
@ -849,7 +853,7 @@ class TestConnectComponents:
|
||||
|
||||
conn = ConnectComponents()
|
||||
conn.set(
|
||||
source_id=openai_id,
|
||||
source_id=model_id,
|
||||
source_output="model_output",
|
||||
target_id=agent_id,
|
||||
target_input="model",
|
||||
@ -874,9 +878,9 @@ class TestConnectComponents:
|
||||
""" # noqa: D205
|
||||
reset_working_flow()
|
||||
|
||||
add_openai = AddComponent()
|
||||
add_openai.set(component_type="OpenAIModel")
|
||||
openai_id = add_openai.add_component().data["id"]
|
||||
add_model = AddComponent()
|
||||
add_model.set(component_type="LanguageModelComponent")
|
||||
model_id = add_model.add_component().data["id"]
|
||||
|
||||
add_agent = AddComponent()
|
||||
add_agent.set(component_type="Agent")
|
||||
@ -886,7 +890,7 @@ class TestConnectComponents:
|
||||
|
||||
conn = ConnectComponents()
|
||||
conn.set(
|
||||
source_id=openai_id,
|
||||
source_id=model_id,
|
||||
source_output="model_output",
|
||||
target_id=agent_id,
|
||||
target_input="model",
|
||||
|
||||
@ -32,8 +32,9 @@ class TestImportAttr:
|
||||
|
||||
def test_import_modibute_from_module(self):
|
||||
"""Test importing a specific attribute from a module."""
|
||||
# Test importing a class from a specific module
|
||||
result = import_mod("AnthropicModelComponent", "anthropic", "lfx.components.anthropic")
|
||||
# Test importing a class from a specific module (in-tree category;
|
||||
# provider categories like anthropic are bundle-package shims now).
|
||||
result = import_mod("LanguageModelComponent", "language_model", "lfx.components.models_and_agents")
|
||||
|
||||
assert result is not None
|
||||
assert hasattr(result, "__name__")
|
||||
@ -42,7 +43,7 @@ class TestImportAttr:
|
||||
def test_import_nonexistent_module(self):
|
||||
"""Test error handling when module doesn't exist."""
|
||||
with pytest.raises(ImportError, match="not found"):
|
||||
import_mod("SomeComponent", "nonexistent_module", "lfx.components.openai")
|
||||
import_mod("SomeComponent", "nonexistent_module", "lfx.components.composio")
|
||||
|
||||
def test_module_not_found_with_none_module_name(self):
|
||||
"""Test ModuleNotFoundError handling when module_name is None."""
|
||||
@ -57,7 +58,7 @@ class TestImportAttr:
|
||||
def test_import_nonexistent_attribute(self):
|
||||
"""Test error handling when attribute doesn't exist in module."""
|
||||
with pytest.raises(AttributeError):
|
||||
import_mod("NonExistentComponent", "anthropic", "lfx.components.anthropic")
|
||||
import_mod("NonExistentComponent", "language_model", "lfx.components.models_and_agents")
|
||||
|
||||
def test_import_with_none_package(self):
|
||||
"""Test behavior when package is None."""
|
||||
@ -67,7 +68,10 @@ class TestImportAttr:
|
||||
|
||||
def test_module_not_found_error_handling(self):
|
||||
"""Test specific ModuleNotFoundError handling."""
|
||||
with patch("lfx.components._importing.import_module") as mock_import_module:
|
||||
# import_mod's canonical home is lfx.utils.lazy_import; patch the
|
||||
# import_module it actually calls (lfx.components._importing is a
|
||||
# re-export and no longer binds import_module).
|
||||
with patch("lfx.utils.lazy_import.import_module") as mock_import_module:
|
||||
mock_import_module.side_effect = ModuleNotFoundError("No module named 'test.package.test_module'")
|
||||
|
||||
with pytest.raises(ImportError, match=r"module .* not found"):
|
||||
@ -115,13 +119,14 @@ class TestImportAttr:
|
||||
|
||||
def test_return_value_types(self):
|
||||
"""Test that import_mod returns appropriate types."""
|
||||
# Test module import
|
||||
module_result = import_mod("openai", "__module__", "lfx.components")
|
||||
# Test module import (composio: in-tree lazy category whose SDK is
|
||||
# absent in the bare lfx env; openai is a bundle-package shim now)
|
||||
module_result = import_mod("composio", "__module__", "lfx.components")
|
||||
assert hasattr(module_result, "__name__")
|
||||
|
||||
# Test class import - this should fail due to missing langchain-openai dependency
|
||||
# Test class import - this should fail due to the missing composio SDK
|
||||
with pytest.raises((ImportError, ModuleNotFoundError)):
|
||||
import_mod("OpenAIModelComponent", "openai_chat_model", "lfx.components.openai")
|
||||
import_mod("ComposioAPIComponent", "composio_api", "lfx.components.composio")
|
||||
|
||||
def test_caching_independence(self):
|
||||
"""Test that import_mod doesn't interfere with Python's module caching."""
|
||||
|
||||
@ -14,11 +14,13 @@ from lfx.mcp.flow_builder_tools import (
|
||||
reset_working_flow,
|
||||
)
|
||||
|
||||
# LanguageModelComponent: in-tree model type (provider model components live
|
||||
# in bundle packages not installed in this env).
|
||||
SIMPLE_FLOW_SPEC = """\
|
||||
name: Test Flow
|
||||
nodes:
|
||||
A: ChatInput
|
||||
B: OpenAIModel
|
||||
B: LanguageModelComponent
|
||||
C: ChatOutput
|
||||
edges:
|
||||
A.message -> B.input_value
|
||||
@ -193,7 +195,7 @@ class TestProposeFieldEditMultiple:
|
||||
flow = _build_test_flow()
|
||||
init_working_flow(flow, "test-flow-id")
|
||||
input_id = _get_component_id(flow, "ChatInput")
|
||||
model_id = _get_component_id(flow, "OpenAIModel")
|
||||
model_id = _get_component_id(flow, "LanguageModelComponent")
|
||||
|
||||
tool1 = ProposeFieldEdit()
|
||||
tool1.set(component_id=input_id, field_name="input_value", new_value="hello")
|
||||
|
||||
Reference in New Issue
Block a user