codekingpro/portable-devtools
114k
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 