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

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 inspect import Parameter, Signature, signature 

5from typing import TYPE_CHECKING, Any, Callable 

6 

7from pydantic_core import PydanticUndefined 

8 

9from ._utils import is_valid_identifier 

10 

11if TYPE_CHECKING: 

12 from ..config import ExtraValues 

13 from ..fields import FieldInfo 

14 

15 

16# Copied over from stdlib dataclasses 

17class _HAS_DEFAULT_FACTORY_CLASS: 

18 def __repr__(self): 

19 return '<factory>' 

20 

21 

22_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() 

23 

24 

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

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): 

39 return field_info.alias 

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

41 return field_info.validation_alias 

42 

43 return field_name 

44 

45 

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

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 

56 

57 param_default = param.default 

58 if isinstance(param_default, FieldInfo): 

59 annotation = param.annotation 

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': 

64 annotation = Any 

65 

66 # Replace the field default 

67 default = param_default.default 

68 if default is PydanticUndefined: 

69 if param_default.default_factory is PydanticUndefined: 

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 

74 return param.replace( 

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

76 ) 

77 return param 

78 

79 

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

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

82 fields: dict[str, FieldInfo], 

83 validate_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 

88 

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

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

91 var_kw = None 

92 use_var_kw = False 

93 

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

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): 

98 # exclude params with init=False 

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

100 continue 

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

102 if param.annotation == 'Any': 

103 param = param.replace(annotation=Any) 

104 if param.kind is param.VAR_KEYWORD: 

105 var_kw = param 

106 continue 

107 merged_params[param.name] = param 

108 

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

110 allow_names = validate_by_name 

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

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

113 param_name = _field_name_for_signature(field_name, field) 

114 

115 if field_name in merged_params or param_name in merged_params: 

116 continue 

117 

118 if not is_valid_identifier(param_name): 

119 if allow_names: 

120 param_name = field_name 

121 else: 

122 use_var_kw = True 

123 continue 

124 

125 if field.is_required(): 

126 default = Parameter.empty 

127 elif field.default_factory is not None: 

128 # Mimics stdlib dataclasses: 

129 default = _HAS_DEFAULT_FACTORY 

130 else: 

131 default = field.default 

132 merged_params[param_name] = Parameter( 

133 param_name, 

134 Parameter.KEYWORD_ONLY, 

135 annotation=field.rebuild_annotation(), 

136 default=default, 

137 ) 

138 

139 if extra == 'allow': 

140 use_var_kw = True 

141 

142 if var_kw and use_var_kw: 

143 # Make sure the parameter for extra kwargs 

144 # does not have the same name as a field 

145 default_model_signature = [ 

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: 

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

151 var_kw_name = 'extra_data' 

152 else: 

153 # else start from var_kw 

154 var_kw_name = var_kw.name 

155 

156 # generate a name that's definitely unique 

157 while var_kw_name in fields: 

158 var_kw_name += '_' 

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

160 

161 return merged_params 

162 

163 

164def generate_pydantic_signature( 

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

166 fields: dict[str, FieldInfo], 

167 validate_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 validate_by_name: The `validate_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, validate_by_name, extra) 

184 

185 if is_dataclass: 

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

187 

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