Skip to content

Commit b89c2f4

Browse files
committed
Add informations on cli output about module
If the operation or fact is not found, it will display the ones available, and if no one is avaible, Warn the user about the possibility that he have a py file or a folder with the same name as the module
1 parent ef8acef commit b89c2f4

File tree

3 files changed

+74
-3
lines changed

3 files changed

+74
-3
lines changed

pyinfra_cli/util.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import gevent
1616

1717
from pyinfra import logger, state
18+
from pyinfra.api import FactBase
1819
from pyinfra.api.command import PyinfraCommand
1920
from pyinfra.api.exceptions import PyinfraError
2021
from pyinfra.api.host import HostData
@@ -153,6 +154,37 @@ def parse_cli_arg(arg):
153154
return arg
154155

155156

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+
156188
def try_import_module_attribute(path, prefix=None, raise_for_none=True):
157189
if ":" in path:
158190
# Allow a.module.name:function syntax
@@ -189,7 +221,36 @@ def try_import_module_attribute(path, prefix=None, raise_for_none=True):
189221
attr = getattr(module, attr_name, None)
190222
if attr is None:
191223
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))
193254
return
194255

195256
return attr

tests/test_cli/test_cli_exceptions.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def test_no_fact_module(self):
4444
def test_no_fact_cls(self):
4545
self.assert_cli_exception(
4646
["my-server.net", "fact", "server.NotAFact"],
47-
"No such attribute in module server: NotAFact",
47+
(
48+
"No such attribute in module server: NotAFact\n"
49+
"Available facts in module are: User, Home, Path, TmpDir, Hostname, Kernel, "
50+
"KernelVersion, Os, OsVersion, Arch, Command, Which, Date, MacosVersion, Mounts, "
51+
"KernelModules, LsbRelease, OsRelease, Sysctl, Groups, Users, LinuxDistribution, "
52+
"Selinux, LinuxGui, Locales, SecurityLimits"
53+
),
4854
)
4955

5056

tests/test_cli/test_cli_util.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ def test_setup_no_op(self):
3636
with self.assertRaises(CliError) as context:
3737
get_func_and_args(("server.no",))
3838

39-
assert context.exception.message == "No such attribute in module server: no"
39+
assert context.exception.message == (
40+
"No such attribute in module server: no\n"
41+
"No operations found. Maybe you have a file or folder named "
42+
"`pyinfra.operations.server` in the current folder ?"
43+
)
4044

4145
def test_setup_op_and_args(self):
4246
commands = ("pyinfra.operations.server.user", "one", "two", "hello=world")

0 commit comments

Comments
 (0)