Coverage for pydantic/mypy.py: 90.97%

593 statements  

« prev     ^ index     » next       coverage.py v7.5.4, created at 2024-07-03 19:29 +0000

1"""This module includes classes and functions designed specifically for use with the mypy plugin.""" 

2 

3from __future__ import annotations 1klmnopqrstuvwxyzABCDEFGHIghfbcaijdeJKLMNOPQRSTU

4 

5import sys 1klmnopqrstuvwxyzABCDEFGHIghfbcaijdeJKLMNOPQRSTU

6from configparser import ConfigParser 1klmnopqrstuvwxyzABCDEFGHIghfbcaijdeJKLMNOPQRSTU

7from typing import Any, Callable, Iterator 1klmnopqrstuvwxyzABCDEFGHIghfbcaijdeJKLMNOPQRSTU

8 

9from mypy.errorcodes import ErrorCode 1klmnopqrstuvwxyzABCDEFGHIghfbcaijdeJKLMNOPQRSTU

10from mypy.expandtype import expand_type, expand_type_by_instance 1ghfbcaijde

11from mypy.nodes import ( 1ghfbcaijde

12 ARG_NAMED, 

13 ARG_NAMED_OPT, 

14 ARG_OPT, 

15 ARG_POS, 

16 ARG_STAR2, 

17 INVARIANT, 

18 MDEF, 

19 Argument, 

20 AssignmentStmt, 

21 Block, 

22 CallExpr, 

23 ClassDef, 

24 Context, 

25 Decorator, 

26 DictExpr, 

27 EllipsisExpr, 

28 Expression, 

29 FuncDef, 

30 IfStmt, 

31 JsonDict, 

32 MemberExpr, 

33 NameExpr, 

34 PassStmt, 

35 PlaceholderNode, 

36 RefExpr, 

37 Statement, 

38 StrExpr, 

39 SymbolTableNode, 

40 TempNode, 

41 TypeAlias, 

42 TypeInfo, 

43 Var, 

44) 

45from mypy.options import Options 1ghfbcaijde

46from mypy.plugin import ( 1ghfbcaijde

47 CheckerPluginInterface, 

48 ClassDefContext, 

49 FunctionContext, 

50 MethodContext, 

51 Plugin, 

52 ReportConfigContext, 

53 SemanticAnalyzerPluginInterface, 

54) 

55from mypy.plugins import dataclasses 1ghfbcaijde

56from mypy.plugins.common import ( 1ghfbcaijde

57 deserialize_and_fixup_type, 

58) 

59from mypy.semanal import set_callable_name 1ghfbcaijde

60from mypy.server.trigger import make_wildcard_trigger 1ghfbcaijde

61from mypy.state import state 1ghfbcaijde

62from mypy.typeops import map_type_from_supertype 1ghfbcaijde

63from mypy.types import ( 1ghfbcaijde

64 AnyType, 

65 CallableType, 

66 Instance, 

67 NoneType, 

68 Overloaded, 

69 Type, 

70 TypeOfAny, 

71 TypeType, 

72 TypeVarType, 

73 UnionType, 

74 get_proper_type, 

75) 

76from mypy.typevars import fill_typevars 1ghfbcaijde

77from mypy.util import get_unique_redefinition_name 1ghfbcaijde

78from mypy.version import __version__ as mypy_version 1ghfbcaijde

79 

80from pydantic._internal import _fields 1ghfbcaijde

81from pydantic.version import parse_mypy_version 1ghfbcaijde

82 

83try: 1ghfbcaijde

84 from mypy.types import TypeVarDef # type: ignore[attr-defined] 1ghfbcaijde

85except ImportError: # pragma: no cover 1ghfbcaijde

86 # Backward-compatible with TypeVarDef from Mypy 0.930. 

87 from mypy.types import TypeVarType as TypeVarDef 1ghfbcaijde

88 

89CONFIGFILE_KEY = 'pydantic-mypy' 1ghfbcaijde

90METADATA_KEY = 'pydantic-mypy-metadata' 1ghfbcaijde

91BASEMODEL_FULLNAME = 'pydantic.main.BaseModel' 1ghfbcaijde

92BASESETTINGS_FULLNAME = 'pydantic_settings.main.BaseSettings' 1ghfbcaijde

93ROOT_MODEL_FULLNAME = 'pydantic.root_model.RootModel' 1ghfbcaijde

94MODEL_METACLASS_FULLNAME = 'pydantic._internal._model_construction.ModelMetaclass' 1ghfbcaijde

95FIELD_FULLNAME = 'pydantic.fields.Field' 1ghfbcaijde

96DATACLASS_FULLNAME = 'pydantic.dataclasses.dataclass' 1ghfbcaijde

97MODEL_VALIDATOR_FULLNAME = 'pydantic.functional_validators.model_validator' 1ghfbcaijde

98DECORATOR_FULLNAMES = { 1ghfbcaijde

99 'pydantic.functional_validators.field_validator', 

100 'pydantic.functional_validators.model_validator', 

101 'pydantic.functional_serializers.serializer', 

102 'pydantic.functional_serializers.model_serializer', 

103 'pydantic.deprecated.class_validators.validator', 

104 'pydantic.deprecated.class_validators.root_validator', 

105} 

106 

107 

108MYPY_VERSION_TUPLE = parse_mypy_version(mypy_version) 1ghfbcaijde

109BUILTINS_NAME = 'builtins' if MYPY_VERSION_TUPLE >= (0, 930) else '__builtins__' 1ghfbcaijde

110 

111# Increment version if plugin changes and mypy caches should be invalidated 

112__version__ = 2 1ghfbcaijde

113 

114 

115def plugin(version: str) -> type[Plugin]: 1ghfbcaijde

116 """`version` is the mypy version string. 

117 

118 We might want to use this to print a warning if the mypy version being used is 

119 newer, or especially older, than we expect (or need). 

120 

121 Args: 

122 version: The mypy version string. 

123 

124 Return: 

125 The Pydantic mypy plugin type. 

126 """ 

127 return PydanticPlugin 1ghfbcaijde

128 

129 

130class PydanticPlugin(Plugin): 1ghfbcaijde

131 """The Pydantic mypy plugin.""" 

132 

133 def __init__(self, options: Options) -> None: 1ghfbcaijde

134 self.plugin_config = PydanticPluginConfig(options) 1ghfbcaijde

135 self._plugin_data = self.plugin_config.to_data() 1ghfbcaijde

136 super().__init__(options) 1ghfbcaijde

137 

138 def get_base_class_hook(self, fullname: str) -> Callable[[ClassDefContext], bool] | None: 1ghfbcaijde

139 """Update Pydantic model class.""" 

140 sym = self.lookup_fully_qualified(fullname) 1ghfbcaijde

141 if sym and isinstance(sym.node, TypeInfo): # pragma: no branch 1ghfbcaijde

142 # No branching may occur if the mypy cache has not been cleared 

143 if any(base.fullname == BASEMODEL_FULLNAME for base in sym.node.mro): 1ghfbcaijde

144 return self._pydantic_model_class_maker_callback 1ghfbcaijde

145 return None 1ghfbcaijde

146 

147 def get_metaclass_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None: 1ghfbcaijde

148 """Update Pydantic `ModelMetaclass` definition.""" 

149 if fullname == MODEL_METACLASS_FULLNAME: 1ghfbcaijde

150 return self._pydantic_model_metaclass_marker_callback 1ghfbcaijde

151 return None 1ghfbcaijde

152 

153 def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type] | None: 1ghfbcaijde

154 """Adjust the return type of the `Field` function.""" 

155 sym = self.lookup_fully_qualified(fullname) 1ghfbcaijde

156 if sym and sym.fullname == FIELD_FULLNAME: 1ghfbcaijde

157 return self._pydantic_field_callback 1ghfbcaijde

158 return None 1ghfbcaijde

159 

160 def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | None: 1ghfbcaijde

161 """Adjust return type of `from_orm` method call.""" 

162 if fullname.endswith('.from_orm'): 1ghfbcaijde

163 return from_attributes_callback 1ghfbcaijde

164 return None 1ghfbcaijde

165 

166 def get_class_decorator_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None: 1ghfbcaijde

167 """Mark pydantic.dataclasses as dataclass. 

168 

169 Mypy version 1.1.1 added support for `@dataclass_transform` decorator. 

170 """ 

171 if fullname == DATACLASS_FULLNAME and MYPY_VERSION_TUPLE < (1, 1): 1ghfbcaijde

172 return dataclasses.dataclass_class_maker_callback # type: ignore[return-value] 1f

173 return None 1ghfbcaijde

174 

175 def report_config_data(self, ctx: ReportConfigContext) -> dict[str, Any]: 1ghfbcaijde

176 """Return all plugin config data. 

177 

178 Used by mypy to determine if cache needs to be discarded. 

179 """ 

180 return self._plugin_data 1ghfbcaijde

181 

182 def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> bool: 1ghfbcaijde

183 transformer = PydanticModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config) 1ghfbcaijde

184 return transformer.transform() 1ghfbcaijde

