2********************************************************************************
3@file doxy_py_checker.py
4@brief Check parameter and return value for valid doxygen specification in python files
5********************************************************************************
11from typing
import Optional
15log = logging.getLogger(
"DoxyPyChecker")
18L_IGNORE_PARAM = [
"self",
"cls"]
19PARAM_DOC_PREFIX =
"@param"
20RETURN_DOC_PREFIX =
"@return"
21INIT_FUNCTION =
"__init__"
22L_EXCLUDE_FOLDER = [
".venv",
"Documentation"]
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
34 def __init__(self, path: Optional[str] =
None, print_checked_files: bool =
True):
39 self.
s_path = S_DEFAULT_PATH
44 @brief Define CMD arguments.
45 @return argument parser.
47 o_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
48 o_parser.add_argument(
"-p",
"--path",
50 help=
"set relative path to check for python files")
51 return o_parser.parse_args()
55 @brief Run doxygen checker
56 @return list of all files
62 log.info(
"Check docs for file: %s", file)
67 log.warning(
"Found %s Function with Warnings in %s files.", len(findings), len(l_files))
71 log.info(
"All functions correctly documented.")
77 @brief Get all python files in folder and subfolders
78 @return list of all 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]
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)
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
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:
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")
106 documented_params.add(param_name)
108 findings.append(f
"No parameter name found. Line content '{line}'")
109 return documented_params
111 def check_return(self, func_def: ast.FunctionDef | ast.AsyncFunctionDef, docstring: str) -> list[str]:
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
119 doc_has_return = RETURN_DOC_PREFIX
in docstring
120 func_has_return: bool |
None =
False
121 for node
in ast.walk(func_def):
122 if isinstance(node, ast.Return)
and node.value
is not None:
123 func_has_return =
True
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
129 if func_has_return
is not None:
130 if doc_has_return != func_has_return:
132 findings.append(
"Return type is not documented")
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")
141 def check_function(self, func_def: ast.FunctionDef | ast.AsyncFunctionDef, class_docstring: Optional[str] =
None) -> list[str]:
143 @brief Check function
144 @param func_def : function definition
145 @param class_docstring : docstring of class
146 @return list of findings in function
148 findings: list[str] = []
149 docstring = ast.get_docstring(func_def)
or class_docstring
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")
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")
179 @brief Check file for missing parameter description
180 @param file_path : file name
181 @return list of findings in file
183 with open(file_path, mode=
"r", encoding=
"utf-8")
as file:
186 tree = ast.parse(code)
189 for node
in ast.walk(tree):
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:
199 elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
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}"
211 file_findings.append(function_finding)
216if __name__ ==
"__main__":
219 args = doxy_checker.get_cmd_args()
221 doxy_checker.s_path = args.path
222 doxy_checker.run_check()
Doxygen documentation checker class.
set[str] get_doc_params(self, str docstring, list[str] findings)
Get documented parameters from docstring.
list[str] run_check(self)
Run doxygen checker.
list[str] print_checked_files
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.