metripy 0.2.7__py3-none-any.whl → 0.3.6__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.

Files changed (67) hide show
  1. metripy/Application/Analyzer.py +23 -3
  2. metripy/Application/Application.py +16 -2
  3. metripy/Application/Config/Config.py +34 -0
  4. metripy/Application/Config/File/ConfigFileReaderFactory.py +6 -5
  5. metripy/Application/Config/File/ConfigFileReaderInterface.py +70 -3
  6. metripy/Application/Config/File/JsonConfigFileReader.py +5 -70
  7. metripy/Application/Config/File/YamlConfigFileReader.py +17 -0
  8. metripy/Application/Config/Parser.py +24 -11
  9. metripy/Application/Config/ProjectConfig.py +64 -0
  10. metripy/Application/Info.py +61 -0
  11. metripy/Dependency/Dependency.py +17 -1
  12. metripy/Dependency/Pip/Pip.py +21 -31
  13. metripy/Dependency/Pip/PyPi.py +1 -0
  14. metripy/Git/GitAnalyzer.py +0 -3
  15. metripy/Import/Json/JsonImporter.py +17 -0
  16. metripy/LangAnalyzer/AbstractLangAnalyzer.py +4 -3
  17. metripy/LangAnalyzer/Php/PhpAnalyzer.py +2 -1
  18. metripy/LangAnalyzer/Python/PythonAnalyzer.py +31 -9
  19. metripy/LangAnalyzer/Python/PythonHalSteadAnalyzer.py +55 -0
  20. metripy/LangAnalyzer/Typescript/TypescriptAnalyzer.py +12 -9
  21. metripy/LangAnalyzer/Typescript/TypescriptAstParser.py +1 -1
  22. metripy/Metric/Code/AggregatedMetrics.py +12 -5
  23. metripy/Metric/Code/FileMetrics.py +32 -1
  24. metripy/Metric/Code/ModuleMetrics.py +5 -5
  25. metripy/Metric/Code/SegmentedMetrics.py +72 -36
  26. metripy/Metric/Code/Segmentor.py +44 -0
  27. metripy/Metric/FileTree/FileTreeParser.py +0 -4
  28. metripy/Metric/Git/GitMetrics.py +1 -1
  29. metripy/Metric/ProjectMetrics.py +29 -0
  30. metripy/Metric/Trend/AggregatedTrendMetric.py +101 -0
  31. metripy/Metric/Trend/ClassTrendMetric.py +20 -0
  32. metripy/Metric/Trend/FileTrendMetric.py +46 -0
  33. metripy/Metric/Trend/FunctionTrendMetric.py +28 -0
  34. metripy/Metric/Trend/SegmentedTrendMetric.py +29 -0
  35. metripy/Report/Html/DependencyPageRenderer.py +21 -0
  36. metripy/Report/Html/FilesPageRenderer.py +28 -0
  37. metripy/Report/Html/GitAnalysisPageRenderer.py +55 -0
  38. metripy/Report/Html/IndexPageRenderer.py +47 -0
  39. metripy/Report/Html/PageRenderer.py +43 -0
  40. metripy/Report/Html/PageRendererFactory.py +37 -0
  41. metripy/Report/Html/Reporter.py +78 -137
  42. metripy/Report/Html/TopOffendersPageRenderer.py +84 -0
  43. metripy/Report/Html/TrendsPageRenderer.py +137 -0
  44. metripy/Report/Json/GitJsonReporter.py +3 -0
  45. metripy/Report/Json/JsonReporter.py +6 -2
  46. metripy/Report/ReporterFactory.py +6 -3
  47. metripy/Tree/ClassNode.py +21 -0
  48. metripy/Tree/FunctionNode.py +66 -1
  49. metripy/Trend/TrendAnalyzer.py +150 -0
  50. metripy/templates/html_report/css/styles.css +1386 -0
  51. metripy/templates/html_report/dependencies.html +411 -0
  52. metripy/templates/html_report/files.html +1080 -0
  53. metripy/templates/html_report/git_analysis.html +325 -0
  54. metripy/templates/html_report/images/logo.svg +31 -0
  55. metripy/templates/html_report/index.html +374 -0
  56. metripy/templates/html_report/js/charts.js +313 -0
  57. metripy/templates/html_report/js/dashboard.js +546 -0
  58. metripy/templates/html_report/js/git_analysis.js +383 -0
  59. metripy/templates/html_report/top_offenders.html +267 -0
  60. metripy/templates/html_report/trends.html +468 -0
  61. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/METADATA +27 -9
  62. metripy-0.3.6.dist-info/RECORD +96 -0
  63. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/licenses/LICENSE +1 -1
  64. metripy-0.2.7.dist-info/RECORD +0 -66
  65. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/WHEEL +0 -0
  66. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/entry_points.txt +0 -0
  67. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,14 @@
