pyrefactor 1.0.7__tar.gz → 1.0.9__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.7/src/pyrefactor.egg-info → pyrefactor-1.0.9}/PKG-INFO +14 -7
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/README.md +13 -6
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/pyproject.toml +2 -2
- pyrefactor-1.0.9/src/pyrefactor/__init__.py +20 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/__main__.py +13 -6
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/_version.py +8 -1
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/analyzer.py +2 -1
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/ast_visitor.py +0 -1
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/config.py +89 -31
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/boolean_logic.py +21 -16
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/comparisons.py +45 -90
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/complexity.py +0 -8
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/context_manager.py +3 -37
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/control_flow.py +12 -36
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/dict_operations.py +66 -60
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/duplication.py +3 -1
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/loops.py +26 -54
- pyrefactor-1.0.9/src/pyrefactor/detectors/performance.py +309 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/reporter.py +1 -1
- {pyrefactor-1.0.7 → pyrefactor-1.0.9/src/pyrefactor.egg-info}/PKG-INFO +14 -7
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_analyzer.py +101 -9
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_cli.py +79 -7
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_complexity_detector.py +0 -126
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_config.py +147 -20
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_config_discovery.py +28 -14
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_dict_operations_detector.py +15 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_duplication_detector.py +7 -8
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_integration.py +18 -20
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_loops_detector.py +2 -5
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_performance_detector.py +126 -6
- pyrefactor-1.0.9/tests/test_version.py +104 -0
- pyrefactor-1.0.7/src/pyrefactor/__init__.py +0 -5
- pyrefactor-1.0.7/src/pyrefactor/detectors/performance.py +0 -201
- pyrefactor-1.0.7/tests/test_version.py +0 -50
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/LICENSE.md +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/setup.cfg +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/__init__.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/models.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/py.typed +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/SOURCES.txt +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/dependency_links.txt +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/entry_points.txt +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/requires.txt +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/top_level.txt +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_ast_visitor.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_boolean_logic_detector.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_comparisons_detector.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_context_manager_detector.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_control_flow_detector.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_models.py +0 -0
- {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_reporter.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.9
|
|
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
|
|
@@ -70,7 +70,7 @@ A Python refactoring and optimization linter that uses AST analysis to identify
|
|
|
70
70
|
## Detectors
|
|
71
71
|
|
|
72
72
|
- **Complexity**: High cyclomatic complexity functions
|
|
73
|
-
- **Performance**: String concatenation in loops, uncached calls, inefficient operations
|
|
73
|
+
- **Performance**: String concatenation in loops (thresholded), repeated uncached calls in loops, inefficient operations
|
|
74
74
|
- **Boolean Logic**: Overcomplicated boolean expressions
|
|
75
75
|
- **Loops**: Nested loops, invariant code, comprehension opportunities
|
|
76
76
|
- **Duplication**: Duplicate code blocks
|
|
@@ -145,7 +145,13 @@ Configure via TOML file (e.g., `pyproject.toml`):
|
|
|
145
145
|
exclude_patterns = ["__pycache__", ".venv", "build", "dist"]
|
|
146
146
|
|
|
147
147
|
[tool.pyrefactor.complexity]
|
|
148
|
-
|
|
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
|
|
149
155
|
|
|
150
156
|
[tool.pyrefactor.performance]
|
|
151
157
|
enabled = true
|
|
@@ -154,16 +160,15 @@ min_duplicate_calls = 3
|
|
|
154
160
|
|
|
155
161
|
[tool.pyrefactor.boolean_logic]
|
|
156
162
|
enabled = true
|
|
157
|
-
|
|
163
|
+
max_boolean_operators = 3
|
|
158
164
|
|
|
159
165
|
[tool.pyrefactor.loops]
|
|
160
166
|
enabled = true
|
|
161
|
-
max_nesting = 3
|
|
162
167
|
|
|
163
168
|
[tool.pyrefactor.duplication]
|
|
164
169
|
enabled = true
|
|
165
|
-
|
|
166
|
-
similarity_threshold = 0.
|
|
170
|
+
min_duplicate_lines = 5
|
|
171
|
+
similarity_threshold = 0.85
|
|
167
172
|
|
|
168
173
|
[tool.pyrefactor.context_manager]
|
|
169
174
|
enabled = true
|
|
@@ -180,6 +185,8 @@ enabled = true
|
|
|
180
185
|
|
|
181
186
|
Configuration is searched in: `--config` → `pyproject.toml` → `pyrefactor.ini` → defaults
|
|
182
187
|
|
|
188
|
+
**Note:** The PyPI package version (`pyproject.toml`) may differ from GitHub release build numbers used for standalone executables.
|
|
189
|
+
|
|
183
190
|
## CI/CD Integration
|
|
184
191
|
|
|
185
192
|
### Pre-commit Hook
|
|
@@ -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
|
|
@@ -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
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyrefactor"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.9"
|
|
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)"}
|
|
12
|
+
license = { text = "Commercial Restricted License (CRL)" }
|
|
13
13
|
requires-python = ">=3.12"
|
|
14
14
|
keywords = [
|
|
15
15
|
"refactoring",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""PyRefactor - A Python refactoring and optimization linter."""
|
|
2
|
+
|
|
3
|
+
from pyrefactor._version import get_version
|
|
4
|
+
from pyrefactor.analyzer import Analyzer
|
|
5
|
+
from pyrefactor.config import Config
|
|
6
|
+
from pyrefactor.models import AnalysisResult, FileAnalysis, Issue, Severity
|
|
7
|
+
from pyrefactor.reporter import ConsoleReporter
|
|
8
|
+
|
|
9
|
+
__version__ = get_version()
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Analyzer",
|
|
13
|
+
"AnalysisResult",
|
|
14
|
+
"Config",
|
|
15
|
+
"ConsoleReporter",
|
|
16
|
+
"FileAnalysis",
|
|
17
|
+
"Issue",
|
|
18
|
+
"Severity",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import argparse
|
|
4
4
|
import logging
|
|
5
5
|
import sys
|
|
6
|
+
import tomllib
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Optional
|
|
@@ -137,8 +138,11 @@ def _load_config(args: Args) -> Optional[Config]:
|
|
|
137
138
|
config = Config.load(args.config)
|
|
138
139
|
logger.info("Loaded configuration: %s", config)
|
|
139
140
|
return config
|
|
140
|
-
except
|
|
141
|
-
|
|
141
|
+
except (ValueError, OSError, tomllib.TOMLDecodeError) as e:
|
|
142
|
+
if args.verbose:
|
|
143
|
+
logger.error("Error loading configuration: %s", e, exc_info=True)
|
|
144
|
+
else:
|
|
145
|
+
logger.error("Error loading configuration: %s", e)
|
|
142
146
|
return None
|
|
143
147
|
|
|
144
148
|
|
|
@@ -154,14 +158,17 @@ def _validate_paths(args: Args) -> Optional[list[Path]]:
|
|
|
154
158
|
|
|
155
159
|
|
|
156
160
|
def _analyze_files_safely(
|
|
157
|
-
analyzer: Analyzer, paths: list[Path], max_workers: int
|
|
161
|
+
analyzer: Analyzer, paths: list[Path], max_workers: int, *, verbose: bool = False
|
|
158
162
|
) -> Optional[AnalysisResult]:
|
|
159
163
|
"""Analyze files and handle errors. Returns result or None on error."""
|
|
160
164
|
try:
|
|
161
165
|
logger.info("Analyzing %d path(s)...", len(paths))
|
|
162
166
|
return analyzer.analyze_files(paths, max_workers=max_workers)
|
|
163
|
-
except
|
|
164
|
-
|
|
167
|
+
except (OSError, RuntimeError) as e:
|
|
168
|
+
if verbose:
|
|
169
|
+
logger.error("Error during analysis: %s", e, exc_info=True)
|
|
170
|
+
else:
|
|
171
|
+
logger.error("Error during analysis: %s", e)
|
|
165
172
|
return None
|
|
166
173
|
|
|
167
174
|
|
|
@@ -216,7 +223,7 @@ def main() -> int:
|
|
|
216
223
|
# Create analyzer and analyze files
|
|
217
224
|
max_workers = max(1, args.jobs)
|
|
218
225
|
analyzer = Analyzer(config)
|
|
219
|
-
result = _analyze_files_safely(analyzer, paths, max_workers)
|
|
226
|
+
result = _analyze_files_safely(analyzer, paths, max_workers, verbose=args.verbose)
|
|
220
227
|
if result is None:
|
|
221
228
|
return 2
|
|
222
229
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Package version resolution."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from functools import lru_cache
|
|
4
5
|
from importlib.metadata import PackageNotFoundError, version
|
|
5
6
|
from pathlib import Path
|
|
@@ -8,7 +9,13 @@ _PACKAGE_NAME = "pyrefactor"
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _pyproject_path() -> Path:
|
|
11
|
-
"""Return the
|
|
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
|
|
12
19
|
return Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
|
|
13
20
|
|
|
14
21
|
|
|
@@ -40,7 +40,8 @@ class Analyzer:
|
|
|
40
40
|
"""Create all enabled detectors for a file."""
|
|
41
41
|
detectors: list[BaseDetector] = []
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
if self.config.complexity.enabled:
|
|
44
|
+
detectors.append(ComplexityDetector(self.config, file_path, source_lines))
|
|
44
45
|
|
|
45
46
|
detector_configs = [
|
|
46
47
|
(self.config.performance.enabled, PerformanceDetector),
|
|
@@ -39,7 +39,6 @@ class BaseDetector(ast.NodeVisitor, ABC):
|
|
|
39
39
|
self.source_lines = source_lines
|
|
40
40
|
self.issues: list[Issue] = []
|
|
41
41
|
self.current_function: Union[ast.FunctionDef, ast.AsyncFunctionDef, None] = None
|
|
42
|
-
self.nesting_level = 0
|
|
43
42
|
|
|
44
43
|
@abstractmethod
|
|
45
44
|
def get_detector_name(self) -> str:
|
|
@@ -11,6 +11,7 @@ from typing import Any, Mapping, Optional, Union
|
|
|
11
11
|
class ComplexityConfig:
|
|
12
12
|
"""Configuration for complexity detector."""
|
|
13
13
|
|
|
14
|
+
enabled: bool = True
|
|
14
15
|
max_branches: int = 10
|
|
15
16
|
max_nesting_depth: int = 3
|
|
16
17
|
max_function_lines: int = 50
|
|
@@ -24,6 +25,8 @@ class PerformanceConfig:
|
|
|
24
25
|
"""Configuration for performance detector."""
|
|
25
26
|
|
|
26
27
|
enabled: bool = True
|
|
28
|
+
min_concatenations: int = 3
|
|
29
|
+
min_duplicate_calls: int = 3
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
@dataclass
|
|
@@ -94,10 +97,14 @@ class Config:
|
|
|
94
97
|
exclude_patterns: list[str] = field(default_factory=list)
|
|
95
98
|
|
|
96
99
|
@staticmethod
|
|
97
|
-
def _parse_complexity_config(
|
|
100
|
+
def _parse_complexity_config(
|
|
101
|
+
config: configparser.ConfigParser,
|
|
102
|
+
) -> dict[str, Union[int, bool]]:
|
|
98
103
|
"""Extract complexity configuration from config parser."""
|
|
99
|
-
complexity_dict: dict[str, int] = {}
|
|
104
|
+
complexity_dict: dict[str, Union[int, bool]] = {}
|
|
100
105
|
if config.has_section("complexity"):
|
|
106
|
+
if config.has_option("complexity", "enabled"):
|
|
107
|
+
complexity_dict["enabled"] = config.getboolean("complexity", "enabled")
|
|
101
108
|
for key in [
|
|
102
109
|
"max_branches",
|
|
103
110
|
"max_nesting_depth",
|
|
@@ -110,6 +117,27 @@ class Config:
|
|
|
110
117
|
complexity_dict[key] = config.getint("complexity", key)
|
|
111
118
|
return complexity_dict
|
|
112
119
|
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _parse_performance_config(
|
|
122
|
+
config: configparser.ConfigParser,
|
|
123
|
+
) -> dict[str, Union[int, bool]]:
|
|
124
|
+
"""Extract performance configuration from config parser."""
|
|
125
|
+
performance_dict: dict[str, Union[int, bool]] = {}
|
|
126
|
+
if config.has_section("performance"):
|
|
127
|
+
if config.has_option("performance", "enabled"):
|
|
128
|
+
performance_dict["enabled"] = config.getboolean(
|
|
129
|
+
"performance", "enabled"
|
|
130
|
+
)
|
|
131
|
+
if config.has_option("performance", "min_concatenations"):
|
|
132
|
+
performance_dict["min_concatenations"] = config.getint(
|
|
133
|
+
"performance", "min_concatenations"
|
|
134
|
+
)
|
|
135
|
+
if config.has_option("performance", "min_duplicate_calls"):
|
|
136
|
+
performance_dict["min_duplicate_calls"] = config.getint(
|
|
137
|
+
"performance", "min_duplicate_calls"
|
|
138
|
+
)
|
|
139
|
+
return performance_dict
|
|
140
|
+
|
|
113
141
|
@staticmethod
|
|
114
142
|
def _parse_duplication_config(
|
|
115
143
|
config: configparser.ConfigParser,
|
|
@@ -207,6 +235,7 @@ class Config:
|
|
|
207
235
|
raise ValueError("Invalid [tool.pyrefactor] section in configuration")
|
|
208
236
|
|
|
209
237
|
complexity_fields = {
|
|
238
|
+
"enabled": bool,
|
|
210
239
|
"max_branches": int,
|
|
211
240
|
"max_nesting_depth": int,
|
|
212
241
|
"max_function_lines": int,
|
|
@@ -214,6 +243,11 @@ class Config:
|
|
|
214
243
|
"max_local_variables": int,
|
|
215
244
|
"max_cyclomatic_complexity": int,
|
|
216
245
|
}
|
|
246
|
+
performance_fields = {
|
|
247
|
+
"enabled": bool,
|
|
248
|
+
"min_concatenations": int,
|
|
249
|
+
"min_duplicate_calls": int,
|
|
250
|
+
}
|
|
217
251
|
duplication_fields = {
|
|
218
252
|
"enabled": bool,
|
|
219
253
|
"min_duplicate_lines": int,
|
|
@@ -232,9 +266,7 @@ class Config:
|
|
|
232
266
|
exclude_patterns = [str(pattern) for pattern in raw_exclude]
|
|
233
267
|
elif isinstance(raw_exclude, str):
|
|
234
268
|
exclude_patterns = [
|
|
235
|
-
pattern.strip()
|
|
236
|
-
for pattern in raw_exclude.split(",")
|
|
237
|
-
if pattern.strip()
|
|
269
|
+
pattern.strip() for pattern in raw_exclude.split(",") if pattern.strip()
|
|
238
270
|
]
|
|
239
271
|
|
|
240
272
|
return cls(
|
|
@@ -246,15 +278,21 @@ class Config:
|
|
|
246
278
|
),
|
|
247
279
|
performance=PerformanceConfig(
|
|
248
280
|
**cls._coerce_section(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
281
|
+
(
|
|
282
|
+
pyrefactor.get("performance", {})
|
|
283
|
+
if isinstance(pyrefactor.get("performance"), dict)
|
|
284
|
+
else {}
|
|
285
|
+
),
|
|
286
|
+
performance_fields,
|
|
253
287
|
)
|
|
254
288
|
),
|
|
255
289
|
duplication=DuplicationConfig(
|
|
256
290
|
**cls._coerce_section(
|
|
257
|
-
|
|
291
|
+
(
|
|
292
|
+
duplication_section
|
|
293
|
+
if isinstance(duplication_section, dict)
|
|
294
|
+
else {}
|
|
295
|
+
),
|
|
258
296
|
duplication_fields,
|
|
259
297
|
)
|
|
260
298
|
),
|
|
@@ -266,47 +304,66 @@ class Config:
|
|
|
266
304
|
),
|
|
267
305
|
loops=LoopsConfig(
|
|
268
306
|
**cls._coerce_section(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
307
|
+
(
|
|
308
|
+
pyrefactor.get("loops", {})
|
|
309
|
+
if isinstance(pyrefactor.get("loops"), dict)
|
|
310
|
+
else {}
|
|
311
|
+
),
|
|
272
312
|
enabled_only,
|
|
273
313
|
)
|
|
274
314
|
),
|
|
275
315
|
context_manager=ContextManagerConfig(
|
|
276
316
|
**cls._coerce_section(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
317
|
+
(
|
|
318
|
+
pyrefactor.get("context_manager", {})
|
|
319
|
+
if isinstance(pyrefactor.get("context_manager"), dict)
|
|
320
|
+
else {}
|
|
321
|
+
),
|
|
280
322
|
enabled_only,
|
|
281
323
|
)
|
|
282
324
|
),
|
|
283
325
|
control_flow=ControlFlowConfig(
|
|
284
326
|
**cls._coerce_section(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
327
|
+
(
|
|
328
|
+
pyrefactor.get("control_flow", {})
|
|
329
|
+
if isinstance(pyrefactor.get("control_flow"), dict)
|
|
330
|
+
else {}
|
|
331
|
+
),
|
|
288
332
|
enabled_only,
|
|
289
333
|
)
|
|
290
334
|
),
|
|
291
335
|
dict_operations=DictOperationsConfig(
|
|
292
336
|
**cls._coerce_section(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
337
|
+
(
|
|
338
|
+
pyrefactor.get("dict_operations", {})
|
|
339
|
+
if isinstance(pyrefactor.get("dict_operations"), dict)
|
|
340
|
+
else {}
|
|
341
|
+
),
|
|
296
342
|
enabled_only,
|
|
297
343
|
)
|
|
298
344
|
),
|
|
299
345
|
comparisons=ComparisonsConfig(
|
|
300
346
|
**cls._coerce_section(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
347
|
+
(
|
|
348
|
+
pyrefactor.get("comparisons", {})
|
|
349
|
+
if isinstance(pyrefactor.get("comparisons"), dict)
|
|
350
|
+
else {}
|
|
351
|
+
),
|
|
304
352
|
enabled_only,
|
|
305
353
|
)
|
|
306
354
|
),
|
|
307
355
|
exclude_patterns=exclude_patterns,
|
|
308
356
|
)
|
|
309
357
|
|
|
358
|
+
@staticmethod
|
|
359
|
+
def _has_pyrefactor_config(data: dict[str, Any]) -> bool:
|
|
360
|
+
"""Return True when parsed TOML contains a non-empty [tool.pyrefactor] table."""
|
|
361
|
+
tool_section = data.get("tool")
|
|
362
|
+
if not isinstance(tool_section, dict):
|
|
363
|
+
return False
|
|
364
|
+
pyrefactor = tool_section.get("pyrefactor")
|
|
365
|
+
return isinstance(pyrefactor, dict) and bool(pyrefactor)
|
|
366
|
+
|
|
310
367
|
@classmethod
|
|
311
368
|
def from_toml_file(cls, config_path: Path) -> "Config":
|
|
312
369
|
"""Load configuration from a TOML file."""
|
|
@@ -328,10 +385,8 @@ class Config:
|
|
|
328
385
|
parser.read(config_path, encoding="utf-8")
|
|
329
386
|
|
|
330
387
|
return cls(
|
|
331
|
-
complexity=ComplexityConfig(**cls._parse_complexity_config(parser)),
|
|
332
|
-
performance=PerformanceConfig(
|
|
333
|
-
**cls._parse_enabled_flag(parser, "performance")
|
|
334
|
-
),
|
|
388
|
+
complexity=ComplexityConfig(**cls._parse_complexity_config(parser)), # type: ignore[arg-type]
|
|
389
|
+
performance=PerformanceConfig(**cls._parse_performance_config(parser)), # type: ignore[arg-type]
|
|
335
390
|
duplication=DuplicationConfig(**cls._parse_duplication_config(parser)), # type: ignore[arg-type]
|
|
336
391
|
boolean_logic=BooleanLogicConfig(**cls._parse_boolean_logic_config(parser)), # type: ignore[arg-type]
|
|
337
392
|
loops=LoopsConfig(**cls._parse_enabled_flag(parser, "loops")),
|
|
@@ -367,8 +422,11 @@ class Config:
|
|
|
367
422
|
return cls.from_file(config_path)
|
|
368
423
|
|
|
369
424
|
pyproject = Path("pyproject.toml")
|
|
370
|
-
if pyproject.
|
|
371
|
-
|
|
425
|
+
if pyproject.is_file():
|
|
426
|
+
with pyproject.open("rb") as config_file:
|
|
427
|
+
data = tomllib.load(config_file)
|
|
428
|
+
if cls._has_pyrefactor_config(data):
|
|
429
|
+
return cls.from_toml_data(data)
|
|
372
430
|
|
|
373
431
|
ini_file = Path("pyrefactor.ini")
|
|
374
432
|
if ini_file.exists():
|
|
@@ -45,24 +45,29 @@ class BooleanLogicDetector(BaseDetector):
|
|
|
45
45
|
self.generic_visit(node)
|
|
46
46
|
return
|
|
47
47
|
|
|
48
|
+
self._check_boolean_singleton_comparison(node)
|
|
49
|
+
self.generic_visit(node)
|
|
50
|
+
|
|
51
|
+
def _check_boolean_singleton_comparison(self, node: ast.Compare) -> None:
|
|
52
|
+
"""Report use of ``is`` when comparing to boolean constants."""
|
|
48
53
|
for comparator in node.comparators:
|
|
49
|
-
if
|
|
50
|
-
comparator
|
|
54
|
+
if not (
|
|
55
|
+
isinstance(comparator, ast.Constant)
|
|
56
|
+
and isinstance(comparator.value, bool)
|
|
51
57
|
):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
self.generic_visit(node)
|
|
58
|
+
continue
|
|
59
|
+
if not any(isinstance(op, ast.Is) for op in node.ops):
|
|
60
|
+
continue
|
|
61
|
+
self.report_issue(
|
|
62
|
+
node,
|
|
63
|
+
severity=Severity.MEDIUM,
|
|
64
|
+
rule_id="B004",
|
|
65
|
+
message="Using 'is' for boolean comparison",
|
|
66
|
+
suggestion=(
|
|
67
|
+
"Use '==' for value comparison or use the boolean directly"
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
return
|
|
66
71
|
|
|
67
72
|
def visit_FunctionDef(
|
|
68
73
|
self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
|