185 

186 def _pydantic_model_metaclass_marker_callback(self, ctx: ClassDefContext) -> None: 1ghfbcaijde

187 """Reset dataclass_transform_spec attribute of ModelMetaclass. 

188 

189 Let the plugin handle it. This behavior can be disabled 

190 if 'debug_dataclass_transform' is set to True', for testing purposes. 

191 """ 

192 if self.plugin_config.debug_dataclass_transform: 192 ↛ 193line 192 didn't jump to line 193 because the condition on line 192 was never true1ghfbcaijde

193 return 

194 info_metaclass = ctx.cls.info.declared_metaclass 1ghfbcaijde

195 assert info_metaclass, "callback not passed from 'get_metaclass_hook'" 1ghfbcaijde

196 if getattr(info_metaclass.type, 'dataclass_transform_spec', None): 1ghfbcaijde

197 info_metaclass.type.dataclass_transform_spec = None 1ghbcaijde

198 

199 def _pydantic_field_callback(self, ctx: FunctionContext) -> Type: 1ghfbcaijde

200 """Extract the type of the `default` argument from the Field function, and use it as the return type. 

201 

202 In particular: 

203 * Check whether the default and default_factory argument is specified. 

204 * Output an error if both are specified. 

205 * Retrieve the type of the argument which is specified, and use it as return type for the function. 

206 """ 

207 default_any_type = ctx.default_return_type 1ghfbcaijde

208 

209 assert ctx.callee_arg_names[0] == 'default', '"default" is no longer first argument in Field()' 1ghfbcaijde

210 assert ctx.callee_arg_names[1] == 'default_factory', '"default_factory" is no longer second argument in Field()' 1ghfbcaijde

211 default_args = ctx.args[0] 1ghfbcaijde

212 default_factory_args = ctx.args[1] 1ghfbcaijde

213 

214 if default_args and default_factory_args: 1ghfbcaijde

215 error_default_and_default_factory_specified(ctx.api, ctx.context) 1ghfbcaijde

216 return default_any_type 1ghfbcaijde

217 

218 if default_args: 1ghfbcaijde

219 default_type = ctx.arg_types[0][0] 1ghfbcaijde

220 default_arg = default_args[0] 1ghfbcaijde

221 

222 # Fallback to default Any type if the field is required 

223 if not isinstance(default_arg, EllipsisExpr): 1ghfbcaijde

224 return default_type 1ghfbcaijde

225 

226 elif default_factory_args: 1ghfbcaijde

227 default_factory_type = ctx.arg_types[1][0] 1ghfbcaijde

228 

229 # Functions which use `ParamSpec` can be overloaded, exposing the callable's types as a parameter 

230 # Pydantic calls the default factory without any argument, so we retrieve the first item 

231 if isinstance(default_factory_type, Overloaded): 1ghfbcaijde

232 default_factory_type = default_factory_type.items[0] 1ghfbcaijde

233 

234 if isinstance(default_factory_type, CallableType): 1ghfbcaijde

235 ret_type = default_factory_type.ret_type 1ghfbcaijde

236 # mypy doesn't think `ret_type` has `args`, you'd think mypy should know, 

237 # add this check in case it varies by version 

238 args = getattr(ret_type, 'args', None) 1ghfbcaijde

239 if args: 1ghfbcaijde

240 if all(isinstance(arg, TypeVarType) for arg in args): 1ghfbcaijde

241 # Looks like the default factory is a type like `list` or `dict`, replace all args with `Any` 

242 ret_type.args = tuple(default_any_type for _ in args) # type: ignore[attr-defined] 1ghfbcaijde

243 return ret_type 1ghfbcaijde

244 

245 return default_any_type 1ghfbcaijde

246 

247 

248class PydanticPluginConfig: 1ghfbcaijde

249 """A Pydantic mypy plugin config holder. 

250 

251 Attributes: 

252 init_forbid_extra: Whether to add a `**kwargs` at the end of the generated `__init__` signature. 

253 init_typed: Whether to annotate fields in the generated `__init__`. 

254 warn_required_dynamic_aliases: Whether to raise required dynamic aliases error. 

255 debug_dataclass_transform: Whether to not reset `dataclass_transform_spec` attribute 

256 of `ModelMetaclass` for testing purposes. 

257 """ 

258 

259 __slots__ = ( 1ghfbcaijde

260 'init_forbid_extra', 

261 'init_typed', 

262 'warn_required_dynamic_aliases', 

263 'debug_dataclass_transform', 

264 ) 

265 init_forbid_extra: bool 1ghfbcaijde

266 init_typed: bool 1ghfbcaijde

267 warn_required_dynamic_aliases: bool 1ghfbcaijde

268 debug_dataclass_transform: bool # undocumented 1ghfbcaijde

269 

270 def __init__(self, options: Options) -> None: 1ghfbcaijde

271 if options.config_file is None: # pragma: no cover 1ghfbcaijde

272 return 

273 

274 toml_config = parse_toml(options.config_file) 1ghfbcaijde

275 if toml_config is not None: 1ghfbcaijde

276 config = toml_config.get('tool', {}).get('pydantic-mypy', {}) 1ghfbcaijde

277 for key in self.__slots__: 1ghfbcaijde

278 setting = config.get(key, False) 1ghfbcaijde

279 if not isinstance(setting, bool): 1ghfbcaijde

280 raise ValueError(f'Configuration value must be a boolean for key: {key}') 1ghfbcaijde

281 setattr(self, key, setting) 1ghfbcaijde

282 else: 

283 plugin_config = ConfigParser() 1ghfbcaijde

284 plugin_config.read(options.config_file) 1ghfbcaijde

285 for key in self.__slots__: 1ghfbcaijde

286 setting = plugin_config.getboolean(CONFIGFILE_KEY, key, fallback=False) 1ghfbcaijde

287 setattr(self, key, setting) 1ghfbcaijde

288 

289 def to_data(self) -> dict[str, Any]: 1ghfbcaijde

290 """Returns a dict of config names to their values.""" 

291 return {key: getattr(self, key) for key in self.__slots__} 1ghfbcaijde

292 

293 

294def from_attributes_callback(ctx: MethodContext) -> Type: 1ghfbcaijde

295 """Raise an error if from_attributes is not enabled.""" 

296 model_type: Instance 

297 ctx_type = ctx.type 1ghfbcaijde

298 if isinstance(ctx_type, TypeType): 1ghfbcaijde

299 ctx_type = ctx_type.item 1ghfbcaijde

300 if isinstance(ctx_type, CallableType) and isinstance(ctx_type.ret_type, Instance): 1ghfbcaijde

301 model_type = ctx_type.ret_type # called on the class 1bcade

302 elif isinstance(ctx_type, Instance): 1ghfbcaijde

303 model_type = ctx_type # called on an instance (unusual, but still valid) 1ghfbcaijde

304 else: # pragma: no cover 

305 detail = f'ctx.type: {ctx_type} (of type {ctx_type.__class__.__name__})' 

306 error_unexpected_behavior(detail, ctx.api, ctx.context) 

307 return ctx.default_return_type 

308 pydantic_metadata = model_type.type.metadata.get(METADATA_KEY) 1ghfbcaijde

309 if pydantic_metadata is None: 1ghfbcaijde

310 return ctx.default_return_type 1ghfbcaijde

311 from_attributes = pydantic_metadata.get('config', {}).get('from_attributes') 1bcade

312 if from_attributes is not True: 312 ↛ 314line 312 didn't jump to line 314 because the condition on line 312 was always true1bcade

313 error_from_attributes(model_type.type.name, ctx.api, ctx.context) 1bcade

314 return ctx.default_return_type 1bcade

315 

316 

317class PydanticModelField: 1ghfbcaijde

318 """Based on mypy.plugins.dataclasses.DataclassAttribute.""" 

319 

320 def __init__( 1ghfbcaijde

321 self, 

322 name: str, 

323 alias: str | None, 

324 has_dynamic_alias: bool, 

325 has_default: bool, 

326 line: int, 

327 column: int, 

328 type: Type | None, 

329 info: TypeInfo, 

330 ): 

331 self.name = name 1ghfbcaijde

332 self.alias = alias 1ghfbcaijde

333 self.has_dynamic_alias = has_dynamic_alias 1ghfbcaijde

334 self.has_default = has_default 1ghfbcaijde

335 self.line = line 1ghfbcaijde

336 self.column = column 1ghfbcaijde

337 self.type = type 1ghfbcaijde

338 self.info = info 1ghfbcaijde

339 

340 def to_argument( 1ghfbcaijde

341 self, 

342 current_info: TypeInfo, 

343 typed: bool, 

344 force_optional: bool, 

345 use_alias: bool, 

346 api: SemanticAnalyzerPluginInterface, 

347 force_typevars_invariant: bool, 

348 ) -> Argument: 

349 """Based on mypy.plugins.dataclasses.DataclassAttribute.to_argument.""" 

350 variable = self.to_var(current_info, api, use_alias, force_typevars_invariant) 1ghfbcaijde

