|
15 | 15 | import gevent
|
16 | 16 |
|
17 | 17 | from pyinfra import logger, state
|
| 18 | +from pyinfra.api import FactBase |
18 | 19 | from pyinfra.api.command import PyinfraCommand
|
19 | 20 | from pyinfra.api.exceptions import PyinfraError
|
20 | 21 | from pyinfra.api.host import HostData
|
@@ -153,6 +154,37 @@ def parse_cli_arg(arg):
|
153 | 154 | return arg
|
154 | 155 |
|
155 | 156 |
|
| 157 | +def get_module_available_actions(module: ModuleType) -> dict[str, list[str]]: |
| 158 | + """ |
| 159 | + Lists operations and facts available in a module. |
| 160 | +
|
| 161 | + Args: |
| 162 | + module (ModuleType): The module to inspect. |
| 163 | +
|
| 164 | + Returns: |
| 165 | + dict: A dictionary with keys 'operations' and 'facts', each containing a list of names. |
| 166 | + """ |
| 167 | + available_actions: dict[str, list[str]] = { |
| 168 | + "operations": [], |
| 169 | + "facts": [], |
| 170 | + } |
| 171 | + |
| 172 | + for name, obj in module.__dict__.items(): |
| 173 | + # Check if it's a decorated operation |
| 174 | + if callable(obj) and getattr(obj, "is_idempotent", None) is not None: |
| 175 | + available_actions["operations"].append(name) |
| 176 | + |
| 177 | + # Check if it's a FactBase subclass |
| 178 | + elif ( |
| 179 | + isinstance(obj, type) |
| 180 | + and issubclass(obj, FactBase) |
| 181 | + and obj.__module__ == module.__name__ |
| 182 | + ): |
| 183 | + available_actions["facts"].append(name) |
| 184 | + |
| 185 | + return available_actions |
| 186 | + |
| 187 | + |
156 | 188 | def try_import_module_attribute(path, prefix=None, raise_for_none=True):
|
157 | 189 | if ":" in path:
|
158 | 190 | # Allow a.module.name:function syntax
|
@@ -189,7 +221,36 @@ def try_import_module_attribute(path, prefix=None, raise_for_none=True):
|
189 | 221 | attr = getattr(module, attr_name, None)
|
190 | 222 | if attr is None:
|
191 | 223 | if raise_for_none:
|
192 |
| - raise CliError(f"No such attribute in module {possible_modules[0]}: {attr_name}") |
| 224 | + extra_info = [] |
| 225 | + module_name = getattr(module, "__name__", str(module)) |
| 226 | + |
| 227 | + if prefix == "pyinfra.operations": |
| 228 | + # List classes of type OperationMeta |
| 229 | + available_operations = get_module_available_actions(module)["operations"] |
| 230 | + if available_operations: |
| 231 | + extra_info.append( |
| 232 | + f"Available operations are: {', '.join(available_operations)}" |
| 233 | + ) |
| 234 | + else: |
| 235 | + extra_info.append( |
| 236 | + "No operations found. Maybe you have a file or folder named " |
| 237 | + f"`{str(module_name)}` in the current folder ?" |
| 238 | + ) |
| 239 | + |
| 240 | + elif prefix == "pyinfra.facts": |
| 241 | + # List classes of type FactBase |
| 242 | + available_facts = get_module_available_actions(module)["facts"] |
| 243 | + if available_facts: |
| 244 | + extra_info.append(f"Available facts are: {', '.join(available_facts)}") |
| 245 | + else: |
| 246 | + extra_info.append( |
| 247 | + "No facts found. Maybe you have a file or folder named " |
| 248 | + f"`{str(module_name)}` in the current folder ?" |
| 249 | + ) |
| 250 | + |
| 251 | + message = [f"No such attribute in module {possible_modules[0]}: {attr_name}"] |
| 252 | + |
| 253 | + raise CliError("\n".join(message + extra_info)) |
193 | 254 | return
|
194 | 255 |
|
195 | 256 | return attr
|
|
0 commit comments