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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 09:33 +0200
1"""
2License GPLv3 or higher.
4(C) 2025 Created by Maikel Mardjan - https://nocomplexity.com/
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.
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.
10You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
12Simple checker reporting #source code, #comments in Python files
13"""
15import ast
16import warnings
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)
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)
40 ast_nodes = 0
41 ast_functions = 0
42 ast_classes = 0
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
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", []))
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 }
66 return result
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
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
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
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
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
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
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