Skip to content

Commit fce49ad

Browse files
committedNov 25, 2024
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 fce49ad

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed
 

‎pyinfra_cli/util.py

+61-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,34 @@ def parse_cli_arg(arg):
153154
return arg
154155

155156

157+
def get_module_available_items(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_items: dict[str, list[str]] = {
168+
"operations": [],
169+
"facts": [],
170+
}
171+
172+
for name, obj in module.__dict__.items():
173+
if (
174+
isinstance(obj, type)
175+
and any([issubclass(obj, OperationMeta), issubclass(obj, FactBase)])
176+
and obj.__module__ == module.__name__
177+
):
178+
available_items["operations" if issubclass(obj, OperationMeta) else "facts"].append(
179+
name
180+
)
181+
182+
return available_items
183+
184+
156185
def try_import_module_attribute(path, prefix=None, raise_for_none=True):
157186
if ":" in path:
158187
# Allow a.module.name:function syntax
@@ -189,7 +218,38 @@ def try_import_module_attribute(path, prefix=None, raise_for_none=True):
189218
attr = getattr(module, attr_name, None)
190219
if attr is None:
191220
if raise_for_none:
192-
raise CliError(f"No such attribute in module {possible_modules[0]}: {attr_name}")
221+
extra_info = []
222+
module_name = getattr(module, "__name__", str(module))
223+
224+
if prefix == "pyinfra.operations":
225+
# List classes of type OperationMeta
226+
available_operations = get_module_available_items(module)["operations"]
227+
if available_operations:
228+
extra_info.append(
229+
f"Available operations in module are: {', '.join(available_operations)}"
230+
)
231+
else:
232+
extra_info.append(
233+
"No operations found. Maybe you have a file or folder named "
234+
f"`{str(module_name)}` in the current folder ?"
235+
)
236+
237+
elif prefix == "pyinfra.facts":
238+
# List classes of type FactBase
239+
available_facts = get_module_available_items(module)["facts"]
240+
if available_facts:
241+
extra_info.append(
242+
f"Available facts in module are: {', '.join(available_facts)}"
243+
)
244+
else:
245+
extra_info.append(
246+
"No facts found. Maybe you have a file or folder named "
247+
f"`{str(module_name)}` in the current folder ?"
248+
)
249+
250+
message = [f"No such attribute in module {possible_modules[0]}: {attr_name}"]
251+
252+
raise CliError("\n".join(message + extra_info))
193253
return
194254

195255
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)
Failed to load comments.