complexipy 4.1.0__tar.gz → 4.2.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.
Files changed (42) hide show
  1. {complexipy-4.1.0 → complexipy-4.2.0}/Cargo.lock +1 -1
  2. {complexipy-4.1.0 → complexipy-4.2.0}/Cargo.toml +1 -1
  3. {complexipy-4.1.0 → complexipy-4.2.0}/PKG-INFO +32 -4
  4. {complexipy-4.1.0 → complexipy-4.2.0}/README.md +31 -3
  5. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/__init__.py +2 -0
  6. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/main.py +11 -1
  7. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/utils.py +33 -7
  8. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy.toml +2 -1
  9. {complexipy-4.1.0 → complexipy-4.2.0}/docs/index.md +27 -2
  10. {complexipy-4.1.0 → complexipy-4.2.0}/src/cognitive_complexity/mod.rs +117 -37
  11. {complexipy-4.1.0 → complexipy-4.2.0}/src/cognitive_complexity/utils.rs +25 -0
  12. {complexipy-4.1.0 → complexipy-4.2.0}/uv.lock +192 -146
  13. {complexipy-4.1.0 → complexipy-4.2.0}/.pre-commit-config.yaml +0 -0
  14. {complexipy-4.1.0 → complexipy-4.2.0}/LICENSE +0 -0
  15. {complexipy-4.1.0 → complexipy-4.2.0}/build-wasm.sh +0 -0
  16. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/_complexipy.pyi +0 -0
  17. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/api.py +0 -0
  18. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/error_handlers.py +0 -0
  19. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/py.typed +0 -0
  20. {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/types.py +0 -0
  21. {complexipy-4.1.0 → complexipy-4.2.0}/docs/img/complexipy_icon.svg +0 -0
  22. {complexipy-4.1.0 → complexipy-4.2.0}/mkdocs.yml +0 -0
  23. {complexipy-4.1.0 → complexipy-4.2.0}/pyproject.toml +0 -0
  24. {complexipy-4.1.0 → complexipy-4.2.0}/serve-web-version.sh +0 -0
  25. {complexipy-4.1.0 → complexipy-4.2.0}/src/classes/mod.rs +0 -0
  26. {complexipy-4.1.0 → complexipy-4.2.0}/src/lib.rs +0 -0
  27. {complexipy-4.1.0 → complexipy-4.2.0}/src/wasm.rs +0 -0
  28. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/.vscode-test.mjs +0 -0
  29. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/.vscodeignore +0 -0
  30. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/CHANGELOG.md +0 -0
  31. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/LICENSE +0 -0
  32. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/README.md +0 -0
  33. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/eslint.config.mjs +0 -0
  34. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/extension.js +0 -0
  35. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/img/complexipy.png +0 -0
  36. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/img/complexipy_icon.png +0 -0
  37. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/img/complexipy_icon.svg +0 -0
  38. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/jsconfig.json +0 -0
  39. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/package-lock.json +0 -0
  40. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/package.json +0 -0
  41. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/test/extension.test.js +0 -0
  42. {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/test/statusbar.test.js +0 -0
@@ -107,7 +107,7 @@ dependencies = [
107
107
 
108
108
  [[package]]
109
109
  name = "complexipy"
110
- version = "4.1.0"
110
+ version = "4.2.0"
111
111
  dependencies = [
112
112
  "console_error_panic_hook",
113
113
  "csv",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "complexipy"
3
- version = "4.1.0"
3
+ version = "4.2.0"
4
4
  edition = "2024"
5
5
  authors = ["Robin Quintero <rohaquinlop301@gmail.com>"]
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: complexipy
3
- Version: 4.1.0
3
+ Version: 4.2.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -33,7 +33,7 @@ Project-URL: Repository, https://github.com/rohaquinlop/complexipy
33
33
  # complexipy
34
34
 
35
35
  <div align="center">
36
- <img src="docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
36
+ <img src="https://raw.githubusercontent.com/rohaquinlop/complexipy/refs/heads/main/docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
37
37
 
38
38
  <p><em>Blazingly fast cognitive complexity analysis for Python, written in Rust.</em></p>
39
39
 
@@ -86,6 +86,9 @@ complexipy . --max-complexity-allowed 10
86
86
 
87
87
  # Save results to JSON/CSV
88
88
  complexipy . --output-json --output-csv
89
+
90
+ # Analyze current directory while excluding specific files
91
+ complexipy . --exclude path/to/exclude.py --exclude path/to/other/exclude.py
89
92
  ```
90
93
 
91
94
  ### Python API
@@ -168,6 +171,7 @@ quiet = false
168
171
  ignore-complexity = false
169
172
  details = "normal"
170
173
  sort = "asc"
174
+ exclude = []
171
175
 
172
176
  [output]
173
177
  csv = true
@@ -183,6 +187,7 @@ quiet = false
183
187
  ignore-complexity = false
184
188
  details = "normal"
185
189
  sort = "asc"
190
+ exclude = []
186
191
 
187
192
  [tool.complexipy.output]
188
193
  csv = true
@@ -193,6 +198,7 @@ json = true
193
198
 
194
199
  | Flag | Description | Default |
195
200
  |------|-------------|---------|
201
+ | `--exclude` | Exclude entries relative to each provided path. Entries resolve to existing directories (prefix match) or files (exact match). Non-existent entries are ignored. | |
196
202
  | `--max-complexity-allowed` | Complexity threshold | `15` |
197
203
  | `--output-json` | Save results as JSON | `false` |
198
204
  | `--output-csv` | Save results as CSV | `false` |
@@ -202,6 +208,28 @@ json = true
202
208
  | `--ignore-complexity` | Don't exit with error on threshold breach | `false` |
203
209
  | `--version` | Show installed complexipy version and exit | - |
204
210
 
211
+ Example:
212
+
213
+ ```
214
+ # Exclude only top-level 'tests' directory under the provided root
215
+ complexipy . --exclude tests
216
+ # This will not exclude './complexipy/utils.py' if you pass '--exclude utils' at repo root,
217
+ # because there is no './utils' directory or file at that level.
218
+ ```
219
+
220
+ ### Inline Ignores
221
+
222
+ You can explicitly ignore a known complex function inline, similar to Ruff's `C901` ignores:
223
+
224
+ ```python
225
+ def legacy_adapter(x, y): # noqa: complexipy (safe wrapper)
226
+ if x and y:
227
+ return x + y
228
+ return 0
229
+ ```
230
+
231
+ Place `# noqa: complexipy` on the function definition line (or the line immediately above). An optional reason can be provided in parentheses or plain text, it’s ignored by the parser.
232
+
205
233
  ## API Reference
206
234
 
207
235
  ```python
@@ -226,11 +254,11 @@ FunctionComplexity:
226
254
 
227
255
  <div align="center">
228
256
 
257
+ <sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource, G. Ann Campbell</sub>
258
+
229
259
  **[Documentation](https://rohaquinlop.github.io/complexipy/) • [PyPI](https://pypi.org/project/complexipy/) • [GitHub](https://github.com/rohaquinlop/complexipy)**
230
260
 
231
261
  <sub>Built with ❤️ by <a href="https://github.com/rohaquinlop">@rohaquinlop</a> and <a href="https://github.com/rohaquinlop/complexipy/graphs/contributors">contributors</a></sub>
232
262
 
233
- <sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource</sub>
234
-
235
263
  </div>
236
264
 
@@ -1,7 +1,7 @@
1
1
  # complexipy
2
2
 
3
3
  <div align="center">
4
- <img src="docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
4
+ <img src="https://raw.githubusercontent.com/rohaquinlop/complexipy/refs/heads/main/docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
5
5
 
6
6
  <p><em>Blazingly fast cognitive complexity analysis for Python, written in Rust.</em></p>
7
7
 
@@ -54,6 +54,9 @@ complexipy . --max-complexity-allowed 10
54
54
 
55
55
  # Save results to JSON/CSV
56
56
  complexipy . --output-json --output-csv
57
+
58
+ # Analyze current directory while excluding specific files
59
+ complexipy . --exclude path/to/exclude.py --exclude path/to/other/exclude.py
57
60
  ```
58
61
 
59
62
  ### Python API
@@ -136,6 +139,7 @@ quiet = false
136
139
  ignore-complexity = false
137
140
  details = "normal"
138
141
  sort = "asc"
142
+ exclude = []
139
143
 
140
144
  [output]
141
145
  csv = true
@@ -151,6 +155,7 @@ quiet = false
151
155
  ignore-complexity = false
152
156
  details = "normal"
153
157
  sort = "asc"
158
+ exclude = []
154
159
 
155
160
  [tool.complexipy.output]
156
161
  csv = true
@@ -161,6 +166,7 @@ json = true
161
166
 
162
167
  | Flag | Description | Default |
163
168
  |------|-------------|---------|
169
+ | `--exclude` | Exclude entries relative to each provided path. Entries resolve to existing directories (prefix match) or files (exact match). Non-existent entries are ignored. | |
164
170
  | `--max-complexity-allowed` | Complexity threshold | `15` |
165
171
  | `--output-json` | Save results as JSON | `false` |
166
172
  | `--output-csv` | Save results as CSV | `false` |
@@ -170,6 +176,28 @@ json = true
170
176
  | `--ignore-complexity` | Don't exit with error on threshold breach | `false` |
171
177
  | `--version` | Show installed complexipy version and exit | - |
172
178
 
179
+ Example:
180
+
181
+ ```
182
+ # Exclude only top-level 'tests' directory under the provided root
183
+ complexipy . --exclude tests
184
+ # This will not exclude './complexipy/utils.py' if you pass '--exclude utils' at repo root,
185
+ # because there is no './utils' directory or file at that level.
186
+ ```
187
+
188
+ ### Inline Ignores
189
+
190
+ You can explicitly ignore a known complex function inline, similar to Ruff's `C901` ignores:
191
+
192
+ ```python
193
+ def legacy_adapter(x, y): # noqa: complexipy (safe wrapper)
194
+ if x and y:
195
+ return x + y
196
+ return 0
197
+ ```
198
+
199
+ Place `# noqa: complexipy` on the function definition line (or the line immediately above). An optional reason can be provided in parentheses or plain text, it’s ignored by the parser.
200
+
173
201
  ## API Reference
174
202
 
175
203
  ```python
@@ -194,10 +222,10 @@ FunctionComplexity:
194
222
 
195
223
  <div align="center">
196
224
 
225
+ <sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource, G. Ann Campbell</sub>
226
+
197
227
  **[Documentation](https://rohaquinlop.github.io/complexipy/) • [PyPI](https://pypi.org/project/complexipy/) • [GitHub](https://github.com/rohaquinlop/complexipy)**
198
228
 
199
229
  <sub>Built with ❤️ by <a href="https://github.com/rohaquinlop">@rohaquinlop</a> and <a href="https://github.com/rohaquinlop/complexipy/graphs/contributors">contributors</a></sub>
200
230
 
201
- <sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource</sub>
202
-
203
231
  </div>
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from complexipy._complexipy import (
2
4
  CodeComplexity,
3
5
  FileComplexity,
@@ -51,6 +51,12 @@ def main(
51
51
  None,
52
52
  help="Paths to the directories or files to analyze, it can be a local paths or a git repository URL.",
53
53
  ),
54
+ exclude: Optional[List[str]] = typer.Option(
55
+ None,
56
+ "--exclude",
57
+ "-e",
58
+ help="Paths to the directories or files to exclude.",
59
+ ),
54
60
  max_complexity_allowed: Optional[int] = typer.Option(
55
61
  None,
56
62
  "--max-complexity-allowed",
@@ -110,6 +116,7 @@ def main(
110
116
  sort,
111
117
  output_csv,
112
118
  output_json,
119
+ exclude,
113
120
  ) = get_arguments_value(
114
121
  TOML_CONFIG,
115
122
  paths,
@@ -120,6 +127,7 @@ def main(
120
127
  sort,
121
128
  output_csv,
122
129
  output_json,
130
+ exclude,
123
131
  )
124
132
 
125
133
  if not quiet:
@@ -128,7 +136,9 @@ def main(
128
136
  else:
129
137
  console.rule(":octopus: complexipy")
130
138
  start_time = time.time()
131
- result: Tuple[List[FileComplexity], List[str]] = _complexipy.main(paths, quiet)
139
+ result: Tuple[List[FileComplexity], List[str]] = _complexipy.main(
140
+ paths, quiet, exclude
141
+ )
132
142
  files_complexities, failed_paths = result
133
143
  execution_time = time.time() - start_time
134
144
  output_csv_path = f"{INVOCATION_PATH}/complexipy.csv"
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import sys
3
5
  from complexipy.types import DetailTypes, Sort, TOMLType
@@ -35,11 +37,32 @@ def get_brain_icon():
35
37
  return ":brain:"
36
38
 
37
39
 
38
- def load_values_from_toml_key(value: str) -> TOMLType:
39
- if value == "details":
40
- DetailTypes[value]
41
- if value == "sort":
42
- Sort[value]
40
+ def load_values_from_toml_key(key: str, value: TOMLType) -> TOMLType:
41
+ """Normalize TOML values to expected runtime types.
42
+
43
+ - Convert `details` and `sort` string values to their Enum variants.
44
+ - Ensure `paths` and `exclude` are lists when provided as strings.
45
+ """
46
+ if key == "details":
47
+ # Accept either Enum or string in TOML
48
+ if isinstance(value, DetailTypes):
49
+ return value
50
+ if isinstance(value, str):
51
+ return DetailTypes(value)
52
+ return value
53
+
54
+ if key == "sort":
55
+ if isinstance(value, Sort):
56
+ return value
57
+ if isinstance(value, str):
58
+ return Sort(value)
59
+ return value
60
+
61
+ if key in ("paths", "exclude"):
62
+ # Allow a single string, normalize to list for downstream processing
63
+ if isinstance(value, str):
64
+ return [value]
65
+ return value
43
66
 
44
67
  return value
45
68
 
@@ -69,7 +92,7 @@ def load_toml_config(invocation_path: str, config_file_name: str) -> TOMLType |
69
92
  return None
70
93
 
71
94
  for key, value in data.items():
72
- data[key] = load_values_from_toml_key(value)
95
+ data[key] = load_values_from_toml_key(key, value)
73
96
 
74
97
  return data
75
98
 
@@ -125,7 +148,8 @@ def get_arguments_value(
125
148
  sort_arg: Sort | None,
126
149
  output_csv: bool | None,
127
150
  output_json: bool | None,
128
- ) -> Tuple[List[str], int, bool, bool, DetailTypes, Sort, bool, bool]:
151
+ exclude: List[str] | None,
152
+ ) -> Tuple[List[str], int, bool, bool, DetailTypes, Sort, bool, bool, List[str]]:
129
153
  paths = get_argument_value(toml_config, "paths", paths)
130
154
  max_complexity_allowed = get_argument_value(
131
155
  toml_config, "max-complexity-allowed", max_complexity_allowed, 15
@@ -138,6 +162,7 @@ def get_arguments_value(
138
162
  sort_arg = get_argument_value(toml_config, "sort", sort_arg, Sort.asc)
139
163
  output_csv = get_argument_value(toml_config, "output-csv", output_csv, False)
140
164
  output_json = get_argument_value(toml_config, "output-json", output_json, False)
165
+ exclude = get_argument_value(toml_config, "exclude", exclude, [])
141
166
 
142
167
  return (
143
168
  paths,
@@ -148,6 +173,7 @@ def get_arguments_value(
148
173
  sort_arg,
149
174
  output_csv,
150
175
  output_json,
176
+ exclude,
151
177
  )
152
178
 
153
179
 
@@ -1,9 +1,10 @@
1
- paths = ["src", "complexipy"]
1
+ paths = ["."]
2
2
  max-complexity-allowed = 15
3
3
  quiet = false
4
4
  ignore-complexity = false
5
5
  details = "normal"
6
6
  sort = "asc"
7
+ exclude = ["tests"]
7
8
 
8
9
  [output]
9
10
  csv = false
@@ -67,6 +67,10 @@ complexipy . --max-complexity-allowed 10
67
67
 
68
68
  # Generate reports
69
69
  complexipy . --output-json --output-csv
70
+
71
+ # Analyze current directory while excluding specific files
72
+ complexipy . --exclude file_to_exclude.py --exclude path/to/other/exclude.py
73
+
70
74
  ```
71
75
 
72
76
  ### Python API
@@ -131,6 +135,7 @@ quiet = false
131
135
  ignore-complexity = false
132
136
  details = "normal"
133
137
  sort = "asc"
138
+ exclude = []
134
139
 
135
140
  [output]
136
141
  csv = true
@@ -148,6 +153,7 @@ quiet = false
148
153
  ignore-complexity = false
149
154
  details = "normal"
150
155
  sort = "asc"
156
+ exclude = []
151
157
 
152
158
  [tool.complexipy.output]
153
159
  csv = true
@@ -161,6 +167,7 @@ json = true
161
167
 
162
168
  | Option | Description | Default |
163
169
  |--------|-------------|---------|
170
+ | `--exclude` | Exclude entries relative to each provided path. Entries resolve to existing directories (prefix match) or files (exact match). Non-existent entries are ignored. | |
164
171
  | `--max-complexity-allowed <N>` | Complexity threshold for warnings | `15` |
165
172
  | `--output-json` | Export results to JSON | `false` |
166
173
  | `--output-csv` | Export results to CSV | `false` |
@@ -181,11 +188,29 @@ complexipy . --max-complexity-allowed 8 --output-json
181
188
 
182
189
  # Silent analysis sorted by name
183
190
  complexipy src/ --sort name --quiet
191
+
192
+ # Exclude specific paths relative to root
193
+ complexipy . --exclude tests --exclude utils
194
+ # Note: this excludes only './tests/**' and './utils/**' under '.',
195
+ # it does not exclude './complexipy/utils.py'.
184
196
  ```
185
197
 
186
198
  !!! tip "Exit Codes"
187
199
  complexipy returns exit code `1` when functions exceed the complexity threshold. Use `--ignore-complexity` to suppress this behavior in CI/CD pipelines.
188
200
 
201
+ ### Inline Ignores
202
+
203
+ You can explicitly ignore a known complex function inline, similar to Ruff's `C901` ignores:
204
+
205
+ ```python
206
+ def legacy_adapter(x, y): # noqa: complexipy (safe wrapper)
207
+ if x and y:
208
+ return x + y
209
+ return 0
210
+ ```
211
+
212
+ Place `# noqa: complexipy` on the function definition line (or the line immediately above). An optional reason can be provided in parentheses or plain text, it’s ignored by the parser.
213
+
189
214
  ## API Reference
190
215
 
191
216
  ### Functions
@@ -366,7 +391,7 @@ def process_orders(orders): # Base: 0
366
391
  ```bash
367
392
  $ complexipy example.py
368
393
 
369
- ───────────────────────────── 🐙 complexipy 4.0.0 ──────────────────────────────
394
+ ───────────────────────────── 🐙 complexipy ──────────────────────────────
370
395
  Summary
371
396
  ┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
372
397
  ┃ Path ┃ File ┃ Function ┃ Complexity ┃
@@ -385,7 +410,7 @@ $ complexipy example.py
385
410
 
386
411
  ## Links & Resources
387
412
 
388
- - **📚 [Research Paper](https://www.sonarsource.com/resources/cognitive-complexity/)** - Original cognitive complexity research by SonarSource
413
+ - **📚 [Research Paper](https://www.sonarsource.com/resources/cognitive-complexity/)** - Original cognitive complexity research by SonarSource, G. Ann Campbell
389
414
  - **🐙 [GitHub Repository](https://github.com/rohaquinlop/complexipy)** - Source code and issue tracking
390
415
  - **📦 [PyPI Package](https://pypi.org/project/complexipy/)** - Installation and version history
391
416
  - **🛠️ [GitHub Action](https://github.com/marketplace/actions/complexipy)** - CI/CD integration
@@ -2,7 +2,7 @@ pub mod utils;
2
2
 
3
3
  #[cfg(any(feature = "python", feature = "wasm"))]
4
4
  mod shared_deps {
5
- pub use super::utils::{count_bool_ops, get_line_number, is_decorator};
5
+ pub use super::utils::{count_bool_ops, get_line_number, has_noqa_complexipy, is_decorator};
6
6
  pub use crate::classes::{CodeComplexity, FileComplexity, FunctionComplexity, LineComplexity};
7
7
  pub use ruff_python_ast::{self as ast, Stmt};
8
8
  }
@@ -37,28 +37,34 @@ type ComplexitiesAndFailedPaths = (Vec<FileComplexity>, Vec<String>);
37
37
 
38
38
  #[cfg(feature = "python")]
39
39
  #[pyfunction]
40
- pub fn main(paths: Vec<&str>, quiet: bool) -> PyResult<ComplexitiesAndFailedPaths> {
40
+ pub fn main(
41
+ paths: Vec<&str>,
42
+ quiet: bool,
43
+ exclude: Vec<&str>,
44
+ ) -> PyResult<ComplexitiesAndFailedPaths> {
41
45
  let re = Regex::new(r"^(https:\/\/|http:\/\/|www\.|git@)(github|gitlab)\.com(\/[\w.-]+){2,}$")
42
46
  .unwrap();
43
47
 
44
- let all_files_paths: Vec<(&str, bool, bool, bool)> = paths
48
+ let all_files_paths: Vec<(&str, bool, bool, bool, Vec<&str>)> = paths
45
49
  .iter()
46
50
  .map(|&path| {
47
51
  let is_url = re.is_match(path);
48
52
 
49
53
  if is_url {
50
- (path, false, true, quiet)
54
+ (path, false, true, quiet, exclude.clone())
51
55
  } else if metadata(path).unwrap().is_dir() {
52
- (path, true, false, quiet)
56
+ (path, true, false, quiet, exclude.clone())
53
57
  } else {
54
- (path, false, false, quiet)
58
+ (path, false, false, quiet, exclude.clone())
55
59
  }
56
60
  })
57
61
  .collect();
58
62
 
59
63
  let all_files_processed: Vec<Result<ComplexitiesAndFailedPaths, PyErr>> = all_files_paths
60
64
  .iter()
61
- .map(|(path, is_dir, is_url, quiet)| process_path(path, *is_dir, *is_url, *quiet))
65
+ .map(|(path, is_dir, is_url, quiet, exclude)| {
66
+ process_path(path, *is_dir, *is_url, *quiet, exclude.clone())
67
+ })
62
68
  .collect();
63
69
 
64
70
  let mut successful = Vec::new();
@@ -83,6 +89,7 @@ pub fn process_path(
83
89
  is_dir: bool,
84
90
  is_url: bool,
85
91
  quiet: bool,
92
+ exclude: Vec<&str>,
86
93
  ) -> Result<ComplexitiesAndFailedPaths, PyErr> {
87
94
  let mut file_complexities = Vec::new();
88
95
  let mut failed_paths = Vec::new();
@@ -121,13 +128,13 @@ pub fn process_path(
121
128
  }
122
129
 
123
130
  let repo_path = dir.path().join(&repo_name).to_str().unwrap().to_string();
124
- let (complexities, f_paths) = evaluate_dir(&repo_path, quiet);
131
+ let (complexities, f_paths) = evaluate_dir(&repo_path, quiet, exclude.clone());
125
132
  dir.close()?;
126
133
 
127
134
  file_complexities = complexities;
128
135
  failed_paths = f_paths;
129
136
  } else if is_dir {
130
- let (complexities, f_paths) = evaluate_dir(path, quiet);
137
+ let (complexities, f_paths) = evaluate_dir(path, quiet, exclude.clone());
131
138
  file_complexities = complexities;
132
139
  failed_paths = f_paths;
133
140
  } else {
@@ -149,18 +156,85 @@ pub fn process_path(
149
156
  }
150
157
 
151
158
  #[cfg(feature = "python")]
152
- fn evaluate_dir(path: &str, quiet: bool) -> ComplexitiesAndFailedPaths {
159
+ fn evaluate_dir(path: &str, quiet: bool, exclude: Vec<&str>) -> ComplexitiesAndFailedPaths {
153
160
  let mut files_paths: Vec<String> = Vec::new();
154
161
 
155
162
  let parent_dir = path::Path::new(path).parent().unwrap().to_str().unwrap();
156
163
 
164
+ // Build path-aware exclude specs, relative to the provided root `path`.
165
+ // Only exclude if the exclude entry resolves to an existing directory (prefix match)
166
+ // or file (exact match) under the root. Otherwise, ignore the entry.
167
+ #[derive(Clone)]
168
+ struct ExcludeSpec {
169
+ // canonical, normalized absolute path
170
+ abs: String,
171
+ is_dir: bool,
172
+ }
173
+
174
+ let root_canon = match path::Path::new(path).canonicalize() {
175
+ Ok(p) => p,
176
+ Err(_) => path::PathBuf::from(path),
177
+ };
178
+
179
+ let mut exclude_specs: Vec<ExcludeSpec> = Vec::new();
180
+ for raw in exclude.iter() {
181
+ let mut candidate: path::PathBuf;
182
+ let p = path::Path::new(raw);
183
+ if p.is_absolute() {
184
+ candidate = p.to_path_buf();
185
+ } else {
186
+ candidate = root_canon.join(p);
187
+ }
188
+
189
+ // Resolve to canonical if possible
190
+ let (exists, is_dir, is_file, abs_str) = match candidate.canonicalize() {
191
+ Ok(canon) => {
192
+ let is_dir = canon.is_dir();
193
+ let is_file = canon.is_file();
194
+ let s = canon.to_string_lossy().replace('\\', "/");
195
+ (true, is_dir, is_file, s)
196
+ }
197
+ Err(_) => (false, false, false, String::new()),
198
+ };
199
+
200
+ if exists {
201
+ if is_dir || is_file {
202
+ exclude_specs.push(ExcludeSpec { abs: abs_str, is_dir });
203
+ }
204
+ }
205
+ // If it doesn't exist under the root, ignore the exclude entry
206
+ }
207
+
157
208
  // Get all the python files in the directory
158
209
  for entry in Walk::new(path) {
159
210
  let entry = entry.unwrap();
160
- let file_path_str = entry.path().to_str().unwrap();
211
+ let entry_path = entry.path();
161
212
 
162
- if entry.path().extension().and_then(|s| s.to_str()) == Some("py") {
163
- files_paths.push(file_path_str.to_string());
213
+ if entry_path.extension().and_then(|s| s.to_str()) != Some("py") {
214
+ continue;
215
+ }
216
+
217
+ // Determine if this file should be excluded using path-aware specs
218
+ let file_abs = match entry_path.canonicalize() {
219
+ Ok(p) => p,
220
+ Err(_) => entry_path.to_path_buf(),
221
+ };
222
+ let file_abs_str = file_abs.to_string_lossy().replace('\\', "/");
223
+
224
+ let is_excluded = exclude_specs.iter().any(|spec| {
225
+ if spec.is_dir {
226
+ // Directory prefix match
227
+ file_abs_str.starts_with(&spec.abs)
228
+ } else {
229
+ // Exact file match
230
+ file_abs_str == spec.abs
231
+ }
232
+ });
233
+
234
+ if !is_excluded {
235
+ if let Some(file_path_str) = entry_path.to_str() {
236
+ files_paths.push(file_path_str.to_string());
237
+ }
164
238
  }
165
239
  }
166
240
 
@@ -289,34 +363,40 @@ pub fn function_level_cognitive_complexity_shared(
289
363
  for node in ast_body.iter() {
290
364
  match node {
291
365
  Stmt::FunctionDef(f) => {
292
- let (func_complexity, line_complexities) =
293
- statement_cognitive_complexity_shared(node, 0, code);
294
-
295
- let function = FunctionComplexity {
296
- name: f.name.to_string(),
297
- complexity: func_complexity,
298
- line_start: get_line_number(usize::from(f.range.start()), code),
299
- line_end: get_line_number(usize::from(f.range.end()), code),
300
- line_complexities,
301
- };
302
-
303
- functions.push(function);
366
+ let start_line = get_line_number(usize::from(f.range.start()), code);
367
+ if !has_noqa_complexipy(start_line, code) {
368
+ let (func_complexity, line_complexities) =
369
+ statement_cognitive_complexity_shared(node, 0, code);
370
+
371
+ let function = FunctionComplexity {
372
+ name: f.name.to_string(),
373
+ complexity: func_complexity,
374
+ line_start: start_line,
375
+ line_end: get_line_number(usize::from(f.range.end()), code),
376
+ line_complexities,
377
+ };
378
+
379
+ functions.push(function);
380
+ }
304
381
  }
305
382
  Stmt::ClassDef(c) => {
306
383
  for node in c.body.iter() {
307
384
  if let Stmt::FunctionDef(f) = node {
308
- let (func_complexity, line_complexities) =
309
- statement_cognitive_complexity_shared(node, 0, code);
310
-
311
- let function = FunctionComplexity {
312
- name: format!("{}::{}", c.name, f.name),
313
- complexity: func_complexity,
314
- line_start: get_line_number(usize::from(f.range.start()), code),
315
- line_end: get_line_number(usize::from(f.range.end()), code),
316
- line_complexities,
317
- };
318
-
319
- functions.push(function);
385
+ let start_line = get_line_number(usize::from(f.range.start()), code);
386
+ if !has_noqa_complexipy(start_line, code) {
387
+ let (func_complexity, line_complexities) =
388
+ statement_cognitive_complexity_shared(node, 0, code);
389
+
390
+ let function = FunctionComplexity {
391
+ name: format!("{}::{}", c.name, f.name),
392
+ complexity: func_complexity,
393
+ line_start: start_line,
394
+ line_end: get_line_number(usize::from(f.range.end()), code),
395
+ line_complexities,
396
+ };
397
+
398
+ functions.push(function);
399
+ }
320
400
  }
321
401
  }
322
402
  }
@@ -241,3 +241,28 @@ pub fn get_line_number(byte_index: usize, code: &str) -> u64 {
241
241
  let newline_count = before_slice.chars().filter(|&c| c == '\n').count();
242
242
  (newline_count + 1) as u64
243
243
  }
244
+
245
+ #[cfg(any(feature = "python", feature = "wasm"))]
246
+ pub fn has_noqa_complexipy(line_number: u64, code: &str) -> bool {
247
+ if line_number == 0 {
248
+ return false;
249
+ }
250
+
251
+ let lines: Vec<&str> = code.lines().collect();
252
+ let idx = (line_number as usize).saturating_sub(1);
253
+
254
+ let contains_marker = |s: &str| -> bool {
255
+ let lower = s.to_lowercase();
256
+ lower.contains("noqa: complexipy")
257
+ };
258
+
259
+ if idx < lines.len() && contains_marker(lines[idx]) {
260
+ return true;
261
+ }
262
+
263
+ if idx > 0 && contains_marker(lines[idx - 1]) {
264
+ return true;
265
+ }
266
+
267
+ false
268
+ }