metripy 0.2.8__py3-none-any.whl → 0.3.0__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.
Files changed (45) hide show
  1. metripy/Application/Analyzer.py +23 -3
  2. metripy/Application/Application.py +16 -1
  3. metripy/Application/Config/Config.py +33 -0
  4. metripy/Application/Config/File/ConfigFileReaderFactory.py +4 -4
  5. metripy/Application/Config/File/JsonConfigFileReader.py +7 -2
  6. metripy/Application/Config/Parser.py +26 -11
  7. metripy/Application/Config/ProjectConfig.py +63 -0
  8. metripy/Application/Info.py +29 -0
  9. metripy/Dependency/Dependency.py +2 -1
  10. metripy/Dependency/Pip/Pip.py +1 -2
  11. metripy/Dependency/Pip/PyPi.py +1 -0
  12. metripy/Git/GitAnalyzer.py +0 -3
  13. metripy/Import/Json/JsonImporter.py +17 -0
  14. metripy/LangAnalyzer/AbstractLangAnalyzer.py +4 -3
  15. metripy/LangAnalyzer/Php/PhpAnalyzer.py +2 -1
  16. metripy/LangAnalyzer/Python/PythonAnalyzer.py +31 -9
  17. metripy/LangAnalyzer/Python/PythonHalSteadAnalyzer.py +55 -0
  18. metripy/LangAnalyzer/Typescript/TypescriptAnalyzer.py +12 -9
  19. metripy/LangAnalyzer/Typescript/TypescriptAstParser.py +1 -1
  20. metripy/Metric/Code/AggregatedMetrics.py +12 -5
  21. metripy/Metric/Code/FileMetrics.py +32 -1
  22. metripy/Metric/Code/ModuleMetrics.py +5 -5
  23. metripy/Metric/Code/SegmentedMetrics.py +72 -36
  24. metripy/Metric/Code/Segmentor.py +42 -0
  25. metripy/Metric/FileTree/FileTreeParser.py +0 -4
  26. metripy/Metric/Git/GitMetrics.py +1 -1
  27. metripy/Metric/ProjectMetrics.py +17 -2
  28. metripy/Metric/Trend/AggregatedTrendMetric.py +101 -0
  29. metripy/Metric/Trend/ClassTrendMetric.py +20 -0
  30. metripy/Metric/Trend/FileTrendMetric.py +46 -0
  31. metripy/Metric/Trend/FunctionTrendMetric.py +28 -0
  32. metripy/Metric/Trend/SegmentedTrendMetric.py +29 -0
  33. metripy/Report/Html/Reporter.py +247 -28
  34. metripy/Report/Json/GitJsonReporter.py +3 -1
  35. metripy/Report/Json/JsonReporter.py +4 -1
  36. metripy/Tree/ClassNode.py +21 -0
  37. metripy/Tree/FunctionNode.py +66 -1
  38. metripy/Trend/TrendAnalyzer.py +150 -0
  39. {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/METADATA +3 -3
  40. metripy-0.3.0.dist-info/RECORD +76 -0
  41. metripy-0.2.8.dist-info/RECORD +0 -66
  42. {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/WHEEL +0 -0
  43. {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/entry_points.txt +0 -0
  44. {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/licenses/LICENSE +0 -0
  45. {metripy-0.2.8.dist-info → metripy-0.3.0.dist-info}/top_level.txt +0 -0
@@ -7,14 +7,15 @@ from metripy.Dependency.Dependency import Dependency
7
7
  from metripy.Dependency.Npm.Npm import Npm
8
8
  from metripy.Dependency.Pip.Pip import Pip
9
9
  from metripy.Git.GitAnalyzer import GitAnalyzer
10
+ from metripy.Import.Json.JsonImporter import JsonImporter
10
11
  from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
11
12
  from metripy.LangAnalyzer.Php.PhpAnalyzer import PhpAnalyzer
12
13
  from metripy.LangAnalyzer.Python.PythonAnalyzer import PythonAnalyzer
13
- from metripy.LangAnalyzer.Typescript.TypescriptAnalyzer import \
14
- TypescriptAnalyzer
14
+ from metripy.LangAnalyzer.Typescript.TypescriptAnalyzer import TypescriptAnalyzer
15
15
  from metripy.Metric.Code.FileMetrics import FileMetrics
16
16
  from metripy.Metric.Git.GitMetrics import GitMetrics
17
17
  from metripy.Metric.ProjectMetrics import ProjectMetrics
18
+ from metripy.Trend.TrendAnalyzer import TrendAnalyzer
18
19
 
19
20
 
20
21
  class Analyzer:
@@ -85,6 +86,18 @@ class Analyzer:
85
86
 
86
87
  return dependencies
87
88
 
89
+ def add_trends(self, project_metrics: ProjectMetrics):
90
+ self.output.writeln("<info>Analyzing trends...</info>")
91
+ importer = JsonImporter(self.output)
92
+ historical_project_metrics = importer.import_data(self.config.history_path)
93
+ TrendAnalyzer().add_historical_file_trends(
94
+ project_metrics.file_metrics, historical_project_metrics.file_metrics
95
+ )
96
+ TrendAnalyzer().add_historical_project_trends(
97
+ project_metrics, historical_project_metrics
98
+ )
99
+ self.output.writeln("<success>Trends analyzed</success>")
100
+
88
101
  def run(self, files: list[str]) -> ProjectMetrics:
89
102
  git_stats = None
90
103
  if self.config.git:
@@ -103,4 +116,11 @@ class Analyzer:
103
116
  elif self.config.npm:
104
117
  packages = self.analyze_npm()
105
118
 
106
- return ProjectMetrics(file_metrics, git_stats, packages)
119
+ if not self.config.history_path:
120
+ return ProjectMetrics(file_metrics, git_stats, packages)
121
+
122
+ # analyze trends
123
+ project_metrics = ProjectMetrics(file_metrics, git_stats, packages)
124
+ self.add_trends(project_metrics)
125
+
126
+ return project_metrics
@@ -8,15 +8,30 @@ from metripy.Component.Output.CliOutput import CliOutput
8
8
  from metripy.Report.ReporterFactory import ReporterFactory
9
9
  from metripy.Report.ReporterInterface import ReporterInterface
10
10
 
11
+ from metripy.Application.Info import Info
12
+
11
13
 
12
14
  class Application:
13
15
  def run(self, argv) -> None:
14
16
  output = CliOutput()
15
17
 
16
18
  # issues and debug
17
- debugger = Debugger(output).enable()
19
+ debugger = Debugger(output)
18
20
 
19
21
  config = Parser().parse(argv)
22
+ if config.debug:
23
+ debugger.enable()
24
+
25
+ if config.version:
26
+ output.writeln(Info().get_version_info())
27
+ return
28
+
29
+ if config.help:
30
+ output.writeln(Info().get_help())
31
+ return
32
+
33
+ if config.quiet:
34
+ output.set_quiet(True)
20
35
 
21
36
  finder = Finder()
22
37
  files = finder.fetch(config.project_configs)
@@ -4,6 +4,10 @@ from metripy.Application.Config.ProjectConfig import ProjectConfig
4
4
  class Config:
5
5
  def __init__(self):
6
6
  self.project_configs: list[ProjectConfig] = []
7
+ self.quiet: bool = False
8
+ self.version: bool = False
9
+ self.help: bool = False
10
+ self.debug: bool = False
7
11
 
8
12
  def to_dict(self) -> dict:
9
13
  return {
@@ -11,3 +15,32 @@ class Config:
11
15
  project_config.to_dict() for project_config in self.project_configs
12
16
  ],
13
17
  }
18
+
19
+ def set(self, param: str, value: any) -> None:
20
+ print(f"Setting {param} to {value}")
21
+ if param == "quiet":
22
+ self.quiet = value
23
+ elif param == "version":
24
+ self.version = value
25
+ elif param == "help":
26
+ self.help = value
27
+ elif param == "debug":
28
+ self.debug = value
29
+ elif param.startswith("configs."):
30
+ self._set_project_value(param[len("configs."):], value)
31
+ else:
32
+ # ignore unknown parameters
33
+ return
34
+
35
+ def _set_project_value(self, param: str, value: any) -> None:
36
+ keys = param.split(".")
37
+ project_name = keys[0]
38
+ project_config = next((pc for pc in self.project_configs if pc.name == project_name), None)
39
+ if not project_config:
40
+ project_config = ProjectConfig(project_name)
41
+ self.project_configs.append(project_config)
42
+ if len(keys) > 1:
43
+ project_config.set(keys[1:], value)
44
+ else:
45
+ # weird but okay
46
+ return
@@ -1,10 +1,10 @@
1
1
  import os
2
2
  import pathlib
3
3
 
4
- from metripy.Application.Config.File.ConfigFileReaderInterface import \
5
- ConfigFileReaderInterface
6
- from metripy.Application.Config.File.JsonConfigFileReader import \
7
- JsonConfigFileReader
4
+ from metripy.Application.Config.File.ConfigFileReaderInterface import (
5
+ ConfigFileReaderInterface,
6
+ )
7
+ from metripy.Application.Config.File.JsonConfigFileReader import JsonConfigFileReader
8
8
 
9
9
 
10
10
  class ConfigFileReaderFactory:
@@ -2,8 +2,9 @@ import json
2
2
  import os
3
3
 
4
4
  from metripy.Application.Config.Config import Config
5
- from metripy.Application.Config.File.ConfigFileReaderInterface import \
6
- ConfigFileReaderInterface
5
+ from metripy.Application.Config.File.ConfigFileReaderInterface import (
6
+ ConfigFileReaderInterface,
7
+ )
7
8
  from metripy.Application.Config.GitConfig import GitConfig
8
9
  from metripy.Application.Config.ProjectConfig import ProjectConfig
9
10
  from metripy.Application.Config.ReportConfig import ReportConfig
@@ -79,4 +80,8 @@ class JsonConfigFileReader(ConfigFileReaderInterface):
79
80
  if npm := json_data.get("npm"):
80
81
  project_config.npm = npm
81
82
 
83
+ # trends
84
+ if history_path := json_data.get("trends"):
85
+ project_config.history_path = self.resolve_path(history_path)
86
+
82
87
  return project_config
@@ -1,21 +1,24 @@
1
1
  import re
2
2
 
3
3
  from metripy.Application.Config.Config import Config
4
- from metripy.Application.Config.File.ConfigFileReaderFactory import \
5
- ConfigFileReaderFactory
4
+ from metripy.Application.Config.File.ConfigFileReaderFactory import (
5
+ ConfigFileReaderFactory,
6
+ )
6
7
 
7
8
 
8
9
  class Parser:
9
10
  def parse(self, argv: list[str]) -> Config:
10
11
  config = Config()
12
+ print(argv)
11
13
 
12
- if argv[0] == "metripy.py" or argv[0] == "metripy":
13
- # TODO, fix when path ends with metripy.py
14
- pass
15
- argv.pop(0)
14
+ if argv[0].endswith("metripy.py") or argv[0].endswith("metripy"):
15
+ argv.pop(0)
16
16
 
17
17
  # check for a config file
18
- for key, arg in enumerate(argv):
18
+ key = 0
19
+ while key < len(argv):
20
+ arg = argv[key]
21
+ print(f"Key: {key} Arg: '{arg}'")
19
22
  if matches := re.search(r"^--config=(.+)$", arg):
20
23
  fileReader = ConfigFileReaderFactory.createFromFileName(
21
24
  matches.group(1)
@@ -23,9 +26,21 @@ class Parser:
23
26
  fileReader.read(config)
24
27
  argv.pop(key)
25
28
 
26
- # TODO: add the following
27
- # arguments with options
28
- # arguments without options
29
- # last argument
29
+ # arguments with options
30
+ elif matches := re.search(r"^--([\w]+(?:\.[\w]+)*)=(.*)$", arg):
31
+ param = matches.group(1)
32
+ value = matches.group(2)
33
+ config.set(param, value)
34
+ argv.pop(key)
35
+
36
+ # arguments without options
37
+ elif matches := re.search(r"^--([\w]+(?:\.[\w]+)*)$", arg):
38
+ param = matches.group(1)
39
+ config.set(param, True)
40
+ argv.pop(key)
41
+ else:
42
+ key += 1
43
+
44
+ # TODO handle remaining arguments
30
45
 
31
46
  return config
@@ -14,6 +14,7 @@ class ProjectConfig:
14
14
  self.pip: bool = False
15
15
  self.npm: bool = False
16
16
  self.reports: list[ReportConfig] = []
17
+ self.history_path: str | None = None
17
18
 
18
19
  def to_dict(self) -> dict:
19
20
  return {
@@ -22,6 +23,68 @@ class ProjectConfig:
22
23
  "includes": self.includes,
23
24
  "excludes": self.excludes,
24
25
  "extensions": self.extensions,
26
+ "composer": self.composer,
27
+ "pip": self.pip,
28
+ "npm": self.npm,
25
29
  "git": self.git.to_dict() if self.git else None,
26
30
  "reports": [report.to_dict() for report in self.reports],
31
+ "history_path": self.history_path,
27
32
  }
33
+
34
+ @staticmethod
35
+ def str_to_bool(value):
36
+ if isinstance(value, bool):
37
+ return value
38
+ return str(value).lower() in ("true", "1", "yes")
39
+
40
+ def set(self, keys: list[str], value: any) -> None:
41
+ if len(keys) == 0:
42
+ return
43
+ primary_key = keys[0]
44
+ print(f"Setting primary {primary_key} to {value}")
45
+ # single value
46
+ if primary_key == "base_path":
47
+ self.base_path = value
48
+ elif primary_key == "pip":
49
+ self.pip = self.str_to_bool(value)
50
+ elif primary_key == "npm":
51
+ self.npm = self.str_to_bool(value)
52
+ elif primary_key == "composer":
53
+ self.composer = self.str_to_bool(value)
54
+ elif primary_key == "trends":
55
+ self.history_path = value
56
+ elif primary_key == "git":
57
+ self.git = GitConfig()
58
+ self.git.repo = self.base_path
59
+ self.git.branch = value
60
+
61
+ # list values
62
+ elif primary_key == "includes":
63
+ if value == "":
64
+ self.includes = []
65
+ else:
66
+ self.includes.append(value)
67
+ elif primary_key == "excludes":
68
+ if value == "":
69
+ self.excludes = []
70
+ else:
71
+ self.excludes.append(value)
72
+ elif primary_key == "extensions":
73
+ if value == "":
74
+ self.extensions = []
75
+ else:
76
+ self.extensions.append(value)
77
+
78
+ # dict values
79
+ elif primary_key == "reports":
80
+ if len(keys) == 1:
81
+ return
82
+ report_type = keys[1]
83
+ report_path = value
84
+ if value != "":
85
+ self.reports.append(ReportConfig(report_type, report_path))
86
+ else:
87
+ report_config = next((rc for rc in self.reports if rc.type == report_type), None)
88
+ if not report_config:
89
+ return
90
+ self.reports.remove(report_config)
@@ -0,0 +1,29 @@
1
+ import toml
2
+
3
+ class Info:
4
+ def __init__(self):
5
+ data = self._get_data()
6
+ self.version = data["project"]["version"]
7
+ self.url = data["project"]["urls"]["Homepage"]
8
+
9
+ def _get_data(self) -> dict:
10
+ with open("pyproject.toml", "r") as file:
11
+ data = toml.load(file)
12
+ return data
13
+
14
+ def get_version_info(self) -> str:
15
+ return f"""
16
+ Metripy {self.version}
17
+ {self.url}
18
+ """
19
+
20
+ def get_help(self) -> str:
21
+ return self.get_version_info() + f"""
22
+ Usage: metripy [options]
23
+ Options:
24
+ --config=<file> Use a custom config file
25
+ --version Show the version and exit
26
+ --help Show this help message and exit
27
+ --debug Enable debug mode
28
+ --quiet Disable output
29
+ """
@@ -1,7 +1,8 @@
1
1
  from typing import Self
2
2
 
3
+
3
4
  class Dependency:
4
- def __init__(self, name: str, version: str|None):
5
+ def __init__(self, name: str, version: str | None):
5
6
  self.name = name
6
7
  self.version = version
7
8
  self.latest: str = ""
@@ -1,5 +1,4 @@
1
1
  import os
2
- import re
3
2
 
4
3
  import toml
5
4
 
@@ -39,7 +38,7 @@ class Pip:
39
38
  return self._parse_dependencies(deps)
40
39
 
41
40
  return []
42
-
41
+
43
42
  def _parse_dependencies(self, lines: list[str]) -> list[Dependency]:
44
43
  dependencies = []
45
44
  for dep in lines:
@@ -16,6 +16,7 @@ class PyPi:
16
16
  print(f"Package '{dependency.name}' has no info section")
17
17
  return dependency
18
18
 
19
+ dependency.type = "pip"
19
20
  dependency.description = info.get("summary")
20
21
  dependency.repository = info.get("project_url") or info.get("home_page")
21
22
  if info.get("license"):
@@ -9,8 +9,6 @@ from metripy.Metric.Git.GitMetrics import GitMetrics
9
9
 
10
10
  class GitAnalyzer:
11
11
  def __init__(self, git_config: GitConfig):
12
- print(git_config.repo)
13
- print(git_config.branch)
14
12
  self.repo = Repo(git_config.repo)
15
13
  self.branch_name = git_config.branch
16
14
 
@@ -22,7 +20,6 @@ class GitAnalyzer:
22
20
  first_of_month_last_year = datetime(now.year - 1, now.month, 1)
23
21
  # first_of_month_last_year = datetime(now.year, now.month, 1)
24
22
  after_date = first_of_month_last_year.strftime("%Y-%m-%d")
25
- print(f"analyzing from {after_date}")
26
23
 
27
24
  return self.get_metrics(after_date)
28
25
 
@@ -0,0 +1,17 @@
1
+ import json
2
+
3
+ from metripy.Component.Output.CliOutput import CliOutput
4
+ from metripy.Metric.ProjectMetrics import ProjectMetrics
5
+
6
+
7
+ class JsonImporter:
8
+ def __init__(self, output: CliOutput):
9
+ self.output = output
10
+
11
+ def import_data(self, path: str) -> ProjectMetrics:
12
+ self.output.writeln(f"<info>Importing data from {path}...</info>")
13
+ with open(path, "r") as file:
14
+ data = json.load(file)
15
+ project_metrics = ProjectMetrics.from_dict(data)
16
+ self.output.writeln("<success>Data imported successfuly</success>")
17
+ return project_metrics
@@ -40,13 +40,13 @@ class AbstractLangAnalyzer(ABC):
40
40
  full_name = module.full_name
41
41
 
42
42
  if len(module.functions) > 0:
43
- avgCcPerFunction = sum(
44
- function.complexity for function in module.functions
45
- ) / len(module.functions)
43
+ totalCc = sum(function.complexity for function in module.functions)
44
+ avgCcPerFunction = totalCc / len(module.functions)
46
45
  avgLocPerFunction = (
47
46
  module.lloc - module.comments - len(module.functions)
48
47
  ) / len(module.functions)
49
48
  else:
49
+ totalCc = 0
50
50
  avgCcPerFunction = 0
51
51
  avgLocPerFunction = 0
52
52
  maintainabilityIndex = module.maintainability_index
@@ -54,6 +54,7 @@ class AbstractLangAnalyzer(ABC):
54
54
  file_metric = FileMetrics(
55
55
  full_name=full_name,
56
56
  loc=module.loc,
57
+ totalCc=totalCc,
57
58
  avgCcPerFunction=avgCcPerFunction,
58
59
  maintainabilityIndex=maintainabilityIndex,
59
60
  avgLocPerFunction=avgLocPerFunction,
@@ -131,7 +131,7 @@ class PhpAnalyzer(AbstractLangAnalyzer):
131
131
 
132
132
  code_lines = code.split("\n")
133
133
  for func_name, function_node in functions.items():
134
- lines = code_lines[function_node.lineno:function_node.line_end]
134
+ lines = code_lines[function_node.lineno : function_node.line_end]
135
135
  function_metrics = self.halstead_analyzer.calculate_halstead_metrics(
136
136
  "\n".join(lines)
137
137
  )
@@ -147,6 +147,7 @@ class PhpAnalyzer(AbstractLangAnalyzer):
147
147
  function_node.calculated_length = function_metrics["calculated_length"]
148
148
  function_node.bugs = function_metrics["bugs"]
149
149
  function_node.time = function_metrics["time"]
150
+ function_node.calc_mi()
150
151
 
151
152
  maintainability_index = self._calculate_maintainability_index(
152
153
  functions.values(), module_node
@@ -5,6 +5,7 @@ from radon.visitors import Class, Function
5
5
 
6
6
  from metripy.Component.Output.ProgressBar import ProgressBar
7
7
  from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
8
+ from metripy.LangAnalyzer.Python.PythonHalSteadAnalyzer import PythonHalSteadAnalyzer
8
9
  from metripy.Metric.Code.FileMetrics import FileMetrics
9
10
  from metripy.Tree.ClassNode import ClassNode
10
11
  from metripy.Tree.FunctionNode import FunctionNode
@@ -15,6 +16,7 @@ class PythonAnalyzer(AbstractLangAnalyzer):
15
16
 
16
17
  def __init__(self):
17
18
  super().__init__()
19
+ self.fallback_halstead_analyzer = PythonHalSteadAnalyzer()
18
20
 
19
21
  def get_lang_name(self) -> str:
20
22
  return "Python"
@@ -62,6 +64,7 @@ class PythonAnalyzer(AbstractLangAnalyzer):
62
64
  function_node = FunctionNode(
63
65
  full_name, item.name, item.lineno, item.col_offset, item.complexity
64
66
  )
67
+ function_node.line_end = item.endline
65
68
  if item.is_method:
66
69
  class_node = classes.get(full_class_name)
67
70
  if class_node is not None:
@@ -74,11 +77,6 @@ class PythonAnalyzer(AbstractLangAnalyzer):
74
77
  else:
75
78
  raise ValueError(f"Unknown item type: {type(item)}")
76
79
 
77
- # print("--------------------------------")
78
- # print(json.dumps([c.__dict__() for c in classes.values()], indent=4))
79
- # print("--------------------------------")
80
- # print(json.dumps([f.__dict__() for f in functions.values()], indent=4))
81
- # exit()
82
80
  module = analyze(code)
83
81
  full_name = self.full_name(filename)
84
82
  module_node = ModuleNode(
@@ -93,12 +91,9 @@ class PythonAnalyzer(AbstractLangAnalyzer):
93
91
  )
94
92
  module_node.classes.extend(classes.values())
95
93
  module_node.functions.extend(functions.values())
96
- # print(module)
97
- # print(json.dumps([m.to_dict() for m in modules.values()], indent=4))
98
- # exit()
94
+
99
95
  h = h_visit(code)
100
96
  assert isinstance(h, Halstead)
101
- # print(h.total)
102
97
  function_name: str
103
98
  report: HalsteadReport
104
99
  for function_name, report in h.functions:
@@ -117,9 +112,36 @@ class PythonAnalyzer(AbstractLangAnalyzer):
117
112
  function_node.effort = report.effort
118
113
  function_node.bugs = report.bugs
119
114
  function_node.time = report.time
115
+ function_node.calc_mi()
120
116
  else:
121
117
  raise ValueError(f"Function node not found for function {full_name}")
122
118
 
119
+ code_lines = code.split("\n")
120
+ for func_name, function_node in functions.items():
121
+ if function_node.maintainability_index != 0:
122
+ continue
123
+ # if MI is 0, we want to take another look, radon does not like boring functions
124
+
125
+ lines = code_lines[function_node.lineno:function_node.line_end]
126
+ function_metrics = (
127
+ self.fallback_halstead_analyzer.calculate_halstead_metrics(
128
+ "\n".join(lines)
129
+ )
130
+ )
131
+ function_node.h1 = function_metrics["n1"]
132
+ function_node.h2 = function_metrics["n2"]
133
+ function_node.N1 = function_metrics["N1"]
134
+ function_node.N2 = function_metrics["N2"]
135
+ function_node.vocabulary = function_metrics["vocabulary"]
136
+ function_node.length = function_metrics["length"]
137
+ function_node.volume = function_metrics["volume"]
138
+ function_node.difficulty = function_metrics["difficulty"]
139
+ function_node.effort = function_metrics["effort"]
140
+ function_node.calculated_length = function_metrics["calculated_length"]
141
+ function_node.bugs = function_metrics["bugs"]
142
+ function_node.time = function_metrics["time"]
143
+ function_node.calc_mi()
144
+
123
145
  maintainability_index = mi_visit(code, True)
124
146
  module_node.maintainability_index = maintainability_index
125
147
 
@@ -0,0 +1,55 @@
1
+ from metripy.LangAnalyzer.Generic.HalSteadAnalyzer import HalSteadAnalyzer
2
+
3
+
4
+ class PythonHalSteadAnalyzer:
5
+ def __init__(self):
6
+ self.operators = [
7
+ "+",
8
+ "-",
9
+ "*",
10
+ "/",
11
+ "//",
12
+ "%",
13
+ "**",
14
+ "==",
15
+ "!=",
16
+ ">",
17
+ "<",
18
+ ">=",
19
+ "<=",
20
+ "=",
21
+ "+=",
22
+ "-=",
23
+ "*=",
24
+ "/=",
25
+ "%=",
26
+ "//=",
27
+ "**=",
28
+ "and",
29
+ "or",
30
+ "not",
31
+ "&",
32
+ "|",
33
+ "^",
34
+ "~",
35
+ "<<",
36
+ ">>",
37
+ "in",
38
+ "not in",
39
+ "is",
40
+ "is not",
41
+ ":",
42
+ ",",
43
+ ".",
44
+ "(",
45
+ ")",
46
+ "[",
47
+ "]",
48
+ "{",
49
+ "}",
50
+ ]
51
+
52
+ self.analyzer = HalSteadAnalyzer(self.operators)
53
+
54
+ def calculate_halstead_metrics(self, code: str):
55
+ return self.analyzer.calculate_halstead_metrics(code)
@@ -4,14 +4,16 @@ import lizard
4
4
 
5
5
  from metripy.Component.Output.ProgressBar import ProgressBar
6
6
  from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
7
- from metripy.LangAnalyzer.Typescript.TypescriptAstParser import \
8
- TypescriptAstParser
9
- from metripy.LangAnalyzer.Typescript.TypescriptBasicComplexityAnalyzer import \
10
- TypescriptBasicComplexityAnalzyer
11
- from metripy.LangAnalyzer.Typescript.TypescriptBasicLocAnalyzer import \
12
- TypescriptBasicLocAnalyzer
13
- from metripy.LangAnalyzer.Typescript.TypescriptHalSteadAnalyzer import \
14
- TypeScriptHalSteadAnalyzer
7
+ from metripy.LangAnalyzer.Typescript.TypescriptAstParser import TypescriptAstParser
8
+ from metripy.LangAnalyzer.Typescript.TypescriptBasicComplexityAnalyzer import (
9
+ TypescriptBasicComplexityAnalzyer,
10
+ )
11
+ from metripy.LangAnalyzer.Typescript.TypescriptBasicLocAnalyzer import (
12
+ TypescriptBasicLocAnalyzer,
13
+ )
14
+ from metripy.LangAnalyzer.Typescript.TypescriptHalSteadAnalyzer import (
15
+ TypeScriptHalSteadAnalyzer,
16
+ )
15
17
  from metripy.Tree.ClassNode import ClassNode
16
18
  from metripy.Tree.FunctionNode import FunctionNode
17
19
  from metripy.Tree.ModuleNode import ModuleNode
@@ -146,7 +148,7 @@ class TypescriptAnalyzer(AbstractLangAnalyzer):
146
148
 
147
149
  code_lines = code.split("\n")
148
150
  for func_name, function_node in functions.items():
149
- lines = code_lines[function_node.lineno:function_node.line_end]
151
+ lines = code_lines[function_node.lineno : function_node.line_end]
150
152
  function_metrics = self.halstead_analyzer.calculate_halstead_metrics(
151
153
  "\n".join(lines)
152
154
  )
@@ -162,6 +164,7 @@ class TypescriptAnalyzer(AbstractLangAnalyzer):
162
164
  function_node.calculated_length = function_metrics["calculated_length"]
163
165
  function_node.bugs = function_metrics["bugs"]
164
166
  function_node.time = function_metrics["time"]
167
+ function_node.calc_mi()
165
168
 
166
169
  maintainability_index = self._calculate_maintainability_index(
167
170
  functions.values(), module_node
@@ -8,7 +8,7 @@ class TypescriptAstParser:
8
8
  self.parser = get_parser("typescript")
9
9
 
10
10
  def _get_node_text(self, code: str, node) -> str:
11
- return code[node.start_byte:node.end_byte].decode("utf-8")
11
+ return code[node.start_byte : node.end_byte].decode("utf-8")
12
12
 
13
13
  def extract_structure(self, code: str) -> dict:
14
14
  tree = self.parser.parse(bytes(code, "utf8"))
@@ -1,4 +1,5 @@
1
1
  from metripy.Metric.Code.SegmentedMetrics import SegmentedMetrics
2
+ from metripy.Metric.Trend.AggregatedTrendMetric import AggregatedTrendMetric
2
3
 
3
4
 
4
5
  class AggregatedMetrics:
@@ -29,13 +30,19 @@ class AggregatedMetrics:
29
30
  "methodSize": segmented_method_size,
30
31
  }
31
32
 
33
+ self.trend: AggregatedTrendMetric | None = None
34
+
32
35
  def to_dict(self) -> dict:
33
36
  return {
34
- "loc": str(self.loc),
35
- "avgCcPerFunction": f"{self.avgCcPerFunction:.2f}",
36
- "maintainabilityIndex": f"{self.maintainabilityIndex:.2f}",
37
- "avgLocPerFunction": f"{self.avgLocPerFunction:.2f}",
38
- "num_files": str(self.num_files),
37
+ "loc": self.loc,
38
+ "avgCcPerFunction": round(self.avgCcPerFunction, 2),
39
+ "maintainabilityIndex": round(self.maintainabilityIndex, 2),
40
+ "avgLocPerFunction": round(self.avgLocPerFunction, 2),
41
+ "num_files": self.num_files,
42
+ "trend": self.trend.to_dict() if self.trend else None,
43
+ "trend_segmentation": (
44
+ self.trend.to_dict_segmentation() if self.trend else None
45
+ ),
39
46
  }
40
47
 
41
48
  def to_dict_segmentation(self) -> dict: