libs/core/langchain_core/utils/pydantic.py PYTHON 631 lines View on github.com → Search inside
1"""Utilities for pydantic."""23from __future__ import annotations45import inspect6import textwrap7import warnings8from contextlib import nullcontext9from functools import lru_cache, wraps10from types import GenericAlias11from typing import (12    TYPE_CHECKING,13    Any,14    TypeVar,15    cast,16    overload,17)1819import pydantic20from packaging import version2122# root_validator is deprecated but we need it for backward compatibility of @pre_init23from pydantic import (  # type: ignore[deprecated]24    BaseModel,25    ConfigDict,26    Field,27    PydanticDeprecationWarning,28    RootModel,29    root_validator,30)31from pydantic import (32    create_model as _create_model_base,33)34from pydantic.fields import FieldInfo as FieldInfoV235from pydantic.json_schema import (36    DEFAULT_REF_TEMPLATE,37    GenerateJsonSchema,38    JsonSchemaMode,39    JsonSchemaValue,40)41from pydantic.v1 import BaseModel as BaseModelV142from pydantic.v1 import create_model as create_model_v143from typing_extensions import deprecated, override4445if TYPE_CHECKING:46    from collections.abc import Callable4748    from pydantic.v1.fields import ModelField49    from pydantic_core import core_schema5051PYDANTIC_VERSION = version.parse(pydantic.__version__)525354@deprecated("Use PYDANTIC_VERSION.major instead.")55def get_pydantic_major_version() -> int:56    """DEPRECATED - Get the major version of Pydantic.5758    Use `PYDANTIC_VERSION.major` instead.5960    Returns:61        The major version of Pydantic.62    """63    return PYDANTIC_VERSION.major646566PYDANTIC_MAJOR_VERSION = PYDANTIC_VERSION.major67PYDANTIC_MINOR_VERSION = PYDANTIC_VERSION.minor6869IS_PYDANTIC_V1 = False70IS_PYDANTIC_V2 = True7172PydanticBaseModel = BaseModel | BaseModelV173TypeBaseModel = type[BaseModel] | type[BaseModelV1]7475TBaseModel = TypeVar("TBaseModel", bound=PydanticBaseModel)767778def is_pydantic_v1_subclass(cls: type) -> bool:79    """Check if the given class is Pydantic v1-like.8081    Returns:82        `True` if the given class is a subclass of Pydantic `BaseModel` 1.x.83    """84    return issubclass(cls, BaseModelV1)858687def is_pydantic_v2_subclass(cls: type) -> bool:88    """Check if the given class is Pydantic v2-like.8990    Returns:91        `True` if the given class is a subclass of Pydantic `BaseModel` 2.x.92    """93    return issubclass(cls, BaseModel)949596def is_basemodel_subclass(cls: type) -> bool:97    """Check if the given class is a subclass of Pydantic `BaseModel`.9899    Check if the given class is a subclass of any of the following:100101    * `pydantic.BaseModel` in Pydantic 2.x102    * `pydantic.v1.BaseModel` in Pydantic 2.x103104    Returns:105        `True` if the given class is a subclass of Pydantic `BaseModel`.106    """107    # Before we can use issubclass on the cls we need to check if it is a class108    if not inspect.isclass(cls) or isinstance(cls, GenericAlias):109        return False110111    return issubclass(cls, (BaseModel, BaseModelV1))112113114def is_basemodel_instance(obj: Any) -> bool:115    """Check if the given class is an instance of Pydantic `BaseModel`.116117    Check if the given class is an instance of any of the following:118119    * `pydantic.BaseModel` in Pydantic 2.x120    * `pydantic.v1.BaseModel` in Pydantic 2.x121122    Returns:123        `True` if the given class is an instance of Pydantic `BaseModel`.124    """125    return isinstance(obj, (BaseModel, BaseModelV1))126127128# How to type hint this?129def pre_init(130    func: Callable[[Any, dict[str, Any]], Any],131) -> Callable[[Any, dict[str, Any]], Any]:132    """Decorator to run a function before model initialization.133134    Args:135        func: The function to run before model initialization.136137    Returns:138        The decorated function.139    """140    with warnings.catch_warnings():141        warnings.filterwarnings(action="ignore", category=PydanticDeprecationWarning)142143        # Ideally we would use @model_validator(mode="before") but this would change the144        # order of the validators. See https://github.com/pydantic/pydantic/discussions/7434.145        # So we keep root_validator for backward compatibility.146        @root_validator(pre=True)  # type: ignore[deprecated]147        @wraps(func)148        def wrapper(cls: type[BaseModel], values: dict[str, Any]) -> Any:149            """Decorator to run a function before model initialization.150151            Args:152                cls: The model class.153                values: The values to initialize the model with.154155            Returns:156                The values to initialize the model with.157            """158            # Insert default values159            fields = cls.model_fields160            for name, field_info in fields.items():161                # Check if allow_population_by_field_name is enabled162                # If yes, then set the field name to the alias163                if (164                    hasattr(cls, "Config")165                    and hasattr(cls.Config, "allow_population_by_field_name")166                    and cls.Config.allow_population_by_field_name167                    and field_info.alias in values168                ):169                    values[name] = values.pop(field_info.alias)170                if (171                    hasattr(cls, "model_config")172                    and cls.model_config.get("populate_by_name")173                    and field_info.alias in values174                ):175                    values[name] = values.pop(field_info.alias)176177                if (178                    name not in values or values[name] is None179                ) and not field_info.is_required():180                    if field_info.default_factory is not None:181                        values[name] = field_info.default_factory()  # type: ignore[call-arg]182                    else:183                        values[name] = field_info.default184185            # Call the decorated function186            return func(cls, values)187188    return wrapper189190191class _IgnoreUnserializable(GenerateJsonSchema):192    """A JSON schema generator that ignores unknown types.193194    https://docs.pydantic.dev/latest/concepts/json_schema/#customizing-the-json-schema-generation-process195    """196197    @override198    def handle_invalid_for_json_schema(199        self, schema: core_schema.CoreSchema, error_info: str200    ) -> JsonSchemaValue:201        return {}202203204def _create_subset_model_v1(205    name: str,206    model: type[BaseModelV1],207    field_names: list[str],208    *,209    descriptions: dict[str, str] | None = None,210    fn_description: str | None = None,211) -> type[BaseModelV1]:212    """Create a Pydantic model with only a subset of model's fields."""213    fields = {}214215    for field_name in field_names:216        # Using pydantic v1 so can access __fields__ as a dict.217        field = model.__fields__[field_name]218        t = (219            # this isn't perfect but should work for most functions220            field.outer_type_221            if field.required and not field.allow_none222            else field.outer_type_ | None223        )224        if descriptions and field_name in descriptions:225            field.field_info.description = descriptions[field_name]226        fields[field_name] = (t, field.field_info)227228    rtn = cast("type[BaseModelV1]", create_model_v1(name, **fields))  # type: ignore[call-overload]229    rtn.__doc__ = textwrap.dedent(fn_description or model.__doc__ or "")230    return rtn231232233def _create_subset_model_v2(234    name: str,235    model: type[BaseModel],236    field_names: list[str],237    *,238    descriptions: dict[str, str] | None = None,239    fn_description: str | None = None,240) -> type[BaseModel]:241    """Create a Pydantic model with a subset of the model fields."""242    descriptions_ = descriptions or {}243    fields = {}244    for field_name in field_names:245        field = model.model_fields[field_name]246        description = descriptions_.get(field_name, field.description)247        field_kwargs: dict[str, Any] = {"description": description}248        if field.default_factory is not None:249            field_kwargs["default_factory"] = field.default_factory250        else:251            field_kwargs["default"] = field.default252        field_info = FieldInfoV2(**field_kwargs)253        if field.metadata:254            field_info.metadata = field.metadata255        fields[field_name] = (field.annotation, field_info)256257    rtn = cast(258        "type[BaseModel]",259        _create_model_base(  # type: ignore[call-overload]260            name, **fields, __config__=ConfigDict(arbitrary_types_allowed=True)261        ),262    )263264    # TODO(0.3): Determine if there is a more "pydantic" way to preserve annotations.265    # This is done to preserve __annotations__ when working with pydantic 2.x266    # and using the Annotated type with TypedDict.267    # Comment out the following line, to trigger the relevant test case.268    selected_annotations = [269        (name, annotation)270        for name, annotation in model.__annotations__.items()271        if name in field_names272    ]273274    rtn.__annotations__ = dict(selected_annotations)275    rtn.__doc__ = textwrap.dedent(fn_description or model.__doc__ or "")276    return rtn277278279# Private functionality to create a subset model that's compatible across280# different versions of pydantic.281# Handles pydantic versions 2.x. including v1 of pydantic in 2.x.282# However, can't find a way to type hint this.283def _create_subset_model(284    name: str,285    model: TypeBaseModel,286    field_names: list[str],287    *,288    descriptions: dict[str, str] | None = None,289    fn_description: str | None = None,290) -> TypeBaseModel:291    """Create subset model using the same pydantic version as the input model.292293    Returns:294        The created subset model.295    """296    if issubclass(model, BaseModelV1):297        return _create_subset_model_v1(298            name,299            model,300            field_names,301            descriptions=descriptions,302            fn_description=fn_description,303        )304    return _create_subset_model_v2(305        name,306        model,307        field_names,308        descriptions=descriptions,309        fn_description=fn_description,310    )311312313@overload314def get_fields(model: type[BaseModel]) -> dict[str, FieldInfoV2]: ...315316317@overload318def get_fields(model: BaseModel) -> dict[str, FieldInfoV2]: ...319320321@overload322def get_fields(model: type[BaseModelV1]) -> dict[str, ModelField]: ...323324325@overload326def get_fields(model: BaseModelV1) -> dict[str, ModelField]: ...327328329def get_fields(330    model: type[BaseModel | BaseModelV1] | BaseModel | BaseModelV1,331) -> dict[str, FieldInfoV2] | dict[str, ModelField]:332    """Return the field names of a Pydantic model.333334    Args:335        model: The Pydantic model or instance.336337    Raises:338        TypeError: If the model is not a Pydantic model.339    """340    if not isinstance(model, type):341        model = type(model)342    if issubclass(model, BaseModel):343        return model.model_fields344    if issubclass(model, BaseModelV1):345        return model.__fields__346    msg = f"Expected a Pydantic model. Got {model}"  # type: ignore[unreachable]347    raise TypeError(msg)348349350def model_json_schema(model: TypeBaseModel) -> dict[str, Any]:351    """Return the JSON schema of a Pydantic model class of either major version.352353    Dispatches to the correct method for Pydantic v1 (`schema`) or v2354    (`model_json_schema`), so callers holding a `TypeBaseModel` don't have to355    branch on the model's version themselves.356357    Args:358        model: The Pydantic model class.359360    Raises:361        TypeError: If the model is not a Pydantic model class.362    """363    if issubclass(model, BaseModel):364        return model.model_json_schema()365    if issubclass(model, BaseModelV1):366        return model.schema()367    msg = f"Expected a Pydantic model. Got {model}"  # type: ignore[unreachable]368    raise TypeError(msg)369370371def model_validate(model: TypeBaseModel, obj: Any) -> PydanticBaseModel:372    """Validate `obj` against a Pydantic model class of either major version.373374    Dispatches to the correct method for Pydantic v1 (`parse_obj`) or v2375    (`model_validate`), so callers holding a `TypeBaseModel` don't have to376    branch on the model's version themselves.377378    Args:379        model: The Pydantic model class to validate against.380        obj: The object to validate.381382    Raises:383        TypeError: If the model is not a Pydantic model class.384    """385    if issubclass(model, BaseModel):386        return model.model_validate(obj)387    if issubclass(model, BaseModelV1):388        return model.parse_obj(obj)389    msg = f"Expected a Pydantic model. Got {model}"  # type: ignore[unreachable]390    raise TypeError(msg)391392393_SchemaConfig = ConfigDict(394    arbitrary_types_allowed=True, frozen=True, protected_namespaces=()395)396397NO_DEFAULT = object()398399400def _create_root_model(401    name: str,402    type_: Any,403    module_name: str | None = None,404    default_: object = NO_DEFAULT,405) -> type[BaseModel]:406    """Create a base class."""407408    def schema(409        cls: type[BaseModelV1],410        by_alias: bool = True,  # noqa: FBT001,FBT002411        ref_template: str = DEFAULT_REF_TEMPLATE,412    ) -> dict[str, Any]:413        super_cls = cast("type[BaseModelV1]", super(cls, cls))414        schema_ = super_cls.schema(by_alias=by_alias, ref_template=ref_template)415        schema_["title"] = name416        return schema_417418    def model_json_schema(419        cls: type[BaseModel],420        by_alias: bool = True,  # noqa: FBT001,FBT002421        ref_template: str = DEFAULT_REF_TEMPLATE,422        schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,423        mode: JsonSchemaMode = "validation",424    ) -> dict[str, Any]:425        super_cls = cast("type[BaseModel]", super(cls, cls))426        schema_ = super_cls.model_json_schema(427            by_alias=by_alias,428            ref_template=ref_template,429            schema_generator=schema_generator,430            mode=mode,431        )432        schema_["title"] = name433        return schema_434435    base_class_attributes = {436        "__annotations__": {"root": type_},437        "model_config": ConfigDict(arbitrary_types_allowed=True),438        "schema": classmethod(schema),439        "model_json_schema": classmethod(model_json_schema),440        "__module__": module_name or "langchain_core.runnables.utils",441    }442443    if default_ is not NO_DEFAULT:444        base_class_attributes["root"] = default_445    with warnings.catch_warnings():446        try:447            if isinstance(type_, type) and issubclass(type_, BaseModelV1):448                warnings.filterwarnings(449                    action="ignore", category=PydanticDeprecationWarning450                )451        except TypeError:452            pass453        custom_root_type = type(name, (RootModel,), base_class_attributes)454    return cast("type[BaseModel]", custom_root_type)455456457@lru_cache(maxsize=256)458def _create_root_model_cached(459    model_name: str,460    type_: Any,461    *,462    module_name: str | None = None,463    default_: object = NO_DEFAULT,464) -> type[BaseModel]:465    return _create_root_model(466        model_name, type_, default_=default_, module_name=module_name467    )468469470@lru_cache(maxsize=256)471def _create_model_cached(472    model_name: str,473    /,474    **field_definitions: Any,475) -> type[BaseModel]:476    return _create_model_base(477        model_name,478        __config__=_SchemaConfig,479        **_remap_field_definitions(field_definitions),480    )481482483def create_model(484    model_name: str,485    module_name: str | None = None,486    /,487    **field_definitions: Any,488) -> type[BaseModel]:489    """Create a Pydantic model with the given field definitions.490491    Please use `create_model_v2` instead of this function.492493    Args:494        model_name: The name of the model.495        module_name: The name of the module where the model is defined.496497            This is used by Pydantic to resolve any forward references.498        **field_definitions: The field definitions for the model.499500    Returns:501        The created model.502    """503    kwargs = {}504    if "__root__" in field_definitions:505        kwargs["root"] = field_definitions.pop("__root__")506507    return create_model_v2(508        model_name,509        module_name=module_name,510        field_definitions=field_definitions,511        **kwargs,512    )513514515# Reserved names should capture all the `public` names / methods that are516# used by BaseModel internally. This will keep the reserved names up-to-date.517# For reference, the reserved names are:518# "construct", "copy", "dict", "from_orm", "json", "parse_file", "parse_obj",519# "parse_raw", "schema", "schema_json", "update_forward_refs", "validate",520# "model_computed_fields", "model_config", "model_construct", "model_copy",521# "model_dump", "model_dump_json", "model_extra", "model_fields",522# "model_fields_set", "model_json_schema", "model_parametrized_name",523# "model_post_init", "model_rebuild", "model_validate", "model_validate_json",524# "model_validate_strings"525_RESERVED_NAMES = {key for key in dir(BaseModel) if not key.startswith("_")}526527528def _remap_field_definitions(field_definitions: dict[str, Any]) -> dict[str, Any]:529    """This remaps fields to avoid colliding with internal pydantic fields."""530    remapped = {}531    for key, value in field_definitions.items():532        if key.startswith("_") or key in _RESERVED_NAMES:533            # Let's add a prefix to avoid colliding with internal pydantic fields534            if isinstance(value, FieldInfoV2):535                msg = (536                    f"Remapping for fields starting with '_' or fields with a name "537                    f"matching a reserved name {_RESERVED_NAMES} is not supported if "538                    f" the field is a pydantic Field instance. Got {key}."539                )540                raise NotImplementedError(msg)541            type_, default_ = value542            remapped[f"private_{key}"] = (543                type_,544                Field(545                    default=default_,546                    alias=key,547                    serialization_alias=key,548                    title=key.lstrip("_").replace("_", " ").title(),549                ),550            )551        else:552            remapped[key] = value553    return remapped554555556def create_model_v2(557    model_name: str,558    *,559    module_name: str | None = None,560    field_definitions: dict[str, Any] | None = None,561    root: Any | None = None,562) -> type[BaseModel]:563    """Create a Pydantic model with the given field definitions.564565    !!! warning566567        Do not use outside of langchain packages. This API is subject to change at any568        time.569570    Args:571        model_name: The name of the model.572        module_name: The name of the module where the model is defined.573574            This is used by Pydantic to resolve any forward references.575        field_definitions: The field definitions for the model.576        root: Type for a root model (`RootModel`)577578    Returns:579        The created model.580    """581    field_definitions = field_definitions or {}582583    if root:584        if field_definitions:585            msg = (586                "When specifying __root__ no other "587                f"fields should be provided. Got {field_definitions}"588            )589            raise NotImplementedError(msg)590591        if isinstance(root, tuple):592            kwargs = {"type_": root[0], "default_": root[1]}593        else:594            kwargs = {"type_": root}595596        try:597            named_root_model = _create_root_model_cached(598                model_name, module_name=module_name, **kwargs599            )600        except TypeError:601            # something in the arguments into _create_root_model_cached is not hashable602            named_root_model = _create_root_model(603                model_name,604                module_name=module_name,605                **kwargs,606            )607        return named_root_model608609    # No root, just field definitions610    names = set(field_definitions.keys())611612    capture_warnings = False613614    for name in names:615        # Also if any non-reserved name is used (e.g., model_id or model_name)616        if name.startswith("model"):617            capture_warnings = True618619    with warnings.catch_warnings() if capture_warnings else nullcontext():620        if capture_warnings:621            warnings.filterwarnings(action="ignore")622        try:623            return _create_model_cached(model_name, **field_definitions)624        except TypeError:625            # something in field definitions is not hashable626            return _create_model_base(627                model_name,628                __config__=_SchemaConfig,629                **_remap_field_definitions(field_definitions),630            )

