Coverage for fastagency/cli/discover.py: 89%

95 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-19 12:16 +0000

1import importlib 1afgheibcd

2import sys 1afgheibcd

3from dataclasses import dataclass 1afgheibcd

4from logging import getLogger 1afgheibcd

5from pathlib import Path 1afgheibcd

6from typing import Tuple, Union 1afgheibcd

7 

8from rich import print 1afgheibcd

9from rich.padding import Padding 1afgheibcd

10from rich.panel import Panel 1afgheibcd

11from rich.syntax import Syntax 1afgheibcd

12from rich.tree import Tree 1afgheibcd

13 

14from .. import FastAgency 1afgheibcd

15from ..exceptions import FastAgencyCLIError 1afgheibcd

16 

17logger = getLogger(__name__) 1afgheibcd

18 

19 

20def get_default_path() -> Path: 1afgheibcd

21 potential_paths = ( 1abcd

22 "main.py", 

23 "app.py", 

24 "api.py", 

25 "app/main.py", 

26 "app/app.py", 

27 "app/api.py", 

28 ) 

29 

30 for full_path in potential_paths: 1abcd

31 path = Path(full_path) 1abcd

32 if path.is_file(): 1abcd

33 return path 1abcd

34 

35 raise FastAgencyCLIError( 1abcd

36 "Could not find a default file to run, please provide an explicit path" 

37 ) 

38 

39 

40@dataclass 1afgheibcd

41class ModuleData: 1afgheibcd

42 module_import_str: str 1afgheibcd

43 extra_sys_path: Path 1afgheibcd

44 

45 

46def get_module_data_from_path(path: Path) -> ModuleData: 1afgheibcd

47 logger.info( 1aebcd

48 "Searching for package file structure from directories with [blue]__init__.py[/blue] files" 

49 ) 

50 use_path = path.resolve() 1aebcd

51 module_path = use_path 1aebcd

52 if use_path.is_file() and use_path.stem == "__init__": 1aebcd

53 module_path = use_path.parent 1abcd

54 module_paths = [module_path] 1aebcd

55 extra_sys_path = module_path.parent 1aebcd

56 for parent in module_path.parents: 1aebcd

57 init_path = parent / "__init__.py" 1aebcd

58 if init_path.is_file(): 1aebcd

59 module_paths.insert(0, parent) 1aebcd

60 extra_sys_path = parent.parent 1aebcd

61 else: 

62 break 1aebcd

63 logger.info(f"Importing from {extra_sys_path.resolve()}") 1aebcd

64 root = module_paths[0] 1aebcd

65 name = f"🐍 {root.name}" if root.is_file() else f"📁 {root.name}" 1aebcd

66 root_tree = Tree(name) 1aebcd

67 if root.is_dir(): 1aebcd

68 root_tree.add("[dim]🐍 __init__.py[/dim]") 1aebcd

69 tree = root_tree 1aebcd

70 for sub_path in module_paths[1:]: 1aebcd

71 sub_name = ( 1aebcd

72 f"🐍 {sub_path.name}" if sub_path.is_file() else f"📁 {sub_path.name}" 

73 ) 

74 tree = tree.add(sub_name) 1aebcd

75 if sub_path.is_dir(): 1aebcd

76 tree.add("[dim]🐍 __init__.py[/dim]") 1e

77 title = "[b green]Python module file[/b green]" 1aebcd

78 if len(module_paths) > 1 or module_path.is_dir(): 1aebcd

79 title = "[b green]Python package file structure[/b green]" 1aebcd

80 panel = Padding( 1aebcd

81 Panel( 

82 root_tree, 

83 title=title, 

84 expand=False, 

85 padding=(1, 2), 

86 ), 

87 1, 

88 ) 

89 print(panel) 1aebcd

90 module_str = ".".join(p.stem for p in module_paths) 1aebcd

91 logger.info(f"Importing module [green]{module_str}[/green]") 1aebcd

92 return ModuleData( 1aebcd

93 module_import_str=module_str, 

94 extra_sys_path=extra_sys_path.resolve(), 

95 ) 

96 

97 

98def get_app_name( # noqa: C901 1afgheibcd

99 *, mod_data: ModuleData, app_name: Union[str, None] = None 

100) -> "Tuple[str, FastAgency]": 

101 try: 1aebcd

102 mod = importlib.import_module(mod_data.module_import_str) # nosemgrep 1aebcd

103 except (ImportError, ValueError) as e: 1abcd

104 logger.error(f"Import error: {e}") 1abcd

105 logger.warning( 1abcd

106 "Ensure all the package directories have an [blue]__init__.py[/blue] file" 

107 ) 

108 raise 1abcd

109 if not FastAgency: # type: ignore[truthy-function] 1aebcd

110 raise FastAgencyCLIError( 

111 "Could not import FastAgency, try running 'pip install fastagency'" 

112 ) from None 

113 object_names = dir(mod) 1aebcd

114 object_names_set = set(object_names) 1aebcd

115 if app_name: 1aebcd

116 if app_name not in object_names_set: 1abcd

