YouTubeDownloader v1.1.2
YouTube content downloader
Loading...
Searching...
No Matches
doxy_py_checker.py
Go to the documentation of this file.
1"""!
2********************************************************************************
3@file doxy_py_checker.py
4@brief Check parameter and return value for valid doxygen specification in python files
5********************************************************************************
6"""
7
8import os
9import logging
10import argparse
11from typing import Optional
12import ast
13import astunparse
14
15log = logging.getLogger("DoxyPyChecker")
16
17S_DEFAULT_PATH = "./"
18L_IGNORE_PARAM = ["self", "cls"]
19PARAM_DOC_PREFIX = "@param"
20RETURN_DOC_PREFIX = "@return"
21INIT_FUNCTION = "__init__"
22L_EXCLUDE_FOLDER = [".venv", "Documentation"]
23
24CHECK_TYPING = False
25
26
28 """!
29 @brief Doxygen documentation checker class
30 @param path : Check python files located in this path
31 @param print_checked_files : status if checked files should print
32 """
33
34 def __init__(self, path: Optional[str] = None, print_checked_files: bool = True):
35 self.warnings: list[str] = []
36 if path is not None:
37 self.s_path = path
38 else:
39 self.s_path = S_DEFAULT_PATH
40 self.print_checked_filesprint_checked_files = print_checked_files
41
42 def get_cmd_args(self) -> argparse.Namespace:
43 """!
44 @brief Define CMD arguments.
45 @return argument parser.
46 """
47 o_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
48 o_parser.add_argument("-p", "--path",
49 type=str,
50 help="set relative path to check for python files")
51 return o_parser.parse_args()
52
53 def run_check(self) -> list[str]:
54 """!
55 @brief Run doxygen checker
56 @return list of all files
57 """
58 findings = []
59 l_files = self.get_files()
60 for file in l_files:
62 log.info("Check docs for file: %s", file)
63 findings += self.check_file(file)
64
65 # print result
66 if findings:
67 log.warning("Found %s Function with Warnings in %s files.", len(findings), len(l_files))
68 for text in findings:
69 log.warning(text)
70 else:
71 log.info("All functions correctly documented.")
72
73 return findings
74
75 def get_files(self) -> list[str]:
76 """!
77 @brief Get all python files in folder and subfolders
78 @return list of all files
79 """
80 l_files = []
81 for root, dirs, files in os.walk(self.s_path):
82 dirs[:] = [d for d in dirs if os.path.basename(d) not in L_EXCLUDE_FOLDER] # Remove the directories to be excluded from the walk
83 for f in files:
84 _file_name, file_type = os.path.splitext(f)
85 if file_type == ".py":
86 fullpath = os.path.join(root, f)
87 l_files.append(fullpath)
88 return l_files
89
90 def get_doc_params(self, docstring: str, findings: list[str]) -> set[str]:
91 """!
92 @brief Get documented parameters from docstring
93 @param docstring : docstring of class
94 @param findings : list to add doc warnings
95 @return collection with documented parameters
96 """
97 documented_params = set()
98 for line in docstring.splitlines():
99 if line.lstrip().startswith(PARAM_DOC_PREFIX):
100 l_param_name = line.split()
101 if len(l_param_name) >= 2: # minimum two names: @param, param_name
102 param_name = l_param_name[1].rstrip(":")
103 if param_name in documented_params:
104 findings.append(f"{param_name} is documented multiple times")
105 else:
106 documented_params.add(param_name)
107 else:
108 findings.append(f"No parameter name found. Line content '{line}'")
109 return documented_params
110
111 def check_return(self, func_def: ast.FunctionDef | ast.AsyncFunctionDef, docstring: str) -> list[str]:
112 """!
113 @brief Check for documented return value
114 @param func_def : function definition
115 @param docstring : docstring of function
116 @return list of return findings in function
117 """
118 findings = []
119 doc_has_return = RETURN_DOC_PREFIX in docstring
120 func_has_return: bool | None = False # True: need doc, False: no doc, None: optional doc
121 for node in ast.walk(func_def):
122 if isinstance(node, ast.Return) and node.value is not None: # function has no return None
123 func_has_return = True
124 break
125 if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and (node.func.attr == "exit"):
126 module = node.func.value
127 if isinstance(module, ast.Name) and (module.id == "sys"):
128 func_has_return = None # documentation of "sys.exit" is optional
129 if func_has_return is not None:
130 if doc_has_return != func_has_return:
131 if func_has_return:
132 findings.append("Return type is not documented")
133 else:
134 findings.append("Return type is documented but not used")
135 if CHECK_TYPING and func_has_return:
136 return_annotation = astunparse.unparse(func_def.returns).strip() if func_def.returns else None
137 if return_annotation is None:
138 findings.append("Return type has no typing")
139 return findings
140
141 def check_function(self, func_def: ast.FunctionDef | ast.AsyncFunctionDef, class_docstring: Optional[str] = None) -> list[str]:
142 """!
143 @brief Check function
144 @param func_def : function definition
145 @param class_docstring : docstring of class
146 @return list of findings in function
147 """
148 findings: list[str] = []
149 docstring = ast.get_docstring(func_def) or class_docstring
150 if docstring:
151 # Get documented parameters and check for duplicate documentation
152 documented_params = self.get_doc_params(docstring, findings)
153
154 # Check that all functional parameters are documented
155 for arg in func_def.args.args:
156 if arg.arg not in L_IGNORE_PARAM:
157 if arg.arg not in documented_params:
158 findings.append(f"{arg.arg} is not documented")
159 if CHECK_TYPING and arg.annotation is None:
160 findings.append(f"{arg.arg} has no typing")
161
162 # Check that the documented parameters are present
163 for documented_param in documented_params:
164 all_args = [arg.arg for arg in func_def.args.args]
165 if func_def.args.vararg:
166 all_args.append(func_def.args.vararg.arg)
167 if func_def.args.kwarg:
168 all_args.append(func_def.args.kwarg.arg)
169 if documented_param not in all_args:
170 findings.append(f"{documented_param} is documented but not used")
171
172 # Check that the function return is correctly specified
173 findings += self.check_return(func_def, docstring)
174
175 return findings
176
177 def check_file(self, file_path: str) -> list[str]:
178 """!
179 @brief Check file for missing parameter description
180 @param file_path : file name
181 @return list of findings in file
182 """
183 with open(file_path, mode="r", encoding="utf-8") as file:
184 code = file.read()
185
186 tree = ast.parse(code)
187
188 file_findings = []
189 for node in ast.walk(tree):
190 func_def = None
191 check_findings = []
192 if isinstance(node, ast.ClassDef):
193 class_docstring = ast.get_docstring(node)
194 for subnode in node.body:
195 if isinstance(subnode, (ast.FunctionDef, ast.AsyncFunctionDef)):
196 if subnode.name == INIT_FUNCTION:
197 func_def = subnode
198 check_findings = self.check_function(func_def, class_docstring)
199 elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
200 func_def = node
201 check_findings = self.check_function(func_def)
202
203 # print findings
204 function_finding = ""
205 if func_def and check_findings:
206 function_finding = f"{file_path}:{func_def.lineno} {func_def.name}"
207 for warning in check_findings:
208 function_finding += f"\n {warning}"
209
210 if function_finding:
211 file_findings.append(function_finding)
212
213 return file_findings
214
215
216if __name__ == "__main__":
217 CHECK_TYPING = True
218 doxy_checker = DoxyPyChecker(path="../../", print_checked_files=False)
219 args = doxy_checker.get_cmd_args()
220 if args.path:
221 doxy_checker.s_path = args.path
222 doxy_checker.run_check()
set[str] get_doc_params(self, str docstring, list[str] findings)
Get documented parameters from docstring.
list[str] get_files(self)
Get all python files in folder and subfolders.
list[str] check_file(self, str file_path)
Check file for missing parameter description.
list[str] check_function(self, ast.FunctionDef|ast.AsyncFunctionDef func_def, Optional[str] class_docstring=None)
Check function.
list[str] check_return(self, ast.FunctionDef|ast.AsyncFunctionDef func_def, str docstring)
Check for documented return value.
__init__(self, Optional[str] path=None, bool print_checked_files=True)
argparse.Namespace get_cmd_args(self)
Define CMD arguments.