|
| 1 | +import random |
| 2 | +import re |
| 3 | +import subprocess |
| 4 | +import sys |
| 5 | +import tempfile |
| 6 | +import time |
| 7 | +from dataclasses import dataclass |
| 8 | +from pathlib import Path |
| 9 | +from typing import Optional, cast |
| 10 | + |
| 11 | +from pysource_codegen import generate |
| 12 | +from pysource_minimize import minimize |
| 13 | + |
| 14 | +import black |
| 15 | + |
| 16 | +base_path = Path(__file__).parent |
| 17 | + |
| 18 | + |
| 19 | +@dataclass() |
| 20 | +class Issue: |
| 21 | + src: str |
| 22 | + mode: black.FileMode |
| 23 | + |
| 24 | + |
| 25 | +def bug_in_code(issue: Issue) -> bool: |
| 26 | + try: |
| 27 | + dst_contents = black.format_str(issue.src, mode=issue.mode) |
| 28 | + |
| 29 | + black.assert_equivalent(issue.src, dst_contents) |
| 30 | + black.assert_stable(issue.src, dst_contents, mode=issue.mode) |
| 31 | + except Exception: |
| 32 | + return True |
| 33 | + return False |
| 34 | + |
| 35 | + |
| 36 | +def current_target_version() -> black.TargetVersion: |
| 37 | + v = sys.version_info |
| 38 | + return cast(black.TargetVersion, getattr(black.TargetVersion, f"PY{v[0]}{v[1]}")) |
| 39 | + |
| 40 | + |
| 41 | +def find_issue() -> Optional[Issue]: |
| 42 | + t = time.time() |
| 43 | + print("search for new issue ", end="", flush=True) |
| 44 | + |
| 45 | + while time.time() - t < 60 * 10: |
| 46 | + for line_length in (100, 1): |
| 47 | + for magic_trailing_comma in (True, False): |
| 48 | + print(".", end="", flush=True) |
| 49 | + mode = black.FileMode( |
| 50 | + line_length=line_length, |
| 51 | + string_normalization=True, |
| 52 | + is_pyi=False, |
| 53 | + magic_trailing_comma=magic_trailing_comma, |
| 54 | + target_versions={current_target_version()}, |
| 55 | + ) |
| 56 | + seed = random.randint(0, 100000000) |
| 57 | + |
| 58 | + src_code = generate(seed) |
| 59 | + |
| 60 | + issue = Issue(src_code, mode) |
| 61 | + |
| 62 | + if bug_in_code(issue): |
| 63 | + print(f"found bug (seed={seed})") |
| 64 | + return issue |
| 65 | + print("no new issue found in 10 minutes") |
| 66 | + return None |
| 67 | + |
| 68 | + |
| 69 | +def minimize_code(issue: Issue) -> Issue: |
| 70 | + minimized = Issue( |
| 71 | + minimize(issue.src, lambda code: bug_in_code(Issue(code, issue.mode))), |
| 72 | + issue.mode, |
| 73 | + ) |
| 74 | + assert bug_in_code(minimized) |
| 75 | + |
| 76 | + print("minimized code:") |
| 77 | + print(minimized.src) |
| 78 | + |
| 79 | + return minimized |
| 80 | + |
| 81 | + |
| 82 | +def mode_to_options(mode: black.FileMode) -> list[str]: |
| 83 | + result = ["-l", str(mode.line_length)] |
| 84 | + if not mode.magic_trailing_comma: |
| 85 | + result.append("-C") |
| 86 | + (v,) = list(mode.target_versions) |
| 87 | + result += ["-t", v.name.lower()] |
| 88 | + return result |
| 89 | + |
| 90 | + |
| 91 | +def create_issue(issue: Issue) -> str: |
| 92 | + |
| 93 | + dir = tempfile.TemporaryDirectory() |
| 94 | + |
| 95 | + cwd = Path(dir.name) |
| 96 | + (cwd / "bug.py").write_text(issue.src) |
| 97 | + |
| 98 | + multiline_code = "\n".join([" " + repr(s + "\n") for s in issue.src.split("\n")]) |
| 99 | + |
| 100 | + parse_code = f"""\ |
| 101 | +from ast import parse |
| 102 | +parse( |
| 103 | +{multiline_code} |
| 104 | +) |
| 105 | +""" |
| 106 | + cwd = Path(dir.name) |
| 107 | + (cwd / "parse_code.py").write_text(parse_code) |
| 108 | + command = ["black", *mode_to_options(issue.mode), "bug.py"] |
| 109 | + |
| 110 | + format_result = subprocess.run( |
| 111 | + [sys.executable, "-m", *command], capture_output=True, cwd=cwd |
| 112 | + ) |
| 113 | + |
| 114 | + error_output = format_result.stderr.decode() |
| 115 | + |
| 116 | + m = re.search("This diff might be helpful: (/.*)", error_output) |
| 117 | + reported_diff = "" |
| 118 | + if m: |
| 119 | + path = Path(m[1]) |
| 120 | + reported_diff = f""" |
| 121 | +the reported diff in {path} is: |
| 122 | +``` diff |
| 123 | +{path.read_text()} |
| 124 | +``` |
| 125 | +""" |
| 126 | + |
| 127 | + run_result = subprocess.run( |
| 128 | + [sys.executable, "parse_code.py"], capture_output=True, cwd=cwd |
| 129 | + ) |
| 130 | + |
| 131 | + git_ref = subprocess.run( |
| 132 | + ["git", "rev-parse", "origin/main"], capture_output=True |
| 133 | + ).stdout |
| 134 | + |
| 135 | + return f""" |
| 136 | +**Describe the bug** |
| 137 | +
|
| 138 | +The following code can not be parsed/formatted by black: |
| 139 | +
|
| 140 | +``` python |
| 141 | +{issue.src} |
| 142 | +``` |
| 143 | +
|
| 144 | +black reported the following error: |
| 145 | +``` |
| 146 | +> {" ".join(command)} |
| 147 | +{format_result.stderr.decode()} |
| 148 | +``` |
| 149 | +{reported_diff} |
| 150 | +
|
| 151 | +but it can be parsed by cpython: |
| 152 | +``` python |
| 153 | +{parse_code} |
| 154 | +``` |
| 155 | +result: |
| 156 | +``` |
| 157 | +{run_result.stderr.decode()} |
| 158 | +returncode: {run_result.returncode} |
| 159 | +``` |
| 160 | +
|
| 161 | +
|
| 162 | +**Environment** |
| 163 | +
|
| 164 | +<!-- Please complete the following information: --> |
| 165 | +
|
| 166 | +- Black's version: current main ({git_ref.decode().strip()}) |
| 167 | +- OS and Python version: Linux/Python {sys.version} |
| 168 | +
|
| 169 | +**Additional context** |
| 170 | +
|
| 171 | +The bug was found by pysource-codegen (see #3908) |
| 172 | +
|
| 173 | +""" |
| 174 | + |
| 175 | + |
| 176 | +def main() -> None: |
| 177 | + issue = find_issue() |
| 178 | + if issue is None: |
| 179 | + return |
| 180 | + |
| 181 | + issue = minimize_code(issue) |
| 182 | + |
| 183 | + while issue.mode.line_length > 1 and bug_in_code(issue): |
| 184 | + issue.mode.line_length -= 1 |
| 185 | + issue.mode.line_length += 1 |
| 186 | + |
| 187 | + issue = minimize_code(issue) |
| 188 | + |
| 189 | + while issue.mode.line_length <= 100 and bug_in_code(issue): |
| 190 | + issue.mode.line_length += 1 |
| 191 | + issue.mode.line_length -= 1 |
| 192 | + |
| 193 | + issue = minimize_code(issue) |
| 194 | + |
| 195 | + print(create_issue(issue)) |
| 196 | + |
| 197 | + |
| 198 | +if __name__ == "__main__": |
| 199 | + main() |
0 commit comments