metripy 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of metripy might be problematic. Click here for more details.
- metripy/Application/Analyzer.py +106 -0
- metripy/Application/Application.py +54 -0
- metripy/Application/Config/Config.py +13 -0
- metripy/Application/Config/File/ConfigFileReaderFactory.py +24 -0
- metripy/Application/Config/File/ConfigFileReaderInterface.py +14 -0
- metripy/Application/Config/File/JsonConfigFileReader.py +82 -0
- metripy/Application/Config/GitConfig.py +10 -0
- metripy/Application/Config/Parser.py +31 -0
- metripy/Application/Config/ProjectConfig.py +27 -0
- metripy/Application/Config/ReportConfig.py +10 -0
- metripy/Application/__init__.py +0 -0
- metripy/Component/Debug/Debugger.py +20 -0
- metripy/Component/File/Finder.py +37 -0
- metripy/Component/Output/CliOutput.py +49 -0
- metripy/Component/Output/ProgressBar.py +27 -0
- metripy/Dependency/Composer/Composer.py +30 -0
- metripy/Dependency/Composer/Packegist.py +55 -0
- metripy/Dependency/Dependency.py +30 -0
- metripy/Dependency/Npm/Npm.py +30 -0
- metripy/Dependency/Npm/NpmOrg.py +47 -0
- metripy/Dependency/Pip/Pip.py +69 -0
- metripy/Dependency/Pip/PyPi.py +49 -0
- metripy/Git/GitAnalyzer.py +86 -0
- metripy/LangAnalyzer/AbstractLangAnalyzer.py +65 -0
- metripy/LangAnalyzer/Generic/HalSteadAnalyzer.py +58 -0
- metripy/LangAnalyzer/Generic/__init__.py +0 -0
- metripy/LangAnalyzer/Php/PhpAnalyzer.py +193 -0
- metripy/LangAnalyzer/Php/PhpBasicAstParser.py +56 -0
- metripy/LangAnalyzer/Php/PhpBasicLocAnalyzer.py +174 -0
- metripy/LangAnalyzer/Php/PhpHalSteadAnalyzer.py +44 -0
- metripy/LangAnalyzer/Python/PythonAnalyzer.py +129 -0
- metripy/LangAnalyzer/Typescript/TypescriptAnalyzer.py +208 -0
- metripy/LangAnalyzer/Typescript/TypescriptAstParser.py +68 -0
- metripy/LangAnalyzer/Typescript/TypescriptBasicComplexityAnalyzer.py +114 -0
- metripy/LangAnalyzer/Typescript/TypescriptBasicLocAnalyzer.py +69 -0
- metripy/LangAnalyzer/Typescript/TypescriptHalSteadAnalyzer.py +55 -0
- metripy/LangAnalyzer/__init__.py +0 -0
- metripy/Metric/Code/AggregatedMetrics.py +42 -0
- metripy/Metric/Code/FileMetrics.py +33 -0
- metripy/Metric/Code/ModuleMetrics.py +32 -0
- metripy/Metric/Code/SegmentedMetrics.py +65 -0
- metripy/Metric/FileTree/FileTree.py +15 -0
- metripy/Metric/FileTree/FileTreeParser.py +42 -0
- metripy/Metric/Git/GitCodeHotspot.py +37 -0
- metripy/Metric/Git/GitContributor.py +37 -0
- metripy/Metric/Git/GitKnowledgeSilo.py +27 -0
- metripy/Metric/Git/GitMetrics.py +148 -0
- metripy/Metric/ProjectMetrics.py +55 -0
- metripy/Report/Csv/Reporter.py +12 -0
- metripy/Report/Html/Reporter.py +210 -0
- metripy/Report/Json/AbstractJsonReporter.py +11 -0
- metripy/Report/Json/GitJsonReporter.py +21 -0
- metripy/Report/Json/JsonReporter.py +12 -0
- metripy/Report/ReporterFactory.py +22 -0
- metripy/Report/ReporterInterface.py +17 -0
- metripy/Tree/ClassNode.py +32 -0
- metripy/Tree/FunctionNode.py +49 -0
- metripy/Tree/ModuleNode.py +42 -0
- metripy/__init__.py +0 -0
- metripy/metripy.py +15 -0
- metripy-0.2.7.dist-info/METADATA +113 -0
- metripy-0.2.7.dist-info/RECORD +66 -0
- metripy-0.2.7.dist-info/WHEEL +5 -0
- metripy-0.2.7.dist-info/entry_points.txt +2 -0
- metripy-0.2.7.dist-info/licenses/LICENSE +21 -0
- metripy-0.2.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from metripy.Metric.Git.GitCodeHotspot import GitCodeHotspot
|
|
2
|
+
from metripy.Metric.Git.GitContributor import GitContributor
|
|
3
|
+
from metripy.Metric.Git.GitKnowledgeSilo import GitKnowledgeSilo
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GitMetrics:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
analysis_start_date: str,
|
|
10
|
+
commit_stats_per_month: dict[str, int],
|
|
11
|
+
churn_per_month: dict[str, dict[str, int]],
|
|
12
|
+
contributor_stats: dict[str, dict[str, int]],
|
|
13
|
+
file_contributors: dict[str, dict[str, int]],
|
|
14
|
+
):
|
|
15
|
+
self.analysis_start_date = analysis_start_date
|
|
16
|
+
self._commit_stats_per_month: dict[str, int] = commit_stats_per_month
|
|
17
|
+
self._churn_per_month: dict[str, dict[str, int]] = churn_per_month
|
|
18
|
+
self._contributor_stats = contributor_stats
|
|
19
|
+
self.total_commits = sum(data["commits"] for data in contributor_stats.values())
|
|
20
|
+
self.contributors: list[GitContributor] = self._calc_contributors(
|
|
21
|
+
contributor_stats
|
|
22
|
+
)
|
|
23
|
+
self.core_contributors, self.core_percentage = self._calc_core_contributors(
|
|
24
|
+
self.contributors
|
|
25
|
+
)
|
|
26
|
+
self.silos: list[GitKnowledgeSilo] = self._calc_silos(file_contributors)
|
|
27
|
+
self.hotspots: list[GitCodeHotspot] = self._calc_hotspots(file_contributors)
|
|
28
|
+
|
|
29
|
+
def get_analysis_start_date(self) -> str:
|
|
30
|
+
return self.analysis_start_date
|
|
31
|
+
|
|
32
|
+
def get_commit_stats_per_month(self) -> dict[str, int]:
|
|
33
|
+
return self._commit_stats_per_month
|
|
34
|
+
|
|
35
|
+
def get_churn_per_month(self) -> dict[str, dict[str, int]]:
|
|
36
|
+
return self._churn_per_month
|
|
37
|
+
|
|
38
|
+
def get_avg_commit_size(self) -> float:
|
|
39
|
+
return (
|
|
40
|
+
sum(
|
|
41
|
+
data["lines_added"] + data["lines_removed"]
|
|
42
|
+
for data in self._contributor_stats.values()
|
|
43
|
+
)
|
|
44
|
+
/ self.total_commits
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def get_contributors_dict(self) -> dict[str, dict[str, int]]:
|
|
48
|
+
return {
|
|
49
|
+
contributor.name: contributor.to_dict() for contributor in self.contributors
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def get_contributors_list(self) -> list[dict[str, int]]:
|
|
53
|
+
return [contributor.to_dict() for contributor in self.contributors]
|
|
54
|
+
|
|
55
|
+
def get_hotspots_list(self) -> list[dict[str, int]]:
|
|
56
|
+
return [hotspot.to_dict() for hotspot in self.hotspots]
|
|
57
|
+
|
|
58
|
+
def get_silos_dict(self) -> dict[str, dict[str, int]]:
|
|
59
|
+
return {silo.file_path: silo.to_dict() for silo in self.silos}
|
|
60
|
+
|
|
61
|
+
def get_silos_list(self) -> list[dict[str, int]]:
|
|
62
|
+
return [silo.to_dict() for silo in self.silos]
|
|
63
|
+
|
|
64
|
+
def _calc_hotspots(
|
|
65
|
+
self, file_contributors: dict[str, dict[str, int]]
|
|
66
|
+
) -> list[GitCodeHotspot]:
|
|
67
|
+
hotspots = []
|
|
68
|
+
for file_path, data in file_contributors.items():
|
|
69
|
+
changes_count = data["commits"]
|
|
70
|
+
if changes_count < 10:
|
|
71
|
+
continue
|
|
72
|
+
hotspots.append(
|
|
73
|
+
GitCodeHotspot(
|
|
74
|
+
file_path=file_path,
|
|
75
|
+
changes_count=changes_count,
|
|
76
|
+
contributors_count=len(data["contributors"]),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
return sorted(hotspots, key=lambda x: x.changes_count, reverse=True)
|
|
80
|
+
|
|
81
|
+
def _calc_contributors(
|
|
82
|
+
self, contributor_stats: dict[str, dict[str, int]]
|
|
83
|
+
) -> list[GitContributor]:
|
|
84
|
+
# determine top contributors
|
|
85
|
+
total_commits = self.total_commits
|
|
86
|
+
contributors = []
|
|
87
|
+
|
|
88
|
+
for name, data in contributor_stats.items():
|
|
89
|
+
percentage = (
|
|
90
|
+
(data["commits"] / total_commits * 100) if total_commits > 0 else 0
|
|
91
|
+
)
|
|
92
|
+
contributors.append(
|
|
93
|
+
GitContributor(
|
|
94
|
+
name=name,
|
|
95
|
+
commits_count=data["commits"],
|
|
96
|
+
lines_added=data["lines_added"],
|
|
97
|
+
lines_removed=data["lines_removed"],
|
|
98
|
+
contribution_percentage=int(percentage),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return sorted(contributors, key=lambda x: x.commits_count, reverse=True)
|
|
103
|
+
|
|
104
|
+
def _calc_core_contributors(
|
|
105
|
+
self, contributors: list[GitContributor]
|
|
106
|
+
) -> tuple[int, int]:
|
|
107
|
+
# Calculate core contributors (top contributors making up 67% of commits)
|
|
108
|
+
core_contributors = 0
|
|
109
|
+
core_percentage = 0
|
|
110
|
+
for contributor in contributors:
|
|
111
|
+
core_percentage += contributor.contribution_percentage
|
|
112
|
+
core_contributors += 1
|
|
113
|
+
if core_percentage >= 67:
|
|
114
|
+
break
|
|
115
|
+
core_percentage = min(67, core_percentage)
|
|
116
|
+
|
|
117
|
+
return core_contributors, core_percentage
|
|
118
|
+
|
|
119
|
+
def _calc_silos(
|
|
120
|
+
self, file_contributors: dict[str, dict[str, int]]
|
|
121
|
+
) -> list[GitKnowledgeSilo]:
|
|
122
|
+
silos = []
|
|
123
|
+
for file_path, data in file_contributors.items():
|
|
124
|
+
if len(data["contributors"]) > 1 or data["commits"] < 3:
|
|
125
|
+
continue
|
|
126
|
+
owner = list(data["contributors"])[0]
|
|
127
|
+
silos.append(
|
|
128
|
+
GitKnowledgeSilo(
|
|
129
|
+
file_path=file_path,
|
|
130
|
+
owner=owner,
|
|
131
|
+
commits_count=data["commits"],
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
return sorted(silos, key=lambda x: x.commits_count, reverse=True)
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> dict[str, any]:
|
|
137
|
+
return {
|
|
138
|
+
"analysis_start_date": self.analysis_start_date,
|
|
139
|
+
"avg_commit_size": f"{self.get_avg_commit_size():.2f}",
|
|
140
|
+
"commit_stats_per_month": self.get_commit_stats_per_month(),
|
|
141
|
+
"churn_per_month": self.get_churn_per_month(),
|
|
142
|
+
"total_commits": self.total_commits,
|
|
143
|
+
"active_contributors": len(self.contributors),
|
|
144
|
+
"contributors": self.get_contributors_dict(),
|
|
145
|
+
"core_contributors": self.core_contributors,
|
|
146
|
+
"core_percentage": self.core_percentage,
|
|
147
|
+
"silos": self.get_silos_list(),
|
|
148
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from metripy.Dependency.Dependency import Dependency
|
|
2
|
+
from metripy.Metric.Code.AggregatedMetrics import AggregatedMetrics
|
|
3
|
+
from metripy.Metric.Code.FileMetrics import FileMetrics
|
|
4
|
+
from metripy.Metric.Code.SegmentedMetrics import SegmentedMetrics
|
|
5
|
+
from metripy.Metric.Git.GitMetrics import GitMetrics
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectMetrics:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
file_metrics: list[FileMetrics],
|
|
12
|
+
git_metrics: GitMetrics | None,
|
|
13
|
+
dependencies: list[Dependency] | None,
|
|
14
|
+
):
|
|
15
|
+
self.file_metrics = file_metrics
|
|
16
|
+
self.git_metrics = git_metrics
|
|
17
|
+
self.dependencies = dependencies
|
|
18
|
+
self.total_code_metrics = self._compile_total_metrics(self.file_metrics)
|
|
19
|
+
|
|
20
|
+
def _compile_total_metrics(
|
|
21
|
+
self, file_metrics: list[FileMetrics]
|
|
22
|
+
) -> AggregatedMetrics:
|
|
23
|
+
files = 0
|
|
24
|
+
locs = []
|
|
25
|
+
avgCcPerFunctions = []
|
|
26
|
+
maintainabilityIndices = []
|
|
27
|
+
avgLocPerFunctions = []
|
|
28
|
+
for file_metric in file_metrics:
|
|
29
|
+
files += 1
|
|
30
|
+
locs.append(file_metric.loc)
|
|
31
|
+
avgCcPerFunctions.append(file_metric.avgCcPerFunction)
|
|
32
|
+
maintainabilityIndices.append(file_metric.maintainabilityIndex)
|
|
33
|
+
avgLocPerFunctions.append(file_metric.avgLocPerFunction)
|
|
34
|
+
|
|
35
|
+
if files == 0:
|
|
36
|
+
return AggregatedMetrics()
|
|
37
|
+
|
|
38
|
+
return AggregatedMetrics(
|
|
39
|
+
loc=sum(locs),
|
|
40
|
+
avgCcPerFunction=self._avg(avgCcPerFunctions),
|
|
41
|
+
maintainabilityIndex=self._avg(maintainabilityIndices),
|
|
42
|
+
avgLocPerFunction=self._avg(avgLocPerFunctions),
|
|
43
|
+
num_files=files,
|
|
44
|
+
segmented_loc=SegmentedMetrics().set_loc(locs),
|
|
45
|
+
segmented_complexity=SegmentedMetrics().set_complexity(avgCcPerFunctions),
|
|
46
|
+
segmented_maintainability=SegmentedMetrics().set_maintainability(
|
|
47
|
+
maintainabilityIndices
|
|
48
|
+
),
|
|
49
|
+
segmented_method_size=SegmentedMetrics().set_method_size(
|
|
50
|
+
avgLocPerFunctions
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _avg(self, items: list[float | int]) -> float:
|
|
55
|
+
return sum(items) / len(items)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from metripy.Application.Config.Config import Config
|
|
2
|
+
from metripy.Component.Output.CliOutput import CliOutput
|
|
3
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Reporter:
|
|
7
|
+
def __init__(self, config: Config, output: CliOutput):
|
|
8
|
+
self.config = config
|
|
9
|
+
self.output = output
|
|
10
|
+
|
|
11
|
+
def generate(self, metrics: ProjectMetrics):
|
|
12
|
+
raise NotImplementedError("CSV metrics report is not yet implemented")
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from py_template_engine import TemplateEngine
|
|
7
|
+
|
|
8
|
+
from metripy.Application.Config.ReportConfig import ReportConfig
|
|
9
|
+
from metripy.Component.Output.CliOutput import CliOutput
|
|
10
|
+
from metripy.Metric.FileTree.FileTreeParser import FileTreeParser
|
|
11
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
12
|
+
from metripy.Report.ReporterInterface import ReporterInterface
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Reporter(ReporterInterface):
|
|
16
|
+
def __init__(
|
|
17
|
+
self, config: ReportConfig, output: CliOutput, project_name: str = "foobar"
|
|
18
|
+
):
|
|
19
|
+
self.config: ReportConfig = config
|
|
20
|
+
self.output = output
|
|
21
|
+
self.template_dir = os.path.join(os.getcwd(), "templates/html_report")
|
|
22
|
+
|
|
23
|
+
self.global_template_args = {
|
|
24
|
+
"project_name": project_name,
|
|
25
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def generate(self, metrics: ProjectMetrics):
|
|
29
|
+
|
|
30
|
+
self.output.writeln("<info>Generating HTML report...</info>")
|
|
31
|
+
|
|
32
|
+
# copy sources
|
|
33
|
+
if not os.path.exists(os.path.join(self.config.path, "js")):
|
|
34
|
+
os.makedirs(os.path.join(self.config.path, "js"))
|
|
35
|
+
if not os.path.exists(os.path.join(self.config.path, "css")):
|
|
36
|
+
os.makedirs(os.path.join(self.config.path, "css"))
|
|
37
|
+
if not os.path.exists(os.path.join(self.config.path, "images")):
|
|
38
|
+
os.makedirs(os.path.join(self.config.path, "images"))
|
|
39
|
+
if not os.path.exists(os.path.join(self.config.path, "fonts")):
|
|
40
|
+
os.makedirs(os.path.join(self.config.path, "fonts"))
|
|
41
|
+
if not os.path.exists(os.path.join(self.config.path, "data")):
|
|
42
|
+
os.makedirs(os.path.join(self.config.path, "data"))
|
|
43
|
+
|
|
44
|
+
# shutil.copy(os.path.join(self.template_dir, "favicon.ico"), os.path.join(self.config.path, "favicon.ico"))
|
|
45
|
+
|
|
46
|
+
shutil.copytree(
|
|
47
|
+
os.path.join(self.template_dir, "js"),
|
|
48
|
+
os.path.join(self.config.path, "js"),
|
|
49
|
+
dirs_exist_ok=True,
|
|
50
|
+
)
|
|
51
|
+
shutil.copytree(
|
|
52
|
+
os.path.join(self.template_dir, "css"),
|
|
53
|
+
os.path.join(self.config.path, "css"),
|
|
54
|
+
dirs_exist_ok=True,
|
|
55
|
+
)
|
|
56
|
+
# shutil.copytree(os.path.join(self.template_dir, "images"), os.path.join(self.config.path, "images"), dirs_exist_ok=True)
|
|
57
|
+
# shutil.copytree(os.path.join(self.template_dir, "fonts"), os.path.join(self.config.path, "fonts"), dirs_exist_ok=True)
|
|
58
|
+
|
|
59
|
+
# render templates
|
|
60
|
+
git_stats_data = {}
|
|
61
|
+
if metrics.git_metrics:
|
|
62
|
+
git_stats_data = metrics.git_metrics.get_commit_stats_per_month()
|
|
63
|
+
|
|
64
|
+
self.output.writeln("<info>Rendering index page</info>")
|
|
65
|
+
# Render main index page
|
|
66
|
+
self.render_template(
|
|
67
|
+
"index.html",
|
|
68
|
+
{
|
|
69
|
+
"git_stats_data": json.dumps(git_stats_data, indent=4),
|
|
70
|
+
"total_code_metrics": metrics.total_code_metrics.to_dict(),
|
|
71
|
+
"segmentation_data": json.dumps(
|
|
72
|
+
metrics.total_code_metrics.to_dict_segmentation(), indent=4
|
|
73
|
+
),
|
|
74
|
+
"project_name": "CodeMetrics",
|
|
75
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
76
|
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
77
|
+
"author": "CodeMetrics",
|
|
78
|
+
"version": "1.0.0",
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
self.output.writeln("<success>Done rendering index page</success>")
|
|
82
|
+
|
|
83
|
+
# Render files page
|
|
84
|
+
self.render_files_page(metrics)
|
|
85
|
+
# Render git analysis page
|
|
86
|
+
self.render_git_analysis_page(metrics)
|
|
87
|
+
|
|
88
|
+
self.render_dependencies_page(metrics)
|
|
89
|
+
|
|
90
|
+
self.output.writeln(
|
|
91
|
+
f"<success>HTML report generated in {self.config.path} directory</success>"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def render_template(self, template_name: str, data: dict) -> str:
|
|
95
|
+
engine = TemplateEngine(os.path.join(self.template_dir, template_name))
|
|
96
|
+
content = engine.render(**data)
|
|
97
|
+
with open(os.path.join(self.config.path, template_name), "w") as file:
|
|
98
|
+
file.write(content)
|
|
99
|
+
|
|
100
|
+
def render_dependencies_page(self, metrics: ProjectMetrics):
|
|
101
|
+
"""Render the dependencies page with dependency details and stats"""
|
|
102
|
+
if not metrics.dependencies:
|
|
103
|
+
self.output.writeln("<success>No dependencies to render</success>")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self.output.writeln("<info>Rendering dependencies page</info>")
|
|
107
|
+
|
|
108
|
+
dependencies = metrics.dependencies if metrics.dependencies is not None else []
|
|
109
|
+
|
|
110
|
+
license_by_type = {}
|
|
111
|
+
|
|
112
|
+
# TODO render a pie chart
|
|
113
|
+
for dependency in dependencies:
|
|
114
|
+
for license_name in dependency.license:
|
|
115
|
+
if license_name not in license_by_type.keys():
|
|
116
|
+
license_by_type[license_name] = 0
|
|
117
|
+
license_by_type[license_name] += 1
|
|
118
|
+
|
|
119
|
+
print(json.dumps(license_by_type, indent=2))
|
|
120
|
+
|
|
121
|
+
self.render_template(
|
|
122
|
+
"dependencies.html",
|
|
123
|
+
{
|
|
124
|
+
"dependencies": [d.to_dict() for d in dependencies],
|
|
125
|
+
"project_name": "CodeMetrics",
|
|
126
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
self.output.writeln(
|
|
130
|
+
"<success>Dependencies page generated successfully</success>"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def render_files_page(self, metrics: ProjectMetrics):
|
|
134
|
+
"""Render the files page with file details and analysis"""
|
|
135
|
+
self.output.writeln("<info>Rendering files page</info>")
|
|
136
|
+
|
|
137
|
+
file_names = []
|
|
138
|
+
file_details = {}
|
|
139
|
+
for file_metrics in metrics.file_metrics:
|
|
140
|
+
file_name = file_metrics.full_name
|
|
141
|
+
file_details[file_name] = file_metrics.to_dict()
|
|
142
|
+
file_names.append(file_name)
|
|
143
|
+
|
|
144
|
+
filetree = FileTreeParser.parse(file_names, shorten=True)
|
|
145
|
+
|
|
146
|
+
self.render_template(
|
|
147
|
+
"files.html",
|
|
148
|
+
{
|
|
149
|
+
"filetree": json.dumps(filetree.to_dict()),
|
|
150
|
+
"file_details": json.dumps(file_details),
|
|
151
|
+
"project_name": "CodeMetrics",
|
|
152
|
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
self.output.writeln("<success>Files page generated successfully</success>")
|
|
156
|
+
|
|
157
|
+
def render_git_analysis_page(self, metrics: ProjectMetrics):
|
|
158
|
+
"""Render the git analysis page with comprehensive git data"""
|
|
159
|
+
if not metrics.git_metrics:
|
|
160
|
+
self.output.writeln("<success>No git metrics to render</success>")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
def stringify_values(obj):
|
|
164
|
+
if isinstance(obj, dict):
|
|
165
|
+
return {key: stringify_values(value) for key, value in obj.items()}
|
|
166
|
+
elif isinstance(obj, list):
|
|
167
|
+
return [stringify_values(item) for item in obj]
|
|
168
|
+
else:
|
|
169
|
+
return str(obj)
|
|
170
|
+
|
|
171
|
+
self.output.writeln("<info>Rendering git analysis page</info>")
|
|
172
|
+
try:
|
|
173
|
+
# Render git analysis template
|
|
174
|
+
self.render_template(
|
|
175
|
+
"git_analysis.html",
|
|
176
|
+
stringify_values(
|
|
177
|
+
{
|
|
178
|
+
"git_analysis": metrics.git_metrics.to_dict(),
|
|
179
|
+
"git_analysis_json": json.dumps(
|
|
180
|
+
metrics.git_metrics.get_contributors_dict(), indent=4
|
|
181
|
+
),
|
|
182
|
+
"git_stats_data": json.dumps(
|
|
183
|
+
metrics.git_metrics.get_commit_stats_per_month(), indent=4
|
|
184
|
+
), # git commit graph
|
|
185
|
+
"git_churn_data": json.dumps(
|
|
186
|
+
metrics.git_metrics.get_churn_per_month(), indent=4
|
|
187
|
+
), # git chrun graph
|
|
188
|
+
"git_silos_data": metrics.git_metrics.get_silos_list()[
|
|
189
|
+
:10
|
|
190
|
+
], # silos list
|
|
191
|
+
"git_contributors": metrics.git_metrics.get_contributors_list()[
|
|
192
|
+
:10
|
|
193
|
+
], # contributors list
|
|
194
|
+
"git_hotspots_data": metrics.git_metrics.get_hotspots_list()[
|
|
195
|
+
:10
|
|
196
|
+
], # hotspots list
|
|
197
|
+
"project_name": "CodeMetrics",
|
|
198
|
+
"last_updated": metrics.git_metrics.get_analysis_start_date(),
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.output.writeln(
|
|
204
|
+
"<success>Git analysis page generated successfully</success>"
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise e
|
|
208
|
+
self.output.writeln(
|
|
209
|
+
f"<error>Error generating git analysis page: {e}</error>"
|
|
210
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from metripy.Report.ReporterInterface import ReporterInterface
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AbstractJsonReporter(ReporterInterface):
|
|
8
|
+
def put_data(self, data: dict) -> None:
|
|
9
|
+
os.makedirs(os.path.dirname(self.config.path), exist_ok=True)
|
|
10
|
+
with open(self.config.path, "w") as file:
|
|
11
|
+
json.dump(data, file, indent=2)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
2
|
+
from metripy.Report.Json.AbstractJsonReporter import AbstractJsonReporter
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GitJsonReporter(AbstractJsonReporter):
|
|
6
|
+
def generate(self, metrics: ProjectMetrics) -> None:
|
|
7
|
+
if not metrics.git_metrics:
|
|
8
|
+
self.output.writeln(
|
|
9
|
+
"<error>Wants git json report, but no git metrics</error>"
|
|
10
|
+
)
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
data = {
|
|
14
|
+
"commits_per_month": metrics.git_metrics.get_commit_stats_per_month(),
|
|
15
|
+
"churn_data": metrics.git_metrics.get_churn_per_month(),
|
|
16
|
+
"possible_silos": metrics.git_metrics.get_silos_list()[:10],
|
|
17
|
+
"top_contributors": metrics.git_metrics.get_contributors_list()[:10],
|
|
18
|
+
"top_hotspots": metrics.git_metrics.get_hotspots_list()[:10],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
self.put_data(data)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from metripy.Application.Config.Config import Config
|
|
2
|
+
from metripy.Component.Output.CliOutput import CliOutput
|
|
3
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JsonReporter:
|
|
7
|
+
def __init__(self, config: Config, output: CliOutput):
|
|
8
|
+
self.config = config
|
|
9
|
+
self.output = output
|
|
10
|
+
|
|
11
|
+
def generate(self, metrics: ProjectMetrics):
|
|
12
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from metripy.Application.Config.ReportConfig import ReportConfig
|
|
2
|
+
from metripy.Component.Output.CliOutput import CliOutput
|
|
3
|
+
from metripy.Report import ReporterInterface
|
|
4
|
+
from metripy.Report.Html.Reporter import Reporter as HtmlReporter
|
|
5
|
+
from metripy.Report.Json.GitJsonReporter import GitJsonReporter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReporterFactory:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def create(config: ReportConfig, output: CliOutput) -> ReporterInterface:
|
|
11
|
+
if config.type == "html":
|
|
12
|
+
return HtmlReporter(config, output)
|
|
13
|
+
elif config.type == "json":
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
elif config.type == "csv":
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
elif config.type == "cli":
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
elif config.type == "json-git":
|
|
20
|
+
return GitJsonReporter(config, output)
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError(f"Unsupported report type: {config.type}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from metripy.Application.Config.ReportConfig import ReportConfig
|
|
4
|
+
from metripy.Component.Output.CliOutput import CliOutput
|
|
5
|
+
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReporterInterface(ABC):
|
|
9
|
+
def __init__(
|
|
10
|
+
self, config: ReportConfig, output: CliOutput, project_name: str = "foobar"
|
|
11
|
+
):
|
|
12
|
+
self.config: ReportConfig = config
|
|
13
|
+
self.output = output
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def generate(self, metrics: ProjectMetrics) -> None:
|
|
17
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from metripy.Tree.FunctionNode import FunctionNode
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ClassNode:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
full_name: str,
|
|
8
|
+
name: str,
|
|
9
|
+
lineno: int,
|
|
10
|
+
col_offset: int,
|
|
11
|
+
real_complexity: int,
|
|
12
|
+
):
|
|
13
|
+
self.full_name = full_name
|
|
14
|
+
self.name = name
|
|
15
|
+
self.lineno = lineno
|
|
16
|
+
self.col_offset = col_offset
|
|
17
|
+
self.real_complexity = real_complexity
|
|
18
|
+
self.functions: list[FunctionNode] = []
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict:
|
|
21
|
+
"""Convert ClassNode to a dictionary for JSON serialization."""
|
|
22
|
+
return {
|
|
23
|
+
"full_name": self.full_name,
|
|
24
|
+
"name": self.name,
|
|
25
|
+
"lineno": self.lineno,
|
|
26
|
+
"col_offset": self.col_offset,
|
|
27
|
+
"real_complexity": self.real_complexity,
|
|
28
|
+
"functions": [func.to_dict() for func in self.functions],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def __dict__(self) -> dict:
|
|
32
|
+
return self.to_dict()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class FunctionNode:
|
|
2
|
+
def __init__(
|
|
3
|
+
self, full_name: str, name: str, lineno: int, col_offset: int, complexity: int
|
|
4
|
+
):
|
|
5
|
+
self.full_name = full_name
|
|
6
|
+
self.name = name
|
|
7
|
+
self.lineno = lineno
|
|
8
|
+
self.line_end = 0
|
|
9
|
+
self.col_offset = col_offset
|
|
10
|
+
self.complexity = complexity
|
|
11
|
+
self.h1 = 0
|
|
12
|
+
self.h2 = 0
|
|
13
|
+
self.N1 = 0
|
|
14
|
+
self.N2 = 0
|
|
15
|
+
self.vocabulary = 0
|
|
16
|
+
self.length = 0
|
|
17
|
+
self.calculated_length = 0
|
|
18
|
+
self.volume = 0
|
|
19
|
+
self.difficulty = 0
|
|
20
|
+
self.effort = 0
|
|
21
|
+
self.time = 0
|
|
22
|
+
self.bugs = 0
|
|
23
|
+
self.maintainability_index = 0
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict:
|
|
26
|
+
"""Convert FunctionNode to a dictionary for JSON serialization."""
|
|
27
|
+
return {
|
|
28
|
+
"full_name": self.full_name,
|
|
29
|
+
"name": self.name,
|
|
30
|
+
"lineno": self.lineno,
|
|
31
|
+
"col_offset": self.col_offset,
|
|
32
|
+
"complexity": self.complexity,
|
|
33
|
+
"h1": self.h1,
|
|
34
|
+
"h2": self.h2,
|
|
35
|
+
"N1": self.N1,
|
|
36
|
+
"N2": self.N2,
|
|
37
|
+
"vocabulary": self.vocabulary,
|
|
38
|
+
"length": self.length,
|
|
39
|
+
"calculated_length": self.calculated_length,
|
|
40
|
+
"volume": self.volume,
|
|
41
|
+
"difficulty": self.difficulty,
|
|
42
|
+
"effort": self.effort,
|
|
43
|
+
"time": self.time,
|
|
44
|
+
"bugs": self.bugs,
|
|
45
|
+
"maintainability_index": self.maintainability_index,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def __dict__(self) -> dict:
|
|
49
|
+
return self.to_dict()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from metripy.Tree.ClassNode import ClassNode
|
|
2
|
+
from metripy.Tree.FunctionNode import FunctionNode
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ModuleNode:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
full_name: str,
|
|
9
|
+
loc: int,
|
|
10
|
+
lloc: int,
|
|
11
|
+
sloc: int,
|
|
12
|
+
comments: int,
|
|
13
|
+
multi: int,
|
|
14
|
+
blank: int,
|
|
15
|
+
single_comments: int,
|
|
16
|
+
):
|
|
17
|
+
self.full_name = full_name
|
|
18
|
+
self.loc = loc
|
|
19
|
+
self.lloc = lloc
|
|
20
|
+
self.sloc = sloc
|
|
21
|
+
self.comments = comments
|
|
22
|
+
self.multi = multi
|
|
23
|
+
self.blank = blank
|
|
24
|
+
self.single_comments = single_comments
|
|
25
|
+
self.maintainability_index = 0
|
|
26
|
+
self.classes: list[ClassNode] = []
|
|
27
|
+
self.functions: list[FunctionNode] = []
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict:
|
|
30
|
+
return {
|
|
31
|
+
"full_name": self.full_name,
|
|
32
|
+
"loc": self.loc,
|
|
33
|
+
"lloc": self.lloc,
|
|
34
|
+
"sloc": self.sloc,
|
|
35
|
+
"comments": self.comments,
|
|
36
|
+
"multi": self.multi,
|
|
37
|
+
"blank": self.blank,
|
|
38
|
+
"single_comments": self.single_comments,
|
|
39
|
+
"maintainability_index": self.maintainability_index,
|
|
40
|
+
"classes": [c.to_dict() for c in self.classes],
|
|
41
|
+
"functions": [f.to_dict() for f in self.functions],
|
|
42
|
+
}
|
metripy/__init__.py
ADDED
|
File without changes
|
metripy/metripy.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""main module"""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from metripy.Application.Application import Application
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
"""cli entry point to application"""
|
|
10
|
+
application = Application()
|
|
11
|
+
application.run(sys.argv)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main()
|