pyrepair 1.0.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.
- pyrepair-1.0.0/LICENSE +21 -0
- pyrepair-1.0.0/PKG-INFO +194 -0
- pyrepair-1.0.0/README.md +169 -0
- pyrepair-1.0.0/pyproject.toml +55 -0
- pyrepair-1.0.0/setup.cfg +4 -0
- pyrepair-1.0.0/src/pyrepair/__init__.py +24 -0
- pyrepair-1.0.0/src/pyrepair/cli.py +127 -0
- pyrepair-1.0.0/src/pyrepair/models.py +63 -0
- pyrepair-1.0.0/src/pyrepair/repair_engine.py +462 -0
- pyrepair-1.0.0/src/pyrepair/report.py +144 -0
- pyrepair-1.0.0/src/pyrepair/scanner.py +45 -0
- pyrepair-1.0.0/src/pyrepair/utils.py +73 -0
- pyrepair-1.0.0/src/pyrepair.egg-info/PKG-INFO +194 -0
- pyrepair-1.0.0/src/pyrepair.egg-info/SOURCES.txt +20 -0
- pyrepair-1.0.0/src/pyrepair.egg-info/dependency_links.txt +1 -0
- pyrepair-1.0.0/src/pyrepair.egg-info/entry_points.txt +2 -0
- pyrepair-1.0.0/src/pyrepair.egg-info/requires.txt +6 -0
- pyrepair-1.0.0/src/pyrepair.egg-info/top_level.txt +1 -0
- pyrepair-1.0.0/tests/test_cli.py +51 -0
- pyrepair-1.0.0/tests/test_indentation.py +133 -0
- pyrepair-1.0.0/tests/test_scanner.py +59 -0
- pyrepair-1.0.0/tests/test_syntax.py +51 -0
pyrepair-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 PyRepair Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyrepair-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrepair
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Repair broken Python code before formatting it.
|
|
5
|
+
Author: Sandeep Kumar Mehta
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: python,linter,formatter,repair,indentation,syntax
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: typer>=0.12
|
|
20
|
+
Requires-Dist: rich>=13.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# PyRepair
|
|
27
|
+
|
|
28
|
+
> **Repair broken Python code before formatting it.**
|
|
29
|
+
|
|
30
|
+
Tools like Black, Ruff, and autopep8 require *valid* Python. PyRepair sits upstream — it fixes indentation errors, tab/space mixes, and missing block bodies so those tools can run.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Broken Python → PyRepair → Valid Python → Black/Ruff → Clean Python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install pyrepair
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install from source:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/sandeep-kumar-mehta/pyrepair.git
|
|
48
|
+
cd pyrepair
|
|
49
|
+
pip install -e ".[dev]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Repair a single file in-place
|
|
58
|
+
pyrepair broken.py
|
|
59
|
+
|
|
60
|
+
# Preview changes without writing (safe)
|
|
61
|
+
pyrepair broken.py --dry-run
|
|
62
|
+
|
|
63
|
+
# Repair all .py files in a directory
|
|
64
|
+
pyrepair src/
|
|
65
|
+
|
|
66
|
+
# Create a backup before writing
|
|
67
|
+
pyrepair broken.py --backup
|
|
68
|
+
|
|
69
|
+
# Show detailed issue table
|
|
70
|
+
pyrepair broken.py --report
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## What Gets Fixed
|
|
76
|
+
|
|
77
|
+
| Rule | What It Does |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `tabs_to_spaces` | Converts leading `\t` to 4 spaces |
|
|
80
|
+
| `mixed_indent` | Fixes lines that mix tabs and spaces |
|
|
81
|
+
| `indentation_repair` | Indents body of `if`/`for`/`while`/`def`/`class` blocks |
|
|
82
|
+
| `missing_block` | Inserts `pass` when a compound statement has no body |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## CLI Reference
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
pyrepair [TARGET] [OPTIONS]
|
|
90
|
+
|
|
91
|
+
Arguments:
|
|
92
|
+
TARGET Python file or directory to repair.
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
--dry-run Show diff only. Do NOT write changes.
|
|
96
|
+
--backup Save .bak copy before overwriting.
|
|
97
|
+
--report, -r Print detailed issue table for each file.
|
|
98
|
+
--help Show this message and exit.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Python API
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pathlib import Path
|
|
107
|
+
from pyrepair import RepairEngine
|
|
108
|
+
|
|
109
|
+
engine = RepairEngine()
|
|
110
|
+
|
|
111
|
+
# Repair a file (returns RepairResult — does NOT write to disk)
|
|
112
|
+
result = engine.repair_file(Path("broken.py"))
|
|
113
|
+
|
|
114
|
+
print(result.status) # RepairStatus.SUCCESS
|
|
115
|
+
print(result.total_issues) # 5
|
|
116
|
+
print(result.total_fixed) # 5
|
|
117
|
+
print(result.validation_passed)# True
|
|
118
|
+
print(result.repaired_source) # fixed source code string
|
|
119
|
+
|
|
120
|
+
# Repair a raw string
|
|
121
|
+
result = engine.repair_source("if True:\nprint('hi')\n")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Adding Custom Rules
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from pyrepair import RepairEngine
|
|
130
|
+
from pyrepair.repair_engine import RepairRule
|
|
131
|
+
from pyrepair.models import Issue, Severity
|
|
132
|
+
|
|
133
|
+
class RemoveDebugPrintsRule(RepairRule):
|
|
134
|
+
name = "remove_debug_prints"
|
|
135
|
+
description = "Remove debug print statements."
|
|
136
|
+
|
|
137
|
+
def detect(self, source: str) -> list[Issue]:
|
|
138
|
+
issues = []
|
|
139
|
+
for i, line in enumerate(source.splitlines(), 1):
|
|
140
|
+
if 'print("DEBUG' in line:
|
|
141
|
+
issues.append(Issue(
|
|
142
|
+
rule_name=self.name,
|
|
143
|
+
description="Debug print found.",
|
|
144
|
+
line_number=i,
|
|
145
|
+
severity=Severity.WARNING,
|
|
146
|
+
original=line,
|
|
147
|
+
))
|
|
148
|
+
return issues
|
|
149
|
+
|
|
150
|
+
def repair(self, source: str) -> str:
|
|
151
|
+
lines = [l for l in source.splitlines() if 'print("DEBUG' not in l]
|
|
152
|
+
return "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
engine = RepairEngine()
|
|
155
|
+
engine.add_rule(RemoveDebugPrintsRule())
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Architecture
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
pyrepair/
|
|
164
|
+
├── cli.py ← Typer CLI (no business logic)
|
|
165
|
+
├── scanner.py ← File discovery
|
|
166
|
+
├── repair_engine.py ← Rule pipeline + RepairEngine
|
|
167
|
+
├── models.py ← Pure data classes (Issue, RepairResult)
|
|
168
|
+
├── report.py ← Rich terminal output
|
|
169
|
+
└── utils.py ← Shared helpers (read/write/validate)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Layers are kept separate (SOLID). The CLI only calls the engine and reporter.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Roadmap
|
|
177
|
+
|
|
178
|
+
**v1.0** Indentation repair, tab conversion, missing block, AST validation
|
|
179
|
+
|
|
180
|
+
**v2.0** Unused/duplicate import detection, import sorting, dead code detection
|
|
181
|
+
|
|
182
|
+
**v3.0** VS Code extension, HTML reports
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT — see [LICENSE](LICENSE).
|
pyrepair-1.0.0/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# PyRepair
|
|
2
|
+
|
|
3
|
+
> **Repair broken Python code before formatting it.**
|
|
4
|
+
|
|
5
|
+
Tools like Black, Ruff, and autopep8 require *valid* Python. PyRepair sits upstream — it fixes indentation errors, tab/space mixes, and missing block bodies so those tools can run.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Broken Python → PyRepair → Valid Python → Black/Ruff → Clean Python
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pyrepair
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install from source:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/sandeep-kumar-mehta/pyrepair.git
|
|
23
|
+
cd pyrepair
|
|
24
|
+
pip install -e ".[dev]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Repair a single file in-place
|
|
33
|
+
pyrepair broken.py
|
|
34
|
+
|
|
35
|
+
# Preview changes without writing (safe)
|
|
36
|
+
pyrepair broken.py --dry-run
|
|
37
|
+
|
|
38
|
+
# Repair all .py files in a directory
|
|
39
|
+
pyrepair src/
|
|
40
|
+
|
|
41
|
+
# Create a backup before writing
|
|
42
|
+
pyrepair broken.py --backup
|
|
43
|
+
|
|
44
|
+
# Show detailed issue table
|
|
45
|
+
pyrepair broken.py --report
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What Gets Fixed
|
|
51
|
+
|
|
52
|
+
| Rule | What It Does |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `tabs_to_spaces` | Converts leading `\t` to 4 spaces |
|
|
55
|
+
| `mixed_indent` | Fixes lines that mix tabs and spaces |
|
|
56
|
+
| `indentation_repair` | Indents body of `if`/`for`/`while`/`def`/`class` blocks |
|
|
57
|
+
| `missing_block` | Inserts `pass` when a compound statement has no body |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## CLI Reference
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
pyrepair [TARGET] [OPTIONS]
|
|
65
|
+
|
|
66
|
+
Arguments:
|
|
67
|
+
TARGET Python file or directory to repair.
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--dry-run Show diff only. Do NOT write changes.
|
|
71
|
+
--backup Save .bak copy before overwriting.
|
|
72
|
+
--report, -r Print detailed issue table for each file.
|
|
73
|
+
--help Show this message and exit.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Python API
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
from pyrepair import RepairEngine
|
|
83
|
+
|
|
84
|
+
engine = RepairEngine()
|
|
85
|
+
|
|
86
|
+
# Repair a file (returns RepairResult — does NOT write to disk)
|
|
87
|
+
result = engine.repair_file(Path("broken.py"))
|
|
88
|
+
|
|
89
|
+
print(result.status) # RepairStatus.SUCCESS
|
|
90
|
+
print(result.total_issues) # 5
|
|
91
|
+
print(result.total_fixed) # 5
|
|
92
|
+
print(result.validation_passed)# True
|
|
93
|
+
print(result.repaired_source) # fixed source code string
|
|
94
|
+
|
|
95
|
+
# Repair a raw string
|
|
96
|
+
result = engine.repair_source("if True:\nprint('hi')\n")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Adding Custom Rules
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pyrepair import RepairEngine
|
|
105
|
+
from pyrepair.repair_engine import RepairRule
|
|
106
|
+
from pyrepair.models import Issue, Severity
|
|
107
|
+
|
|
108
|
+
class RemoveDebugPrintsRule(RepairRule):
|
|
109
|
+
name = "remove_debug_prints"
|
|
110
|
+
description = "Remove debug print statements."
|
|
111
|
+
|
|
112
|
+
def detect(self, source: str) -> list[Issue]:
|
|
113
|
+
issues = []
|
|
114
|
+
for i, line in enumerate(source.splitlines(), 1):
|
|
115
|
+
if 'print("DEBUG' in line:
|
|
116
|
+
issues.append(Issue(
|
|
117
|
+
rule_name=self.name,
|
|
118
|
+
description="Debug print found.",
|
|
119
|
+
line_number=i,
|
|
120
|
+
severity=Severity.WARNING,
|
|
121
|
+
original=line,
|
|
122
|
+
))
|
|
123
|
+
return issues
|
|
124
|
+
|
|
125
|
+
def repair(self, source: str) -> str:
|
|
126
|
+
lines = [l for l in source.splitlines() if 'print("DEBUG' not in l]
|
|
127
|
+
return "\n".join(lines)
|
|
128
|
+
|
|
129
|
+
engine = RepairEngine()
|
|
130
|
+
engine.add_rule(RemoveDebugPrintsRule())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Architecture
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
pyrepair/
|
|
139
|
+
├── cli.py ← Typer CLI (no business logic)
|
|
140
|
+
├── scanner.py ← File discovery
|
|
141
|
+
├── repair_engine.py ← Rule pipeline + RepairEngine
|
|
142
|
+
├── models.py ← Pure data classes (Issue, RepairResult)
|
|
143
|
+
├── report.py ← Rich terminal output
|
|
144
|
+
└── utils.py ← Shared helpers (read/write/validate)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Layers are kept separate (SOLID). The CLI only calls the engine and reporter.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Roadmap
|
|
152
|
+
|
|
153
|
+
**v1.0** Indentation repair, tab conversion, missing block, AST validation
|
|
154
|
+
|
|
155
|
+
**v2.0** Unused/duplicate import detection, import sorting, dead code detection
|
|
156
|
+
|
|
157
|
+
**v3.0** VS Code extension, HTML reports
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyrepair"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Repair broken Python code before formatting it."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Sandeep Kumar Mehta" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["python", "linter", "formatter", "repair", "indentation", "syntax"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"typer>=0.12",
|
|
29
|
+
"rich>=13.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"pytest-cov>=5.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
pyrepair = "pyrepair.cli:run"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
addopts = "-v --tb=short"
|
|
47
|
+
|
|
48
|
+
[tool.coverage.run]
|
|
49
|
+
source = ["src/pyrepair"]
|
|
50
|
+
branch = true
|
|
51
|
+
|
|
52
|
+
[tool.coverage.report]
|
|
53
|
+
show_missing = true
|
|
54
|
+
skip_covered = false
|
|
55
|
+
fail_under = 80
|
pyrepair-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyRepair — Repair broken Python code before formatting it.
|
|
3
|
+
|
|
4
|
+
Public surface
|
|
5
|
+
──────────────
|
|
6
|
+
from pyrepair import RepairEngine
|
|
7
|
+
engine = RepairEngine()
|
|
8
|
+
result = engine.repair_file(Path("broken.py"))
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .repair_engine import RepairEngine, RepairRule
|
|
12
|
+
from .models import Issue, RepairResult, RepairStatus, Severity
|
|
13
|
+
from .scanner import Scanner
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
__all__ = [
|
|
17
|
+
"RepairEngine",
|
|
18
|
+
"RepairRule",
|
|
19
|
+
"Issue",
|
|
20
|
+
"RepairResult",
|
|
21
|
+
"RepairStatus",
|
|
22
|
+
"Severity",
|
|
23
|
+
"Scanner",
|
|
24
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for PyRepair.
|
|
3
|
+
|
|
4
|
+
All business logic lives in repair_engine / scanner / report.
|
|
5
|
+
This file only:
|
|
6
|
+
1. Parses CLI arguments (via Typer)
|
|
7
|
+
2. Calls the right service
|
|
8
|
+
3. Displays results (via report module)
|
|
9
|
+
4. Writes files back when appropriate
|
|
10
|
+
|
|
11
|
+
Commands
|
|
12
|
+
────────
|
|
13
|
+
pyrepair <target> repair in-place
|
|
14
|
+
pyrepair <target> --dry-run show diff only, don't write
|
|
15
|
+
pyrepair <target> --backup create .bak before writing
|
|
16
|
+
pyrepair <target> --report verbose issue table
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
import typer
|
|
25
|
+
|
|
26
|
+
from .repair_engine import RepairEngine
|
|
27
|
+
from .scanner import Scanner
|
|
28
|
+
from .models import RepairStatus
|
|
29
|
+
from .utils import backup_file, write_source
|
|
30
|
+
from . import report
|
|
31
|
+
|
|
32
|
+
# Create the Typer application
|
|
33
|
+
app = typer.Typer(
|
|
34
|
+
name="pyrepair",
|
|
35
|
+
help="Repair broken Python code before formatting it.",
|
|
36
|
+
add_completion=False,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def main(
|
|
42
|
+
target: Path = typer.Argument(
|
|
43
|
+
...,
|
|
44
|
+
help="Python file or directory to repair.",
|
|
45
|
+
exists=True,
|
|
46
|
+
),
|
|
47
|
+
dry_run: bool = typer.Option(
|
|
48
|
+
False,
|
|
49
|
+
"--dry-run",
|
|
50
|
+
help="Show diff only — do NOT write changes to disk.",
|
|
51
|
+
),
|
|
52
|
+
backup: bool = typer.Option(
|
|
53
|
+
False,
|
|
54
|
+
"--backup",
|
|
55
|
+
help="Save a .bak copy before overwriting the file.",
|
|
56
|
+
),
|
|
57
|
+
verbose: bool = typer.Option(
|
|
58
|
+
False,
|
|
59
|
+
"--report",
|
|
60
|
+
"-r",
|
|
61
|
+
help="Print detailed issue table for each file.",
|
|
62
|
+
),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Repair broken Python indentation, tabs, and missing blocks.
|
|
66
|
+
|
|
67
|
+
Examples
|
|
68
|
+
────────
|
|
69
|
+
pyrepair app.py
|
|
70
|
+
pyrepair src/ --dry-run
|
|
71
|
+
pyrepair broken.py --backup --report
|
|
72
|
+
"""
|
|
73
|
+
report.print_banner()
|
|
74
|
+
|
|
75
|
+
# ── 1. Discover files ─────────────────────────────────────────────────
|
|
76
|
+
scanner = Scanner()
|
|
77
|
+
try:
|
|
78
|
+
files = scanner.scan(target)
|
|
79
|
+
except FileNotFoundError as exc:
|
|
80
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
81
|
+
raise typer.Exit(code=1)
|
|
82
|
+
|
|
83
|
+
if not files:
|
|
84
|
+
typer.echo("No Python files found.")
|
|
85
|
+
raise typer.Exit()
|
|
86
|
+
|
|
87
|
+
# ── 2. Repair each file ───────────────────────────────────────────────
|
|
88
|
+
engine = RepairEngine()
|
|
89
|
+
results = []
|
|
90
|
+
|
|
91
|
+
for file_path in files:
|
|
92
|
+
result = engine.repair_file(file_path)
|
|
93
|
+
results.append(result)
|
|
94
|
+
|
|
95
|
+
# Show per-file result
|
|
96
|
+
report.print_result(result, verbose=verbose)
|
|
97
|
+
|
|
98
|
+
if dry_run:
|
|
99
|
+
# Show what WOULD change, but don't touch the file
|
|
100
|
+
report.print_diff(result)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if result.status == RepairStatus.FAILED:
|
|
104
|
+
# File couldn't be read — skip
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if result.was_modified:
|
|
108
|
+
if backup:
|
|
109
|
+
backup_path = backup_file(file_path)
|
|
110
|
+
typer.echo(f" Backup → {backup_path}")
|
|
111
|
+
|
|
112
|
+
write_source(file_path, result.repaired_source)
|
|
113
|
+
|
|
114
|
+
report.print_validation(result.validation_passed)
|
|
115
|
+
|
|
116
|
+
# ── 3. Print aggregate summary ────────────────────────────────────────
|
|
117
|
+
report.print_summary(results)
|
|
118
|
+
|
|
119
|
+
# Exit with non-zero code if any file failed
|
|
120
|
+
any_failed = any(r.status == RepairStatus.FAILED for r in results)
|
|
121
|
+
if any_failed:
|
|
122
|
+
raise typer.Exit(code=1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run() -> None:
|
|
126
|
+
"""Entry point registered in pyproject.toml."""
|
|
127
|
+
app()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for PyRepair.
|
|
3
|
+
|
|
4
|
+
These are pure data classes — no business logic lives here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Severity(str, Enum):
|
|
15
|
+
"""How serious is the issue?"""
|
|
16
|
+
INFO = "info"
|
|
17
|
+
WARNING = "warning"
|
|
18
|
+
ERROR = "error"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RepairStatus(str, Enum):
|
|
22
|
+
"""Final outcome after the repair pipeline runs."""
|
|
23
|
+
SUCCESS = "success" # repaired AND valid Python
|
|
24
|
+
PARTIAL = "partial" # changes made but still invalid
|
|
25
|
+
UNCHANGED = "unchanged" # nothing needed fixing
|
|
26
|
+
FAILED = "failed" # could not even read the file
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Issue:
|
|
31
|
+
"""One detected problem in the source code."""
|
|
32
|
+
rule_name: str # e.g. "tabs_to_spaces"
|
|
33
|
+
description: str # human-readable explanation
|
|
34
|
+
line_number: int # 1-based line where issue was found
|
|
35
|
+
severity: Severity # how bad is it?
|
|
36
|
+
original: str = "" # the raw line that had the problem
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class RepairResult:
|
|
41
|
+
"""Everything PyRepair learned and did for one file."""
|
|
42
|
+
file_path: Path
|
|
43
|
+
original_source: str
|
|
44
|
+
repaired_source: str
|
|
45
|
+
status: RepairStatus = RepairStatus.UNCHANGED
|
|
46
|
+
issues: list[Issue] = field(default_factory=list)
|
|
47
|
+
fixed_issues: list[Issue] = field(default_factory=list)
|
|
48
|
+
validation_passed: bool = False
|
|
49
|
+
elapsed_seconds: float = 0.0
|
|
50
|
+
error_message: str = ""
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def was_modified(self) -> bool:
|
|
54
|
+
"""True when the repaired source differs from the original."""
|
|
55
|
+
return self.original_source != self.repaired_source
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def total_issues(self) -> int:
|
|
59
|
+
return len(self.issues)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def total_fixed(self) -> int:
|
|
63
|
+
return len(self.fixed_issues)
|