Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
mypy.py1413 linesDownload Raw Back to pydantic
1"""This module includes classes and functions designed specifically for use with the mypy plugin."""2 3from __future__ import annotations4 5import sys6from collections.abc import Iterator7from configparser import ConfigParser8from typing import Any, Callable9 10from mypy.errorcodes import ErrorCode11from mypy.expandtype import expand_type, expand_type_by_instance12from mypy.nodes import (13    ARG_NAMED,14    ARG_NAMED_OPT,15    ARG_OPT,16    ARG_POS,17    ARG_STAR2,18    INVARIANT,19    MDEF,20    Argument,21    AssignmentStmt,22    Block,23    CallExpr,24    ClassDef,25    Context,26    Decorator,27    DictExpr,28    EllipsisExpr,29    Expression,30    FuncDef,31    IfStmt,32    JsonDict,33    MemberExpr,34    NameExpr,35    PassStmt,36    PlaceholderNode,37    RefExpr,38    Statement,39    StrExpr,40    SymbolTableNode,41    TempNode,42    TypeAlias,43    TypeInfo,44    Var,45)46from mypy.options import Options47from mypy.plugin import (48    CheckerPluginInterface,49    ClassDefContext,50    DynamicClassDefContext,51    MethodContext,52    Plugin,53    ReportConfigContext,54    SemanticAnalyzerPluginInterface,55)56from mypy.plugins.common import (57    deserialize_and_fixup_type,58)59from mypy.semanal import set_callable_name60from mypy.server.trigger import make_wildcard_trigger61from mypy.state import state62from mypy.type_visitor import TypeTranslator63from mypy.typeops import map_type_from_supertype64from mypy.types import (65    AnyType,66    CallableType,67    Instance,68    NoneType,69    Type,70    TypeOfAny,71    TypeType,72    TypeVarType,73    UnionType,74    get_proper_type,75)76from mypy.typevars import fill_typevars77from mypy.util import get_unique_redefinition_name78from mypy.version import __version__ as mypy_version79 80from pydantic._internal import _fields81from pydantic.version import parse_mypy_version82 83CONFIGFILE_KEY = 'pydantic-mypy'84METADATA_KEY = 'pydantic-mypy-metadata'85BASEMODEL_FULLNAME = 'pydantic.main.BaseModel'86CREATE_MODEL_FULLNAME = 'pydantic.main.create_model'87BASESETTINGS_FULLNAME = 'pydantic_settings.main.BaseSettings'88ROOT_MODEL_FULLNAME = 'pydantic.root_model.RootModel'89MODEL_METACLASS_FULLNAME = 'pydantic._internal._model_construction.ModelMetaclass'90FIELD_FULLNAME = 'pydantic.fields.Field'91DATACLASS_FULLNAME = 'pydantic.dataclasses.dataclass'92MODEL_VALIDATOR_FULLNAME = 'pydantic.functional_validators.model_validator'93DECORATOR_FULLNAMES = {94    'pydantic.functional_validators.field_validator',95    'pydantic.functional_validators.model_validator',96    'pydantic.functional_serializers.serializer',97    'pydantic.functional_serializers.model_serializer',98    'pydantic.deprecated.class_validators.validator',99    'pydantic.deprecated.class_validators.root_validator',100}101IMPLICIT_CLASSMETHOD_DECORATOR_FULLNAMES = DECORATOR_FULLNAMES - {'pydantic.functional_serializers.model_serializer'}102 103 104MYPY_VERSION_TUPLE = parse_mypy_version(mypy_version)105BUILTINS_NAME = 'builtins'106 107# Increment version if plugin changes and mypy caches should be invalidated108__version__ = 2109 110 111def plugin(version: str) -> type[Plugin]:112    """`version` is the mypy version string.113 114    We might want to use this to print a warning if the mypy version being used is115    newer, or especially older, than we expect (or need).116 117    Args:118        version: The mypy version string.119 120    Return:121        The Pydantic mypy plugin type.122    """123    return PydanticPlugin124 125 126class PydanticPlugin(Plugin):127    """The Pydantic mypy plugin."""128 129    def __init__(self, options: Options) -> None:130        self.plugin_config = PydanticPluginConfig(options)131        self._plugin_data = self.plugin_config.to_data()132        super().__init__(options)133 134    def get_base_class_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None:135        """Update Pydantic model class."""136        sym = self.lookup_fully_qualified(fullname)137        if sym and isinstance(sym.node, TypeInfo):  # pragma: no branch138            # No branching may occur if the mypy cache has not been cleared139            if sym.node.has_base(BASEMODEL_FULLNAME):140                return self._pydantic_model_class_maker_callback141        return None142 143    def get_metaclass_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None:144        """Update Pydantic `ModelMetaclass` definition."""145        if fullname == MODEL_METACLASS_FULLNAME:146            return self._pydantic_model_metaclass_marker_callback147        return None148 149    def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | None:150        """Adjust return type of `from_orm` method call."""151        if fullname.endswith('.from_orm'):152            return from_attributes_callback153        return None154 155    def get_dynamic_class_hook(self, fullname: str) -> Callable[[DynamicClassDefContext], None] | None:156        """Recognize `create_model()` calls as dynamic BaseModel subclasses."""157        if fullname == CREATE_MODEL_FULLNAME:158            return self._pydantic_create_model_callback159        return None160 161    def report_config_data(self, ctx: ReportConfigContext) -> dict[str, Any]:162        """Return all plugin config data.163 164        Used by mypy to determine if cache needs to be discarded.165        """166        return self._plugin_data167 168    def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> None:169        transformer = PydanticModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config)170        transformer.transform()171 172    def _pydantic_model_metaclass_marker_callback(self, ctx: ClassDefContext) -> None:173        """Reset dataclass_transform_spec attribute of ModelMetaclass.174 175        Let the plugin handle it. This behavior can be disabled176        if 'debug_dataclass_transform' is set to True', for testing purposes.177        """178        if self.plugin_config.debug_dataclass_transform:179            return180        info_metaclass = ctx.cls.info.declared_metaclass181        assert info_metaclass, "callback not passed from 'get_metaclass_hook'"182        if getattr(info_metaclass.type, 'dataclass_transform_spec', None):183            info_metaclass.type.dataclass_transform_spec = None184 185    def _pydantic_create_model_callback(self, ctx: DynamicClassDefContext) -> None:186        """Make variables assigned from `create_model()` usable as types by mypy."""187        # Determine the base class from __base__ argument if provided188        base_fullname = BASEMODEL_FULLNAME189        for arg_name, arg_expr in zip(ctx.call.arg_names, ctx.call.args):190            if arg_name == '__base__' and isinstance(arg_expr, RefExpr) and arg_expr.node is not None:191                if isinstance(arg_expr.node, TypeInfo):192                    base_fullname = arg_expr.node.fullname193                elif isinstance(arg_expr.node, Var) and isinstance(arg_expr.node.type, Instance):194                    base_fullname = arg_expr.node.type.type.fullname195 196        base_sym = ctx.api.lookup_fully_qualified_or_none(base_fullname)197        if base_sym is None or not isinstance(base_sym.node, TypeInfo):198            # Fall back to BaseModel199            base_sym = ctx.api.lookup_fully_qualified_or_none(BASEMODEL_FULLNAME)200            if base_sym is None or not isinstance(base_sym.node, TypeInfo):201                return202 203        base_info = base_sym.node204        base_instance = fill_typevars(base_info)205        assert isinstance(base_instance, Instance)206 207        info = ctx.api.basic_new_typeinfo(ctx.name, base_instance, ctx.call.line)208        info.metaclass_type = base_info.metaclass_type209 210        ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(MDEF, info))211 212 213class PydanticPluginConfig:214    """A Pydantic mypy plugin config holder.215 216    Attributes:217        init_forbid_extra: Whether to add a `**kwargs` at the end of the generated `__init__` signature.218        init_typed: Whether to annotate fields in the generated `__init__`.219        warn_required_dynamic_aliases: Whether to raise required dynamic aliases error.220        debug_dataclass_transform: Whether to not reset `dataclass_transform_spec` attribute221            of `ModelMetaclass` for testing purposes.222    """223 224    __slots__ = (225        'init_forbid_extra',226        'init_typed',227        'warn_required_dynamic_aliases',228        'debug_dataclass_transform',229    )230    init_forbid_extra: bool231    init_typed: bool232    warn_required_dynamic_aliases: bool233    debug_dataclass_transform: bool  # undocumented234 235    def __init__(self, options: Options) -> None:236        if options.config_file is None:  # pragma: no cover237            return238 239        toml_config = parse_toml(options.config_file)240        if toml_config is not None:241            config = toml_config.get('tool', {}).get('pydantic-mypy', {})242            for key in self.__slots__:243                setting = config.get(key, False)244                if not isinstance(setting, bool):245                    raise ValueError(f'Configuration value must be a boolean for key: {key}')246                setattr(self, key, setting)247        else:248            plugin_config = ConfigParser()249            plugin_config.read(options.config_file)250            for key in self.__slots__:251                setting = plugin_config.getboolean(CONFIGFILE_KEY, key, fallback=False)252                setattr(self, key, setting)253 254    def to_data(self) -> dict[str, Any]:255        """Returns a dict of config names to their values."""256        return {key: getattr(self, key) for key in self.__slots__}257 258 259def from_attributes_callback(ctx: MethodContext) -> Type:260    """Raise an error if from_attributes is not enabled."""261    model_type: Instance262    ctx_type = ctx.type263    if isinstance(ctx_type, TypeType):264        ctx_type = ctx_type.item265    if isinstance(ctx_type, CallableType) and isinstance(ctx_type.ret_type, Instance):266        model_type = ctx_type.ret_type  # called on the class267    elif isinstance(ctx_type, Instance):268        model_type = ctx_type  # called on an instance (unusual, but still valid)269    else:  # pragma: no cover270        detail = f'ctx.type: {ctx_type} (of type {ctx_type.__class__.__name__})'271        error_unexpected_behavior(detail, ctx.api, ctx.context)272        return ctx.default_return_type273    pydantic_metadata = model_type.type.metadata.get(METADATA_KEY)274    if pydantic_metadata is None:275        return ctx.default_return_type276    if not model_type.type.has_base(BASEMODEL_FULLNAME):277        # not a Pydantic v2 model278        return ctx.default_return_type279    from_attributes = pydantic_metadata.get('config', {}).get('from_attributes')280    if from_attributes is not True:281        error_from_attributes(model_type.type.name, ctx.api, ctx.context)282    return ctx.default_return_type283 284 285class PydanticModelField:286    """Based on mypy.plugins.dataclasses.DataclassAttribute."""287 288    def __init__(289        self,290        name: str,291        alias: str | None,292        is_frozen: bool,293        has_dynamic_alias: bool,294        has_default: bool,295        strict: bool | None,296        line: int,297        column: int,298        type: Type | None,299        info: TypeInfo,300    ):301        self.name = name302        self.alias = alias303        self.is_frozen = is_frozen304        self.has_dynamic_alias = has_dynamic_alias305        self.has_default = has_default306        self.strict = strict307        self.line = line308        self.column = column309        self.type = type310        self.info = info311 312    def to_argument(313        self,314        current_info: TypeInfo,315        typed: bool,316        model_strict: bool,317        force_optional: bool,318        use_alias: bool,319        api: SemanticAnalyzerPluginInterface,320        force_typevars_invariant: bool,321        is_root_model_root: bool,322    ) -> Argument:323        """Based on mypy.plugins.dataclasses.DataclassAttribute.to_argument."""324        variable = self.to_var(current_info, api, use_alias, force_typevars_invariant)325 326        strict = model_strict if self.strict is None else self.strict327        if typed or strict:328            type_annotation = self.expand_type(current_info, api, include_root_type=True)329        else:330            type_annotation = AnyType(TypeOfAny.explicit)331 332        return Argument(333            variable=variable,334            type_annotation=type_annotation,335            initializer=None,336            kind=ARG_OPT337            if is_root_model_root338            else (ARG_NAMED_OPT if force_optional or self.has_default else ARG_NAMED),339        )340 341    def expand_type(342        self,343        current_info: TypeInfo,344        api: SemanticAnalyzerPluginInterface,345        force_typevars_invariant: bool = False,346        include_root_type: bool = False,347    ) -> Type | None:348        """Based on mypy.plugins.dataclasses.DataclassAttribute.expand_type."""349        if force_typevars_invariant:350            # In some cases, mypy will emit an error "Cannot use a covariant type variable as a parameter"351            # To prevent that, we add an option to replace typevars with invariant ones while building certain352            # method signatures (in particular, `__init__`). There may be a better way to do this, if this causes353            # us problems in the future, we should look into why the dataclasses plugin doesn't have this issue.354            if isinstance(self.type, TypeVarType):355                modified_type = self.type.copy_modified()356                modified_type.variance = INVARIANT357                self.type = modified_type358 359        if self.type is not None and self.info.self_type is not None:360            # In general, it is not safe to call `expand_type()` during semantic analysis,361            # however this plugin is called very late, so all types should be fully ready.362            # Also, it is tricky to avoid eager expansion of Self types here (e.g. because363            # we serialize attributes).364            with state.strict_optional_set(api.options.strict_optional):365                filled_with_typevars = fill_typevars(current_info)366                # Cannot be TupleType as current_info represents a Pydantic model:367                assert isinstance(filled_with_typevars, Instance)368                if force_typevars_invariant:369                    for arg in filled_with_typevars.args:370                        if isinstance(arg, TypeVarType):371                            arg.variance = INVARIANT372 373                expanded_type = expand_type(self.type, {self.info.self_type.id: filled_with_typevars})374                if include_root_type and isinstance(expanded_type, Instance) and is_root_model(expanded_type.type):375                    # When a root model is used as a field, Pydantic allows both an instance of the root model376                    # as well as instances of the `root` field type:377                    root_type = expanded_type.type['root'].type378                    if root_type is None:379                        # Happens if the hint for 'root' has unsolved forward references380                        return expanded_type381                    expanded_root_type = expand_type_by_instance(root_type, expanded_type)382                    expanded_type = UnionType([expanded_type, expanded_root_type])383                return expanded_type384        return self.type385 386    def to_var(387        self,388        current_info: TypeInfo,389        api: SemanticAnalyzerPluginInterface,390        use_alias: bool,391        force_typevars_invariant: bool = False,392    ) -> Var:393        """Based on mypy.plugins.dataclasses.DataclassAttribute.to_var."""394        if use_alias and self.alias is not None:395            name = self.alias396        else:397            name = self.name398 399        return Var(name, self.expand_type(current_info, api, force_typevars_invariant))400 401    def serialize(self) -> JsonDict:402        """Based on mypy.plugins.dataclasses.DataclassAttribute.serialize."""403        assert self.type404        return {405            'name': self.name,406            'alias': self.alias,407            'is_frozen': self.is_frozen,408            'has_dynamic_alias': self.has_dynamic_alias,409            'has_default': self.has_default,410            'strict': self.strict,411            'line': self.line,412            'column': self.column,413            'type': self.type.serialize(),414        }415 416    @classmethod417    def deserialize(cls, info: TypeInfo, data: JsonDict, api: SemanticAnalyzerPluginInterface) -> PydanticModelField:418        """Based on mypy.plugins.dataclasses.DataclassAttribute.deserialize."""419        data = data.copy()420        typ = deserialize_and_fixup_type(data.pop('type'), api)421        return cls(type=typ, info=info, **data)422 423    def expand_typevar_from_subtype(self, sub_type: TypeInfo, api: SemanticAnalyzerPluginInterface) -> None:424        """Expands type vars in the context of a subtype when an attribute is inherited425        from a generic super type.426        """427        if self.type is not None:428            with state.strict_optional_set(api.options.strict_optional):429                self.type = map_type_from_supertype(self.type, sub_type, self.info)430 431 432class PydanticModelClassVar:433    """Based on mypy.plugins.dataclasses.DataclassAttribute.434 435    ClassVars are ignored by subclasses.436 437    Attributes:438        name: the ClassVar name439    """440 441    def __init__(self, name):442        self.name = name443 444    @classmethod445    def deserialize(cls, data: JsonDict) -> PydanticModelClassVar:446        """Based on mypy.plugins.dataclasses.DataclassAttribute.deserialize."""447        data = data.copy()448        return cls(**data)449 450    def serialize(self) -> JsonDict:451        """Based on mypy.plugins.dataclasses.DataclassAttribute.serialize."""452        return {453            'name': self.name,454        }455 456 457class PydanticModelTransformer:458    """Transform the BaseModel subclass according to the plugin settings.459 460    Attributes:461        tracked_config_fields: A set of field configs that the plugin has to track their value.462    """463 464    tracked_config_fields: set[str] = {465        'extra',466        'frozen',467        'from_attributes',468        'populate_by_name',469        'validate_by_alias',470        'validate_by_name',471        'alias_generator',472        'strict',473    }474 475    def __init__(476        self,477        cls: ClassDef,478        reason: Expression | Statement,479        api: SemanticAnalyzerPluginInterface,480        plugin_config: PydanticPluginConfig,481    ) -> None:482        self._cls = cls483        self._reason = reason484        self._api = api485 486        self.plugin_config = plugin_config487 488    def transform(self) -> bool:489        """Configures the BaseModel subclass according to the plugin settings.490 491        In particular:492 493        * determines the model config and fields,494        * adds a fields-aware signature for the initializer and construct methods495        * freezes the class if frozen = True496        * stores the fields, config, and if the class is settings in the mypy metadata for access by subclasses497        """498        info = self._cls.info499        is_a_root_model = is_root_model(info)500        config = self.collect_config()501        fields, class_vars = self.collect_fields_and_class_vars(config, is_a_root_model)502        if fields is None or class_vars is None:503            # Some definitions are not ready. We need another pass.504            return False505        for field in fields:506            if field.type is None:507                return False508 509        is_settings = info.has_base(BASESETTINGS_FULLNAME)510        self.add_initializer(fields, config, is_settings, is_a_root_model)511        self.add_model_construct_method(fields, config, is_settings, is_a_root_model)512        self.set_frozen(fields, self._api, frozen=config.frozen is True)513 514        self.adjust_decorator_signatures()515 516        info.metadata[METADATA_KEY] = {517            'fields': {field.name: field.serialize() for field in fields},518            'class_vars': {class_var.name: class_var.serialize() for class_var in class_vars},519            'config': config.get_values_dict(),520        }521 522        return True523 524    def adjust_decorator_signatures(self) -> None:525        """When we decorate a function `f` with `pydantic.validator(...)`, `pydantic.field_validator`526        or `pydantic.serializer(...)`, mypy sees `f` as a regular method taking a `self` instance,527        even though pydantic internally wraps `f` with `classmethod` if necessary.528 529        Teach mypy this by marking any function whose outermost decorator is a `validator()`,530        `field_validator()` or `serializer()` call as a `classmethod`.531        """532        for sym in self._cls.info.names.values():533            if isinstance(sym.node, Decorator):534                first_dec = sym.node.original_decorators[0]535                if (536                    isinstance(first_dec, CallExpr)537                    and isinstance(first_dec.callee, NameExpr)538                    and first_dec.callee.fullname in IMPLICIT_CLASSMETHOD_DECORATOR_FULLNAMES539                    # @model_validator(mode="after") is an exception, it expects a regular method540                    and not (541                        first_dec.callee.fullname == MODEL_VALIDATOR_FULLNAME542                        and any(543                            first_dec.arg_names[i] == 'mode' and isinstance(arg, StrExpr) and arg.value == 'after'544                            for i, arg in enumerate(first_dec.args)545                        )546                    )547                ):548                    # TODO: Only do this if the first argument of the decorated function is `cls`549                    sym.node.func.is_class = True550 551    def collect_config(self) -> ModelConfigData:  # noqa: C901 (ignore complexity)552        """Collects the values of the config attributes that are used by the plugin, accounting for parent classes."""553        cls = self._cls554        config = ModelConfigData()555 556        has_config_kwargs = False557        has_config_from_namespace = False558 559        # Handle `class MyModel(BaseModel, <name>=<expr>, ...):`560        for name, expr in cls.keywords.items():561            config_data = self.get_config_update(name, expr)562            if config_data:563                has_config_kwargs = True564                config.update(config_data)565 566        # Handle `model_config`567        stmt: Statement | None = None568        for stmt in cls.defs.body:569            if not isinstance(stmt, (AssignmentStmt, ClassDef)):570                continue571 572            if isinstance(stmt, AssignmentStmt):573                lhs = stmt.lvalues[0]574                if not isinstance(lhs, NameExpr) or lhs.name != 'model_config':575                    continue576 577                if isinstance(stmt.rvalue, CallExpr):  # calls to `dict` or `ConfigDict`578                    for arg_name, arg in zip(stmt.rvalue.arg_names, stmt.rvalue.args):579                        if arg_name is None:580                            continue581                        config.update(self.get_config_update(arg_name, arg, lax_extra=True))582                elif isinstance(stmt.rvalue, DictExpr):  # dict literals583                    for key_expr, value_expr in stmt.rvalue.items:584                        if not isinstance(key_expr, StrExpr):585                            continue586                        config.update(self.get_config_update(key_expr.value, value_expr))587 588            elif isinstance(stmt, ClassDef):589                if stmt.name != 'Config':  # 'deprecated' Config-class590                    continue591                for substmt in stmt.defs.body:592                    if not isinstance(substmt, AssignmentStmt):593                        continue594                    lhs = substmt.lvalues[0]595                    if not isinstance(lhs, NameExpr):596                        continue597                    config.update(self.get_config_update(lhs.name, substmt.rvalue))598 599            if has_config_kwargs:600                self._api.fail(601                    'Specifying config in two places is ambiguous, use either Config attribute or class kwargs',602                    cls,603                )604                break605 606            has_config_from_namespace = True607 608        if has_config_kwargs or has_config_from_namespace:609            if (610                stmt611                and config.has_alias_generator612                and not (config.validate_by_name or config.populate_by_name)613                and self.plugin_config.warn_required_dynamic_aliases614            ):615                error_required_dynamic_aliases(self._api, stmt)616 617        for info in cls.info.mro[1:]:  # 0 is the current class618            if METADATA_KEY not in info.metadata:619                continue620 621            # Each class depends on the set of fields in its ancestors622            self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname))623            for name, value in info.metadata[METADATA_KEY]['config'].items():624                config.setdefault(name, value)625        return config626 627    def collect_fields_and_class_vars(628        self, model_config: ModelConfigData, is_root_model: bool629    ) -> tuple[list[PydanticModelField] | None, list[PydanticModelClassVar] | None]:630        """Collects the fields for the model, accounting for parent classes."""631        cls = self._cls632 633        # First, collect fields and ClassVars belonging to any class in the MRO, ignoring duplicates.634        #635        # We iterate through the MRO in reverse because attrs defined in the parent must appear636        # earlier in the attributes list than attrs defined in the child. See:637        # https://docs.python.org/3/library/dataclasses.html#inheritance638        #639        # However, we also want fields defined in the subtype to override ones defined640        # in the parent. We can implement this via a dict without disrupting the attr order641        # because dicts preserve insertion order in Python 3.7+.642        found_fields: dict[str, PydanticModelField] = {}643        found_class_vars: dict[str, PydanticModelClassVar] = {}644        for info in reversed(cls.info.mro[1:-1]):  # 0 is the current class, -2 is BaseModel, -1 is object645            # if BASEMODEL_METADATA_TAG_KEY in info.metadata and BASEMODEL_METADATA_KEY not in info.metadata:646            #     # We haven't processed the base class yet. Need another pass.647            #     return None, None648            if METADATA_KEY not in info.metadata:649                continue650 651            # Each class depends on the set of attributes in its dataclass ancestors.652            self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname))653 654            for name, data in info.metadata[METADATA_KEY]['fields'].items():655                field = PydanticModelField.deserialize(info, data, self._api)656                # (The following comment comes directly from the dataclasses plugin)657                # TODO: We shouldn't be performing type operations during the main658                #       semantic analysis pass, since some TypeInfo attributes might659                #       still be in flux. This should be performed in a later phase.660                field.expand_typevar_from_subtype(cls.info, self._api)661                found_fields[name] = field662 663                sym_node = cls.info.names.get(name)664                if sym_node and sym_node.node and not isinstance(sym_node.node, (Var, PlaceholderNode)):665                    self._api.fail(666                        'BaseModel field may only be overridden by another field',667                        sym_node.node,668                    )669            # Collect ClassVars670            for name, data in info.metadata[METADATA_KEY]['class_vars'].items():671                found_class_vars[name] = PydanticModelClassVar.deserialize(data)672 673        # Second, collect fields and ClassVars belonging to the current class.674        current_field_names: set[str] = set()675        current_class_vars_names: set[str] = set()676        for stmt in self._get_assignment_statements_from_block(cls.defs):677            maybe_field = self.collect_field_or_class_var_from_stmt(stmt, model_config, found_class_vars)678            if maybe_field is None:679                continue680 681            lhs = stmt.lvalues[0]682            assert isinstance(lhs, NameExpr)  # collect_field_or_class_var_from_stmt guarantees this683            if isinstance(maybe_field, PydanticModelField):684                if is_root_model and lhs.name != 'root':685                    error_extra_fields_on_root_model(self._api, stmt)686                else:687                    current_field_names.add(lhs.name)688                    found_fields[lhs.name] = maybe_field689            elif isinstance(maybe_field, PydanticModelClassVar):690                current_class_vars_names.add(lhs.name)691                found_class_vars[lhs.name] = maybe_field692 693        return list(found_fields.values()), list(found_class_vars.values())694 695    def _get_assignment_statements_from_if_statement(self, stmt: IfStmt) -> Iterator[AssignmentStmt]:696        for body in stmt.body:697            if not body.is_unreachable:698                yield from self._get_assignment_statements_from_block(body)699        if stmt.else_body is not None and not stmt.else_body.is_unreachable:700            yield from self._get_assignment_statements_from_block(stmt.else_body)701 702    def _get_assignment_statements_from_block(self, block: Block) -> Iterator[AssignmentStmt]:703        for stmt in block.body:704            if isinstance(stmt, AssignmentStmt):705                yield stmt706            elif isinstance(stmt, IfStmt):707                yield from self._get_assignment_statements_from_if_statement(stmt)708 709    def collect_field_or_class_var_from_stmt(  # noqa C901710        self, stmt: AssignmentStmt, model_config: ModelConfigData, class_vars: dict[str, PydanticModelClassVar]711    ) -> PydanticModelField | PydanticModelClassVar | None:712        """Get pydantic model field from statement.713 714        Args:715            stmt: The statement.716            model_config: Configuration settings for the model.717            class_vars: ClassVars already known to be defined on the model.718 719        Returns:720            A pydantic model field if it could find the field in statement. Otherwise, `None`.721        """722        cls = self._cls723 724        lhs = stmt.lvalues[0]725        if not isinstance(lhs, NameExpr) or not _fields.is_valid_field_name(lhs.name) or lhs.name == 'model_config':726            return None727 728        if not stmt.new_syntax:729            if (730                isinstance(stmt.rvalue, CallExpr)731                and isinstance(stmt.rvalue.callee, CallExpr)732                and isinstance(stmt.rvalue.callee.callee, NameExpr)733                and stmt.rvalue.callee.callee.fullname in DECORATOR_FULLNAMES734            ):735                # This is a (possibly-reused) validator or serializer, not a field736                # In particular, it looks something like: my_validator = validator('my_field')(f)737                # Eventually, we may want to attempt to respect model_config['ignored_types']738                return None739 740            if lhs.name in class_vars:741                # Class vars are not fields and are not required to be annotated742                return None743 744            # The assignment does not have an annotation, and it's not anything else we recognize745            error_untyped_fields(self._api, stmt)746            return None747 748        lhs = stmt.lvalues[0]749        if not isinstance(lhs, NameExpr):750            return None751 752        if not _fields.is_valid_field_name(lhs.name) or lhs.name == 'model_config':753            return None754 755        sym = cls.info.names.get(lhs.name)756        if sym is None:  # pragma: no cover757            # This is likely due to a star import (see the dataclasses plugin for a more detailed explanation)758            # This is the same logic used in the dataclasses plugin759            return None760 761        node = sym.node762        if isinstance(node, PlaceholderNode):  # pragma: no cover763            # See the PlaceholderNode docstring for more detail about how this can occur764            # Basically, it is an edge case when dealing with complex import logic765 766            # The dataclasses plugin now asserts this cannot happen, but I'd rather not error if it does..767            return None768 769        if isinstance(node, TypeAlias):770            self._api.fail(771                'Type aliases inside BaseModel definitions are not supported at runtime',772                node,773            )774            # Skip processing this node. This doesn't match the runtime behaviour,775            # but the only alternative would be to modify the SymbolTable,776            # and it's a little hairy to do that in a plugin.777            return None778 779        if not isinstance(node, Var):  # pragma: no cover780            # Don't know if this edge case still happens with the `is_valid_field` check above781            # but better safe than sorry782 783            # The dataclasses plugin now asserts this cannot happen, but I'd rather not error if it does..784            return None785 786        # x: ClassVar[int] is not a field787        if node.is_classvar:788            return PydanticModelClassVar(lhs.name)789 790        # x: InitVar[int] is not supported in BaseModel791        node_type = get_proper_type(node.type)792        if isinstance(node_type, Instance) and node_type.type.fullname == 'dataclasses.InitVar':793            self._api.fail(794                'InitVar is not supported in BaseModel',795                node,796            )797 798        has_default = self.get_has_default(stmt)799        strict = self.get_strict(stmt)800 801        if sym.type is None and node.is_final and node.is_inferred:802            # This follows the logic from the dataclasses plugin. The following comment is taken verbatim:803            #804            # This is a special case, assignment like x: Final = 42 is classified805            # annotated above, but mypy strips the `Final` turning it into x = 42.806            # We do not support inferred types in dataclasses, so we can try inferring807            # type for simple literals, and otherwise require an explicit type808            # argument for Final[...].809            typ = self._api.analyze_simple_literal_type(stmt.rvalue, is_final=True)810            if typ:811                node.type = typ812            else:813                self._api.fail(814                    'Need type argument for Final[...] with non-literal default in BaseModel',815                    stmt,816                )817                node.type = AnyType(TypeOfAny.from_error)818 819        if node.is_final and has_default:820            # TODO this path should be removed (see https://github.com/pydantic/pydantic/issues/11119)821            return PydanticModelClassVar(lhs.name)822 823        alias, has_dynamic_alias = self.get_alias_info(stmt)824        if (825            has_dynamic_alias826            and not (model_config.validate_by_name or model_config.populate_by_name)827            and self.plugin_config.warn_required_dynamic_aliases828        ):829            error_required_dynamic_aliases(self._api, stmt)830        is_frozen = self.is_field_frozen(stmt)831 832        init_type = self._infer_dataclass_attr_init_type(sym, lhs.name, stmt)833        return PydanticModelField(834            name=lhs.name,835            has_dynamic_alias=has_dynamic_alias,836            has_default=has_default,837            strict=strict,838            alias=alias,839            is_frozen=is_frozen,840            line=stmt.line,841            column=stmt.column,842            type=init_type,843            info=cls.info,844        )845 846    def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str, context: Context) -> Type | None:847        """Infer __init__ argument type for an attribute.848 849        In particular, possibly use the signature of __set__.850        """851        default = sym.type852        if sym.implicit:853            return default854        t = get_proper_type(sym.type)855 856        # Perform a simple-minded inference from the signature of __set__, if present.857        # We can't use mypy.checkmember here, since this plugin runs before type checking.858        # We only support some basic scanerios here, which is hopefully sufficient for859        # the vast majority of use cases.860        if not isinstance(t, Instance):861            return default862        setter = t.type.get('__set__')863        if setter:864            if isinstance(setter.node, FuncDef):865                super_info = t.type.get_containing_type_info('__set__')866                assert super_info867                if setter.type:868                    setter_type = get_proper_type(map_type_from_supertype(setter.type, t.type, super_info))869                else:870                    return AnyType(TypeOfAny.unannotated)871                if isinstance(setter_type, CallableType) and setter_type.arg_kinds == [872                    ARG_POS,873                    ARG_POS,874                    ARG_POS,875                ]:876                    return expand_type_by_instance(setter_type.arg_types[2], t)877                else:878                    self._api.fail(f'Unsupported signature for "__set__" in "{t.type.name}"', context)879            else:880                self._api.fail(f'Unsupported "__set__" in "{t.type.name}"', context)881 882        return default883 884    def add_initializer(885        self, fields: list[PydanticModelField], config: ModelConfigData, is_settings: bool, is_root_model: bool886    ) -> None:887        """Adds a fields-aware `__init__` method to the class.888 889        The added `__init__` will be annotated with types vs. all `Any` depending on the plugin settings.890        """891        if '__init__' in self._cls.info.names and not self._cls.info.names['__init__'].plugin_generated:892            return  # Don't generate an __init__ if one already exists893 894        typed = self.plugin_config.init_typed895        model_strict = bool(config.strict)896        use_alias = not (config.validate_by_name or config.populate_by_name) and config.validate_by_alias is not False897        requires_dynamic_aliases = bool(config.has_alias_generator and not config.validate_by_name)898        args = self.get_field_arguments(899            fields,900            typed=typed,901            model_strict=model_strict,902            requires_dynamic_aliases=requires_dynamic_aliases,903            use_alias=use_alias,904            is_settings=is_settings,905            is_root_model=is_root_model,906            force_typevars_invariant=True,907        )908 909        if is_settings:910            base_settings_node = self._api.lookup_fully_qualified(BASESETTINGS_FULLNAME).node911            assert isinstance(base_settings_node, TypeInfo)912            if '__init__' in base_settings_node.names:913                base_settings_init_node = base_settings_node.names['__init__'].node914                assert isinstance(base_settings_init_node, FuncDef)915                if base_settings_init_node is not None and base_settings_init_node.type is not None:916                    func_type = base_settings_init_node.type917                    assert isinstance(func_type, CallableType)918                    for arg_idx, arg_name in enumerate(func_type.arg_names):919                        if arg_name is None or arg_name.startswith('__') or not arg_name.startswith('_'):920                            continue921                        analyzed_variable_type = self._api.anal_type(func_type.arg_types[arg_idx])922                        if analyzed_variable_type is not None and arg_name in (923                            '_cli_settings_source',924                            '_build_sources',925                        ):926                            # These arg names are annotated with types explicitly parameterized with `Any`, and as such927                            # the Any causes issues with --disallow-any-explicit. As a workaround, change928                            # the Any type (as if the generic type was left unparameterized):929                            analyzed_variable_type = analyzed_variable_type.accept(930                                ChangeExplicitTypeOfAny(TypeOfAny.from_omitted_generics)931                            )932                        variable = Var(arg_name, analyzed_variable_type)933                        args.append(Argument(variable, analyzed_variable_type, None, ARG_OPT))934 935        if not self.should_init_forbid_extra(fields, config):936            var = Var('kwargs')937            args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2))938 939        add_method(self._api, self._cls, '__init__', args=args, return_type=NoneType())940 941    def add_model_construct_method(942        self,943        fields: list[PydanticModelField],944        config: ModelConfigData,945        is_settings: bool,946        is_root_model: bool,947    ) -> None:948        """Adds a fully typed `model_construct` classmethod to the class.949 950        Similar to the fields-aware __init__ method, but always uses the field names (not aliases),951        and does not treat settings fields as optional.952        """953        set_str = self._api.named_type(f'{BUILTINS_NAME}.set', [self._api.named_type(f'{BUILTINS_NAME}.str')])954        optional_set_str = UnionType([set_str, NoneType()])955        fields_set_argument = Argument(Var('_fields_set', optional_set_str), optional_set_str, None, ARG_OPT)956        with state.strict_optional_set(self._api.options.strict_optional):957            args = self.get_field_arguments(958                fields,959                typed=True,960                model_strict=bool(config.strict),961                requires_dynamic_aliases=False,962                use_alias=False,963                is_settings=is_settings,964                is_root_model=is_root_model,965            )966        if not self.should_init_forbid_extra(fields, config):967            var = Var('kwargs')968            args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2))969 970        args = args + [fields_set_argument] if is_root_model else [fields_set_argument] + args971 972        add_method(973            self._api,974            self._cls,975            'model_construct',976            args=args,977            return_type=fill_typevars(self._cls.info),978            is_classmethod=True,979        )980 981    def set_frozen(self, fields: list[PydanticModelField], api: SemanticAnalyzerPluginInterface, frozen: bool) -> None:982        """Marks all fields as properties so that attempts to set them trigger mypy errors.983 984        This is the same approach used by the attrs and dataclasses plugins.985        """986        info = self._cls.info987        for field in fields:988            sym_node = info.names.get(field.name)989            if sym_node is not None:990                var = sym_node.node991                if isinstance(var, Var):992                    var.is_property = frozen or field.is_frozen993                elif isinstance(var, PlaceholderNode) and not self._api.final_iteration:994                    # See https://github.com/pydantic/pydantic/issues/5191 to hit this branch for test coverage995                    self._api.defer()996                # `var` can also be a FuncDef or Decorator node (e.g. when overriding a field with a function or property).997                # In that case, we don't want to do anything. Mypy will already raise an error that a field was not properly998                # overridden.999            else:1000                var = field.to_var(info, api, use_alias=False)1001                var.info = info1002                var.is_property = frozen1003                var._fullname = info.fullname + '.' + var.name1004                info.names[var.name] = SymbolTableNode(MDEF, var)1005 1006    def get_config_update(self, name: str, arg: Expression, lax_extra: bool = False) -> ModelConfigData | None:1007        """Determines the config update due to a single kwarg in the ConfigDict definition.1008 1009        Warns if a tracked config attribute is set to a value the plugin doesn't know how to interpret (e.g., an int)1010        """1011        if name not in self.tracked_config_fields:1012            return None1013        if name == 'extra':1014            if isinstance(arg, StrExpr):1015                forbid_extra = arg.value == 'forbid'1016            elif isinstance(arg, MemberExpr):1017                forbid_extra = arg.name == 'forbid'1018            else:1019                if not lax_extra:1020                    # Only emit an error for other types of `arg` (e.g., `NameExpr`, `ConditionalExpr`, etc.) when1021                    # reading from a config class, etc. If a ConfigDict is used, then we don't want to emit an error1022                    # because you'll get type checking from the ConfigDict itself.1023                    #1024                    # It would be nice if we could introspect the types better otherwise, but I don't know what the API1025                    # is to evaluate an expr into its type and then check if that type is compatible with the expected1026                    # type. Note that you can still get proper type checking via: `model_config = ConfigDict(...)`, just1027                    # if you don't use an explicit string, the plugin won't be able to infer whether extra is forbidden.1028                    error_invalid_config_value(name, self._api, arg)1029                return None1030            return ModelConfigData(forbid_extra=forbid_extra)1031        if name == 'alias_generator':1032            has_alias_generator = True1033            if isinstance(arg, NameExpr) and arg.fullname == 'builtins.None':1034                has_alias_generator = False1035            return ModelConfigData(has_alias_generator=has_alias_generator)1036        if isinstance(arg, NameExpr) and arg.fullname in ('builtins.True', 'builtins.False'):1037            return ModelConfigData(**{name: arg.fullname == 'builtins.True'})1038        error_invalid_config_value(name, self._api, arg)1039        return None1040 1041    @staticmethod1042    def get_has_default(stmt: AssignmentStmt) -> bool:1043        """Returns a boolean indicating whether the field defined in `stmt` is a required field."""1044        expr = stmt.rvalue1045        if isinstance(expr, TempNode):1046            # TempNode means annotation-only, so has no default1047            return False1048        if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME:1049            # The "default value" is a call to `Field`; at this point, the field has a default if and only if:1050            # * there is a positional argument that is not `...`1051            # * there is a keyword argument named "default" that is not `...`1052            # * there is a "default_factory" that is not `None`1053            for arg, name in zip(expr.args, expr.arg_names):1054                # If name is None, then this arg is the default because it is the only positional argument.1055                if name is None or name == 'default':1056                    return arg.__class__ is not EllipsisExpr1057                if name == 'default_factory':1058                    return not (isinstance(arg, NameExpr) and arg.fullname == 'builtins.None')1059            return False1060        # Has no default if the "default value" is Ellipsis (i.e., `field_name: Annotation = ...`)1061        return not isinstance(expr, EllipsisExpr)1062 1063    @staticmethod1064    def get_strict(stmt: AssignmentStmt) -> bool | None:1065        """Returns a the `strict` value of a field if defined, otherwise `None`."""1066        expr = stmt.rvalue1067        if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME:1068            for arg, name in zip(expr.args, expr.arg_names):1069                if name != 'strict':1070                    continue1071                if isinstance(arg, NameExpr):1072                    if arg.fullname == 'builtins.True':1073                        return True1074                    elif arg.fullname == 'builtins.False':1075                        return False1076                return None1077        return None1078 1079    @staticmethod1080    def get_alias_info(stmt: AssignmentStmt) -> tuple[str | None, bool]:1081        """Returns a pair (alias, has_dynamic_alias), extracted from the declaration of the field defined in `stmt`.1082 1083        `has_dynamic_alias` is True if and only if an alias is provided, but not as a string literal.1084        If `has_dynamic_alias` is True, `alias` will be None.1085        """1086        expr = stmt.rvalue1087        if isinstance(expr, TempNode):1088            # TempNode means annotation-only1089            return None, False1090 1091        if not (1092            isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME1093        ):1094            # Assigned value is not a call to pydantic.fields.Field1095            return None, False1096 1097        if 'validation_alias' in expr.arg_names:1098            arg = expr.args[expr.arg_names.index('validation_alias')]1099        elif 'alias' in expr.arg_names:1100            arg = expr.args[expr.arg_names.index('alias')]1101        else:1102            return None, False1103 1104        if isinstance(arg, StrExpr):1105            return arg.value, False1106        else:1107            return None, True1108 1109    @staticmethod1110    def is_field_frozen(stmt: AssignmentStmt) -> bool:1111        """Returns whether the field is frozen, extracted from the declaration of the field defined in `stmt`.1112 1113        Note that this is only whether the field was declared to be frozen in a `<field_name> = Field(frozen=True)`1114        sense; this does not determine whether the field is frozen because the entire model is frozen; that is1115        handled separately.1116        """1117        expr = stmt.rvalue1118        if isinstance(expr, TempNode):1119            # TempNode means annotation-only1120            return False1121 1122        if not (1123            isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME1124        ):1125            # Assigned value is not a call to pydantic.fields.Field1126            return False1127 1128        for i, arg_name in enumerate(expr.arg_names):1129            if arg_name == 'frozen':1130                arg = expr.args[i]1131                return isinstance(arg, NameExpr) and arg.fullname == 'builtins.True'1132        return False1133 1134    def get_field_arguments(1135        self,1136        fields: list[PydanticModelField],1137        typed: bool,1138        model_strict: bool,1139        use_alias: bool,1140        requires_dynamic_aliases: bool,1141        is_settings: bool,1142        is_root_model: bool,1143        force_typevars_invariant: bool = False,1144    ) -> list[Argument]:1145        """Helper function used during the construction of the `__init__` and `model_construct` method signatures.1146 1147        Returns a list of mypy Argument instances for use in the generated signatures.1148        """1149        info = self._cls.info1150        arguments = [1151            field.to_argument(1152                info,1153                typed=typed,1154                model_strict=model_strict,1155                force_optional=requires_dynamic_aliases or is_settings,1156                use_alias=use_alias,1157                api=self._api,1158                force_typevars_invariant=force_typevars_invariant,1159                is_root_model_root=is_root_model and field.name == 'root',1160            )1161            for field in fields1162            if not (use_alias and field.has_dynamic_alias)1163        ]1164        return arguments1165 1166    def should_init_forbid_extra(self, fields: list[PydanticModelField], config: ModelConfigData) -> bool:1167        """Indicates whether the generated `__init__` should get a `**kwargs` at the end of its signature.1168 1169        We disallow arbitrary kwargs if the extra config setting is "forbid", or if the plugin config says to,1170        *unless* a required dynamic alias is present (since then we can't determine a valid signature).1171        """1172        if not (config.validate_by_name or config.populate_by_name):1173            if self.is_dynamic_alias_present(fields, bool(config.has_alias_generator)):1174                return False1175        if config.forbid_extra:1176            return True1177        return self.plugin_config.init_forbid_extra1178 1179    @staticmethod1180    def is_dynamic_alias_present(fields: list[PydanticModelField], has_alias_generator: bool) -> bool:1181        """Returns whether any fields on the model have a "dynamic alias", i.e., an alias that cannot be1182        determined during static analysis.1183        """1184        for field in fields:1185            if field.has_dynamic_alias:1186                return True1187        if has_alias_generator:1188            for field in fields:1189                if field.alias is None:1190                    return True1191        return False1192 1193 1194class ChangeExplicitTypeOfAny(TypeTranslator):1195    """A type translator used to change type of Any's, if explicit."""1196 1197    def __init__(self, type_of_any: int) -> None:1198        self._type_of_any = type_of_any1199        super().__init__()1200 

Showing the first 1,200 of 1413 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai