Coverage for pydantic/_internal/_signature.py: 93.65%

80 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-13 19:35 +0000

1from __future__ import annotations 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

2 

3import dataclasses 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

4from inspect import Parameter, Signature, signature 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

5from typing import TYPE_CHECKING, Any, Callable 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

6 

7from pydantic_core import PydanticUndefined 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

8 

9from ._utils import is_valid_identifier 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

10 

11if TYPE_CHECKING: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

12 from ..config import ExtraValues 

13 from ..fields import FieldInfo 

14 

15 

16# Copied over from stdlib dataclasses 

17class _HAS_DEFAULT_FACTORY_CLASS: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

18 def __repr__(self): 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

19 return '<factory>' 1yzabcdefghFABCijklmnopDEqrstuvwx

20 

21 

22_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

23 

24 

25def _field_name_for_signature(field_name: str, field_info: FieldInfo) -> str: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

26 """Extract the correct name to use for the field when generating a signature. 

27 

28 Assuming the field has a valid alias, this will return the alias. Otherwise, it will return the field name. 

29 First priority is given to the alias, then the validation_alias, then the field name. 

30 

31 Args: 

32 field_name: The name of the field 

33 field_info: The corresponding FieldInfo object. 

34 

35 Returns: 

36 The correct name to use when generating a signature. 

37 """ 

38 if isinstance(field_info.alias, str) and is_valid_identifier(field_info.alias): 1yzabcdefghFABCijklmnopDEqrstuvwx

39 return field_info.alias 1yzabcdefghFABCijklmnopDEqrstuvwx

40 if isinstance(field_info.validation_alias, str) and is_valid_identifier(field_info.validation_alias): 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true1yzabcdefghFABCijklmnopDEqrstuvwx

41 return field_info.validation_alias 

42 

43 return field_name 1yzabcdefghFABCijklmnopDEqrstuvwx

44 

45 

46def _process_param_defaults(param: Parameter) -> Parameter: 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

47 """Modify the signature for a parameter in a dataclass where the default value is a FieldInfo instance. 

48 

49 Args: 

50 param (Parameter): The parameter 

51 

52 Returns: 

53 Parameter: The custom processed parameter 

54 """ 

55 from ..fields import FieldInfo 1yzabcdefghFABCijklmnopDEqrstuvwx

56 

57 param_default = param.default 1yzabcdefghFABCijklmnopDEqrstuvwx

58 if isinstance(param_default, FieldInfo): 1yzabcdefghFABCijklmnopDEqrstuvwx

59 annotation = param.annotation 1yzabcdefghFABCijklmnopDEqrstuvwx

60 # Replace the annotation if appropriate 

61 # inspect does "clever" things to show annotations as strings because we have 

62 # `from __future__ import annotations` in main, we don't want that 

63 if annotation == 'Any': 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true1yzabcdefghFABCijklmnopDEqrstuvwx

64 annotation = Any 

65 

66 # Replace the field default 

67 default = param_default.default 1yzabcdefghFABCijklmnopDEqrstuvwx

68 if default is PydanticUndefined: 1yzabcdefghFABCijklmnopDEqrstuvwx

69 if param_default.default_factory is PydanticUndefined: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true1yzabcdefghFABCijklmnopDEqrstuvwx

70 default = Signature.empty 

71 else: 

72 # this is used by dataclasses to indicate a factory exists: 

73 default = dataclasses._HAS_DEFAULT_FACTORY # type: ignore 1yzabcdefghFABCijklmnopDEqrstuvwx

74 return param.replace( 1yzabcdefghFABCijklmnopDEqrstuvwx

75 annotation=annotation, name=_field_name_for_signature(param.name, param_default), default=default 

76 ) 

77 return param 1yzabcdefghFABCijklmnopDEqrstuvwx

78 

79 

80def _generate_signature_parameters( # noqa: C901 (ignore complexity, could use a refactor) 1yzabcdefghFABCijklmnopGHIJKLMDEqrstuvwx

81 init: Callable[..., None], 

82 fields: dict[str, FieldInfo], 

83 populate_by_name: bool, 

84 extra: ExtraValues | None, 

85) -> dict[str, Parameter]: 

86 """Generate a mapping of parameter names to Parameter objects for a pydantic BaseModel or dataclass.""" 

87 from itertools import islice 1yzabcdefghFABCijklmnopDEqrstuvwx

88 

89 present_params = signature(init).parameters.values() 1yzabcdefghFABCijklmnopDEqrstuvwx

90 merged_params: dict[str, Parameter] = {} 1yzabcdefghFABCijklmnopDEqrstuvwx

91 var_kw = None 1yzabcdefghFABCijklmnopDEqrstuvwx

92 use_var_kw = False 1yzabcdefghFABCijklmnopDEqrstuvwx

93 

94 for param in islice(present_params, 1, None): # skip self arg 1yzabcdefghFABCijklmnopDEqrstuvwx

95 # inspect does "clever" things to show annotations as strings because we have 

96 # `from __future__ import annotations` in main, we don't want that 

97 if fields.get(param.name): 1yzabcdefghFABCijklmnopDEqrstuvwx

98 # exclude params with init=False 

99 if getattr(fields[param.name], 'init', True) is False: 1yzabcdefghFABCijklmnopDEqrstuvwx

100 continue 1yzabcdefghFABCijklmnopDEqrstuvwx

101 param = param.replace(name=_field_name_for_signature(param.name, fields[param.name])) 1yzabcdefghFABCijklmnopDEqrstuvwx