351 type_annotation = self.expand_type(current_info, api) if typed else AnyType(TypeOfAny.explicit) 1ghfbcaijde

352 return Argument( 1ghfbcaijde

353 variable=variable, 

354 type_annotation=type_annotation, 

355 initializer=None, 

356 kind=ARG_NAMED_OPT if force_optional or self.has_default else ARG_NAMED, 

357 ) 

358 

359 def expand_type( 1ghfbcaijde

360 self, current_info: TypeInfo, api: SemanticAnalyzerPluginInterface, force_typevars_invariant: bool = False 

361 ) -> Type | None: 

362 """Based on mypy.plugins.dataclasses.DataclassAttribute.expand_type.""" 

363 # The getattr in the next line is used to prevent errors in legacy versions of mypy without this attribute 

364 if force_typevars_invariant: 1ghfbcaijde

365 # In some cases, mypy will emit an error "Cannot use a covariant type variable as a parameter" 

366 # To prevent that, we add an option to replace typevars with invariant ones while building certain 

367 # method signatures (in particular, `__init__`). There may be a better way to do this, if this causes 

368 # us problems in the future, we should look into why the dataclasses plugin doesn't have this issue. 

369 if isinstance(self.type, TypeVarType): 1ghfbcaijde

370 modified_type = self.type.copy_modified() 1ghfbcaijde

371 modified_type.variance = INVARIANT 1ghfbcaijde

372 self.type = modified_type 1ghfbcaijde

373 

374 if self.type is not None and getattr(self.info, 'self_type', None) is not None: 1ghfbcaijde

375 # In general, it is not safe to call `expand_type()` during semantic analyzis, 

376 # however this plugin is called very late, so all types should be fully ready. 

377 # Also, it is tricky to avoid eager expansion of Self types here (e.g. because 

378 # we serialize attributes). 

379 with state.strict_optional_set(api.options.strict_optional): 1ghfbcaijde

380 filled_with_typevars = fill_typevars(current_info) 1ghfbcaijde

381 if force_typevars_invariant: 1ghfbcaijde

382 for arg in filled_with_typevars.args: 1ghfbcaijde

383 if isinstance(arg, TypeVarType): 383 ↛ 382line 383 didn't jump to line 382 because the condition on line 383 was always true1ade

384 arg.variance = INVARIANT 1ade

385 return expand_type(self.type, {self.info.self_type.id: filled_with_typevars}) 1ghfbcaijde

386 return self.type 1ghfbcij

387 

388 def to_var( 1ghfbcaijde

389 self, 

390 current_info: TypeInfo, 

391 api: SemanticAnalyzerPluginInterface, 

392 use_alias: bool, 

393 force_typevars_invariant: bool = False, 

394 ) -> Var: 

395 """Based on mypy.plugins.dataclasses.DataclassAttribute.to_var.""" 

396 if use_alias and self.alias is not None: 1ghfbcaijde

397 name = self.alias 1ghfbcaijde

398 else: 

399 name = self.name 1ghfbcaijde

400 

401 return Var(name, self.expand_type(current_info, api, force_typevars_invariant)) 1ghfbcaijde

402 

403 def serialize(self) -> JsonDict: 1ghfbcaijde

404 """Based on mypy.plugins.dataclasses.DataclassAttribute.serialize.""" 

405 assert self.type 1ghfbcaijde

406 return { 1ghfbcaijde

407 'name': self.name, 

408 'alias': self.alias, 

409 'has_dynamic_alias': self.has_dynamic_alias, 

410 'has_default': self.has_default, 

411 'line': self.line, 

412 'column': self.column, 

413 'type': self.type.serialize(), 

414 } 

415 

416 @classmethod 1ghfbcaijde

417 def deserialize(cls, info: TypeInfo, data: JsonDict, api: SemanticAnalyzerPluginInterface) -> PydanticModelField: 1ghfbcaijde

418 """Based on mypy.plugins.dataclasses.DataclassAttribute.deserialize.""" 

419 data = data.copy() 1ghfbcaijde

420 typ = deserialize_and_fixup_type(data.pop('type'), api) 1ghfbcaijde

421 return cls(type=typ, info=info, **data) 1ghfbcaijde

422 

423 def expand_typevar_from_subtype(self, sub_type: TypeInfo, api: SemanticAnalyzerPluginInterface) -> None: 1ghfbcaijde

424 """Expands type vars in the context of a subtype when an attribute is inherited 

425 from a generic super type. 

426 """ 

427 if self.type is not None: 427 ↛ exitline 427 didn't return from function 'expand_typevar_from_subtype' because the condition on line 427 was always true1ghfbcaijde

428 with state.strict_optional_set(api.options.strict_optional): 1ghfbcaijde

429 self.type = map_type_from_supertype(self.type, sub_type, self.info) 1ghfbcaijde

430 

431 

432class PydanticModelClassVar: 1ghfbcaijde

433 """Based on mypy.plugins.dataclasses.DataclassAttribute. 

434 

435 ClassVars are ignored by subclasses. 

436 

437 Attributes: 

438 name: the ClassVar name 

439 """ 

440 

441 def __init__(self, name): 1ghfbcaijde

442 self.name = name 1ghfbcaijde

443 

444 @classmethod 1ghfbcaijde

445 def deserialize(cls, data: JsonDict) -> PydanticModelClassVar: 1ghfbcaijde

446 """Based on mypy.plugins.dataclasses.DataclassAttribute.deserialize.""" 

447 data = data.copy() 

448 return cls(**data) 

449 

450 def serialize(self) -> JsonDict: 1ghfbcaijde

451 """Based on mypy.plugins.dataclasses.DataclassAttribute.serialize.""" 

452 return { 1ghfbcaijde

453 'name': self.name, 

454 } 

455 

456 

457class PydanticModelTransformer: 1ghfbcaijde

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] = { 1ghfbcaijde

465 'extra', 

466 'frozen', 

467 'from_attributes', 

468 'populate_by_name', 

469 'alias_generator', 

470 } 

471 

472 def __init__( 1ghfbcaijde

473 self, 

474 cls: ClassDef, 

475 reason: Expression | Statement, 

476 api: SemanticAnalyzerPluginInterface, 

477 plugin_config: PydanticPluginConfig, 

478 ) -> None: 

479 self._cls = cls 1ghfbcaijde

480 self._reason = reason 1ghfbcaijde

481 self._api = api 1ghfbcaijde

482 

483 self.plugin_config = plugin_config 1ghfbcaijde

484 

485 def transform(self) -> bool: 1ghfbcaijde

486 """Configures the BaseModel subclass according to the plugin settings. 

487 

488 In particular: 

489 

490 * determines the model config and fields, 

491 * adds a fields-aware signature for the initializer and construct methods 

492 * freezes the class if frozen = True 

493 * stores the fields, config, and if the class is settings in the mypy metadata for access by subclasses 

494 """ 

495 info = self._cls.info 1ghfbcaijde

496 is_root_model = any(ROOT_MODEL_FULLNAME in base.fullname for base in info.mro[:-1]) 1ghfbcaijde

497 config = self.collect_config() 1ghfbcaijde

498 fields, class_vars = self.collect_fields_and_class_vars(config, is_root_model) 1ghfbcaijde

499 if fields is None or class_vars is None: 499 ↛ 501line 499 didn't jump to line 501 because the condition on line 499 was never true1ghfbcaijde

500 # Some definitions are not ready. We need another pass. 

501 return False 

502 for field in fields: 1ghfbcaijde

503 if field.type is None: 1ghfbcaijde

504 return False 1ghfbcaijde

505 

506 is_settings = any(base.fullname == BASESETTINGS_FULLNAME for base in info.mro[:-1]) 1ghfbcaijde

507 self.add_initializer(fields, config, is_settings, is_root_model) 1ghfbcaijde

508 if not is_root_model: 1ghfbcaijde

509 self.add_model_construct_method(fields, config, is_settings) 1ghfbcaijde

510 self.set_frozen(fields, self._api, frozen=config.frozen is True) 1ghfbcaijde

511 

512 self.adjust_decorator_signatures() 1ghfbcaijde

513 

514 info.metadata[METADATA_KEY] = { 1ghfbcaijde

515 'fields': {field.name: field.serialize() for field in fields}, 

516 'class_vars': {class_var.name: class_var.serialize() for class_var in class_vars}, 

517 'config': config.get_values_dict(), 

518 } 

519 

520 return True 1ghfbcaijde

521 

522 def adjust_decorator_signatures(self) -> None: 1ghfbcaijde

523 """When we decorate a function `f` with `pydantic.validator(...)`, `pydantic.field_validator` 

524 or `pydantic.serializer(...)`, mypy sees `f` as a regular method taking a `self` instance, 

525 even though pydantic internally wraps `f` with `classmethod` if necessary. 

526 

527 Teach mypy this by marking any function whose outermost decorator is a `validator()`, 

528 `field_validator()` or `serializer()` call as a `classmethod`. 

529 """ 

530 for name, sym in self._cls.info.names.items(): 1ghfbcaijde

531 if isinstance(sym.node, Decorator): 1ghfbcaijde

