multimetric 2.2.1__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.
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2023 Konrad Weihmann
2
+ # SPDX-License-Identifier: Zlib
3
+ __all__ = ["cls"]
@@ -0,0 +1,215 @@
1
+ # SPDX-FileCopyrightText: 2023 Konrad Weihmann
2
+ # SPDX-License-Identifier: Zlib
3
+
4
+ import argparse
5
+ import json
6
+ import logging
7
+ import multiprocessing as mp
8
+ import os
9
+ import sys
10
+ import textwrap
11
+
12
+ import chardet
13
+ from pygments import lexers
14
+
15
+ from multimetric.cls.importer.filtered import FilteredImporter
16
+ from multimetric.cls.importer.pick import importer_pick
17
+ from multimetric.cls.modules import get_additional_parser_args
18
+ from multimetric.cls.modules import get_modules_calculated
19
+ from multimetric.cls.modules import get_modules_metrics
20
+ from multimetric.cls.modules import get_modules_stats
21
+
22
+
23
+ def ArgParser():
24
+ parser = argparse.ArgumentParser(
25
+ formatter_class=argparse.RawTextHelpFormatter,
26
+ prog="multimetric", description='Calculate code metrics in various languages',
27
+ epilog=textwrap.dedent("""
28
+ Currently you could import files of the following types for --warn_* or --coverage
29
+
30
+ Following information can be read
31
+
32
+ <file> = full path to file
33
+ <severity> = severity [error, warning, info]
34
+ <content> = optional string
35
+
36
+ Note: you could also add a single line, then <content>
37
+ has to be a number reflecting to total number of findings
38
+
39
+ File formats
40
+
41
+ csv: CSV file of following line format
42
+ <file>,<severity>,[<content>]
43
+
44
+ json: JSON file
45
+ <file>: {
46
+ ["content": <content>,]
47
+ "severity": <severity>
48
+ }
49
+ """))
50
+ parser.add_argument(
51
+ "--warn_compiler",
52
+ default=None,
53
+ help="File(s) holding information about compiler warnings")
54
+ parser.add_argument(
55
+ "--warn_duplication",
56
+ default=None,
57
+ help="File(s) holding information about code duplications")
58
+ parser.add_argument(
59
+ "--warn_functional",
60
+ default=None,
61
+ help="File(s) holding information about static code analysis findings")
62
+ parser.add_argument(
63
+ "--warn_standard",
64
+ default=None,
65
+ help="File(s) holding information about language standard violations")
66
+ parser.add_argument(
67
+ "--warn_security",
68
+ default=None,
69
+ help="File(s) File(s) holding information about found security issue")
70
+ parser.add_argument(
71
+ "--coverage",
72
+ default=None,
73
+ help="File(s) with compiler warningsFile(s) holding information about testing coverage")
74
+ parser.add_argument(
75
+ "--dump",
76
+ default=False,
77
+ action="store_true",
78
+ help="Just dump the token tree")
79
+ parser.add_argument(
80
+ "--verbose",
81
+ default=False,
82
+ action="store_true",
83
+ help="Verbose logging output")
84
+ parser.add_argument(
85
+ "--jobs",
86
+ type=int,
87
+ default=mp.cpu_count(),
88
+ help="Run x jobs in parallel")
89
+ get_additional_parser_args(parser)
90
+ parser.add_argument("files", nargs='+', help="Files to parse")
91
+ return parser
92
+
93
+
94
+ def parse_args(*args):
95
+ RUNARGS = ArgParser().parse_args(*args)
96
+ # Turn all paths to abs-paths right here
97
+ RUNARGS.files = [os.path.abspath(x) for x in RUNARGS.files]
98
+
99
+ # Setup logging
100
+ stdout_log = logging.getLogger('stdout')
101
+ stdout_log.setLevel(logging.DEBUG if RUNARGS.verbose else logging.INFO)
102
+
103
+ handler = logging.StreamHandler(sys.stdout)
104
+ handler.setLevel(logging.DEBUG if RUNARGS.verbose else logging.INFO)
105
+ formatter = logging.Formatter('%(message)s')
106
+ handler.setFormatter(formatter)
107
+ stdout_log.addHandler(handler)
108
+
109
+ stderr_log = logging.getLogger('stderr')
110
+ stderr_log.setLevel(logging.DEBUG if RUNARGS.verbose else logging.INFO)
111
+
112
+ handler = logging.StreamHandler(sys.stderr)
113
+ handler.setLevel(logging.DEBUG if RUNARGS.verbose else logging.INFO)
114
+ formatter = logging.Formatter('%(levelname)s - %(message)s')
115
+ handler.setFormatter(formatter)
116
+ stderr_log.addHandler(handler)
117
+
118
+ return RUNARGS
119
+
120
+
121
+ def file_process(_file, _args, _importer):
122
+ res = {}
123
+ store = {}
124
+ try:
125
+ _lexer = lexers.get_lexer_for_filename(_file)
126
+ except ValueError: # pragma: no cover - bug in pytest-cov
127
+ logging.getLogger('stderr').error(
128
+ f'The file {_file} could not be identified automatically. Skipping this file.') # pragma: no cover - bug in pytest-cov
129
+ return ({}, _file, 'lexer.error', [], {}) # pragma: no cover - bug in pytest-cov
130
+ try:
131
+ with open(_file, "rb") as i:
132
+ _cnt = i.read()
133
+ _enc = chardet.detect(_cnt)
134
+ _cnt = _cnt.decode(_enc["encoding"]).encode("utf-8")
135
+ _localImporter = {k: FilteredImporter(
136
+ v, _file) for k, v in _importer.items()}
137
+ tokens = list(_lexer.get_tokens(_cnt))
138
+ if _args.dump: # pragma: no cover
139
+ for x in tokens: # pragma: no cover
140
+ logging.getLogger('stdout').info(f"{_file}: {x[0]} -> {repr(x[1])}") # pragma: no cover
141
+ else:
142
+ _localMetrics = get_modules_metrics(_args, **_localImporter)
143
+ _localCalc = get_modules_calculated(_args, **_localImporter)
144
+ for x in _localMetrics:
145
+ x.parse_tokens(_lexer.name, tokens)
146
+ res.update(x.get_results())
147
+ store.update(x.get_internal_store())
148
+ for x in _localCalc:
149
+ res.update(x.get_results(res))
150
+ store.update(x.get_internal_store())
151
+ except Exception as e: # pragma: no cover
152
+ logging.getLogger('stderr').exception(e) # pragma: no cover
153
+ tokens = [] # pragma: no cover
154
+ return (res, _file, _lexer.name, tokens, store)
155
+
156
+
157
+ def run(_args):
158
+ _result = {"files": {}, "overall": {}}
159
+
160
+ # Get importer
161
+ _importer = {}
162
+ _importer["import_compiler"] = importer_pick(_args, _args.warn_compiler)
163
+ _importer["import_coverage"] = importer_pick(_args, _args.coverage)
164
+ _importer["import_duplication"] = importer_pick(
165
+ _args, _args.warn_duplication)
166
+ _importer["import_functional"] = importer_pick(
167
+ _args, _args.warn_functional)
168
+ _importer["import_security"] = importer_pick(_args, _args.warn_standard)
169
+ _importer["import_standard"] = importer_pick(_args, _args.warn_security)
170
+ # sanity check
171
+ _importer = {k: v for k, v in _importer.items() if v}
172
+
173
+ # instance metric modules
174
+ _overallMetrics = get_modules_metrics(_args, **_importer)
175
+ _overallCalc = get_modules_calculated(_args, **_importer)
176
+
177
+ with mp.Pool(processes=_args.jobs) as pool:
178
+ results = [pool.apply(file_process, args=(
179
+ f, _args, _importer)) for f in _args.files]
180
+
181
+ for x in results:
182
+ _result["files"][x[1]] = x[0]
183
+
184
+ for y in _overallMetrics:
185
+ _result["overall"].update(
186
+ y.get_results_global([x[4] for x in results]))
187
+ for y in _overallCalc:
188
+ _result["overall"].update(y.get_results(_result["overall"]))
189
+ for m in get_modules_stats(_args, **_importer):
190
+ _result = m.get_results(_result, "files", "overall")
191
+
192
+ def round_float(item):
193
+ if isinstance(item, dict):
194
+ for k, v in item.items():
195
+ item[k] = round_float(v)
196
+ elif isinstance(item, list):
197
+ for index, value in enumerate(item):
198
+ item[index] = round_float(value)
199
+ elif isinstance(item, float):
200
+ item = round(item, 3)
201
+ return item
202
+
203
+ return round_float(_result)
204
+
205
+
206
+ def main(): # pragma: no cover
207
+ _args = parse_args() # pragma: no cover
208
+ _result = run(_args) # pragma: no cover
209
+ if not _args.dump: # pragma: no cover
210
+ # Output
211
+ logging.getLogger('stdout').info(json.dumps(_result, indent=2, sort_keys=True)) # pragma: no cover
212
+
213
+
214
+ if __name__ == '__main__':
215
+ main() # pragma: no cover
File without changes
@@ -0,0 +1,26 @@
1
+ # SPDX-FileCopyrightText: 2023 Konrad Weihmann
2
+ # SPDX-License-Identifier: Zlib
3
+ class MetricBase():
4
+ def __init__(self, args, **kwargs):
5
+ self._metrics = {"lang": []}
6
+ self._internalstore = {}
7
+
8
+ def parse_tokens(self, language, tokens):
9
+ if language not in self._metrics["lang"]: # pragma: no cover
10
+ self._metrics["lang"].append(language)
11
+
12
+ def get_results(self):
13
+ return self._metrics
14
+
15
+ def get_internal_store(self):
16
+ return {self.__class__.__name__: self._internalstore}
17
+
18
+ def _get_all_matching_store_objects(self, store):
19
+ res = []
20
+ for item in store:
21
+ if self.__class__.__name__ in item:
22
+ res.append(item[self.__class__.__name__])
23
+ return res
24
+
25
+ def get_results_global(self, value_stores):
26
+ return {} # pragma: no cover
@@ -0,0 +1,16 @@
1
+ # SPDX-FileCopyrightText: 2023 Konrad Weihmann
2
+ # SPDX-License-Identifier: Zlib
3
+ class MetricBaseCalc():
4
+
5
+ def __init__(self, args, **kwargs):
6
+ self._metrics = {}
7
+ self._internalstore = {}
8
+
9
+ def get_results(self, metrics):
10
+ """
11
+ This alters the originally passed metrics by calculated ones
12
+ """
13
+ return metrics
14
+
15
+ def get_internal_store(self):
16
+ return self._internalstore
@@ -0,0 +1,12 @@
1
+ # SPDX-FileCopyrightText: 2023 Konrad Weihmann
2
+ # SPDX-License-Identifier: Zlib
3
+ class MetricBaseStats():
4
+
5
+ def __init__(self, args, **kwargs):
6
+ self._metrics = {}
7
+
8
+ def get_results(self, metrics, files="files", overall="overall"):
9
+ """
10
+ This alters the originally passed metrics by calculated ones
11
+ """
12
+ return metrics
File without changes
@@ -0,0 +1,308 @@
1
+ # SPDX-FileCopyrightText: 2023 Konrad Weihmann
2
+ # SPDX-License-Identifier: Zlib
3
+
4
+ import math
5
+
6
+ from multimetric.cls.base_calc import MetricBaseCalc
7
+ from multimetric.cls.metric.operands import MetricBaseOperands
8
+ from multimetric.cls.metric.operators import MetricBaseOperator
9
+
10
+
11
+ class MetricBaseCalcHalstead(MetricBaseCalc):
12
+ """A class for calculating Halstead metrics.
13
+
14
+ This class inherits from MetricBaseCalc.
15
+
16
+ Attributes
17
+ ----------
18
+ METRIC_HALSTEAD_VOLUME : str
19
+ The name of the Halstead volume metric.
20
+ METRIC_HALSTEAD_EFFORT : str
21
+ The name of the Halstead effort metric.
22
+ METRIC_HALSTEAD_DIFFICULTY : str
23
+ The name of the Halstead difficulty metric.
24
+ METRIC_HALSTEAD_BUGS : str
25
+ The name of the Halstead bugprop metric.
26
+ METRIC_HALSTEAD_TIMEREQ : str
27
+ The name of the Halstead timerequired metric.
28
+
29
+ Methods
30
+ -------
31
+ _bugpred_old(obj)
32
+ Calculate the bug prediction using the old method.
33
+ _bugpred_new(obj)
34
+ Calculate the bug prediction using the new method.
35
+ _getNs(metrics)
36
+ Calculate the values of N1, N2, n1, n2.
37
+ _getVocabulary(metrics)
38
+ Calculate the vocabulary of the program.
39
+ _getProgLength(metrics)
40
+ Calculate the program length.
41
+ _getVolume(metrics)
42
+ Calculate the Halstead volume.
43
+ _getDifficulty(metrics)
44
+ Calculate the Halstead difficulty.
45
+ _getEffort(metrics)
46
+ Calculate the Halstead effort.
47
+ _getTime(metrics)
48
+ Calculate the time required.
49
+ _getBug(metrics)
50
+ Calculate the bugprop.
51
+ get_results(metrics)
52
+ Calculate all the Halstead metrics and return the results.
53
+
54
+ """
55
+
56
+ METRIC_HALSTEAD_VOLUME = "halstead_volume"
57
+ METRIC_HALSTEAD_EFFORT = "halstead_effort"
58
+ METRIC_HALSTEAD_DIFFICULTY = "halstead_difficulty"
59
+ METRIC_HALSTEAD_BUGS = "halstead_bugprop"
60
+ METRIC_HALSTEAD_TIMEREQ = "halstead_timerequired"
61
+
62
+ @staticmethod
63
+ def _bugpred_old(obj):
64
+ """Calculate the bug prediction using the old method.
65
+
66
+ Parameters
67
+ ----------
68
+ obj : MetricBaseCalcHalstead
69
+ The MetricBaseCalcHalstead object.
70
+
71
+ Returns
72
+ -------
73
+ float
74
+ The bug prediction using the old method.
75
+
76
+ """
77
+ return (obj._effort * (2.0 / 3.0)) / 3000.0
78
+
79
+ @staticmethod
80
+ def _bugpred_new(obj):
81
+ """Calculate the bug prediction using the new method.
82
+
83
+ Parameters
84
+ ----------
85
+ obj : MetricBaseCalcHalstead
86
+ The MetricBaseCalcHalstead object.
87
+
88
+ Returns
89
+ -------
90
+ float
91
+ The bug prediction using the new method.
92
+
93
+ """
94
+ return obj._volume / 3000.0
95
+
96
+ BUGPRED_DEFAULT = "new"
97
+ BUGPRED_METHOD = {
98
+ "old": _bugpred_old,
99
+ "new": _bugpred_new,
100
+ }
101
+
102
+ def __init__(self, args, **kwargs):
103
+ """Initialize the MetricBaseCalcHalstead object.
104
+
105
+ Parameters
106
+ ----------
107
+ args : argparse.Namespace
108
+ The command line arguments.
109
+ **kwargs
110
+ Additional keyword arguments.
111
+
112
+ """
113
+ super().__init__(args, **kwargs)
114
+ self.__bugPredicMethod = args.halstead_bug_predict_method
115
+
116
+ def _getNs(self, metrics):
117
+ """Calculate the values of N1, N2, n1, n2.
118
+
119
+ Parameters
120
+ ----------
121
+ metrics : dict
122
+ The metrics dictionary.
123
+
124
+ Notes
125
+ -----
126
+ - N1 is the total number of operators
127
+ - N2 is the total number of operands
128
+ - n1 is the number of distinct operators
129
+ - n2 is the number of distinct operands
130
+ """
131
+ self._N2 = float(metrics[MetricBaseOperands.METRIC_OPERANDS_SUM])
132
+ self._N1 = float(metrics[MetricBaseOperator.METRIC_OPERATORS_SUM])
133
+ self._n2 = float(metrics[MetricBaseOperands.METRIC_OPERANDS_UNIQUE])
134
+ self._n1 = float(metrics[MetricBaseOperator.METRIC_OPERATORS_UNIQUE])
135
+ # to avoid any Divbyzero bugs set the minimum to 1
136
+ self._n1 = max(self._n1, 1)
137
+ self._n2 = max(self._n2, 1)
138
+ self._N1 = max(self._N1, 1)
139
+ self._N2 = max(self._N2, 1)
140
+
141
+ def _getVocabulary(self, metrics):
142
+ """Calculate the vocabulary of the program.
143
+
144
+ Halstead vocabulary (n) is defined as the sum of distinct
145
+ operators and operands. (n1 + n2)
146
+
147
+ Parameters
148
+ ----------
149
+ metrics : dict
150
+ The metrics dictionary.
151
+
152
+ Returns
153
+ -------
154
+ float
155
+ The vocabulary of the program.
156
+
157
+ """
158
+ self._getNs(metrics)
159
+ self._vocabulary = self._n1 + self._n2
160
+ # to avoid a log(0) the minimum of vocabulary is 1
161
+ self._vocabulary = max(1, self._vocabulary)
162
+ return self._vocabulary
163
+
164
+ def _getProgLength(self, metrics):
165
+ """Calculate the program length.
166
+
167
+ Halstead program length (N) is defined as the sum of total
168
+ operators and operands. (N1 + N2).
169
+
170
+ Parameters
171
+ ----------
172
+ metrics : dict
173
+ The metrics dictionary.
174
+
175
+ Returns
176
+ -------
177
+ float
178
+ The program length.
179
+
180
+ """
181
+ self._getNs(metrics)
182
+ self._length = self._N1 + self._N2
183
+ return self._length
184
+
185
+ def _getVolume(self, metrics):
186
+ """Calculate the Halstead volume.
187
+
188
+ Parameters
189
+ ----------
190
+ metrics : dict
191
+ The metrics dictionary.
192
+
193
+ Returns
194
+ -------
195
+ float
196
+ The Halstead volume.
197
+
198
+ """
199
+ self._getVocabulary(metrics)
200
+ self._getProgLength(metrics)
201
+ self._volume = self._length * math.log2(self._vocabulary)
202
+ # to avoid a log(0) the minimum of volume is 1
203
+ self._volume = max(1, self._volume)
204
+ return self._volume
205
+
206
+ def _getDifficulty(self, metrics):
207
+ """Calculate the Halstead difficulty.
208
+
209
+ The difficulty measure is related to the difficulty of the program
210
+ to write or understand, e.g. when doing code review.
211
+
212
+ Parameters
213
+ ----------
214
+ metrics : dict
215
+ The metrics dictionary.
216
+
217
+ Returns
218
+ -------
219
+ float
220
+ The Halstead difficulty.
221
+
222
+ """
223
+ self._getNs(metrics)
224
+ self._difficulty = (self._n1 / 2.0) * (self._N2 / self._n2)
225
+ return self._difficulty
226
+
227
+ def _getEffort(self, metrics):
228
+ """Calculate the Halstead effort.
229
+
230
+ The effort measure translates into actual coding time.
231
+
232
+ Parameters
233
+ ----------
234
+ metrics : dict
235
+ The metrics dictionary.
236
+
237
+ Returns
238
+ -------
239
+ float
240
+ The Halstead effort.
241
+
242
+ """
243
+ self._getVolume(metrics)
244
+ self._getDifficulty(metrics)
245
+ self._effort = self._volume * self._difficulty
246
+ return self._effort
247
+
248
+ def _getTime(self, metrics):
249
+ """Calculate the estimated time required to write program.
250
+
251
+ Parameters
252
+ ----------
253
+ metrics : dict
254
+ The metrics dictionary.
255
+
256
+ Returns
257
+ -------
258
+ float
259
+ The time required to write the program, in seconds.
260
+
261
+ """
262
+ self._getEffort(metrics)
263
+ self._timeRequired = self._effort / 18.0
264
+ return self._timeRequired
265
+
266
+ def _getBug(self, metrics):
267
+ """Calculate the estimate for the number of errors in the implementation.
268
+
269
+ Parameters
270
+ ----------
271
+ metrics : dict
272
+ The metrics dictionary.
273
+
274
+ Returns
275
+ -------
276
+ float
277
+ The number of delivered bugs.
278
+ """
279
+ self._getEffort(metrics)
280
+ self._bug = MetricBaseCalcHalstead.BUGPRED_METHOD[self.__bugPredicMethod](self)
281
+ return self._bug
282
+
283
+ def get_results(self, metrics):
284
+ """Calculate all the Halstead metrics and return the results.
285
+
286
+ Parameters
287
+ ----------
288
+ metrics : dict
289
+ The metrics dictionary.
290
+
291
+ Returns
292
+ -------
293
+ dict
294
+ The metrics dictionary with the Halstead metrics added.
295
+
296
+ """
297
+ metrics[MetricBaseCalcHalstead.METRIC_HALSTEAD_VOLUME] = self._getVolume(
298
+ metrics,
299
+ )
300
+ metrics[
301
+ MetricBaseCalcHalstead.METRIC_HALSTEAD_DIFFICULTY
302
+ ] = self._getDifficulty(metrics)
303
+ metrics[MetricBaseCalcHalstead.METRIC_HALSTEAD_EFFORT] = self._getEffort(
304
+ metrics,
305
+ )
306
+ metrics[MetricBaseCalcHalstead.METRIC_HALSTEAD_TIMEREQ] = self._getTime(metrics)
307
+ metrics[MetricBaseCalcHalstead.METRIC_HALSTEAD_BUGS] = self._getBug(metrics)
308
+ return super().get_results(metrics)