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

71 statements  

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

1from __future__ import annotations 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

2 

3import dataclasses 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

4from inspect import Parameter, Signature, signature 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

5from typing import TYPE_CHECKING, Any, Callable 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

6 

7from pydantic_core import PydanticUndefined 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

8 

9from ._config import ConfigWrapper 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

10from ._utils import is_valid_identifier 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

11 

12if TYPE_CHECKING: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

13 from ..fields import FieldInfo 

14 

15 

16def _field_name_for_signature(field_name: str, field_info: FieldInfo) -> str: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

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

18 

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

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

21 

22 Args: 

23 field_name: The name of the field 

24 field_info: The corresponding FieldInfo object. 

25 

26 Returns: 

27 The correct name to use when generating a signature. 

28 """ 

29 

30 def _alias_if_valid(x: Any) -> str | None: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

31 """Return the alias if it is a valid alias and identifier, else None.""" 

32 return x if isinstance(x, str) and is_valid_identifier(x) else None 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

33 

34 return _alias_if_valid(field_info.alias) or _alias_if_valid(field_info.validation_alias) or field_name 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

35 

36 

37def _process_param_defaults(param: Parameter) -> Parameter: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

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

39 

40 Args: 

41 param (Parameter): The parameter 

42 

43 Returns: 

44 Parameter: The custom processed parameter 

45 """ 

46 from ..fields import FieldInfo 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

47 

48 param_default = param.default 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

49 if isinstance(param_default, FieldInfo): 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

50 annotation = param.annotation 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

51 # Replace the annotation if appropriate 

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

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

54 if annotation == 'Any': 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

55 annotation = Any 

56 

57 # Replace the field default 

58 default = param_default.default 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

59 if default is PydanticUndefined: 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

60 if param_default.default_factory is PydanticUndefined: 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

61 default = Signature.empty 

62 else: 

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

64 default = dataclasses._HAS_DEFAULT_FACTORY # type: ignore 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

65 return param.replace( 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

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

67 ) 

68 return param 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

69 

70 

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

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

73 fields: dict[str, FieldInfo], 

74 config_wrapper: ConfigWrapper, 

75) -> dict[str, Parameter]: 

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

77 from itertools import islice 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

78 

79 present_params = signature(init).parameters.values() 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

80 merged_params: dict[str, Parameter] = {} 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

81 var_kw = None 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

82 use_var_kw = False 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

83 

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

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

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

87 if fields.get(param.name): 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

88 # exclude params with init=False 

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

90 continue 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

91 param = param.replace(name=_field_name_for_signature(param.name, fields[param.name])) 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

92 if param.annotation == 'Any': 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

93 param = param.replace(annotation=Any) 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

94 if param.kind is param.VAR_KEYWORD: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

95 var_kw = param 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

96 continue 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

97 merged_params[param.name] = param 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

98 

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

100 allow_names = config_wrapper.populate_by_name 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

101 for field_name, field in fields.items(): 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

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

103 param_name = _field_name_for_signature(field_name, field) 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

104 

105 if field_name in merged_params or param_name in merged_params: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

106 continue 1abcdefghCijklmnopMNOPQRSTqrstuvwx

107 

108 if not is_valid_identifier(param_name): 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

109 if allow_names: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

110 param_name = field_name 

111 else: 

112 use_var_kw = True 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

113 continue 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

114 

115 kwargs = {} if field.is_required() else {'default': field.get_default(call_default_factory=False)} 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

116 merged_params[param_name] = Parameter( 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

117 param_name, Parameter.KEYWORD_ONLY, annotation=field.rebuild_annotation(), **kwargs 

118 ) 

119 

120 if config_wrapper.extra == 'allow': 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

121 use_var_kw = True 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

122 

123 if var_kw and use_var_kw: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

124 # Make sure the parameter for extra kwargs 

125 # does not have the same name as a field 

126 default_model_signature = [ 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

127 ('self', Parameter.POSITIONAL_ONLY), 

128 ('data', Parameter.VAR_KEYWORD), 

129 ] 

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

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

132 var_kw_name = 'extra_data' 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

133 else: 

134 # else start from var_kw 

135 var_kw_name = var_kw.name 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

136 

137 # generate a name that's definitely unique 

138 while var_kw_name in fields: 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

139 var_kw_name += '_' 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

140 merged_params[var_kw_name] = var_kw.replace(name=var_kw_name) 1yzABabcdefghLCDEFGijklmnopHIJKqrstuvwx

141 

142 return merged_params 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

143 

144 

145def generate_pydantic_signature( 1yzABabcdefghDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

146 init: Callable[..., None], fields: dict[str, FieldInfo], config_wrapper: ConfigWrapper, is_dataclass: bool = False 

147) -> Signature: 

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

149 

150 Args: 

151 init: The class init. 

152 fields: The model fields. 

153 config_wrapper: The config wrapper instance. 

154 is_dataclass: Whether the model is a dataclass. 

155 

156 Returns: 

157 The dataclass/BaseModel subclass signature. 

158 """ 

159 merged_params = _generate_signature_parameters(init, fields, config_wrapper) 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

160 

161 if is_dataclass: 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx

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

163 

164 return Signature(parameters=list(merged_params.values()), return_annotation=None) 1yzABabcdefghLCDEFGijklmnopUVMNOPQRSTHIJKqrstuvwx