532 first_dec = sym.node.original_decorators[0] 1ghfbcaijde

533 if ( 1ghfbcaij

534 isinstance(first_dec, CallExpr) 

535 and isinstance(first_dec.callee, NameExpr) 

536 and first_dec.callee.fullname in DECORATOR_FULLNAMES 

537 # @model_validator(mode="after") is an exception, it expects a regular method 

538 and not ( 

539 first_dec.callee.fullname == MODEL_VALIDATOR_FULLNAME 

540 and any( 

541 first_dec.arg_names[i] == 'mode' and isinstance(arg, StrExpr) and arg.value == 'after' 

542 for i, arg in enumerate(first_dec.args) 

543 ) 

544 ) 

545 ): 

546 # TODO: Only do this if the first argument of the decorated function is `cls` 

547 sym.node.func.is_class = True 1ghfbcaijde

548 

549 def collect_config(self) -> ModelConfigData: # noqa: C901 (ignore complexity) 1ghfbcaijde

550 """Collects the values of the config attributes that are used by the plugin, accounting for parent classes.""" 

551 cls = self._cls 1ghfbcaijde

552 config = ModelConfigData() 1ghfbcaijde

553 

554 has_config_kwargs = False 1ghfbcaijde

555 has_config_from_namespace = False 1ghfbcaijde

556 

557 # Handle `class MyModel(BaseModel, <name>=<expr>, ...):` 

558 for name, expr in cls.keywords.items(): 1ghfbcaijde

559 config_data = self.get_config_update(name, expr) 1ghfbcaijde

560 if config_data: 1ghfbcaijde

561 has_config_kwargs = True 1ghfbcaijde

562 config.update(config_data) 1ghfbcaijde

563 

564 # Handle `model_config` 

565 stmt: Statement | None = None 1ghfbcaijde

566 for stmt in cls.defs.body: 1ghfbcaijde

567 if not isinstance(stmt, (AssignmentStmt, ClassDef)): 1ghfbcaijde

568 continue 1ghfbcaijde

569 

570 if isinstance(stmt, AssignmentStmt): 1ghfbcaijde

571 lhs = stmt.lvalues[0] 1ghfbcaijde

572 if not isinstance(lhs, NameExpr) or lhs.name != 'model_config': 1ghfbcaijde

573 continue 1ghfbcaijde

574 

575 if isinstance(stmt.rvalue, CallExpr): # calls to `dict` or `ConfigDict` 575 ↛ 580line 575 didn't jump to line 580 because the condition on line 575 was always true1ghfbcaijde

576 for arg_name, arg in zip(stmt.rvalue.arg_names, stmt.rvalue.args): 1ghfbcaijde

577 if arg_name is None: 577 ↛ 578line 577 didn't jump to line 578 because the condition on line 577 was never true1ghfbcaijde

578 continue 

579 config.update(self.get_config_update(arg_name, arg, lax_extra=True)) 1ghfbcaijde

580 elif isinstance(stmt.rvalue, DictExpr): # dict literals 

581 for key_expr, value_expr in stmt.rvalue.items: 

582 if not isinstance(key_expr, StrExpr): 

583 continue 

584 config.update(self.get_config_update(key_expr.value, value_expr)) 

585 

586 elif isinstance(stmt, ClassDef): 586 ↛ 597line 586 didn't jump to line 597 because the condition on line 586 was always true1ghfbcaijde

587 if stmt.name != 'Config': # 'deprecated' Config-class 1ghfbcaijde

588 continue 1ghfbcaijde

589 for substmt in stmt.defs.body: 1ghfbcaijde

590 if not isinstance(substmt, AssignmentStmt): 1ghfbcaijde

591 continue 1ghfbcaijde

592 lhs = substmt.lvalues[0] 1ghfbcaijde

593 if not isinstance(lhs, NameExpr): 593 ↛ 594line 593 didn't jump to line 594 because the condition on line 593 was never true1ghfbcaijde

594 continue 

595 config.update(self.get_config_update(lhs.name, substmt.rvalue)) 1ghfbcaijde

596 

597 if has_config_kwargs: 597 ↛ 598line 597 didn't jump to line 598 because the condition on line 597 was never true1ghfbcaijde

598 self._api.fail( 

599 'Specifying config in two places is ambiguous, use either Config attribute or class kwargs', 

600 cls, 

601 ) 

602 break 

603 

604 has_config_from_namespace = True 1ghfbcaijde

605 

606 if has_config_kwargs or has_config_from_namespace: 1ghfbcaijde

607 if ( 1ghfbcaij

608 stmt 

609 and config.has_alias_generator 

610 and not config.populate_by_name 

611 and self.plugin_config.warn_required_dynamic_aliases 

612 ): 

613 error_required_dynamic_aliases(self._api, stmt) 1ghfbcaijde

614 

615 for info in cls.info.mro[1:]: # 0 is the current class 1ghfbcaijde

616 if METADATA_KEY not in info.metadata: 1ghfbcaijde

617 continue 1ghfbcaijde

618 

619 # Each class depends on the set of fields in its ancestors 

620 self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname)) 1ghfbcaijde

621 for name, value in info.metadata[METADATA_KEY]['config'].items(): 1ghfbcaijde

622 config.setdefault(name, value) 1ghfbcaijde

623 return config 1ghfbcaijde

624 

625 def collect_fields_and_class_vars( 1ghfbcaijde

626 self, model_config: ModelConfigData, is_root_model: bool 

627 ) -> tuple[list[PydanticModelField] | None, list[PydanticModelClassVar] | None]: 

628 """Collects the fields for the model, accounting for parent classes.""" 

629 cls = self._cls 1ghfbcaijde

630 

631 # First, collect fields and ClassVars belonging to any class in the MRO, ignoring duplicates. 

632 # 

633 # We iterate through the MRO in reverse because attrs defined in the parent must appear 

634 # earlier in the attributes list than attrs defined in the child. See: 

635 # https://docs.python.org/3/library/dataclasses.html#inheritance 

636 # 

637 # However, we also want fields defined in the subtype to override ones defined 

638 # in the parent. We can implement this via a dict without disrupting the attr order 

639 # because dicts preserve insertion order in Python 3.7+. 

640 found_fields: dict[str, PydanticModelField] = {} 1ghfbcaijde

641 found_class_vars: dict[str, PydanticModelClassVar] = {} 1ghfbcaijde

642 for info in reversed(cls.info.mro[1:-1]): # 0 is the current class, -2 is BaseModel, -1 is object 1ghfbcaijde

643 # if BASEMODEL_METADATA_TAG_KEY in info.metadata and BASEMODEL_METADATA_KEY not in info.metadata: 

644 # # We haven't processed the base class yet. Need another pass. 

645 # return None, None 

646 if METADATA_KEY not in info.metadata: 1ghfbcaijde

647 continue 1ghfbcaijde

648 

649 # Each class depends on the set of attributes in its dataclass ancestors. 

650 self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname)) 1ghfbcaijde

651 

652 for name, data in info.metadata[METADATA_KEY]['fields'].items(): 1ghfbcaijde

653 field = PydanticModelField.deserialize(info, data, self._api) 1ghfbcaijde

654 # (The following comment comes directly from the dataclasses plugin) 

655 # TODO: We shouldn't be performing type operations during the main 

656 # semantic analysis pass, since some TypeInfo attributes might 

657 # still be in flux. This should be performed in a later phase. 

658 field.expand_typevar_from_subtype(cls.info, self._api) 1ghfbcaijde

659 found_fields[name] = field 1ghfbcaijde

660 

661 sym_node = cls.info.names.get(name) 1ghfbcaijde

662 if sym_node and sym_node.node and not isinstance(sym_node.node, Var): 662 ↛ 663line 662 didn't jump to line 663 because the condition on line 662 was never true1ghfbcaijde

663 self._api.fail( 

664 'BaseModel field may only be overridden by another field', 

665 sym_node.node, 

666 ) 

667 # Collect ClassVars 

668 for name, data in info.metadata[METADATA_KEY]['class_vars'].items(): 668 ↛ 669line 668 didn't jump to line 669 because the loop on line 668 never started1ghfbcaijde

669 found_class_vars[name] = PydanticModelClassVar.deserialize(data) 

670 

671 # Second, collect fields and ClassVars belonging to the current class. 

672 current_field_names: set[str] = set() 1ghfbcaijde

673 current_class_vars_names: set[str] = set() 1ghfbcaijde

674 for stmt in self._get_assignment_statements_from_block(cls.defs): 1ghfbcaijde

675 maybe_field = self.collect_field_or_class_var_from_stmt(stmt, model_config, found_class_vars) 1ghfbcaijde

676 if isinstance(maybe_field, PydanticModelField): 1ghfbcaijde

677 lhs = stmt.lvalues[0] 1ghfbcaijde

678 if is_root_model and lhs.name != 'root': 1ghfbcaijde

679 error_extra_fields_on_root_model(self._api, stmt) 1ghfbcaijde

680 else: 

681 current_field_names.add(lhs.name) 1ghfbcaijde