1
- import json
2
1
  import os
3
2
  import shutil
3
+ import sys
4
4
  from datetime import datetime
5
+ from pathlib import Path
5
6
 
6
- from py_template_engine import TemplateEngine
7
7
 
8
8
  from metripy.Application.Config.ReportConfig import ReportConfig
9
9
  from metripy.Component.Output.CliOutput import CliOutput
10
- from metripy.Metric.FileTree.FileTreeParser import FileTreeParser
11
10
  from metripy.Metric.ProjectMetrics import ProjectMetrics
11
+ from metripy.Report.Html.PageRendererFactory import PageRendererFactory
12
12
  from metripy.Report.ReporterInterface import ReporterInterface
13
13
 
14
14
 
@@ -18,12 +18,48 @@ class Reporter(ReporterInterface):
18
18
  ):
19
19
  self.config: ReportConfig = config
20
20
  self.output = output
21
- self.template_dir = os.path.join(os.getcwd(), "templates/html_report")
21
+
22
+ # Find templates directory - works both in development and when installed
23
+ template_dir = self._find_template_dir()
24
+ if not template_dir.exists():
25
+ raise FileNotFoundError(
26
+ f"Could not find templates directory. Searched in: "
27
+ f"{template_dir}"
28
+ )
29
+
30
+ self.template_dir = str(template_dir)
31
+ self.project_name = project_name
32
+
33
+ self.page_renderer_factory = PageRendererFactory(
34
+ self.template_dir, self.config.path, self.project_name
35
+ )
22
36
 
23
37
  self.global_template_args = {
24
38
  "project_name": project_name,
25
39
  "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
26
40
  }
41
+
42
+ def _find_template_dir(self) -> Path:
43
+ """Find the templates directory, checking multiple possible locations"""
44
+ package_dir = Path(__file__).parent.parent.parent # metripy package root
45
+
46
+ # List of possible locations to check
47
+ possible_locations = [
48
+ # Development: templates at project root
49
+ package_dir.parent / "templates" / "html_report",
50
+ # Alternative: templates inside metripy package
51
+ package_dir / "templates" / "html_report",
52
+ # System install location
53
+ Path(sys.prefix) / "share" / "metripy" / "templates" / "html_report",
54
+ # Fallback to cwd (for development)
55
+ Path.cwd() / "metripy" / "templates" / "html_report",
56
+ ]
57
+
58
+ for location in possible_locations:
59
+ if location.exists() and (location / "index.html").exists():
60
+ return location
61
+
62
+ return possible_locations[0]
27
63
 
28
64
  def generate(self, metrics: ProjectMetrics):
29
65
 
@@ -53,158 +89,63 @@ class Reporter(ReporterInterface):
53
89
  os.path.join(self.config.path, "css"),
54
90
  dirs_exist_ok=True,
55
91
  )
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
- },
92
+ shutil.copytree(
93
+ os.path.join(self.template_dir, "images"),
94
+ os.path.join(self.config.path, "images"),
95
+ dirs_exist_ok=True,
80
96
  )
