libs/core/langchain_core/utils/utils.py PYTHON 522 lines View on github.com → Search inside
1"""Generic utility functions."""23import contextlib4import datetime5import functools6import importlib7import os8import warnings9from collections.abc import Callable, Iterator, Sequence10from importlib.metadata import version11from typing import Any, overload12from uuid import uuid41314from packaging.version import parse15from pydantic import SecretStr16from requests import HTTPError, Response17from typing_extensions import override1819from langchain_core.utils.pydantic import (20    is_pydantic_v1_subclass,21)222324def xor_args(*arg_groups: tuple[str, ...]) -> Callable:25    """Validate specified keyword args are mutually exclusive.2627    Args:28        *arg_groups: Groups of mutually exclusive keyword args.2930    Returns:31        Decorator that validates the specified keyword args are mutually exclusive.32    """3334    def decorator(func: Callable) -> Callable:35        @functools.wraps(func)36        def wrapper(*args: Any, **kwargs: Any) -> Any:37            """Validate exactly one arg in each group is not None."""38            counts = [39                sum(1 for arg in arg_group if kwargs.get(arg) is not None)40                for arg_group in arg_groups41            ]42            invalid_groups = [i for i, count in enumerate(counts) if count != 1]43            if invalid_groups:44                invalid_group_names = [", ".join(arg_groups[i]) for i in invalid_groups]45                msg = (46                    "Exactly one argument in each of the following"47                    " groups must be defined:"48                    f" {', '.join(invalid_group_names)}"49                )50                raise ValueError(msg)51            return func(*args, **kwargs)5253        return wrapper5455    return decorator565758def raise_for_status_with_text(response: Response) -> None:59    """Raise an error with the response text.6061    Args:62        response: The response to check for errors.6364    Raises:65        ValueError: If the response has an error status code.66    """67    try:68        response.raise_for_status()69    except HTTPError as e:70        raise ValueError(response.text) from e717273@contextlib.contextmanager74def mock_now(dt_value: datetime.datetime) -> Iterator[type]:75    """Context manager for mocking out datetime.now() in unit tests.7677    Args:78        dt_value: The datetime value to use for datetime.now().7980    Yields:81        The mocked datetime class.8283    Example:84        ```python85        with mock_now(datetime.datetime(2011, 2, 3, 10, 11)):86            assert datetime.datetime.now() == datetime.datetime(2011, 2, 3, 10, 11)87        ```88    """8990    class MockDateTime(datetime.datetime):91        """Mock datetime.datetime.now() with a fixed datetime."""9293        @classmethod94        @override95        def now(cls, tz: datetime.tzinfo | None = None) -> "MockDateTime":96            # Create a copy of dt_value.97            return MockDateTime(98                dt_value.year,99                dt_value.month,100                dt_value.day,101                dt_value.hour,102                dt_value.minute,103                dt_value.second,104                dt_value.microsecond,105                dt_value.tzinfo,106            )107108    real_datetime = datetime.datetime109    datetime.datetime = MockDateTime  # type: ignore[misc]110    try:111        yield datetime.datetime112    finally:113        datetime.datetime = real_datetime  # type: ignore[misc]114115116def guard_import(117    module_name: str, *, pip_name: str | None = None, package: str | None = None118) -> Any:119    """Dynamically import a module.120121    Raise an exception if the module is not installed.122123    Args:124        module_name: The name of the module to import.125        pip_name: The name of the module to install with pip.126        package: The package to import the module from.127128    Returns:129        The imported module.130131    Raises:132        ImportError: If the module is not installed.133    """134    try:135        module = importlib.import_module(module_name, package)136    except (ImportError, ModuleNotFoundError) as e:137        pip_name = pip_name or module_name.split(".", maxsplit=1)[0].replace("_", "-")138        msg = (139            f"Could not import {module_name} python package. "140            f"Please install it with `pip install {pip_name}`."141        )142        raise ImportError(msg) from e143    return module144145146def check_package_version(147    package: str,148    lt_version: str | None = None,149    lte_version: str | None = None,150    gt_version: str | None = None,151    gte_version: str | None = None,152) -> None:153    """Check the version of a package.154155    Args:156        package: The name of the package.157        lt_version: The version must be less than this.158        lte_version: The version must be less than or equal to this.159        gt_version: The version must be greater than this.160        gte_version: The version must be greater than or equal to this.161162163    Raises:164        ValueError: If the package version does not meet the requirements.165    """166    imported_version = parse(version(package))167    if lt_version is not None and imported_version >= parse(lt_version):168        msg = (169            f"Expected {package} version to be < {lt_version}. Received "170            f"{imported_version}."171        )172        raise ValueError(msg)173    if lte_version is not None and imported_version > parse(lte_version):174        msg = (175            f"Expected {package} version to be <= {lte_version}. Received "176            f"{imported_version}."177        )178        raise ValueError(msg)179    if gt_version is not None and imported_version <= parse(gt_version):180        msg = (181            f"Expected {package} version to be > {gt_version}. Received "182            f"{imported_version}."183        )184        raise ValueError(msg)185    if gte_version is not None and imported_version < parse(gte_version):186        msg = (187            f"Expected {package} version to be >= {gte_version}. Received "188            f"{imported_version}."189        )190        raise ValueError(msg)191192193def get_pydantic_field_names(pydantic_cls: Any) -> set[str]:194    """Get field names, including aliases, for a pydantic class.195196    Args:197        pydantic_cls: Pydantic class.198199    Returns:200        Field names.201    """202    all_required_field_names = set()203    if is_pydantic_v1_subclass(pydantic_cls):204        for field in pydantic_cls.__fields__.values():205            all_required_field_names.add(field.name)206            if field.has_alias:207                all_required_field_names.add(field.alias)208    else:  # Assuming pydantic 2 for now209        for name, field in pydantic_cls.model_fields.items():210            all_required_field_names.add(name)211            if field.alias:212                all_required_field_names.add(field.alias)213    return all_required_field_names214215216def _build_model_kwargs(217    values: dict[str, Any],218    all_required_field_names: set[str],219) -> dict[str, Any]:220    """Build `model_kwargs` param from Pydantic constructor values.221222    Args:223        values: All init args passed in by user.224        all_required_field_names: All required field names for the pydantic class.225226    Returns:227        Extra kwargs.228229    Raises:230        ValueError: If a field is specified in both `values` and `extra_kwargs`.231        ValueError: If a field is specified in `model_kwargs`.232    """233    extra_kwargs = values.get("model_kwargs", {})234    for field_name in list(values):235        if field_name in extra_kwargs:236            msg = f"Found {field_name} supplied twice."237            raise ValueError(msg)238        if field_name not in all_required_field_names:239            warnings.warn(240                f"""WARNING! {field_name} is not default parameter.241                {field_name} was transferred to model_kwargs.242                Please confirm that {field_name} is what you intended.""",243                stacklevel=7,244            )245            extra_kwargs[field_name] = values.pop(field_name)246247    invalid_model_kwargs = all_required_field_names.intersection(extra_kwargs.keys())248    if invalid_model_kwargs:249        warnings.warn(250            f"Parameters {invalid_model_kwargs} should be specified explicitly. "251            f"Instead they were passed in as part of `model_kwargs` parameter.",252            stacklevel=7,253        )254        for k in invalid_model_kwargs:255            values[k] = extra_kwargs.pop(k)256257    values["model_kwargs"] = extra_kwargs258    return values259260261# DON'T USE! Kept for backwards-compatibility but should never have been public.262def build_extra_kwargs(263    extra_kwargs: dict[str, Any],264    values: dict[str, Any],265    all_required_field_names: set[str],266) -> dict[str, Any]:267    """Build extra kwargs from values and extra_kwargs.268269    !!! danger "DON'T USE"270271        Kept for backwards-compatibility but should never have been public. Use the272        internal `_build_model_kwargs` function instead.273274    Args:275        extra_kwargs: Extra kwargs passed in by user.276        values: Values passed in by user.277        all_required_field_names: All required field names for the pydantic class.278279    Returns:280        Extra kwargs.281282    Raises:283        ValueError: If a field is specified in both `values` and `extra_kwargs`.284        ValueError: If a field is specified in `model_kwargs`.285    """286    # DON'T USE! Kept for backwards-compatibility but should never have been public.287    for field_name in list(values):288        if field_name in extra_kwargs:289            msg = f"Found {field_name} supplied twice."290            raise ValueError(msg)291        if field_name not in all_required_field_names:292            warnings.warn(293                f"""WARNING! {field_name} is not default parameter.294                {field_name} was transferred to model_kwargs.295                Please confirm that {field_name} is what you intended.""",296                stacklevel=7,297            )298            extra_kwargs[field_name] = values.pop(field_name)299300    # DON'T USE! Kept for backwards-compatibility but should never have been public.301    invalid_model_kwargs = all_required_field_names.intersection(extra_kwargs.keys())302    if invalid_model_kwargs:303        msg = (304            f"Parameters {invalid_model_kwargs} should be specified explicitly. "305            f"Instead they were passed in as part of `model_kwargs` parameter."306        )307        raise ValueError(msg)308309    # DON'T USE! Kept for backwards-compatibility but should never have been public.310    return extra_kwargs311312313def convert_to_secret_str(value: SecretStr | str) -> SecretStr:314    """Convert a string to a `SecretStr` if needed.315316    Args:317        value: The value to convert.318319    Returns:320        The `SecretStr` value.321    """322    if isinstance(value, SecretStr):323        return value324    return SecretStr(value)325326327class _NoDefaultType:328    """Type to indicate no default value is provided."""329330331_NoDefault = _NoDefaultType()332333334@overload335def from_env(key: str, /) -> Callable[[], str]: ...336337338@overload339def from_env(key: str, /, *, default: str) -> Callable[[], str]: ...340341342@overload343def from_env(key: Sequence[str], /, *, default: str) -> Callable[[], str]: ...344345346@overload347def from_env(key: str, /, *, error_message: str) -> Callable[[], str]: ...348349350@overload351def from_env(352    key: str | Sequence[str], /, *, default: str, error_message: str | None353) -> Callable[[], str]: ...354355356@overload357def from_env(358    key: str, /, *, default: None, error_message: str | None359) -> Callable[[], str | None]: ...360361362@overload363def from_env(364    key: str | Sequence[str], /, *, default: None365) -> Callable[[], str | None]: ...366367368def from_env(369    key: str | Sequence[str],370    /,371    *,372    default: str | _NoDefaultType | None = _NoDefault,373    error_message: str | None = None,374) -> Callable[[], str] | Callable[[], str | None]:375    """Create a factory method that gets a value from an environment variable.376377    Args:378        key: The environment variable to look up.379380            If a list of keys is provided, the first key found in the environment will381            be used. If no key is found, the default value will be used if set,382            otherwise an error will be raised.383        default: The default value to return if the environment variable is not set.384        error_message: The error message which will be raised if the key is not found385            and no default value is provided.386387            This will be raised as a ValueError.388389    Returns:390        Factory method that will look up the value from the environment.391    """392393    def get_from_env_fn() -> str | None:394        """Get a value from an environment variable.395396        Raises:397            ValueError: If the environment variable is not set and no default is398                provided.399400        Returns:401            The value from the environment.402        """403        if isinstance(key, (list, tuple)):404            for k in key:405                if k in os.environ:406                    return os.environ[k]407        if isinstance(key, str) and key in os.environ:408            return os.environ[key]409410        if isinstance(default, (str, type(None))):411            return default412        if error_message:413            raise ValueError(error_message)414        msg = (415            f"Did not find {key}, please add an environment variable"416            f" `{key}` which contains it, or pass"417            f" `{key}` as a named parameter."418        )419        raise ValueError(msg)420421    return get_from_env_fn422423424@overload425def secret_from_env(key: str | Sequence[str], /) -> Callable[[], SecretStr]: ...426427428@overload429def secret_from_env(key: str, /, *, default: str) -> Callable[[], SecretStr]: ...430431432@overload433def secret_from_env(434    key: str | Sequence[str], /, *, default: None435) -> Callable[[], SecretStr | None]: ...436437438@overload439def secret_from_env(key: str, /, *, error_message: str) -> Callable[[], SecretStr]: ...440441442def secret_from_env(443    key: str | Sequence[str],444    /,445    *,446    default: str | _NoDefaultType | None = _NoDefault,447    error_message: str | None = None,448) -> Callable[[], SecretStr | None] | Callable[[], SecretStr]:449    """Secret from env.450451    Args:452        key: The environment variable to look up.453        default: The default value to return if the environment variable is not set.454        error_message: The error message which will be raised if the key is not found455            and no default value is provided.456457            This will be raised as a `ValueError`.458459    Returns:460        Factory method that will look up the secret from the environment.461    """462463    def get_secret_from_env() -> SecretStr | None:464        """Get a value from an environment variable.465466        Raises:467            ValueError: If the environment variable is not set and no default is468                provided.469470        Returns:471            The secret from the environment.472        """473        if isinstance(key, (list, tuple)):474            for k in key:475                if k in os.environ:476                    return SecretStr(os.environ[k])477        if isinstance(key, str) and key in os.environ:478            return SecretStr(os.environ[key])479        if isinstance(default, str):480            return SecretStr(default)481        if default is None:482            return None483        if error_message:484            raise ValueError(error_message)485        msg = (486            f"Did not find {key}, please add an environment variable"487            f" `{key}` which contains it, or pass"488            f" `{key}` as a named parameter."489        )490        raise ValueError(msg)491492    return get_secret_from_env493494495LC_AUTO_PREFIX = "lc_"496"""LangChain auto-generated ID prefix for messages and content blocks."""497498LC_ID_PREFIX = "lc_run-"499"""Internal tracing/callback system identifier.500501Used for:502503- Tracing. Every LangChain operation (LLM call, chain execution, tool use, etc.)504    gets a unique run_id (UUID)505- Enables tracking parent-child relationships between operations506"""507508509def ensure_id(id_val: str | None) -> str:510    """Ensure the ID is a valid string, generating a new UUID if not provided.511512    Auto-generated UUIDs are prefixed by `'lc_'` to indicate they are513    LangChain-generated IDs.514515    Args:516        id_val: Optional string ID value to validate.517518    Returns:519        A string ID, either the validated provided value or a newly generated UUID4.520    """521    return id_val or f"{LC_AUTO_PREFIX}{uuid4()}"

