Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/airflow/utils/deprecation_tools.py: 38%

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

48 statements  

1# Licensed to the Apache Software Foundation (ASF) under one 

2# or more contributor license agreements. See the NOTICE file 

3# distributed with this work for additional information 

4# regarding copyright ownership. The ASF licenses this file 

5# to you under the Apache License, Version 2.0 (the 

6# "License"); you may not use this file except in compliance 

7# with the License. You may obtain a copy of the License at 

8# 

9# http://www.apache.org/licenses/LICENSE-2.0 

10# 

11# Unless required by applicable law or agreed to in writing, 

12# software distributed under the License is distributed on an 

13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 

14# KIND, either express or implied. See the License for the 

15# specific language governing permissions and limitations 

16# under the License. 

17from __future__ import annotations 

18 

19import functools 

20import importlib 

21import sys 

22import warnings 

23from types import ModuleType 

24 

25 

26class DeprecatedImportWarning(FutureWarning): 

27 """ 

28 Warning class for deprecated imports in Airflow. 

29 

30 This warning is raised when users import deprecated classes or functions 

31 from Airflow modules that have been moved to better locations. 

32 """ 

33 

34 ... 

35 

36 

37def getattr_with_deprecation( 

38 imports: dict[str, str], 

39 module: str, 

40 override_deprecated_classes: dict[str, str], 

41 extra_message: str, 

42 name: str, 

43): 

44 """ 

45 Retrieve the imported attribute from the redirected module and raises a deprecation warning. 

46 

47 :param imports: dict of imports and their redirection for the module 

48 :param module: name of the module in the package to get the attribute from 

49 :param override_deprecated_classes: override target attributes with deprecated ones. If target attribute is 

50 found in the dictionary, it will be displayed in the warning message. 

51 :param extra_message: extra message to display in the warning or import error message 

52 :param name: attribute name 

53 :return: 

54 """ 

55 target_class_full_name = imports.get(name) 

56 

57 # Handle wildcard pattern "*" - redirect all attributes to target module 

58 # Skip Python special attributes (dunder attributes) as they shouldn't be redirected 

59 if not target_class_full_name and "*" in imports and not (name.startswith("__") and name.endswith("__")): 

60 target_class_full_name = f"{imports['*']}.{name}" 

61 

62 if not target_class_full_name: 

63 raise AttributeError(f"The module `{module!r}` has no attribute `{name!r}`") 

64 

65 # Determine the warning class name (may be overridden) 

66 warning_class_name = target_class_full_name 

67 if override_deprecated_classes and name in override_deprecated_classes: 

68 warning_class_name = override_deprecated_classes[name] 

69 

70 message = f"The `{module}.{name}` attribute is deprecated. Please use `{warning_class_name!r}`." 

71 if extra_message: 

72 message += f" {extra_message}." 

73 warnings.warn(message, DeprecatedImportWarning, stacklevel=2) 

74 

75 # Import and return the target attribute 

76 new_module, new_class_name = target_class_full_name.rsplit(".", 1) 

77 try: 

78 return getattr(importlib.import_module(new_module), new_class_name) 

79 except ImportError as e: 

80 error_message = ( 

81 f"Could not import `{new_module}.{new_class_name}` while trying to import `{module}.{name}`." 

82 ) 

83 if extra_message: 

84 error_message += f" {extra_message}." 

85 raise ImportError(error_message) from e 

86 

87 

88def add_deprecated_classes( 

89 module_imports: dict[str, dict[str, str]], 

90 package: str, 

91 override_deprecated_classes: dict[str, dict[str, str]] | None = None, 

92 extra_message: str | None = None, 

93): 

