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:
Eric Hare
2026-06-11 09:21:55 -07:00
parent 03a2439737
commit 14dfa7f268
7 changed files with 102 additions and 67 deletions

View File

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

View File

@ -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"},
}

View File

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

View File

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

View File

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

View File

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

View File

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