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 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.
@@ -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).
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)