94 """ 

95 Add deprecated attribute PEP-563 imports and warnings modules to the package. 

96 

97 Works for classes, functions, variables, and other module attributes. 

98 Supports both creating virtual modules and modifying existing modules. 

99 

100 :param module_imports: imports to use. Format: dict[str, dict[str, str]] 

101 - Keys are module names (creates virtual modules) 

102 - Special key __name__ modifies the current module for direct attribute imports 

103 - Can mix both approaches in a single call 

104 :param package: package name (typically __name__) 

105 :param override_deprecated_classes: override target attributes with deprecated ones. 

106 Format: dict[str, dict[str, str]] matching the structure of module_imports 

107 :param extra_message: extra message to display in the warning or import error message 

108 

109 Examples: 

110 # Create virtual modules (e.g., for removed .py files) 

111 add_deprecated_classes( 

112 {"basenotifier": {"BaseNotifier": "airflow.sdk.bases.notifier.BaseNotifier"}}, 

113 package=__name__, 

114 ) 

115 

116 # Wildcard support - redirect all attributes to new module 

117 add_deprecated_classes( 

118 {"timezone": {"*": "airflow.sdk.timezone"}}, 

119 package=__name__, 

120 ) 

121 

122 # Current module direct imports 

123 add_deprecated_classes( 

124 { 

125 __name__: { 

126 "get_fs": "airflow.sdk.io.fs.get_fs", 

127 "has_fs": "airflow.sdk.io.fs.has_fs", 

128 } 

129 }, 

130 package=__name__, 

131 ) 

132 

133 # Mixed behavior - both current module and submodule attributes 

134 add_deprecated_classes( 

135 { 

136 __name__: { 

137 "get_fs": "airflow.sdk.io.fs.get_fs", 

138 "has_fs": "airflow.sdk.io.fs.has_fs", 

139 "Properties": "airflow.sdk.io.typedef.Properties", 

140 }, 

141 "typedef": { 

142 "Properties": "airflow.sdk.io.typedef.Properties", 

143 } 

144 }, 

145 package=__name__, 

146 ) 

147 

148 The first example makes 'from airflow.notifications.basenotifier import BaseNotifier' work 

149 even if 'basenotifier.py' was removed. 

150 

151 The second example makes 'from airflow.utils.timezone import utc' redirect to 'airflow.sdk.timezone.utc', 

152 allowing any attribute from the deprecated module to be accessed from the new location. 

153 

154 The third example makes 'from airflow.io import get_fs' work with direct imports from the current module. 

155 

156 The fourth example handles both direct imports from the current module and submodule imports. 

157 """ 

158 # Handle both current module and virtual module deprecations 

159 for module_name, imports in module_imports.items(): 

160 if module_name == package: 

161 # Special case: modify the current module for direct attribute imports 

162 if package not in sys.modules: 

163 raise ValueError(f"Module {package} not found in sys.modules") 

164 

165 module = sys.modules[package] 

166 

167 # Create the __getattr__ function for current module 

168 current_override = {} 

169 if override_deprecated_classes and package in override_deprecated_classes: 

170 current_override = override_deprecated_classes[package] 

171 

172 getattr_func = functools.partial( 

173 getattr_with_deprecation, 

174 imports, 

175 package, 

176 current_override, 

177 extra_message or "", 

178 ) 

179 

180 # Set the __getattr__ function on the current module 

181 setattr(module, "__getattr__", getattr_func) 

182 else: 

183 # Create virtual modules for submodule imports 

184 full_module_name = f"{package}.{module_name}" 

185 module_type = ModuleType(full_module_name) 

186 if override_deprecated_classes and module_name in override_deprecated_classes: 

187 override_deprecated_classes_for_module = override_deprecated_classes[module_name] 

188 else: 

189 override_deprecated_classes_for_module = {} 

190 

191 # Mypy is not able to derive the right function signature https://github.com/python/mypy/issues/2427 

192 module_type.__getattr__ = functools.partial( # type: ignore[method-assign] 

193 getattr_with_deprecation, 

194 imports, 

195 full_module_name, 

196 override_deprecated_classes_for_module, 

197 extra_message or "", 

198 ) 

199 sys.modules.setdefault(full_module_name, module_type)