682 found_fields[lhs.name] = maybe_field 1ghfbcaijde

683 elif isinstance(maybe_field, PydanticModelClassVar): 1ghfbcaijde

684 lhs = stmt.lvalues[0] 1ghfbcaijde

685 current_class_vars_names.add(lhs.name) 1ghfbcaijde

686 found_class_vars[lhs.name] = maybe_field 1ghfbcaijde

687 

688 return list(found_fields.values()), list(found_class_vars.values()) 1ghfbcaijde

689 

690 def _get_assignment_statements_from_if_statement(self, stmt: IfStmt) -> Iterator[AssignmentStmt]: 1ghfbcaijde

691 for body in stmt.body: 1ghfbcaijde

692 if not body.is_unreachable: 692 ↛ 691line 692 didn't jump to line 691 because the condition on line 692 was always true1ghfbcaijde

693 yield from self._get_assignment_statements_from_block(body) 1ghfbcaijde

694 if stmt.else_body is not None and not stmt.else_body.is_unreachable: 694 ↛ 695line 694 didn't jump to line 695 because the condition on line 694 was never true1ghfbcaijde

695 yield from self._get_assignment_statements_from_block(stmt.else_body) 

696 

697 def _get_assignment_statements_from_block(self, block: Block) -> Iterator[AssignmentStmt]: 1ghfbcaijde

698 for stmt in block.body: 1ghfbcaijde

699 if isinstance(stmt, AssignmentStmt): 1ghfbcaijde

700 yield stmt 1ghfbcaijde

701 elif isinstance(stmt, IfStmt): 1ghfbcaijde

702 yield from self._get_assignment_statements_from_if_statement(stmt) 1ghfbcaijde

703 

704 def collect_field_or_class_var_from_stmt( # noqa C901 1ghfbcaijde

705 self, stmt: AssignmentStmt, model_config: ModelConfigData, class_vars: dict[str, PydanticModelClassVar] 

706 ) -> PydanticModelField | PydanticModelClassVar | None: 

707 """Get pydantic model field from statement. 

708 

709 Args: 

710 stmt: The statement. 

711 model_config: Configuration settings for the model. 

712 class_vars: ClassVars already known to be defined on the model. 

713 

714 Returns: 

715 A pydantic model field if it could find the field in statement. Otherwise, `None`. 

716 """ 

717 cls = self._cls 1ghfbcaijde

718 

719 lhs = stmt.lvalues[0] 1ghfbcaijde

720 if not isinstance(lhs, NameExpr) or not _fields.is_valid_field_name(lhs.name) or lhs.name == 'model_config': 1ghfbcaijde

721 return None 1ghfbcaijde

722 

723 if not stmt.new_syntax: 1ghfbcaijde

724 if ( 1ghfbcaij

725 isinstance(stmt.rvalue, CallExpr) 

726 and isinstance(stmt.rvalue.callee, CallExpr) 

727 and isinstance(stmt.rvalue.callee.callee, NameExpr) 

728 and stmt.rvalue.callee.callee.fullname in DECORATOR_FULLNAMES 

729 ): 

730 # This is a (possibly-reused) validator or serializer, not a field 

731 # In particular, it looks something like: my_validator = validator('my_field')(f) 

732 # Eventually, we may want to attempt to respect model_config['ignored_types'] 

733 return None 1ghfbcaijde

734 

735 if lhs.name in class_vars: 735 ↛ 737line 735 didn't jump to line 737 because the condition on line 735 was never true1ghfbcaijde

736 # Class vars are not fields and are not required to be annotated 

737 return None 

738 

739 # The assignment does not have an annotation, and it's not anything else we recognize 

740 error_untyped_fields(self._api, stmt) 1ghfbcaijde

741 return None 1ghfbcaijde

742 

743 lhs = stmt.lvalues[0] 1ghfbcaijde

744 if not isinstance(lhs, NameExpr): 744 ↛ 745line 744 didn't jump to line 745 because the condition on line 744 was never true1ghfbcaijde

745 return None 

746 

747 if not _fields.is_valid_field_name(lhs.name) or lhs.name == 'model_config': 747 ↛ 748line 747 didn't jump to line 748 because the condition on line 747 was never true1ghfbcaijde

748 return None 

749 

750 sym = cls.info.names.get(lhs.name) 1ghfbcaijde

751 if sym is None: # pragma: no cover 1ghfbcaijde

752 # This is likely due to a star import (see the dataclasses plugin for a more detailed explanation) 

753 # This is the same logic used in the dataclasses plugin 

754 return None 

755 

756 node = sym.node 1ghfbcaijde

757 if isinstance(node, PlaceholderNode): # pragma: no cover 1ghfbcaijde

758 # See the PlaceholderNode docstring for more detail about how this can occur 

759 # Basically, it is an edge case when dealing with complex import logic 

760 

761 # The dataclasses plugin now asserts this cannot happen, but I'd rather not error if it does.. 

762 return None 

763 

764 if isinstance(node, TypeAlias): 1ghfbcaijde

765 self._api.fail( 

766 'Type aliases inside BaseModel definitions are not supported at runtime', 

767 node, 

768 ) 

769 # Skip processing this node. This doesn't match the runtime behaviour, 

770 # but the only alternative would be to modify the SymbolTable, 

771 # and it's a little hairy to do that in a plugin. 

772 return None 

773 

774 if not isinstance(node, Var): # pragma: no cover 1ghfbcaijde

775 # Don't know if this edge case still happens with the `is_valid_field` check above 

776 # but better safe than sorry 

777 

778 # The dataclasses plugin now asserts this cannot happen, but I'd rather not error if it does.. 

779 return None 

780 

781 # x: ClassVar[int] is not a field 

782 if node.is_classvar: 1ghfbcaijde

783 return PydanticModelClassVar(lhs.name) 1ghfbcaijde

784 

785 # x: InitVar[int] is not supported in BaseModel 

786 node_type = get_proper_type(node.type) 1ghfbcaijde

787 if isinstance(node_type, Instance) and node_type.type.fullname == 'dataclasses.InitVar': 787 ↛ 788line 787 didn't jump to line 788 because the condition on line 787 was never true1ghfbcaijde

788 self._api.fail( 

789 'InitVar is not supported in BaseModel', 

790 node, 

791 ) 

792 

793 has_default = self.get_has_default(stmt) 1ghfbcaijde

794 

795 if sym.type is None and node.is_final and node.is_inferred: 795 ↛ 803line 795 didn't jump to line 803 because the condition on line 795 was never true1ghfbcaijde

796 # This follows the logic from the dataclasses plugin. The following comment is taken verbatim: 

797 # 

798 # This is a special case, assignment like x: Final = 42 is classified 

799 # annotated above, but mypy strips the `Final` turning it into x = 42. 

800 # We do not support inferred types in dataclasses, so we can try inferring 

801 # type for simple literals, and otherwise require an explicit type 

802 # argument for Final[...]. 

803 typ = self._api.analyze_simple_literal_type(stmt.rvalue, is_final=True) 

804 if typ: 

805 node.type = typ 

806 else: 

807 self._api.fail( 

808 'Need type argument for Final[...] with non-literal default in BaseModel', 

809 stmt, 

810 ) 

811 node.type = AnyType(TypeOfAny.from_error) 

812 

813 alias, has_dynamic_alias = self.get_alias_info(stmt) 1ghfbcaijde

814 if has_dynamic_alias and not model_config.populate_by_name and self.plugin_config.warn_required_dynamic_aliases: 1ghfbcaijde

815 error_required_dynamic_aliases(self._api, stmt) 1ghfbcaijde

816 

817 init_type = self._infer_dataclass_attr_init_type(sym, lhs.name, stmt) 1ghfbcaijde

818 return PydanticModelField( 1ghfbcaijde

819 name=lhs.name, 

820 has_dynamic_alias=has_dynamic_alias, 

821 has_default=has_default, 

822 alias=alias, 

823 line=stmt.line, 

824 column=stmt.column, 

825 type=init_type, 

826 info=cls.info, 

827 ) 

828 

829 def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str, context: Context) -> Type | None: 1ghfbcaijde

830 """Infer __init__ argument type for an attribute. 

831 

832 In particular, possibly use the signature of __set__. 

833 """ 

834 default = sym.type 1ghfbcaijde

835 if sym.implicit: 835 ↛ 836line 835 didn't jump to line 836 because the condition on line 835 was never true1ghfbcaijde

836 return default 

837 t = get_proper_type(sym.type) 1ghfbcaijde

838 

839 # Perform a simple-minded inference from the signature of __set__, if present. 

840 # We can't use mypy.checkmember here, since this plugin runs before type checking. 

841 # We only support some basic scanerios here, which is hopefully sufficient for 

842 # the vast majority of use cases. 

843 if not isinstance(t, Instance): 1ghfbcaijde

844 return default 1ghfbcaijde

845 setter = t.type.get('__set__') 1ghfbcaijde

846 if setter: 846 ↛ 847line 846 didn't jump to line 847 because the condition on line 846 was never true1ghfbcaijde