117 raise FastAgencyCLIError( 1abcd

118 f"Could not find app name {app_name} in {mod_data.module_import_str}" 

119 ) 

120 app = getattr(mod, app_name) 1abcd

121 if not isinstance(app, FastAgency): 1abcd

122 raise FastAgencyCLIError( 1abcd

123 f"The app name {app_name} in {mod_data.module_import_str} doesn't seem to be a FastAgency app" 

124 ) 

125 return app_name, app 1abcd

126 for preferred_name in ["app", "api"]: 126 ↛ 131line 126 didn't jump to line 131 because the loop on line 126 didn't complete1aebcd

127 if preferred_name in object_names_set: 127 ↛ 126line 127 didn't jump to line 126 because the condition on line 127 was always true1aebcd

128 obj = getattr(mod, preferred_name) 1aebcd

129 if isinstance(obj, FastAgency): 129 ↛ 126line 129 didn't jump to line 126 because the condition on line 129 was always true1aebcd

130 return preferred_name, obj 1aebcd

131 for name in object_names: 

132 obj = getattr(mod, name) 

133 if isinstance(obj, FastAgency): 

134 return name, obj 

135 raise FastAgencyCLIError("Could not find FastAgency app in module, try using --app") 

136 

137 

138def get_import_string( 1afgheibcd

139 *, path: Union[Path, None] = None, app_name: Union[str, None] = None 

140) -> tuple[str, FastAgency]: 

141 if not path: 1aebcd

142 path = get_default_path() 

143 logger.info(f"Using path [blue]{path}[/blue]") 1aebcd

144 logger.info(f"Resolved absolute path {path.resolve()}") 1aebcd

145 if not path.exists(): 1aebcd

146 raise FastAgencyCLIError(f"Path does not exist {path}") 1abcd

147 mod_data = get_module_data_from_path(path) 1aebcd

148 sys.path.insert(0, str(mod_data.extra_sys_path)) 1aebcd

149 use_app_name, app = get_app_name(mod_data=mod_data, app_name=app_name) 1aebcd

150 import_example = Syntax( 1aebcd

151 f"from {mod_data.module_import_str} import {use_app_name}", "python" 

152 ) 

153 import_panel = Padding( 1aebcd

154 Panel( 

155 import_example, 

156 title="[b green]Importable FastAgency app[/b green]", 

157 expand=False, 

158 padding=(1, 2), 

159 ), 

160 1, 

161 ) 

162 logger.info("Found importable FastAgency app") 1aebcd

163 print(import_panel) 1aebcd

164 import_string = f"{mod_data.module_import_str}:{use_app_name}" 1aebcd

165 logger.info(f"Using import string [b green]{import_string}[/b green]") 1aebcd

166 return import_string, app 1aebcd

167 

168 

169def import_from_string(import_string: str) -> FastAgency: 1afgheibcd

170 """Import a module and attribute from an import string. 

171 

172 Import a module and an attribute from a string like 'file_name:app_name'. 

173 Checks if the file exists before attempting to import the module. 

174 

175 Args: 

176 import_string (str): The import string in 'module_name:attribute_name' format. 

177 

178 Returns: 

179 Any: The attribute from the module. 

180 

181 Raises: 

182 ImportError: If the import string is not in the correct format or the module or attribute is not found. 

183 ValueError: If the import string is not in 'module_name:attribute_name' format. 

184 ModuleNotFoundError: If the module is not found. 

185 AttributeError: If the attribute is not found in the module. 

186 

187 """ 

188 try: 1abcd

189 # Split the string into module and attribute parts 

190 module_name, attribute_name = import_string.split(":") 1abcd

191 

192 # Ensure the module name points to a valid Python file before importing 

193 module_path = f"{module_name.replace('.', '/')}.py" 1abcd

194 if not Path(module_path).is_file(): 1abcd

195 raise ImportError(f"The file for module '{module_name}' does not exist.") 1abcd

196 

197 # Add the current directory to the Python path to allow imports from local files 

198 sys.path.append(str(Path.cwd())) 1abcd

199 

200 # Import the module using importlib 

201 module = importlib.import_module(module_name) # nosemgrep 1abcd

202 

203 # Get the attribute (like 'app') from the module 

204 attribute = getattr(module, attribute_name) 1abcd

205 if not isinstance(attribute, FastAgency): 205 ↛ 206line 205 didn't jump to line 206 because the condition on line 205 was never true1abcd

206 raise ImportError( 

207 f"The attribute '{attribute_name}' in module '{module_name}' is not a FastAgency app." 

208 ) 

209 

210 return attribute 1abcd

211 except ValueError: 1abcd

212 raise ImportError( 1abcd

213 "Import string must be in 'module_name:attribute_name' format." 

214 ) from None 

215 except ModuleNotFoundError: 1abcd

216 raise ImportError(f"Module '{module_name}' not found.") from None 

217 except AttributeError: 1abcd

218 raise ImportError( 1abcd

219 f"Attribute '{attribute_name}' not found in module '{module_name}'." 

220 ) from None