Skip to content

Commit bc9c3bf

Browse files
authored
Use fully qualified collection name (FQCN) for generated snippets with Ansible 2.10 or greater
1 parent de93341 commit bc9c3bf

File tree

1 file changed

+95
-35
lines changed

1 file changed

+95
-35
lines changed

UltiSnips/generate.py

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import argparse
33
import os
44
import os.path
5+
import ansible
6+
from packaging import version
57
import ansible.modules
68
from ansible.utils.plugin_docs import get_docstring
79
from ansible.plugins.loader import fragment_loader
@@ -14,14 +16,11 @@
1416
"priority -50",
1517
]
1618
MAX_DESCRIPTION_LENGTH = 512
19+
ANSIBLE_VERSION = ansible.release.__version__
1720

1821

19-
def get_files(include_user: bool = False) -> List[str]:
20-
"""Return the sorted list of all module files that ansible provides
21-
Parameters
22-
----------
23-
include_user: bool
24-
Include modules from the user's ansible-galaxy
22+
def get_files_builtin() -> List[str]:
23+
"""Return the sorted list of all module files that ansible provides with the ansible package
2524
2625
Returns
2726
-------
@@ -32,47 +31,63 @@ def get_files(include_user: bool = False) -> List[str]:
3231

3332
file_names: List[str] = []
3433
for root, dirs, files in os.walk(os.path.dirname(ansible.modules.__file__)):
34+
files_without_symlinks = []
35+
for f in files:
36+
if not os.path.islink(os.path.join(root, f)):
37+
files_without_symlinks.append(f)
3538
file_names += [
3639
f"{root}/{file_name}"
37-
for file_name in files
40+
for file_name in files_without_symlinks
3841
if file_name.endswith(".py") and not file_name.startswith("__init__")
3942
]
40-
if include_user:
41-
for root, dirs, files in os.walk(os.path.expanduser('~/.ansible/collections/ansible_collections/')):
42-
file_names += [
43-
f"{root}/{file_name}"
44-
for file_name in files
45-
if file_name.endswith(".py") and not file_name.startswith("__init__")
46-
]
4743

4844
return sorted(file_names)
4945

46+
def get_files_user() -> List[str]:
47+
"""Return the sorted list of all module files provided by collections installed in the
48+
user home folder ~/.ansible/collections/
5049
51-
def get_docstrings(file_names: List[str]) -> List[Any]:
52-
"""Extract and return a list of docstring information from a list of files
50+
Returns
51+
-------
52+
List[str]
53+
A list of strings representing the Python module files installed in ~/.ansible/collections/
54+
"""
55+
56+
file_names: List[str] = []
57+
for root, dirs, files in os.walk(os.path.expanduser('~/.ansible/collections/ansible_collections/')):
58+
files_without_symlinks = []
59+
for f in files:
60+
if not os.path.islink(os.path.join(root, f)):
61+
files_without_symlinks.append(f)
62+
file_names += [
63+
f"{root}/{file_name}"
64+
for file_name in files_without_symlinks
65+
if file_name.endswith(".py") and not file_name.startswith("__init__") and "plugins/modules" in root
66+
]
67+
68+
return sorted(file_names)
69+
70+
71+
def get_module_docstring(file_path: str) -> Any:
72+
"""Extract and return docstring information from a module file
5373
5474
Parameters
5575
----------
56-
file_names: List[str]
57-
A list of strings representing file names
76+
file_names: file_path[str]
77+
string representing module file
5878
5979
Returns
6080
-------
61-
List[Any]
62-
A list of AnsibleMapping objects, representing docstring information
81+
Any
82+
An AnsibleMapping object, representing docstring information
6383
(in dict form), excluding those that are marked as deprecated.
6484
6585
"""
6686

67-
found_docstrings: List[Any] = []
68-
found_docstrings += [
69-
get_docstring(file_name, fragment_loader)[0] for file_name in file_names
70-
]
71-
return [
72-
current_docstring
73-
for current_docstring in found_docstrings
74-
if current_docstring and not current_docstring.get("deprecated")
75-
]
87+
docstring = get_docstring(file_path, fragment_loader)[0]
88+
89+
if docstring and not docstring.get("deprecated"):
90+
return docstring
7691

7792

7893
def escape_strings(escapist: str) -> str:
@@ -228,7 +243,7 @@ def module_options_to_snippet_options(module_options: Any) -> List[str]:
228243
return options
229244

230245

231-
def convert_docstring_to_snippet(convert_docstring: Any) -> List[str]:
246+
def convert_docstring_to_snippet(convert_docstring: Any, collection_name) -> List[str]:
232247
"""Converts data about an Ansible module into an UltiSnips snippet string
233248
234249
Parameters
@@ -250,17 +265,36 @@ def convert_docstring_to_snippet(convert_docstring: Any) -> List[str]:
250265
module_name = convert_docstring["module"]
251266
module_short_description = convert_docstring["short_description"]
252267