847 if isinstance(setter.node, FuncDef): 

848 super_info = t.type.get_containing_type_info('__set__') 

849 assert super_info 

850 if setter.type: 

851 setter_type = get_proper_type(map_type_from_supertype(setter.type, t.type, super_info)) 

852 else: 

853 return AnyType(TypeOfAny.unannotated) 

854 if isinstance(setter_type, CallableType) and setter_type.arg_kinds == [ 

855 ARG_POS, 

856 ARG_POS, 

857 ARG_POS, 

858 ]: 

859 return expand_type_by_instance(setter_type.arg_types[2], t) 

860 else: 

861 self._api.fail(f'Unsupported signature for "__set__" in "{t.type.name}"', context) 

862 else: 

863 self._api.fail(f'Unsupported "__set__" in "{t.type.name}"', context) 

864 

865 return default 1ghfbcaijde

866 

867 def add_initializer( 1ghfbcaijde

868 self, fields: list[PydanticModelField], config: ModelConfigData, is_settings: bool, is_root_model: bool 

869 ) -> None: 

870 """Adds a fields-aware `__init__` method to the class. 

871 

872 The added `__init__` will be annotated with types vs. all `Any` depending on the plugin settings. 

873 """ 

874 if '__init__' in self._cls.info.names and not self._cls.info.names['__init__'].plugin_generated: 1ghfbcaijde

875 return # Don't generate an __init__ if one already exists 1ghfbcaijde

876 

877 typed = self.plugin_config.init_typed 1ghfbcaijde

878 use_alias = config.populate_by_name is not True 1ghfbcaijde

879 requires_dynamic_aliases = bool(config.has_alias_generator and not config.populate_by_name) 1ghfbcaijde

880 args = self.get_field_arguments( 1ghfbcaijde

881 fields, 

882 typed=typed, 

883 requires_dynamic_aliases=requires_dynamic_aliases, 

884 use_alias=use_alias, 

885 is_settings=is_settings, 

886 force_typevars_invariant=True, 

887 ) 

888 

889 if is_root_model and MYPY_VERSION_TUPLE <= (1, 0, 1): 1ghfbcaijde

890 # convert root argument to positional argument 

891 # This is needed because mypy support for `dataclass_transform` isn't complete on 1.0.1 

892 args[0].kind = ARG_POS if args[0].kind == ARG_NAMED else ARG_OPT 1f

893 

894 if is_settings: 1ghfbcaijde

895 base_settings_node = self._api.lookup_fully_qualified(BASESETTINGS_FULLNAME).node 1ghfbcaijde

896 if '__init__' in base_settings_node.names: 896 ↛ 907line 896 didn't jump to line 907 because the condition on line 896 was always true1ghfbcaijde

897 base_settings_init_node = base_settings_node.names['__init__'].node 1ghfbcaijde

898 if base_settings_init_node is not None and base_settings_init_node.type is not None: 898 ↛ 907line 898 didn't jump to line 907 because the condition on line 898 was always true1ghfbcaijde

899 func_type = base_settings_init_node.type 1ghfbcaijde

900 for arg_idx, arg_name in enumerate(func_type.arg_names): 1ghfbcaijde

901 if arg_name.startswith('__') or not arg_name.startswith('_'): 1ghfbcaijde

902 continue 1ghfbcaijde

903 analyzed_variable_type = self._api.anal_type(func_type.arg_types[arg_idx]) 1ghfbcaijde

904 variable = Var(arg_name, analyzed_variable_type) 1ghfbcaijde

905 args.append(Argument(variable, analyzed_variable_type, None, ARG_OPT)) 1ghfbcaijde

906 

907 if not self.should_init_forbid_extra(fields, config): 1ghfbcaijde

908 var = Var('kwargs') 1ghfbcaijde

909 args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2)) 1ghfbcaijde

910 

911 add_method(self._api, self._cls, '__init__', args=args, return_type=NoneType()) 1ghfbcaijde

912 

913 def add_model_construct_method( 1ghfbcaijde

914 self, fields: list[PydanticModelField], config: ModelConfigData, is_settings: bool 

915 ) -> None: 

916 """Adds a fully typed `model_construct` classmethod to the class. 

917 

918 Similar to the fields-aware __init__ method, but always uses the field names (not aliases), 

919 and does not treat settings fields as optional. 

920 """ 

921 set_str = self._api.named_type(f'{BUILTINS_NAME}.set', [self._api.named_type(f'{BUILTINS_NAME}.str')]) 1ghfbcaijde

922 optional_set_str = UnionType([set_str, NoneType()]) 1ghfbcaijde

923 fields_set_argument = Argument(Var('_fields_set', optional_set_str), optional_set_str, None, ARG_OPT) 1ghfbcaijde

924 with state.strict_optional_set(self._api.options.strict_optional): 1ghfbcaijde

925 args = self.get_field_arguments( 1ghfbcaijde

926 fields, typed=True, requires_dynamic_aliases=False, use_alias=False, is_settings=is_settings 

927 ) 

928 if not self.should_init_forbid_extra(fields, config): 1ghfbcaijde

929 var = Var('kwargs') 1ghfbcaijde

930 args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2)) 1ghfbcaijde

931 

932 args = [fields_set_argument] + args 1ghfbcaijde

933 

934 add_method( 1ghfbcaijde

935 self._api, 

936 self._cls, 

937 'model_construct', 

938 args=args, 

939 return_type=fill_typevars(self._cls.info), 

940 is_classmethod=True, 

941 ) 

942 

943 def set_frozen(self, fields: list[PydanticModelField], api: SemanticAnalyzerPluginInterface, frozen: bool) -> None: 1ghfbcaijde

944 """Marks all fields as properties so that attempts to set them trigger mypy errors. 

945 

946 This is the same approach used by the attrs and dataclasses plugins. 

947 """ 

948 info = self._cls.info 1ghfbcaijde

949 for field in fields: 1ghfbcaijde

950 sym_node = info.names.get(field.name) 1ghfbcaijde

951 if sym_node is not None: 1ghfbcaijde

952 var = sym_node.node 1ghfbcaijde

953 if isinstance(var, Var): 953 ↛ 955line 953 didn't jump to line 955 because the condition on line 953 was always true1ghfbcaijde

954 var.is_property = frozen 1ghfbcaijde

955 elif isinstance(var, PlaceholderNode) and not self._api.final_iteration: 

956 # See https://github.com/pydantic/pydantic/issues/5191 to hit this branch for test coverage 

957 self._api.defer() 

958 else: # pragma: no cover 

959 # I don't know whether it's possible to hit this branch, but I've added it for safety 

960 try: 

961 var_str = str(var) 

962 except TypeError: 

963 # This happens for PlaceholderNode; perhaps it will happen for other types in the future.. 

964 var_str = repr(var) 

965 detail = f'sym_node.node: {var_str} (of type {var.__class__})' 

966 error_unexpected_behavior(detail, self._api, self._cls) 

967 else: 

968 var = field.to_var(info, api, use_alias=False) 1ghfbcaijde

969 var.info = info 1ghfbcaijde

970 var.is_property = frozen 1ghfbcaijde

971 var._fullname = info.fullname + '.' + var.name 1ghfbcaijde

972 info.names[var.name] = SymbolTableNode(MDEF, var) 1ghfbcaijde

973 

974 def get_config_update(self, name: str, arg: Expression, lax_extra: bool = False) -> ModelConfigData | None: 1ghfbcaijde

975 """Determines the config update due to a single kwarg in the ConfigDict definition. 

976 

977 Warns if a tracked config attribute is set to a value the plugin doesn't know how to interpret (e.g., an int) 

978 """ 

979 if name not in self.tracked_config_fields: 1ghfbcaijde

980 return None 1ghfbcaijde

981 if name == 'extra': 1ghfbcaijde

982 if isinstance(arg, StrExpr): 1ghfbcaijde

983 forbid_extra = arg.value == 'forbid' 1ghfbcaijde

984 elif isinstance(arg, MemberExpr): 1ghfbcaijde

985 forbid_extra = arg.name == 'forbid' 1ghfbcaijde

986 else: 

987 if not lax_extra: 1ghfbcaijde

988 # Only emit an error for other types of `arg` (e.g., `NameExpr`, `ConditionalExpr`, etc.) when 

989 # reading from a config class, etc. If a ConfigDict is used, then we don't want to emit an error 

990 # because you'll get type checking from the ConfigDict itself. 

991 # 

992 # It would be nice if we could introspect the types better otherwise, but I don't know what the API 

993 # is to evaluate an expr into its type and then check if that type is compatible with the expected 

994 # type. Note that you can still get proper type checking via: `model_config = ConfigDict(...)`, just 

995 # if you don't use an explicit string, the plugin won't be able to infer whether extra is forbidden. 

996 error_invalid_config_value(name, self._api, arg) 1ghfbcaijde

997 return None 1ghfbcaijde

998 return ModelConfigData(forbid_extra=forbid_extra) 1ghfbcaijde

999 if name == 'alias_generator': 1ghfbcaijde

