Coverage for /usr/local/lib/python3.12/site-packages/prefect/_internal/compatibility/deprecated.py: 28%

114 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-12-05 11:21 +0000

1""" 

2Utilities for deprecated items. 

3 

4When a deprecated item is used, a warning will be displayed. Warnings may not be 

5disabled with Prefect settings. Instead, the standard Python warnings filters can be 

6used. 

7 

8Deprecated items require a start or end date. If a start date is given, the end date 

9will be calculated 6 months later. Start and end dates are always in the format MMM YYYY 

10e.g. Jan 2023. 

11""" 

12 

13from __future__ import annotations 1a

14 

15import datetime 1a

16import functools 1a

17import sys 1a

18import warnings 1a

19from typing import TYPE_CHECKING, Any, Callable, Optional, Union 1a

20 

21from pydantic import BaseModel 1a

22from typing_extensions import ParamSpec, TypeAlias, TypeVar 1a

23 

24from prefect.types._datetime import now 1a

25from prefect.utilities.callables import get_call_parameters 1a

26from prefect.utilities.importtools import ( 1a

27 AliasedModuleDefinition, 

28 AliasedModuleFinder, 

29 to_qualified_name, 

30) 

31 

32P = ParamSpec("P") 1a

33R = TypeVar("R", infer_variance=True) 1a

34M = TypeVar("M", bound=BaseModel) 1a

35T = TypeVar("T") 1a

36 

37# Note: A datetime is strongly preferred over a string, but a string is acceptable for 

38# backwards compatibility until support is dropped from dateparser in Python 3.15. 

39_AcceptableDate: TypeAlias = Optional[Union[datetime.datetime, str]] 1a

40 

41DEPRECATED_WARNING = ( 1a

42 "{name} has been deprecated{when}. It will not be available in new releases after {end_date}." 

43 " {help}" 

44) 

45DEPRECATED_MOVED_WARNING = ( 1a

46 "{name} has moved to {new_location}. It will not be available at the old import " 

47 "path after {end_date}. {help}" 

48) 

49HUMAN_DATEFMT = "%b %Y" # e.g. Feb 2023 1a

50DEPRECATED_MODULE_ALIASES: list[AliasedModuleDefinition] = [] 1a

51 

52 

53class PrefectDeprecationWarning(DeprecationWarning): 1a

54 """ 

55 A deprecation warning. 

56 """ 

57 

58 

59def _coerce_datetime( 1a

60 dt: Optional[_AcceptableDate], 

61) -> Optional[datetime.datetime]: 

62 if dt is None or isinstance(dt, datetime.datetime): 

63 return dt 

64 with warnings.catch_warnings(): 

65 warnings.filterwarnings("ignore", category=DeprecationWarning) 

66 import dateparser 

67 

68 return dateparser.parse(dt) 

69 

70 

71def generate_deprecation_message( 1a

72 name: str, 

73 start_date: Optional[_AcceptableDate] = None, 

74 end_date: Optional[_AcceptableDate] = None, 

75 help: str = "", 

76 when: str = "", 

77) -> str: 

78 start_date = _coerce_datetime(start_date) 

79 end_date = _coerce_datetime(end_date) 

80 

81 if start_date is None and end_date is None: 

82 raise ValueError( 

83 "A start date is required if an end date is not provided. Suggested start" 

84 f" date is {now('UTC').strftime(HUMAN_DATEFMT)}" 

85 ) 

86 

87 if end_date is None: 

88 if TYPE_CHECKING: 

89 assert start_date is not None 

90 

91 if sys.version_info >= (3, 13): 

92 from whenever import PlainDateTime 

93 

94 end_date = ( 

95 PlainDateTime.from_py_datetime(start_date).add(months=6).py_datetime() 

96 ) 

97 else: 

98 import pendulum 

99 

100 end_date = pendulum.instance(start_date).add(months=6) 

101 

102 if when: 

103 when = " when " + when 

104 

105 message = DEPRECATED_WARNING.format( 

106 name=name, when=when, end_date=end_date.strftime(HUMAN_DATEFMT), help=help 

107 ) 

108 return message.rstrip() 

109 

110 

111def deprecated_callable( 1a

112 *, 

113 start_date: Optional[_AcceptableDate] = None, 

114 end_date: Optional[_AcceptableDate] = None, 

115 stacklevel: int = 2, 

116 help: str = "", 

117) -> Callable[[Callable[P, R]], Callable[P, R]]: 

118 def decorator(fn: Callable[P, R]) -> Callable[P, R]: 1a

119 @functools.wraps(fn) 1a

120 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: 1a

121 message = generate_deprecation_message( 

122 name=to_qualified_name(fn), 

123 start_date=start_date, 

124 end_date=end_date, 

125 help=help, 

126 ) 

127 warnings.warn(message, PrefectDeprecationWarning, stacklevel=stacklevel) 

128 return fn(*args, **kwargs) 

129 

130 return wrapper 1a

131 

132 return decorator 1a

133 

134 

135def deprecated_class( 1a

136 *, 

137 start_date: Optional[_AcceptableDate] = None, 

138 end_date: Optional[_AcceptableDate] = None, 

139 stacklevel: int = 2, 

140 help: str = "", 

141) -> Callable[[type[T]], type[T]]: 

142 def decorator(cls: type[T]) -> type[T]: 

143 original_init = cls.__init__ 

144 

145 @functools.wraps(original_init) 

146 def new_init(self: T, *args: Any, **kwargs: Any) -> None: 

147 message = generate_deprecation_message( 

148 name=to_qualified_name(cls), 

149 start_date=start_date, 

150 end_date=end_date, 

151 help=help, 

152 ) 