81
- self.output.writeln("<success>Done rendering index page</success>")
97
+ # shutil.copytree(os.path.join(self.template_dir, "fonts"), os.path.join(self.config.path, "fonts"), dirs_exist_ok=True)
82
98
 
83
- # Render files page
99
+ # Render main pages
100
+ self.render_index_page(metrics)
84
101
  self.render_files_page(metrics)
85
- # Render git analysis page
102
+ self.render_top_offenders_page(metrics)
86
103
  self.render_git_analysis_page(metrics)
87
-
88
104
  self.render_dependencies_page(metrics)
105
+ self.render_trends_page(metrics)
89
106
 
90
107
  self.output.writeln(
91
108
  f"<success>HTML report generated in {self.config.path} directory</success>"
92
109
  )
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
110
  self.output.writeln(
130
- "<success>Dependencies page generated successfully</success>"
111
+ f"<success>Open HTML report: {self.config.path}/index.html</success>"
131
112
  )
132
113
 
114
+ def render_index_page(self, metrics: ProjectMetrics):
115
+ self.output.writeln("<info>Rendering index page</info>")
116
+ self.page_renderer_factory.create_index_page_renderer().render(metrics)
117
+ self.output.writeln("<success>Done rendering index page</success>")
118
+
133
119
  def render_files_page(self, metrics: ProjectMetrics):
134
120
  """Render the files page with file details and analysis"""
135
121
  self.output.writeln("<info>Rendering files page</info>")
122
+ self.page_renderer_factory.create_files_page_renderer().render(metrics)
123
+ self.output.writeln("<success>Files page generated successfully</success>")
136
124
 
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
- },
125
+ def render_top_offenders_page(self, metrics: ProjectMetrics):
126
+ self.output.writeln("<info>Rendering top offenders page</info>")
127
+ self.page_renderer_factory.create_top_offenders_page_renderer().render(metrics)
128
+ self.output.writeln(
129
+ "<success>Top offenders page generated successfully</success>"
154
130
  )
155
- self.output.writeln("<success>Files page generated successfully</success>")
156
131
 
157
132
  def render_git_analysis_page(self, metrics: ProjectMetrics):
