scripts/r8-float-env-codemod.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""One-shot codemod: replace float(os.getenv(...)) with float_env(...) in butler/."""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent / "butler"
SKIP = {"env_parse.py"}
PAT_MINMAX = re.compile(
r"max\(\s*(\d+(?:\.\d+)?)\s*,\s*min\(\s*(\d+(?:\.\d+)?)\s*,\s*float\(\s*os\.getenv\(\s*"
r'"([A-Z][A-Z0-9_]*)"\s*,\s*"([^"]*)"\s*\)\s*\)\s*\)\s*\)'
)
PAT_MAX = re.compile(
r"max\(\s*(\d+(?:\.\d+)?)\s*,\s*float\(\s*os\.getenv\(\s*"
r'"([A-Z][A-Z0-9_]*)"\s*,\s*"([^"]*)"\s*\)\s*\)\s*\)'
)
PAT_OR = re.compile(
r'float\(\s*os\.getenv\(\s*"([A-Z][A-Z0-9_]*)"\s*,\s*""\s*\)\s*or\s*"([^"]*)"\s*\)'
)
PAT_OR_NUM = re.compile(
r'float\(\s*os\.getenv\(\s*"([A-Z][A-Z0-9_]*)"\s*,\s*"([^"]*)"\s*\)\s*or\s*"([^"]*)"\s*\)'
)
PAT_SIMPLE = re.compile(
r'float\(\s*os\.getenv\(\s*"([A-Z][A-Z0-9_]*)"\s*,\s*"([^"]*)"\s*\)\s*\)'
)
PAT_MAX_OR_CONST = re.compile(
r"max\(\s*(\d+(?:\.\d+)?)\s*,\s*float\(\s*os\.getenv\(\s*"
r'"([A-Z][A-Z0-9_]*)"\s*,\s*""\s*\)\s*or\s*([_A-Z][A-Z0-9_]+)\s*\)\s*\)'
)
PAT_STRIP_OR = re.compile(
r'float\(\s*os\.getenv\(\s*"([A-Z][A-Z0-9_]*)"\s*,\s*"([^"]*)"\s*\)\.strip\(\)\s*or\s*"([^"]*)"\s*\)'
)
def _ensure_import(text: str) -> str:
if re.search(r"from butler\.env_parse import[^\n]*\bfloat_env\b", text):
return text
if "from butler.env_parse import" in text:
return re.sub(
r"(from butler\.env_parse import [^\n]+)",
lambda m: m.group(1).rstrip() + ", float_env",
text,
count=1,
)
anchor = "from __future__ import annotations\n\n"
if anchor in text:
return text.replace(
anchor,
anchor + "from butler.env_parse import float_env\n",
1,
)
return "from butler.env_parse import float_env\n\n" + text
def _transform(content: str) -> tuple[str, int]:
n = 0
def bump(m: re.Match[str], repl: str) -> str:
nonlocal n
n += 1
return repl
content = PAT_MINMAX.sub(
lambda m: bump(m, f'float_env("{m.group(3)}", {m.group(4)}, min={m.group(1)}, max={m.group(2)})'),
content,
)
content = PAT_MAX.sub(
lambda m: bump(m, f'float_env("{m.group(2)}", {m.group(3)}, min={m.group(1)})'),
content,
)
content = PAT_MAX_OR_CONST.sub(
lambda m: bump(m, f'float_env("{m.group(2)}", {m.group(3)}, min={m.group(1)})'),
content,
)
content = PAT_OR.sub(
lambda m: bump(m, f'float_env("{m.group(1)}", {m.group(2)})'),
content,
)
content = PAT_OR_NUM.sub(
lambda m: bump(m, f'float_env("{m.group(1)}", {m.group(3)})'),
content,
)
content = PAT_STRIP_OR.sub(
lambda m: bump(m, f'float_env("{m.group(1)}", {m.group(3)})'),
content,
)
content = PAT_SIMPLE.sub(
lambda m: bump(m, f'float_env("{m.group(1)}", {m.group(2)})'),
content,
)
if n:
content = _ensure_import(content)
return content, n
def main() -> None:
total = 0
for path in sorted(ROOT.rglob("*.py")):
if path.name in SKIP:
continue
original = path.read_text(encoding="utf-8")
updated, n = _transform(original)
if n:
path.write_text(updated, encoding="utf-8")
print(f"{path.relative_to(ROOT.parent)}: {n}")
total += n
print(f"total replacements: {total}")
if __name__ == "__main__":
main()