Ecosystem CI: Optionally diff fixes (#4193)

* Generate fixes when using --show-fixes

Example command: `cargo run --bin ruff -- --no-cache --select F401
--show-source --show-fixes
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py`

Before, `--show-fixes` was ignored:

```
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py:4:22: F401 [*] `foo.baz` imported but unused
  |
4 | __all__ = ("bar",)
5 | from foo import bar, baz
  |                      ^^^ F401
  |
  = help: Remove unused import: `foo.baz`

Found 1 error.
[*] 1 potentially fixable with the --fix option.
```

After:

```
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py:4:22: F401 [*] `foo.baz` imported but unused
  |
4 | __all__ = ("bar",)
5 | from foo import bar, baz
  |                      ^^^ F401
  |
  = help: Remove unused import: `foo.baz`

ℹ Suggested fix
1 1 | """Test: late-binding of `__all__`."""
2 2 |
3 3 | __all__ = ("bar",)
4   |-from foo import bar, baz
  4 |+from foo import bar

Found 1 error.
[*] 1 potentially fixable with the --fix option.
```

Also fixes git clone
This commit is contained in:
konstin 2023-05-19 11:49:57 +02:00 committed by GitHub
parent 32f1edc555
commit 625849b846
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 33 additions and 10 deletions

View file

@ -17,11 +17,12 @@
# docker buildx build -f scripts/Dockerfile.ecosystem -t ruff-ecosystem-checker --load . # docker buildx build -f scripts/Dockerfile.ecosystem -t ruff-ecosystem-checker --load .
# docker run --rm -v ./target/x86_64-unknown-linux-musl/debug/ruff:/app/ruff-new -v ./ruff-old:/app/ruff-old ruff-ecosystem-checker # docker run --rm -v ./target/x86_64-unknown-linux-musl/debug/ruff:/app/ruff-new -v ./ruff-old:/app/ruff-old ruff-ecosystem-checker
# ``` # ```
# You can customize this, e.g. cache the git checkouts and use a custom json file: # You can customize this, e.g. cache the git checkouts, a custom json file and a glibc build:
# ``` # ```
# docker run -v ./target/x86_64-unknown-linux-musl/debug/ruff:/app/ruff-new -v ./ruff-old:/app/ruff-old \ # docker run -v ./target/debug/ruff:/app/ruff-new -v ./ruff-old:/app/ruff-old -v ./target/checkouts:/app/checkouts \
# -v ./target/checkouts:/app/checkouts -v ./github_search.jsonl:/app/github_search.jsonl \ # -v ./github_search.jsonl:/app/github_search.jsonl --rm ruff-ecosystem-checker \
# --rm ruff-ecosystem-checker python check_ecosystem.py -v ruff-new ruff-old --checkouts checkouts > output.txt # python check_ecosystem.py --verbose ruff-new ruff-old --projects github_search.jsonl --checkouts checkouts \
# > target/ecosystem-ci.txt
# ``` # ```
FROM python:3.11 FROM python:3.11

View file

@ -19,6 +19,7 @@ import time
from asyncio.subprocess import PIPE, create_subprocess_exec from asyncio.subprocess import PIPE, create_subprocess_exec
from contextlib import asynccontextmanager, nullcontext from contextlib import asynccontextmanager, nullcontext
from pathlib import Path from pathlib import Path
from signal import SIGINT, SIGTERM
from typing import TYPE_CHECKING, NamedTuple, Self from typing import TYPE_CHECKING, NamedTuple, Self
if TYPE_CHECKING: if TYPE_CHECKING:
@ -36,6 +37,8 @@ class Repository(NamedTuple):
select: str = "" select: str = ""
ignore: str = "" ignore: str = ""
exclude: str = "" exclude: str = ""
# Generating fixes is slow and verbose
show_fixes: bool = False
@asynccontextmanager @asynccontextmanager
async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[Path]: async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[Path]:
@ -102,6 +105,7 @@ async def check(
select: str = "", select: str = "",
ignore: str = "", ignore: str = "",
exclude: str = "", exclude: str = "",
show_fixes: bool = False,
) -> Sequence[str]: ) -> Sequence[str]:
"""Run the given ruff binary against the specified path.""" """Run the given ruff binary against the specified path."""
logger.debug(f"Checking {name} with {ruff}") logger.debug(f"Checking {name} with {ruff}")
@ -112,6 +116,8 @@ async def check(
ruff_args.extend(["--ignore", ignore]) ruff_args.extend(["--ignore", ignore])
if exclude: if exclude:
ruff_args.extend(["--exclude", exclude]) ruff_args.extend(["--exclude", exclude])
if show_fixes:
ruff_args.extend(["--show-fixes", "--ecosystem-ci"])
start = time.time() start = time.time()
proc = await create_subprocess_exec( proc = await create_subprocess_exec(
@ -169,16 +175,16 @@ async def compare(
# Allows to keep the checkouts locations # Allows to keep the checkouts locations
if checkouts: if checkouts:
checkout_dir = checkouts.joinpath(repo.org).joinpath(repo.repo) checkout_parent = checkouts.joinpath(repo.org)
# Don't create the repodir itself, we need that for checking for existing # Don't create the repodir itself, we need that for checking for existing
# clones # clones
checkout_dir.parent.mkdir(exist_ok=True, parents=True) checkout_parent.mkdir(exist_ok=True, parents=True)
location_context = nullcontext(checkout_dir) location_context = nullcontext(checkout_parent)
else: else:
location_context = tempfile.TemporaryDirectory() location_context = tempfile.TemporaryDirectory()
with location_context as checkout_dir: with location_context as checkout_parent:
checkout_dir = Path(checkout_dir) checkout_dir = Path(checkout_parent).joinpath(repo.repo)
async with repo.clone(checkout_dir) as path: async with repo.clone(checkout_dir) as path:
try: try:
async with asyncio.TaskGroup() as tg: async with asyncio.TaskGroup() as tg:
@ -190,6 +196,7 @@ async def compare(
select=repo.select, select=repo.select,
ignore=repo.ignore, ignore=repo.ignore,
exclude=repo.exclude, exclude=repo.exclude,
show_fixes=repo.show_fixes,
), ),
) )
check2 = tg.create_task( check2 = tg.create_task(
@ -200,6 +207,7 @@ async def compare(
select=repo.select, select=repo.select,
ignore=repo.ignore, ignore=repo.ignore,
exclude=repo.exclude, exclude=repo.exclude,
show_fixes=repo.show_fixes,
), ),
) )
except ExceptionGroup as e: except ExceptionGroup as e:
@ -237,6 +245,9 @@ def read_projects_jsonl(projects_jsonl: Path) -> dict[str, Repository]:
repository["owner"]["login"], repository["owner"]["login"],
repository["name"], repository["name"],
None, None,
select=repository.get("select"),
ignore=repository.get("ignore"),
exclude=repository.get("exclude"),
) )
else: else:
assert "owner" in data, "Unknown ruff-usage-aggregate format" assert "owner" in data, "Unknown ruff-usage-aggregate format"
@ -247,6 +258,9 @@ def read_projects_jsonl(projects_jsonl: Path) -> dict[str, Repository]:
data["owner"], data["owner"],
data["repo"], data["repo"],
data.get("ref"), data.get("ref"),
select=data.get("select"),
ignore=data.get("ignore"),
exclude=data.get("exclude"),
) )
return repositories return repositories
@ -414,7 +428,8 @@ if __name__ == "__main__":
else: else:
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
asyncio.run( loop = asyncio.get_event_loop()
main_task = asyncio.ensure_future(
main( main(
ruff1=args.ruff1, ruff1=args.ruff1,
ruff2=args.ruff2, ruff2=args.ruff2,
@ -422,3 +437,10 @@ if __name__ == "__main__":
checkouts=args.checkouts, checkouts=args.checkouts,
), ),
) )
# https://stackoverflow.com/a/58840987/3549270
for signal in [SIGINT, SIGTERM]:
loop.add_signal_handler(signal, main_task.cancel)
try:
loop.run_until_complete(main_task)
finally:
loop.close()