Coverage for /pythoncovmergedfiles/medio/medio/src/pydantic/pydantic/_internal/_config.py: 84%

95 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-27 07:38 +0000

1from __future__ import annotations as _annotations 

2 

3import warnings 

4from typing import TYPE_CHECKING, Any, Callable, cast 

5 

6from pydantic_core import core_schema 

7from typing_extensions import Literal, Self 

8 

9from ..config import ConfigDict, ExtraValues 

10from ..errors import PydanticUserError 

11 

12DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.' 

13 

14 

15class ConfigWrapper: 

16 """ 

17 Internal wrapper for Config which exposes ConfigDict items as attributes. 

18 """ 

19 

20 __slots__ = ('config_dict',) 

21 

22 config_dict: ConfigDict 

23 

24 # all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they 

25 # stop matching 

26 title: str | None 

27 str_to_lower: bool 

28 str_to_upper: bool 

29 str_strip_whitespace: bool 

30 str_min_length: int 

31 str_max_length: int | None 

32 extra: ExtraValues | None 

33 frozen: bool 

34 populate_by_name: bool 

35 use_enum_values: bool 

36 validate_assignment: bool 

37 arbitrary_types_allowed: bool 

38 undefined_types_warning: bool 

39 from_attributes: bool 

40 # whether to use the used alias (or first alias for "field required" errors) instead of field_names 

41 # to construct error `loc`s, default True 

42 loc_by_alias: bool 

43 alias_generator: Callable[[str], str] | None 

44 ignored_types: tuple[type, ...] 

45 allow_inf_nan: bool 

46 

47 # new in V2 

48 strict: bool 

49 # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never' 

50 revalidate_instances: Literal['always', 'never', 'subclass-instances'] 

51 ser_json_timedelta: Literal['iso8601', 'float'] 

52 ser_json_bytes: Literal['utf8', 'base64'] 

53 # whether to validate default values during validation, default False 

54 validate_default: bool 

55 validate_return: bool 

56 

57 def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True): 

58 if check: 

59 self.config_dict = prepare_config(config) 

60 else: 

61 self.config_dict = cast(ConfigDict, config) 

62 

63 @classmethod 

64 def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwargs: dict[str, Any]) -> Self: 

65 """ 

66 Build a new `ConfigDict` instance for a `BaseModel` based on (from lowest to highest) 

67 - options defined in base 

68 - options defined in namespace 

69 - options defined via kwargs 

70 

71 :param bases: tuple of base classes 

72 :param namespace: namespace of the class being created 

73 :param kwargs: kwargs passed to the class being created 

74 """ 

75 

76 config_new = ConfigDict() 

77 for base in bases: 

78 config = getattr(base, 'model_config', None) 

79 if config: 

80 config_new.update(config.copy()) 

81 

82 config_class_from_namespace = namespace.get('Config') 

83 config_dict_from_namespace = namespace.get('model_config') 

84 

85 if config_class_from_namespace and config_dict_from_namespace: 

86 raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both') 

87 

88 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 

89 

90 if config_from_namespace is not None: 

91 config_new.update(config_from_namespace) 

92 

93 for k in list(kwargs.keys()): 

94 if k in config_keys: 

95 config_new[k] = kwargs.pop(k) 

96 

97 return cls(config_new) 

98 

99 # we don't show `__getattr__` to type checkers so missing attributes cause errors 

100 if not TYPE_CHECKING: 

101 

102 def __getattr__(self, name: str) -> Any: 

103 try: 

104 return self.config_dict[name] 

105 except KeyError: 

106 try: 

107 return config_defaults[name] 

108 except KeyError: 

109 raise AttributeError(f'Config has no attribute {name!r}') from None 

110 

111 def core_config(self, obj: Any = None) -> core_schema.CoreConfig: 

112 """ 

113 Create a pydantic-core config, `obj` is just used to populate `title` if not set in config. 

114 

115 We don't use getattr here since we don't want to populate with defaults. 

116 """ 

117 extra = self.config_dict.get('extra') 

