Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pydantic/_internal/_signature.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

83 statements  

1from __future__ import annotations 

2 

3import dataclasses 

4from collections.abc import Callable 

5from inspect import Parameter, Signature 

6from typing import TYPE_CHECKING, Any 

7 

8from pydantic_core import PydanticUndefined 

9 

10from ._typing_extra import signature_no_eval 

11from ._utils import is_valid_identifier 

12 

13if TYPE_CHECKING: 

14 from ..config import ExtraValues 

15 from ..fields import FieldInfo 

16 

17 

18# Copied over from stdlib dataclasses 

19class _HAS_DEFAULT_FACTORY_CLASS: 

20 def __repr__(self): 

21 return '<factory>' 

22 

23 

24_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() 

25 

26 

27def _field_name_for_signature(field_name: str, field_info: FieldInfo, validate_by_alias: bool) -> str: 

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

29 

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

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

32 

33 Args: 

34 field_name: The name of the field 

35 field_info: The corresponding FieldInfo object. 

36 validate_by_alias: The `validate_by_alias` value of the config. 

37 

38 Returns: 

39 The correct name to use when generating a signature. 

40 """ 

41 if validate_by_alias: 

42 if isinstance(field_info.alias, str) and is_valid_identifier(field_info.alias): 

43 return field_info.alias 

44 if isinstance(field_info.validation_alias, str) and is_valid_identifier(field_info.validation_alias): 

45 return field_info.validation_alias 

46 

47 return field_name 

48 

49 

50def _process_param_defaults(param: Parameter, validate_by_alias: bool) -> Parameter: 

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

52 

53 Args: 

54 param (Parameter): The parameter 

55 validate_by_alias: The `validate_by_alias` value of the config. 

56 

57 Returns: 

58 Parameter: The custom processed parameter 

59 """ 

60 from ..fields import FieldInfo 

61 

62 param_default = param.default 

63 if isinstance(param_default, FieldInfo): 

64 annotation = param.annotation 

65 # Replace the annotation if appropriate 

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

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

68 if annotation == 'Any': 

69 annotation = Any 

70 

71 # Replace the field default 

72 default = param_default.default 

73 if default is PydanticUndefined: 

74 if param_default.default_factory is None: 

75 default = Signature.empty 

76 else: 

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

78 default = dataclasses._HAS_DEFAULT_FACTORY # type: ignore 

79 return param.replace( 

80 annotation=annotation, 

81 name=_field_name_for_signature(param.name, param_default, validate_by_alias), 

82 default=default, 

83 ) 

84 return param 

85 

86 

87def _generate_signature_parameters( # noqa: C901 (ignore complexity, could use a refactor) 

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

89 fields: dict[str, FieldInfo], 

90 extra: ExtraValues | None, 

91 validate_by_alias: bool, 

92) -> dict[str, Parameter]: 

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

94 from itertools import islice 

95 

96 present_params = signature_no_eval(init).parameters.values() 

97 merged_params: dict[str, Parameter] = {} 

98 var_kw = None 

99 use_var_kw = False 

100 

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

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

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

104 if fields.get(param.name): 

105 # exclude params with init=False 

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

107 continue 

108 param = param.replace(name=_field_name_for_signature(param.name, fields[param.name], validate_by_alias)) 

109 if param.annotation == 'Any': 

110 param = param.replace(annotation=Any) 

111 if param.kind is param.VAR_KEYWORD: 

112 var_kw = param 

113 continue 

114 merged_params[param.name] = param 

115 

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

117 for field_name, field in fields.items(): 

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

119 param_name = _field_name_for_signature(field_name, field, validate_by_alias) 

120 

121 if field_name in merged_params or param_name in merged_params: 

122 continue 

123 

124 if not is_valid_identifier(param_name): 

125 # Can happen with `create_model()`, where field names are not validated as identifiers: 

126 use_var_kw = True 

127 continue 

128 

129 if field.is_required(): 

130 default = Parameter.empty 

131 elif field.default_factory is not None: 

132 # Mimics stdlib dataclasses: 

133 default = _HAS_DEFAULT_FACTORY 

134 else: 

135 default = field.default 

136 merged_params[param_name] = Parameter( 

137 param_name, 

138 Parameter.KEYWORD_ONLY, 

139 annotation=field.rebuild_annotation(), 

140 default=default, 

141 ) 

142 

143 if extra == 'allow': 

144 use_var_kw = True 

145 

146 if var_kw and use_var_kw: 

147 # Make sure the parameter for extra kwargs 

148 # does not have the same name as a field 

149 default_model_signature = [ 

150 ('self', Parameter.POSITIONAL_ONLY), 

151 ('data', Parameter.VAR_KEYWORD), 

152 ] 

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

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

155 var_kw_name = 'extra_data' 

156 else: 

157 # else start from var_kw 

158 var_kw_name = var_kw.name 

159 

160 # generate a name that's definitely unique 

161 while var_kw_name in fields: 

162 var_kw_name += '_' 

163 merged_params[var_kw_name] = var_kw.replace(name=var_kw_name) 

164 

165 return merged_params 

166 

167 

168def generate_pydantic_signature( 

169 *, 

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

171 fields: dict[str, FieldInfo], 

172 validate_by_alias: bool, 

173 extra: ExtraValues | None, 

174 is_dataclass: bool = False, 

175) -> Signature: 

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

177 

178 Args: 

179 init: The class init. 

180 fields: The model fields. 

181 validate_by_alias: The `validate_by_alias` value of the config. 

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

183 is_dataclass: Whether the model is a dataclass. 

184 

185 Returns: 

186 The dataclass/BaseModel subclass signature. 

187 """ 

188 merged_params = _generate_signature_parameters(init, fields, extra, validate_by_alias) 

189 

190 if is_dataclass: 

191 merged_params = {k: _process_param_defaults(v, validate_by_alias) for k, v in merged_params.items()} 

192 

193 return Signature(parameters=list(merged_params.values()), return_annotation=None)