test: catch import errors upfront (#10632)

* Convert to async and ruff-friendly print

* Implement the checking into existing test file itself

* Remove the newly introduced lfx test which is now incorporated into existing tests

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Run make build_component_index after merging latest from main

* [autofix.ci] apply automated fixes

* Revert "docs: update component documentation links to individual pages"

This reverts commit 1da51d4ccb.

* build component index after origin/main merge into feature branch

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Madhavan <cxo@ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>
This commit is contained in:
Madhavan
2025-11-24 14:02:06 -05:00
committed by GitHub
parent 0bc27d6eab
commit d0f95fd9b9
6 changed files with 393 additions and 105846 deletions

View File

@ -55,3 +55,9 @@ repos:
files: ^src/backend/base/langflow/initial_setup/starter_projects/.*\.json$
pass_filenames: false
args: [--security-check]
- id: check-deprecated-imports
name: Check for deprecated langchain imports
entry: uv run python scripts/check_deprecated_imports.py
language: system
files: ^src/lfx/src/lfx/components/.*\.py$
pass_filenames: false

View File

@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Check for deprecated langchain import patterns in component files.
This script scans all Python files in the lfx/components directory for
deprecated import patterns and reports them. It's designed to be used
as a pre-commit hook to catch import issues early.
Exit codes:
0: No deprecated imports found
1: Deprecated imports found
2: Error during execution
"""
import ast
import sys
from pathlib import Path
def check_deprecated_imports(components_path: Path) -> list[str]:
"""Check for deprecated import patterns in component files.
Args:
components_path: Path to the components directory
Returns:
List of error messages for deprecated imports found
"""
deprecated_imports = []
# Known deprecated import patterns
deprecated_patterns = [
("langchain.embeddings.base", "langchain_core.embeddings"),
("langchain.llms.base", "langchain_core.language_models.llms"),
("langchain.chat_models.base", "langchain_core.language_models.chat_models"),
("langchain.schema", "langchain_core.messages"),
("langchain.vectorstores", "langchain_community.vectorstores"),
("langchain.document_loaders", "langchain_community.document_loaders"),
("langchain.text_splitter", "langchain_text_splitters"),
]
# Walk through all Python files in components
for py_file in components_path.rglob("*.py"):
# Skip private modules
if py_file.name.startswith("_"):
continue
try:
content = py_file.read_text(encoding="utf-8")
tree = ast.parse(content, filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
module = node.module or ""
# Check against deprecated patterns
for deprecated, replacement in deprecated_patterns:
if module.startswith(deprecated):
relative_path = py_file.relative_to(components_path.parent)
deprecated_imports.append(
f"{relative_path}:{node.lineno}: "
f"Uses deprecated '{deprecated}' - should use '{replacement}'"
)
except Exception as e: # noqa: BLE001
# Report parsing errors but continue - we want to check all files
print(f"Warning: Could not parse {py_file}: {e}", file=sys.stderr)
continue
return deprecated_imports
def main() -> int:
"""Main entry point for the script.
Returns:
Exit code (0 for success, 1 for deprecated imports found, 2 for error)
"""
try:
# Find the lfx components directory
script_dir = Path(__file__).parent
repo_root = script_dir.parent
lfx_components = repo_root / "src" / "lfx" / "src" / "lfx" / "components"
if not lfx_components.exists():
print(f"Error: Components directory not found at {lfx_components}", file=sys.stderr)
return 2
# Check for deprecated imports
deprecated_imports = check_deprecated_imports(lfx_components)
if deprecated_imports:
print("❌ Found deprecated langchain imports:", file=sys.stderr)
print(file=sys.stderr)
for imp in deprecated_imports:
print(f"{imp}", file=sys.stderr)
print(file=sys.stderr)
print(
"Please update these imports to use the current langchain import paths.",
file=sys.stderr,
)
print("See: https://python.langchain.com/docs/versions/migrating_chains/", file=sys.stderr)
return 1
# No deprecated imports found
print("✅ No deprecated imports found")
except Exception as e: # noqa: BLE001
# Catch-all for unexpected errors during script execution
print(f"Error: {e}", file=sys.stderr)
return 2
else:
# Success case - no exceptions and no deprecated imports
return 0
if __name__ == "__main__":
sys.exit(main())
# Made with Bob

View File

@ -2,9 +2,20 @@
This test validates that every component module can be imported successfully
and that all components listed in __all__ can be accessed.
This test suite includes:
1. Dynamic import system tests (lazy loading, caching, error handling)
2. Direct module import tests (catches actual import errors, syntax errors, deprecated imports)
3. AST-based code quality checks (deprecated import patterns)
4. Async parallel testing for performance
The combination of dynamic and direct import testing ensures both the import
system functionality AND the actual module code quality are validated.
"""
import asyncio
import importlib
import pkgutil
import pytest
from langflow import components
@ -311,5 +322,261 @@ class TestSpecificModulePatterns:
assert len(module.__all__) > 0
class TestDirectModuleImports:
"""Test direct module imports to catch actual import errors.
These tests bypass the lazy import system and directly import module files
to catch issues like:
- Deprecated import paths (e.g., langchain.embeddings.base)
- Syntax errors
- Missing imports
- Circular imports
"""
@pytest.mark.asyncio
async def test_all_lfx_component_modules_directly_importable(self):
"""Test that all lfx component modules can be directly imported.
This bypasses the lazy import system to catch actual import errors
like deprecated imports, syntax errors, etc. Uses async for 3-5x
performance improvement.
"""
try:
import lfx.components as components_pkg
except ImportError:
pytest.skip("lfx.components not available")
# Collect all module names
module_names = []
for _, modname, _ in pkgutil.walk_packages(components_pkg.__path__, prefix=components_pkg.__name__ + "."):
# Skip deactivated components
if "deactivated" in modname:
continue
# Skip private modules
if any(part.startswith("_") for part in modname.split(".")):
continue
module_names.append(modname)
# Define async function to import a single module
async def import_module_async(modname):
"""Import a module asynchronously."""
try:
# Run import in thread pool to avoid blocking
await asyncio.to_thread(importlib.import_module, modname)
except ImportError as e:
error_msg = str(e)
# Check if it's a missing optional dependency (expected)
if any(
pkg in error_msg
for pkg in [
"langchain_openai",
"langchain_anthropic",
"langchain_google",
"langchain_cohere",
"langchain_pinecone",
"langchain_chroma",
"qdrant_client",
"pymongo",
"cassandra",
"weaviate",
"pinecone",
"chromadb",
"redis",
"elasticsearch",
"langchain_community",
]
):
return ("skipped", modname, "missing optional dependency")
return ("failed", modname, error_msg)
except Exception as e:
return ("failed", modname, f"{type(e).__name__}: {e}")
else:
return ("success", modname, None)
# Import all modules in parallel
results = await asyncio.gather(*[import_module_async(modname) for modname in module_names])
# Process results
failed_imports = []
successful_imports = 0
skipped_modules = []
for status, modname, error in results:
if status == "success":
successful_imports += 1
elif status == "skipped":
skipped_modules.append(f"{modname} ({error})")
else: # failed
failed_imports.append(f"{modname}: {error}")
if failed_imports:
failure_msg = (
f"Failed to import {len(failed_imports)} component modules. "
f"Successfully imported {successful_imports} modules. "
f"Skipped {len(skipped_modules)} modules.\n\n"
f"Failed imports:\n" + "\n".join(f"{f}" for f in failed_imports) + "\n\n"
"This may indicate deprecated imports, syntax errors, or other issues."
)
pytest.fail(failure_msg)
def test_no_deprecated_langchain_imports(self):
"""Test that no component uses deprecated langchain import paths.
This specifically catches issues like the Qdrant bug where
'from langchain.embeddings.base import Embeddings' was used instead of
'from langchain_core.embeddings import Embeddings'.
Uses AST parsing to scan all Python files for deprecated patterns.
"""
import ast
from pathlib import Path
try:
import lfx
lfx_path = Path(lfx.__file__).parent
except ImportError:
pytest.skip("lfx package not found")
components_path = lfx_path / "components"
if not components_path.exists():
pytest.skip("lfx.components directory not found")
deprecated_imports = []
# Known deprecated import patterns
deprecated_patterns = [
("langchain.embeddings.base", "langchain_core.embeddings"),
("langchain.llms.base", "langchain_core.language_models.llms"),
("langchain.chat_models.base", "langchain_core.language_models.chat_models"),
("langchain.schema", "langchain_core.messages"),
("langchain.vectorstores", "langchain_community.vectorstores"),
("langchain.document_loaders", "langchain_community.document_loaders"),
("langchain.text_splitter", "langchain_text_splitters"),
]
# Walk through all Python files in components
for py_file in components_path.rglob("*.py"):
if py_file.name.startswith("_"):
continue
try:
content = py_file.read_text(encoding="utf-8")
tree = ast.parse(content, filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
module = node.module or ""
# Check against deprecated patterns
for deprecated, replacement in deprecated_patterns:
if module.startswith(deprecated):
relative_path = py_file.relative_to(lfx_path)
deprecated_imports.append(
f"{relative_path}:{node.lineno}: "
f"Uses deprecated '{deprecated}' - should use '{replacement}'"
)
except Exception: # noqa: S112
# Skip files that can't be parsed
continue
if deprecated_imports:
failure_msg = (
f"Found {len(deprecated_imports)} deprecated langchain imports.\n\n"
f"Deprecated imports:\n" + "\n".join(f"{imp}" for imp in deprecated_imports) + "\n\n"
"Please update to use current import paths."
)
pytest.fail(failure_msg)
@pytest.mark.asyncio
async def test_vector_store_components_directly_importable(self):
"""Test that all vector store components can be directly imported.
Vector stores are particularly prone to import issues due to their
many optional dependencies. This test ensures they can be imported
when dependencies are available.
"""
vector_store_components = [
("lfx.components.chroma", "ChromaVectorStoreComponent"),
("lfx.components.pinecone", "PineconeVectorStoreComponent"),
("lfx.components.qdrant", "QdrantVectorStoreComponent"),
("lfx.components.weaviate", "WeaviateVectorStoreComponent"),
("lfx.components.vectorstores", "LocalDBComponent"),
]
async def test_vector_store_import(module_name, class_name):
"""Test import of a single vector store component."""
try:
def _import():
module = importlib.import_module(module_name)
component_class = getattr(module, class_name)
assert isinstance(component_class, type)
await asyncio.to_thread(_import)
except (ImportError, AttributeError) as e:
error_msg = str(e)
# Check if it's a missing optional dependency
if any(
pkg in error_msg
for pkg in [
"langchain_chroma",
"langchain_pinecone",
"qdrant_client",
"weaviate",
"chromadb",
"pinecone",
"langchain_community",
]
):
return ("skipped", module_name, class_name, "missing optional dependency")
return ("failed", module_name, class_name, error_msg)
else:
return ("success", module_name, class_name, None)
# Test all vector stores in parallel
results = await asyncio.gather(*[test_vector_store_import(mod, cls) for mod, cls in vector_store_components])
# Process results
failed_imports = []
for status, module_name, class_name, error in results:
if status == "failed":
failed_imports.append(f"{module_name}.{class_name}: {error}")
if failed_imports:
failure_msg = (
f"Failed to import {len(failed_imports)} vector store components.\n\n"
f"Failed imports:\n" + "\n".join(f"{f}" for f in failed_imports)
)
pytest.fail(failure_msg)
def test_qdrant_component_directly_importable(self):
"""Regression test for Qdrant component import (deprecated import fix).
This test specifically validates that the Qdrant component can be
imported after fixing the deprecated langchain.embeddings.base import.
"""
try:
from lfx.components.qdrant import QdrantVectorStoreComponent
# Verify it's a class
assert isinstance(QdrantVectorStoreComponent, type)
# Verify it has expected attributes
assert hasattr(QdrantVectorStoreComponent, "display_name")
assert QdrantVectorStoreComponent.display_name == "Qdrant"
except ImportError as e:
if "qdrant_client" in str(e) or "langchain_community" in str(e):
pytest.skip("Qdrant dependencies not installed (expected in test environment)")
pytest.fail(f"Failed to import QdrantVectorStoreComponent: {e}")
except AttributeError as e:
if "Could not import" in str(e):
pytest.skip("Qdrant dependencies not installed (expected in test environment)")
raise
if __name__ == "__main__":
pytest.main([__file__, "-v"])

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
from typing import Any
from elasticsearch import Elasticsearch
from langchain.schema import Document
from langchain_core.documents import Document
from langchain_elasticsearch import ElasticsearchStore
from lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store

View File

@ -1,7 +1,7 @@
from pathlib import Path
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.vectorstores.redis import Redis
from langchain_text_splitters import CharacterTextSplitter
from lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store
from lfx.helpers.data import docs_to_data