core[patch]: Raise AttributeError (instead of ModuleNotFoundError) in custom __getattr__ (#30905)

Follow up to https://github.com/langchain-ai/langchain/pull/30769,
fixing the regression reported
[here](https://github.com/langchain-ai/langchain/pull/30769#issuecomment-2807483610),
thanks @krassowski for the report!

Fix inspired by https://github.com/PrefectHQ/prefect/pull/16172/files

Other changes:
* Using tuples for `__all__`, except in `output_parsers` bc of a list
namespace conflict
* Using a helper function for imports due to repeated logic across
`__init__.py` files becoming hard to maintain.

Co-authored-by: Michał Krassowski < krassowski 5832902+krassowski@users.noreply.github.com>"
This commit is contained in:
Sydney Runkle 2025-04-17 14:15:28 -04:00 committed by GitHub
parent 61d2dc011e
commit 75e50a3efd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 142 additions and 153 deletions

View File

@ -9,9 +9,10 @@ This module is only relevant for LangChain developers, not for users.
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from .beta_decorator import (
LangChainBetaWarning,
@ -28,7 +29,7 @@ if TYPE_CHECKING:
)
from .path import as_import_path, get_relative_path
__all__ = [
__all__ = (
"as_import_path",
"beta",
"deprecated",
@ -40,7 +41,7 @@ __all__ = [
"suppress_langchain_deprecation_warning",
"surface_langchain_deprecation_warnings",
"warn_deprecated",
]
)
_dynamic_imports = {
"LangChainBetaWarning": "beta_decorator",
@ -59,12 +60,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -0,0 +1,34 @@
from importlib import import_module
from typing import Union
def import_attr(
attr_name: str,
module_name: Union[str, None],
package: Union[str, None],
) -> object:
"""Import an attribute from a module located in a package.
This utility function is used in custom __getattr__ methods within __init__.py
files to dynamically import attributes.
Args:
attr_name: The name of the attribute to import.
module_name: The name of the module to import from. If None, the attribute
is imported from the package itself.
package: The name of the package where the module is located.
"""
if module_name == "__module__" or module_name is None:
try:
result = import_module(f".{attr_name}", package=package)
except ModuleNotFoundError:
msg = f"module '{package!r}' has no attribute {attr_name!r}"
raise AttributeError(msg) from None
else:
try:
module = import_module(f".{module_name}", package=package)
except ModuleNotFoundError:
msg = f"module '{package!r}.{module_name!r}' not found"
raise ImportError(msg) from None
result = getattr(module, attr_name)
return result

View File

@ -7,9 +7,10 @@
BaseCallbackHandler --> <name>CallbackHandler # Example: AimCallbackHandler
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.callbacks.base import (
AsyncCallbackHandler,
@ -52,7 +53,7 @@ if TYPE_CHECKING:
get_usage_metadata_callback,
)
__all__ = [
__all__ = (
"dispatch_custom_event",
"adispatch_custom_event",
"RetrieverManagerMixin",
@ -87,7 +88,7 @@ __all__ = [
"FileCallbackHandler",
"UsageMetadataCallbackHandler",
"get_usage_metadata_callback",
]
)
_dynamic_imports = {
"AsyncCallbackHandler": "base",
@ -129,12 +130,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -1,21 +1,22 @@
"""Document loaders."""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.document_loaders.base import BaseBlobParser, BaseLoader
from langchain_core.document_loaders.blob_loaders import Blob, BlobLoader, PathLike
from langchain_core.document_loaders.langsmith import LangSmithLoader
__all__ = [
__all__ = (
"BaseBlobParser",
"BaseLoader",
"Blob",
"BlobLoader",
"PathLike",
"LangSmithLoader",
]
)
_dynamic_imports = {
"BaseBlobParser": "base",
@ -29,12 +30,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -5,15 +5,16 @@ and their transformations.
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from .base import Document
from .compressor import BaseDocumentCompressor
from .transformers import BaseDocumentTransformer
__all__ = ["Document", "BaseDocumentTransformer", "BaseDocumentCompressor"]
__all__ = ("Document", "BaseDocumentTransformer", "BaseDocumentCompressor")
_dynamic_imports = {
"Document": "base",
@ -24,12 +25,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -1,8 +1,9 @@
"""Embeddings."""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.embeddings.embeddings import Embeddings
from langchain_core.embeddings.fake import (
@ -10,7 +11,7 @@ if TYPE_CHECKING:
FakeEmbeddings,
)
__all__ = ["DeterministicFakeEmbedding", "Embeddings", "FakeEmbeddings"]
__all__ = ("DeterministicFakeEmbedding", "Embeddings", "FakeEmbeddings")
_dynamic_imports = {
"Embeddings": "embeddings",
@ -21,12 +22,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -4,9 +4,10 @@
This allows us to select examples that are most relevant to the input.
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.example_selectors.base import BaseExampleSelector
from langchain_core.example_selectors.length_based import (
@ -18,13 +19,13 @@ if TYPE_CHECKING:
sorted_values,
)
__all__ = [
__all__ = (
"BaseExampleSelector",
"LengthBasedExampleSelector",
"MaxMarginalRelevanceExampleSelector",
"SemanticSimilarityExampleSelector",
"sorted_values",
]
)
_dynamic_imports = {
"BaseExampleSelector": "base",
@ -37,12 +38,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -5,9 +5,10 @@ a vectorstore while avoiding duplicated content and over-writing content
if it's unchanged.
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.indexing.api import IndexingResult, aindex, index
from langchain_core.indexing.base import (
@ -18,7 +19,7 @@ if TYPE_CHECKING:
UpsertResponse,
)
__all__ = [
__all__ = (
"aindex",
"DeleteResponse",
"DocumentIndex",
@ -27,7 +28,7 @@ __all__ = [
"InMemoryRecordManager",
"RecordManager",
"UpsertResponse",
]
)
_dynamic_imports = {
"aindex": "api",
@ -43,12 +44,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -41,9 +41,10 @@ https://python.langchain.com/docs/how_to/custom_llm/
""" # noqa: E501
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.language_models.base import (
BaseLanguageModel,
@ -66,7 +67,7 @@ if TYPE_CHECKING:
)
from langchain_core.language_models.llms import LLM, BaseLLM
__all__ = [
__all__ = (
"BaseLanguageModel",
"BaseChatModel",
"SimpleChatModel",
@ -83,7 +84,7 @@ __all__ = [
"FakeMessagesListChatModel",
"GenericFakeChatModel",
"ParrotFakeChatModel",
]
)
_dynamic_imports = {
"BaseLanguageModel": "base",
@ -107,12 +108,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -1,8 +1,9 @@
"""**Load** module helps with serialization and deserialization."""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.load.dump import dumpd, dumps
from langchain_core.load.load import loads
@ -14,7 +15,7 @@ if TYPE_CHECKING:
# the `from langchain_core.load.load import load` absolute import should also work.
from langchain_core.load.load import load
__all__ = ["dumpd", "dumps", "load", "loads", "Serializable"]
__all__ = ("dumpd", "dumps", "load", "loads", "Serializable")
_dynamic_imports = {
"dumpd": "dump",
@ -26,12 +27,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -15,9 +15,10 @@
""" # noqa: E501
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.messages.ai import (
AIMessage,
@ -60,7 +61,7 @@ if TYPE_CHECKING:
trim_messages,
)
__all__ = [
__all__ = (
"AIMessage",
"AIMessageChunk",
"AnyMessage",
@ -95,7 +96,7 @@ __all__ = [
"merge_message_runs",
"trim_messages",
"convert_to_openai_messages",
]
)
_dynamic_imports = {
"AIMessage": "ai",
@ -137,12 +138,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -13,9 +13,10 @@
Serializable, Generation, PromptValue
""" # noqa: E501
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.output_parsers.base import (
BaseGenerationOutputParser,
@ -88,12 +89,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -21,9 +21,10 @@ in the AIMessage object, it is recommended to access it from there rather than
from the `LLMResult` object.
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.outputs.chat_generation import (
ChatGeneration,
@ -34,7 +35,7 @@ if TYPE_CHECKING:
from langchain_core.outputs.llm_result import LLMResult
from langchain_core.outputs.run_info import RunInfo
__all__ = [
__all__ = (
"ChatGeneration",
"ChatGenerationChunk",
"ChatResult",
@ -42,7 +43,7 @@ __all__ = [
"GenerationChunk",
"LLMResult",
"RunInfo",
]
)
_dynamic_imports = {
"ChatGeneration": "chat_generation",
@ -57,12 +58,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -25,9 +25,10 @@ from multiple components and prompt values. Prompt classes and functions make co
""" # noqa: E501
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.prompts.base import (
BasePromptTemplate,
@ -61,7 +62,7 @@ if TYPE_CHECKING:
validate_jinja2,
)
__all__ = [
__all__ = (
"AIMessagePromptTemplate",
"BaseChatPromptTemplate",
"BasePromptTemplate",
@ -83,7 +84,7 @@ __all__ = [
"get_template_variables",
"jinja2_formatter",
"validate_jinja2",
]
)
_dynamic_imports = {
"BasePromptTemplate": "base",
@ -112,12 +113,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -17,9 +17,10 @@ creating more responsive UX.
This module contains schema and implementation of LangChain Runnables primitives.
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.runnables.base import (
Runnable,
@ -58,7 +59,7 @@ if TYPE_CHECKING:
add,
)
__all__ = [
__all__ = (
"chain",
"AddableDict",
"ConfigurableField",
@ -88,7 +89,7 @@ __all__ = [
"get_config_list",
"aadd",
"add",
]
)
_dynamic_imports = {
"chain": "base",
@ -125,12 +126,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -19,9 +19,10 @@ tool for the job.
from __future__ import annotations
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.tools.base import (
FILTERED_ARGS,
@ -51,7 +52,7 @@ if TYPE_CHECKING:
from langchain_core.tools.simple import Tool
from langchain_core.tools.structured import StructuredTool
__all__ = [
__all__ = (
"ArgsSchema",
"BaseTool",
"BaseToolkit",
@ -71,7 +72,7 @@ __all__ = [
"create_retriever_tool",
"Tool",
"StructuredTool",
]
)
_dynamic_imports = {
"FILTERED_ARGS": "base",
@ -98,12 +99,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -8,9 +8,10 @@
--> <name> # Examples: LogStreamCallbackHandler
""" # noqa: E501
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.tracers.base import BaseTracer
from langchain_core.tracers.evaluation import EvaluatorCallbackHandler
@ -23,7 +24,7 @@ if TYPE_CHECKING:
from langchain_core.tracers.schemas import Run
from langchain_core.tracers.stdout import ConsoleCallbackHandler
__all__ = [
__all__ = (
"BaseTracer",
"EvaluatorCallbackHandler",
"LangChainTracer",
@ -32,7 +33,7 @@ __all__ = [
"RunLog",
"RunLogPatch",
"LogStreamCallbackHandler",
]
)
_dynamic_imports = {
"BaseTracer": "base",
@ -48,12 +49,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -3,9 +3,10 @@
These functions do not depend on any other LangChain module.
"""
from importlib import import_module
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
# for type checking and IDE support, we include the imports here
# but we don't want to eagerly import them at runtime
@ -36,7 +37,7 @@ if TYPE_CHECKING:
xor_args,
)
__all__ = [
__all__ = (
"build_extra_kwargs",
"StrictFormatter",
"check_package_version",
@ -63,7 +64,7 @@ __all__ = [
"abatch_iterate",
"from_env",
"secret_from_env",
]
)
_dynamic_imports = {
"image": "__module__",
@ -97,12 +98,7 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
package = __spec__.parent
if module_name == "__module__" or module_name is None:
result = import_module(f".{attr_name}", package=package)
else:
module = import_module(f".{module_name}", package=package)
result = getattr(module, attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result

View File

@ -1,11 +1,34 @@
"""Vector stores."""
from langchain_core.vectorstores.base import VST, VectorStore, VectorStoreRetriever
from langchain_core.vectorstores.in_memory import InMemoryVectorStore
from typing import TYPE_CHECKING
__all__ = [
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.vectorstores.base import VST, VectorStore, VectorStoreRetriever
from langchain_core.vectorstores.in_memory import InMemoryVectorStore
__all__ = (
"VectorStore",
"VST",
"VectorStoreRetriever",
"InMemoryVectorStore",
]
)
_dynamic_imports = {
"VectorStore": "base",
"VST": "base",
"VectorStoreRetriever": "base",
"InMemoryVectorStore": "in_memory",
}
def __getattr__(attr_name: str) -> object:
module_name = _dynamic_imports.get(attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
return result
def __dir__() -> list[str]:
return list(__all__)

View File

@ -3,7 +3,7 @@ from langchain_core.indexing import __all__
def test_all() -> None:
"""Use to catch obvious breaking changes."""
assert __all__ == sorted(__all__, key=str.lower)
assert list(__all__) == sorted(__all__, key=str.lower)
assert set(__all__) == {
"aindex",
"DeleteResponse",