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
« 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
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
14from .. import FastAgency 1afgheibcd
15from ..exceptions import FastAgencyCLIError 1afgheibcd
17logger = getLogger(__name__) 1afgheibcd
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 )
30 for full_path in potential_paths: 1abcd
31 path = Path(full_path) 1abcd
32 if path.is_file(): 1abcd
33 return path 1abcd
35 raise FastAgencyCLIError( 1abcd
36 "Could not find a default file to run, please provide an explicit path"
37 )
40@dataclass 1afgheibcd
41class ModuleData: 1afgheibcd
42 module_import_str: str 1afgheibcd
43 extra_sys_path: Path 1afgheibcd
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 )
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")
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
169def import_from_string(import_string: str) -> FastAgency: 1afgheibcd
170 """Import a module and attribute from an import string.
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.
175 Args:
176 import_string (str): The import string in 'module_name:attribute_name' format.
178 Returns:
179 Any: The attribute from the module.
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.
187 """
188 try: 1abcd
189 # Split the string into module and attribute parts
190 module_name, attribute_name = import_string.split(":") 1abcd
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
197 # Add the current directory to the Python path to allow imports from local files
198 sys.path.append(str(Path.cwd())) 1abcd
200 # Import the module using importlib
201 module = importlib.import_module(module_name) # nosemgrep 1abcd
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 )
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