mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-23 17:58:35 +08:00
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:
@ -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
|
||||
|
||||
117
scripts/check_deprecated_imports.py
Executable file
117
scripts/check_deprecated_imports.py
Executable 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
|
||||
@ -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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user