codelexity 0.1.0__tar.gz
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.
- codelexity-0.1.0/LICENSE +21 -0
- codelexity-0.1.0/PKG-INFO +139 -0
- codelexity-0.1.0/README.md +118 -0
- codelexity-0.1.0/pyproject.toml +79 -0
- codelexity-0.1.0/pyproject.toml.orig +73 -0
- codelexity-0.1.0/src/codelexity/__init__.py +7 -0
- codelexity-0.1.0/src/codelexity/calculations.py +194 -0
- codelexity-0.1.0/src/codelexity/graph.py +92 -0
- codelexity-0.1.0/src/codelexity/halstead.py +59 -0
- codelexity-0.1.0/src/codelexity/main.py +72 -0
codelexity-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nickgiki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codelexity
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A package that measures code complexity in the age of AI slop.
|
|
5
|
+
Keywords: complexity,halstead,maintainability,cyclomatic,static-analysis,metrics
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: networkx>=3.0
|
|
15
|
+
Requires-Dist: pyvis>=0.3.2
|
|
16
|
+
Maintainer: nickgiki
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Homepage, https://github.com/nickgiki/codelexity
|
|
19
|
+
Project-URL: Issues, https://github.com/nickgiki/codelexity/issues
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<small>_This project was created with minimal help from AI assistants, mainly for the tests and a couple helper functions. This document is 100% human written._</small>
|
|
23
|
+
|
|
24
|
+
# `Codelexity`
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
A python package that helps you measure, visualize and ultimately manage code complexity.
|
|
31
|
+
|
|
32
|
+
## Motivation
|
|
33
|
+
|
|
34
|
+
In the age of AI, codebases are becoming messier and more difficult to maintain. `Codelexity` helps you visualize and manage this complexity.
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
- Step 1: `uv add codelexity`
|
|
39
|
+
- Step 2: `uv run codelexity <your_package_path> --plot` - this will create a `codelexity.html` that you can open and play with in your browser.
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
_Codelexity HTML report on the `codelexity` repo_
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Intuition
|
|
46
|
+
|
|
47
|
+
There's a ton of literature describing the relationship between complexity and maintainability of code.
|
|
48
|
+
|
|
49
|
+
### The basics
|
|
50
|
+
|
|
51
|
+
[Halstead measures](https://en.wikipedia.org/wiki/Halstead_complexity_measures) are pretty robust in measuring the complexity. All Halstead metrics are derived from 4 numbers:
|
|
52
|
+
|
|
53
|
+
1. $n_1$ = the number of distinct operators
|
|
54
|
+
2. $n_1$ = the number of distinct operands
|
|
55
|
+
3. $N_2$ = the total number of operators
|
|
56
|
+
4. $N_2$ = the total number of operands
|
|
57
|
+
|
|
58
|
+
From these, another 7 metrics can be calculated, with most important the **volume**:
|
|
59
|
+
|
|
60
|
+
$$V=N*log_2(n)$$
|
|
61
|
+
|
|
62
|
+
where:
|
|
63
|
+
- $n = n_1 + n_2$ the vocabulary of the program and
|
|
64
|
+
- $N = N_1 + N_2$ the length of the program
|
|
65
|
+
|
|
66
|
+
Another important metric is the [**Mc Cabe Cyclomatic Complexity**](https://en.wikipedia.org/wiki/Cyclomatic_complexity) measured as:
|
|
67
|
+
|
|
68
|
+
$$M=E-N+2P$$
|
|
69
|
+
|
|
70
|
+
where $M$ the complexity, $E$ and $N$ the number of edges and nodes in the computation graph and $P$ the number of connected components.
|
|
71
|
+
|
|
72
|
+
With Halstead's volume and McCabe's complexity one can compute the [**Maintainability Index**](https://ieeexplore.ieee.org/document/242525) computed as:
|
|
73
|
+
|
|
74
|
+
$$ 171 - 5.2 * log_2(V) - 0.23 * M- 16.2 * log_2(SLOC)+ 50 * \sqrt{2.4 * perCOM}$$
|
|
75
|
+
|
|
76
|
+
where $SLOC$ the total lines of code and $perCOM$ the % of comments in the code.
|
|
77
|
+
|
|
78
|
+
Research is divided about how to interpret the score, with most academic sources (i.e. [Ardito et al., 2020](https://onlinelibrary.wiley.com/doi/10.1155/2020/8840389), [Heričko & Šumak, 2023](https://www.mdpi.com/2076-3417/13/5/2972)) citing $MI>=85$ as high, $85>MI>=65$ as medium and $MI<65$ as low and [Microsoft Visual Studio](https://learn.microsoft.com/en-us/visualstudio/code-quality/code-metrics-maintainability-index-range-and-meaning) citing $MI>=20$ as high, $20>MI>=10$ as medium and $MI<10$ as low.
|
|
79
|
+
|
|
80
|
+
This is how the metrics in this repo are calculated.
|
|
81
|
+
|
|
82
|
+
### Maintainability propagation in the package graph
|
|
83
|
+
|
|
84
|
+
It is easy to understand that a densly connected dependency graph affects the maintainability. Central nodes (those that are imported from other modules that are reachable downstream) are more likely to cause issues. Therefore central modules that are not easy to maintain affect the maintainability of the whole package.
|
|
85
|
+
|
|
86
|
+
To measure centrality, `codelexity` uses [Katz centrality](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.katz_centrality.html). The centrality value is then multiplied by the module length and normalized by the sum of the respective value in all modules. The corresponding value is used as a weight to compute the total Maintainability Index.
|
|
87
|
+
|
|
88
|
+
### Example - NetworkX
|
|
89
|
+
|
|
90
|
+
This is a result for the [`networkx` library](https://networkx.org/en/), a large and complex repo. The command used to create the analysis was:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
codelexity networkx --json --plot
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+
Note that the flags `json` and `plot` denote whether the output will be stored as a json named `codelexity.json` and as an html `codelexity.html` in the working directory.
|
|
99
|
+
|
|
100
|
+
The `codelexity.json` containts aggregate analytics for the whole package and per-module details.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"analytics": {
|
|
105
|
+
"total_lines": 234629,
|
|
106
|
+
"total_functions": 9555,
|
|
107
|
+
"total_modules": 624
|
|
108
|
+
},
|
|
109
|
+
"modules": {
|
|
110
|
+
"conftest.py": {
|
|
111
|
+
"imports": [
|
|
112
|
+
"<stdlib>/importlib/metadata/__init__.py",
|
|
113
|
+
"<stdlib>/os.py",
|
|
114
|
+
"<stdlib>/warnings.py",
|
|
115
|
+
"__init__.py"
|
|
116
|
+
],
|
|
117
|
+
"total_lines": 262,
|
|
118
|
+
"empty_lines": 39,
|
|
119
|
+
"comments": 12,
|
|
120
|
+
"code_length": 211,
|
|
121
|
+
...
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Running the tests
|
|
126
|
+
|
|
127
|
+
The suite lives in `tests/` and uses the standard library's `unittest` — no test dependencies to install.
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
uv run python -m unittest discover # all tests
|
|
131
|
+
uv run python -m unittest discover -v # verbose, one line per test
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Coverage, which needs no dev dependency either:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
uv run --with coverage coverage run --source=src -m unittest discover
|
|
138
|
+
uv run --with coverage coverage report
|
|
139
|
+
```
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<small>_This project was created with minimal help from AI assistants, mainly for the tests and a couple helper functions. This document is 100% human written._</small>
|
|
2
|
+
|
|
3
|
+
# `Codelexity`
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
A python package that helps you measure, visualize and ultimately manage code complexity.
|
|
10
|
+
|
|
11
|
+
## Motivation
|
|
12
|
+
|
|
13
|
+
In the age of AI, codebases are becoming messier and more difficult to maintain. `Codelexity` helps you visualize and manage this complexity.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
- Step 1: `uv add codelexity`
|
|
18
|
+
- Step 2: `uv run codelexity <your_package_path> --plot` - this will create a `codelexity.html` that you can open and play with in your browser.
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
_Codelexity HTML report on the `codelexity` repo_
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Intuition
|
|
25
|
+
|
|
26
|
+
There's a ton of literature describing the relationship between complexity and maintainability of code.
|
|
27
|
+
|
|
28
|
+
### The basics
|
|
29
|
+
|
|
30
|
+
[Halstead measures](https://en.wikipedia.org/wiki/Halstead_complexity_measures) are pretty robust in measuring the complexity. All Halstead metrics are derived from 4 numbers:
|
|
31
|
+
|
|
32
|
+
1. $n_1$ = the number of distinct operators
|
|
33
|
+
2. $n_1$ = the number of distinct operands
|
|
34
|
+
3. $N_2$ = the total number of operators
|
|
35
|
+
4. $N_2$ = the total number of operands
|
|
36
|
+
|
|
37
|
+
From these, another 7 metrics can be calculated, with most important the **volume**:
|
|
38
|
+
|
|
39
|
+
$$V=N*log_2(n)$$
|
|
40
|
+
|
|
41
|
+
where:
|
|
42
|
+
- $n = n_1 + n_2$ the vocabulary of the program and
|
|
43
|
+
- $N = N_1 + N_2$ the length of the program
|
|
44
|
+
|
|
45
|
+
Another important metric is the [**Mc Cabe Cyclomatic Complexity**](https://en.wikipedia.org/wiki/Cyclomatic_complexity) measured as:
|
|
46
|
+
|
|
47
|
+
$$M=E-N+2P$$
|
|
48
|
+
|
|
49
|
+
where $M$ the complexity, $E$ and $N$ the number of edges and nodes in the computation graph and $P$ the number of connected components.
|
|
50
|
+
|
|
51
|
+
With Halstead's volume and McCabe's complexity one can compute the [**Maintainability Index**](https://ieeexplore.ieee.org/document/242525) computed as:
|
|
52
|
+
|
|
53
|
+
$$ 171 - 5.2 * log_2(V) - 0.23 * M- 16.2 * log_2(SLOC)+ 50 * \sqrt{2.4 * perCOM}$$
|
|
54
|
+
|
|
55
|
+
where $SLOC$ the total lines of code and $perCOM$ the % of comments in the code.
|
|
56
|
+
|
|
57
|
+
Research is divided about how to interpret the score, with most academic sources (i.e. [Ardito et al., 2020](https://onlinelibrary.wiley.com/doi/10.1155/2020/8840389), [Heričko & Šumak, 2023](https://www.mdpi.com/2076-3417/13/5/2972)) citing $MI>=85$ as high, $85>MI>=65$ as medium and $MI<65$ as low and [Microsoft Visual Studio](https://learn.microsoft.com/en-us/visualstudio/code-quality/code-metrics-maintainability-index-range-and-meaning) citing $MI>=20$ as high, $20>MI>=10$ as medium and $MI<10$ as low.
|
|
58
|
+
|
|
59
|
+
This is how the metrics in this repo are calculated.
|
|
60
|
+
|
|
61
|
+
### Maintainability propagation in the package graph
|
|
62
|
+
|
|
63
|
+
It is easy to understand that a densly connected dependency graph affects the maintainability. Central nodes (those that are imported from other modules that are reachable downstream) are more likely to cause issues. Therefore central modules that are not easy to maintain affect the maintainability of the whole package.
|
|
64
|
+
|
|
65
|
+
To measure centrality, `codelexity` uses [Katz centrality](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.katz_centrality.html). The centrality value is then multiplied by the module length and normalized by the sum of the respective value in all modules. The corresponding value is used as a weight to compute the total Maintainability Index.
|
|
66
|
+
|
|
67
|
+
### Example - NetworkX
|
|
68
|
+
|
|
69
|
+
This is a result for the [`networkx` library](https://networkx.org/en/), a large and complex repo. The command used to create the analysis was:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
codelexity networkx --json --plot
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
Note that the flags `json` and `plot` denote whether the output will be stored as a json named `codelexity.json` and as an html `codelexity.html` in the working directory.
|
|
78
|
+
|
|
79
|
+
The `codelexity.json` containts aggregate analytics for the whole package and per-module details.
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"analytics": {
|
|
84
|
+
"total_lines": 234629,
|
|
85
|
+
"total_functions": 9555,
|
|
86
|
+
"total_modules": 624
|
|
87
|
+
},
|
|
88
|
+
"modules": {
|
|
89
|
+
"conftest.py": {
|
|
90
|
+
"imports": [
|
|
91
|
+
"<stdlib>/importlib/metadata/__init__.py",
|
|
92
|
+
"<stdlib>/os.py",
|
|
93
|
+
"<stdlib>/warnings.py",
|
|
94
|
+
"__init__.py"
|
|
95
|
+
],
|
|
96
|
+
"total_lines": 262,
|
|
97
|
+
"empty_lines": 39,
|
|
98
|
+
"comments": 12,
|
|
99
|
+
"code_length": 211,
|
|
100
|
+
...
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Running the tests
|
|
105
|
+
|
|
106
|
+
The suite lives in `tests/` and uses the standard library's `unittest` — no test dependencies to install.
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
uv run python -m unittest discover # all tests
|
|
110
|
+
uv run python -m unittest discover -v # verbose, one line per test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Coverage, which needs no dev dependency either:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
uv run --with coverage coverage run --source=src -m unittest discover
|
|
117
|
+
uv run --with coverage coverage report
|
|
118
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "codelexity"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A package that measures code complexity in the age of AI slop."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"networkx>=3.0",
|
|
11
|
+
"pyvis>=0.3.2",
|
|
12
|
+
]
|
|
13
|
+
keywords = [
|
|
14
|
+
"complexity",
|
|
15
|
+
"halstead",
|
|
16
|
+
"maintainability",
|
|
17
|
+
"cyclomatic",
|
|
18
|
+
"static-analysis",
|
|
19
|
+
"metrics",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[project.maintainers]]
|
|
31
|
+
name = "nickgiki"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/nickgiki/codelexity"
|
|
35
|
+
Issues = "https://github.com/nickgiki/codelexity/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
codelexity = "codelexity.main:main"
|
|
39
|
+
|
|
40
|
+
[tool.uv.sources.codelexity]
|
|
41
|
+
path = "src"
|
|
42
|
+
editable = true
|
|
43
|
+
|
|
44
|
+
[tool.bumpver]
|
|
45
|
+
current_version = "0.1.0"
|
|
46
|
+
version_pattern = "MAJOR.MINOR.PATCH"
|
|
47
|
+
push = false
|
|
48
|
+
|
|
49
|
+
[tool.bumpver.file_patterns]
|
|
50
|
+
"pyproject.toml" = [
|
|
51
|
+
'current_version = "{version}"',
|
|
52
|
+
'version = "{version}"',
|
|
53
|
+
]
|
|
54
|
+
"README.md" = ["version-{version}-"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 120
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = [
|
|
61
|
+
"E",
|
|
62
|
+
"F",
|
|
63
|
+
"I",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.isort]
|
|
67
|
+
force-single-line = false
|
|
68
|
+
order-by-type = true
|
|
69
|
+
|
|
70
|
+
[build-system]
|
|
71
|
+
requires = ["uv_build>=0.10,<0.12"]
|
|
72
|
+
build-backend = "uv_build"
|
|
73
|
+
|
|
74
|
+
[dependency-groups]
|
|
75
|
+
dev = [
|
|
76
|
+
"bumpver>=2026.1132",
|
|
77
|
+
"prek>=0.4.14",
|
|
78
|
+
"ruff>=0.16.3",
|
|
79
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "codelexity"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A package that measures code complexity in the age of AI slop."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
maintainers = [{ name = "nickgiki" }]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
dependencies = [
|
|
11
|
+
# Only DiGraph, katz_centrality and PowerIterationFailedConvergence are used, all stable since
|
|
12
|
+
# networkx 3.0; from_nx/save_graph likewise for pyvis. Floors verified with --resolution
|
|
13
|
+
# lowest-direct, no upper caps.
|
|
14
|
+
"networkx>=3.0",
|
|
15
|
+
"pyvis>=0.3.2",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
keywords = ["complexity", "halstead", "maintainability", "cyclomatic", "static-analysis", "metrics"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/nickgiki/codelexity"
|
|
30
|
+
Issues = "https://github.com/nickgiki/codelexity/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
codelexity = "codelexity.main:main"
|
|
34
|
+
|
|
35
|
+
[tool.uv.sources]
|
|
36
|
+
codelexity = { path = "src", editable = true }
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.10,<0.12"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"bumpver>=2026.1132",
|
|
45
|
+
"prek>=0.4.14",
|
|
46
|
+
"ruff>=0.16.3",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.bumpver]
|
|
50
|
+
current_version = "0.1.0"
|
|
51
|
+
version_pattern = "MAJOR.MINOR.PATCH"
|
|
52
|
+
push = false # inspect the tag before it leaves the machine
|
|
53
|
+
|
|
54
|
+
[tool.bumpver.file_patterns]
|
|
55
|
+
# The version appears twice in this file: [project] version and current_version above.
|
|
56
|
+
"pyproject.toml" = ['current_version = "{version}"', 'version = "{version}"']
|
|
57
|
+
"README.md" = ['version-{version}-']
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
# Set your preferred line length rule
|
|
61
|
+
line-length = 120
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
# E, F = standard errors/warnings
|
|
65
|
+
# I = Isort (handles argument/import sorting rules)
|
|
66
|
+
select = ["E", "F", "I"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.isort]
|
|
69
|
+
# Forces arguments/imports into a specific sorted structure
|
|
70
|
+
force-single-line = false
|
|
71
|
+
order-by-type = true
|
|
72
|
+
|
|
73
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
# Read from installed metadata so pyproject.toml stays the only place the version is written.
|
|
4
|
+
try:
|
|
5
|
+
__version__ = version("codelexity")
|
|
6
|
+
except PackageNotFoundError: # running from a source checkout that was never installed
|
|
7
|
+
__version__ = "0.0.0.dev0"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import dis
|
|
3
|
+
import importlib.machinery
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import sysconfig
|
|
7
|
+
from math import log, sin, sqrt
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from codelexity.halstead import halstead_metrics
|
|
11
|
+
|
|
12
|
+
MULTILINE_COMMENTS = re.compile(r"^[\t ]*\"\"\".*?\"\"\"|^[\t ]*'''.*?'''", re.DOTALL | re.MULTILINE)
|
|
13
|
+
SINGLE_LINE_COMMENTS = re.compile(r"^[ \t]*#", re.MULTILINE)
|
|
14
|
+
EMPTY_LINES = re.compile("^[ \t]*$", re.MULTILINE)
|
|
15
|
+
|
|
16
|
+
# One decision point each. BoolOp is counted separately: `a and b and c` is two branches, not one.
|
|
17
|
+
DECISION_POINTS = (
|
|
18
|
+
ast.If,
|
|
19
|
+
ast.IfExp,
|
|
20
|
+
ast.For,
|
|
21
|
+
ast.AsyncFor,
|
|
22
|
+
ast.While,
|
|
23
|
+
ast.ExceptHandler,
|
|
24
|
+
ast.Assert,
|
|
25
|
+
ast.match_case,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def empty_lines(string: str):
|
|
30
|
+
return re.findall(EMPTY_LINES, string)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def comments_and_docstrings(string: str):
|
|
34
|
+
return re.findall(SINGLE_LINE_COMMENTS, string) + re.findall(MULTILINE_COMMENTS, string)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def functions(string):
|
|
38
|
+
tree = ast.parse(string)
|
|
39
|
+
fns = []
|
|
40
|
+
for node in ast.walk(tree):
|
|
41
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
42
|
+
fns.append(ast.get_source_segment(string, node))
|
|
43
|
+
return fns
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cyclomatic_complexity(module_source: str):
|
|
47
|
+
"""McCabe complexity for the whole module: one linearly independent path, plus one per branch."""
|
|
48
|
+
total = 1
|
|
49
|
+
for node in ast.walk(ast.parse(module_source)):
|
|
50
|
+
if isinstance(node, DECISION_POINTS):
|
|
51
|
+
total += 1
|
|
52
|
+
elif isinstance(node, ast.BoolOp):
|
|
53
|
+
total += len(node.values) - 1
|
|
54
|
+
elif isinstance(node, ast.comprehension):
|
|
55
|
+
total += 1 + len(node.ifs)
|
|
56
|
+
return total
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def maintainability_index(module_source: str):
|
|
60
|
+
"""Coleman-Oman index rescaled to 0-100, where higher is more maintainable."""
|
|
61
|
+
volume = halstead_metrics(module_source)["volume"]
|
|
62
|
+
comments = len(comments_and_docstrings(module_source))
|
|
63
|
+
sloc = len(module_source.split("\n")) - len(empty_lines(module_source)) - comments
|
|
64
|
+
if sloc <= 0 or volume <= 0:
|
|
65
|
+
return 100.0
|
|
66
|
+
raw = (
|
|
67
|
+
171
|
|
68
|
+
- 5.2 * log(volume)
|
|
69
|
+
- 0.23 * cyclomatic_complexity(module_source)
|
|
70
|
+
- 16.2 * log(sloc)
|
|
71
|
+
+ 50 * sin(sqrt(2.4 * comments / sloc))
|
|
72
|
+
)
|
|
73
|
+
# ponytail: clamped to 0-100 so it reads as a percentage. The comment term can push raw above
|
|
74
|
+
# 171, which is why the upper bound is here and not just a max(0, ...).
|
|
75
|
+
return min(100.0, max(0.0, raw * 100 / 171))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _import_names(code):
|
|
79
|
+
"""Dotted names a code object imports, e.g. `from a.b import c` -> a, a.b, a.b.c."""
|
|
80
|
+
for name, level, fromlist in dis._find_imports(code):
|
|
81
|
+
yield name
|
|
82
|
+
for item in fromlist or ():
|
|
83
|
+
if item != "*":
|
|
84
|
+
yield f"{name}.{item}"
|
|
85
|
+
for const in code.co_consts:
|
|
86
|
+
if isinstance(const, type(code)):
|
|
87
|
+
yield from _import_names(const)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve(name, search):
|
|
91
|
+
"""Deepest importable spec for a dotted name, walking package search
|
|
92
|
+
locations without importing/executing anything."""
|
|
93
|
+
spec, parts = None, name.split(".")
|
|
94
|
+
for i in range(len(parts)):
|
|
95
|
+
locations = spec.submodule_search_locations if spec else search
|
|
96
|
+
try:
|
|
97
|
+
found = locations and importlib.machinery.PathFinder.find_spec(".".join(parts[: i + 1]), locations)
|
|
98
|
+
except KeyError:
|
|
99
|
+
# ponytail: namespace packages need their own parent in sys.modules
|
|
100
|
+
# to build a submodule spec; we never import, so treat as unresolved.
|
|
101
|
+
found = None
|
|
102
|
+
if not found:
|
|
103
|
+
break
|
|
104
|
+
spec = found
|
|
105
|
+
return spec
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def imports(module_path, root=None):
|
|
109
|
+
"""Full filesystem paths of the local modules `module_path` imports."""
|
|
110
|
+
path = Path(module_path).resolve()
|
|
111
|
+
code = compile(path.read_text(), str(path), "exec")
|
|
112
|
+
root = Path(root).resolve() if root else path.parent
|
|
113
|
+
search = sys.path + [str(root), *(str(d) for d in root.rglob("*") if d.is_dir())]
|
|
114
|
+
names = {n for n in _import_names(code) if n.split(".")[0] not in sys.builtin_module_names}
|
|
115
|
+
specs = (_resolve(n, search) for n in names)
|
|
116
|
+
return sorted({s.origin for s in specs if s and s.origin})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def analyze_module(path, root=None):
|
|
120
|
+
pth = Path(path).resolve()
|
|
121
|
+
st = pth.open().read()
|
|
122
|
+
total, empty, comments = (
|
|
123
|
+
len(st.split("\n")),
|
|
124
|
+
len(empty_lines(st)),
|
|
125
|
+
len(comments_and_docstrings(st)),
|
|
126
|
+
)
|
|
127
|
+
return {
|
|
128
|
+
"imports": imports(pth, root),
|
|
129
|
+
"total_lines": total,
|
|
130
|
+
"empty_lines": empty,
|
|
131
|
+
"comments": comments,
|
|
132
|
+
"code_length": total - empty - comments,
|
|
133
|
+
"contained_function_length": sorted(
|
|
134
|
+
[len(f.split("\n")) - len(empty_lines(f)) - len(comments_and_docstrings(f)) for f in functions(st)]
|
|
135
|
+
),
|
|
136
|
+
"halstead_metrics": halstead_metrics(st),
|
|
137
|
+
"maintainability_index": round(maintainability_index(st), 1),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def normalized_path_list(path: str):
|
|
142
|
+
suffixes = Path(path).suffixes
|
|
143
|
+
name = Path(path).name
|
|
144
|
+
path_list = path.split("/")[:-1]
|
|
145
|
+
for suff in suffixes:
|
|
146
|
+
name = name.replace(suff, "")
|
|
147
|
+
return path_list + [name]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def shorten(path: Path, root: Path):
|
|
151
|
+
ANON_BASES = (
|
|
152
|
+
(sysconfig.get_paths()["stdlib"], "<stdlib>/"),
|
|
153
|
+
(sysconfig.get_paths()["purelib"], "<site-packages>/"),
|
|
154
|
+
(Path.home(), "~/"),
|
|
155
|
+
)
|
|
156
|
+
for base, tag in ((root, ""), *ANON_BASES):
|
|
157
|
+
if path.is_relative_to(base):
|
|
158
|
+
return tag + path.relative_to(base).as_posix()
|
|
159
|
+
return path.as_posix()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def is_valid(module_path: str, include_only, exclude):
|
|
163
|
+
pathlist = normalized_path_list(module_path)
|
|
164
|
+
included = not include_only or set(pathlist).isdisjoint(set(include_only))
|
|
165
|
+
excluded = exclude and set(pathlist).isdisjoint(set(exclude))
|
|
166
|
+
return included and not excluded
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def advanced_analysis(package_data: dict):
|
|
170
|
+
return {
|
|
171
|
+
"total_lines": sum(d["total_lines"] for d in package_data.values()),
|
|
172
|
+
"total_functions": sum(len(d["contained_function_length"]) for d in package_data.values()),
|
|
173
|
+
"total_modules": len(package_data.keys()),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def analyze_package(path, exclude=(), include_only=(), max_recursion=25):
|
|
178
|
+
module_dict = {}
|
|
179
|
+
resolved_path = Path(path)
|
|
180
|
+
for p in resolved_path.rglob("*.py"):
|
|
181
|
+
if not is_valid(p.as_posix(), exclude, include_only):
|
|
182
|
+
continue
|
|
183
|
+
module_data = analyze_module(p, root=resolved_path)
|
|
184
|
+
module_data["imports"] = [
|
|
185
|
+
imp_mod for imp_mod in module_data["imports"] if is_valid(imp_mod, exclude, include_only)
|
|
186
|
+
]
|
|
187
|
+
module_dict[p.resolve().as_posix()] = module_data
|
|
188
|
+
for imp in set(module_data["imports"]).difference(set(module_dict.keys())):
|
|
189
|
+
module_data = analyze_module(imp, root=resolved_path)
|
|
190
|
+
module_data["imports"] = [
|
|
191
|
+
imp_mod for imp_mod in module_data["imports"] if is_valid(imp_mod, exclude, include_only)
|
|
192
|
+
]
|
|
193
|
+
module_dict[imp] = module_data
|
|
194
|
+
return {"analytics": advanced_analysis(module_dict), "modules": module_dict}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from pyvis.network import Network
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def node_data(data: dict):
|
|
8
|
+
return "\n-".join(f"{k}: {v}" for k, v in data.items() if isinstance(v, (str, int, float)))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
node_size = lambda num: max(min(num**0.5, 50), 3)
|
|
12
|
+
|
|
13
|
+
MI_BANDS = ((60, "#97c2fc89"), (20, "#ccb7b789"), (0, "#d0515189"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def mi_color(score):
|
|
17
|
+
"""Position on the light blue -> gray -> red ramp: healthy blue, middling gray, poor red."""
|
|
18
|
+
for threshold, colour in MI_BANDS:
|
|
19
|
+
if score >= threshold:
|
|
20
|
+
return colour
|
|
21
|
+
return MI_BANDS[-1][-1]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_graph(package_data: dict):
|
|
25
|
+
G = nx.DiGraph()
|
|
26
|
+
for i, (module, data) in enumerate(package_data["modules"].items()):
|
|
27
|
+
G.add_node(
|
|
28
|
+
module,
|
|
29
|
+
label=module.split("/")[-1],
|
|
30
|
+
title=node_data({"path": module, **data}),
|
|
31
|
+
size=node_size(data["total_lines"]),
|
|
32
|
+
maintainability=data["maintainability_index"],
|
|
33
|
+
color=mi_color(data["maintainability_index"]),
|
|
34
|
+
)
|
|
35
|
+
for module, data in package_data["modules"].items():
|
|
36
|
+
for imported in data["imports"]:
|
|
37
|
+
# Only edges between analyzed modules. analyze_package stops at depth 1, so deeper
|
|
38
|
+
# imports have no metrics; add_edge would invent attribute-less nodes for them.
|
|
39
|
+
if imported in package_data["modules"]:
|
|
40
|
+
G.add_edge(imported, module)
|
|
41
|
+
return G
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def maintainability(G, damping=0.5):
|
|
45
|
+
"""0-100. Importance-weighted mean of raw MI over the modules that carry data."""
|
|
46
|
+
# Edges pull in transitive imports that were never analyzed, so they have no attributes.
|
|
47
|
+
try:
|
|
48
|
+
centrality = nx.katz_centrality(G.reverse(copy=True), alpha=damping)
|
|
49
|
+
except nx.PowerIterationFailedConvergence:
|
|
50
|
+
print("Failed to converge!")
|
|
51
|
+
centrality = dict.fromkeys(G, 1.0)
|
|
52
|
+
weights = {n: G.nodes[n]["size"] * centrality[n] for n in G.nodes} # weight by size and centrality
|
|
53
|
+
weights = {n: v / sum(weights.values()) for n, v in weights.items()}
|
|
54
|
+
adjusted_m = [G.nodes[n]["maintainability"] * weights[n] for n in G.nodes]
|
|
55
|
+
return sum(adjusted_m)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def create_viz(package_data: dict, fpath: str):
|
|
59
|
+
G = create_graph(package_data=package_data)
|
|
60
|
+
maintainability_score = int(round(maintainability(G)))
|
|
61
|
+
|
|
62
|
+
net = Network(
|
|
63
|
+
height="600px",
|
|
64
|
+
width="100%",
|
|
65
|
+
notebook=False,
|
|
66
|
+
directed=True,
|
|
67
|
+
)
|
|
68
|
+
net.from_nx(G)
|
|
69
|
+
net.set_options("""
|
|
70
|
+
var options = {
|
|
71
|
+
"physics": {
|
|
72
|
+
"maxVelocity": 5
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
""")
|
|
76
|
+
net.save_graph(fpath)
|
|
77
|
+
|
|
78
|
+
legend = (
|
|
79
|
+
'<div style="position:fixed;top:10px;left:10px;background:#fff;'
|
|
80
|
+
"border:1px solid #ccc;padding:8px 12px;"
|
|
81
|
+
"font-family:ui-monospace,Consolas,monospace;"
|
|
82
|
+
'letter-spacing:0.2px;font-size:14px;z-index:1000;">'
|
|
83
|
+
'<div style="font-size:20px;margin-bottom:6px;">Codelexity</div>'
|
|
84
|
+
f'<div style="font-size:15px;margin-bottom:4px;display:inline-block;'
|
|
85
|
+
f'background:{mi_color(maintainability_score)};color:#333;padding:2px 10px;border-radius:2px;">'
|
|
86
|
+
f"Maintainability: {maintainability_score}%</div><br>"
|
|
87
|
+
f"Total lines of code: {package_data['analytics']['total_lines']}<br>"
|
|
88
|
+
f"Total modules: {package_data['analytics']['total_modules']}<br>"
|
|
89
|
+
f"Total functions/methods: {package_data['analytics']['total_functions']}</div>"
|
|
90
|
+
)
|
|
91
|
+
path = Path(fpath)
|
|
92
|
+
path.write_text(path.read_text().replace("<body>", f"<body>\n{legend}", 1))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from math import log2
|
|
4
|
+
|
|
5
|
+
IGNORED = (
|
|
6
|
+
ast.Module,
|
|
7
|
+
ast.Expr,
|
|
8
|
+
ast.expr_context,
|
|
9
|
+
ast.arguments,
|
|
10
|
+
ast.BinOp,
|
|
11
|
+
ast.UnaryOp,
|
|
12
|
+
ast.BoolOp,
|
|
13
|
+
ast.Compare,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def operators_and_operands(module_source: str):
|
|
18
|
+
"""Counts of each distinct operator and operand in `module_source`."""
|
|
19
|
+
operators, operands = Counter(), Counter()
|
|
20
|
+
for node in ast.walk(ast.parse(module_source)):
|
|
21
|
+
if isinstance(node, IGNORED):
|
|
22
|
+
continue
|
|
23
|
+
if isinstance(node, ast.Constant):
|
|
24
|
+
operands[repr(node.value)] += 1
|
|
25
|
+
elif isinstance(node, ast.Name):
|
|
26
|
+
operands[node.id] += 1
|
|
27
|
+
elif isinstance(node, ast.arg):
|
|
28
|
+
operands[node.arg] += 1
|
|
29
|
+
elif isinstance(node, ast.alias):
|
|
30
|
+
operands[node.name] += 1
|
|
31
|
+
else:
|
|
32
|
+
operators[type(node).__name__] += 1
|
|
33
|
+
if isinstance(node, ast.Attribute):
|
|
34
|
+
operands[node.attr] += 1 # `a.b` is the `.` operator applied to operand `b`
|
|
35
|
+
return operators, operands
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def halstead_metrics(module_source: str):
|
|
39
|
+
operators, operands = operators_and_operands(module_source)
|
|
40
|
+
distinct_operators, distinct_operands = len(operators), len(operands)
|
|
41
|
+
total_operators, total_operands = sum(operators.values()), sum(operands.values())
|
|
42
|
+
vocabulary = distinct_operators + distinct_operands
|
|
43
|
+
length = total_operators + total_operands
|
|
44
|
+
volume = length * log2(vocabulary) if vocabulary else 0.0
|
|
45
|
+
difficulty = (distinct_operators * total_operands) / (2 * distinct_operands) if distinct_operands else 0.0
|
|
46
|
+
effort = difficulty * volume
|
|
47
|
+
return {
|
|
48
|
+
"distinct_operators": distinct_operators,
|
|
49
|
+
"distinct_operands": distinct_operands,
|
|
50
|
+
"total_operators": total_operators,
|
|
51
|
+
"total_operands": total_operands,
|
|
52
|
+
"vocabulary": vocabulary,
|
|
53
|
+
"length": length,
|
|
54
|
+
"volume": volume,
|
|
55
|
+
"difficulty": difficulty,
|
|
56
|
+
"effort": effort,
|
|
57
|
+
"time": effort / 18,
|
|
58
|
+
"estimated_bugs": volume / 3000,
|
|
59
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from codelexity.calculations import analyze_package, shorten
|
|
6
|
+
from codelexity.graph import create_viz
|
|
7
|
+
|
|
8
|
+
HTML_NAME = "codelexity.html"
|
|
9
|
+
JSON_NAME = "codelexity.json"
|
|
10
|
+
|
|
11
|
+
parser = argparse.ArgumentParser(description="Codelexity helps you measure and visualize the complexity of your code.")
|
|
12
|
+
|
|
13
|
+
parser.add_argument("filepath", help="The path to the code you want to process.")
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"-j",
|
|
16
|
+
"--json",
|
|
17
|
+
action="store_true",
|
|
18
|
+
help=f"Store the codelexity data in a json file `{JSON_NAME}` in the current directory.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-p",
|
|
22
|
+
"--plot",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help=f"Store the codelexity interactive plot in an html file `{HTML_NAME}` in the current directory.",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-i", "--include-only", nargs="+", type=str, default=(), help="Provide the list of packages/modules to be included."
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"-e",
|
|
31
|
+
"--exclude",
|
|
32
|
+
nargs="+",
|
|
33
|
+
type=str,
|
|
34
|
+
default=(".venv", "bin"),
|
|
35
|
+
help="Provide a list of packages/modules to exclude.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument("-a", "--absolute", action="store_true", help="If added all paths will be absolute.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main():
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
# find and resolve path
|
|
44
|
+
path = Path(args.filepath).resolve()
|
|
45
|
+
print(f"Analyzing code in : {path.as_posix()}")
|
|
46
|
+
|
|
47
|
+
if not path.exists():
|
|
48
|
+
raise FileNotFoundError(f"Could not locate: {args.filepath}")
|
|
49
|
+
|
|
50
|
+
# analyze code
|
|
51
|
+
data = analyze_package(path, exclude=args.exclude, include_only=args.include_only)
|
|
52
|
+
|
|
53
|
+
if not args.absolute:
|
|
54
|
+
# Keys and imports shortened together — create_graph matches edges between the two.
|
|
55
|
+
data["modules"] = {
|
|
56
|
+
shorten(Path(mod), path): {**d, "imports": [shorten(Path(i), path) for i in d["imports"]]}
|
|
57
|
+
for mod, d in data["modules"].items()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if args.plot:
|
|
61
|
+
create_viz(data, HTML_NAME)
|
|
62
|
+
print(f"Interactive plot saved in: `{Path(HTML_NAME).resolve().as_posix()}`")
|
|
63
|
+
|
|
64
|
+
if args.json:
|
|
65
|
+
Path("codelexity.json").write_text(json.dumps(data, indent=4), encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
if not (args.plot or args.json):
|
|
68
|
+
print(json.dumps(data, indent=4))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|