118 core_config = core_schema.CoreConfig( 

119 **core_schema.dict_not_none( 

120 title=self.config_dict.get('title') or (obj and obj.__name__), 

121 extra_fields_behavior=extra, 

122 allow_inf_nan=self.config_dict.get('allow_inf_nan'), 

123 populate_by_name=self.config_dict.get('populate_by_name'), 

124 str_strip_whitespace=self.config_dict.get('str_strip_whitespace'), 

125 str_to_lower=self.config_dict.get('str_to_lower'), 

126 str_to_upper=self.config_dict.get('str_to_upper'), 

127 strict=self.config_dict.get('strict'), 

128 ser_json_timedelta=self.config_dict.get('ser_json_timedelta'), 

129 ser_json_bytes=self.config_dict.get('ser_json_bytes'), 

130 from_attributes=self.config_dict.get('from_attributes'), 

131 loc_by_alias=self.config_dict.get('loc_by_alias'), 

132 revalidate_instances=self.config_dict.get('revalidate_instances'), 

133 validate_default=self.config_dict.get('validate_default'), 

134 str_max_length=self.config_dict.get('str_max_length'), 

135 str_min_length=self.config_dict.get('str_min_length'), 

136 ) 

137 ) 

138 return core_config 

139 

140 def __repr__(self): 

141 c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items()) 

142 return f'ConfigWrapper({c})' 

143 

144 

145config_defaults = ConfigDict( 

146 title=None, 

147 str_to_lower=False, 

148 str_to_upper=False, 

149 str_strip_whitespace=False, 

150 str_min_length=0, 

151 str_max_length=None, 

152 # let the model / dataclass decide how to handle it 

153 extra=None, 

154 frozen=False, 

155 populate_by_name=False, 

156 use_enum_values=False, 

157 validate_assignment=False, 

158 arbitrary_types_allowed=False, 

159 undefined_types_warning=True, 

160 from_attributes=False, 

161 loc_by_alias=True, 

162 alias_generator=None, 

163 ignored_types=(), 

164 allow_inf_nan=True, 

165 strict=False, 

166 revalidate_instances='never', 

167 ser_json_timedelta='iso8601', 

168 ser_json_bytes='utf8', 

169 validate_default=False, 

170 validate_return=False, 

171) 

172 

173 

174def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict: 

175 """ 

176 Create a `ConfigDict` instance from an existing dict, a class (e.g. old class-based config) or None. 

177 """ 

178 if config is None: 

179 return ConfigDict() 

180 

181 if not isinstance(config, dict): 

182 warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning) 

183 config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')} 

184 

185 config_dict = cast(ConfigDict, config) 

186 check_deprecated(config_dict) 

187 return config_dict 

188 

189 

190config_keys = set(ConfigDict.__annotations__.keys()) 

191 

192 

193V2_REMOVED_KEYS = { 

194 'allow_mutation', 

195 'error_msg_templates', 

196 'fields', 

197 'getter_dict', 

198 'schema_extra', 

199 'smart_union', 

200 'underscore_attrs_are_private', 

201 'json_loads', 

202 'json_dumps', 

203 'json_encoders', 

204 'copy_on_model_validation', 

205 'post_init_call', 

206} 

207V2_RENAMED_KEYS = { 

208 'allow_population_by_field_name': 'populate_by_name', 

209 'anystr_lower': 'str_to_lower', 

210 'anystr_strip_whitespace': 'str_strip_whitespace', 

211 'anystr_upper': 'str_to_upper', 

212 'keep_untouched': 'ignored_types', 

213 'max_anystr_length': 'str_max_length', 

214 'min_anystr_length': 'str_min_length', 

215 'orm_mode': 'from_attributes', 

216 'validate_all': 'validate_default', 

217} 

218 

219 

220def check_deprecated(config_dict: ConfigDict) -> None: 

221 """ 

222 Check for depreciated config keys and warn the user. 

223 """ 

224 deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys() 

225 deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys() 

226 if deprecated_removed_keys or deprecated_renamed_keys: 

227 renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)} 

228 renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()] 

229 removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)] 

230 message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets) 

231 warnings.warn(message, UserWarning)