158
133
  """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
134
  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
- )
135
+ self.page_renderer_factory.create_git_analysis_page_renderer().render(metrics)
136
+ self.output.writeln(
137
+ "<success>Git analysis page generated successfully</success>"
138
+ )
202
139
 
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
- )
140
+ def render_dependencies_page(self, metrics: ProjectMetrics):
141
+ """Render the dependencies page with dependency details and stats"""
142
+ self.output.writeln("<info>Rendering dependencies page</info>")
143
+ self.page_renderer_factory.create_dependency_page_renderer().render(metrics)
144
+ self.output.writeln(
145
+ "<success>Dependencies page generated successfully</success>"
146
+ )
147
+
148
+ def render_trends_page(self, metrics: ProjectMetrics):
149
+ self.output.writeln("<info>Rendering trends page</info>")
150
+ self.page_renderer_factory.create_trends_page_renderer().render(metrics)
151
+ self.output.writeln("<success>Trends page generated successfully</success>")
@@ -0,0 +1,84 @@
1
+ from metripy.Metric.Code.Segmentor import Segmentor
2
+ from metripy.Metric.ProjectMetrics import ProjectMetrics
3
+ from metripy.Report.Html.PageRenderer import PageRenderer
4
+
5
+
6
+ class TopOffendersPageRenderer(PageRenderer):
7
+ def __init__(self, template_dir: str, output_dir: str, project_name: str):
8
+ super().__init__(template_dir, output_dir, project_name)
9
+
10
+ def render(self, metrics: ProjectMetrics):
11
+ orderedByTotalCc = sorted(
12
+ metrics.file_metrics, key=lambda x: x.totalCc, reverse=True
13
+ )[:10]
14
+ orderedByMI = sorted(
15
+ metrics.file_metrics, key=lambda x: x.maintainabilityIndex, reverse=False
16
+ )[:10]
17
+ orderedByLoc = sorted(metrics.file_metrics, key=lambda x: x.loc, reverse=True)[
18
+ :10
19
+ ]
20
+
21
+ all_functions: list = []
22
+ for fm in metrics.file_metrics:
23
+ all_functions.extend(fm.function_nodes)
24
+
25
+ functionsOrderedByCc = sorted(
26
+ all_functions, key=lambda x: x.complexity, reverse=True
27
+ )[:10]
28
+ functionsOrderedByMi = sorted(
29
+ all_functions, key=lambda x: x.maintainability_index, reverse=False
30
+ )[:10]
31
+ functionsOrderedByLoc = sorted(
32
+ all_functions, key=lambda x: x.get_loc(), reverse=True
33
+ )[:10]
34
+
35
+ # TODO maintainability index per function, we dont calc yet
36
+
37
+ self.render_template(
38
+ "top_offenders.html",
39
+ {
40
+ "file_loc_offenders": [
41
+ {**e.to_dict(), "status": Segmentor.get_loc_segment(e.loc)}
42
+ for e in orderedByLoc
43
+ ],
44
+ "file_cc_offenders": [
45
+ {
46
+ **e.to_dict(),
47
+ "status": Segmentor.get_complexity_segment(e.totalCc),
48
+ }
49
+ for e in orderedByTotalCc
50
+ ],
51
+ "file_mi_offenders": [
52
+ {
53
+ **e.to_dict(),
54
+ "status": Segmentor.get_maintainability_segment(
55
+ e.maintainabilityIndex
56
+ ),
57
+ }
58
+ for e in orderedByMI
59
+ ],
60
+ "function_size_offenders": [
61
+ {
62
+ **e.to_dict(),
63
+ "status": Segmentor.get_method_size_segment(e.get_loc()),
64
+ }
65
+ for e in functionsOrderedByLoc
66
+ ],
67
+ "function_cc_offenders": [
68
+ {
69
+ **e.to_dict(),
70
+ "status": Segmentor.get_complexity_segment(e.complexity),
71
+ }
72
+ for e in functionsOrderedByCc
73
+ ],
74
+ "function_mi_offenders": [
75
+ {
76
+ **e.to_dict(),
77
+ "status": Segmentor.get_maintainability_segment(
78
+ e.maintainability_index
79
+ ),
80
+ }
81
+ for e in functionsOrderedByMi
82
+ ],
83
+ },
84
+ )
@@ -0,0 +1,137 @@
1
+ from metripy.Metric.Code.FileMetrics import FileMetrics
2
+ from metripy.Metric.ProjectMetrics import ProjectMetrics
3
+ from metripy.Report.Html.PageRenderer import PageRenderer
4
+
5
+
6
+ class TrendsPageRenderer(PageRenderer):
7
+ def __init__(self, template_dir: str, output_dir: str, project_name: str):
8
+ super().__init__(template_dir, output_dir, project_name)
9
+
10
+ def _compile_trend_item(self, file: FileMetrics) -> dict:
11
+ return {
12
+ "name": file.full_name,
13
+ "path": file.full_name,
14
+ "complexity_current": file.totalCc,
15
+ "complexity_prev": round(file.trend.historical_totalCc, 2),
16
+ "complexity_delta": round(file.trend.totalCc_delta, 2),
17
+ "maintainability_current": round(file.maintainabilityIndex, 2),
18
+ "maintainability_prev": round(
19
+ file.trend.historical_maintainabilityIndex, 2
20
+ ),
21
+ "maintainability_delta": round(file.trend.maintainabilityIndex_delta, 2),
22
+ }
23
+
24
+ def render(self, metrics: ProjectMetrics):
25
+ if metrics.total_code_metrics.trend is None:
26
+ self.render_template(
27
+ "trends.html",
28
+ {
29
+ "has_trend_data": False,
30
+ "trend_data": {
31
+ "top_improved_complexity": [],
32
+ "top_improved_maintainability": [],
33
+ "top_worsened_complexity": [],
34
+ "top_worsened_maintainability": [],
35
+ "loc_segments_current": {},
36
+ "loc_segments_prev": {},
37
+ "complexity_segments_current": {},
38
+ "complexity_segments_prev": {},
39
+ "maintainability_segments_current": {},
40
+ "maintainability_segments_prev": {},
41
+ "method_size_segments_current": {},
42
+ "method_size_segments_prev": {},
43
+ },
44
+ },
45
+ )
46
+ return
47
+
48
+ # Top improved complexity (complexity went down - negative delta)
49
+ top_improved_complexity = [
50
+ x
51
+ for x in metrics.file_metrics
52
+ if x.trend is not None and x.trend.totalCc_delta < 0
53
+ ]
54
+ top_improved_complexity = sorted(
55
+ top_improved_complexity, key=lambda x: x.trend.totalCc_delta
56
+ )[:10]
57
+
58
+ # Top worsened complexity (complexity went up - positive delta)
59
+ top_worsened_complexity = [
60
+ x
61
+ for x in metrics.file_metrics
62
+ if x.trend is not None and x.trend.totalCc_delta > 0
63
+ ]
64
+ top_worsened_complexity = sorted(
65
+ top_worsened_complexity, key=lambda x: x.trend.totalCc_delta, reverse=True
66
+ )[:10]
67
+
68
+ # Top improved maintainability (maintainability went up - positive delta)
69
+ top_improved_maintainability = [
70
+ x
71
+ for x in metrics.file_metrics
72
+ if x.trend is not None and round(x.trend.maintainabilityIndex_delta, 2) > 0
73
+ ]
74
+ top_improved_maintainability = sorted(
75
+ top_improved_maintainability,
76
+ key=lambda x: x.trend.maintainabilityIndex_delta,
77
+ reverse=True,
78
+ )[:10]
79
+
80
+ # Top worsened maintainability (maintainability went down - negative delta)
81
+ top_worsened_maintainability = [
82
+ x
83
+ for x in metrics.file_metrics
84
+ if x.trend is not None and round(x.trend.maintainabilityIndex_delta, 2) < 0
85
+ ]
86
+ top_worsened_maintainability = sorted(
87
+ top_worsened_maintainability,
88
+ key=lambda x: x.trend.maintainabilityIndex_delta,
89
+ )[:10]
90
+
91
+ trend_data = {
92
+ # Segment distributions for each metric
93
+ "loc_segments_current": metrics.total_code_metrics.segmentation_data[
94
+ "loc"
95
+ ].to_dict_with_percent(),
96
+ "loc_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
97
+ "loc"
98
+ ].to_dict_with_percent(),
99
+ "complexity_segments_current": metrics.total_code_metrics.segmentation_data[
100
+ "complexity"
101
+ ].to_dict_with_percent(),
102
+ "complexity_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
103
+ "complexity"
104
+ ].to_dict_with_percent(),
105
+ "maintainability_segments_current": metrics.total_code_metrics.segmentation_data[
106
+ "maintainability"
107
+ ].to_dict_with_percent(),
108
+ "maintainability_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
109
+ "maintainability"
110
+ ].to_dict_with_percent(),
111
+ "method_size_segments_current": metrics.total_code_metrics.segmentation_data[
112
+ "methodSize"
113
+ ].to_dict_with_percent(),
114
+ "method_size_segments_prev": metrics.total_code_metrics.trend.historical_segmentation_data[
115
+ "methodSize"
116
+ ].to_dict_with_percent(),
117
+ "top_improved_complexity": [
118
+ self._compile_trend_item(x) for x in top_improved_complexity
119
+ ],
120
+ "top_improved_maintainability": [
121
+ self._compile_trend_item(x) for x in top_improved_maintainability
122
+ ],
123
+ "top_worsened_complexity": [
124
+ self._compile_trend_item(x) for x in top_worsened_complexity
125
+ ],
126
+ "top_worsened_maintainability": [
127
+ self._compile_trend_item(x) for x in top_worsened_maintainability
128
+ ],
129
+ }
130
+
131
+ self.render_template(
132
+ "trends.html",
133
+ {
134
+ "has_trend_data": metrics.total_code_metrics.trend is not None,
135
+ "trend_data": trend_data,
136
+ },
137
+ )
@@ -19,3 +19,6 @@ class GitJsonReporter(AbstractJsonReporter):
19
19
  }
20
20
 
21
21
  self.put_data(data)
22
+ self.output.writeln(
23
+ f"<success>Create git json report in {self.config.path}</success>"
24
+ )
@@ -1,12 +1,16 @@
1
1
  from metripy.Application.Config.Config import Config
2
2
  from metripy.Component.Output.CliOutput import CliOutput
3
3
  from metripy.Metric.ProjectMetrics import ProjectMetrics
4
+ from metripy.Report.Json.AbstractJsonReporter import AbstractJsonReporter
4
5
 
5
6
 
6
- class JsonReporter:
7
+ class JsonReporter(AbstractJsonReporter):
7
8
  def __init__(self, config: Config, output: CliOutput):
8
9
  self.config = config
9
10
  self.output = output
10
11
 
11
12
  def generate(self, metrics: ProjectMetrics):
12
- raise NotImplementedError
13
+ self.put_data(metrics.to_dict())
14
+ self.output.writeln(
15
+ f"<success>Create json report in {self.config.path}</success>"
16
+ )
@@ -3,15 +3,18 @@ from metripy.Component.Output.CliOutput import CliOutput
3
3
  from metripy.Report import ReporterInterface
4
4
  from metripy.Report.Html.Reporter import Reporter as HtmlReporter
5
5
  from metripy.Report.Json.GitJsonReporter import GitJsonReporter
6
+ from metripy.Report.Json.JsonReporter import JsonReporter
6
7
 
7
8
 
8
9
  class ReporterFactory:
9
10
  @staticmethod
10
- def create(config: ReportConfig, output: CliOutput) -> ReporterInterface:
11
+ def create(
12
+ config: ReportConfig, output: CliOutput, project_name: str
13
+ ) -> ReporterInterface:
11
14
  if config.type == "html":
12
- return HtmlReporter(config, output)
15
+ return HtmlReporter(config, output, project_name)
13
16
  elif config.type == "json":
14
- raise NotImplementedError
17
+ return JsonReporter(config, output)
15
18
  elif config.type == "csv":
16
19
  raise NotImplementedError
17
20
  elif config.type == "cli":
metripy/Tree/ClassNode.py CHANGED
@@ -1,3 +1,7 @@
1
+ from typing import Self
2
+
3
+ from metripy.Metric.Code.Segmentor import Segmentor
4
+ from metripy.Metric.Trend.ClassTrendMetric import ClassTrendMetric
1
5
  from metripy.Tree.FunctionNode import FunctionNode
2
6
 
3
7
 
@@ -17,6 +21,8 @@ class ClassNode:
17
21
  self.real_complexity = real_complexity
18
22
  self.functions: list[FunctionNode] = []
19
23
 
24
+ self.trend: ClassTrendMetric | None = None
25
+
20
26
  def to_dict(self) -> dict:
21
27
  """Convert ClassNode to a dictionary for JSON serialization."""
22
28
  return {
@@ -25,8 +31,23 @@ class ClassNode:
25
31
  "lineno": self.lineno,
26
32
  "col_offset": self.col_offset,
27
33
  "real_complexity": self.real_complexity,
34
+ "complexity_segment": Segmentor.get_complexity_segment(
35
+ self.real_complexity
36
+ ),
28
37
  "functions": [func.to_dict() for func in self.functions],
29
38
  }
30
39
 
31
40
  def __dict__(self) -> dict:
32
41
  return self.to_dict()
42
+
43
+ @staticmethod
44
+ def from_dict(data: dict) -> Self:
45
+ node = ClassNode(
46
+ full_name=data["full_name"],
47
+ name=data["name"],
48
+ lineno=data["lineno"],
49
+ col_offset=data["col_offset"],
50
+ real_complexity=data["real_complexity"],
51
+ )
52
+ node.functions = [FunctionNode.from_dict(d) for d in data["functions"]]
53
+ return node
@@ -1,3 +1,10 @@
1
+ import math
2
+ from typing import Self
3
+
4
+ from metripy.Metric.Code.Segmentor import Segmentor
5
+ from metripy.Metric.Trend.FunctionTrendMetric import FunctionTrendMetric
6
+
7
+
1
8
  class FunctionNode:
2
9
  def __init__(
3
10
  self, full_name: str, name: str, lineno: int, col_offset: int, complexity: int
@@ -21,6 +28,35 @@ class FunctionNode:
21
28
  self.time = 0
22
29
  self.bugs = 0
23
30
  self.maintainability_index = 0
31
+ self.trend: FunctionTrendMetric | None = None
32
+
33
+ def get_loc(self) -> int:
34
+ return self.line_end - self.lineno
35
+
36
+ def calc_mi(self):
37
+
38
+ total_volume = self.volume
39
+ total_complexity = self.complexity
40
+ total_length = self.length
41
+
42
+ if total_volume == 0 or total_length == 0:
43
+ return 100.0
44
+
45
+ # PHP maintainability index calculation
46
+ mi_base = max(
47
+ (
48
+ 171
49
+ - 5.2 * math.log(total_volume)
50
+ - 0.23 * total_complexity
51
+ - 16.2 * math.log(total_length)
52
+ )
53
+ * 100
54
+ / 171,
55
+ 0,
56
+ )
57
+
58
+ # no comment weight
59
+ self.maintainability_index = mi_base
24
60
 
25
61
  def to_dict(self) -> dict:
26
62
  """Convert FunctionNode to a dictionary for JSON serialization."""
@@ -28,8 +64,12 @@ class FunctionNode:
28
64
  "full_name": self.full_name,
29
65
  "name": self.name,
30
66
  "lineno": self.lineno,
67
+ "line_end": self.line_end,
68
+ "loc": self.get_loc(),
69
+ "loc_segment": Segmentor.get_loc_segment(self.get_loc()),
31
70
  "col_offset": self.col_offset,
32
71
  "complexity": self.complexity,
72
+ "complexity_segment": Segmentor.get_complexity_segment(self.complexity),
33
73
  "h1": self.h1,
34
74
  "h2": self.h2,
35
75
  "N1": self.N1,
@@ -42,8 +82,33 @@ class FunctionNode:
42
82
  "effort": self.effort,
43
83
  "time": self.time,
44
84
  "bugs": self.bugs,
45
- "maintainability_index": self.maintainability_index,
85
+ "maintainability_index": round(self.maintainability_index, 2),
86
+ "maintainability_segment": Segmentor.get_maintainability_segment(
87
+ self.maintainability_index
88
+ ),
46
89
  }
47
90
 
48
91
  def __dict__(self) -> dict:
49
92
  return self.to_dict()
93
+
94
+ @staticmethod
95
+ def from_dict(data: dict) -> Self:
96
+ node = FunctionNode(
97
+ full_name=data["full_name"],
98
+ name=data["name"],
99
+ lineno=data["lineno"],
100
+ col_offset=data["col_offset"],
101
+ complexity=data["complexity"],
102
+ )
103
+ node.line_end = data["line_end"]
104
+ node.vocabulary = data["vocabulary"]
105
+ node.length = data["length"]
106
+ node.calculated_length = data["calculated_length"]
107
+ node.volume = data["volume"]
108
+ node.difficulty = data["difficulty"]
109
+ node.effort = data["effort"]
110
+ node.time = data["time"]
111
+ node.bugs = data["bugs"]
112
+ node.maintainability_index = data["maintainability_index"]
113
+
114
+ return node