1000 has_alias_generator = True 1ghfbcaijde

1001 if isinstance(arg, NameExpr) and arg.fullname == 'builtins.None': 1ghfbcaijde

1002 has_alias_generator = False 1ghfbcaijde

1003 return ModelConfigData(has_alias_generator=has_alias_generator) 1ghfbcaijde

1004 if isinstance(arg, NameExpr) and arg.fullname in ('builtins.True', 'builtins.False'): 1ghfbcaijde

1005 return ModelConfigData(**{name: arg.fullname == 'builtins.True'}) 1ghfbcaijde

1006 error_invalid_config_value(name, self._api, arg) 1ghfbcaijde

1007 return None 1ghfbcaijde

1008 

1009 @staticmethod 1ghfbcaijde

1010 def get_has_default(stmt: AssignmentStmt) -> bool: 1ghfbcaijde

1011 """Returns a boolean indicating whether the field defined in `stmt` is a required field.""" 

1012 expr = stmt.rvalue 1ghfbcaijde

1013 if isinstance(expr, TempNode): 1ghfbcaijde

1014 # TempNode means annotation-only, so has no default 

1015 return False 1ghfbcaijde

1016 if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME: 1ghfbcaijde

1017 # The "default value" is a call to `Field`; at this point, the field has a default if and only if: 

1018 # * there is a positional argument that is not `...` 

1019 # * there is a keyword argument named "default" that is not `...` 

1020 # * there is a "default_factory" that is not `None` 

1021 for arg, name in zip(expr.args, expr.arg_names): 1ghfbcaijde

1022 # If name is None, then this arg is the default because it is the only positional argument. 

1023 if name is None or name == 'default': 1ghfbcaijde

1024 return arg.__class__ is not EllipsisExpr 1ghfbcaijde

1025 if name == 'default_factory': 1ghfbcaijde

1026 return not (isinstance(arg, NameExpr) and arg.fullname == 'builtins.None') 1ghfbcaijde

1027 return False 1ghfbcaijde

1028 # Has no default if the "default value" is Ellipsis (i.e., `field_name: Annotation = ...`) 

1029 return not isinstance(expr, EllipsisExpr) 1ghfbcaijde

1030 

1031 @staticmethod 1ghfbcaijde

1032 def get_alias_info(stmt: AssignmentStmt) -> tuple[str | None, bool]: 1ghfbcaijde

1033 """Returns a pair (alias, has_dynamic_alias), extracted from the declaration of the field defined in `stmt`. 

1034 

1035 `has_dynamic_alias` is True if and only if an alias is provided, but not as a string literal. 

1036 If `has_dynamic_alias` is True, `alias` will be None. 

1037 """ 

1038 expr = stmt.rvalue 1ghfbcaijde

1039 if isinstance(expr, TempNode): 1ghfbcaijde

1040 # TempNode means annotation-only 

1041 return None, False 1ghfbcaijde

1042 

1043 if not ( 1ghfbcaij

1044 isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME 

1045 ): 

1046 # Assigned value is not a call to pydantic.fields.Field 

1047 return None, False 1ghfbcaijde

1048 

1049 for i, arg_name in enumerate(expr.arg_names): 1ghfbcaijde

1050 if arg_name != 'alias': 1ghfbcaijde

1051 continue 1ghfbcaijde

1052 arg = expr.args[i] 1ghfbcaijde

1053 if isinstance(arg, StrExpr): 1ghfbcaijde

1054 return arg.value, False 1ghfbcaijde

1055 else: 

1056 return None, True 1ghfbcaijde

1057 return None, False 1ghfbcaijde

1058 

1059 def get_field_arguments( 1ghfbcaijde

1060 self, 

1061 fields: list[PydanticModelField], 

1062 typed: bool, 

1063 use_alias: bool, 

1064 requires_dynamic_aliases: bool, 

1065 is_settings: bool, 

1066 force_typevars_invariant: bool = False, 

1067 ) -> list[Argument]: 

1068 """Helper function used during the construction of the `__init__` and `model_construct` method signatures. 

1069 

1070 Returns a list of mypy Argument instances for use in the generated signatures. 

1071 """ 

1072 info = self._cls.info 1ghfbcaijde

1073 arguments = [ 1ghfbcaijde

1074 field.to_argument( 

1075 info, 

1076 typed=typed, 

1077 force_optional=requires_dynamic_aliases or is_settings, 

1078 use_alias=use_alias, 

1079 api=self._api, 

1080 force_typevars_invariant=force_typevars_invariant, 

1081 ) 

1082 for field in fields 

1083 if not (use_alias and field.has_dynamic_alias) 

1084 ] 

1085 return arguments 1ghfbcaijde

1086 

1087 def should_init_forbid_extra(self, fields: list[PydanticModelField], config: ModelConfigData) -> bool: 1ghfbcaijde

1088 """Indicates whether the generated `__init__` should get a `**kwargs` at the end of its signature. 

1089 

1090 We disallow arbitrary kwargs if the extra config setting is "forbid", or if the plugin config says to, 

1091 *unless* a required dynamic alias is present (since then we can't determine a valid signature). 

1092 """ 

1093 if not config.populate_by_name: 1ghfbcaijde

1094 if self.is_dynamic_alias_present(fields, bool(config.has_alias_generator)): 1ghfbcaijde

1095 return False 1ghfbcaijde

1096 if config.forbid_extra: 1ghfbcaijde

1097 return True 1ghfbcaijde

1098 return self.plugin_config.init_forbid_extra 1ghfbcaijde

1099 

1100 @staticmethod 1ghfbcaijde

1101 def is_dynamic_alias_present(fields: list[PydanticModelField], has_alias_generator: bool) -> bool: 1ghfbcaijde

1102 """Returns whether any fields on the model have a "dynamic alias", i.e., an alias that cannot be 

1103 determined during static analysis. 

1104 """ 

1105 for field in fields: 1ghfbcaijde

1106 if field.has_dynamic_alias: 1ghfbcaijde

1107 return True 1ghfbcaijde

1108 if has_alias_generator: 1ghfbcaijde

1109 for field in fields: 1ghfbcaijde

1110 if field.alias is None: 1ghfbcaijde

1111 return True 1ghfbcaijde

1112 return False 1ghfbcaijde

1113 

1114 

1115class ModelConfigData: 1ghfbcaijde

1116 """Pydantic mypy plugin model config class.""" 

1117 

1118 def __init__( 1ghfbcaijde

1119 self, 

1120 forbid_extra: bool | None = None, 

1121 frozen: bool | None = None, 

1122 from_attributes: bool | None = None, 

1123 populate_by_name: bool | None = None, 

1124 has_alias_generator: bool | None = None, 

1125 ): 

1126 self.forbid_extra = forbid_extra 1ghfbcaijde

1127 self.frozen = frozen 1ghfbcaijde

1128 self.from_attributes = from_attributes 1ghfbcaijde

1129 self.populate_by_name = populate_by_name 1ghfbcaijde

1130 self.has_alias_generator = has_alias_generator 1ghfbcaijde

1131 

1132 def get_values_dict(self) -> dict[str, Any]: 1ghfbcaijde

1133 """Returns a dict of Pydantic model config names to their values. 

1134 

1135 It includes the config if config value is not `None`. 

1136 """ 

1137 return {k: v for k, v in self.__dict__.items() if v is not None} 1ghfbcaijde

1138 

1139 def update(self, config: ModelConfigData | None) -> None: 1ghfbcaijde

1140 """Update Pydantic model config values.""" 

1141 if config is None: 1ghfbcaijde

1142 return 1ghfbcaijde

1143 for k, v in config.get_values_dict().items(): 1ghfbcaijde

1144 setattr(self, k, v) 1ghfbcaijde

1145 

1146 def setdefault(self, key: str, value: Any) -> None: 1ghfbcaijde

1147 """Set default value for Pydantic model config if config value is `None`.""" 

1148 if getattr(self, key) is None: 1ghfbcaijde

1149 setattr(self, key, value) 1ghfbcaijde

1150 

1151 

1152ERROR_ORM = ErrorCode('pydantic-orm', 'Invalid from_attributes call', 'Pydantic') 1ghfbcaijde

1153ERROR_CONFIG = ErrorCode('pydantic-config', 'Invalid config value', 'Pydantic') 1ghfbcaijde

1154ERROR_ALIAS = ErrorCode('pydantic-alias', 'Dynamic alias disallowed', 'Pydantic') 1ghfbcaijde

1155ERROR_UNEXPECTED = ErrorCode('pydantic-unexpected', 'Unexpected behavior', 'Pydantic') 1ghfbcaijde

1156ERROR_UNTYPED = ErrorCode('pydantic-field', 'Untyped field disallowed', 'Pydantic') 1ghfbcaijde

1157ERROR_FIELD_DEFAULTS = ErrorCode('pydantic-field', 'Invalid Field defaults', 'Pydantic') 1ghfbcaijde

1158ERROR_EXTRA_FIELD_ROOT_MODEL = ErrorCode('pydantic-field', 'Extra field on RootModel subclass', 'Pydantic') 1ghfbcaijde