Code quality findings 19

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not inspect.isclass(cls) or isinstance(cls, GenericAlias):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
return isinstance(obj, (BaseModel, BaseModelV1))
Ensure functions have docstrings for documentation
missing-docstring
def pre_init(
Ensure functions have docstrings for documentation
missing-docstring
def handle_invalid_for_json_schema(
Ensure functions have docstrings for documentation
missing-docstring
def get_fields(model: type[BaseModel]) -> dict[str, FieldInfoV2]: ...
Ensure functions have docstrings for documentation
missing-docstring
def get_fields(model: BaseModel) -> dict[str, FieldInfoV2]: ...
Ensure functions have docstrings for documentation
missing-docstring
def get_fields(model: type[BaseModelV1]) -> dict[str, ModelField]: ...
Ensure functions have docstrings for documentation
missing-docstring
def get_fields(model: BaseModelV1) -> dict[str, ModelField]: ...
Ensure functions have docstrings for documentation
missing-docstring
def get_fields(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(model, type):
Use isinstance() for type checking instead of type()
type-check
model = type(model)
Ensure functions have docstrings for documentation
missing-docstring
def schema(
Ensure functions have docstrings for documentation
missing-docstring
def model_json_schema(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(type_, type) and issubclass(type_, BaseModelV1):
Use isinstance() for type checking instead of type()
type-check
custom_root_type = type(name, (RootModel,), base_class_attributes)
Ensure functions have docstrings for documentation
missing-docstring
def create_model(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(value, FieldInfoV2):
Ensure functions have docstrings for documentation
missing-docstring
def create_model_v2(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(root, tuple):

Get this view in your editor

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