Coverage for src / codeaudit / totals.py: 67%

94 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-09 09:33 +0200

1""" 

2License GPLv3 or higher. 

3 

4(C) 2025 Created by Maikel Mardjan - https://nocomplexity.com/ 

5 

6This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 

7 

8This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

9 

10You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. 

11 

12Simple checker reporting #source code, #comments in Python files 

13""" 

14 

15import ast 

16import warnings 

17 

18from codeaudit.checkmodules import get_all_modules, get_imported_modules 

19from codeaudit.complexitycheck import ( 

20 calculate_complexity, 

21 count_static_warnings_in_file, 

22) 

23from codeaudit.filehelpfunctions import ( 

24 collect_python_source_files, 

25 get_filename_from_path, 

26 read_in_source_file, 

27) 

28 

29 

30def count_ast_objects(source): 

31 """ 

32 Counts AST nodes and objects. 

33 This gives an indication of complexity and code maintainability. 

34 Suppresses SyntaxWarnings like invalid escape sequences. 

35 """ 

36 with warnings.catch_warnings(): 

37 warnings.simplefilter("ignore", category=SyntaxWarning) 

38 tree = ast.parse(source) 

39 

40 ast_nodes = 0 

41 ast_functions = 0 

42 ast_classes = 0 

43 

44 for node in ast.walk(tree): 

45 if hasattr(node, "lineno") and isinstance(node, (ast.stmt, ast.Expr)): 

46 ast_nodes += 1 

47 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): 

48 ast_functions += 1 

49 # if isinstance(node, (ast.Import, ast.ImportFrom)): 

50 # ast_modules += 1 

51 if isinstance(node, ast.ClassDef): 

52 ast_classes += 1 

53 

54 used_modules = get_imported_modules(source) 

55 number_core_modules = len(used_modules.get("core_modules", [])) 

56 number_external_modules = len(used_modules.get("imported_modules", [])) 

57 

58 result = { 

59 "AST_Nodes": ast_nodes, 

60 "Std-Modules": number_core_modules, 

61 "External-Modules": number_external_modules, 

62 "Functions": ast_functions, 

63 "Classes": ast_classes, 

64 } 

65 

66 return result 

67 

68 

69def count_comment_lines(source): 

70 """Counts lines with comments and all lines inside triple-double-quoted strings""" 

71 comment_lines = set() 

72 lines = source.splitlines() 

73 in_triple_double = False 

74 

75 for i, line in enumerate(lines, start=1): 

76 stripped = line.strip() 

77 # Check for triple-double-quote boundaries 

78 if stripped.count('"""') == 1: 

79 # Start or end of a multiline string 

80 in_triple_double = not in_triple_double 

81 comment_lines.add(i) # Count this line 

82 elif stripped.count('"""') >= 2: 

83 # Triple quotes open and close on the same line 

84 comment_lines.add(i) 

85 elif in_triple_double: 

86 comment_lines.add(i) 

87 elif line.startswith("#"): 

88 # Regular comment - note that inline comment are NOT counted as comment lines 

89 comment_lines.add(i) 

90 result = {"Comment_Lines": len(comment_lines)} 

91 return result 

92 

93 

94def count_lines_iterate(filepath): 

95 """ 

96 Counts the number of lines in a file by iterating through the file object. 

97 This method is memory-efficient for large files. 

98 """ 

99 count = 0 

100 try: 

101 with open(filepath, "r") as f: 

102 for line in f: 

103 count += 1 

104 return count 

105 except FileNotFoundError: 

106 print(f"Error: File not found at {filepath}") 

107 return -1 

108 except Exception as e: 

109 print(f"An error occurred: {e}") 

110 return -1 

111 

112 

113def get_statistics(directory): 

114 """Get codeaudit statistics from source files in a directory""" 

115 files_to_check = collect_python_source_files(directory) 

116 total_result = [] 

117 for python_file in files_to_check: 

118 result = overview_per_file(python_file) 

119 total_result.append(result) 

120 return total_result 

121 

122 

123def total_modules(directory): 

124 """get the total number of modules (core and imported) for the overview""" 

125 used_modules = get_all_modules(directory) 

126 number_core_modules = len(used_modules.get("core_modules", [])) 

127 number_external_modules = len(used_modules.get("imported_modules", [])) 

128 module_result = { 

129 "Std-Modules": number_core_modules, 

130 "External-Modules": number_external_modules, 

131 } 

132 return module_result 

133 

134 

135def overview_per_file(python_file): 

136 """gets the relevant security statistics overview per file.""" 

137 result = {} 

138 source = read_in_source_file(python_file) 

139 name_of_file = get_filename_from_path(python_file) 

140 name_dict = {"FileName": name_of_file} 

141 file_location = {"FilePath": python_file} 

142 number_of_lines = count_lines_iterate(python_file) 

143 lines = {"Number_Of_Lines": number_of_lines} 

144 complexity_score = calculate_complexity(source) 

145 complexity = {"Complexity_Score": complexity_score} 

146 warnings_count = count_static_warnings_in_file(python_file) 

147 result = ( 

148 name_dict 

149 | file_location 

150 | lines 

151 | count_ast_objects(source) 

152 | count_comment_lines(source) 

153 | complexity 

154 | warnings_count 

155 ) # merge the dicts 

156 return result 

157 

158 

159def overview_count(df): 

160 """returns a dataframe with simple overview for all files""" 

161 columns_to_sum = [ 

162 "Number_Of_Lines", 

163 "AST_Nodes", 

164 "Functions", 

165 "Classes", 

166 "Comment_Lines", 

167 ] 

168 df_totals = df[columns_to_sum].sum().to_frame().T # .T to make it a single row 

169 total_number_of_files = df.shape[0] 

170 df_totals.insert( 

171 0, "Number_Of_Files", total_number_of_files 

172 ) # insert new column as first colum 

173 number_cm = df.at[0, "Std-Modules"] 

174 df_totals.insert(3, "Core Modules", number_cm) 

175 number_em = df.at[0, "External-Modules"] 

176 df_totals.insert(4, "External Modules", number_em) 

177 median_complexity = round(df["Complexity_Score"].mean(), 1) 

178 df_totals["Median_Complexity"] = median_complexity 

179 maximum_complexity = df["Complexity_Score"].max() 

180 df_totals["Maximum_Complexity"] = maximum_complexity 

181 return df_totals