pyi generator git diff (#2460)

This commit is contained in:
benedikt-bartscher 2024-01-26 23:21:25 +01:00 committed by GitHub
parent 069892d8ee
commit 08524e22aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 171 additions and 19 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ examples/
.venv .venv
venv venv
requirements.txt requirements.txt
.pyi_generator_last_run
.pyi_generator_diff

View File

@ -5,8 +5,8 @@ import contextlib
import importlib import importlib
import inspect import inspect
import logging import logging
import os
import re import re
import subprocess
import sys import sys
import textwrap import textwrap
import typing import typing
@ -25,6 +25,12 @@ from reflex.vars import Var
logger = logging.getLogger("pyi_generator") logger = logging.getLogger("pyi_generator")
LAST_RUN_COMMIT_SHA_FILE = Path(".pyi_generator_last_run").resolve()
INIT_FILE = Path("reflex/__init__.pyi").resolve()
PWD = Path(".").resolve()
GENERATOR_FILE = Path(__file__).resolve()
GENERATOR_DIFF_FILE = Path(".pyi_generator_diff").resolve()
EXCLUDED_FILES = [ EXCLUDED_FILES = [
"__init__.py", "__init__.py",
"component.py", "component.py",
@ -62,6 +68,108 @@ DEFAULT_TYPING_IMPORTS = {
} }
def _walk_files(path):
"""Walk all files in a path.
This can be replaced with Path.walk() in python3.12.
Args:
path: The path to walk.
Yields:
The next file in the path.
"""
for p in Path(path).iterdir():
if p.is_dir():
yield from _walk_files(p)
continue
yield p.resolve()
def _relative_to_pwd(path: Path) -> Path:
"""Get the relative path of a path to the current working directory.
Args:
path: The path to get the relative path for.
Returns:
The relative path.
"""
return path.relative_to(PWD)
def _git_diff(args: list[str]) -> str:
"""Run a git diff command.
Args:
args: The args to pass to git diff.
Returns:
The output of the git diff command.
"""
cmd = ["git", "diff", "--no-color", *args]
return subprocess.run(cmd, capture_output=True, encoding="utf-8").stdout
def _git_changed_files(args: list[str] | None = None) -> list[Path]:
"""Get the list of changed files for a git diff command.
Args:
args: The args to pass to git diff.
Returns:
The list of changed files.
"""
if not args:
args = []
if "--name-only" not in args:
args.insert(0, "--name-only")
diff = _git_diff(args).splitlines()
return [Path(file.strip()) for file in diff]
def _get_changed_files() -> list[Path] | None:
"""Get the list of changed files since the last run of the generator.
Returns:
The list of changed files, or None if all files should be regenerated.
"""
try:
last_run_commit_sha = LAST_RUN_COMMIT_SHA_FILE.read_text().strip()
except FileNotFoundError:
logger.info(
"pyi_generator.py last run could not be determined, regenerating all .pyi files"
)
return None
changed_files = _git_changed_files([f"{last_run_commit_sha}..HEAD"])
# get all unstaged changes
changed_files.extend(_git_changed_files())
if _relative_to_pwd(GENERATOR_FILE) not in changed_files:
return changed_files
logger.info("pyi_generator.py has changed, checking diff now")
diff = "".join(_git_diff([GENERATOR_FILE.as_posix()]).splitlines()[2:])
try:
last_diff = GENERATOR_DIFF_FILE.read_text()
if diff != last_diff:
logger.info("pyi_generator.py has changed, regenerating all .pyi files")
changed_files = None
else:
logger.info(
"pyi_generator.py has not changed, only regenerating changed files"
)
except FileNotFoundError:
logger.info(
"pyi_generator.py diff could not be determined, regenerating all .pyi files"
)
changed_files = None
GENERATOR_DIFF_FILE.write_text(diff)
return changed_files
def _get_type_hint(value, type_hint_globals, is_optional=True) -> str: def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
"""Resolve the type hint for value. """Resolve the type hint for value.
@ -592,8 +700,9 @@ class PyiGenerator:
current_module: Any = {} current_module: Any = {}
def _write_pyi_file(self, module_path: Path, source: str): def _write_pyi_file(self, module_path: Path, source: str):
relpath = _relative_to_pwd(module_path)
pyi_content = [ pyi_content = [
f'"""Stub file for {module_path}"""', f'"""Stub file for {relpath}"""',
"# ------------------- DO NOT EDIT ----------------------", "# ------------------- DO NOT EDIT ----------------------",
"# This file was generated by `scripts/pyi_generator.py`!", "# This file was generated by `scripts/pyi_generator.py`!",
"# ------------------------------------------------------", "# ------------------------------------------------------",
@ -616,10 +725,13 @@ class PyiGenerator:
pyi_path = module_path.with_suffix(".pyi") pyi_path = module_path.with_suffix(".pyi")
pyi_path.write_text("\n".join(pyi_content)) pyi_path.write_text("\n".join(pyi_content))
logger.info(f"Wrote {pyi_path}") logger.info(f"Wrote {relpath}")
def _scan_file(self, module_path: Path): def _scan_file(self, module_path: Path):
module_import = str(module_path.with_suffix("")).replace("/", ".") # module_import = str(module_path.with_suffix("")).replace("/", ".")
module_import = (
_relative_to_pwd(module_path).with_suffix("").as_posix().replace("/", ".")
)
module = importlib.import_module(module_import) module = importlib.import_module(module_import)
logger.debug(f"Read {module_path}") logger.debug(f"Read {module_path}")
class_names = { class_names = {
@ -638,29 +750,56 @@ class PyiGenerator:
) )
self._write_pyi_file(module_path, ast.unparse(new_tree)) self._write_pyi_file(module_path, ast.unparse(new_tree))
def _scan_files_multiprocess(self, files): def _scan_files_multiprocess(self, files: list[Path]):
with Pool(processes=cpu_count()) as pool: with Pool(processes=cpu_count()) as pool:
pool.map(self._scan_file, files) pool.map(self._scan_file, files)
def scan_all(self, targets): def _scan_files(self, files: list[Path]):
for file in files:
self._scan_file(file)
def scan_all(self, targets, changed_files: list[Path] | None = None):
"""Scan all targets for class inheriting Component and generate the .pyi files. """Scan all targets for class inheriting Component and generate the .pyi files.
Args: Args:
targets: the list of file/folders to scan. targets: the list of file/folders to scan.
changed_files (optional): the list of changed files since the last run.
""" """
file_targets = [] file_targets = []
for target in targets: for target in targets:
path = Path(target) target_path = Path(target)
if target.endswith(".py") and path.is_file(): if target_path.is_file() and target_path.suffix == ".py":
file_targets.append(path) file_targets.append(target_path)
elif path.is_dir(): continue
for root, _, files in os.walk(path): if not target_path.is_dir():
for file in files: continue
if file in EXCLUDED_FILES or not file.endswith(".py"): for file_path in _walk_files(target_path):
continue relative = _relative_to_pwd(file_path)
file_targets.append(Path(root) / file) if relative.name in EXCLUDED_FILES or file_path.suffix != ".py":
continue
if (
changed_files is not None
and _relative_to_pwd(file_path) not in changed_files
):
continue
file_targets.append(file_path)
self._scan_files_multiprocess(file_targets) # check if pyi changed but not the source
if changed_files is not None:
for changed_file in changed_files:
if changed_file.suffix != ".pyi":
continue
py_file_path = changed_file.with_suffix(".py")
if not py_file_path.exists() and changed_file.exists():
changed_file.unlink()
if py_file_path in file_targets:
continue
subprocess.run(["git", "checkout", changed_file])
if cpu_count() == 1 or len(file_targets) < 5:
self._scan_files(file_targets)
else:
self._scan_files_multiprocess(file_targets)
def generate_init(): def generate_init():
@ -673,8 +812,7 @@ def generate_init():
] ]
imports.append("") imports.append("")
with open("reflex/__init__.pyi", "w") as pyi_file: INIT_FILE.write_text("\n".join(imports))
pyi_file.writelines("\n".join(imports))
if __name__ == "__main__": if __name__ == "__main__":
@ -683,6 +821,18 @@ if __name__ == "__main__":
targets = sys.argv[1:] if len(sys.argv) > 1 else ["reflex/components"] targets = sys.argv[1:] if len(sys.argv) > 1 else ["reflex/components"]
logger.info(f"Running .pyi generator for {targets}") logger.info(f"Running .pyi generator for {targets}")
changed_files = _get_changed_files()
if changed_files is None:
logger.info("Changed files could not be detected, regenerating all .pyi files")
else:
logger.info(f"Detected changed files: {changed_files}")
gen = PyiGenerator() gen = PyiGenerator()
gen.scan_all(targets) gen.scan_all(targets, changed_files)
generate_init() generate_init()
current_commit_sha = subprocess.run(
["git", "rev-parse", "HEAD"], capture_output=True, encoding="utf-8"
).stdout.strip()
LAST_RUN_COMMIT_SHA_FILE.write_text(current_commit_sha)