1159 

1160 

1161def error_from_attributes(model_name: str, api: CheckerPluginInterface, context: Context) -> None: 1ghfbcaijde

1162 """Emits an error when the model does not have `from_attributes=True`.""" 

1163 api.fail(f'"{model_name}" does not have from_attributes=True', context, code=ERROR_ORM) 1bcade

1164 

1165 

1166def error_invalid_config_value(name: str, api: SemanticAnalyzerPluginInterface, context: Context) -> None: 1ghfbcaijde

1167 """Emits an error when the config value is invalid.""" 

1168 api.fail(f'Invalid value for "Config.{name}"', context, code=ERROR_CONFIG) 1ghfbcaijde

1169 

1170 

1171def error_required_dynamic_aliases(api: SemanticAnalyzerPluginInterface, context: Context) -> None: 1ghfbcaijde

1172 """Emits required dynamic aliases error. 

1173 

1174 This will be called when `warn_required_dynamic_aliases=True`. 

1175 """ 

1176 api.fail('Required dynamic aliases disallowed', context, code=ERROR_ALIAS) 1ghfbcaijde

1177 

1178 

1179def error_unexpected_behavior( 1ghfbcaijde

1180 detail: str, api: CheckerPluginInterface | SemanticAnalyzerPluginInterface, context: Context 1de

1181) -> None: # pragma: no cover 1de

1182 """Emits unexpected behavior error.""" 

1183 # Can't think of a good way to test this, but I confirmed it renders as desired by adding to a non-error path 

1184 link = 'https://github.com/pydantic/pydantic/issues/new/choose' 

1185 full_message = f'The pydantic mypy plugin ran into unexpected behavior: {detail}\n' 

1186 full_message += f'Please consider reporting this bug at {link} so we can try to fix it!' 

1187 api.fail(full_message, context, code=ERROR_UNEXPECTED) 

1188 

1189 

1190def error_untyped_fields(api: SemanticAnalyzerPluginInterface, context: Context) -> None: 1ghfbcaijde

1191 """Emits an error when there is an untyped field in the model.""" 

1192 api.fail('Untyped fields disallowed', context, code=ERROR_UNTYPED) 1ghfbcaijde

1193 

1194 

1195def error_extra_fields_on_root_model(api: CheckerPluginInterface, context: Context) -> None: 1ghfbcaijde

1196 """Emits an error when there is more than just a root field defined for a subclass of RootModel.""" 

1197 api.fail('Only `root` is allowed as a field of a `RootModel`', context, code=ERROR_EXTRA_FIELD_ROOT_MODEL) 1ghfbcaijde

1198 

1199 

1200def error_default_and_default_factory_specified(api: CheckerPluginInterface, context: Context) -> None: 1ghfbcaijde

1201 """Emits an error when `Field` has both `default` and `default_factory` together.""" 

1202 api.fail('Field default and default_factory cannot be specified together', context, code=ERROR_FIELD_DEFAULTS) 1ghfbcaijde

1203 

1204 

1205def add_method( 1ghfbcaijde

1206 api: SemanticAnalyzerPluginInterface | CheckerPluginInterface, 

1207 cls: ClassDef, 

1208 name: str, 

1209 args: list[Argument], 

1210 return_type: Type, 

1211 self_type: Type | None = None, 

1212 tvar_def: TypeVarDef | None = None, 

1213 is_classmethod: bool = False, 

1214) -> None: 

1215 """Very closely related to `mypy.plugins.common.add_method_to_class`, with a few pydantic-specific changes.""" 

1216 info = cls.info 1ghfbcaijde

1217 

1218 # First remove any previously generated methods with the same name 

1219 # to avoid clashes and problems in the semantic analyzer. 

1220 if name in info.names: 1ghfbcaijde

1221 sym = info.names[name] 1ghfbcaijde

1222 if sym.plugin_generated and isinstance(sym.node, FuncDef): 1ghfbcaijde

1223 cls.defs.body.remove(sym.node) # pragma: no cover 1ghfbcaijde

1224 

1225 if isinstance(api, SemanticAnalyzerPluginInterface): 1225 ↛ 1228line 1225 didn't jump to line 1228 because the condition on line 1225 was always true1ghfbcaijde

1226 function_type = api.named_type('builtins.function') 1ghfbcaijde

1227 else: 

1228 function_type = api.named_generic_type('builtins.function', []) 

1229 

1230 if is_classmethod: 1ghfbcaijde

1231 self_type = self_type or TypeType(fill_typevars(info)) 1ghfbcaijde

1232 first = [Argument(Var('_cls'), self_type, None, ARG_POS, True)] 1ghfbcaijde

1233 else: 

1234 self_type = self_type or fill_typevars(info) 1ghfbcaijde

1235 # `self` is positional *ONLY* here, but this can't be expressed 

1236 # fully in the mypy internal API. ARG_POS is the closest we can get. 

1237 # Using ARG_POS will, however, give mypy errors if a `self` field 

1238 # is present on a model: 

1239 # 

1240 # Name "self" already defined (possibly by an import) [no-redef] 

1241 # 

1242 # As a workaround, we give this argument a name that will 

1243 # never conflict. By its positional nature, this name will not 

1244 # be used or exposed to users. 

1245 first = [Argument(Var('__pydantic_self__'), self_type, None, ARG_POS)] 1ghfbcaijde

1246 args = first + args 1ghfbcaijde

1247 

1248 arg_types, arg_names, arg_kinds = [], [], [] 1ghfbcaijde

1249 for arg in args: 1ghfbcaijde

1250 assert arg.type_annotation, 'All arguments must be fully typed.' 1ghfbcaijde

1251 arg_types.append(arg.type_annotation) 1ghfbcaijde

1252 arg_names.append(arg.variable.name) 1ghfbcaijde

1253 arg_kinds.append(arg.kind) 1ghfbcaijde

1254 

1255 signature = CallableType(arg_types, arg_kinds, arg_names, return_type, function_type) 1ghfbcaijde

1256 if tvar_def: 1256 ↛ 1257line 1256 didn't jump to line 1257 because the condition on line 1256 was never true1ghfbcaijde

1257 signature.variables = [tvar_def] 

1258 

1259 func = FuncDef(name, args, Block([PassStmt()])) 1ghfbcaijde

1260 func.info = info 1ghfbcaijde

1261 func.type = set_callable_name(signature, func) 1ghfbcaijde

1262 func.is_class = is_classmethod 1ghfbcaijde

1263 func._fullname = info.fullname + '.' + name 1ghfbcaijde

1264 func.line = info.line 1ghfbcaijde

1265 

1266 # NOTE: we would like the plugin generated node to dominate, but we still 

1267 # need to keep any existing definitions so they get semantically analyzed. 

1268 if name in info.names: 1ghfbcaijde

1269 # Get a nice unique name instead. 

1270 r_name = get_unique_redefinition_name(name, info.names) 1ghfbcaijde

1271 info.names[r_name] = info.names[name] 1ghfbcaijde

1272 

1273 # Add decorator for is_classmethod 

1274 # The dataclasses plugin claims this is unnecessary for classmethods, but not including it results in a 

1275 # signature incompatible with the superclass, which causes mypy errors to occur for every subclass of BaseModel. 

1276 if is_classmethod: 1ghfbcaijde

1277 func.is_decorated = True 1ghfbcaijde

1278 v = Var(name, func.type) 1ghfbcaijde

1279 v.info = info 1ghfbcaijde

1280 v._fullname = func._fullname 1ghfbcaijde

1281 v.is_classmethod = True 1ghfbcaijde

1282 dec = Decorator(func, [NameExpr('classmethod')], v) 1ghfbcaijde

1283 dec.line = info.line 1ghfbcaijde

1284 sym = SymbolTableNode(MDEF, dec) 1ghfbcaijde

1285 else: 

1286 sym = SymbolTableNode(MDEF, func) 1ghfbcaijde

1287 sym.plugin_generated = True 1ghfbcaijde

1288 info.names[name] = sym 1ghfbcaijde

1289 

1290 info.defn.defs.body.append(func) 1ghfbcaijde

1291 

1292 

1293def parse_toml(config_file: str) -> dict[str, Any] | None: 1ghfbcaijde

1294 """Returns a dict of config keys to values. 

1295 

1296 It reads configs from toml file and returns `None` if the file is not a toml file. 

1297 """ 

1298 if not config_file.endswith('.toml'): 1ghfbcaijde

1299 return None 1ghfbcaijde

1300 

1301 if sys.version_info >= (3, 11): 1ghfbcaijde

1302 import tomllib as toml_ 1jde

1303 else: 

1304 try: 1ghfbcai

1305 import tomli as toml_ 1ghfbcai

1306 except ImportError: # pragma: no cover 

1307 import warnings 

1308 

1309 warnings.warn('No TOML parser installed, cannot read configuration from `pyproject.toml`.') 

1310 return None 

1311 

1312 with open(config_file, 'rb') as rf: 1ghfbcaijde

1313 return toml_.load(rf) 1ghfbcaijde