pyrefactor 1.0.5__tar.gz → 1.0.7__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.
- {pyrefactor-1.0.5/src/pyrefactor.egg-info → pyrefactor-1.0.7}/PKG-INFO +48 -9
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/README.md +31 -4
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/pyproject.toml +30 -9
- pyrefactor-1.0.7/src/pyrefactor/__init__.py +5 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/__main__.py +16 -11
- pyrefactor-1.0.7/src/pyrefactor/_version.py +33 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/analyzer.py +86 -50
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/ast_visitor.py +121 -10
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/config.py +165 -12
- pyrefactor-1.0.7/src/pyrefactor/detectors/boolean_logic.py +154 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/comparisons.py +6 -3
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/complexity.py +9 -6
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/context_manager.py +21 -17
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/duplication.py +2 -2
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/performance.py +48 -114
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/models.py +2 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/reporter.py +1 -1
- {pyrefactor-1.0.5 → pyrefactor-1.0.7/src/pyrefactor.egg-info}/PKG-INFO +48 -9
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/SOURCES.txt +5 -6
- pyrefactor-1.0.7/src/pyrefactor.egg-info/requires.txt +16 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_analyzer.py +90 -4
- pyrefactor-1.0.7/tests/test_ast_visitor.py +82 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_boolean_logic_detector.py +4 -8
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_cli.py +16 -3
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_comparisons_detector.py +6 -6
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_complexity_detector.py +7 -6
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_config.py +104 -0
- pyrefactor-1.0.7/tests/test_config_discovery.py +64 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_context_manager_detector.py +18 -5
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_models.py +61 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_reporter.py +56 -0
- pyrefactor-1.0.7/tests/test_version.py +50 -0
- pyrefactor-1.0.5/src/pyrefactor/__init__.py +0 -3
- pyrefactor-1.0.5/src/pyrefactor/detectors/boolean_logic.py +0 -231
- pyrefactor-1.0.5/src/pyrefactor.egg-info/requires.txt +0 -1
- pyrefactor-1.0.5/tests/test_hypothesis_analyzer.py +0 -499
- pyrefactor-1.0.5/tests/test_hypothesis_ast_visitor.py +0 -563
- pyrefactor-1.0.5/tests/test_hypothesis_config.py +0 -382
- pyrefactor-1.0.5/tests/test_hypothesis_models.py +0 -429
- pyrefactor-1.0.5/tests/test_hypothesis_reporter.py +0 -526
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/LICENSE.md +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/setup.cfg +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/__init__.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/control_flow.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/dict_operations.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/detectors/loops.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor/py.typed +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/dependency_links.txt +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/entry_points.txt +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/top_level.txt +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_control_flow_detector.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_dict_operations_detector.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_duplication_detector.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_integration.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_loops_detector.py +0 -0
- {pyrefactor-1.0.5 → pyrefactor-1.0.7}/tests/test_performance_detector.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrefactor
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A Python refactoring and optimization linter that analyzes code for performance issues, complexity problems, and opportunities for improvement
|
|
5
5
|
Author: tboy1337
|
|
6
6
|
Maintainer: tboy1337
|
|
@@ -20,11 +20,9 @@ Classifier: Operating System :: POSIX :: Linux
|
|
|
20
20
|
Classifier: Operating System :: MacOS
|
|
21
21
|
Classifier: Programming Language :: Python
|
|
22
22
|
Classifier: Programming Language :: Python :: 3
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
26
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
27
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
26
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
29
27
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
30
28
|
Classifier: Topic :: Software Development
|
|
@@ -35,17 +33,31 @@ Classifier: Topic :: Utilities
|
|
|
35
33
|
Classifier: Typing :: Typed
|
|
36
34
|
Classifier: Environment :: Console
|
|
37
35
|
Classifier: Natural Language :: English
|
|
38
|
-
Requires-Python: >=3.
|
|
36
|
+
Requires-Python: >=3.12
|
|
39
37
|
Description-Content-Type: text/markdown
|
|
40
38
|
License-File: LICENSE.md
|
|
41
39
|
Requires-Dist: colorama
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
46
|
+
Requires-Dist: pylint; extra == "dev"
|
|
47
|
+
Requires-Dist: mypy; extra == "dev"
|
|
48
|
+
Requires-Dist: autopep8; extra == "dev"
|
|
49
|
+
Requires-Dist: black; extra == "dev"
|
|
50
|
+
Requires-Dist: isort; extra == "dev"
|
|
51
|
+
Requires-Dist: types-colorama; extra == "dev"
|
|
52
|
+
Requires-Dist: bandit; extra == "dev"
|
|
53
|
+
Requires-Dist: safety; extra == "dev"
|
|
42
54
|
Dynamic: license-file
|
|
43
55
|
|
|
44
56
|
# PyRefactor
|
|
45
57
|
|
|
46
58
|
A Python refactoring and optimization linter that uses AST analysis to identify performance issues, complexity problems, and code improvements.
|
|
47
59
|
|
|
48
|
-
[](https://www.python.org/downloads/)
|
|
49
61
|
|
|
50
62
|
## Features
|
|
51
63
|
|
|
@@ -87,7 +99,7 @@ cd PyRefactor
|
|
|
87
99
|
pip install -e .
|
|
88
100
|
```
|
|
89
101
|
|
|
90
|
-
**Requirements**: Python 3.
|
|
102
|
+
**Requirements**: Python 3.12+
|
|
91
103
|
|
|
92
104
|
## Usage
|
|
93
105
|
|
|
@@ -197,7 +209,7 @@ jobs:
|
|
|
197
209
|
- uses: actions/checkout@v3
|
|
198
210
|
- uses: actions/setup-python@v4
|
|
199
211
|
with:
|
|
200
|
-
python-version: '3.
|
|
212
|
+
python-version: '3.12'
|
|
201
213
|
- run: pip install pyrefactor
|
|
202
214
|
- run: pyrefactor --min-severity medium src/
|
|
203
215
|
```
|
|
@@ -206,8 +218,35 @@ jobs:
|
|
|
206
218
|
|
|
207
219
|
Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
|
|
208
220
|
|
|
221
|
+
### Development
|
|
222
|
+
|
|
223
|
+
Install the package with development dependencies:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
pip install -e ".[dev]"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Alternatively:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip install -e .
|
|
233
|
+
pip install -r requirements-dev.txt
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Run the local verification script (formatting, type checks, lint, security scan, tests):
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
py scripts/verify.py
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Run tests directly:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
pytest
|
|
246
|
+
```
|
|
247
|
+
|
|
209
248
|
1. Follow existing code style (Black, isort)
|
|
210
|
-
2. Add tests for new features (>
|
|
249
|
+
2. Add tests for new features (>90% coverage)
|
|
211
250
|
3. Run type checking and linting
|
|
212
251
|
|
|
213
252
|
## License
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Python refactoring and optimization linter that uses AST analysis to identify performance issues, complexity problems, and code improvements.
|
|
4
4
|
|
|
5
|
-
[](https://www.python.org/downloads/)
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -44,7 +44,7 @@ cd PyRefactor
|
|
|
44
44
|
pip install -e .
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
**Requirements**: Python 3.
|
|
47
|
+
**Requirements**: Python 3.12+
|
|
48
48
|
|
|
49
49
|
## Usage
|
|
50
50
|
|
|
@@ -154,7 +154,7 @@ jobs:
|
|
|
154
154
|
- uses: actions/checkout@v3
|
|
155
155
|
- uses: actions/setup-python@v4
|
|
156
156
|
with:
|
|
157
|
-
python-version: '3.
|
|
157
|
+
python-version: '3.12'
|
|
158
158
|
- run: pip install pyrefactor
|
|
159
159
|
- run: pyrefactor --min-severity medium src/
|
|
160
160
|
```
|
|
@@ -163,8 +163,35 @@ jobs:
|
|
|
163
163
|
|
|
164
164
|
Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
|
|
165
165
|
|
|
166
|
+
### Development
|
|
167
|
+
|
|
168
|
+
Install the package with development dependencies:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pip install -e ".[dev]"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Alternatively:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pip install -e .
|
|
178
|
+
pip install -r requirements-dev.txt
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Run the local verification script (formatting, type checks, lint, security scan, tests):
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
py scripts/verify.py
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Run tests directly:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pytest
|
|
191
|
+
```
|
|
192
|
+
|
|
166
193
|
1. Follow existing code style (Black, isort)
|
|
167
|
-
2. Add tests for new features (>
|
|
194
|
+
2. Add tests for new features (>90% coverage)
|
|
168
195
|
3. Run type checking and linting
|
|
169
196
|
|
|
170
197
|
## License
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77.0", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyrefactor"
|
|
7
|
-
|
|
7
|
+
version = "1.0.7"
|
|
8
8
|
description = "A Python refactoring and optimization linter that analyzes code for performance issues, complexity problems, and opportunities for improvement"
|
|
9
9
|
authors = [{name = "tboy1337"}]
|
|
10
10
|
maintainers = [{name = "tboy1337"}]
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = {text = "Commercial Restricted License (CRL)"}
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
14
|
keywords = [
|
|
15
15
|
"refactoring",
|
|
16
16
|
"linter",
|
|
@@ -38,11 +38,9 @@ classifiers = [
|
|
|
38
38
|
"Operating System :: MacOS",
|
|
39
39
|
"Programming Language :: Python",
|
|
40
40
|
"Programming Language :: Python :: 3",
|
|
41
|
-
"Programming Language :: Python :: 3.9",
|
|
42
|
-
"Programming Language :: Python :: 3.10",
|
|
43
|
-
"Programming Language :: Python :: 3.11",
|
|
44
41
|
"Programming Language :: Python :: 3.12",
|
|
45
42
|
"Programming Language :: Python :: 3.13",
|
|
43
|
+
"Programming Language :: Python :: 3.14",
|
|
46
44
|
"Programming Language :: Python :: 3 :: Only",
|
|
47
45
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
48
46
|
"Topic :: Software Development",
|
|
@@ -58,6 +56,23 @@ dependencies = [
|
|
|
58
56
|
"colorama",
|
|
59
57
|
]
|
|
60
58
|
|
|
59
|
+
[project.optional-dependencies]
|
|
60
|
+
dev = [
|
|
61
|
+
"pytest",
|
|
62
|
+
"pytest-timeout",
|
|
63
|
+
"pytest-xdist",
|
|
64
|
+
"pytest-mock",
|
|
65
|
+
"pytest-cov",
|
|
66
|
+
"pylint",
|
|
67
|
+
"mypy",
|
|
68
|
+
"autopep8",
|
|
69
|
+
"black",
|
|
70
|
+
"isort",
|
|
71
|
+
"types-colorama",
|
|
72
|
+
"bandit",
|
|
73
|
+
"safety",
|
|
74
|
+
]
|
|
75
|
+
|
|
61
76
|
[project.urls]
|
|
62
77
|
Homepage = "https://github.com/tboy1337/PyRefactor"
|
|
63
78
|
Repository = "https://github.com/tboy1337/PyRefactor"
|
|
@@ -68,15 +83,20 @@ Documentation = "https://github.com/tboy1337/PyRefactor#readme"
|
|
|
68
83
|
[project.scripts]
|
|
69
84
|
pyrefactor = "pyrefactor.__main__:main"
|
|
70
85
|
|
|
71
|
-
[tool.setuptools
|
|
72
|
-
|
|
86
|
+
[tool.setuptools]
|
|
87
|
+
package-dir = {"" = "src"}
|
|
88
|
+
|
|
89
|
+
[tool.setuptools.packages.find]
|
|
90
|
+
where = ["src"]
|
|
91
|
+
include = ["pyrefactor*"]
|
|
92
|
+
exclude = ["tests*"]
|
|
73
93
|
|
|
74
94
|
[tool.setuptools.package-data]
|
|
75
95
|
pyrefactor = ["py.typed"]
|
|
76
96
|
|
|
77
97
|
[tool.black]
|
|
78
98
|
line-length = 88
|
|
79
|
-
target-version = ['
|
|
99
|
+
target-version = ['py312']
|
|
80
100
|
|
|
81
101
|
[tool.isort]
|
|
82
102
|
profile = "black"
|
|
@@ -86,3 +106,4 @@ include_trailing_comma = true
|
|
|
86
106
|
force_grid_wrap = 0
|
|
87
107
|
use_parentheses = true
|
|
88
108
|
ensure_newline_before_comments = true
|
|
109
|
+
known_first_party = ["pyrefactor"]
|
|
@@ -65,7 +65,11 @@ def _add_parser_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
65
65
|
help="Minimum severity level to report (default: info)",
|
|
66
66
|
)
|
|
67
67
|
parser.add_argument(
|
|
68
|
-
"-j",
|
|
68
|
+
"-j",
|
|
69
|
+
"--jobs",
|
|
70
|
+
type=int,
|
|
71
|
+
default=4,
|
|
72
|
+
help="Number of parallel jobs (default: 4, minimum 1)",
|
|
69
73
|
)
|
|
70
74
|
parser.add_argument(
|
|
71
75
|
"-v", "--verbose", action="store_true", help="Enable verbose logging"
|
|
@@ -102,13 +106,13 @@ def parse_arguments() -> Args:
|
|
|
102
106
|
# Convert to our typed class
|
|
103
107
|
# Note: argparse returns Any type for namespace attributes
|
|
104
108
|
return Args(
|
|
105
|
-
paths=namespace.paths,
|
|
106
|
-
config=namespace.config,
|
|
107
|
-
group_by=namespace.group_by,
|
|
108
|
-
min_severity=namespace.min_severity,
|
|
109
|
-
jobs=namespace.jobs,
|
|
110
|
-
verbose=namespace.verbose,
|
|
111
|
-
version=namespace.version,
|
|
109
|
+
paths=namespace.paths,
|
|
110
|
+
config=namespace.config,
|
|
111
|
+
group_by=namespace.group_by,
|
|
112
|
+
min_severity=namespace.min_severity,
|
|
113
|
+
jobs=namespace.jobs,
|
|
114
|
+
verbose=namespace.verbose,
|
|
115
|
+
version=namespace.version,
|
|
112
116
|
)
|
|
113
117
|
|
|
114
118
|
|
|
@@ -150,12 +154,12 @@ def _validate_paths(args: Args) -> Optional[list[Path]]:
|
|
|
150
154
|
|
|
151
155
|
|
|
152
156
|
def _analyze_files_safely(
|
|
153
|
-
analyzer: Analyzer, paths: list[Path]
|
|
157
|
+
analyzer: Analyzer, paths: list[Path], max_workers: int
|
|
154
158
|
) -> Optional[AnalysisResult]:
|
|
155
159
|
"""Analyze files and handle errors. Returns result or None on error."""
|
|
156
160
|
try:
|
|
157
161
|
logger.info("Analyzing %d path(s)...", len(paths))
|
|
158
|
-
return analyzer.analyze_files(paths)
|
|
162
|
+
return analyzer.analyze_files(paths, max_workers=max_workers)
|
|
159
163
|
except Exception as e:
|
|
160
164
|
logger.error("Error during analysis: %s", e)
|
|
161
165
|
return None
|
|
@@ -210,8 +214,9 @@ def main() -> int:
|
|
|
210
214
|
return 2
|
|
211
215
|
|
|
212
216
|
# Create analyzer and analyze files
|
|
217
|
+
max_workers = max(1, args.jobs)
|
|
213
218
|
analyzer = Analyzer(config)
|
|
214
|
-
result = _analyze_files_safely(analyzer, paths)
|
|
219
|
+
result = _analyze_files_safely(analyzer, paths, max_workers)
|
|
215
220
|
if result is None:
|
|
216
221
|
return 2
|
|
217
222
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Package version resolution."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_PACKAGE_NAME = "pyrefactor"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _pyproject_path() -> Path:
|
|
11
|
+
"""Return the repository pyproject.toml path."""
|
|
12
|
+
return Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@lru_cache(maxsize=1)
|
|
16
|
+
def _fallback_version() -> str:
|
|
17
|
+
"""Read version from pyproject.toml when the package is not installed."""
|
|
18
|
+
pyproject = _pyproject_path()
|
|
19
|
+
if not pyproject.is_file():
|
|
20
|
+
return "unknown"
|
|
21
|
+
for line in pyproject.read_text(encoding="utf-8").splitlines():
|
|
22
|
+
stripped = line.strip()
|
|
23
|
+
if stripped.startswith("version = "):
|
|
24
|
+
return stripped.split("=", 1)[1].strip().strip('"').strip("'")
|
|
25
|
+
return "unknown"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_version() -> str:
|
|
29
|
+
"""Return the installed package version, falling back to pyproject.toml."""
|
|
30
|
+
try:
|
|
31
|
+
return version(_PACKAGE_NAME)
|
|
32
|
+
except PackageNotFoundError:
|
|
33
|
+
return _fallback_version()
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import concurrent.futures
|
|
5
|
+
import fnmatch
|
|
5
6
|
import logging
|
|
6
|
-
from pathlib import Path
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
7
8
|
|
|
8
9
|
from .ast_visitor import BaseDetector
|
|
9
10
|
from .config import Config
|
|
@@ -22,6 +23,9 @@ from .models import AnalysisResult, FileAnalysis
|
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
24
25
|
|
|
26
|
+
# Maximum file size to read for analysis (10 MB)
|
|
27
|
+
MAX_FILE_BYTES = 10 * 1024 * 1024
|
|
28
|
+
|
|
25
29
|
|
|
26
30
|
class Analyzer:
|
|
27
31
|
"""Main analyzer that orchestrates all detectors."""
|
|
@@ -33,16 +37,11 @@ class Analyzer:
|
|
|
33
37
|
def _create_detectors(
|
|
34
38
|
self, file_path: str, source_lines: list[str]
|
|
35
39
|
) -> list[BaseDetector]:
|
|
36
|
-
"""Create all enabled detectors for a file.
|
|
37
|
-
|
|
38
|
-
Factory method to consolidate detector initialization and reduce duplication.
|
|
39
|
-
"""
|
|
40
|
+
"""Create all enabled detectors for a file."""
|
|
40
41
|
detectors: list[BaseDetector] = []
|
|
41
42
|
|
|
42
|
-
# Complexity detector (always enabled)
|
|
43
43
|
detectors.append(ComplexityDetector(self.config, file_path, source_lines))
|
|
44
44
|
|
|
45
|
-
# Conditionally enabled detectors
|
|
46
45
|
detector_configs = [
|
|
47
46
|
(self.config.performance.enabled, PerformanceDetector),
|
|
48
47
|
(self.config.boolean_logic.enabled, BooleanLogicDetector),
|
|
@@ -60,24 +59,42 @@ class Analyzer:
|
|
|
60
59
|
|
|
61
60
|
return detectors
|
|
62
61
|
|
|
62
|
+
def _read_source(self, file_path: Path) -> tuple[str, list[str]] | str:
|
|
63
|
+
"""Read source from a file, returning an error message on failure."""
|
|
64
|
+
try:
|
|
65
|
+
file_size = file_path.stat().st_size
|
|
66
|
+
if file_size > MAX_FILE_BYTES:
|
|
67
|
+
return (
|
|
68
|
+
f"File exceeds maximum size of {MAX_FILE_BYTES} bytes "
|
|
69
|
+
f"({file_size} bytes)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
source_code = file_path.read_text(encoding="utf-8")
|
|
73
|
+
return source_code, source_code.splitlines()
|
|
74
|
+
except UnicodeDecodeError:
|
|
75
|
+
return "File is not valid UTF-8 text"
|
|
76
|
+
except OSError as e:
|
|
77
|
+
return f"Error reading file: {e}"
|
|
78
|
+
|
|
63
79
|
def analyze_file(self, file_path: Path) -> FileAnalysis:
|
|
64
80
|
"""Analyze a single Python file."""
|
|
65
81
|
analysis = FileAnalysis(file_path=str(file_path))
|
|
66
82
|
|
|
67
83
|
try:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
read_result = self._read_source(file_path)
|
|
85
|
+
if isinstance(read_result, str):
|
|
86
|
+
analysis.parse_error = read_result
|
|
87
|
+
return analysis
|
|
88
|
+
|
|
89
|
+
source_code, source_lines = read_result
|
|
71
90
|
analysis.lines_of_code = len(source_lines)
|
|
72
91
|
|
|
73
|
-
# Parse the AST
|
|
74
92
|
try:
|
|
75
93
|
tree = ast.parse(source_code, filename=str(file_path))
|
|
76
94
|
except SyntaxError as e:
|
|
77
95
|
analysis.parse_error = f"Syntax error: {e}"
|
|
78
96
|
return analysis
|
|
79
97
|
|
|
80
|
-
# Create and run all enabled detectors
|
|
81
98
|
detectors = self._create_detectors(str(file_path), source_lines)
|
|
82
99
|
self._run_detectors(detectors, tree, analysis, file_path)
|
|
83
100
|
|
|
@@ -114,18 +131,54 @@ class Analyzer:
|
|
|
114
131
|
"""Analyze all Python files in a directory."""
|
|
115
132
|
result = AnalysisResult()
|
|
116
133
|
|
|
117
|
-
# Find all Python files
|
|
118
134
|
python_files = list(directory.rglob("*.py"))
|
|
119
|
-
|
|
120
|
-
# Filter excluded patterns
|
|
121
135
|
python_files = self._filter_excluded_files(python_files)
|
|
122
136
|
|
|
123
137
|
if not python_files:
|
|
124
138
|
logger.warning("No Python files found in %s", directory)
|
|
125
139
|
return result
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
return self._analyze_paths_parallel(python_files, max_workers, result)
|
|
142
|
+
|
|
143
|
+
def analyze_files(
|
|
144
|
+
self, file_paths: list[Path], max_workers: int = 4
|
|
145
|
+
) -> AnalysisResult:
|
|
146
|
+
"""Analyze a list of Python files and directories."""
|
|
147
|
+
result = AnalysisResult()
|
|
148
|
+
paths_to_analyze: list[Path] = []
|
|
149
|
+
|
|
150
|
+
for file_path in file_paths:
|
|
151
|
+
if file_path.is_file():
|
|
152
|
+
if file_path.suffix == ".py" and not self._is_excluded(file_path):
|
|
153
|
+
paths_to_analyze.append(file_path)
|
|
154
|
+
elif file_path.is_dir():
|
|
155
|
+
python_files = [
|
|
156
|
+
path
|
|
157
|
+
for path in file_path.rglob("*.py")
|
|
158
|
+
if not self._is_excluded(path)
|
|
159
|
+
]
|
|
160
|
+
paths_to_analyze.extend(python_files)
|
|
161
|
+
|
|
162
|
+
if not paths_to_analyze:
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
return self._analyze_paths_parallel(paths_to_analyze, max_workers, result)
|
|
166
|
+
|
|
167
|
+
def _analyze_paths_parallel(
|
|
168
|
+
self,
|
|
169
|
+
python_files: list[Path],
|
|
170
|
+
max_workers: int,
|
|
171
|
+
result: AnalysisResult,
|
|
172
|
+
) -> AnalysisResult:
|
|
173
|
+
"""Analyze multiple Python files in parallel."""
|
|
174
|
+
workers = max(1, max_workers)
|
|
175
|
+
|
|
176
|
+
if workers == 1 or len(python_files) == 1:
|
|
177
|
+
for file_path in python_files:
|
|
178
|
+
result.add_file_analysis(self.analyze_file(file_path))
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
|
129
182
|
future_to_file = {
|
|
130
183
|
executor.submit(self.analyze_file, file_path): file_path
|
|
131
184
|
for file_path in python_files
|
|
@@ -147,39 +200,22 @@ class Analyzer:
|
|
|
147
200
|
|
|
148
201
|
return result
|
|
149
202
|
|
|
150
|
-
def
|
|
151
|
-
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"""
|
|
166
|
-
if file_path.is_file():
|
|
167
|
-
analysis = self.analyze_file(file_path)
|
|
168
|
-
result.add_file_analysis(analysis)
|
|
169
|
-
elif file_path.is_dir():
|
|
170
|
-
dir_result = self.analyze_directory(file_path)
|
|
171
|
-
for analysis in dir_result.file_analyses:
|
|
172
|
-
result.add_file_analysis(analysis)
|
|
203
|
+
def _is_excluded(self, file_path: Path) -> bool:
|
|
204
|
+
"""Check if a file matches any exclusion pattern."""
|
|
205
|
+
if not self.config.exclude_patterns:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
posix_path = PurePosixPath(file_path.as_posix())
|
|
209
|
+
for pattern in self.config.exclude_patterns:
|
|
210
|
+
normalized = pattern.replace("\\", "/")
|
|
211
|
+
if posix_path.match(normalized):
|
|
212
|
+
return True
|
|
213
|
+
if fnmatch.fnmatch(posix_path.as_posix(), normalized):
|
|
214
|
+
return True
|
|
215
|
+
if fnmatch.fnmatch(file_path.name, normalized):
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
173
218
|
|
|
174
219
|
def _filter_excluded_files(self, files: list[Path]) -> list[Path]:
|
|
175
220
|
"""Filter out files matching exclusion patterns."""
|
|
176
|
-
if not self.
|
|
177
|
-
return files
|
|
178
|
-
|
|
179
|
-
return [
|
|
180
|
-
file_path
|
|
181
|
-
for file_path in files
|
|
182
|
-
if not any(
|
|
183
|
-
pattern in str(file_path) for pattern in self.config.exclude_patterns
|
|
184
|
-
)
|
|
185
|
-
]
|
|
221
|
+
return [file_path for file_path in files if not self._is_excluded(file_path)]
|