Compare commits

..

1 Commits

Author SHA1 Message Date
Zanie
69fc2caa31 Add CI job to auto-update pre-commit dependencies weekly 2023-11-01 12:06:14 -05:00
25 changed files with 211 additions and 532 deletions

View File

@@ -209,13 +209,7 @@ jobs:
run: |
pip install ./python/ruff-ecosystem
- name: Restore cached checkouts
uses: actions/cache/restore@v3
with:
path: ./checkouts
key: ecosystem-checkouts
- name: Run `ruff check` stable ecosystem check
- name: Run `ruff check` ecosystem check
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
run: |
# Make executable, since artifact download doesn't preserve this
@@ -224,30 +218,13 @@ jobs:
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check
cat ecosystem-result-check-stable > $GITHUB_STEP_SUMMARY
echo "### Linter (stable)" > ecosystem-result
cat ecosystem-result-check-stable >> ecosystem-result
cat ecosystem-result-check > $GITHUB_STEP_SUMMARY
cat ecosystem-result-check > ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff check` preview ecosystem check
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
cat ecosystem-result-check-preview > $GITHUB_STEP_SUMMARY
echo "### Linter (preview)" >> ecosystem-result
cat ecosystem-result-check-preview >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff format` stable ecosystem check
- name: Run `ruff format` ecosystem check
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
run: |
# Make executable, since artifact download doesn't preserve this
@@ -256,34 +233,10 @@ jobs:
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format
cat ecosystem-result-format-stable > $GITHUB_STEP_SUMMARY
echo "### Formatter (stable)" >> ecosystem-result
cat ecosystem-result-format-stable >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff format` preview ecosystem check
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
cat ecosystem-result-format-preview > $GITHUB_STEP_SUMMARY
echo "### Formatter (preview)" >> ecosystem-result
cat ecosystem-result-format-preview >> ecosystem-result
echo "" >> ecosystem-result
- name: Save cached checkouts
uses: actions/cache/save@v3
with:
path: ./checkouts
key: ecosystem-checkouts
cat ecosystem-result-format > $GITHUB_STEP_SUMMARY
cat ecosystem-result-format >> ecosystem-result
- name: Export pull request number
run: |

View File

@@ -48,7 +48,9 @@ jobs:
id: generate-comment
if: steps.download-ecosystem-result.outputs.found_artifact == 'true'
run: |
echo '## `ruff-ecosystem` results' >> comment.txt
echo '## PR Check Results' >> comment.txt
echo "### Ecosystem" >> comment.txt
cat pr/ecosystem/ecosystem-result >> comment.txt
echo "" >> comment.txt

36
.github/workflows/pre-commit.yaml vendored Normal file
View File

@@ -0,0 +1,36 @@
# Until Dependabot support is released https://github.com/dependabot/dependabot-core/issues/1524
name: Pre-commit update
on:
# every week on monday
schedule:
- cron: "0 0 * * 1"
workflow_dispatch:
permissions:
pull-requests: write
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run autoupdate
run: |
pre-commit autoupdate
- name: Commit and push
run: |
git add ".pre-commit-config.yaml"
git commit -m "Upgrade pre-commit dependencies"
git push origin upgrade/pre-commit
- name: Open pull request
run: |
gh pr create --fill

View File

@@ -48,6 +48,7 @@ jobs:
args: --out dist
- name: "Test sdist"
run: |
rustup default $(cat rust-toolchain)
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
ruff --help
python -m ruff --help

View File

@@ -47,13 +47,10 @@ repos:
language: system
types: [rust]
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.3
hooks:
- id: ruff-format
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
name: ruff
entry: cargo run --bin ruff -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix
language: system
types_or: [python, pyi]
require_serial: true
exclude: |
@@ -62,6 +59,12 @@ repos:
crates/ruff_python_formatter/resources/.*
)$
# Black
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
# Prettier
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0

View File

@@ -337,15 +337,16 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
## Ecosystem CI
GitHub Actions will run your changes against a number of real-world projects from GitHub and
report on any linter or formatter differences. You can also run those checks locally via:
report on any diagnostic differences. You can also run those checks locally via:
```shell
pip install -e ./python/ruff-ecosystem
ruff-ecosystem check ruff "./target/debug/ruff"
ruff-ecosystem format ruff "./target/debug/ruff"
python scripts/check_ecosystem.py path/to/your/ruff path/to/older/ruff
```
See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/python/ruff-ecosystem) for more details.
You can also run the Ecosystem CI check in a Docker container across a larger set of projects by
downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-aggregate/blob/master/data/known-github-tomls.jsonl)
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/astral-sh/ruff/blob/main/scripts/Dockerfile.ecosystem).
Note that this check will take a while to run.
## Benchmarking and Profiling

View File

@@ -409,9 +409,6 @@ pub struct FormatCommand {
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// Set the line-length.
#[arg(long, help_heading = "Format configuration")]
pub line_length: Option<LineLength>,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
pub isolated: bool,
@@ -555,7 +552,6 @@ impl FormatCommand {
stdin_filename: self.stdin_filename,
},
CliOverrides {
line_length: self.line_length,
respect_gitignore: resolve_bool_arg(
self.respect_gitignore,
self.no_respect_gitignore,

View File

@@ -57,9 +57,3 @@ r'\%03o' % (ord(c),)
'(%r, %r, %r, %r)' % (hostname, address, username, '$PASSWORD')
'%r' % ({'server_school_roles': server_school_roles, 'is_school_multiserver_domain': is_school_multiserver_domain}, )
"%d" % (1 if x > 0 else 2)
# Special cases for %c allowing single character strings
# https://github.com/astral-sh/ruff/issues/8406
"%c" % ("x",)
"%c" % "x"
"%c" % "œ"

View File

@@ -23,11 +23,3 @@ MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])
MyType = typing.NamedTuple("MyType", [("a", int)], b=str)
MyType = typing.NamedTuple(typename="MyType", a=int, b=str)
# Regression test for: https://github.com/astral-sh/ruff/issues/8402#issuecomment-1788787357
S3File = NamedTuple(
"S3File",
[
("dataHPK",* str),
],
)

View File

@@ -43,6 +43,3 @@ if 1 is {1}:
if "a" == "a":
pass
if 1 in {*[1]}:
pass

View File

@@ -119,23 +119,12 @@ fn collect_specs(formats: &[CFormatStrOrBytes<String>]) -> Vec<&CFormatSpec> {
/// Return `true` if the format string is equivalent to the constant type
fn equivalent(format: &CFormatSpec, value: &Expr) -> bool {
let format_type = FormatType::from(format.format_char);
let format = FormatType::from(format.format_char);
match ResolvedPythonType::from(value) {
ResolvedPythonType::Atom(atom) => {
// Special case where `%c` allows single character strings to be formatted
if format.format_char == 'c' {
if let Expr::StringLiteral(string) = value {
let mut chars = string.chars();
if chars.next().is_some() && chars.next().is_none() {
return true;
}
}
}
format_type.is_compatible_with(atom)
ResolvedPythonType::Atom(atom) => format.is_compatible_with(atom),
ResolvedPythonType::Union(atoms) => {
atoms.iter().all(|atom| format.is_compatible_with(*atom))
}
ResolvedPythonType::Union(atoms) => atoms
.iter()
.all(|atom| format_type.is_compatible_with(*atom)),
ResolvedPythonType::Unknown => true,
ResolvedPythonType::TypeError => true,
}

View File

@@ -181,9 +181,6 @@ fn create_fields_from_fields_arg(fields: &Expr) -> Option<Vec<Stmt>> {
let [field, annotation] = elts.as_slice() else {
return None;
};
if annotation.is_starred_expr() {
return None;
}
let ast::ExprStringLiteral { value: field, .. } = field.as_string_literal_expr()?;
if !is_identifier(field) {
return None;

View File

@@ -1,7 +1,8 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::generate_comparison;
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprStringLiteral};
use ruff_python_ast::ExprStringLiteral;
use ruff_python_ast::{CmpOp, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -94,17 +95,13 @@ pub(crate) fn single_item_membership_test(
checker.diagnostics.push(diagnostic);
}
/// Return the single item wrapped in `Some` if the expression contains a single
/// item, otherwise return `None`.
/// Return the single item wrapped in Some if the expression contains a single
/// item, otherwise return None.
fn single_item(expr: &Expr) -> Option<&Expr> {
match expr {
Expr::List(ast::ExprList { elts, .. })
| Expr::Tuple(ast::ExprTuple { elts, .. })
| Expr::Set(ast::ExprSet { elts, .. }) => match elts.as_slice() {
[Expr::Starred(_)] => None,
[item] => Some(item),
_ => None,
},
Expr::List(list) if list.elts.len() == 1 => Some(&list.elts[0]),
Expr::Tuple(tuple) if tuple.elts.len() == 1 => Some(&tuple.elts[0]),
Expr::Set(set) if set.elts.len() == 1 => Some(&set.elts[0]),
string_expr @ Expr::StringLiteral(ExprStringLiteral { value: string, .. })
if string.chars().count() == 1 =>
{

View File

@@ -117,11 +117,10 @@ quote-style = "single"
```
The Ruff formatter also respects Ruff's [`line-length`](https://docs.astral.sh/ruff/settings/#line-length)
setting, which also can be provided via a `pyproject.toml` or `ruff.toml` file, or on the CLI, as
in:
setting, which also can be provided via a `pyproject.toml` or `ruff.toml` file.
```console
ruff format --line-length 100 /path/to/file.py
```toml
line-length = 80
```
### Excluding code from formatting

View File

@@ -402,9 +402,6 @@ File selection:
--exclude <FILE_PATTERN> List of paths, used to omit files and/or directories from analysis
--force-exclude Enforce exclusions, even for paths passed to Ruff directly on the command-line. Use `--no-force-exclude` to disable
Format configuration:
--line-length <LINE_LENGTH> Set the line-length
Log levels:
-v, --verbose Enable verbose logging
-q, --quiet Print diagnostics, but nothing else

View File

@@ -277,8 +277,6 @@ Ruff will also respect variants of these action comments with a `# ruff:` prefix
convey that the action comment is intended for Ruff, but are functionally equivalent to the
isort variants.
Unlike isort, Ruff does not respect action comments within docstrings.
See the [isort documentation](https://pycqa.github.io/isort/docs/configuration/action_comments.html)
for more.

View File

@@ -52,9 +52,6 @@ exclude = [
"crates/ruff_linter/resources/test/fixtures/**/*",
"crates/ruff_linter/src/rules/*/snapshots/**/*"
]
include = [
"rust-toolchain.toml"
]
[tool.ruff]
extend-exclude = [

View File

@@ -31,21 +31,6 @@ Run `ruff format` ecosystem checks comparing your debug build to your system Ruf
ruff-ecosystem format ruff "./target/debug/ruff"
```
Run `ruff format` ecosystem checks comparing with changes to code that is already formatted:
```shell
ruff-ecosystem format ruff "./target/debug/ruff" --format-comparison ruff-then-ruff
```
Run `ruff format` ecosystem checks comparing with the Black formatter:
```shell
ruff-ecosystem format black ruff -v --cache python/checkouts --format-comparison black-and-ruff
```
The default output format is markdown, which includes nice summaries of the changes. You can use `--output-format json` to display the raw data — this is
particularly useful when making changes to the ecosystem checks.
## Development
When developing, it can be useful to set the `--pdb` flag to drop into a debugger on failure:

View File

@@ -24,11 +24,12 @@ from ruff_ecosystem.types import (
Comparison,
Diff,
Result,
ToolError,
RuffError,
Serializable,
)
if TYPE_CHECKING:
from ruff_ecosystem.projects import CheckOptions, ClonedRepository, Project
from ruff_ecosystem.projects import ClonedRepository, Project
# Matches lines that are summaries rather than diagnostics
@@ -500,8 +501,8 @@ async def ruff_check(
*, executable: Path, path: Path, name: str, options: CheckOptions
) -> Sequence[str]:
"""Run the given ruff binary against the specified path."""
ruff_args = options.to_ruff_args()
logger.debug(f"Checking {name} with {executable} " + " ".join(ruff_args))
logger.debug(f"Checking {name} with {executable}")
ruff_args = options.to_cli_args()
start = time.time()
proc = await create_subprocess_exec(
@@ -518,7 +519,7 @@ async def ruff_check(
logger.debug(f"Finished checking {name} with {executable} in {end - start:.2f}s")
if proc.returncode != 0:
raise ToolError(err.decode("utf8"))
raise RuffError(err.decode("utf8"))
# Strip summary lines so the diff is only diagnostic lines
lines = [
@@ -528,3 +529,35 @@ async def ruff_check(
]
return lines
@dataclass(frozen=True)
class CheckOptions(Serializable):
"""
Ruff check options
"""
select: str = ""
ignore: str = ""
exclude: str = ""
# Generating fixes is slow and verbose
show_fixes: bool = False
# Limit the number of reported lines per rule
max_lines_per_rule: int | None = 50
def markdown(self) -> str:
return f"select {self.select} ignore {self.ignore} exclude {self.exclude}"
def to_cli_args(self) -> list[str]:
args = ["check", "--no-cache", "--exit-zero"]
if self.select:
args.extend(["--select", self.select])
if self.ignore:
args.extend(["--ignore", self.ignore])
if self.exclude:
args.extend(["--exclude", self.exclude])
if self.show_fixes:
args.extend(["--show-fixes", "--ecosystem-ci"])
return args

View File

@@ -12,7 +12,6 @@ from signal import SIGINT, SIGTERM
from ruff_ecosystem import logger
from ruff_ecosystem.defaults import DEFAULT_TARGETS
from ruff_ecosystem.format import FormatComparison
from ruff_ecosystem.main import OutputFormat, main
from ruff_ecosystem.projects import RuffCommand
@@ -46,58 +45,45 @@ def entrypoint():
tempfile.TemporaryDirectory() if not args.cache else nullcontext(args.cache)
)
baseline_executable = args.baseline_executable
if not args.baseline_executable.exists():
baseline_executable = get_executable_path(str(args.baseline_executable))
if not baseline_executable:
ruff_baseline = args.ruff_baseline
if not args.ruff_baseline.exists():
ruff_baseline = get_executable_path(str(args.ruff_baseline))
if not ruff_baseline:
print(
f"Could not find ruff baseline executable: {args.baseline_executable}",
f"Could not find ruff baseline executable: {args.ruff_baseline}",
sys.stderr,
)
exit(1)
logger.info(
"Resolved baseline executable %s to %s",
args.baseline_executable,
baseline_executable,
"Resolved baseline executable %s to %s", args.ruff_baseline, ruff_baseline
)
comparison_executable = args.comparison_executable
if not args.comparison_executable.exists():
comparison_executable = get_executable_path(str(args.comparison_executable))
if not comparison_executable:
ruff_comparison = args.ruff_comparison
if not args.ruff_comparison.exists():
ruff_comparison = get_executable_path(str(args.ruff_comparison))
if not ruff_comparison:
print(
f"Could not find ruff comparison executable: {args.comparison_executable}",
f"Could not find ruff comparison executable: {args.ruff_comparison}",
sys.stderr,
)
exit(1)
logger.info(
"Resolved comparison executable %s to %s",
args.comparison_executable,
comparison_executable,
args.ruff_comparison,
ruff_comparison,
)
targets = DEFAULT_TARGETS
if args.force_preview:
targets = [target.with_preview_enabled() for target in targets]
format_comparison = (
FormatComparison(args.format_comparison)
if args.ruff_command == RuffCommand.format.value
else None
)
with cache_context as cache:
loop = asyncio.get_event_loop()
main_task = asyncio.ensure_future(
main(
command=RuffCommand(args.ruff_command),
baseline_executable=baseline_executable,
comparison_executable=comparison_executable,
targets=targets,
ruff_baseline_executable=ruff_baseline,
ruff_comparison_executable=ruff_comparison,
targets=DEFAULT_TARGETS,
format=OutputFormat(args.output_format),
project_dir=Path(cache),
raise_on_failure=args.pdb,
format_comparison=format_comparison,
)
)
# https://stackoverflow.com/a/58840987/3549270
@@ -130,8 +116,8 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"--output-format",
choices=[option.value for option in OutputFormat],
default="markdown",
choices=[option.name for option in OutputFormat],
default="json",
help="Location for caching cloned repositories",
)
parser.add_argument(
@@ -145,28 +131,17 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Enable debugging on failure",
)
parser.add_argument(
"--force-preview",
action="store_true",
help="Force preview mode to be enabled for all projects",
)
parser.add_argument(
"--format-comparison",
choices=[option.value for option in FormatComparison],
default=FormatComparison.ruff_and_ruff,
help="Type of comparison to make when checking formatting.",
)
parser.add_argument(
"ruff_command",
choices=[option.value for option in RuffCommand],
choices=[option.name for option in RuffCommand],
help="The Ruff command to test",
)
parser.add_argument(
"baseline_executable",
"ruff_baseline",
type=Path,
)
parser.add_argument(
"comparison_executable",
"ruff_comparison",
type=Path,
)

View File

@@ -6,7 +6,7 @@ from __future__ import annotations
import time
from asyncio import create_subprocess_exec
from enum import Enum
from dataclasses import dataclass
from pathlib import Path
from subprocess import PIPE
from typing import TYPE_CHECKING, Sequence
@@ -15,10 +15,10 @@ from unidiff import PatchSet
from ruff_ecosystem import logger
from ruff_ecosystem.markdown import markdown_project_section
from ruff_ecosystem.types import Comparison, Diff, Result, ToolError
from ruff_ecosystem.types import Comparison, Diff, Result, RuffError
if TYPE_CHECKING:
from ruff_ecosystem.projects import ClonedRepository, FormatOptions
from ruff_ecosystem.projects import ClonedRepository
def markdown_format_result(result: Result) -> str:
@@ -124,89 +124,28 @@ async def compare_format(
ruff_comparison_executable: Path,
options: FormatOptions,
cloned_repo: ClonedRepository,
format_comparison: FormatComparison,
):
args = (ruff_baseline_executable, ruff_comparison_executable, options, cloned_repo)
match format_comparison:
case FormatComparison.ruff_then_ruff:
coro = format_then_format(Formatter.ruff, *args)
case FormatComparison.ruff_and_ruff:
coro = format_and_format(Formatter.ruff, *args)
case FormatComparison.black_then_ruff:
coro = format_then_format(Formatter.black, *args)
case FormatComparison.black_and_ruff:
coro = format_and_format(Formatter.black, *args)
case _:
raise ValueError(f"Unknown format comparison type {format_comparison!r}.")
diff = await coro
return Comparison(diff=Diff(diff), repo=cloned_repo)
async def format_then_format(
baseline_formatter: Formatter,
ruff_baseline_executable: Path,
ruff_comparison_executable: Path,
options: FormatOptions,
cloned_repo: ClonedRepository,
) -> Sequence[str]:
# Run format to get the baseline
await format(
formatter=baseline_formatter,
# Run format without diff to get the baseline
await ruff_format(
executable=ruff_baseline_executable.resolve(),
path=cloned_repo.path,
name=cloned_repo.fullname,
options=options,
)
# Then get the diff from stdout
diff = await format(
formatter=Formatter.ruff,
diff = await ruff_format(
executable=ruff_comparison_executable.resolve(),
path=cloned_repo.path,
name=cloned_repo.fullname,
options=options,
diff=True,
)
return diff
return Comparison(diff=Diff(diff), repo=cloned_repo)
async def format_and_format(
baseline_formatter: Formatter,
ruff_baseline_executable: Path,
ruff_comparison_executable: Path,
options: FormatOptions,
cloned_repo: ClonedRepository,
) -> Sequence[str]:
# Run format without diff to get the baseline
await format(
formatter=baseline_formatter,
executable=ruff_baseline_executable.resolve(),
path=cloned_repo.path,
name=cloned_repo.fullname,
options=options,
)
# Commit the changes
commit = await cloned_repo.commit(
message=f"Formatted with baseline {ruff_baseline_executable}"
)
# Then reset
await cloned_repo.reset()
# Then run format again
await format(
formatter=Formatter.ruff,
executable=ruff_comparison_executable.resolve(),
path=cloned_repo.path,
name=cloned_repo.fullname,
options=options,
)
# Then get the diff from the commit
diff = await cloned_repo.diff(commit)
return diff
async def format(
async def ruff_format(
*,
formatter: Formatter,
executable: Path,
path: Path,
name: str,
@@ -214,20 +153,16 @@ async def format(
diff: bool = False,
) -> Sequence[str]:
"""Run the given ruff binary against the specified path."""
args = (
options.to_ruff_args()
if formatter == Formatter.ruff
else options.to_black_args()
)
logger.debug(f"Formatting {name} with {executable} " + " ".join(args))
logger.debug(f"Formatting {name} with {executable}")
ruff_args = options.to_cli_args()
if diff:
args.append("--diff")
ruff_args.append("--diff")
start = time.time()
proc = await create_subprocess_exec(
executable.absolute(),
*args,
*ruff_args,
".",
stdout=PIPE,
stderr=PIPE,
@@ -239,34 +174,22 @@ async def format(
logger.debug(f"Finished formatting {name} with {executable} in {end - start:.2f}s")
if proc.returncode not in [0, 1]:
raise ToolError(err.decode("utf8"))
raise RuffError(err.decode("utf8"))
lines = result.decode("utf8").splitlines()
return lines
class FormatComparison(Enum):
ruff_then_ruff = "ruff-then-ruff"
@dataclass(frozen=True)
class FormatOptions:
"""
Run Ruff baseline then Ruff comparison; checks for changes in behavior when formatting previously "formatted" code
Ruff format options.
"""
ruff_and_ruff = "ruff-and-ruff"
"""
Run Ruff baseline then reset and run Ruff comparison; checks changes in behavior when formatting "unformatted" code
"""
exclude: str = ""
black_then_ruff = "black-then-ruff"
"""
Run Black baseline then Ruff comparison; checks for changes in behavior when formatting previously "formatted" code
"""
black_and_ruff = "black-and-ruff"
""""
Run Black baseline then reset and run Ruff comparison; checks changes in behavior when formatting "unformatted" code
"""
class Formatter(Enum):
black = "black"
ruff = "ruff"
def to_cli_args(self) -> list[str]:
args = ["format"]
if self.exclude:
args.extend(["--exclude", self.exclude])
return args

View File

@@ -7,11 +7,7 @@ from typing import Awaitable, TypeVar
from ruff_ecosystem import logger
from ruff_ecosystem.check import compare_check, markdown_check_result
from ruff_ecosystem.format import (
FormatComparison,
compare_format,
markdown_format_result,
)
from ruff_ecosystem.format import compare_format, markdown_format_result
from ruff_ecosystem.projects import (
Project,
RuffCommand,
@@ -29,21 +25,18 @@ class OutputFormat(Enum):
async def main(
command: RuffCommand,
baseline_executable: Path,
comparison_executable: Path,
ruff_baseline_executable: Path,
ruff_comparison_executable: Path,
targets: list[Project],
project_dir: Path,
format: OutputFormat,
format_comparison: FormatComparison | None,
max_parallelism: int = 50,
raise_on_failure: bool = False,
) -> None:
logger.debug("Using command %s", command.value)
logger.debug("Using baseline executable at %s", baseline_executable)
logger.debug("Using comparison executable at %s", comparison_executable)
logger.debug("Using baseline executable at %s", ruff_baseline_executable)
logger.debug("Using comparison executable at %s", ruff_comparison_executable)
logger.debug("Using checkout_dir directory %s", project_dir)
if format_comparison:
logger.debug("Using format comparison type %s", format_comparison.value)
logger.debug("Checking %s targets", len(targets))
# Limit parallelism to avoid high memory consumption
@@ -58,11 +51,10 @@ async def main(
limited_parallelism(
clone_and_compare(
command,
baseline_executable,
comparison_executable,
ruff_baseline_executable,
ruff_comparison_executable,
target,
project_dir,
format_comparison,
)
)
for target in targets
@@ -100,11 +92,10 @@ async def main(
async def clone_and_compare(
command: RuffCommand,
baseline_executable: Path,
comparison_executable: Path,
ruff_baseline_executable: Path,
ruff_comparison_executable: Path,
target: Project,
project_dir: Path,
format_comparison: FormatComparison | None,
) -> Comparison:
"""Check a specific repository against two versions of ruff."""
assert ":" not in target.repo.owner
@@ -112,12 +103,14 @@ async def clone_and_compare(
match command:
case RuffCommand.check:
compare, options, kwargs = (compare_check, target.check_options, {})
compare, options = (
compare_check,
target.check_options,
)
case RuffCommand.format:
compare, options, kwargs = (
compare, options = (
compare_format,
target.format_options,
{"format_comparison": format_comparison},
)
case _:
raise ValueError(f"Unknown target Ruff command {command}")
@@ -127,11 +120,10 @@ async def clone_and_compare(
try:
return await compare(
baseline_executable,
comparison_executable,
ruff_baseline_executable,
ruff_comparison_executable,
options,
cloned_repo,
**kwargs,
)
except ExceptionGroup as e:
raise e.exceptions[0] from e

View File

@@ -12,7 +12,7 @@ def markdown_project_section(
return markdown_details(
summary=f'<a href="{project.repo.url}">{project.repo.fullname}</a> ({title})',
# Show the command used for the check
preface="<pre>ruff " + " ".join(options.to_ruff_args()) + "</pre>",
preface="<pre>ruff " + " ".join(options.to_cli_args()) + "</pre>",
content=content,
)

View File

@@ -4,16 +4,16 @@ Abstractions and utilities for working with projects to run ecosystem checks on.
from __future__ import annotations
import abc
import dataclasses
from asyncio import create_subprocess_exec
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from subprocess import DEVNULL, PIPE
from subprocess import PIPE
from typing import Self
from ruff_ecosystem import logger
from ruff_ecosystem.check import CheckOptions
from ruff_ecosystem.format import FormatOptions
from ruff_ecosystem.types import Serializable
@@ -27,90 +27,12 @@ class Project(Serializable):
check_options: CheckOptions = field(default_factory=lambda: CheckOptions())
format_options: FormatOptions = field(default_factory=lambda: FormatOptions())
def with_preview_enabled(self: Self) -> Self:
return type(self)(
repo=self.repo,
check_options=self.check_options.with_options(preview=True),
format_options=self.format_options.with_options(preview=True),
)
class RuffCommand(Enum):
check = "check"
format = "format"
@dataclass(frozen=True)
class CommandOptions(Serializable, abc.ABC):
def with_options(self: Self, **kwargs) -> Self:
"""
Return a copy of self with the given options set.
"""
return type(self)(**{**dataclasses.asdict(self), **kwargs})
@abc.abstractmethod
def to_ruff_args(self) -> list[str]:
pass
@dataclass(frozen=True)
class CheckOptions(CommandOptions):
"""
Ruff check options
"""
select: str = ""
ignore: str = ""
exclude: str = ""
preview: bool = False
# Generating fixes is slow and verbose
show_fixes: bool = False
# Limit the number of reported lines per rule
max_lines_per_rule: int | None = 50
def to_ruff_args(self) -> list[str]:
args = ["check", "--no-cache", "--exit-zero"]
if self.select:
args.extend(["--select", self.select])
if self.ignore:
args.extend(["--ignore", self.ignore])
if self.exclude:
args.extend(["--exclude", self.exclude])
if self.show_fixes:
args.extend(["--show-fixes", "--ecosystem-ci"])
if self.preview:
args.append("--preview")
return args
@dataclass(frozen=True)
class FormatOptions(CommandOptions):
"""
Format ecosystem check options.
"""
preview: bool = False
exclude: str = ""
def to_ruff_args(self) -> list[str]:
args = ["format"]
if self.exclude:
args.extend(["--exclude", self.exclude])
if self.preview:
args.append("--preview")
return args
def to_black_args(self) -> list[str]:
args = []
if self.exclude:
args.extend(["--exclude", self.exclude])
if self.preview:
args.append("--preview")
return args
class ProjectSetupError(Exception):
"""An error setting up a project."""
@@ -138,11 +60,10 @@ class Repository(Serializable):
Shallow clone this repository
"""
if checkout_dir.exists():
logger.debug(f"Reusing cached {self.fullname}")
logger.debug(f"Reusing {self.owner}:{self.name}")
if self.ref:
logger.debug(f"Checking out {self.fullname} @ {self.ref}")
logger.debug(f"Checking out ref {self.ref}")
process = await create_subprocess_exec(
*["git", "checkout", "-f", self.ref],
cwd=checkout_dir,
@@ -156,68 +77,39 @@ class Repository(Serializable):
f"Failed to checkout {self.ref}: {stderr.decode()}"
)
cloned_repo = await ClonedRepository.from_path(checkout_dir, self)
await cloned_repo.reset()
return await ClonedRepository.from_path(checkout_dir, self)
logger.debug(f"Pulling latest changes for {self.fullname} @ {self.ref}")
await cloned_repo.pull()
logger.debug(f"Cloning {self.owner}:{self.name} to {checkout_dir}")
command = [
"git",
"clone",
"--config",
"advice.detachedHead=false",
"--quiet",
"--depth",
"1",
"--no-tags",
]
if self.ref:
command.extend(["--branch", self.ref])
else:
logger.debug(f"Cloning {self.owner}:{self.name} to {checkout_dir}")
command = [
"git",
"clone",
"--config",
"advice.detachedHead=false",
"--quiet",
"--depth",
"1",
"--no-tags",
]
if self.ref:
command.extend(["--branch", self.ref])
command.extend(
[
f"https://github.com/{self.owner}/{self.name}",
str(checkout_dir),
],
)
command.extend(
[
f"https://github.com/{self.owner}/{self.name}",
str(checkout_dir),
],
)
process = await create_subprocess_exec(
*command, env={"GIT_TERMINAL_PROMPT": "0"}
)
process = await create_subprocess_exec(
*command, env={"GIT_TERMINAL_PROMPT": "0"}
)
status_code = await process.wait()
status_code = await process.wait()
logger.debug(
f"Finished cloning {self.fullname} with status {status_code}",
)
cloned_repo = await ClonedRepository.from_path(checkout_dir, self)
# Configure git user — needed for `self.commit` to work
await (
await create_subprocess_exec(
*["git", "config", "user.email", "ecosystem@astral.sh"],
cwd=checkout_dir,
env={"GIT_TERMINAL_PROMPT": "0"},
stdout=DEVNULL,
stderr=DEVNULL,
)
).wait()
await (
await create_subprocess_exec(
*["git", "config", "user.name", "Ecosystem Bot"],
cwd=checkout_dir,
env={"GIT_TERMINAL_PROMPT": "0"},
stdout=DEVNULL,
stderr=DEVNULL,
)
).wait()
return cloned_repo
logger.debug(
f"Finished cloning {self.fullname} with status {status_code}",
)
return await ClonedRepository.from_path(checkout_dir, self)
@dataclass(frozen=True)
@@ -274,73 +166,3 @@ class ClonedRepository(Repository, Serializable):
raise ProjectSetupError(f"Failed to retrieve commit sha at {checkout_dir}")
return stdout.decode().strip()
async def reset(self: Self) -> None:
"""
Reset the cloned repository to the ref it started at.
"""
process = await create_subprocess_exec(
*["git", "reset", "--hard", "origin/" + self.ref] if self.ref else [],
cwd=self.path,
env={"GIT_TERMINAL_PROMPT": "0"},
stdout=PIPE,
stderr=PIPE,
)
_, stderr = await process.communicate()
if await process.wait() != 0:
raise RuntimeError(f"Failed to reset: {stderr.decode()}")
async def pull(self: Self) -> None:
"""
Pull the latest changes.
Typically `reset` should be run first.
"""
process = await create_subprocess_exec(
*["git", "pull"],
cwd=self.path,
env={"GIT_TERMINAL_PROMPT": "0"},
stdout=PIPE,
stderr=PIPE,
)
_, stderr = await process.communicate()
if await process.wait() != 0:
raise RuntimeError(f"Failed to pull: {stderr.decode()}")
async def commit(self: Self, message: str) -> str:
"""
Commit all current changes.
Empty commits are allowed.
"""
process = await create_subprocess_exec(
*["git", "commit", "--allow-empty", "-a", "-m", message],
cwd=self.path,
env={"GIT_TERMINAL_PROMPT": "0"},
stdout=PIPE,
stderr=PIPE,
)
_, stderr = await process.communicate()
if await process.wait() != 0:
raise RuntimeError(f"Failed to commit: {stderr.decode()}")
return await self._get_head_commit(self.path)
async def diff(self: Self, *args: str) -> list[str]:
"""
Get the current diff from git.
Arguments are passed to `git diff ...`
"""
process = await create_subprocess_exec(
*["git", "diff", *args],
cwd=self.path,
env={"GIT_TERMINAL_PROMPT": "0"},
stdout=PIPE,
stderr=PIPE,
)
stdout, stderr = await process.communicate()
if await process.wait() != 0:
raise RuntimeError(f"Failed to commit: {stderr.decode()}")
return stdout.decode().splitlines()

View File

@@ -89,5 +89,5 @@ class Comparison(Serializable):
repo: ClonedRepository
class ToolError(Exception):
"""An error reported by the checked executable."""
class RuffError(Exception):
"""An error reported by Ruff."""