complexipy 0.3.1__cp39-none-win_amd64.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.
complexipy/__init__.py ADDED
File without changes
complexipy/main.py ADDED
@@ -0,0 +1,113 @@
1
+ from .types import (
2
+ DetailTypes,
3
+ Level,
4
+ Sort,
5
+ )
6
+ from .utils import (
7
+ output_summary,
8
+ has_success_file_level,
9
+ has_success_function_level,
10
+ )
11
+ from complexipy import (
12
+ rust,
13
+ )
14
+ from complexipy.rust import (
15
+ FileComplexity,
16
+ )
17
+ import os
18
+ from pathlib import (
19
+ Path,
20
+ )
21
+ import re
22
+ from rich.console import (
23
+ Console,
24
+ )
25
+ import time
26
+ import typer
27
+
28
+ root_dir = Path(__file__).resolve().parent.parent
29
+ app = typer.Typer(name="complexipy")
30
+ console = Console()
31
+ version = "0.3.1"
32
+
33
+
34
+ @app.command()
35
+ def main(
36
+ path: str = typer.Argument(
37
+ help="Path to the directory or file to analyze, it can be a local path or a git repository URL.",
38
+ ),
39
+ max_complexity: int = typer.Option(
40
+ 15,
41
+ "--max-complexity",
42
+ "-c",
43
+ help="The maximum complexity allowed per file, set this value as 0 to set it as unlimited. Default is 15.",
44
+ ),
45
+ output: bool = typer.Option(
46
+ False, "--output", "-o", help="Output the results to a CSV file."
47
+ ),
48
+ details: DetailTypes = typer.Option(
49
+ DetailTypes.normal.value,
50
+ "--details",
51
+ "-d",
52
+ help="Specify how detailed should be output, it can be 'low' or 'normal'. Default is 'normal'.",
53
+ ),
54
+ level: Level = typer.Option(
55
+ Level.function.value,
56
+ "--level",
57
+ "-l",
58
+ help="Specify the level of measurement, it can be 'function' or 'file'. Default is 'function'.",
59
+ ),
60
+ quiet: bool = typer.Option(
61
+ False, "--quiet", "-q", help="Suppress the output to the console."
62
+ ),
63
+ sort: Sort = typer.Option(
64
+ Sort.asc.value,
65
+ "--sort",
66
+ "-s",
67
+ help="Sort the output by complexity, it can be 'asc', 'desc' or 'name'. Default is 'asc'.",
68
+ ),
69
+ ):
70
+ is_dir = Path(path).is_dir()
71
+ _url_pattern = (
72
+ r"^(https:\/\/|http:\/\/|www\.|git@)(github|gitlab)\.com(\/[\w.-]+){2,}$"
73
+ )
74
+ is_url = bool(re.match(_url_pattern, path))
75
+ invocation_path = os.getcwd()
76
+ file_level = level == Level.file
77
+
78
+ console.rule(f":octopus: complexipy {version}")
79
+ start_time = time.time()
80
+ files: list[FileComplexity] = rust.main(
81
+ path, is_dir, is_url, max_complexity, file_level
82
+ )
83
+ execution_time = time.time() - start_time
84
+ output_csv_path = f"{invocation_path}/complexipy.csv"
85
+
86
+ if output and file_level:
87
+ rust.output_csv_file_level(output_csv_path, files, sort.value)
88
+ console.print(f"Results saved in {output_csv_path}")
89
+ if output and not file_level:
90
+ rust.output_csv_function_level(output_csv_path, files, sort.value)
91
+ console.print(f"Results saved in {output_csv_path}")
92
+
93
+ # Summary
94
+ if not quiet:
95
+ has_success = output_summary(
96
+ console, file_level, files, max_complexity, details, path, sort
97
+ )
98
+ if quiet and not file_level:
99
+ has_success = has_success_function_level(files, max_complexity)
100
+ if quiet and file_level:
101
+ has_success = has_success_file_level(files, max_complexity)
102
+
103
+ console.print(
104
+ f"{len(files)} file{'s' if len(files)> 1 else ''} analyzed in {execution_time:.4f} seconds"
105
+ )
106
+ console.rule(":tada: Analysis completed! :tada:")
107
+
108
+ if not has_success:
109
+ raise typer.Exit(code=1)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ app()
Binary file
complexipy/types.py ADDED
@@ -0,0 +1,17 @@
1
+ from enum import Enum
2
+
3
+
4
+ class DetailTypes(Enum):
5
+ low = "low" # Show only files with complexity above the max_complexity
6
+ normal = "normal" # Show all files with their complexity
7
+
8
+
9
+ class Level(Enum):
10
+ function = "function"
11
+ file = "file"
12
+
13
+
14
+ class Sort(Enum):
15
+ asc = "asc"
16
+ desc = "desc"
17
+ name = "name"
complexipy/utils.py ADDED
@@ -0,0 +1,139 @@
1
+ from .types import (
2
+ DetailTypes,
3
+ Sort,
4
+ )
5
+ from complexipy.rust import (
6
+ FileComplexity,
7
+ FunctionComplexity,
8
+ )
9
+ from rich.align import (
10
+ Align,
11
+ )
12
+ from rich.console import (
13
+ Console,
14
+ )
15
+ from rich.table import Table
16
+
17
+
18
+ def output_summary(
19
+ console: Console,
20
+ file_level: bool,
21
+ files: list[FileComplexity],
22
+ max_complexity: int,
23
+ details: DetailTypes,
24
+ path: str,
25
+ sort: Sort,
26
+ ) -> bool:
27
+ if file_level:
28
+ table, has_success, total_complexity = create_table_file_level(
29
+ files, max_complexity, details, sort
30
+ )
31
+ else:
32
+ table, has_success, total_complexity = create_table_function_level(
33
+ files, max_complexity, details, sort
34
+ )
35
+ console.print(Align.center(table))
36
+ console.print(f":brain: Total Cognitive Complexity in {path}: {total_complexity}")
37
+
38
+ return has_success
39
+
40
+
41
+ def create_table_file_level(
42
+ files: list[FileComplexity], max_complexity: int, details: DetailTypes, sort: Sort
43
+ ) -> tuple[Table, bool, int]:
44
+ has_success = True
45
+
46
+ table = Table(
47
+ title="Summary", show_header=True, header_style="bold magenta", show_lines=True
48
+ )
49
+ table.add_column("Path")
50
+ table.add_column("File")
51
+ table.add_column("Complexity")
52
+ total_complexity = 0
53
+
54
+ if sort != Sort.name:
55
+ files.sort(key=lambda x: x.complexity)
56
+
57
+ if sort == Sort.desc:
58
+ files.reverse()
59
+
60
+ for file in files:
61
+ total_complexity += file.complexity
62
+ if file.complexity > max_complexity and max_complexity != 0:
63
+ table.add_row(
64
+ f"{file.path}",
65
+ f"[green]{file.file_name}[/green]",
66
+ f"[red]{file.complexity}[/red]",
67
+ )
68
+ has_success = False
69
+ elif details != DetailTypes.low or max_complexity == 0:
70
+ table.add_row(
71
+ f"{file.path}",
72
+ f"[green]{file.file_name}[/green]",
73
+ f"[blue]{file.complexity}[/blue]",
74
+ )
75
+ return table, has_success, total_complexity
76
+
77
+
78
+ def create_table_function_level(
79
+ files: list[FileComplexity],
80
+ complexity: int,
81
+ details: DetailTypes,
82
+ sort: bool = False,
83
+ ) -> tuple[Table, bool, int]:
84
+ has_success = True
85
+ all_functions: list[tuple[str, str, FunctionComplexity]] = []
86
+ total_complexity = 0
87
+
88
+ table = Table(
89
+ title="Summary", show_header=True, header_style="bold magenta", show_lines=True
90
+ )
91
+ table.add_column("Path")
92
+ table.add_column("File")
93
+ table.add_column("Function")
94
+ table.add_column("Complexity")
95
+
96
+ for file in files:
97
+ total_complexity += file.complexity
98
+ for function in file.functions:
99
+ total_complexity += function.complexity
100
+ all_functions.append((file.path, file.file_name, function))
101
+
102
+ if sort != Sort.name:
103
+ all_functions.sort(key=lambda x: x[2].complexity)
104
+
105
+ if sort == Sort.desc:
106
+ all_functions.reverse()
107
+
108
+ for function in all_functions:
109
+ if function[2].complexity > complexity and complexity != 0:
110
+ table.add_row(
111
+ f"{function[0]}",
112
+ f"[green]{function[1]}[/green]",
113
+ f"[green]{function[2].name}[/green]",
114
+ f"[red]{function[2].complexity}[/red]",
115
+ )
116
+ has_success = False
117
+ elif details != DetailTypes.low or complexity == 0:
118
+ table.add_row(
119
+ f"{function[0]}",
120
+ f"[green]{function[1]}[/green]",
121
+ f"[green]{function[2].name}[/green]",
122
+ f"[blue]{function[2].complexity}[/blue]",
123
+ )
124
+ return table, has_success, total_complexity
125
+
126
+
127
+ def has_success_file_level(files: list[FileComplexity], max_complexity: int) -> bool:
128
+ for file in files:
129
+ if file.complexity > max_complexity and max_complexity != 0:
130
+ return False
131
+ return True
132
+
133
+
134
+ def has_success_function_level(files: list[FileComplexity], complexity: int) -> bool:
135
+ for file in files:
136
+ for function in file.functions:
137
+ if function.complexity > complexity and complexity != 0:
138
+ return False
139
+ return True
@@ -0,0 +1,244 @@
1
+ Metadata-Version: 2.3
2
+ Name: complexipy
3
+ Version: 0.3.1
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
7
+ Requires-Dist: typer[all]
8
+ License-File: LICENSE
9
+ Summary: An extremely fast Python library to calculate the cognitive complexity of Python files, written in Rust.
10
+ Keywords: cognitive,complexity,cognitive complexity,rust,fast
11
+ Home-Page: https://github.com/rohaquinlop/complexipy
12
+ Author: Robin Quintero <rohaquinlop301@gmail.com>
13
+ Author-email: Robin Quintero <rohaquinlop301@gmail.com>
14
+ License: MIT
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
17
+ Project-URL: Source Code, https://github.com/rohaquinlop/complexipy
18
+
19
+ # complexipy
20
+
21
+ <p align="center">
22
+ <a href="https://rohaquinlop.github.io/complexipy/"><img src="https://raw.githubusercontent.com/rohaquinlop/complexipy/main/docs/img/logo-vector.svg" alt="complexipy"></a>
23
+ </p>
24
+
25
+ <p align="center">
26
+ <em>An extremely fast Python library to calculate the cognitive complexity of Python files, written in Rust.</em>
27
+ </p>
28
+
29
+ <p align="center">
30
+ <a href="https://sonarcloud.io/summary/new_code?id=rohaquinlop_complexipy" target="_blank">
31
+ <img src="https://sonarcloud.io/api/project_badges/measure?project=rohaquinlop_complexipy&metric=alert_status" alt="Quality Gate">
32
+ </a>
33
+ <a href="https://pypi.org/project/complexipy" target="_blank">
34
+ <img src="https://img.shields.io/pypi/v/complexipy?color=%2334D058&label=pypi%20package" alt="Package version">
35
+ </a>
36
+ </p>
37
+
38
+
39
+ Cognitive Complexity breaks from using mathematical models to assess software
40
+ maintainability by combining Cyclomatic Complexity precedents with human
41
+ assessment. It yields method complexity scores that align well with how
42
+ developers perceive maintainability. Read the white paper here: [Cognitive Complexity, a new way of measuring understandability](https://www.sonarsource.com/resources/cognitive-complexity/)
43
+
44
+
45
+ ---
46
+
47
+ **Documentation**: <a href="https://rohaquinlop.github.io/complexipy/" target="_blank">https://rohaquinlop.github.io/complexipy/</a>
48
+
49
+ **Source Code**: <a href="https://github.com/rohaquinlop/complexipy" target="_blank">https://github.com/rohaquinlop/complexipy</a>
50
+
51
+ **PyPI**: <a href="https://pypi.org/project/complexipy/" target="_blank">https://pypi.org/project/complexipy/</a>
52
+
53
+ ---
54
+
55
+
56
+ ## Contributors
57
+
58
+ <p align="center">
59
+ <a href = "https://github.com/rohaquinlop/complexipy/graphs/contributors">
60
+ <img src = "https://contrib.rocks/image?repo=rohaquinlop/complexipy"/>
61
+ </a>
62
+ </p>
63
+
64
+ Made with [contributors-img](https://contrib.rocks)
65
+
66
+ ## Requirements
67
+
68
+ - Python >= 3.8
69
+ - You also need to install `git` in your computer if you want to analyze a git repository.
70
+
71
+ ## Installation
72
+
73
+ ```bash
74
+ pip install complexipy
75
+ ```
76
+
77
+ ## Usage
78
+
79
+ To run **complexipy** you can use the following command:
80
+
81
+ ```shell
82
+ complexipy . # Use complexipy to analyze the current directory and any subdirectories
83
+ complexipy path/to/directory # Use complexipy to analyze a specific directory and any subdirectories
84
+ complexipy git_repository_url # Use complexipy to analyze a git repository
85
+ complexipy path/to/file.py # Use complexipy to analyze a specific file
86
+ complexipy path/to/file.py -c 20 # Use the -c option to set the maximum congnitive complexity, default is 15
87
+ complexipy path/to/directory -c 0 # Set the maximum cognitive complexity to 0 to disable the exit with error
88
+ complexipy path/to/directory -o # Use the -o option to output the results to a CSV file, default is False
89
+ complexipy path/to/directory -d low # Use the -d option to set detail level, default is "normal". If set to "low" will show only files with complexity greater than the maximum complexity
90
+ complexipy path/to/directory -l file # Use the -l option to set the level of measurement, default is "function". If set to "file" will measure the complexity of the file and will validate the maximum complexity according to the file complexity.
91
+ complexipy path/to/directory -q # Use the -q option to disable the output to the console, default is False.
92
+ complexipy path/to/directory -s desc # Use the -s option to set the sort order, default is "asc". If set to "desc" will sort the results in descending order. If set to "asc" will sort the results in ascending order. If set to "name" will sort the results by name.
93
+ ```
94
+
95
+ ### Options
96
+
97
+ - `-c` or `--max-complexity`: Set the maximum cognitive complexity, default is 15.
98
+ If the cognitive complexity of a file is greater than the maximum cognitive,
99
+ then the return code will be 1 and exit with error, otherwise it will be 0.
100
+ If set to 0, the exit with error will be disabled.
101
+ - `-o` or `--output`: Output the results to a CSV file, default is False. The
102
+ filename will be `complexipy.csv` and will be saved in the invocation directory.
103
+ - `-d` or `--detail`: Set the detail level, default is "normal". If set to "low"
104
+ will show only files or functions with complexity greater than the maximum
105
+ complexity.
106
+ - `-l` or `--level` Set the level of measurement, default is "function". If set
107
+ to "file" will measure the complexity of the file and will validate the maximum
108
+ complexity according to the file complexity. If set to "function" will measure
109
+ the complexity of the functions and will validate the maximum complexity
110
+ according to the function complexity. This option is useful if you want to set
111
+ a maximum complexity according for each file or for each function in the file
112
+ (or files).
113
+ - `-q` or `--quiet`: Disable the output to the console, default is False.
114
+ - `-s` or `--sort`: Set the sort order, default is "asc". If set to "desc" will
115
+ sort the results in descending order. If set to "asc" will sort the results in
116
+ ascending order. If set to "name" will sort the results by name. This option will
117
+ affect the output to the console and the output to the CSV file.
118
+
119
+ If the cognitive complexity of a file or a function is greater than the maximum
120
+ cognitive cognitive complexity, then the return code will be 1 and exit with
121
+ error, otherwise it will be 0.
122
+
123
+ ## Example
124
+
125
+ ### Analyzing a file
126
+
127
+ For example, given the following file:
128
+
129
+ ```python
130
+ def a_decorator(a, b):
131
+ def inner(func):
132
+ return func
133
+ return inner
134
+
135
+ def b_decorator(a, b):
136
+ def inner(func):
137
+ if func:
138
+ return None
139
+ return func
140
+ return inner
141
+ ```
142
+
143
+ The cognitive complexity of the file is 1, and the output of the command
144
+ `complexipy path/to/file.py` will be:
145
+
146
+ ```txt
147
+ ───────────────────────────── 🐙 complexipy 0.3.1 ──────────────────────────────
148
+ Summary
149
+ ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
150
+ ┃ Path ┃ File ┃ Function ┃ Complexity ┃
151
+ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
152
+ │ test_decorator.py │ test_decorator.py │ a_decorator │ 0 │
153
+ ├───────────────────┼───────────────────┼─────────────┼────────────┤
154
+ │ test_decorator.py │ test_decorator.py │ b_decorator │ 1 │
155
+ └───────────────────┴───────────────────┴─────────────┴────────────┘
156
+ 🧠 Total Cognitive Complexity in ./tests/src/test_decorator.py: 1
157
+ 1 file analyzed in 0.0032 seconds
158
+ ────────────────────────── 🎉 Analysis completed! 🎉 ───────────────────────────
159
+ ```
160
+
161
+ #### Explaining the results of the analysis
162
+
163
+ ```python
164
+ def a_decorator(a, b): # 0
165
+ def inner(func): # 0
166
+ return func # 0
167
+ return inner # 0
168
+
169
+ def b_decorator(a, b): # 0
170
+ def inner(func): # 0
171
+ if func: # 1 (nested = 0), total 1
172
+ return None # 0
173
+ return func # 0
174
+ return inner # 0
175
+ ```
176
+
177
+ The cognitive complexity of the file is 1, and the cognitive complexity of the
178
+ function `b_decorator` is 1. This example is simple, but it shows how
179
+ **complexipy** calculates the cognitive complexity according to the specifications
180
+ of the paper "Cognitive Complexity a new way to measure understandability",
181
+ considering the decorators and the if statement.
182
+
183
+ #### Output to a CSV file
184
+
185
+ If you want to output the results to a CSV file, you can use the `-o` option,
186
+ this is really useful if you want to integrate **complexipy** with other tools,
187
+ for example, a CI/CD pipeline. You will get the output in the console and will
188
+ create a CSV file with the results of the analysis.
189
+
190
+ The filename will be `complexipy.csv` and will be saved in the current directory.
191
+
192
+ ```bash
193
+ $ complexipy path/to/file.py -o
194
+ ```
195
+
196
+ The output will be:
197
+
198
+ ```csv
199
+ Path,File Name,Function Name,Cognitive Complexity
200
+ test_decorator.py,test_decorator.py,a_decorator,0
201
+ test_decorator.py,test_decorator.py,b_decorator,1
202
+ ```
203
+
204
+ ### Analyzing a directory
205
+
206
+ You can also analyze a directory, for example:
207
+
208
+ ```bash
209
+ $ complexipy .
210
+ ```
211
+
212
+ And **complexipy** will analyze all the files in the current directory and any
213
+ subdirectories.
214
+
215
+ ### Analyzing a git repository
216
+
217
+ You can also analyze a git repository, for example:
218
+
219
+ ```bash
220
+ $ complexipy https://github.com/rohaquinlop/complexipy
221
+ ```
222
+
223
+ And to generate the output to a CSV file:
224
+
225
+ ```bash
226
+ $ complexipy https://github.com/rohaquinlop/complexipy -o
227
+ ```
228
+
229
+ ## License
230
+
231
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/rohaquinlop/complexipy/blob/main/LICENSE) file
232
+ for details.
233
+
234
+ ## Acknowledgments
235
+
236
+ - Thanks to G. Ann Campbell for publishing the paper "Cognitive Complexity a new
237
+ way to measure understandability".
238
+ - This project is inspired by the Sonar way to calculate the cognitive
239
+ complexity.
240
+
241
+ ## References
242
+
243
+ - [Cognitive Complexity](https://www.sonarsource.com/resources/cognitive-complexity/)
244
+
@@ -0,0 +1,10 @@
1
+ complexipy-0.3.1.dist-info/METADATA,sha256=rZ67V2LDk3PZwrqwRWIDYZiZ_QSQ94IhcHI4Yawmge8,10740
2
+ complexipy-0.3.1.dist-info/WHEEL,sha256=1WVBudP2uGuWxretrBFqLgXGduBywn6TuK3XY8BMSpY,94
3
+ complexipy-0.3.1.dist-info/entry_points.txt,sha256=7BRIkSuvn7A9AIDAQhKigIXLQxrXjnz7bsB1wYis61g,49
4
+ complexipy-0.3.1.dist-info/license_files/LICENSE,sha256=b9V0Fd9UX8GbtBEyaglrPjW1ihY1Vfl1QWOYzdCd1MA,1092
5
+ complexipy/main.py,sha256=XNwRjm0tgTdrhR8Ku20uxToq2UqkftC-CeqlxvwjEcY,3381
6
+ complexipy/types.py,sha256=6jjU1jEIMj5_2jfceINKgfPYPLDA8zGQo8bR17jjVYE,341
7
+ complexipy/utils.py,sha256=YLfKQd43Zy6pMe-y0FBuTv5ePxMSnv3lLYJKG7XEKac,4285
8
+ complexipy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ complexipy/rust.cp39-win_amd64.pyd,sha256=e9_z7G9ezMyUmob6KGZAzxcvOKvk1CAgu1NPn7PkNUs,5762048
10
+ complexipy-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.5.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-none-win_amd64
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ complexipy=complexipy.main:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Robin Quintero
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.