153 warnings.warn(message, PrefectDeprecationWarning, stacklevel=stacklevel) 

154 original_init(self, *args, **kwargs) 

155 

156 cls.__init__ = new_init 

157 return cls 

158 

159 return decorator 

160 

161 

162def deprecated_parameter( 1a

163 name: str, 

164 *, 

165 start_date: Optional[_AcceptableDate] = None, 

166 end_date: Optional[_AcceptableDate] = None, 

167 stacklevel: int = 2, 

168 help: str = "", 

169 when: Optional[Callable[[Any], bool]] = None, 

170 when_message: str = "", 

171) -> Callable[[Callable[P, R]], Callable[P, R]]: 

172 """ 

173 Mark a parameter in a callable as deprecated. 

174 

175 Example: 

176 

177 ```python 

178 

179 @deprecated_parameter("y", when=lambda y: y is not None) 

180 def foo(x, y = None): 

181 return x + 1 + (y or 0) 

182 ``` 

183 """ 

184 

185 when = when or (lambda _: True) 

186 

187 def decorator(fn: Callable[P, R]) -> Callable[P, R]: 

188 @functools.wraps(fn) 

189 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: 

190 try: 

191 parameters = get_call_parameters(fn, args, kwargs, apply_defaults=False) 

192 except Exception: 

193 # Avoid raising any parsing exceptions here 

194 parameters = kwargs 

195 

196 if name in parameters and when(parameters[name]): 

197 message = generate_deprecation_message( 

198 name=f"The parameter {name!r} for {fn.__name__!r}", 

199 start_date=start_date, 

200 end_date=end_date, 

201 help=help, 

202 when=when_message, 

203 ) 

204 warnings.warn(message, PrefectDeprecationWarning, stacklevel=stacklevel) 

205 

206 return fn(*args, **kwargs) 

207 

208 return wrapper 

209 

210 return decorator 

211 

212 

213JsonValue: TypeAlias = Union[int, float, str, bool, None, list["JsonValue"], "JsonDict"] 1a

214JsonDict: TypeAlias = dict[str, JsonValue] 1a

215 

216 

217def deprecated_field( 1a

218 name: str, 

219 *, 

220 start_date: Optional[_AcceptableDate] = None, 

221 end_date: Optional[_AcceptableDate] = None, 

222 when_message: str = "", 

223 help: str = "", 

224 when: Optional[Callable[[Any], bool]] = None, 

225 stacklevel: int = 2, 

226) -> Callable[[type[M]], type[M]]: 

227 """ 

228 Mark a field in a Pydantic model as deprecated. 

229 

230 Raises warning only if the field is specified during init. 

231 

232 Example: 

233 

234 ```python 

235 

236 @deprecated_field("x", when=lambda x: x is not None) 

237 class Model(BaseModel) 

238 x: Optional[int] = None 

239 y: str 

240 ``` 

241 """ 

242 

243 when = when or (lambda _: True) 

244 

245 # Replaces the model's __init__ method with one that performs an additional warning 

246 # check 

247 def decorator(model_cls: type[M]) -> type[M]: 

248 cls_init = model_cls.__init__ 

249 

250 @functools.wraps(model_cls.__init__) 

251 def __init__(__pydantic_self__: M, **data: Any) -> None: 

252 if name in data.keys() and when(data[name]): 

253 message = generate_deprecation_message( 

254 name=f"The field {name!r} in {model_cls.__name__!r}", 

255 start_date=start_date, 

256 end_date=end_date, 

257 help=help, 

258 when=when_message, 

259 ) 

260 warnings.warn(message, PrefectDeprecationWarning, stacklevel=stacklevel) 

261 

262 cls_init(__pydantic_self__, **data) 

263 

264 field = __pydantic_self__.__class__.model_fields.get(name) 

265 if field is not None: 

266 json_schema_extra = field.json_schema_extra or {} 

267 

268 if not isinstance(json_schema_extra, dict): 

269 # json_schema_extra is a hook function; wrap it to add the deprecated flag. 

270 extra_func = json_schema_extra 

271 

272 @functools.wraps(extra_func) 

273 def wrapped(__json_schema: JsonDict) -> None: 

274 extra_func(__json_schema) 

275 __json_schema["deprecated"] = True 

276 

277 json_schema_extra = wrapped 

278 

279 else: 

280 json_schema_extra["deprecated"] = True 

281 

282 field.json_schema_extra = json_schema_extra 

283 

284 # Patch the model's init method 

285 model_cls.__init__ = __init__ 

286 

287 return model_cls 

288 

289 return decorator 

290 

291 

292def inject_renamed_module_alias_finder(): 1a

293 """ 

294 Insert an aliased module finder into Python's import machinery. 

295 

296 Required for `register_renamed_module` to work. 

297 """ 

298 sys.meta_path.insert(0, AliasedModuleFinder(DEPRECATED_MODULE_ALIASES)) 1a

299 

300 

301def register_renamed_module(old_name: str, new_name: str, start_date: _AcceptableDate): 1a

302 """ 

303 Register a renamed module. 

304 

305 Adds backwwards compatibility imports for the old module name and displays a 

306 deprecation warnings on import of the module. 

307 """ 

308 message = generate_deprecation_message( 

309 name=f"The {old_name!r} module", 

310 start_date=start_date, 

311 help=f"Use {new_name!r} instead.", 

312 ) 

313 

314 # Executed on module load 

315 def callback(_): 

316 return warnings.warn(message, DeprecationWarning, stacklevel=3) 

317 

318 DEPRECATED_MODULE_ALIASES.append( 

319 AliasedModuleDefinition(old_name, new_name, callback) 

320 )