ballpython 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ballpython/__init__.py +9 -0
- ballpython/__main__.py +8 -0
- ballpython/cli.py +7 -0
- ballpython-2.0.0.dist-info/METADATA +322 -0
- ballpython-2.0.0.dist-info/RECORD +24 -0
- ballpython-2.0.0.dist-info/WHEEL +5 -0
- ballpython-2.0.0.dist-info/entry_points.txt +3 -0
- ballpython-2.0.0.dist-info/top_level.txt +2 -0
- pycleaner/__init__.py +53 -0
- pycleaner/__main__.py +8 -0
- pycleaner/cli.py +1548 -0
- pycleaner/complexity_analyzer.py +473 -0
- pycleaner/config.py +254 -0
- pycleaner/dead_code_detector.py +515 -0
- pycleaner/dependency_auditor.py +331 -0
- pycleaner/import_resolver.py +832 -0
- pycleaner/linter_formatter.py +590 -0
- pycleaner/pipeline.py +349 -0
- pycleaner/security_scanner.py +563 -0
- pycleaner/syntax_healer.py +577 -0
- pycleaner/taint_engine.py +720 -0
- pycleaner/test_generator.py +444 -0
- pycleaner/type_checker.py +989 -0
- pycleaner/typeshed_resolver.py +395 -0
pycleaner/config.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration system for pycleaner.
|
|
3
|
+
|
|
4
|
+
Loads settings from pyproject.toml [tool.pycleaner] or .pycleaner.toml,
|
|
5
|
+
merges with CLI arguments and built-in defaults.
|
|
6
|
+
Exposes configuration as a frozen PyCleanerConfig dataclass.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import tomllib # Python 3.11+
|
|
17
|
+
except ModuleNotFoundError:
|
|
18
|
+
try:
|
|
19
|
+
import tomli as tomllib # type: ignore[import-not-found,no-redef]
|
|
20
|
+
except ModuleNotFoundError:
|
|
21
|
+
tomllib = None # type: ignore[assignment]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigError(ValueError):
|
|
25
|
+
"""Raised when configuration file parsing or validation fails."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class PyCleanerConfig:
|
|
30
|
+
"""Resolved immutable configuration for a pycleaner run."""
|
|
31
|
+
|
|
32
|
+
# File targeting
|
|
33
|
+
exclude: list[str] = field(
|
|
34
|
+
default_factory=lambda: [
|
|
35
|
+
"migrations/",
|
|
36
|
+
"generated/",
|
|
37
|
+
"*_pb2.py",
|
|
38
|
+
"*_pb2_grpc.py",
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
include: list[str] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
# Syntax healing
|
|
44
|
+
fix_py2_syntax: bool = True
|
|
45
|
+
fix_conditional_assignments: bool = True
|
|
46
|
+
|
|
47
|
+
# Import resolution
|
|
48
|
+
custom_import_map: dict[str, str] = field(default_factory=dict)
|
|
49
|
+
auto_add_future_annotations: bool = False
|
|
50
|
+
|
|
51
|
+
# Linting & formatting
|
|
52
|
+
line_length: int = 88
|
|
53
|
+
select_rules: str = "F401,F841,I,UP,E,W"
|
|
54
|
+
|
|
55
|
+
# Dead code
|
|
56
|
+
ignore_decorators: list[str] = field(
|
|
57
|
+
default_factory=lambda: [
|
|
58
|
+
"@app.route",
|
|
59
|
+
"@pytest.fixture",
|
|
60
|
+
"@override",
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
ignore_names: list[str] = field(
|
|
64
|
+
default_factory=lambda: [
|
|
65
|
+
"_*",
|
|
66
|
+
"test_*",
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Security
|
|
71
|
+
security_severity_threshold: str = "LOW"
|
|
72
|
+
ignore_security_rules: list[str] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
# Type Verification
|
|
75
|
+
strict_types: bool = False
|
|
76
|
+
|
|
77
|
+
# Complexity thresholds
|
|
78
|
+
max_cyclomatic_complexity: int = 10
|
|
79
|
+
max_cognitive_complexity: int = 15
|
|
80
|
+
max_function_length: int = 50
|
|
81
|
+
max_arguments: int = 5
|
|
82
|
+
|
|
83
|
+
# Behavior
|
|
84
|
+
backup: bool = True
|
|
85
|
+
parallel: bool = False
|
|
86
|
+
max_workers: int = 4
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_KEY_MAP: dict[str, str] = {
|
|
90
|
+
"exclude": "exclude",
|
|
91
|
+
"include": "include",
|
|
92
|
+
"fix-py2-syntax": "fix_py2_syntax",
|
|
93
|
+
"fix_py2_syntax": "fix_py2_syntax",
|
|
94
|
+
"fix-conditional-assignments": "fix_conditional_assignments",
|
|
95
|
+
"fix_conditional_assignments": "fix_conditional_assignments",
|
|
96
|
+
"custom-import-map": "custom_import_map",
|
|
97
|
+
"custom_import_map": "custom_import_map",
|
|
98
|
+
"auto-add-future-annotations": "auto_add_future_annotations",
|
|
99
|
+
"auto_add_future_annotations": "auto_add_future_annotations",
|
|
100
|
+
"line-length": "line_length",
|
|
101
|
+
"line_length": "line_length",
|
|
102
|
+
"select-rules": "select_rules",
|
|
103
|
+
"select_rules": "select_rules",
|
|
104
|
+
"ignore-decorators": "ignore_decorators",
|
|
105
|
+
"ignore_decorators": "ignore_decorators",
|
|
106
|
+
"ignore-names": "ignore_names",
|
|
107
|
+
"ignore_names": "ignore_names",
|
|
108
|
+
"security-severity-threshold": "security_severity_threshold",
|
|
109
|
+
"security_severity_threshold": "security_severity_threshold",
|
|
110
|
+
"ignore-security-rules": "ignore_security_rules",
|
|
111
|
+
"ignore_security_rules": "ignore_security_rules",
|
|
112
|
+
"strict-types": "strict_types",
|
|
113
|
+
"strict_types": "strict_types",
|
|
114
|
+
"max-cyclomatic-complexity": "max_cyclomatic_complexity",
|
|
115
|
+
"max_cyclomatic_complexity": "max_cyclomatic_complexity",
|
|
116
|
+
"max-cognitive-complexity": "max_cognitive_complexity",
|
|
117
|
+
"max_cognitive_complexity": "max_cognitive_complexity",
|
|
118
|
+
"max-function-length": "max_function_length",
|
|
119
|
+
"max_function_length": "max_function_length",
|
|
120
|
+
"max-arguments": "max_arguments",
|
|
121
|
+
"max_arguments": "max_arguments",
|
|
122
|
+
"backup": "backup",
|
|
123
|
+
"parallel": "parallel",
|
|
124
|
+
"max-workers": "max_workers",
|
|
125
|
+
"max_workers": "max_workers",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _resolve_file_config(
|
|
130
|
+
project_root: Path | str | None,
|
|
131
|
+
explicit_config_file: Path | str | None,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
if explicit_config_file is not None:
|
|
134
|
+
config_path = Path(explicit_config_file).resolve()
|
|
135
|
+
if not config_path.is_file():
|
|
136
|
+
raise ConfigError(f"Config file not found: {config_path}")
|
|
137
|
+
if config_path.name == "pyproject.toml":
|
|
138
|
+
return _load_from_pyproject(config_path.parent) or {}
|
|
139
|
+
return _load_from_toml_file(config_path) or {}
|
|
140
|
+
|
|
141
|
+
if project_root is not None:
|
|
142
|
+
root = Path(project_root).resolve()
|
|
143
|
+
if not root.is_dir():
|
|
144
|
+
root = root.parent
|
|
145
|
+
return _load_from_pyproject(root) or _load_from_dotfile(root) or {}
|
|
146
|
+
|
|
147
|
+
return {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_config(
|
|
151
|
+
project_root: Path | str | None = None,
|
|
152
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
153
|
+
explicit_config_file: Path | str | None = None,
|
|
154
|
+
) -> PyCleanerConfig:
|
|
155
|
+
"""Load and merge configuration from file and CLI overrides."""
|
|
156
|
+
file_config = _resolve_file_config(project_root, explicit_config_file)
|
|
157
|
+
merged: dict[str, Any] = {
|
|
158
|
+
"exclude": ["migrations/", "generated/", "*_pb2.py", "*_pb2_grpc.py"],
|
|
159
|
+
"include": [],
|
|
160
|
+
"fix_py2_syntax": True,
|
|
161
|
+
"fix_conditional_assignments": True,
|
|
162
|
+
"custom_import_map": {},
|
|
163
|
+
"auto_add_future_annotations": False,
|
|
164
|
+
"line_length": 88,
|
|
165
|
+
"select_rules": "F401,F841,I,UP,E,W",
|
|
166
|
+
"ignore_decorators": ["@app.route", "@pytest.fixture", "@override"],
|
|
167
|
+
"ignore_names": ["_*", "test_*"],
|
|
168
|
+
"security_severity_threshold": "LOW",
|
|
169
|
+
"ignore_security_rules": [],
|
|
170
|
+
"strict_types": False,
|
|
171
|
+
"max_cyclomatic_complexity": 10,
|
|
172
|
+
"max_cognitive_complexity": 15,
|
|
173
|
+
"max_function_length": 50,
|
|
174
|
+
"max_arguments": 5,
|
|
175
|
+
"backup": True,
|
|
176
|
+
"parallel": False,
|
|
177
|
+
"max_workers": 4,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if file_config:
|
|
181
|
+
_apply_dict(merged, file_config)
|
|
182
|
+
if cli_overrides:
|
|
183
|
+
_apply_dict(merged, cli_overrides)
|
|
184
|
+
return PyCleanerConfig(**merged)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _load_from_pyproject(root: Path) -> dict[str, Any] | None:
|
|
188
|
+
"""Attempt to load [tool.ballpython] or [tool.pycleaner] from pyproject.toml."""
|
|
189
|
+
pyproject = root / "pyproject.toml"
|
|
190
|
+
if not pyproject.is_file():
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
if tomllib is None:
|
|
194
|
+
raise ConfigError("tomllib or tomli is required to parse pyproject.toml")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
with open(pyproject, "rb") as f:
|
|
198
|
+
data = tomllib.load(f)
|
|
199
|
+
tool_section = data.get("tool", {})
|
|
200
|
+
return tool_section.get("ballpython") or tool_section.get("pycleaner")
|
|
201
|
+
except Exception as err:
|
|
202
|
+
raise ConfigError(f"Failed to parse pyproject.toml: {err}") from err
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _load_from_dotfile(root: Path) -> dict[str, Any] | None:
|
|
206
|
+
"""Attempt to load .ballpython.toml or .pycleaner.toml."""
|
|
207
|
+
for name in (".ballpython.toml", ".pycleaner.toml"):
|
|
208
|
+
dotfile = root / name
|
|
209
|
+
if dotfile.is_file():
|
|
210
|
+
return _load_from_toml_file(dotfile)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _load_from_toml_file(path: Path) -> dict[str, Any] | None:
|
|
215
|
+
"""Load a flat pycleaner-format TOML file (top-level keys, no [tool.pycleaner] wrapper)."""
|
|
216
|
+
if tomllib is None:
|
|
217
|
+
raise ConfigError(f"tomllib or tomli is required to parse {path}")
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
with open(path, "rb") as f:
|
|
221
|
+
return tomllib.load(f)
|
|
222
|
+
except Exception as err:
|
|
223
|
+
raise ConfigError(f"Failed to parse {path}: {err}") from err
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
_EXPECTED_TYPES: dict[type, tuple[type, str]] = {
|
|
227
|
+
bool: (bool, "boolean"),
|
|
228
|
+
str: (str, "string"),
|
|
229
|
+
list: (list, "list"),
|
|
230
|
+
dict: (dict, "dict"),
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _validate_value_type(key: str, current: Any, value: Any) -> None:
|
|
235
|
+
if type(current) is int:
|
|
236
|
+
if type(value) is not int:
|
|
237
|
+
raise ConfigError(f"Invalid integer value for '{key}': {value!r}")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
expected = _EXPECTED_TYPES.get(type(current))
|
|
241
|
+
if expected is not None:
|
|
242
|
+
expected_cls, type_name = expected
|
|
243
|
+
if not isinstance(value, expected_cls):
|
|
244
|
+
raise ConfigError(f"Invalid {type_name} value for '{key}': {value!r}")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _apply_dict(target: dict[str, Any], data: dict[str, Any]) -> None:
|
|
248
|
+
"""Apply a dictionary of settings into target dict, normalizing key names and validating types."""
|
|
249
|
+
for key, value in data.items():
|
|
250
|
+
attr = _KEY_MAP.get(key)
|
|
251
|
+
if attr is None or attr not in target:
|
|
252
|
+
raise ConfigError(f"Unknown configuration key: '{key}'")
|
|
253
|
+
_validate_value_type(key, target[attr], value)
|
|
254
|
+
target[attr] = value
|