Code quality findings 28

Ensure functions have docstrings for documentation
missing-docstring
def decorator(func: Callable) -> Callable:
Ensure functions have docstrings for documentation
missing-docstring
def now(cls, tz: datetime.tzinfo | None = None) -> "MockDateTime":
Ensure functions have docstrings for documentation
missing-docstring
def guard_import(
Ensure functions have docstrings for documentation
missing-docstring
def check_package_version(
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
for field_name in list(values):
Ensure functions have docstrings for documentation
missing-docstring
def build_extra_kwargs(
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
for field_name in list(values):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(value, SecretStr):
Ensure functions have docstrings for documentation
missing-docstring
def from_env(key: str, /) -> Callable[[], str]: ...
Ensure functions have docstrings for documentation
missing-docstring
def from_env(key: str, /, *, default: str) -> Callable[[], str]: ...
Ensure functions have docstrings for documentation
missing-docstring
def from_env(key: Sequence[str], /, *, default: str) -> Callable[[], str]: ...
Ensure functions have docstrings for documentation
missing-docstring
def from_env(key: str, /, *, error_message: str) -> Callable[[], str]: ...
Ensure functions have docstrings for documentation
missing-docstring
def from_env(
Ensure functions have docstrings for documentation
missing-docstring
def from_env(
Ensure functions have docstrings for documentation
missing-docstring
def from_env(
Ensure functions have docstrings for documentation
missing-docstring
def from_env(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(key, (list, tuple)):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(key, str) and key in os.environ:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(default, (str, type(None))):
Use isinstance() for type checking instead of type()
type-check
if isinstance(default, (str, type(None))):
Ensure functions have docstrings for documentation
missing-docstring
def secret_from_env(key: str | Sequence[str], /) -> Callable[[], SecretStr]: ...
Ensure functions have docstrings for documentation
missing-docstring
def secret_from_env(key: str, /, *, default: str) -> Callable[[], SecretStr]: ...
Ensure functions have docstrings for documentation
missing-docstring
def secret_from_env(
Ensure functions have docstrings for documentation
missing-docstring
def secret_from_env(key: str, /, *, error_message: str) -> Callable[[], SecretStr]: ...
Ensure functions have docstrings for documentation
missing-docstring
def secret_from_env(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(key, (list, tuple)):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(key, str) and key in os.environ:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(default, str):

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.