268+
# use only the module name if ansible version < 2.10
269+
if version.parse(ANSIBLE_VERSION) < version.parse("2.10"):
270+
snippet_module_name = f"{module_name}:"
271+
# use FQCN if ansible version is 2.10 or higher
272+
else:
273+
snippet_module_name = f"{collection_name}.{module_name}:"
274+
253275
snippet += [f'snippet {module_name} "{escape_strings(module_short_description)}" {snippet_options}']
254276
if args.style == "dictionary":
255-
snippet += [f"{module_name}:"]
277+
snippet += [f"{snippet_module_name}"]
256278
else:
257-
snippet += [f"{module_name}:{' >' if convert_docstring.get('options') else ''}"]
279+
snippet += [f"{snippet_module_name}:{' >' if convert_docstring.get('options') else ''}"]
258280
module_options = module_options_to_snippet_options(convert_docstring.get("options"))
259281
snippet += module_options
260282
snippet += ["endsnippet"]
261283

262284
return snippet
263285

286+
def get_collection_name(filepath:str) -> str:
287+
""" Returns the collection name for a full file path """
288+
289+
path_splitted = filepath.split('/')
290+
291+
collection_top_folder_index = path_splitted.index('ansible_collections')
292+
collection_namespace = path_splitted[collection_top_folder_index + 1]
293+
collection_name = path_splitted[collection_top_folder_index + 2]
294+
295+
# print(f"{collection_namespace}.{collection_name}")
296+
return f"{collection_namespace}.{collection_name}"
297+
264298

265299
if __name__ == "__main__":
266300

@@ -284,10 +318,36 @@ def convert_docstring_to_snippet(convert_docstring: Any) -> List[str]:
284318
)
285319
args = parser.parse_args()
286320

287-
docstrings = get_docstrings(get_files(include_user=args.user))
321+
322+
if version.parse(ANSIBLE_VERSION) < version.parse("2.10"):
323+
print(f"ansible version {ANSIBLE_VERSION} doesn't support FQCN")
324+
print("generated snippets will only use the module name e.g. 'yum' instead of 'ansible.builtin.yum'")
325+
else:
326+
print(f"ansible version {ANSIBLE_VERSION} supports using FQCN")
327+
print("Generated snippets will use FQCN e.g. 'ansible.builtin.yum' instead of 'yum'")
328+
print("Still, you only need to type 'yum' to trigger the snippet")
329+
330+
modules_docstrings = []
331+
332+
builtin_modules_paths = get_files_builtin()
333+
for f in builtin_modules_paths:
334+
docstring_builtin = get_module_docstring(f)
335+
if docstring_builtin and docstring_builtin not in modules_docstrings:
336+
docstring_builtin['collection_name'] = "ansible.builtin"
337+
modules_docstrings.append(docstring_builtin)
338+
339+
if args.user:
340+
user_modules_paths = get_files_user()
341+
for f in user_modules_paths:
342+
docstring_user = get_module_docstring(f)
343+
if docstring_user and docstring_user not in modules_docstrings:
344+
collection_name = get_collection_name(f)
345+
docstring_user['collection_name'] = collection_name
346+
modules_docstrings.append(docstring_user)
347+
288348
with open(args.output, "w") as f:
289349
f.writelines(f"{header}\n" for header in HEADER)
290-
for docstring in docstrings:
350+
for docstring in modules_docstrings:
291351
f.writelines(
292-
f"{line}\n" for line in convert_docstring_to_snippet(docstring)
352+
f"{line}\n" for line in convert_docstring_to_snippet(docstring, docstring.get("collection_name"))
293353
)

0 commit comments

Comments
 (0)