102 if param.annotation == 'Any': 1yzabcdefghFABCijklmnopDEqrstuvwx

103 param = param.replace(annotation=Any) 1yzabcdefghFABCijklmnopDEqrstuvwx

104 if param.kind is param.VAR_KEYWORD: 1yzabcdefghFABCijklmnopDEqrstuvwx

105 var_kw = param 1yzabcdefghFABCijklmnopDEqrstuvwx

106 continue 1yzabcdefghFABCijklmnopDEqrstuvwx

107 merged_params[param.name] = param 1yzabcdefghFABCijklmnopDEqrstuvwx

108 

109 if var_kw: # if custom init has no var_kw, fields which are not declared in it cannot be passed through 1yzabcdefghFABCijklmnopDEqrstuvwx

110 allow_names = populate_by_name 1yzabcdefghFABCijklmnopDEqrstuvwx

111 for field_name, field in fields.items(): 1yzabcdefghFABCijklmnopDEqrstuvwx

112 # when alias is a str it should be used for signature generation 

113 param_name = _field_name_for_signature(field_name, field) 1yzabcdefghFABCijklmnopDEqrstuvwx

114 

115 if field_name in merged_params or param_name in merged_params: 1yzabcdefghFABCijklmnopDEqrstuvwx

116 continue 1abcdefghAijklmnopqrstuvwx

117 

118 if not is_valid_identifier(param_name): 1yzabcdefghFABCijklmnopDEqrstuvwx

119 if allow_names: 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true1yzabcdefghFABCijklmnopDEqrstuvwx

120 param_name = field_name 

121 else: 

122 use_var_kw = True 1yzabcdefghFABCijklmnopDEqrstuvwx

123 continue 1yzabcdefghFABCijklmnopDEqrstuvwx

124 

125 if field.is_required(): 1yzabcdefghFABCijklmnopDEqrstuvwx

126 default = Parameter.empty 1yzabcdefghFABCijklmnopDEqrstuvwx

127 elif field.default_factory is not None: 1yzabcdefghFABCijklmnopDEqrstuvwx

128 # Mimics stdlib dataclasses: 

129 default = _HAS_DEFAULT_FACTORY 1yzabcdefghFABCijklmnopDEqrstuvwx

130 else: 

131 default = field.default 1yzabcdefghFABCijklmnopDEqrstuvwx

132 merged_params[param_name] = Parameter( 1yzabcdefghFABCijklmnopDEqrstuvwx

133 param_name, 

134 Parameter.KEYWORD_ONLY, 

135 annotation=field.rebuild_annotation(), 

136 default=default, 

137 ) 

138 

139 if extra == 'allow': 1yzabcdefghFABCijklmnopDEqrstuvwx

140 use_var_kw = True 1yzabcdefghFABCijklmnopDEqrstuvwx

141 

142 if var_kw and use_var_kw: 1yzabcdefghFABCijklmnopDEqrstuvwx

143 # Make sure the parameter for extra kwargs 

144 # does not have the same name as a field 

145 default_model_signature = [ 1yzabcdefghFABCijklmnopDEqrstuvwx

146 ('self', Parameter.POSITIONAL_ONLY), 

147 ('data', Parameter.VAR_KEYWORD), 

148 ] 

149 if [(p.name, p.kind) for p in present_params] == default_model_signature: 1yzabcdefghFABCijklmnopDEqrstuvwx

150 # if this is the standard model signature, use extra_data as the extra args name 

151 var_kw_name = 'extra_data' 1yzabcdefghFABCijklmnopDEqrstuvwx

152 else: 

153 # else start from var_kw 

154 var_kw_name = var_kw.name 1yzabcdefghFABCijklmnopDEqrstuvwx

155 

156 # generate a name that's definitely unique 

157 while var_kw_name in fields: 1yzabcdefghFABCijklmnopDEqrstuvwx

158 var_kw_name += '_' 1yzabcdefghFABCijklmnopDEqrstuvwx

159 merged_params[var_kw_name] = var_kw.replace(name=var_kw_name) 1yzabcdefghFABCijklmnopDEqrstuvwx

160 

161 return merged_params 1yzabcdefghFABCijklmnopDEqrstuvwx

162 

163 

164def generate_pydantic_signature( 1yzabcdefghBCijklmnopGHIJKLMDEqrstuvwx

165 init: Callable[..., None], 

166 fields: dict[str, FieldInfo], 

167 populate_by_name: bool, 

168 extra: ExtraValues | None, 

169 is_dataclass: bool = False, 

170) -> Signature: 

171 """Generate signature for a pydantic BaseModel or dataclass. 

172 

173 Args: 

174 init: The class init. 

175 fields: The model fields. 

176 populate_by_name: The `populate_by_name` value of the config. 

177 extra: The `extra` value of the config. 

178 is_dataclass: Whether the model is a dataclass. 

179 

180 Returns: 

181 The dataclass/BaseModel subclass signature. 

182 """ 

183 merged_params = _generate_signature_parameters(init, fields, populate_by_name, extra) 1yzabcdefghFABCijklmnopDEqrstuvwx

184 

185 if is_dataclass: 1yzabcdefghFABCijklmnopDEqrstuvwx

186 merged_params = {k: _process_param_defaults(v) for k, v in merged_params.items()} 1yzabcdefghFABCijklmnopDEqrstuvwx

187 

188 return Signature(parameters=list(merged_params.values()), return_annotation=None) 1yzabcdefghFABCijklmnopDEqrstuvwx