pyrefactor 1.0.6__tar.gz → 1.0.8__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.6/src/pyrefactor.egg-info → pyrefactor-1.0.8}/PKG-INFO +61 -15
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/README.md +44 -10
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/pyproject.toml +31 -10
- pyrefactor-1.0.8/src/pyrefactor/__init__.py +5 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/__main__.py +24 -13
- pyrefactor-1.0.8/src/pyrefactor/_version.py +40 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/analyzer.py +88 -51
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/ast_visitor.py +121 -10
- pyrefactor-1.0.8/src/pyrefactor/config.py +423 -0
- pyrefactor-1.0.8/src/pyrefactor/detectors/boolean_logic.py +159 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/comparisons.py +6 -3
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/complexity.py +9 -6
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/context_manager.py +21 -17
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/duplication.py +5 -3
- pyrefactor-1.0.8/src/pyrefactor/detectors/performance.py +336 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/models.py +2 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/reporter.py +2 -2
- {pyrefactor-1.0.6 → pyrefactor-1.0.8/src/pyrefactor.egg-info}/PKG-INFO +61 -15
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/SOURCES.txt +5 -1
- pyrefactor-1.0.8/src/pyrefactor.egg-info/requires.txt +16 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_analyzer.py +108 -8
- pyrefactor-1.0.8/tests/test_ast_visitor.py +82 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_boolean_logic_detector.py +4 -8
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_cli.py +88 -3
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_comparisons_detector.py +6 -6
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_complexity_detector.py +7 -132
- pyrefactor-1.0.8/tests/test_config.py +303 -0
- pyrefactor-1.0.8/tests/test_config_discovery.py +62 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_context_manager_detector.py +18 -5
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_integration.py +18 -20
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_models.py +61 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_performance_detector.py +123 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_reporter.py +56 -0
- pyrefactor-1.0.8/tests/test_version.py +87 -0
- pyrefactor-1.0.6/src/pyrefactor/__init__.py +0 -3
- pyrefactor-1.0.6/src/pyrefactor/config.py +0 -224
- pyrefactor-1.0.6/src/pyrefactor/detectors/boolean_logic.py +0 -231
- pyrefactor-1.0.6/src/pyrefactor/detectors/performance.py +0 -267
- pyrefactor-1.0.6/src/pyrefactor.egg-info/requires.txt +0 -1
- pyrefactor-1.0.6/tests/test_config.py +0 -132
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/LICENSE.md +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/setup.cfg +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/__init__.py +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/control_flow.py +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/dict_operations.py +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/loops.py +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/py.typed +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/dependency_links.txt +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/entry_points.txt +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/top_level.txt +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_control_flow_detector.py +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_dict_operations_detector.py +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_duplication_detector.py +0 -0
- {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_loops_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.8
|
|
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
|
|
|
@@ -58,7 +70,7 @@ A Python refactoring and optimization linter that uses AST analysis to identify
|
|
|
58
70
|
## Detectors
|
|
59
71
|
|
|
60
72
|
- **Complexity**: High cyclomatic complexity functions
|
|
61
|
-
- **Performance**: String concatenation in loops, uncached calls, inefficient operations
|
|
73
|
+
- **Performance**: String concatenation in loops (thresholded), repeated uncached calls in loops, inefficient operations
|
|
62
74
|
- **Boolean Logic**: Overcomplicated boolean expressions
|
|
63
75
|
- **Loops**: Nested loops, invariant code, comprehension opportunities
|
|
64
76
|
- **Duplication**: Duplicate code blocks
|
|
@@ -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
|
|
|
@@ -133,7 +145,13 @@ Configure via TOML file (e.g., `pyproject.toml`):
|
|
|
133
145
|
exclude_patterns = ["__pycache__", ".venv", "build", "dist"]
|
|
134
146
|
|
|
135
147
|
[tool.pyrefactor.complexity]
|
|
136
|
-
|
|
148
|
+
enabled = true
|
|
149
|
+
max_cyclomatic_complexity = 10
|
|
150
|
+
max_branches = 10
|
|
151
|
+
max_nesting_depth = 3
|
|
152
|
+
max_function_lines = 50
|
|
153
|
+
max_arguments = 5
|
|
154
|
+
max_local_variables = 15
|
|
137
155
|
|
|
138
156
|
[tool.pyrefactor.performance]
|
|
139
157
|
enabled = true
|
|
@@ -142,16 +160,15 @@ min_duplicate_calls = 3
|
|
|
142
160
|
|
|
143
161
|
[tool.pyrefactor.boolean_logic]
|
|
144
162
|
enabled = true
|
|
145
|
-
|
|
163
|
+
max_boolean_operators = 3
|
|
146
164
|
|
|
147
165
|
[tool.pyrefactor.loops]
|
|
148
166
|
enabled = true
|
|
149
|
-
max_nesting = 3
|
|
150
167
|
|
|
151
168
|
[tool.pyrefactor.duplication]
|
|
152
169
|
enabled = true
|
|
153
|
-
|
|
154
|
-
similarity_threshold = 0.
|
|
170
|
+
min_duplicate_lines = 5
|
|
171
|
+
similarity_threshold = 0.85
|
|
155
172
|
|
|
156
173
|
[tool.pyrefactor.context_manager]
|
|
157
174
|
enabled = true
|
|
@@ -168,6 +185,8 @@ enabled = true
|
|
|
168
185
|
|
|
169
186
|
Configuration is searched in: `--config` → `pyproject.toml` → `pyrefactor.ini` → defaults
|
|
170
187
|
|
|
188
|
+
**Note:** The PyPI package version (`pyproject.toml`) may differ from GitHub release build numbers used for standalone executables.
|
|
189
|
+
|
|
171
190
|
## CI/CD Integration
|
|
172
191
|
|
|
173
192
|
### Pre-commit Hook
|
|
@@ -197,7 +216,7 @@ jobs:
|
|
|
197
216
|
- uses: actions/checkout@v3
|
|
198
217
|
- uses: actions/setup-python@v4
|
|
199
218
|
with:
|
|
200
|
-
python-version: '3.
|
|
219
|
+
python-version: '3.12'
|
|
201
220
|
- run: pip install pyrefactor
|
|
202
221
|
- run: pyrefactor --min-severity medium src/
|
|
203
222
|
```
|
|
@@ -206,8 +225,35 @@ jobs:
|
|
|
206
225
|
|
|
207
226
|
Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
|
|
208
227
|
|
|
228
|
+
### Development
|
|
229
|
+
|
|
230
|
+
Install the package with development dependencies:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
pip install -e ".[dev]"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Alternatively:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
pip install -e .
|
|
240
|
+
pip install -r requirements-dev.txt
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Run the local verification script (formatting, type checks, lint, security scan, tests):
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
py scripts/verify.py
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Run tests directly:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
pytest
|
|
253
|
+
```
|
|
254
|
+
|
|
209
255
|
1. Follow existing code style (Black, isort)
|
|
210
|
-
2. Add tests for new features (>
|
|
256
|
+
2. Add tests for new features (>90% coverage)
|
|
211
257
|
3. Run type checking and linting
|
|
212
258
|
|
|
213
259
|
## 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
|
|
|
@@ -15,7 +15,7 @@ A Python refactoring and optimization linter that uses AST analysis to identify
|
|
|
15
15
|
## Detectors
|
|
16
16
|
|
|
17
17
|
- **Complexity**: High cyclomatic complexity functions
|
|
18
|
-
- **Performance**: String concatenation in loops, uncached calls, inefficient operations
|
|
18
|
+
- **Performance**: String concatenation in loops (thresholded), repeated uncached calls in loops, inefficient operations
|
|
19
19
|
- **Boolean Logic**: Overcomplicated boolean expressions
|
|
20
20
|
- **Loops**: Nested loops, invariant code, comprehension opportunities
|
|
21
21
|
- **Duplication**: Duplicate code blocks
|
|
@@ -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
|
|
|
@@ -90,7 +90,13 @@ Configure via TOML file (e.g., `pyproject.toml`):
|
|
|
90
90
|
exclude_patterns = ["__pycache__", ".venv", "build", "dist"]
|
|
91
91
|
|
|
92
92
|
[tool.pyrefactor.complexity]
|
|
93
|
-
|
|
93
|
+
enabled = true
|
|
94
|
+
max_cyclomatic_complexity = 10
|
|
95
|
+
max_branches = 10
|
|
96
|
+
max_nesting_depth = 3
|
|
97
|
+
max_function_lines = 50
|
|
98
|
+
max_arguments = 5
|
|
99
|
+
max_local_variables = 15
|
|
94
100
|
|
|
95
101
|
[tool.pyrefactor.performance]
|
|
96
102
|
enabled = true
|
|
@@ -99,16 +105,15 @@ min_duplicate_calls = 3
|
|
|
99
105
|
|
|
100
106
|
[tool.pyrefactor.boolean_logic]
|
|
101
107
|
enabled = true
|
|
102
|
-
|
|
108
|
+
max_boolean_operators = 3
|
|
103
109
|
|
|
104
110
|
[tool.pyrefactor.loops]
|
|
105
111
|
enabled = true
|
|
106
|
-
max_nesting = 3
|
|
107
112
|
|
|
108
113
|
[tool.pyrefactor.duplication]
|
|
109
114
|
enabled = true
|
|
110
|
-
|
|
111
|
-
similarity_threshold = 0.
|
|
115
|
+
min_duplicate_lines = 5
|
|
116
|
+
similarity_threshold = 0.85
|
|
112
117
|
|
|
113
118
|
[tool.pyrefactor.context_manager]
|
|
114
119
|
enabled = true
|
|
@@ -125,6 +130,8 @@ enabled = true
|
|
|
125
130
|
|
|
126
131
|
Configuration is searched in: `--config` → `pyproject.toml` → `pyrefactor.ini` → defaults
|
|
127
132
|
|
|
133
|
+
**Note:** The PyPI package version (`pyproject.toml`) may differ from GitHub release build numbers used for standalone executables.
|
|
134
|
+
|
|
128
135
|
## CI/CD Integration
|
|
129
136
|
|
|
130
137
|
### Pre-commit Hook
|
|
@@ -154,7 +161,7 @@ jobs:
|
|
|
154
161
|
- uses: actions/checkout@v3
|
|
155
162
|
- uses: actions/setup-python@v4
|
|
156
163
|
with:
|
|
157
|
-
python-version: '3.
|
|
164
|
+
python-version: '3.12'
|
|
158
165
|
- run: pip install pyrefactor
|
|
159
166
|
- run: pyrefactor --min-severity medium src/
|
|
160
167
|
```
|
|
@@ -163,8 +170,35 @@ jobs:
|
|
|
163
170
|
|
|
164
171
|
Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
|
|
165
172
|
|
|
173
|
+
### Development
|
|
174
|
+
|
|
175
|
+
Install the package with development dependencies:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pip install -e ".[dev]"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Alternatively:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install -e .
|
|
185
|
+
pip install -r requirements-dev.txt
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Run the local verification script (formatting, type checks, lint, security scan, tests):
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
py scripts/verify.py
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Run tests directly:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pytest
|
|
198
|
+
```
|
|
199
|
+
|
|
166
200
|
1. Follow existing code style (Black, isort)
|
|
167
|
-
2. Add tests for new features (>
|
|
201
|
+
2. Add tests for new features (>90% coverage)
|
|
168
202
|
3. Run type checking and linting
|
|
169
203
|
|
|
170
204
|
## 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.8"
|
|
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
|
-
license = {text = "Commercial Restricted License (CRL)"}
|
|
13
|
-
requires-python = ">=3.
|
|
12
|
+
license = { text = "Commercial Restricted License (CRL)" }
|
|
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
|
|
|
@@ -134,7 +138,10 @@ def _load_config(args: Args) -> Optional[Config]:
|
|
|
134
138
|
logger.info("Loaded configuration: %s", config)
|
|
135
139
|
return config
|
|
136
140
|
except Exception as e:
|
|
137
|
-
|
|
141
|
+
if args.verbose:
|
|
142
|
+
logger.error("Error loading configuration: %s", e, exc_info=True)
|
|
143
|
+
else:
|
|
144
|
+
logger.error("Error loading configuration: %s", e)
|
|
138
145
|
return None
|
|
139
146
|
|
|
140
147
|
|
|
@@ -150,14 +157,17 @@ def _validate_paths(args: Args) -> Optional[list[Path]]:
|
|
|
150
157
|
|
|
151
158
|
|
|
152
159
|
def _analyze_files_safely(
|
|
153
|
-
analyzer: Analyzer, paths: list[Path]
|
|
160
|
+
analyzer: Analyzer, paths: list[Path], max_workers: int, *, verbose: bool = False
|
|
154
161
|
) -> Optional[AnalysisResult]:
|
|
155
162
|
"""Analyze files and handle errors. Returns result or None on error."""
|
|
156
163
|
try:
|
|
157
164
|
logger.info("Analyzing %d path(s)...", len(paths))
|
|
158
|
-
return analyzer.analyze_files(paths)
|
|
165
|
+
return analyzer.analyze_files(paths, max_workers=max_workers)
|
|
159
166
|
except Exception as e:
|
|
160
|
-
|
|
167
|
+
if verbose:
|
|
168
|
+
logger.error("Error during analysis: %s", e, exc_info=True)
|
|
169
|
+
else:
|
|
170
|
+
logger.error("Error during analysis: %s", e)
|
|
161
171
|
return None
|
|
162
172
|
|
|
163
173
|
|
|
@@ -210,8 +220,9 @@ def main() -> int:
|
|
|
210
220
|
return 2
|
|
211
221
|
|
|
212
222
|
# Create analyzer and analyze files
|
|
223
|
+
max_workers = max(1, args.jobs)
|
|
213
224
|
analyzer = Analyzer(config)
|
|
214
|
-
result = _analyze_files_safely(analyzer, paths)
|
|
225
|
+
result = _analyze_files_safely(analyzer, paths, max_workers, verbose=args.verbose)
|
|
215
226
|
if result is None:
|
|
216
227
|
return 2
|
|
217
228
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Package version resolution."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_PACKAGE_NAME = "pyrefactor"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _pyproject_path() -> Path:
|
|
12
|
+
"""Return the pyproject.toml path for version fallback."""
|
|
13
|
+
if getattr(sys, "frozen", False):
|
|
14
|
+
meipass = getattr(sys, "_MEIPASS", None)
|
|
15
|
+
if meipass:
|
|
16
|
+
bundled = Path(meipass) / "pyproject.toml"
|
|
17
|
+
if bundled.is_file():
|
|
18
|
+
return bundled
|
|
19
|
+
return Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@lru_cache(maxsize=1)
|
|
23
|
+
def _fallback_version() -> str:
|
|
24
|
+
"""Read version from pyproject.toml when the package is not installed."""
|
|
25
|
+
pyproject = _pyproject_path()
|
|
26
|
+
if not pyproject.is_file():
|
|
27
|
+
return "unknown"
|
|
28
|
+
for line in pyproject.read_text(encoding="utf-8").splitlines():
|
|
29
|
+
stripped = line.strip()
|
|
30
|
+
if stripped.startswith("version = "):
|
|
31
|
+
return stripped.split("=", 1)[1].strip().strip('"').strip("'")
|
|
32
|
+
return "unknown"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_version() -> str:
|
|
36
|
+
"""Return the installed package version, falling back to pyproject.toml."""
|
|
37
|
+
try:
|
|
38
|
+
return version(_PACKAGE_NAME)
|
|
39
|
+
except PackageNotFoundError:
|
|
40
|
+
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,12 @@ 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
|
-
|
|
43
|
-
|
|
43
|
+
if self.config.complexity.enabled:
|
|
44
|
+
detectors.append(ComplexityDetector(self.config, file_path, source_lines))
|
|
44
45
|
|
|
45
|
-
# Conditionally enabled detectors
|
|
46
46
|
detector_configs = [
|
|
47
47
|
(self.config.performance.enabled, PerformanceDetector),
|
|
48
48
|
(self.config.boolean_logic.enabled, BooleanLogicDetector),
|
|
@@ -60,24 +60,42 @@ class Analyzer:
|
|
|
60
60
|
|
|
61
61
|
return detectors
|
|
62
62
|
|
|
63
|
+
def _read_source(self, file_path: Path) -> tuple[str, list[str]] | str:
|
|
64
|
+
"""Read source from a file, returning an error message on failure."""
|
|
65
|
+
try:
|
|
66
|
+
file_size = file_path.stat().st_size
|
|
67
|
+
if file_size > MAX_FILE_BYTES:
|
|
68
|
+
return (
|
|
69
|
+
f"File exceeds maximum size of {MAX_FILE_BYTES} bytes "
|
|
70
|
+
f"({file_size} bytes)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
source_code = file_path.read_text(encoding="utf-8")
|
|
74
|
+
return source_code, source_code.splitlines()
|
|
75
|
+
except UnicodeDecodeError:
|
|
76
|
+
return "File is not valid UTF-8 text"
|
|
77
|
+
except OSError as e:
|
|
78
|
+
return f"Error reading file: {e}"
|
|
79
|
+
|
|
63
80
|
def analyze_file(self, file_path: Path) -> FileAnalysis:
|
|
64
81
|
"""Analyze a single Python file."""
|
|
65
82
|
analysis = FileAnalysis(file_path=str(file_path))
|
|
66
83
|
|
|
67
84
|
try:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
read_result = self._read_source(file_path)
|
|
86
|
+
if isinstance(read_result, str):
|
|
87
|
+
analysis.parse_error = read_result
|
|
88
|
+
return analysis
|
|
89
|
+
|
|
90
|
+
source_code, source_lines = read_result
|
|
71
91
|
analysis.lines_of_code = len(source_lines)
|
|
72
92
|
|
|
73
|
-
# Parse the AST
|
|
74
93
|
try:
|
|
75
94
|
tree = ast.parse(source_code, filename=str(file_path))
|
|
76
95
|
except SyntaxError as e:
|
|
77
96
|
analysis.parse_error = f"Syntax error: {e}"
|
|
78
97
|
return analysis
|
|
79
98
|
|
|
80
|
-
# Create and run all enabled detectors
|
|
81
99
|
detectors = self._create_detectors(str(file_path), source_lines)
|
|
82
100
|
self._run_detectors(detectors, tree, analysis, file_path)
|
|
83
101
|
|
|
@@ -114,18 +132,54 @@ class Analyzer:
|
|
|
114
132
|
"""Analyze all Python files in a directory."""
|
|
115
133
|
result = AnalysisResult()
|
|
116
134
|
|
|
117
|
-
# Find all Python files
|
|
118
135
|
python_files = list(directory.rglob("*.py"))
|
|
119
|
-
|
|
120
|
-
# Filter excluded patterns
|
|
121
136
|
python_files = self._filter_excluded_files(python_files)
|
|
122
137
|
|
|
123
138
|
if not python_files:
|
|
124
139
|
logger.warning("No Python files found in %s", directory)
|
|
125
140
|
return result
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
return self._analyze_paths_parallel(python_files, max_workers, result)
|
|
143
|
+
|
|
144
|
+
def analyze_files(
|
|
145
|
+
self, file_paths: list[Path], max_workers: int = 4
|
|
146
|
+
) -> AnalysisResult:
|
|
147
|
+
"""Analyze a list of Python files and directories."""
|
|
148
|
+
result = AnalysisResult()
|
|
149
|
+
paths_to_analyze: list[Path] = []
|
|
150
|
+
|
|
151
|
+
for file_path in file_paths:
|
|
152
|
+
if file_path.is_file():
|
|
153
|
+
if file_path.suffix == ".py" and not self._is_excluded(file_path):
|
|
154
|
+
paths_to_analyze.append(file_path)
|
|
155
|
+
elif file_path.is_dir():
|
|
156
|
+
python_files = [
|
|
157
|
+
path
|
|
158
|
+
for path in file_path.rglob("*.py")
|
|
159
|
+
if not self._is_excluded(path)
|
|
160
|
+
]
|
|
161
|
+
paths_to_analyze.extend(python_files)
|
|
162
|
+
|
|
163
|
+
if not paths_to_analyze:
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
return self._analyze_paths_parallel(paths_to_analyze, max_workers, result)
|
|
167
|
+
|
|
168
|
+
def _analyze_paths_parallel(
|
|
169
|
+
self,
|
|
170
|
+
python_files: list[Path],
|
|
171
|
+
max_workers: int,
|
|
172
|
+
result: AnalysisResult,
|
|
173
|
+
) -> AnalysisResult:
|
|
174
|
+
"""Analyze multiple Python files in parallel."""
|
|
175
|
+
workers = max(1, max_workers)
|
|
176
|
+
|
|
177
|
+
if workers == 1 or len(python_files) == 1:
|
|
178
|
+
for file_path in python_files:
|
|
179
|
+
result.add_file_analysis(self.analyze_file(file_path))
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
|
129
183
|
future_to_file = {
|
|
130
184
|
executor.submit(self.analyze_file, file_path): file_path
|
|
131
185
|
for file_path in python_files
|
|
@@ -147,39 +201,22 @@ class Analyzer:
|
|
|
147
201
|
|
|
148
202
|
return result
|
|
149
203
|
|
|
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)
|
|
204
|
+
def _is_excluded(self, file_path: Path) -> bool:
|
|
205
|
+
"""Check if a file matches any exclusion pattern."""
|
|
206
|
+
if not self.config.exclude_patterns:
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
posix_path = PurePosixPath(file_path.as_posix())
|
|
210
|
+
for pattern in self.config.exclude_patterns:
|
|
211
|
+
normalized = pattern.replace("\\", "/")
|
|
212
|
+
if posix_path.match(normalized):
|
|
213
|
+
return True
|
|
214
|
+
if fnmatch.fnmatch(posix_path.as_posix(), normalized):
|
|
215
|
+
return True
|
|
216
|
+
if fnmatch.fnmatch(file_path.name, normalized):
|
|
217
|
+
return True
|
|
218
|
+
return False
|
|
173
219
|
|
|
174
220
|
def _filter_excluded_files(self, files: list[Path]) -> list[Path]:
|
|
175
221
|
"""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
|
-
]
|
|
222
|
+
return [file_path for file_path in files if not self._is_excluded(file_path)]
|