Compare commits

...

34 Commits

Author SHA1 Message Date
Charlie Marsh
3e7b92991b Bump version to v0.1.3 (#8259)
Includes the changelog, which I'm currently editing.
2023-10-26 18:57:05 +00:00
Jaap Roes
25d4ddaa60 Add title attribute to icons (#8060)
## Summary

Explain the meaning of the icon for screen readers (and mouse over).
Hide "inactive" (low opacity) icons from screen readers.

Remove opacity: 1 styling, it's the default opacity.

Without this change a screen reader will just read "Hammer and spanner
test tube" for the last column in each row.
2023-10-26 13:23:02 -04:00
Charlie Marsh
63a5a12a41 Improve documentation around linter-formatter conflicts (#8257)
Closes https://github.com/astral-sh/ruff/issues/8245.
2023-10-26 17:19:16 +00:00
Micha Reiser
c32f943d86 Don't warn about magic trailing comma when isort.force-single-line is true (#8244)
## Summary

Based on [this
feedback](https://github.com/astral-sh/ruff/issues/8185#issuecomment-1780092525).
Avoid warning about `force-wrap-aliases` and `split-on-trailing-comma`
if `force-single-line` is true (which creates a dedicated import for
each imported member).

## Test Plan

Ran `ruff format . --no-cache` and verified that the warning show up
when `force-single-line=false` and aren't shown when
`force-single-line=true`
2023-10-26 16:38:20 +00:00
Charlie Marsh
d211074f59 Clarify unsafe case in RSE102 (#8256) 2023-10-26 16:31:40 +00:00
Dhruv Manilawala
4ffd4ed61f Correct quick fix message for W605 (#8255)
## Summary

This PR fixes the `W605` rule implementation to provide the quickfix
message as
per the fix provided.

## Test Plan

Update snapshots.

fixes: #8155
2023-10-26 16:23:20 +00:00
Micha Reiser
a4dd1e5fad Refine the warnings about incompatible linter options (#8196)
## Summary

Avoid warning about incompatible rules except if their configuration
directly conflicts with the formatter. This should reduce the noise and
potentially the need for https://github.com/astral-sh/ruff/issues/8175
and https://github.com/astral-sh/ruff/issues/8185

I also extended the rule and option documentation to mention any
potential formatter incompatibilities or whether they're redundant when
using the formatter.

* `LineTooLong`: This is a use case we explicitly want to support. Don't
warn about it
* `TabIndentation`, `IndentWithSpaces`: Only warn if
`indent-style="tab"`
* `IndentationWithInvalidMultiple`,
`IndentationWithInvalidMultipleComment`: Only warn if `indent-width !=
4`
* `OverIndented`: Don't warn, but mention that the rule is redundant
* `BadQuotesInlineString`: Warn if quote setting is different from
`format.quote-style`
* `BadQuotesMultilineString`, `BadQuotesDocstring`: Warn if `quote !=
"double"`

## Test Plan

I added a new integration test for the default configuration with `ALL`.
`ruff format` now only shows two incompatible rules, which feels more
reasonable.
2023-10-26 16:22:56 +00:00
Charlie Marsh
be3307e9a6 Make unnecessary-paren-on-raise-exception an unsafe edit (#8231)
## Summary

This rule is now unsafe if we can't verify that the `obj` in `raise
obj()` is a class or builtin. (If we verify that it's a function, we
don't raise at all, as before.)

See the documentation change for motivation behind the unsafe edit.

Closes https://github.com/astral-sh/ruff/issues/8228.
2023-10-26 11:33:54 -04:00
konsti
317d3dd612 Add test and basic implementation for formatter preview mode (#8044)
**Summary** Prepare for the black preview style becoming the black
stable style at the end of the year.

This adds a new test file to compare stable and preview on some relevant
preview options in black, and makes `format_dev` understand the black
preview flag. I've added poetry as a project that uses preview.

I've implemented one specific deviation (collapsing of stub
implementation in non-stub files) which showed up in poetry for testing.
This also improves poetry compatibility from 0.99891 to 0.99919.

Fixes #7440

New compatibility stats:
| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75803 | 1799 | 1647 |
| django | 0.99983 | 2772 | 35 |
| home-assistant | 0.99953 | 10596 | 189 |
| poetry | 0.99919 | 317 | 12 |
| transformers | 0.99963 | 2657 | 332 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99978 | 3669 | 20 |
| warehouse | 0.99969 | 654 | 15 |
| zulip | 0.99970 | 1459 | 22 |
2023-10-26 15:33:26 +00:00
Micha Reiser
f5e850745c Only omit optional parentheses for starting or ending with parentheses (#8238) 2023-10-26 07:28:58 +01:00
Dhruv Manilawala
a7d1f7e1ec Use SourceKind::diff for formatter (#8240)
## Summary

This PR refactors the formatter diff code to reuse the
`SourceKind::diff` logic. This has the benefit that the Notebook diff
now includes the cell numbers which was not present before.

## Test Plan

Update the snapshots and verified the cell numbers.
2023-10-26 11:08:13 +05:30
Charlie Marsh
88c8b47326 Avoid introducing new parentheses in annotated assignments (#8233)
## Summary

We decided to avoid changing this in
https://github.com/astral-sh/ruff/issues/7315, but it's been reported
multiple times (e.g., in https://github.com/astral-sh/ruff/issues/8226,
also on Discord). I suggest we change it to improve compatibility. In
general, it also seems to lend itself to better code style.

Closes #8188 
Closes #8226

## Test Plan

Shows improvements for CPython, home-assistant, Poetry, and typeshed.

Before:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75803 | 1799 | 1647 |
| django | 0.99983 | 2772 | 34 |
| home-assistant | 0.99953 | 10596 | 186 |
| poetry | 0.99891 | 317 | 17 |
| transformers | 0.99966 | 2657 | 330 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99978 | 3669 | 20 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 22 |

After:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1647 |
| django | 0.99983 | 2772 | 34 |
| home-assistant | 0.99960 | 10596 | 156 |
| poetry | 0.99897 | 317 | 17 |
| transformers | 0.99966 | 2657 | 330 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 22 |
2023-10-25 22:51:50 -04:00
Micha Reiser
133a745de1 Use line-length setting for isort (#8235) 2023-10-26 02:16:59 +01:00
Micha Reiser
6983d96d27 Fix fmt:off with trailing child comment (#8234) 2023-10-26 01:03:34 +00:00
Charlie Marsh
3c3d9ab173 Insert necessary blank line between class and leading comments (#8224)
## Summary

Given:

```python
# comment

class A:
    def foo(self):
        pass
```

We need to insert an additional newline between `# comment` and `class
A`. We were missing this handling for the case in which `# comment` is a
leading comment on `class A`, as opposed to a trailing comment of some
preceding statement.

In practice, I think this only applies to the specific case in which a
class or function is the first statement in a module, and there's a
single empty line between a leading comment and that class or function.
If there are no empty lines, then the comment "sticks" to the
definition; if there are two or more, then `leading_comments` will
truncate appropriately. If the class or function is nested, then we only
need one empty line anyway.

Closes https://github.com/astral-sh/ruff/issues/8215.

## Test Plan

No change in similarity.

Before:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75803 | 1799 | 1647 |
| django | 0.99983 | 2772 | 34 |
| home-assistant | 0.99953 | 10596 | 186 |
| poetry | 0.99891 | 317 | 17 |
| transformers | 0.99966 | 2657 | 330 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99978 | 3669 | 20 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 22 |

After:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75803 | 1799 | 1648 |
| django | 0.99983 | 2772 | 34 |
| home-assistant | 0.99953 | 10596 | 186 |
| poetry | 0.99891 | 317 | 17 |
| transformers | 0.99966 | 2657 | 330 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99978 | 3669 | 20 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 22 |
2023-10-25 20:31:59 -04:00
Charlie Marsh
ff9fb0da54 Memoize and avoid candidate creation calls (#8230)
Trivial thing I noticed recently that improves performance by 1% in the
cached case :)
2023-10-25 17:50:58 -04:00
Ju4tCode
9792b1551b Add NoneBot to user list (#8198)
## Summary

Add [NoneBot](https://github.com/nonebot/nonebot2) to list of projects
using ruff. NoneBot is an asynchronous multi-platform chatbot framework
written in Python.
2023-10-25 15:03:04 -04:00
T-256
d1c67f91bd Document: Fix default lint rules (#8218)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

closes #8217 
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
2023-10-25 09:42:05 -05:00
Dhruv Manilawala
dbd84c947b Formatter parentheses support for IpyEscapeCommand (#8207)
## Summary

This PR removes the `todo!()` around `IpyEscapeCommand` in the
formatter.

The `NeedsParentheses` trait needs to be implemented which always return
`Never`. The reason being that if an escape command is parenthesized,
then that's not parsed as an escape command. IOW, the parentheses
shouldn't be present around an escape command.

In the similar way, the `CanSkipOptionalParenthesesVisitor` will skip
this node.

## Test Plan

Updated the `unformatted.ipynb` fixture with new cells containing
IPython escape commands and the corresponding snapshot was verified.
Also, tested it out in a few open source repositories containing
notebooks (`openai/openai-cookbook`, `huggingface/notebooks`).

#### New cells in `unformatted.ipynb`

**Cell 2**
```markdown
A markdown cell
```

**Cell 3**
```python
def some_function(foo, bar):
    pass
%matplotlib inline
```

**Cell 4**
```python
foo = %pwd
def some_function(foo,bar,):
	foo = %pwd
    print(foo
	)
```

fixes: #8204
2023-10-25 14:01:50 +00:00
Dhruv Manilawala
c2ec5f0bc9 Use source type to determine parser mode for formatting (#8205)
## Summary

This PR fixes the bug where if a Notebook contained IPython syntax, then
the format command would fail. This was because the correct mode was not
being used while parsing through the formatter code path.

## Test Plan

This PR isn't the only requirement for Notebook formatting to start
working with IPython escape commands. The following PR in the stack is
required as well.
2023-10-25 19:20:02 +05:30
Piotr Dybowski
31032f4f70 Fix skipping formatting examples (#8210) 2023-10-25 11:57:30 +01:00
Otso Velhonoja
f55b724254 Fix misspelled TOML headers in the tutorial (#8209)
## Summary

Fixes misspelled TOML headers in the tutorial regarding the
configuration of the Ruff Linter.
2023-10-25 12:52:42 +02:00
Micha Reiser
fd07a12a52 Refine warning about incompatible isort settings (#8192) 2023-10-25 08:41:17 +01:00
Ran Isenberg
1ee73bdedf docs: fix name of magic-trailing-comma option in README (#8200)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-10-25 06:31:43 +00:00
Micha Reiser
23b55aea30 Fix typo in max-doc-length documentation (#8201)
## Summary

Fix typo in `max-doc-length` documentation
2023-10-25 15:26:42 +09:00
Micha Reiser
e36afc3324 Avoid space around pow for None, True and False (#8189) 2023-10-25 07:24:06 +01:00
Spencer Brown
8304c41714 [pylint] Add buffer methods to bad-dunder-method-name (PLW3201) exclusions (#8190)
## Summary

Python 3.12 added the `__buffer__()`/`__release_buffer_()` special
methods, which are incorrectly flagged as invalid dunder methods by
`PLW3201`.

## Test Plan

Added definitions to the test suite, and confirmed they failed without
the fix and are ignored after the fix was done.
2023-10-25 00:03:44 -05:00
Zanie Blue
6f31e9c00e Match rule prefixes from external codes setting in unused-noqa (#8177)
Supersedes https://github.com/astral-sh/ruff/pull/8176
Closes https://github.com/astral-sh/ruff/pull/8174

## Test plan

Old snapshot contains the new / unmatched `V` code
New snapshot contains no `V` prefixed codes
2023-10-24 22:28:35 +00:00
Luca Mancusi
a6cc56fd98 Fix a wrong setting in configuration.md (#8186)
## Summary

The previous configuration for `ruff` contained an unrecognized field
`magic-trailing-comma` set to "respect". As of version 0.1.2 of `ruff`,
this field was not recognized and resulted in a TOML parse error when
running the `ruff format .` command. This change removes the
`magic-trailing-comma` field and adds the recognized
`skip-magic-trailing-comma` field set to `false`.

## Test Plan

Tested locally with `ruff` 0.1.2.
2023-10-24 17:05:09 -05:00
Charlie Marsh
0236e0751c Avoid sorting all paths in the format command (#8181)
## Summary

Related to https://github.com/astral-sh/ruff/issues/8135.

If we're not printing a `--diff`, or a summary of `--check` changes, we
can avoid sorting the list of results. Further, when sorting, we only
need to sort a small subset of the entries, in the common case (i.e., in
general, it's much more likely that a file is formatted than not).

## Test Plan

Local benchmarks suggest a 5-10% speedup on the cached behavior:

```
❯ hyperfine --warmup 3 "./target/release/ruff format ../airflow" "./target/release/sort format ../airflow"
Benchmark 1: ./target/release/ruff format ../airflow
  Time (mean ± σ):      70.3 ms ±   5.2 ms    [User: 52.1 ms, System: 59.0 ms]
  Range (min … max):    68.3 ms … 101.7 ms    42 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Benchmark 2: ./target/release/sort format ../airflow
  Time (mean ± σ):      66.0 ms ±   1.4 ms    [User: 48.3 ms, System: 58.4 ms]
  Range (min … max):    64.7 ms …  71.8 ms    44 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Summary
  './target/release/sort format ../airflow' ran
    1.07 ± 0.08 times faster than './target/release/ruff format ../airflow'
```
2023-10-24 20:54:06 +00:00
Zanie Blue
2d0769e324 Add external option to unused-noqa documentation (#8171) 2023-10-24 12:38:42 -05:00
Zanie Blue
80473c3f5c Link to 0.1.2 blog post (#8173) 2023-10-24 12:24:12 -05:00
Zanie Blue
4d7f90e045 Fix link to error supression documentation in unused-noqa (#8172) 2023-10-24 12:23:42 -05:00
Zanie Blue
75bd95e58c Add note about scope of rule changing in versioning policy (#8169)
Per some previous discussion, the policy is not clear about what happens
if the behavior is similar but the _scope_ in which a rule is applied
changes.
2023-10-24 11:38:31 -05:00
91 changed files with 2541 additions and 618 deletions

View File

@@ -1,9 +1,53 @@
# Changelog
## 0.1.3
This release includes a variety of improvements to the Ruff formatter, removing several known and
unintentional deviations from Black.
### Formatter
- Avoid space around pow for `None`, `True` and `False` ([#8189](https://github.com/astral-sh/ruff/pull/8189))
- Avoid sorting all paths in the format command ([#8181](https://github.com/astral-sh/ruff/pull/8181))
- Insert necessary blank line between class and leading comments ([#8224](https://github.com/astral-sh/ruff/pull/8224))
- Avoid introducing new parentheses in annotated assignments ([#8233](https://github.com/astral-sh/ruff/pull/8233))
- Refine the warnings about incompatible linter options ([#8196](https://github.com/astral-sh/ruff/pull/8196))
- Add test and basic implementation for formatter preview mode ([#8044](https://github.com/astral-sh/ruff/pull/8044))
- Refine warning about incompatible `isort` settings ([#8192](https://github.com/astral-sh/ruff/pull/8192))
- Only omit optional parentheses for starting or ending with parentheses ([#8238](https://github.com/astral-sh/ruff/pull/8238))
- Use source type to determine parser mode for formatting ([#8205](https://github.com/astral-sh/ruff/pull/8205))
- Don't warn about magic trailing comma when `isort.force-single-line` is true ([#8244](https://github.com/astral-sh/ruff/pull/8244))
- Use `SourceKind::diff` for formatter ([#8240](https://github.com/astral-sh/ruff/pull/8240))
- Fix `fmt:off` with trailing child comment ([#8234](https://github.com/astral-sh/ruff/pull/8234))
- Formatter parentheses support for `IpyEscapeCommand` ([#8207](https://github.com/astral-sh/ruff/pull/8207))
### Linter
- \[`pylint`\] Add buffer methods to `bad-dunder-method-name` (`PLW3201`) exclusions ([#8190](https://github.com/astral-sh/ruff/pull/8190))
- Match rule prefixes from `external` codes setting in `unused-noqa` ([#8177](https://github.com/astral-sh/ruff/pull/8177))
- Use `line-length` setting for isort in lieu of `pycodestyle.max-line-length` ([#8235](https://github.com/astral-sh/ruff/pull/8235))
- Update fix for `unnecessary-paren-on-raise-exception` to unsafe for unknown types ([#8231](https://github.com/astral-sh/ruff/pull/8231))
- Correct quick fix message for `W605` ([#8255](https://github.com/astral-sh/ruff/pull/8255))
### Documentation
- Fix typo in max-doc-length documentation ([#8201](https://github.com/astral-sh/ruff/pull/8201))
- Improve documentation around linter-formatter conflicts ([#8257](https://github.com/astral-sh/ruff/pull/8257))
- Fix link to error suppression documentation in `unused-noqa` ([#8172](https://github.com/astral-sh/ruff/pull/8172))
- Add `external` option to `unused-noqa` documentation ([#8171](https://github.com/astral-sh/ruff/pull/8171))
- Add title attribute to icons ([#8060](https://github.com/astral-sh/ruff/pull/8060))
- Clarify unsafe case in RSE102 ([#8256](https://github.com/astral-sh/ruff/pull/8256))
- Fix skipping formatting examples ([#8210](https://github.com/astral-sh/ruff/pull/8210))
- docs: fix name of `magic-trailing-comma` option in README ([#8200](https://github.com/astral-sh/ruff/pull/8200))
- Add note about scope of rule changing in versioning policy ([#8169](https://github.com/astral-sh/ruff/pull/8169))
- Document: Fix default lint rules ([#8218](https://github.com/astral-sh/ruff/pull/8218))
- Fix a wrong setting in configuration.md ([#8186](https://github.com/astral-sh/ruff/pull/8186))
- Fix misspelled TOML headers in the tutorial ([#8209](https://github.com/astral-sh/ruff/pull/8209))
## 0.1.2
This release includes the Beta version of the Ruff formatter — an extremely fast, Black-compatible Python formatter.
Try it today with `ruff format`.
Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blog/the-ruff-formatter) and [read the docs](https://docs.astral.sh/ruff/formatter/).
### Preview features

8
Cargo.lock generated
View File

@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"anyhow",
"clap",
@@ -2051,7 +2051,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2188,7 +2188,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.1",
@@ -2438,7 +2438,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"anyhow",
"clap",

View File

@@ -151,7 +151,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
# Run the Ruff linter.
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.2
rev: v0.1.3
hooks:
# Run the Ruff linter.
- id: ruff
@@ -238,7 +238,7 @@ quote-style = "double"
indent-style = "space"
# Like Black, respect magic trailing commas.
magic-trailing-comma = "respect"
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
@@ -409,6 +409,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Mypy](https://github.com/python/mypy)
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
- [Neon](https://github.com/neondatabase/neon)
- [NoneBot](https://github.com/nonebot/nonebot2)
- [ONNX](https://github.com/onnx/onnx)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [PDM](https://github.com/pdm-project/pdm)

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.1.2"
version = "0.1.3"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.2"
version = "0.1.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -11,6 +11,41 @@
"maths = (numpy.arange(100)**2).sum()\n",
"stats= numpy.asarray([1,2,3,4]).median()"
]
},
{
"cell_type": "markdown",
"id": "83a0b1b8",
"metadata": {},
"source": [
"A markdown cell"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ae12f012",
"metadata": {},
"outputs": [],
"source": [
"# A cell with IPython escape command\n",
"def some_function(foo, bar):\n",
" pass\n",
"%matplotlib inline"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10f3bbf9",
"metadata": {},
"outputs": [],
"source": [
"foo = %pwd\n",
"def some_function(foo,bar,):\n",
" # Another cell with IPython escape command\n",
" foo = %pwd\n",
" print(foo)"
]
}
],
"metadata": {

View File

@@ -11,7 +11,6 @@ use itertools::Itertools;
use log::{error, warn};
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use similar::TextDiff;
use thiserror::Error;
use tracing::debug;
@@ -19,12 +18,11 @@ use ruff_diagnostics::SourceMap;
use ruff_linter::fs;
use ruff_linter::logging::LogLevel;
use ruff_linter::registry::Rule;
use ruff_linter::rules::isort;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::rules::flake8_quotes::settings::Quote;
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError};
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
@@ -107,7 +105,7 @@ pub(crate) fn format(
};
let start = Instant::now();
let (mut results, mut errors): (Vec<_>, Vec<_>) = paths
let (results, mut errors): (Vec<_>, Vec<_>) = paths
.par_iter()
.filter_map(|entry| {
match entry {
@@ -168,27 +166,6 @@ pub(crate) fn format(
});
let duration = start.elapsed();
// Make output deterministic, at least as long as we have a path
results.sort_unstable_by(|x, y| x.path.cmp(&y.path));
errors.sort_by(|x, y| {
fn get_key(error: &FormatCommandError) -> Option<&PathBuf> {
match &error {
FormatCommandError::Ignore(ignore) => {
if let ignore::Error::WithPath { path, .. } = ignore {
Some(path)
} else {
None
}
}
FormatCommandError::Panic(path, _)
| FormatCommandError::Read(path, _)
| FormatCommandError::Format(path, _)
| FormatCommandError::Write(path, _) => path.as_ref(),
}
}
get_key(x).cmp(&get_key(y))
});
debug!(
"Formatted {} files in {:.2?}",
results.len() + errors.len(),
@@ -198,15 +175,21 @@ pub(crate) fn format(
caches.persist()?;
// Report on any errors.
errors.sort_unstable_by(|a, b| a.path().cmp(&b.path()));
for error in &errors {
error!("{error}");
}
results.sort_unstable_by(|a, b| a.path.cmp(&b.path));
let results = FormatResults::new(results.as_slice(), mode);
if mode.is_diff() {
results.write_diff(&mut stdout().lock())?;
match mode {
FormatMode::Write => {}
FormatMode::Check => {
results.write_changed(&mut stdout().lock())?;
}
FormatMode::Diff => {
results.write_diff(&mut stdout().lock())?;
}
}
// Report on the formatting changes.
@@ -470,27 +453,51 @@ impl<'a> FormatResults<'a> {
})
}
/// Write a diff of the formatting changes to the given writer.
fn write_diff(&self, f: &mut impl Write) -> io::Result<()> {
for result in self.results {
if let FormatResult::Diff {
unformatted,
formatted,
} = &result.result
{
let text_diff =
TextDiff::from_lines(unformatted.source_code(), formatted.source_code());
let mut unified_diff = text_diff.unified_diff();
unified_diff.header(
&fs::relativize_path(&result.path),
&fs::relativize_path(&result.path),
);
unified_diff.to_writer(&mut *f)?;
}
for (path, unformatted, formatted) in self
.results
.iter()
.filter_map(|result| {
if let FormatResult::Diff {
unformatted,
formatted,
} = &result.result
{
Some((result.path.as_path(), unformatted, formatted))
} else {
None
}
})
.sorted_unstable_by_key(|(path, _, _)| *path)
{
unformatted.diff(formatted, Some(path), f)?;
}
Ok(())
}
/// Write a list of the files that would be changed to the given writer.
fn write_changed(&self, f: &mut impl Write) -> io::Result<()> {
for path in self
.results
.iter()
.filter_map(|result| {
if result.result.is_formatted() {
Some(result.path.as_path())
} else {
None
}
})
.sorted_unstable()
{
writeln!(f, "Would reformat: {}", fs::relativize_path(path).bold())?;
}
Ok(())
}
/// Write a summary of the formatting results to the given writer.
fn write_summary(&self, f: &mut impl Write) -> io::Result<()> {
// Compute the number of changed and unchanged files.
let mut changed = 0u32;
@@ -498,14 +505,6 @@ impl<'a> FormatResults<'a> {
for result in self.results {
match &result.result {
FormatResult::Formatted => {
// If we're running in check mode, report on any files that would be formatted.
if self.mode.is_check() {
writeln!(
f,
"Would reformat: {}",
fs::relativize_path(&result.path).bold()
)?;
}
changed += 1;
}
FormatResult::Unchanged => unchanged += 1,
@@ -562,6 +561,26 @@ pub(crate) enum FormatCommandError {
Read(Option<PathBuf>, SourceError),
Format(Option<PathBuf>, FormatModuleError),
Write(Option<PathBuf>, SourceError),
Diff(Option<PathBuf>, io::Error),
}
impl FormatCommandError {
fn path(&self) -> Option<&Path> {
match self {
Self::Ignore(err) => {
if let ignore::Error::WithPath { path, .. } = err {
Some(path.as_path())
} else {
None
}
}
Self::Panic(path, _)
| Self::Read(path, _)
| Self::Format(path, _)
| Self::Write(path, _)
| Self::Diff(path, _) => path.as_deref(),
}
}
}
impl Display for FormatCommandError {
@@ -627,6 +646,24 @@ impl Display for FormatCommandError {
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
}
}
Self::Diff(path, err) => {
if let Some(path) = path {
write!(
f,
"{}{}{} {err}",
"Failed to generate diff for ".bold(),
fs::relativize_path(path).bold(),
":".bold()
)
} else {
write!(
f,
"{}{} {err}",
"Failed to generate diff".bold(),
":".bold()
)
}
}
Self::Panic(path, err) => {
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
@@ -663,59 +700,110 @@ pub(super) fn warn_incompatible_formatter_settings(
{
let mut incompatible_rules = Vec::new();
for incompatible_rule in RuleTable::from_iter([
Rule::LineTooLong,
Rule::TabIndentation,
Rule::IndentationWithInvalidMultiple,
Rule::IndentationWithInvalidMultipleComment,
Rule::OverIndented,
Rule::IndentWithSpaces,
for rule in [
// The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation,
// Flags missing trailing commas when all arguments are on its own line:
// ```python
// def args(
// aaaaaaaa, bbbbbbbbb, cccccccccc, ddddddddd, eeeeeeee, ffffff, gggggggggggg, hhhh
// ):
// pass
// ```
Rule::MissingTrailingComma,
Rule::ProhibitedTrailingComma,
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
Rule::AvoidableEscapedQuote,
])
.iter_enabled()
{
if setting.linter.rules.enabled(incompatible_rule) {
incompatible_rules.push(format!("'{}'", incompatible_rule.noqa_code()));
] {
if setting.linter.rules.enabled(rule) {
incompatible_rules.push(rule);
}
}
// Rules asserting for space indentation
if setting.formatter.indent_style.is_tab() {
for rule in [Rule::TabIndentation, Rule::IndentWithSpaces] {
if setting.linter.rules.enabled(rule) {
incompatible_rules.push(rule);
}
}
}
// Rules asserting for indent-width=4
if setting.formatter.indent_width.value() != 4 {
for rule in [
Rule::IndentationWithInvalidMultiple,
Rule::IndentationWithInvalidMultipleComment,
] {
if setting.linter.rules.enabled(rule) {
incompatible_rules.push(rule);
}
}
}
if !incompatible_rules.is_empty() {
incompatible_rules.sort();
warn!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.", incompatible_rules.join(", "));
let mut rule_names: Vec<_> = incompatible_rules
.into_iter()
.map(|rule| format!("`{}`", rule.noqa_code()))
.collect();
rule_names.sort();
warn!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.", rule_names.join(", "));
}
let mut incompatible_options = Vec::new();
let isort_defaults = isort::settings::Settings::default();
if setting.linter.isort.force_single_line != isort_defaults.force_single_line {
incompatible_options.push("'isort.force-single-line'");
// Rules with different quote styles.
if setting
.linter
.rules
.any_enabled(&[Rule::BadQuotesInlineString, Rule::AvoidableEscapedQuote])
{
match (
setting.linter.flake8_quotes.inline_quotes,
setting.formatter.quote_style,
) {
(Quote::Double, QuoteStyle::Single) => {
warn!("The `flake8-quotes.inline-quotes=\"double\"` option is incompatible with the formatter's `format.quote-style=\"single\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
}
(Quote::Single, QuoteStyle::Double) => {
warn!("The `flake8-quotes.inline-quotes=\"single\"` option is incompatible with the formatter's `format.quote-style=\"double\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
}
_ => {}
}
}
if setting.linter.isort.force_wrap_aliases != isort_defaults.force_wrap_aliases {
incompatible_options.push("'isort.force-wrap-aliases'");
if setting.linter.rules.enabled(Rule::BadQuotesMultilineString)
&& setting.linter.flake8_quotes.multiline_quotes == Quote::Single
{
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`");
}
if setting.linter.isort.lines_after_imports != isort_defaults.lines_after_imports {
incompatible_options.push("'isort.lines-after-imports'");
if setting.linter.rules.enabled(Rule::BadQuotesDocstring)
&& setting.linter.flake8_quotes.docstring_quotes == Quote::Single
{
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
}
if setting.linter.isort.lines_between_types != isort_defaults.lines_between_types {
incompatible_options.push("'isort.lines_between_types'");
}
if setting.linter.rules.enabled(Rule::UnsortedImports) {
// The formatter removes empty lines if the value is larger than 2 but always inserts a empty line after imports.
// Two empty lines are okay because `isort` only uses this setting for top-level imports (not in nested blocks).
if !matches!(setting.linter.isort.lines_after_imports, 1 | 2 | -1) {
warn!("The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).");
}
if setting.linter.isort.split_on_trailing_comma != isort_defaults.split_on_trailing_comma {
incompatible_options.push("'isort.split_on_trailing_comma'");
}
// Values larger than two get reduced to one line by the formatter if the import is in a nested block.
if setting.linter.isort.lines_between_types > 1 {
warn!("The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).");
}
if !incompatible_options.is_empty() {
warn!("The following isort options may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these options by removing them from the configuration.", incompatible_options.join(", "));
// isort inserts a trailing comma which the formatter preserves, but only if `skip-magic-trailing-comma` isn't false.
// This isn't relevant when using `force-single-line`, since isort will never include a trailing comma in that case.
if setting.formatter.magic_trailing_comma.is_ignore()
&& !setting.linter.isort.force_single_line
{
if setting.linter.isort.force_wrap_aliases {
warn!("The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.");
}
if setting.linter.isort.split_on_trailing_comma {
warn!("The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.");
}
}
}
}
}

View File

@@ -3,8 +3,6 @@ use std::path::Path;
use anyhow::Result;
use log::error;
use ruff_linter::fs;
use similar::TextDiff;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
@@ -109,14 +107,9 @@ fn format_source_code(
}
FormatMode::Check => {}
FormatMode::Diff => {
let mut writer = stdout().lock();
let text_diff =
TextDiff::from_lines(source_kind.source_code(), formatted.source_code());
let mut unified_diff = text_diff.unified_diff();
if let Some(path) = path {
unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path));
}
unified_diff.to_writer(&mut writer).unwrap();
source_kind
.diff(formatted, path, &mut stdout().lock())
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
}
},
FormattedSource::Unchanged => {

View File

@@ -230,7 +230,9 @@ fn format_option_inheritance() -> Result<()> {
&ruff_toml,
r#"
extend = "base.toml"
extend-select = ["Q000"]
[lint]
extend-select = ["COM812"]
[format]
quote-style = "single"
@@ -273,7 +275,7 @@ if condition:
print('Should change quotes')
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: 'Q000'. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
warning: The following rules may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
"###);
Ok(())
}
@@ -359,15 +361,27 @@ fn conflicting_options() -> Result<()> {
fs::write(
&ruff_toml,
r#"
indent-width = 2
[lint]
select = ["ALL"]
ignore = ["D203", "D212"]
[isort]
force-single-line = true
force-wrap-aliases = true
lines-after-imports = 0
[lint.isort]
lines-after-imports = 3
lines-between-types = 2
force-wrap-aliases = true
combine-as-imports = true
split-on-trailing-comma = true
[lint.flake8-quotes]
inline-quotes = "single"
docstring-quotes = "single"
multiline-quotes = "single"
[format]
skip-magic-trailing-comma = true
indent-style = "tab"
"#,
)?;
@@ -389,8 +403,14 @@ def say_hy(name: str):
1 file reformatted
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: 'COM812', 'COM819', 'D206', 'E501', 'ISC001', 'Q000', 'Q001', 'Q002', 'Q003', 'W191'. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
warning: The following isort options may cause conflicts when used with the formatter: 'isort.force-single-line', 'isort.force-wrap-aliases', 'isort.lines-after-imports', 'isort.lines_between_types'. To avoid unexpected behavior, we recommend disabling these options by removing them from the configuration.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
"###);
Ok(())
}
@@ -402,15 +422,27 @@ fn conflicting_options_stdin() -> Result<()> {
fs::write(
&ruff_toml,
r#"
indent-width = 2
[lint]
select = ["ALL"]
ignore = ["D203", "D212"]
[isort]
force-single-line = true
force-wrap-aliases = true
lines-after-imports = 0
[lint.isort]
lines-after-imports = 3
lines-between-types = 2
force-wrap-aliases = true
combine-as-imports = true
split-on-trailing-comma = true
[lint.flake8-quotes]
inline-quotes = "single"
docstring-quotes = "single"
multiline-quotes = "single"
[format]
skip-magic-trailing-comma = true
indent-style = "tab"
"#,
)?;
@@ -425,14 +457,110 @@ def say_hy(name: str):
exit_code: 0
----- stdout -----
def say_hy(name: str):
print(f"Hy {name}")
print(f"Hy {name}")
----- stderr -----
warning: The following rules may cause conflicts when used with the formatter: 'COM812', 'COM819', 'D206', 'E501', 'ISC001', 'Q000', 'Q001', 'Q002', 'Q003', 'W191'. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
warning: The following isort options may cause conflicts when used with the formatter: 'isort.force-single-line', 'isort.force-wrap-aliases', 'isort.lines-after-imports', 'isort.lines_between_types'. To avoid unexpected behavior, we recommend disabling these options by removing them from the configuration.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
"###);
Ok(())
}
#[test]
fn valid_linter_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
select = ["ALL"]
ignore = ["D203", "D212", "COM812", "ISC001"]
[lint.isort]
lines-after-imports = 2
lines-between-types = 1
force-wrap-aliases = true
combine-as-imports = true
split-on-trailing-comma = true
[lint.flake8-quotes]
inline-quotes = "single"
docstring-quotes = "double"
multiline-quotes = "double"
[format]
skip-magic-trailing-comma = false
quote-style = "single"
"#,
)?;
let test_path = tempdir.path().join("test.py");
fs::write(
&test_path,
r#"
def say_hy(name: str):
print(f"Hy {name}")"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg(test_path), @r###"
success: true
exit_code: 0
----- stdout -----
1 file reformatted
----- stderr -----
"###);
Ok(())
}
#[test]
fn all_rules_default_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
select = ["ALL"]
"#,
)?;
let test_path = tempdir.path().join("test.py");
fs::write(
&test_path,
r#"
def say_hy(name: str):
print(f"Hy {name}")"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg(test_path), @r###"
success: true
exit_code: 0
----- stdout -----
1 file reformatted
----- stderr -----
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
"###);
Ok(())
}
#[test]
fn test_diff() {
let args = ["format", "--no-cache", "--isolated", "--diff"];
@@ -452,8 +580,8 @@ fn test_diff() {
success: false
exit_code: 1
----- stdout -----
--- resources/test/fixtures/unformatted.ipynb
+++ resources/test/fixtures/unformatted.ipynb
--- resources/test/fixtures/unformatted.ipynb:cell 1
+++ resources/test/fixtures/unformatted.ipynb:cell 1
@@ -1,3 +1,4 @@
import numpy
-maths = (numpy.arange(100)**2).sum()
@@ -461,6 +589,30 @@ fn test_diff() {
+
+maths = (numpy.arange(100) ** 2).sum()
+stats = numpy.asarray([1, 2, 3, 4]).median()
--- resources/test/fixtures/unformatted.ipynb:cell 3
+++ resources/test/fixtures/unformatted.ipynb:cell 3
@@ -1,4 +1,6 @@
# A cell with IPython escape command
def some_function(foo, bar):
pass
+
+
%matplotlib inline
--- resources/test/fixtures/unformatted.ipynb:cell 4
+++ resources/test/fixtures/unformatted.ipynb:cell 4
@@ -1,5 +1,10 @@
foo = %pwd
-def some_function(foo,bar,):
+
+
+def some_function(
+ foo,
+ bar,
+):
# Another cell with IPython escape command
foo = %pwd
print(foo)
--- resources/test/fixtures/unformatted.py
+++ resources/test/fixtures/unformatted.py
@@ -1,3 +1,3 @@
@@ -469,6 +621,7 @@ fn test_diff() {
+y = 2
z = 3
----- stderr -----
2 files would be reformatted, 1 file left unchanged
"###);
@@ -498,6 +651,7 @@ fn test_diff_no_change() {
+y = 2
z = 3
----- stderr -----
1 file would be reformatted
"###
@@ -531,6 +685,7 @@ fn test_diff_stdin_unformatted() {
+y = 2
z = 3
----- stderr -----
"###);
}

View File

@@ -33,7 +33,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
use ruff_linter::settings::types::{FilePattern, FilePatternSet};
use ruff_python_formatter::{
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
format_module_source, FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions,
};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
@@ -871,9 +871,7 @@ struct BlackOptions {
line_length: NonZeroU16,
#[serde(alias = "skip-magic-trailing-comma")]
skip_magic_trailing_comma: bool,
#[allow(unused)]
#[serde(alias = "force-exclude")]
force_exclude: Option<String>,
preview: bool,
}
impl Default for BlackOptions {
@@ -881,7 +879,7 @@ impl Default for BlackOptions {
Self {
line_length: NonZeroU16::new(88).unwrap(),
skip_magic_trailing_comma: false,
force_exclude: None,
preview: false,
}
}
}
@@ -929,6 +927,11 @@ impl BlackOptions {
} else {
MagicTrailingComma::Respect
})
.with_preview(if self.preview {
PreviewMode::Enabled
} else {
PreviewMode::Disabled
})
}
}

View File

@@ -22,14 +22,16 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
for rule in rules {
let fix_token = match rule.fixable() {
FixAvailability::Always | FixAvailability::Sometimes => {
format!("<span style='opacity: 1'>{FIX_SYMBOL}</span>")
format!("<span title='Automatic fix available'>{FIX_SYMBOL}</span>")
}
FixAvailability::None => {
format!("<span style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
}
FixAvailability::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
};
let preview_token = if rule.is_preview() || rule.is_nursery() {
format!("<span style='opacity: 1'>{PREVIEW_SYMBOL}</span>")
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
} else {
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")
format!("<span style='opacity: 0.1' aria-hidden='true'>{PREVIEW_SYMBOL}</span>")
};
let status_token = format!("{fix_token} {preview_token}");

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.2"
version = "0.1.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -79,3 +79,6 @@ from ZeroDivisionError
raise IndexError() from ZeroDivisionError
raise IndexError();
# RSE102
raise Foo()

View File

@@ -49,6 +49,13 @@ class Apples:
def __doc__(self):
return "Docstring"
# Added in Python 3.12
def __buffer__(self):
return memoryview(b'')
def __release_buffer__(self, buf):
pass
# Allow dunder methods recommended by attrs.
def __attrs_post_init__(self):
pass

View File

@@ -21,6 +21,9 @@ def f() -> None:
# Invalid (but external)
d = 1 # noqa: F841, V101
# Invalid (but external)
d = 1 # noqa: V500
# fmt: off
# Invalid - no space before #
d = 1# noqa: E501

View File

@@ -128,7 +128,10 @@ pub(crate) fn check_noqa(
}
if line.matches.iter().any(|match_| *match_ == code)
|| settings.external.contains(code)
|| settings
.external
.iter()
.any(|external| code.starts_with(external))
{
valid_codes.push(code);
} else {

View File

@@ -27,6 +27,13 @@ use crate::settings::LinterSettings;
/// ```python
/// foo = "bar's"
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter automatically removes unnecessary escapes, making the rule
/// redundant.
///
/// [formatter]: https://docs.astral.sh/ruff/formatter
#[violation]
pub struct AvoidableEscapedQuote;

View File

@@ -32,6 +32,13 @@ use super::super::settings::Quote;
///
/// ## Options
/// - `flake8-quotes.inline-quotes`
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent quotes for inline strings, making the rule
/// redundant.
///
/// [formatter]: https://docs.astral.sh/ruff/formatter
#[violation]
pub struct BadQuotesInlineString {
preferred_quote: Quote,
@@ -81,6 +88,13 @@ impl AlwaysFixableViolation for BadQuotesInlineString {
///
/// ## Options
/// - `flake8-quotes.multiline-quotes`
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces double quotes for multiline strings, making the rule
/// redundant.
///
/// [formatter]: https://docs.astral.sh/ruff/formatter
#[violation]
pub struct BadQuotesMultilineString {
preferred_quote: Quote,
@@ -129,6 +143,13 @@ impl AlwaysFixableViolation for BadQuotesMultilineString {
///
/// ## Options
/// - `flake8-quotes.docstring-quotes`
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces double quotes for docstrings, making the rule
/// redundant.
///
/// [formatter]: https://docs.astral.sh/ruff/formatter
#[violation]
pub struct BadQuotesDocstring {
preferred_quote: Quote,

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::BindingKind;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -15,6 +16,16 @@ use crate::checkers::ast::Checker;
///
/// Removing the parentheses makes the code more concise.
///
/// ## Known problems
/// Parentheses can only be omitted if the exception is a class, as opposed to
/// a function call. This rule isn't always capable of distinguishing between
/// the two.
///
/// For example, if you import a function `module.get_exception` from another
/// module, and `module.get_exception` returns an exception object, this rule will
/// incorrectly mark the parentheses in `raise module.get_exception()` as
/// unnecessary.
///
/// ## Example
/// ```python
/// raise TypeError()
@@ -54,25 +65,32 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
if arguments.is_empty() {
// `raise func()` still requires parentheses; only `raise Class()` does not.
if checker
.semantic()
.lookup_attribute(func)
.is_some_and(|id| checker.semantic().binding(id).kind.is_function_definition())
{
return;
}
let exception_type = if let Some(id) = checker.semantic().lookup_attribute(func) {
match checker.semantic().binding(id).kind {
BindingKind::FunctionDefinition(_) => return,
BindingKind::ClassDefinition(_) => Some(ExceptionType::Class),
BindingKind::Builtin => Some(ExceptionType::Builtin),
_ => None,
}
} else {
None
};
// `ctypes.WinError()` is a function, not a class. It's part of the standard library, so
// we might as well get it right.
if checker
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
if exception_type
.as_ref()
.is_some_and(ExceptionType::is_builtin)
&& checker
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
{
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, arguments.range());
// If the arguments are immediately followed by a `from`, insert whitespace to avoid
// a syntax error, as in:
// ```python
@@ -85,13 +103,25 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
.next()
.is_some_and(char::is_alphanumeric)
{
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
" ".to_string(),
arguments.range(),
)));
diagnostic.set_fix(if exception_type.is_some() {
Fix::safe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
} else {
Fix::unsafe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
});
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(arguments.range())));
diagnostic.set_fix(if exception_type.is_some() {
Fix::safe_edit(Edit::range_deletion(arguments.range()))
} else {
Fix::unsafe_edit(Edit::range_deletion(arguments.range()))
});
}
checker.diagnostics.push(diagnostic);
}
}
#[derive(Debug, is_macro::Is)]
enum ExceptionType {
Class,
Builtin,
}

View File

@@ -238,6 +238,7 @@ RSE102.py:79:17: RSE102 [*] Unnecessary parentheses on raised exception
79 |+raise IndexError from ZeroDivisionError
80 80 |
81 81 | raise IndexError();
82 82 |
RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
|
@@ -245,6 +246,8 @@ RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
80 |
81 | raise IndexError();
| ^^ RSE102
82 |
83 | # RSE102
|
= help: Remove unnecessary parentheses
@@ -254,5 +257,23 @@ RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
80 80 |
81 |-raise IndexError();
81 |+raise IndexError;
82 82 |
83 83 | # RSE102
84 84 | raise Foo()
RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
|
83 | # RSE102
84 | raise Foo()
| ^^ RSE102
|
= help: Remove unnecessary parentheses
Suggested fix
81 81 | raise IndexError();
82 82 |
83 83 | # RSE102
84 |-raise Foo()
84 |+raise Foo

View File

@@ -120,7 +120,7 @@ pub(crate) fn organize_imports(
block,
comments,
locator,
settings.pycodestyle.max_line_length,
settings.line_length,
LineWidthBuilder::new(settings.tab_size).add_str(indentation),
stylist,
&settings.src,

View File

@@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::Tok;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::fix::edits::pad_start;
@@ -25,23 +25,51 @@ use crate::fix::edits::pad_start;
/// regex = r"\.png$"
/// ```
///
/// Or, if the string already contains a valid escape sequence:
/// ```python
/// value = "new line\nand invalid escape \_ here"
/// ```
///
/// Use instead:
/// ```python
/// value = "new line\nand invalid escape \\_ here"
/// ```
///
/// ## References
/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals)
#[violation]
pub struct InvalidEscapeSequence(char);
pub struct InvalidEscapeSequence {
ch: char,
fix_title: FixTitle,
}
impl AlwaysFixableViolation for InvalidEscapeSequence {
#[derive_message_formats]
fn message(&self) -> String {
let InvalidEscapeSequence(char) = self;
format!("Invalid escape sequence: `\\{char}`")
let InvalidEscapeSequence { ch, .. } = self;
format!("Invalid escape sequence: `\\{ch}`")
}
fn fix_title(&self) -> String {
"Add backslash to escape sequence".to_string()
match self.fix_title {
FixTitle::AddBackslash => format!("Add backslash to escape sequence"),
FixTitle::UseRawStringLiteral => format!("Use a raw string literal"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum FixTitle {
AddBackslash,
UseRawStringLiteral,
}
#[derive(Debug)]
struct InvalidEscapeChar {
ch: char,
range: TextRange,
}
/// W605
pub(crate) fn invalid_escape_sequence(
diagnostics: &mut Vec<Diagnostic>,
@@ -67,7 +95,7 @@ pub(crate) fn invalid_escape_sequence(
};
let mut contains_valid_escape_sequence = false;
let mut invalid_escape_sequence = Vec::new();
let mut invalid_escape_chars = Vec::new();
let mut prev = None;
let bytes = token_source_code.as_bytes();
@@ -154,16 +182,28 @@ pub(crate) fn invalid_escape_sequence(
let location = token_range.start() + TextSize::try_from(i).unwrap();
let range = TextRange::at(location, next_char.text_len() + TextSize::from(1));
invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(next_char), range));
invalid_escape_chars.push(InvalidEscapeChar {
ch: next_char,
range,
});
}
let mut invalid_escape_sequence = Vec::new();
if contains_valid_escape_sequence {
// Escape with backslash.
for diagnostic in &mut invalid_escape_sequence {
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
for invalid_escape_char in &invalid_escape_chars {
let diagnostic = Diagnostic::new(
InvalidEscapeSequence {
ch: invalid_escape_char.ch,
fix_title: FixTitle::AddBackslash,
},
invalid_escape_char.range,
)
.with_fix(Fix::safe_edit(Edit::insertion(
r"\".to_string(),
diagnostic.start() + TextSize::from(1),
invalid_escape_char.range.start() + TextSize::from(1),
)));
invalid_escape_sequence.push(diagnostic);
}
} else {
let tok_start = if token.is_f_string_middle() {
@@ -178,14 +218,24 @@ pub(crate) fn invalid_escape_sequence(
token_range.start()
};
// Turn into raw string.
for diagnostic in &mut invalid_escape_sequence {
// If necessary, add a space between any leading keyword (`return`, `yield`,
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
// `returnr"foo"` is not.
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
pad_start("r".to_string(), tok_start, locator),
tok_start,
)));
for invalid_escape_char in &invalid_escape_chars {
let diagnostic = Diagnostic::new(
InvalidEscapeSequence {
ch: invalid_escape_char.ch,
fix_title: FixTitle::UseRawStringLiteral,
},
invalid_escape_char.range,
)
.with_fix(
// If necessary, add a space between any leading keyword (`return`, `yield`,
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
// `returnr"foo"` is not.
Fix::safe_edit(Edit::insertion(
pad_start("r".to_string(), tok_start, locator),
tok_start,
)),
);
invalid_escape_sequence.push(diagnostic);
}
}

View File

@@ -23,7 +23,16 @@ use super::LogicalLine;
/// a = 1
/// ```
///
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.
///
/// The rule is also incompatible with the [formatter] when using
/// `indent-width` with a value other than `4`.
///
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
/// [formatter]:https://docs.astral.sh/ruff/formatter/
#[violation]
pub struct IndentationWithInvalidMultiple {
indent_size: usize,
@@ -55,7 +64,15 @@ impl Violation for IndentationWithInvalidMultiple {
/// # a = 1
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.
///
/// The rule is also incompatible with the [formatter] when using
/// `indent-width` with a value other than `4`.
///
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
/// [formatter]:https://docs.astral.sh/ruff/formatter/
#[violation]
pub struct IndentationWithInvalidMultipleComment {
indent_size: usize,

View File

@@ -26,7 +26,15 @@ use ruff_text_size::{TextLen, TextRange, TextSize};
/// a = 1
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.
///
/// The rule is also incompatible with the [formatter] when using
/// `format.indent-style="tab"`.
///
/// [PEP 8]: https://peps.python.org/pep-0008/#tabs-or-spaces
/// [formatter]: https://docs.astral.sh/ruff/formatter
#[violation]
pub struct TabIndentation;

View File

@@ -9,7 +9,7 @@ W605_0.py:2:10: W605 [*] Invalid escape sequence: `\.`
3 |
4 | #: W605:2:1
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
1 1 | #: W605:1:10
@@ -27,7 +27,7 @@ W605_0.py:6:1: W605 [*] Invalid escape sequence: `\.`
| ^^ W605
7 | '''
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
2 2 | regex = '\.png$'
@@ -47,7 +47,7 @@ W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_`
| ^^ W605
12 | )
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
8 8 |
@@ -68,7 +68,7 @@ W605_0.py:18:6: W605 [*] Invalid escape sequence: `\_`
19 | in the middle
20 | """
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
12 12 | )
@@ -107,7 +107,7 @@ W605_0.py:28:12: W605 [*] Invalid escape sequence: `\.`
29 |
30 | #: Okay
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
25 25 |

View File

@@ -9,7 +9,7 @@ W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.`
3 |
4 | #: W605:2:1
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
1 1 | #: W605:1:10
@@ -27,7 +27,7 @@ W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.`
| ^^ W605
7 | '''
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
2 2 | regex = '\.png$'
@@ -47,7 +47,7 @@ W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_`
| ^^ W605
12 | )
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
8 8 |
@@ -68,7 +68,7 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_`
19 | in the middle
20 | """
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
12 12 | )
@@ -89,7 +89,7 @@ W605_1.py:25:12: W605 [*] Invalid escape sequence: `\.`
26 |
27 | #: Okay
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
22 22 |

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/src/rules/pycodestyle/mod.rs
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
@@ -9,7 +9,7 @@ W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.`
5 |
6 | #: W605:2:1
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
1 1 | # Same as `W605_0.py` but using f-strings instead.
@@ -29,7 +29,7 @@ W605_2.py:8:1: W605 [*] Invalid escape sequence: `\.`
| ^^ W605
9 | '''
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
4 4 | regex = f'\.png$'
@@ -49,7 +49,7 @@ W605_2.py:13:7: W605 [*] Invalid escape sequence: `\_`
| ^^ W605
14 | )
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
10 10 |
@@ -70,7 +70,7 @@ W605_2.py:20:6: W605 [*] Invalid escape sequence: `\_`
21 | in the middle
22 | """
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
14 14 | )
@@ -129,7 +129,7 @@ W605_2.py:44:11: W605 [*] Invalid escape sequence: `\{`
45 | value = f'\{1}'
46 | value = f'{1:\}'
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
41 41 | ''' # noqa
@@ -150,7 +150,7 @@ W605_2.py:45:11: W605 [*] Invalid escape sequence: `\{`
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
42 42 |
@@ -171,7 +171,7 @@ W605_2.py:46:14: W605 [*] Invalid escape sequence: `\}`
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
43 43 | regex = f'\\\_'
@@ -191,7 +191,7 @@ W605_2.py:47:14: W605 [*] Invalid escape sequence: `\{`
| ^^ W605
48 | value = rf"{f"\{1}"}"
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
44 44 | value = f'\{{1}}'
@@ -212,7 +212,7 @@ W605_2.py:48:15: W605 [*] Invalid escape sequence: `\{`
49 |
50 | # Okay
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Fix
45 45 | value = f'\{1}'

View File

@@ -14,9 +14,7 @@ use crate::registry::Rule;
/// Checks for docstrings that are indented with tabs.
///
/// ## Why is this bad?
/// [PEP 8](https://peps.python.org/pep-0008/#tabs-or-spaces) recommends using
/// spaces over tabs for indentation.
///
/// [PEP 8] recommends using spaces over tabs for indentation.
///
/// ## Example
/// ```python
@@ -38,10 +36,20 @@ use crate::registry::Rule;
/// """
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.
///
/// The rule is also incompatible with the [formatter] when using
/// `format.indent-style="tab"`.
///
/// ## References
/// - [PEP 257 Docstring Conventions](https://peps.python.org/pep-0257/)
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
///
/// [PEP 8]: https://peps.python.org/pep-0008/#tabs-or-spaces
/// [formatter]: https://docs.astral.sh/ruff/formatter
#[violation]
pub struct IndentWithSpaces;
@@ -126,12 +134,17 @@ impl AlwaysFixableViolation for UnderIndentation {
/// """
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.
///
/// ## References
/// - [PEP 257 Docstring Conventions](https://peps.python.org/pep-0257/)
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
///
/// [PEP 257]: https://peps.python.org/pep-0257/
/// [formatter]:https://docs.astral.sh/ruff/formatter/
#[violation]
pub struct OverIndentation;

View File

@@ -88,6 +88,7 @@ fn is_known_dunder_method(method: &str) -> bool {
| "__attrs_pre_init__"
| "__await__"
| "__bool__"
| "__buffer__"
| "__bytes__"
| "__call__"
| "__ceil__"
@@ -166,6 +167,7 @@ fn is_known_dunder_method(method: &str) -> bool {
| "__rdivmod__"
| "__reduce__"
| "__reduce_ex__"
| "__release_buffer__"
| "__repr__"
| "__reversed__"
| "__rfloordiv__"

View File

@@ -106,7 +106,26 @@ mod tests {
let diagnostics = test_path(
Path::new("ruff/RUF100_0.py"),
&settings::LinterSettings {
external: FxHashSet::from_iter(vec!["V101".to_string()]),
external: vec!["V101".to_string()],
..settings::LinterSettings::for_rules(vec![
Rule::UnusedNOQA,
Rule::LineTooLong,
Rule::UnusedImport,
Rule::UnusedVariable,
Rule::TabIndentation,
])
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn ruf100_0_prefix() -> Result<()> {
let diagnostics = test_path(
Path::new("ruff/RUF100_0.py"),
&settings::LinterSettings {
external: vec!["V".to_string()],
..settings::LinterSettings::for_rules(vec![
Rule::UnusedNOQA,
Rule::LineTooLong,

View File

@@ -35,8 +35,11 @@ pub struct UnusedCodes {
/// foo.bar()
/// ```
///
/// ## Options
/// - `external`
///
/// ## References
/// - [Automatic `noqa` management](https://docs.astral.sh/ruff/configuration/#automatic-noqa-management)
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[violation]
pub struct UnusedNOQA {
pub codes: Option<UnusedCodes>,

View File

@@ -86,7 +86,7 @@ RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`)
22 | d = 1 # noqa: F841, V101
| ^^^^^^^^^^^^^^^^^^ RUF100
23 |
24 | # fmt: off
24 | # Invalid (but external)
|
= help: Remove unused `noqa` directive
@@ -97,171 +97,191 @@ RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`)
22 |- d = 1 # noqa: F841, V101
22 |+ d = 1 # noqa: V101
23 23 |
24 24 | # fmt: off
25 25 | # Invalid - no space before #
24 24 | # Invalid (but external)
25 25 | d = 1 # noqa: V500
RUF100_0.py:26:10: RUF100 [*] Unused `noqa` directive (unused: `E501`)
RUF100_0.py:25:12: RUF100 [*] Unused `noqa` directive (unknown: `V500`)
|
24 | # fmt: off
25 | # Invalid - no space before #
26 | d = 1# noqa: E501
| ^^^^^^^^^^^^ RUF100
27 |
28 | # Invalid - many spaces before #
24 | # Invalid (but external)
25 | d = 1 # noqa: V500
| ^^^^^^^^^^^^ RUF100
26 |
27 | # fmt: off
|
= help: Remove unused `noqa` directive
Fix
22 22 | d = 1 # noqa: F841, V101
23 23 |
24 24 | # fmt: off
25 25 | # Invalid - no space before #
26 |- d = 1# noqa: E501
26 |+ d = 1
27 27 |
28 28 | # Invalid - many spaces before #
29 29 | d = 1 # noqa: E501
24 24 | # Invalid (but external)
25 |- d = 1 # noqa: V500
25 |+ d = 1
26 26 |
27 27 | # fmt: off
28 28 | # Invalid - no space before #
RUF100_0.py:29:5: F841 [*] Local variable `d` is assigned to but never used
RUF100_0.py:29:10: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
28 | # Invalid - many spaces before #
29 | d = 1 # noqa: E501
27 | # fmt: off
28 | # Invalid - no space before #
29 | d = 1# noqa: E501
| ^^^^^^^^^^^^ RUF100
30 |
31 | # Invalid - many spaces before #
|
= help: Remove unused `noqa` directive
Fix
26 26 |
27 27 | # fmt: off
28 28 | # Invalid - no space before #
29 |- d = 1# noqa: E501
29 |+ d = 1
30 30 |
31 31 | # Invalid - many spaces before #
32 32 | d = 1 # noqa: E501
RUF100_0.py:32:5: F841 [*] Local variable `d` is assigned to but never used
|
31 | # Invalid - many spaces before #
32 | d = 1 # noqa: E501
| ^ F841
30 | # fmt: on
33 | # fmt: on
|
= help: Remove assignment to unused variable `d`
Suggested fix
26 26 | d = 1# noqa: E501
27 27 |
28 28 | # Invalid - many spaces before #
29 |- d = 1 # noqa: E501
30 29 | # fmt: on
31 30 |
32 31 |
29 29 | d = 1# noqa: E501
30 30 |
31 31 | # Invalid - many spaces before #
32 |- d = 1 # noqa: E501
33 32 | # fmt: on
34 33 |
35 34 |
RUF100_0.py:29:33: RUF100 [*] Unused `noqa` directive (unused: `E501`)
RUF100_0.py:32:33: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
28 | # Invalid - many spaces before #
29 | d = 1 # noqa: E501
31 | # Invalid - many spaces before #
32 | d = 1 # noqa: E501
| ^^^^^^^^^^^^ RUF100
30 | # fmt: on
33 | # fmt: on
|
= help: Remove unused `noqa` directive
Fix
26 26 | d = 1# noqa: E501
27 27 |
28 28 | # Invalid - many spaces before #
29 |- d = 1 # noqa: E501
29 |+ d = 1
30 30 | # fmt: on
31 31 |
32 32 |
29 29 | d = 1# noqa: E501
30 30 |
31 31 | # Invalid - many spaces before #
32 |- d = 1 # noqa: E501
32 |+ d = 1
33 33 | # fmt: on
34 34 |
35 35 |
RUF100_0.py:55:6: RUF100 [*] Unused `noqa` directive (unused: `F841`)
RUF100_0.py:58:6: RUF100 [*] Unused `noqa` directive (unused: `F841`)
|
54 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
55 | """ # noqa: E501, F841
57 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
58 | """ # noqa: E501, F841
| ^^^^^^^^^^^^^^^^^^ RUF100
56 |
57 | # Invalid
59 |
60 | # Invalid
|
= help: Remove unused `noqa` directive
Fix
52 52 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
53 53 |
54 54 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
55 |-""" # noqa: E501, F841
55 |+""" # noqa: E501
55 55 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
56 56 |
57 57 | # Invalid
58 58 | _ = """Lorem ipsum dolor sit amet.
57 57 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
58 |-""" # noqa: E501, F841
58 |+""" # noqa: E501
59 59 |
60 60 | # Invalid
61 61 | _ = """Lorem ipsum dolor sit amet.
RUF100_0.py:63:6: RUF100 [*] Unused `noqa` directive (unused: `E501`)
RUF100_0.py:66:6: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
62 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
63 | """ # noqa: E501
65 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
66 | """ # noqa: E501
| ^^^^^^^^^^^^ RUF100
64 |
65 | # Invalid
67 |
68 | # Invalid
|
= help: Remove unused `noqa` directive
Fix
60 60 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
61 61 |
62 62 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
63 |-""" # noqa: E501
63 |+"""
63 63 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
64 64 |
65 65 | # Invalid
66 66 | _ = """Lorem ipsum dolor sit amet.
65 65 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
66 |-""" # noqa: E501
66 |+"""
67 67 |
68 68 | # Invalid
69 69 | _ = """Lorem ipsum dolor sit amet.
RUF100_0.py:71:6: RUF100 [*] Unused blanket `noqa` directive
RUF100_0.py:74:6: RUF100 [*] Unused blanket `noqa` directive
|
70 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
71 | """ # noqa
73 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
74 | """ # noqa
| ^^^^^^ RUF100
72 |
73 | # Valid
75 |
76 | # Valid
|
= help: Remove unused `noqa` directive
Fix
68 68 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
69 69 |
70 70 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
71 |-""" # noqa
71 |+"""
71 71 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
72 72 |
73 73 | # Valid
74 74 | # this is a veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy long comment # noqa: E501
73 73 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
74 |-""" # noqa
74 |+"""
75 75 |
76 76 | # Valid
77 77 | # this is a veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy long comment # noqa: E501
RUF100_0.py:85:8: F401 [*] `shelve` imported but unused
RUF100_0.py:88:8: F401 [*] `shelve` imported but unused
|
83 | import collections # noqa
84 | import os # noqa: F401, RUF100
85 | import shelve # noqa: RUF100
86 | import collections # noqa
87 | import os # noqa: F401, RUF100
88 | import shelve # noqa: RUF100
| ^^^^^^ F401
86 | import sys # noqa: F401, RUF100
89 | import sys # noqa: F401, RUF100
|
= help: Remove unused import: `shelve`
Fix
82 82 |
83 83 | import collections # noqa
84 84 | import os # noqa: F401, RUF100
85 |-import shelve # noqa: RUF100
86 85 | import sys # noqa: F401, RUF100
87 86 |
88 87 | print(sys.path)
85 85 |
86 86 | import collections # noqa
87 87 | import os # noqa: F401, RUF100
88 |-import shelve # noqa: RUF100
89 88 | import sys # noqa: F401, RUF100
90 89 |
91 90 | print(sys.path)
RUF100_0.py:90:89: E501 Line too long (89 > 88)
RUF100_0.py:93:89: E501 Line too long (89 > 88)
|
88 | print(sys.path)
89 |
90 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
91 | print(sys.path)
92 |
93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
| ^ E501
|
RUF100_0.py:90:92: RUF100 [*] Unused `noqa` directive (unused: `F401`)
RUF100_0.py:93:92: RUF100 [*] Unused `noqa` directive (unused: `F401`)
|
88 | print(sys.path)
89 |
90 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
91 | print(sys.path)
92 |
93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
| ^^^^^^^^^^^^ RUF100
|
= help: Remove unused `noqa` directive
Fix
87 87 |
88 88 | print(sys.path)
89 89 |
90 |-"shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
90 |+"shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]"
91 91 |
90 90 |
91 91 | print(sys.path)
92 92 |
93 93 | def f():
93 |-"shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
93 |+"shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]"
94 94 |
95 95 |
96 96 | def f():

View File

@@ -0,0 +1,267 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF100_0.py:9:12: RUF100 [*] Unused blanket `noqa` directive
|
8 | # Invalid
9 | c = 1 # noqa
| ^^^^^^ RUF100
10 | print(c)
|
= help: Remove unused `noqa` directive
Fix
6 6 | b = 2 # noqa: F841
7 7 |
8 8 | # Invalid
9 |- c = 1 # noqa
9 |+ c = 1
10 10 | print(c)
11 11 |
12 12 | # Invalid
RUF100_0.py:13:12: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
12 | # Invalid
13 | d = 1 # noqa: E501
| ^^^^^^^^^^^^ RUF100
14 |
15 | # Invalid
|
= help: Remove unused `noqa` directive
Fix
10 10 | print(c)
11 11 |
12 12 | # Invalid
13 |- d = 1 # noqa: E501
13 |+ d = 1
14 14 |
15 15 | # Invalid
16 16 | d = 1 # noqa: F841, E501
RUF100_0.py:16:12: RUF100 [*] Unused `noqa` directive (unused: `F841`, `E501`)
|
15 | # Invalid
16 | d = 1 # noqa: F841, E501
| ^^^^^^^^^^^^^^^^^^ RUF100
17 |
18 | # Invalid (and unimplemented or not enabled)
|
= help: Remove unused `noqa` directive
Fix
13 13 | d = 1 # noqa: E501
14 14 |
15 15 | # Invalid
16 |- d = 1 # noqa: F841, E501
16 |+ d = 1
17 17 |
18 18 | # Invalid (and unimplemented or not enabled)
19 19 | d = 1 # noqa: F841, W191, F821
RUF100_0.py:19:12: RUF100 [*] Unused `noqa` directive (unused: `F841`, `W191`; non-enabled: `F821`)
|
18 | # Invalid (and unimplemented or not enabled)
19 | d = 1 # noqa: F841, W191, F821
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
20 |
21 | # Invalid (but external)
|
= help: Remove unused `noqa` directive
Fix
16 16 | d = 1 # noqa: F841, E501
17 17 |
18 18 | # Invalid (and unimplemented or not enabled)
19 |- d = 1 # noqa: F841, W191, F821
19 |+ d = 1
20 20 |
21 21 | # Invalid (but external)
22 22 | d = 1 # noqa: F841, V101
RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`)
|
21 | # Invalid (but external)
22 | d = 1 # noqa: F841, V101
| ^^^^^^^^^^^^^^^^^^ RUF100
23 |
24 | # Invalid (but external)
|
= help: Remove unused `noqa` directive
Fix
19 19 | d = 1 # noqa: F841, W191, F821
20 20 |
21 21 | # Invalid (but external)
22 |- d = 1 # noqa: F841, V101
22 |+ d = 1 # noqa: V101
23 23 |
24 24 | # Invalid (but external)
25 25 | d = 1 # noqa: V500
RUF100_0.py:29:10: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
27 | # fmt: off
28 | # Invalid - no space before #
29 | d = 1# noqa: E501
| ^^^^^^^^^^^^ RUF100
30 |
31 | # Invalid - many spaces before #
|
= help: Remove unused `noqa` directive
Fix
26 26 |
27 27 | # fmt: off
28 28 | # Invalid - no space before #
29 |- d = 1# noqa: E501
29 |+ d = 1
30 30 |
31 31 | # Invalid - many spaces before #
32 32 | d = 1 # noqa: E501
RUF100_0.py:32:5: F841 [*] Local variable `d` is assigned to but never used
|
31 | # Invalid - many spaces before #
32 | d = 1 # noqa: E501
| ^ F841
33 | # fmt: on
|
= help: Remove assignment to unused variable `d`
Suggested fix
29 29 | d = 1# noqa: E501
30 30 |
31 31 | # Invalid - many spaces before #
32 |- d = 1 # noqa: E501
33 32 | # fmt: on
34 33 |
35 34 |
RUF100_0.py:32:33: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
31 | # Invalid - many spaces before #
32 | d = 1 # noqa: E501
| ^^^^^^^^^^^^ RUF100
33 | # fmt: on
|
= help: Remove unused `noqa` directive
Fix
29 29 | d = 1# noqa: E501
30 30 |
31 31 | # Invalid - many spaces before #
32 |- d = 1 # noqa: E501
32 |+ d = 1
33 33 | # fmt: on
34 34 |
35 35 |
RUF100_0.py:58:6: RUF100 [*] Unused `noqa` directive (unused: `F841`)
|
57 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
58 | """ # noqa: E501, F841
| ^^^^^^^^^^^^^^^^^^ RUF100
59 |
60 | # Invalid
|
= help: Remove unused `noqa` directive
Fix
55 55 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
56 56 |
57 57 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
58 |-""" # noqa: E501, F841
58 |+""" # noqa: E501
59 59 |
60 60 | # Invalid
61 61 | _ = """Lorem ipsum dolor sit amet.
RUF100_0.py:66:6: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
65 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
66 | """ # noqa: E501
| ^^^^^^^^^^^^ RUF100
67 |
68 | # Invalid
|
= help: Remove unused `noqa` directive
Fix
63 63 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
64 64 |
65 65 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
66 |-""" # noqa: E501
66 |+"""
67 67 |
68 68 | # Invalid
69 69 | _ = """Lorem ipsum dolor sit amet.
RUF100_0.py:74:6: RUF100 [*] Unused blanket `noqa` directive
|
73 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
74 | """ # noqa
| ^^^^^^ RUF100
75 |
76 | # Valid
|
= help: Remove unused `noqa` directive
Fix
71 71 | https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
72 72 |
73 73 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
74 |-""" # noqa
74 |+"""
75 75 |
76 76 | # Valid
77 77 | # this is a veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy long comment # noqa: E501
RUF100_0.py:88:8: F401 [*] `shelve` imported but unused
|
86 | import collections # noqa
87 | import os # noqa: F401, RUF100
88 | import shelve # noqa: RUF100
| ^^^^^^ F401
89 | import sys # noqa: F401, RUF100
|
= help: Remove unused import: `shelve`
Fix
85 85 |
86 86 | import collections # noqa
87 87 | import os # noqa: F401, RUF100
88 |-import shelve # noqa: RUF100
89 88 | import sys # noqa: F401, RUF100
90 89 |
91 90 | print(sys.path)
RUF100_0.py:93:89: E501 Line too long (89 > 88)
|
91 | print(sys.path)
92 |
93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
| ^ E501
|
RUF100_0.py:93:92: RUF100 [*] Unused `noqa` directive (unused: `F401`)
|
91 | print(sys.path)
92 |
93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
| ^^^^^^^^^^^^ RUF100
|
= help: Remove unused `noqa` directive
Fix
90 90 |
91 91 | print(sys.path)
92 92 |
93 |-"shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401
93 |+"shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]"
94 94 |
95 95 |
96 96 | def f():

View File

@@ -2,7 +2,6 @@
//! command-line options. Structure is optimized for internal usage, as opposed
//! to external visibility or parsing.
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use anyhow::Result;
@@ -15,6 +14,7 @@ use rustc_hash::FxHashSet;
use crate::codes::RuleCodePrefix;
use ruff_macros::CacheKey;
use crate::line_width::LineLength;
use crate::registry::{Linter, Rule, RuleSet};
use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
@@ -54,12 +54,13 @@ pub struct LinterSettings {
pub allowed_confusables: FxHashSet<char>,
pub builtins: Vec<String>,
pub dummy_variable_rgx: Regex,
pub external: FxHashSet<String>,
pub external: Vec<String>,
pub ignore_init_module_imports: bool,
pub logger_objects: Vec<String>,
pub namespace_packages: Vec<PathBuf>,
pub src: Vec<PathBuf>,
pub tab_size: IndentWidth,
pub line_length: LineLength,
pub task_tags: Vec<String>,
pub typing_modules: Vec<String>,
@@ -144,7 +145,7 @@ impl LinterSettings {
builtins: vec![],
dummy_variable_rgx: DUMMY_VARIABLE_RGX.clone(),
external: HashSet::default(),
external: vec![],
ignore_init_module_imports: false,
logger_objects: vec![],
namespace_packages: vec![],
@@ -156,6 +157,7 @@ impl LinterSettings {
src: vec![path_dedot::CWD.clone()],
// Needs duplicating
tab_size: IndentWidth::default(),
line_length: LineLength::default(),
task_tags: TASK_TAGS.iter().map(ToString::to_string).collect(),
typing_modules: vec![],

View File

@@ -2,7 +2,7 @@ use std::io;
use std::io::Write;
use std::path::Path;
use anyhow::{bail, Result};
use anyhow::Result;
use similar::TextDiff;
use thiserror::Error;
@@ -88,7 +88,12 @@ impl SourceKind {
}
/// Write a diff of the transformed source file to `stdout`.
pub fn diff(&self, other: &Self, path: Option<&Path>, writer: &mut dyn Write) -> Result<()> {
pub fn diff(
&self,
other: &Self,
path: Option<&Path>,
writer: &mut dyn Write,
) -> io::Result<()> {
match (self, other) {
(SourceKind::Python(src), SourceKind::Python(dst)) => {
let text_diff = TextDiff::from_lines(src, dst);
@@ -154,7 +159,7 @@ impl SourceKind {
Ok(())
}
_ => bail!("cannot diff Python source code with Jupyter notebook source code"),
_ => panic!("cannot diff Python source code with Jupyter notebook source code"),
}
}
}

View File

@@ -537,28 +537,6 @@ def update_emission_strength():
value = self.emission_strength * 2
```
#### Type annotations may be parenthesized when expanded ([#7315](https://github.com/astral-sh/ruff/issues/7315))
Black will avoid parenthesizing type annotations in an annotated assignment, while Ruff will insert
parentheses in some cases.
For example:
```python
# Black
StartElementHandler: Callable[[str, dict[str, str]], Any] | Callable[[str, list[str]], Any] | Callable[
[str, dict[str, str], list[str]], Any
] | None
# Ruff
StartElementHandler: (
Callable[[str, dict[str, str]], Any]
| Callable[[str, list[str]], Any]
| Callable[[str, dict[str, str], list[str]], Any]
| None
)
```
#### Call chain calls break differently ([#7051](https://github.com/astral-sh/ruff/issues/7051))
Black occasionally breaks call chains differently than Ruff; in particular, Black occasionally

View File

@@ -0,0 +1,10 @@
# No spacing
5 ** 5
5.0 ** 5.0
1e5 ** 2e5
True ** True
False ** False
None ** None
# Space
"a" ** "b"

View File

@@ -180,3 +180,16 @@ if "root" not in (
):
msg = "Could not find root. Please try a different forest."
raise ValueError(msg)
# Regression for https://github.com/astral-sh/ruff/issues/8183
def foo():
while (
not (aaaaaaaaaaaaaaaaaaaaa(bbbbbbbb, ccccccc)) and dddddddddd < eeeeeeeeeeeeeee
):
pass
def foo():
while (
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass

View File

@@ -0,0 +1,8 @@
# Regression test for https://github.com/astral-sh/ruff/issues/8211
# fmt: off
from dataclasses import dataclass
if True:
if False:
x: int # Optional[int]

View File

@@ -0,0 +1,8 @@
# Regression test for https://github.com/astral-sh/ruff/issues/8211
# fmt: off
from dataclasses import dataclass
@dataclass
class A:
x: int # Optional[int]

View File

@@ -1,7 +1,39 @@
# Below is black stable style
# In preview style, black always breaks the right side first
"""
Black's `Preview.module_docstring_newlines`
"""
first_stmt_after_module_level_docstring = 1
if True:
class CachedRepository:
# Black's `Preview.dummy_implementations`
def get_release_info(self): ...
def raw_docstring():
r"""Black's `Preview.accept_raw_docstrings`
a
b
"""
pass
def reference_docstring_newlines():
"""A regular docstring for comparison
a
b
"""
pass
class RemoveNewlineBeforeClassDocstring:
"""Black's `Preview.no_blank_line_before_class_docstring`"""
def f():
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
] = cccccccc.ccccccccccccc.cccccccc

View File

@@ -1,6 +1,21 @@
# Regression test: Don't forget the parentheses in the value when breaking
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
)
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)= Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
JSONSerializable: TypeAlias = (
"str | int | float | bool | None | list | tuple | JSONMapping"
)
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = {1, 2, 3, 4}
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = aaaaaaaaaaaaaaaa
# Regression test: Don't forget the parentheses in the annotation when breaking
class DefaultRunner:

View File

@@ -1,3 +1,5 @@
# comment
class Test(
Aaaaaaaaaaaaaaaaa,
Bbbbbbbbbbbbbbbb,

View File

@@ -6,12 +6,13 @@ use anyhow::{format_err, Context, Result};
use clap::{command, Parser, ValueEnum};
use ruff_formatter::SourceCode;
use ruff_python_ast::PySourceType;
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::{parse_ok_tokens, Mode};
use ruff_python_parser::{parse_ok_tokens, AsMode};
use ruff_text_size::Ranged;
use crate::comments::collect_comments;
use crate::{format_module_ast, PyFormatOptions};
use crate::{format_module_ast, PreviewMode, PyFormatOptions};
#[derive(ValueEnum, Clone, Debug)]
pub enum Emit {
@@ -23,6 +24,7 @@ pub enum Emit {
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)] // It's only the dev cli anyways
pub struct Cli {
/// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported
pub files: Vec<PathBuf>,
@@ -33,20 +35,27 @@ pub struct Cli {
#[clap(long)]
pub check: bool,
#[clap(long)]
pub preview: bool,
#[clap(long)]
pub print_ir: bool,
#[clap(long)]
pub print_comments: bool,
}
pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Result<String> {
let (tokens, comment_ranges) = tokens_and_ranges(source)
pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Result<String> {
let source_type = PySourceType::from(source_path);
let (tokens, comment_ranges) = tokens_and_ranges(source, source_type)
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
// Parse the AST.
let module = parse_ok_tokens(tokens, source, Mode::Module, "<filename>")
let module = parse_ok_tokens(tokens, source, source_type.as_mode(), "<filename>")
.context("Syntax error in input")?;
let options = PyFormatOptions::from_extension(source_type);
let options = PyFormatOptions::from_extension(source_path).with_preview(if cli.preview {
PreviewMode::Enabled
} else {
PreviewMode::Disabled
});
let source_code = SourceCode::new(source);
let formatted = format_module_ast(&module, &comment_ranges, source, options)

View File

@@ -507,13 +507,12 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
///
/// For example, given:
/// ```python
/// def func():
/// class Class:
/// ...
/// # comment
/// ```
///
/// This builder will insert two empty lines before the comment.
/// ```
pub(crate) fn empty_lines_before_trailing_comments<'a>(
f: &PyFormatter,
comments: &'a [SourceComment],
@@ -555,3 +554,69 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_>
Ok(())
}
}
/// Format the empty lines between a node and its leading comments.
///
/// For example, given:
/// ```python
/// # comment
///
/// class Class:
/// ...
/// ```
///
/// While `leading_comments` will preserve the existing empty line, this builder will insert an
/// additional empty line before the comment.
pub(crate) fn empty_lines_after_leading_comments<'a>(
f: &PyFormatter,
comments: &'a [SourceComment],
) -> FormatEmptyLinesAfterLeadingComments<'a> {
// Black has different rules for stub vs. non-stub and top level vs. indented
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
(PySourceType::Stub, NodeLevel::TopLevel) => 1,
(PySourceType::Stub, _) => 0,
(_, NodeLevel::TopLevel) => 2,
(_, _) => 1,
};
FormatEmptyLinesAfterLeadingComments {
comments,
empty_lines,
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct FormatEmptyLinesAfterLeadingComments<'a> {
/// The leading comments of the node.
comments: &'a [SourceComment],
/// The expected number of empty lines after the leading comments.
empty_lines: u32,
}
impl Format<PyFormatContext<'_>> for FormatEmptyLinesAfterLeadingComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
if let Some(comment) = self
.comments
.iter()
.rev()
.find(|comment| comment.line_position().is_own_line())
{
let actual = lines_after(comment.end(), f.context().source()).saturating_sub(1);
// If there are no empty lines, keep the comment tight to the node.
if actual == 0 {
return Ok(());
}
// If there are more than enough empty lines already, `leading_comments` will
// trim them as necessary.
if actual >= self.empty_lines {
return Ok(());
}
for _ in actual..self.empty_lines {
write!(f, [empty_line()])?;
}
}
Ok(())
}
}

View File

@@ -548,10 +548,10 @@ mod tests {
use insta::assert_debug_snapshot;
use ruff_formatter::SourceCode;
use ruff_python_ast::Mod;
use ruff_python_ast::{Mod, PySourceType};
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::{parse_ok_tokens, Mode};
use ruff_python_parser::{parse_ok_tokens, AsMode};
use ruff_python_trivia::CommentRanges;
use crate::comments::Comments;
@@ -565,9 +565,10 @@ mod tests {
impl<'a> CommentsTestCase<'a> {
fn from_code(source: &'a str) -> Self {
let source_code = SourceCode::new(source);
let source_type = PySourceType::Python;
let (tokens, comment_ranges) =
tokens_and_ranges(source).expect("Expect source to be valid Python");
let parsed = parse_ok_tokens(tokens, source, Mode::Module, "test.py")
tokens_and_ranges(source, source_type).expect("Expect source to be valid Python");
let parsed = parse_ok_tokens(tokens, source, source_type.as_mode(), "test.py")
.expect("Expect source to be valid Python");
CommentsTestCase {

View File

@@ -506,7 +506,12 @@ const fn is_simple_power_operand(expr: &Expr) -> bool {
op: UnaryOp::Not, ..
}) => false,
Expr::Constant(ExprConstant {
value: Constant::Complex { .. } | Constant::Float(_) | Constant::Int(_),
value:
Constant::Complex { .. }
| Constant::Float(_)
| Constant::Int(_)
| Constant::None
| Constant::Bool(_),
..
}) => true,
Expr::Name(_) => true,

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::ExprIpyEscapeCommand;
use ruff_text_size::Ranged;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
#[derive(Default)]
@@ -11,3 +12,13 @@ impl FormatNodeRule<ExprIpyEscapeCommand> for FormatExprIpyEscapeCommand {
source_text_slice(item.range()).fmt(f)
}
}
impl NeedsParentheses for ExprIpyEscapeCommand {
fn needs_parentheses(
&self,
_parent: ruff_python_ast::AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View File

@@ -476,7 +476,7 @@ impl NeedsParentheses for Expr {
Expr::List(expr) => expr.needs_parentheses(parent, context),
Expr::Tuple(expr) => expr.needs_parentheses(parent, context),
Expr::Slice(expr) => expr.needs_parentheses(parent, context),
Expr::IpyEscapeCommand(_) => todo!(),
Expr::IpyEscapeCommand(expr) => expr.needs_parentheses(parent, context),
}
}
}
@@ -526,16 +526,20 @@ fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool
&& has_parentheses(expr, context).is_some_and(OwnParentheses::is_non_empty)
}
// Only use the layout if the first or last expression has parentheses of some sort, and
// Only use the layout if the first expression starts with parentheses
// or the last expression ends with parentheses of some sort, and
// those parentheses are non-empty.
let first_parenthesized = visitor
.first
.is_some_and(|first| is_parenthesized(first, context));
let last_parenthesized = visitor
if visitor
.last
.is_some_and(|last| is_parenthesized(last, context));
first_parenthesized || last_parenthesized
.is_some_and(|last| is_parenthesized(last, context))
{
true
} else {
visitor
.first
.expression()
.is_some_and(|first| is_parenthesized(first, context))
}
}
}
@@ -545,7 +549,7 @@ struct CanOmitOptionalParenthesesVisitor<'input> {
max_precedence_count: u32,
any_parenthesized_expressions: bool,
last: Option<&'input Expr>,
first: Option<&'input Expr>,
first: First<'input>,
context: &'input PyFormatContext<'input>,
}
@@ -557,7 +561,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
max_precedence_count: 0,
any_parenthesized_expressions: false,
last: None,
first: None,
first: First::None,
}
}
@@ -670,6 +674,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
if op.is_invert() {
self.update_max_precedence(OperatorPrecedence::BitwiseInversion);
}
self.first.set_if_none(First::Token);
}
// `[a, b].test.test[300].dot`
@@ -706,20 +711,25 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
self.update_max_precedence(OperatorPrecedence::String);
}
Expr::Tuple(_)
| Expr::NamedExpr(_)
| Expr::GeneratorExp(_)
| Expr::Lambda(_)
// Expressions with sub expressions but a preceding token
// Mark this expression as first expression and not the sub expression.
Expr::Lambda(_)
| Expr::Await(_)
| Expr::Yield(_)
| Expr::YieldFrom(_)
| Expr::Starred(_) => {
self.first.set_if_none(First::Token);
}
Expr::Tuple(_)
| Expr::NamedExpr(_)
| Expr::GeneratorExp(_)
| Expr::FormattedValue(_)
| Expr::FString(_)
| Expr::Constant(_)
| Expr::Starred(_)
| Expr::Name(_)
| Expr::Slice(_) => {}
Expr::IpyEscapeCommand(_) => todo!(),
| Expr::Slice(_)
| Expr::IpyEscapeCommand(_) => {}
};
walk_expr(self, expr);
@@ -741,8 +751,32 @@ impl<'input> PreorderVisitor<'input> for CanOmitOptionalParenthesesVisitor<'inpu
self.visit_subexpression(expr);
}
if self.first.is_none() {
self.first = Some(expr);
self.first.set_if_none(First::Expression(expr));
}
}
#[derive(Copy, Clone, Debug)]
enum First<'a> {
None,
/// Expression starts with a non-parentheses token. E.g. `not a`
Token,
Expression(&'a Expr),
}
impl<'a> First<'a> {
#[inline]
fn set_if_none(&mut self, first: First<'a>) {
if matches!(self, First::None) {
*self = first;
}
}
fn expression(self) -> Option<&'a Expr> {
match self {
First::None | First::Token => None,
First::Expression(expr) => Some(expr),
}
}
}

View File

@@ -7,7 +7,7 @@ use ruff_python_ast::AstNode;
use ruff_python_ast::Mod;
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::lexer::LexicalError;
use ruff_python_parser::{parse_ok_tokens, Mode, ParseError};
use ruff_python_parser::{parse_ok_tokens, AsMode, ParseError};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::Locator;
@@ -130,8 +130,9 @@ pub fn format_module_source(
source: &str,
options: PyFormatOptions,
) -> Result<Printed, FormatModuleError> {
let (tokens, comment_ranges) = tokens_and_ranges(source)?;
let module = parse_ok_tokens(tokens, source, Mode::Module, "<filename>")?;
let source_type = options.source_type();
let (tokens, comment_ranges) = tokens_and_ranges(source, source_type)?;
let module = parse_ok_tokens(tokens, source, source_type.as_mode(), "<filename>")?;
let formatted = format_module_ast(&module, &comment_ranges, source, options)?;
Ok(formatted.print()?)
}
@@ -172,9 +173,10 @@ mod tests {
use anyhow::Result;
use insta::assert_snapshot;
use ruff_python_ast::PySourceType;
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::{parse_ok_tokens, Mode};
use ruff_python_parser::{parse_ok_tokens, AsMode};
use crate::{format_module_ast, format_module_source, PyFormatOptions};
@@ -213,11 +215,12 @@ def main() -> None:
]
"#;
let (tokens, comment_ranges) = tokens_and_ranges(source).unwrap();
let source_type = PySourceType::Python;
let (tokens, comment_ranges) = tokens_and_ranges(source, source_type).unwrap();
// Parse the AST.
let source_path = "code_inline.py";
let module = parse_ok_tokens(tokens, source, Mode::Module, source_path).unwrap();
let module = parse_ok_tokens(tokens, source, source_type.as_mode(), source_path).unwrap();
let options = PyFormatOptions::from_extension(Path::new(source_path));
let formatted = format_module_ast(&module, &comment_ranges, source, options).unwrap();

View File

@@ -250,6 +250,10 @@ impl MagicTrailingComma {
pub const fn is_respect(self) -> bool {
matches!(self, Self::Respect)
}
pub const fn is_ignore(self) -> bool {
matches!(self, Self::Ignore)
}
}
impl FromStr for MagicTrailingComma {

View File

@@ -391,7 +391,9 @@ pub(crate) fn clause_body<'a>(
impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
if f.options().source_type().is_stub()
// In stable, stubs are only collapsed in stub files, in preview this is consistently
// applied everywhere
if (f.options().source_type().is_stub() || f.options().preview().is_enabled())
&& contains_only_an_ellipsis(self.body, f.context().comments())
&& self.trailing_comments.is_empty()
{

View File

@@ -21,12 +21,7 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
write!(
f,
[
target.format(),
token(":"),
space(),
maybe_parenthesize_expression(annotation, item, Parenthesize::IfBreaks)
]
[target.format(), token(":"), space(), annotation.format(),]
)?;
if let Some(value) = value {

View File

@@ -3,7 +3,9 @@ use ruff_python_ast::{Decorator, StmtClassDef};
use ruff_python_trivia::lines_after_ignoring_end_of_line_trivia;
use ruff_text_size::Ranged;
use crate::comments::format::empty_lines_before_trailing_comments;
use crate::comments::format::{
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
};
use crate::comments::{leading_comments, trailing_comments, SourceComment};
use crate::prelude::*;
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
@@ -32,6 +34,29 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start);
// If the class contains leading comments, insert newlines before them.
// For example, given:
// ```python
// # comment
//
// class Class:
// ...
// ```
//
// At the top-level in a non-stub file, reformat as:
// ```python
// # comment
//
//
// class Class:
// ...
// ```
// Note that this is only really relevant for the specific case in which there's a single
// newline between the comment and the node, but we _require_ two newlines. If there are
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
// are more than two, then `leading_comments` will preserve the correct number of newlines.
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;
write!(
f,
[

View File

@@ -1,7 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::StmtFunctionDef;
use crate::comments::format::empty_lines_before_trailing_comments;
use crate::comments::format::{
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
};
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::{Parentheses, Parenthesize};
@@ -30,6 +32,29 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start);
// If the class contains leading comments, insert newlines before them.
// For example, given:
// ```python
// # comment
//
// def func():
// ...
// ```
//
// At the top-level in a non-stub file, reformat as:
// ```python
// # comment
//
//
// def func():
// ...
// ```
// Note that this is only really relevant for the specific case in which there's a single
// newline between the comment and the node, but we _require_ two newlines. If there are
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
// are more than two, then `leading_comments` will preserve the correct number of newlines.
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;
write!(
f,
[

View File

@@ -536,7 +536,7 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Suite {
}
/// A statement representing a docstring.
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub(crate) struct DocstringStmt<'a>(&'a Stmt);
impl<'a> DocstringStmt<'a> {
@@ -589,7 +589,7 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
}
/// A Child of a suite.
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub(crate) enum SuiteChildStatement<'a> {
/// A docstring documenting a class or function definition.
Docstring(DocstringStmt<'a>),

View File

@@ -327,7 +327,7 @@ fn write_suppressed_statements<'a>(
for range in CommentRangeIter::in_suppression(comments.trailing(statement), source) {
match range {
// All leading comments are suppressed
// All trailing comments are suppressed
// ```python
// statement
// # suppressed
@@ -394,10 +394,14 @@ fn write_suppressed_statements<'a>(
statement = SuiteChildStatement::Other(next_statement);
leading_node_comments = comments.leading(next_statement);
} else {
let end = comments
.trailing(statement)
.last()
.map_or(statement.end(), Ranged::end);
let mut nodes =
std::iter::successors(Some(AnyNodeRef::from(statement.statement())), |statement| {
statement.last_child_in_body()
});
let end = nodes
.find_map(|statement| comments.trailing(statement).last().map(Ranged::end))
.unwrap_or(statement.end());
FormatVerbatimStatementRange {
verbatim_range: TextRange::new(format_off_comment.end(), end),

View File

@@ -1,5 +1,5 @@
use ruff_formatter::FormatOptions;
use ruff_python_formatter::{format_module_source, PyFormatOptions};
use ruff_python_formatter::{format_module_source, PreviewMode, PyFormatOptions};
use similar::TextDiff;
use std::fmt::{Formatter, Write};
use std::io::BufReader;
@@ -142,16 +142,40 @@ fn format() {
} else {
let printed =
format_module_source(&content, options.clone()).expect("Formatting to succeed");
let formatted_code = printed.as_code();
let formatted = printed.as_code();
ensure_stability_when_formatting_twice(formatted_code, options, input_path);
ensure_stability_when_formatting_twice(formatted, options.clone(), input_path);
writeln!(
snapshot,
"## Output\n{}",
CodeFrame::new("py", &formatted_code)
)
.unwrap();
// We want to capture the differences in the preview style in our fixtures
let options_preview = options.with_preview(PreviewMode::Enabled);
let printed_preview = format_module_source(&content, options_preview.clone())
.expect("Formatting to succeed");
let formatted_preview = printed_preview.as_code();
ensure_stability_when_formatting_twice(
formatted_preview,
options_preview.clone(),
input_path,
);
if formatted == formatted_preview {
writeln!(snapshot, "## Output\n{}", CodeFrame::new("py", &formatted)).unwrap();
} else {
// Having both snapshots makes it hard to see the difference, so we're keeping only
// diff.
writeln!(
snapshot,
"## Output\n{}\n## Preview changes\n{}",
CodeFrame::new("py", &formatted),
CodeFrame::new(
"diff",
TextDiff::from_lines(formatted, formatted_preview)
.unified_diff()
.header("Stable", "Preview")
)
)
.unwrap();
}
}
insta::with_settings!({

View File

@@ -162,7 +162,7 @@ def f():
```diff
--- Black
+++ Ruff
@@ -1,29 +1,205 @@
@@ -1,29 +1,206 @@
+# This file doesn't use the standard decomposition.
+# Decorator syntax test cases are separated by double # comments.
+# Those before the 'output' comment are valid under the old syntax.
@@ -172,6 +172,7 @@ def f():
+
+##
+
+
+@decorator
+def f():
+ ...
@@ -209,43 +210,48 @@ def f():
+ ...
+
+
+##
+
##
-@decorator()()
+
+@decorator(**kwargs)
+def f():
+ ...
+
+
+##
def f():
...
+
##
-@(decorator)
+
+@decorator(*args, **kwargs)
+def f():
+ ...
+
+
+##
def f():
...
+
##
-@sequence["decorator"]
+
+@decorator(
+ *args,
+ **kwargs,
+)
+def f():
+ ...
+
+
+##
def f():
...
+
##
-@decorator[List[str]]
+
+@dotted.decorator
+def f():
+ ...
+
+
+##
def f():
...
+
##
-@var := decorator
+
+@dotted.decorator(arg)
+def f():
@@ -260,48 +266,43 @@ def f():
+ ...
+
+
##
-@decorator()()
+##
+
+
+@dotted.decorator(*args)
def f():
...
+def f():
+ ...
+
+
+##
+
##
-@(decorator)
+
+@dotted.decorator(**kwargs)
def f():
...
+def f():
+ ...
+
+
+##
+
##
-@sequence["decorator"]
+
+@dotted.decorator(*args, **kwargs)
def f():
...
+def f():
+ ...
+
+
+##
+
##
-@decorator[List[str]]
+
+@dotted.decorator(
+ *args,
+ **kwargs,
+)
def f():
...
+def f():
+ ...
+
+
+##
+
##
-@var := decorator
+
+@double.dotted.decorator
+def f():
@@ -387,6 +388,7 @@ def f():
##
@decorator
def f():
...

View File

@@ -1,86 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py
---
## Input
```py
#!/usr/bin/env python3.9
@relaxed_decorator[0]
def f():
...
@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length]
def f():
...
@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line")
def f():
...
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,6 +1,5 @@
#!/usr/bin/env python3.9
-
@relaxed_decorator[0]
def f():
...
```
## Ruff Output
```py
#!/usr/bin/env python3.9
@relaxed_decorator[0]
def f():
...
@relaxed_decorator[
extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length
]
def f():
...
@extremely_long_variable_name_that_doesnt_fit := complex.expression(
with_long="arguments_value_that_wont_fit_at_the_end_of_the_line"
)
def f():
...
```
## Black Output
```py
#!/usr/bin/env python3.9
@relaxed_decorator[0]
def f():
...
@relaxed_decorator[
extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length
]
def f():
...
@extremely_long_variable_name_that_doesnt_fit := complex.expression(
with_long="arguments_value_that_wont_fit_at_the_end_of_the_line"
)
def f():
...
```

View File

@@ -0,0 +1,34 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_pow_spacing.py
---
## Input
```py
# No spacing
5 ** 5
5.0 ** 5.0
1e5 ** 2e5
True ** True
False ** False
None ** None
# Space
"a" ** "b"
```
## Output
```py
# No spacing
5**5
5.0**5.0
1e5**2e5
True**True
False**False
None**None
# Space
"a" ** "b"
```

View File

@@ -186,6 +186,19 @@ if "root" not in (
):
msg = "Could not find root. Please try a different forest."
raise ValueError(msg)
# Regression for https://github.com/astral-sh/ruff/issues/8183
def foo():
while (
not (aaaaaaaaaaaaaaaaaaaaa(bbbbbbbb, ccccccc)) and dddddddddd < eeeeeeeeeeeeeee
):
pass
def foo():
while (
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
```
## Output
@@ -292,10 +305,13 @@ if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & (
):
pass
if not (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
if (
not (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)
& aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
):
pass
@@ -383,6 +399,21 @@ if "root" not in (
):
msg = "Could not find root. Please try a different forest."
raise ValueError(msg)
# Regression for https://github.com/astral-sh/ruff/issues/8183
def foo():
while (
not (aaaaaaaaaaaaaaaaaaaaa(bbbbbbbb, ccccccc)) and dddddddddd < eeeeeeeeeeeeeee
):
pass
def foo():
while (
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
```

View File

@@ -0,0 +1,30 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_unclosed_deep_nested_trailing_comment.py
---
## Input
```py
# Regression test for https://github.com/astral-sh/ruff/issues/8211
# fmt: off
from dataclasses import dataclass
if True:
if False:
x: int # Optional[int]
```
## Output
```py
# Regression test for https://github.com/astral-sh/ruff/issues/8211
# fmt: off
from dataclasses import dataclass
if True:
if False:
x: int # Optional[int]
```

View File

@@ -0,0 +1,30 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_unclosed_trailing_comment.py
---
## Input
```py
# Regression test for https://github.com/astral-sh/ruff/issues/8211
# fmt: off
from dataclasses import dataclass
@dataclass
class A:
x: int # Optional[int]
```
## Output
```py
# Regression test for https://github.com/astral-sh/ruff/issues/8211
# fmt: off
from dataclasses import dataclass
@dataclass
class A:
x: int # Optional[int]
```

View File

@@ -20,6 +20,7 @@ def test():
# fmt: on
def test():
pass
```

View File

@@ -93,4 +93,21 @@ def test3 ():
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -21,8 +21,7 @@
# formatted
-def test2():
- ...
+def test2(): ...
a = 10
```

View File

@@ -549,4 +549,27 @@ if True:
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -245,13 +245,11 @@
class Path:
if sys.version_info >= (3, 11):
- def joinpath(self):
- ...
+ def joinpath(self): ...
# The .open method comes from pathlib.pyi and should be kept in sync.
@overload
- def open(self):
- ...
+ def open(self): ...
def fakehttp():
```

View File

@@ -1,13 +1,45 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.py
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py
---
## Input
```py
# Below is black stable style
# In preview style, black always breaks the right side first
"""
Black's `Preview.module_docstring_newlines`
"""
first_stmt_after_module_level_docstring = 1
if True:
class CachedRepository:
# Black's `Preview.dummy_implementations`
def get_release_info(self): ...
def raw_docstring():
r"""Black's `Preview.accept_raw_docstrings`
a
b
"""
pass
def reference_docstring_newlines():
"""A regular docstring for comparison
a
b
"""
pass
class RemoveNewlineBeforeClassDocstring:
"""Black's `Preview.no_blank_line_before_class_docstring`"""
def f():
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
] = cccccccc.ccccccccccccc.cccccccc
@@ -52,10 +84,41 @@ preview = Disabled
```
```py
# Below is black stable style
# In preview style, black always breaks the right side first
"""
Black's `Preview.module_docstring_newlines`
"""
first_stmt_after_module_level_docstring = 1
if True:
class CachedRepository:
# Black's `Preview.dummy_implementations`
def get_release_info(self):
...
def raw_docstring():
r"""Black's `Preview.accept_raw_docstrings`
a
b
"""
pass
def reference_docstring_newlines():
"""A regular docstring for comparison
a
b
"""
pass
class RemoveNewlineBeforeClassDocstring:
"""Black's `Preview.no_blank_line_before_class_docstring`"""
def f():
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
] = cccccccc.ccccccccccccc.cccccccc
@@ -100,10 +163,40 @@ preview = Enabled
```
```py
# Below is black stable style
# In preview style, black always breaks the right side first
"""
Black's `Preview.module_docstring_newlines`
"""
first_stmt_after_module_level_docstring = 1
if True:
class CachedRepository:
# Black's `Preview.dummy_implementations`
def get_release_info(self): ...
def raw_docstring():
r"""Black's `Preview.accept_raw_docstrings`
a
b
"""
pass
def reference_docstring_newlines():
"""A regular docstring for comparison
a
b
"""
pass
class RemoveNewlineBeforeClassDocstring:
"""Black's `Preview.no_blank_line_before_class_docstring`"""
def f():
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
] = cccccccc.ccccccccccccc.cccccccc

View File

@@ -7,6 +7,21 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/
# Regression test: Don't forget the parentheses in the value when breaking
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
)
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)= Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
JSONSerializable: TypeAlias = (
"str | int | float | bool | None | list | tuple | JSONMapping"
)
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = {1, 2, 3, 4}
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = aaaaaaaaaaaaaaaa
# Regression test: Don't forget the parentheses in the annotation when breaking
class DefaultRunner:
@@ -20,12 +35,35 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int =
a + 1 * a
)
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
)
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) = Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
JSONSerializable: TypeAlias = (
"str | int | float | bool | None | list | tuple | JSONMapping"
)
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = {
1,
2,
3,
4,
}
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = (
aaaaaaaaaaaaaaaa
)
# Regression test: Don't forget the parentheses in the annotation when breaking
class DefaultRunner:
task_runner_cls: (
TaskRunnerProtocol | typing.Callable[[], typing.Any]
) = DefaultTaskRunner
task_runner_cls: TaskRunnerProtocol | typing.Callable[
[], typing.Any
] = DefaultTaskRunner
```

View File

@@ -4,6 +4,8 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/
---
## Input
```py
# comment
class Test(
Aaaaaaaaaaaaaaaaa,
Bbbbbbbbbbbbbbbb,
@@ -232,6 +234,9 @@ class QuerySet(AltersData):
## Output
```py
# comment
class Test(
Aaaaaaaaaaaaaaaaa,
Bbbbbbbbbbbbbbbb,
@@ -494,4 +499,45 @@ class QuerySet(AltersData):
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -28,8 +28,7 @@
pass
-class Test((Aaaa)):
- ...
+class Test((Aaaa)): ...
class Test(
@@ -159,20 +158,17 @@
@dataclass
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
-class AltCLIPOutput(ModelOutput):
- ...
+class AltCLIPOutput(ModelOutput): ...
@dataclass
-class AltCLIPOutput: # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
- ...
+class AltCLIPOutput: ... # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
@dataclass
class AltCLIPOutput(
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
-):
- ...
+): ...
class TestTypeParams[
```

View File

@@ -996,4 +996,167 @@ def default_arg_comments2( #
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -2,8 +2,7 @@
def test(
# comment
# another
-):
- ...
+): ...
# Argument empty line spacing
@@ -12,8 +11,7 @@
a,
# another
b,
-):
- ...
+): ...
### Different function argument wrappings
@@ -57,8 +55,7 @@
b,
# comment
*args,
-):
- ...
+): ...
def kwarg_with_leading_comments(
@@ -66,8 +63,7 @@
b,
# comment
**kwargs,
-):
- ...
+): ...
def argument_with_long_default(
@@ -75,8 +71,7 @@
b=ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
+ [dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff],
h=[],
-):
- ...
+): ...
def argument_with_long_type_annotation(
@@ -85,12 +80,10 @@
| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
| zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3],
h=[],
-):
- ...
+): ...
-def test():
- ...
+def test(): ...
# Type parameter empty line spacing
@@ -99,8 +92,7 @@
A,
# another
B,
-]():
- ...
+](): ...
# Type parameter comments
@@ -159,8 +151,7 @@
# Comment
-def with_leading_comment():
- ...
+def with_leading_comment(): ...
# Comment that could be mistaken for a trailing comment of the function declaration when
@@ -192,8 +183,7 @@
# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989
def foo(
b=3 + 2, # comment
-):
- ...
+): ...
# Comments on the slash or the star, both of which don't have a node
@@ -454,8 +444,7 @@
def f(
# first
# second
-):
- ...
+): ...
def f( # first
@@ -475,8 +464,7 @@
# first
b,
# second
-):
- ...
+): ...
def f( # first
@@ -484,8 +472,7 @@
# second
b,
# third
-):
- ...
+): ...
def f( # first
@@ -494,8 +481,7 @@
# third
b,
# fourth
-):
- ...
+): ...
def f( # first
@@ -522,17 +508,14 @@
a,
# third
/, # second
-):
- ...
+): ...
# Walrus operator in return type.
-def this_is_unusual() -> (please := no):
- ...
+def this_is_unusual() -> (please := no): ...
-def this_is_unusual(x) -> (please := no):
- ...
+def this_is_unusual(x) -> (please := no): ...
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
```

View File

@@ -544,4 +544,298 @@ def process_board_action(
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -7,8 +7,7 @@
start: int | None = None,
num: int | None = None,
) -> ( # type: ignore[override]
-):
- ...
+): ...
def zrevrangebylex(
@@ -20,8 +19,7 @@
num: int | None = None,
) -> ( # type: ignore[override]
# comment
-):
- ...
+): ...
def zrevrangebylex(
@@ -33,8 +31,7 @@
num: int | None = None,
) -> ( # type: ignore[override]
1
-):
- ...
+): ...
def zrevrangebylex(
@@ -47,8 +44,7 @@
) -> ( # type: ignore[override]
1,
2,
-):
- ...
+): ...
def zrevrangebylex(
@@ -60,14 +56,12 @@
num: int | None = None,
) -> ( # type: ignore[override]
(1, 2)
-):
- ...
+): ...
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
self, m: Match[str], data: str
-) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
- ...
+) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: ...
def double(
@@ -95,50 +89,44 @@
# function arguments break here with a single argument; we do not.)
def f(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
- ...
+) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
def f(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, a
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
- ...
+) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
def f(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-) -> a:
- ...
+) -> a: ...
def f(
a
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
- ...
+) -> (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+): ...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-):
- ...
+): ...
def f[
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-]() -> a:
- ...
+]() -> a: ...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
- ...
+) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-) -> a:
- ...
+) -> a: ...
# Breaking return type annotations. Black adds parentheses if the parameters are
@@ -147,137 +135,126 @@
Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
]
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
]
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
]
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
]
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
x
) -> Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-]:
- ...
+]: ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
x
) -> Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-]:
- ...
+]: ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
*args
) -> Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-]:
- ...
+]: ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx( # foo
) -> Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-]:
- ...
+]: ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
# bar
) -> Set[
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-]:
- ...
+]: ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
x
-) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
- ...
+) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
x
-) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
- ...
+) -> (
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+): ...
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> X + Y + foooooooooooooooooooooooooooooooooooo():
- ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+ X + Y + foooooooooooooooooooooooooooooooooooo()
+): ...
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(x) -> X + Y + foooooooooooooooooooooooooooooooooooo():
- ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+ x
+) -> X + Y + foooooooooooooooooooooooooooooooooooo(): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
X and Y and foooooooooooooooooooooooooooooooooooo()
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
x
-) -> X and Y and foooooooooooooooooooooooooooooooooooo():
- ...
+) -> X and Y and foooooooooooooooooooooooooooooooooooo(): ...
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> X | Y | foooooooooooooooooooooooooooooooooooo():
- ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+ X | Y | foooooooooooooooooooooooooooooooooooo()
+): ...
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(x) -> X | Y | foooooooooooooooooooooooooooooooooooo():
- ...
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+ x
+) -> X | Y | foooooooooooooooooooooooooooooooooooo(): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
X | Y | foooooooooooooooooooooooooooooooooooo() # comment
-):
- ...
+): ...
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
x
) -> (
X | Y | foooooooooooooooooooooooooooooooooooo() # comment
-):
- ...
+): ...
def double() -> (
```

View File

@@ -115,4 +115,57 @@ def quuz():
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -12,25 +12,20 @@
pass
-class Del(expr_context):
- ...
+class Del(expr_context): ...
-class Load(expr_context):
- ...
+class Load(expr_context): ...
# Some comment.
-class Other(expr_context):
- ...
+class Other(expr_context): ...
-class Store(expr_context):
- ...
+class Store(expr_context): ...
-class Foo(Bar):
- ...
+class Foo(Bar): ...
class Baz(Qux):
@@ -49,12 +44,10 @@
pass
-def bar():
- ...
+def bar(): ...
-def baz():
- ...
+def baz(): ...
def quux():
```

View File

@@ -1,7 +1,8 @@
use std::fmt::Debug;
use ruff_python_ast::PySourceType;
use ruff_python_parser::lexer::{lex, LexicalError};
use ruff_python_parser::{Mode, Tok};
use ruff_python_parser::{AsMode, Tok};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::TextRange;
@@ -25,11 +26,12 @@ impl CommentRangesBuilder {
/// Helper method to lex and extract comment ranges
pub fn tokens_and_ranges(
source: &str,
source_type: PySourceType,
) -> Result<(Vec<(Tok, TextRange)>, CommentRanges), LexicalError> {
let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default();
for result in lex(source, Mode::Module) {
for result in lex(source, source_type.as_mode()) {
let (token, range) = result?;
comment_ranges.visit_token(&token, range);

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_shrinking"
version = "0.1.2"
version = "0.1.3"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -226,8 +226,9 @@ impl Configuration {
dummy_variable_rgx: lint
.dummy_variable_rgx
.unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()),
external: FxHashSet::from_iter(lint.external.unwrap_or_default()),
external: lint.external.unwrap_or_default(),
ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or_default(),
line_length,
tab_size: self.indent_width.unwrap_or_default(),
namespace_packages: self.namespace_packages.unwrap_or_default(),
per_file_ignores: resolve_per_file_ignores(

View File

@@ -352,7 +352,7 @@ pub struct Options {
// Global Formatting options
/// The line length to use when enforcing long-lines violations (like `E501`)
/// and at which the formatter prefers to wrap lines.
/// and at which `isort` and the formatter prefers to wrap lines.
///
/// The length is determined by the number of characters per line, except for lines containing East Asian characters or emojis.
/// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
@@ -548,7 +548,7 @@ pub struct LintCommonOptions {
)]
pub extend_unfixable: Option<Vec<RuleSelector>>,
/// A list of rule codes that are unsupported by Ruff, but should be
/// A list of rule codes or prefixes that are unsupported by Ruff, but should be
/// preserved when (e.g.) validating `# noqa` directives. Useful for
/// retaining `# noqa` directives that cover plugins not yet implemented
/// by Ruff.
@@ -556,9 +556,9 @@ pub struct LintCommonOptions {
default = "[]",
value_type = "list[str]",
example = r#"
# Avoiding flagging (and removing) `V101` from any `# noqa`
# directives, despite Ruff's lack of support for `vulture`.
external = ["V101"]
# Avoiding flagging (and removing) any codes starting with `V` from any
# `# noqa` directives, despite Ruff's lack of support for `vulture`.
external = ["V"]
"#
)]
pub external: Option<Vec<String>>,
@@ -1412,6 +1412,9 @@ impl Flake8PytestStyleOptions {
pub struct Flake8QuotesOptions {
/// Quote style to prefer for inline strings (either "single" or
/// "double").
///
/// When using the formatter, ensure that `format.quote-style` is set to
/// the same preferred quote style.
#[option(
default = r#""double""#,
value_type = r#""single" | "double""#,
@@ -1423,6 +1426,9 @@ pub struct Flake8QuotesOptions {
/// Quote style to prefer for multiline strings (either "single" or
/// "double").
///
/// When using the formatter, only "double" is compatible, as the formatter
/// enforces double quotes for multiline strings.
#[option(
default = r#""double""#,
value_type = r#""single" | "double""#,
@@ -1433,6 +1439,9 @@ pub struct Flake8QuotesOptions {
pub multiline_quotes: Option<Quote>,
/// Quote style to prefer for docstrings (either "single" or "double").
///
/// When using the formatter, only "double" is compatible, as the formatter
/// enforces double quotes for docstrings strings.
#[option(
default = r#""double""#,
value_type = r#""single" | "double""#,
@@ -1683,6 +1692,9 @@ pub struct IsortOptions {
/// `combine-as-imports = true`. When `combine-as-imports` isn't
/// enabled, every aliased `import from` will be given its own line, in
/// which case, wrapping is not necessary.
///
/// When using the formatter, ensure that `format.skip-magic-trailing-comma` is set to `false` (default)
/// when enabling `force-wrap-aliases` to avoid that the formatter collapses members if they all fit on a single line.
#[option(
default = r#"false"#,
value_type = "bool",
@@ -1726,6 +1738,9 @@ pub struct IsortOptions {
/// the imports will never be folded into one line.
///
/// See isort's [`split-on-trailing-comma`](https://pycqa.github.io/isort/docs/configuration/options.html#split-on-trailing-comma) option.
///
/// When using the formatter, ensure that `format.skip-magic-trailing-comma` is set to `false` (default) when enabling `split-on-trailing-comma`
/// to avoid that the formatter removes the trailing commas.
#[option(
default = r#"true"#,
value_type = "bool",
@@ -1907,6 +1922,9 @@ pub struct IsortOptions {
/// The number of blank lines to place after imports.
/// Use `-1` for automatic determination.
///
/// When using the formatter, only the values `-1`, `1`, and `2` are compatible because
/// it enforces at least one empty and at most two empty lines after imports.
#[option(
default = r#"-1"#,
value_type = "int",
@@ -1918,6 +1936,9 @@ pub struct IsortOptions {
pub lines_after_imports: Option<isize>,
/// The number of lines to place between "direct" and `import from` imports.
///
/// When using the formatter, only the values `0` and `1` are compatible because
/// it preserves up to one empty line after imports in nested blocks.
#[option(
default = r#"0"#,
value_type = "int",
@@ -2301,7 +2322,7 @@ pub struct PycodestyleOptions {
/// documentation (`W505`), including standalone comments. By default,
/// this is set to null which disables reporting violations.
///
/// The length is determined by the number of characters per line, except for lines containinAsian characters or emojis.
/// The length is determined by the number of characters per line, except for lines containing Asian characters or emojis.
/// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
///
/// See the [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) rule for more information.

View File

@@ -9,6 +9,7 @@ use std::sync::RwLock;
use anyhow::Result;
use anyhow::{anyhow, bail};
use globset::{Candidate, GlobSet};
use ignore::{WalkBuilder, WalkState};
use itertools::Itertools;
use log::debug;
@@ -333,12 +334,18 @@ pub fn python_files_in_path(
let resolver = resolver.read().unwrap();
let settings = resolver.resolve(path, pyproject_config);
if let Some(file_name) = path.file_name() {
if match_exclusion(path, file_name, &settings.file_resolver.exclude) {
let file_path = Candidate::new(path);
let file_basename = Candidate::new(file_name);
if match_candidate_exclusion(
&file_path,
&file_basename,
&settings.file_resolver.exclude,
) {
debug!("Ignored path via `exclude`: {:?}", path);
return WalkState::Skip;
} else if match_exclusion(
path,
file_name,
} else if match_candidate_exclusion(
&file_path,
&file_basename,
&settings.file_resolver.extend_exclude,
) {
debug!("Ignored path via `extend-exclude`: {:?}", path);
@@ -509,10 +516,20 @@ fn is_file_excluded(
for path in path.ancestors() {
let settings = resolver.resolve(path, pyproject_strategy);
if let Some(file_name) = path.file_name() {
if match_exclusion(path, file_name, &settings.file_resolver.exclude) {
let file_path = Candidate::new(path);
let file_basename = Candidate::new(file_name);
if match_candidate_exclusion(
&file_path,
&file_basename,
&settings.file_resolver.exclude,
) {
debug!("Ignored path via `exclude`: {:?}", path);
return true;
} else if match_exclusion(path, file_name, &settings.file_resolver.extend_exclude) {
} else if match_candidate_exclusion(
&file_path,
&file_basename,
&settings.file_resolver.extend_exclude,
) {
debug!("Ignored path via `extend-exclude`: {:?}", path);
return true;
}
@@ -533,11 +550,27 @@ fn is_file_excluded(
pub fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
file_path: P,
file_basename: R,
exclusion: &globset::GlobSet,
exclusion: &GlobSet,
) -> bool {
if exclusion.is_empty() {
return false;
}
exclusion.is_match(file_path) || exclusion.is_match(file_basename)
}
/// Return `true` if the given candidates should be ignored based on the exclusion
/// criteria.
pub fn match_candidate_exclusion(
file_path: &Candidate,
file_basename: &Candidate,
exclusion: &GlobSet,
) -> bool {
if exclusion.is_empty() {
return false;
}
exclusion.is_match_candidate(file_path) || exclusion.is_match_candidate(file_basename)
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir, File};

View File

@@ -67,7 +67,7 @@ quote-style = "double"
indent-style = "space"
# Like Black, respect magic trailing commas.
magic-trailing-comma = "respect"
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
@@ -78,7 +78,7 @@ As an example, the following would configure Ruff to:
```toml
[tool.ruff.lint]
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
select = ["E", "F", "B"]
select = ["E4", "E7", "E9", "F", "B"]
# 2. Avoid enforcing line-length violations (`E501`)
ignore = ["E501"]
@@ -101,7 +101,7 @@ Linter plugin configurations are expressed as subsections, e.g.:
```toml
[tool.ruff.lint]
# Add "Q" to the list of enabled codes.
select = ["E", "F", "Q"]
select = ["E4", "E7", "E9", "F", "Q"]
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
@@ -121,7 +121,7 @@ For example, the `pyproject.toml` described above would be represented via the f
```toml
[lint]
# Enable flake8-bugbear (`B`) rules.
select = ["E", "F", "B"]
select = ["E4", "E7", "E9", "F", "B"]
# Never enforce `E501` (line length violations).
ignore = ["E501"]

View File

@@ -164,10 +164,10 @@ elif False: # fmt: skip
pass
@Test
@Test2 # fmt: off
@Test2 # fmt: skip
def test(): ...
a = [1, 2, 3, 4, 5] # fmt: off
a = [1, 2, 3, 4, 5] # fmt: skip
def test(a, b, c, d, e, f) -> int: # fmt: skip
pass
@@ -180,9 +180,10 @@ comments, which are treated equivalently to `# fmt: off` and `# fmt: on`, respec
Ruff's formatter is designed to be used alongside the linter. However, the linter includes
some rules that, when enabled, can cause conflicts with the formatter, leading to unexpected
behavior.
behavior. When configured appropriately, the goal of Ruff's formatter-linter compatibility is
such that running the formatter should never introduce new lint errors.
When using Ruff as a formatter, we recommend disabling the following rules:
As such, when using Ruff as a formatter, we recommend avoiding the following lint rules:
- [`tab-indentation`](rules/tab-indentation.md) (`W191`)
- [`indentation-with-invalid-multiple`](rules/indentation-with-invalid-multiple.md) (`E111`)
@@ -199,7 +200,11 @@ When using Ruff as a formatter, we recommend disabling the following rules:
- [`single-line-implicit-string-concatenation`](rules/single-line-implicit-string-concatenation.md) (`ISC001`)
- [`multi-line-implicit-string-concatenation`](rules/multi-line-implicit-string-concatenation.md) (`ISC002`)
Similarly, we recommend disabling the following isort settings, which are incompatible with the
None of the above are included in Ruff's default configuration. However, if you've enabled
any of these rules or their parent categories (like `Q`), we recommend disabling them via the
linter's [`ignore`](settings.md#ignore) setting.
Similarly, we recommend avoiding the following isort settings, which are incompatible with the
formatter's treatment of import statements when set to non-default values:
- [`force-single-line`](settings.md#isort-force-single-line)
@@ -208,6 +213,12 @@ formatter's treatment of import statements when set to non-default values:
- [`lines-between-types`](settings.md#isort-lines-between-types)
- [`split-on-trailing-comma`](settings.md#isort-split-on-trailing-comma)
If you've configured any of these settings to take on non-default values, we recommend removing
them from your Ruff configuration.
When an incompatible lint rule or setting is enabled, `ruff format` will emit a warning. If your
`ruff format` is free of warnings, you're good to go!
## Exit codes
`ruff format` exits with the following status codes:

View File

@@ -399,28 +399,6 @@ def update_emission_strength():
value = self.emission_strength * 2
```
### Type annotations may be parenthesized when expanded
Black will avoid parenthesizing type annotations in an annotated assignment, while Ruff will insert
parentheses in some cases.
For example:
```python
# Black
StartElementHandler: Callable[[str, dict[str, str]], Any] | Callable[[str, list[str]], Any] | Callable[
[str, dict[str, str], list[str]], Any
] | None
# Ruff
StartElementHandler: (
Callable[[str, dict[str, str]], Any]
| Callable[[str, list[str]], Any]
| Callable[[str, dict[str, str], list[str]], Any]
| None
)
```
### Call chain calls break differently
Black occasionally breaks call chains differently than Ruff; in particular, Black occasionally

View File

@@ -55,6 +55,10 @@ or isort, _unless_ you enable autofix, in which case, Ruff's pre-commit hook sho
Black, isort, and other formatting tools, as Ruff's autofix behavior can output code changes that
require reformatting.
As long as your Ruff configuration avoids any [linter-formatter incompatibilities](formatter.md#conflicting-lint-rules),
`ruff format` should never introduce new lint errors, so it's safe to run Ruff's format hook _after_
`ruff check --fix`.
## Language Server Protocol (Official)
Ruff supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/)

View File

@@ -1,4 +1,4 @@
PyYAML==6.0
black==23.3.0
black==23.10.0
mkdocs==1.5.0
git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@38c0b8187325c3bab386b666daf3518ac036f2f4

View File

@@ -1,4 +1,4 @@
PyYAML==6.0
black==23.3.0
black==23.10.0
mkdocs==1.5.0
mkdocs-material==9.1.18

View File

@@ -110,7 +110,7 @@ To configure Ruff, let's create a `pyproject.toml` file in our project's root di
# Set the maximum line length to 79.
line-length = 79
[tool.ruff.linter]
[tool.ruff.lint]
# Add the `line-too-long` rule to the enforced rule set. By default, Ruff omits rules that
# overlap with the use of a formatter, like Black, but we can override this behavior by
# explicitly adding the rule.
@@ -137,7 +137,7 @@ requires-python = ">=3.10"
# Set the maximum line length to 79.
line-length = 79
[tool.ruff.linter]
[tool.ruff.lint]
# Add the `line-too-long` rule to the enforced rule set.
extend-select = ["E501"]
```
@@ -164,7 +164,7 @@ rules, we can set our `pyproject.toml` to the following:
[project]
requires-python = ">=3.10"
[tool.ruff.linter]
[tool.ruff.lint]
extend-select = [
"UP", # pyupgrade
]
@@ -187,13 +187,13 @@ all functions have docstrings:
[project]
requires-python = ">=3.10"
[tool.ruff.linter]
[tool.ruff.lint]
extend-select = [
"UP", # pyupgrade
"D", # pydocstyle
]
[tool.ruff.linter.pydocstyle]
[tool.ruff.lint.pydocstyle]
convention = "google"
```
@@ -284,13 +284,13 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
# Run the Ruff linter.
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.2
rev: v0.1.3
hooks:
- id: ruff
# Run the Ruff formatter.
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.291
rev: v0.1.3
hooks:
- id: ruff-format
```

View File

@@ -13,6 +13,8 @@ Ruff uses a custom versioning scheme that uses the **minor** version number for
- Support for a new file type is promoted to stable
- Support for an end-of-life Python version is dropped
- The behavior of a stable rule is changed
- The scope of a stable rule is significantly increased
- The intent of the rule changes
- Does not include bug fixes that follow the original intent of the rule
- Stable rules are added to the default set
- Stable rules are removed from the default set
@@ -23,6 +25,7 @@ Ruff uses a custom versioning scheme that uses the **minor** version number for
- Bugs are fixed, _including behavior changes that fix bugs_
- An unsafe fix for a rule is added
- A safe fix for a rule is added in preview
- The scope of a rule is increased in preview
- A fixs applicability is demoted
- A new configuration option is added
- A rule is added in preview

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.1.2"
version = "0.1.3"
description = "An extremely fast Python linter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

22
ruff.schema.json generated
View File

@@ -159,7 +159,7 @@
}
},
"external": {
"description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
"description": "A list of rule codes or prefixes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
"type": [
"array",
"null"
@@ -436,7 +436,7 @@
]
},
"line-length": {
"description": "The line length to use when enforcing long-lines violations (like `E501`) and at which the formatter prefers to wrap lines.\n\nThe length is determined by the number of characters per line, except for lines containing East Asian characters or emojis. For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.\n\nThe value must be greater than `0` and less than or equal to `320`.\n\nNote: While the formatter will attempt to format lines such that they remain within the `line-length`, it isn't a hard upper bound, and formatted lines may exceed the `line-length`.\n\nSee [`pycodestyle.max-line-length`](#pycodestyle-max-line-length) to configure different lengths for `E501` and the formatter.",
"description": "The line length to use when enforcing long-lines violations (like `E501`) and at which `isort` and the formatter prefers to wrap lines.\n\nThe length is determined by the number of characters per line, except for lines containing East Asian characters or emojis. For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.\n\nThe value must be greater than `0` and less than or equal to `320`.\n\nNote: While the formatter will attempt to format lines such that they remain within the `line-length`, it isn't a hard upper bound, and formatted lines may exceed the `line-length`.\n\nSee [`pycodestyle.max-line-length`](#pycodestyle-max-line-length) to configure different lengths for `E501` and the formatter.",
"anyOf": [
{
"$ref": "#/definitions/LineLength"
@@ -1083,7 +1083,7 @@
]
},
"docstring-quotes": {
"description": "Quote style to prefer for docstrings (either \"single\" or \"double\").",
"description": "Quote style to prefer for docstrings (either \"single\" or \"double\").\n\nWhen using the formatter, only \"double\" is compatible, as the formatter enforces double quotes for docstrings strings.",
"anyOf": [
{
"$ref": "#/definitions/Quote"
@@ -1094,7 +1094,7 @@
]
},
"inline-quotes": {
"description": "Quote style to prefer for inline strings (either \"single\" or \"double\").",
"description": "Quote style to prefer for inline strings (either \"single\" or \"double\").\n\nWhen using the formatter, ensure that `format.quote-style` is set to the same preferred quote style.",
"anyOf": [
{
"$ref": "#/definitions/Quote"
@@ -1105,7 +1105,7 @@
]
},
"multiline-quotes": {
"description": "Quote style to prefer for multiline strings (either \"single\" or \"double\").",
"description": "Quote style to prefer for multiline strings (either \"single\" or \"double\").\n\nWhen using the formatter, only \"double\" is compatible, as the formatter enforces double quotes for multiline strings.",
"anyOf": [
{
"$ref": "#/definitions/Quote"
@@ -1424,7 +1424,7 @@
}
},
"force-wrap-aliases": {
"description": "Force `import from` statements with multiple members and at least one alias (e.g., `import A as B`) to wrap such that every line contains exactly one member. For example, this formatting would be retained, rather than condensing to a single line:\n\n```python from .utils import ( test_directory as test_directory, test_id as test_id ) ```\n\nNote that this setting is only effective when combined with `combine-as-imports = true`. When `combine-as-imports` isn't enabled, every aliased `import from` will be given its own line, in which case, wrapping is not necessary.",
"description": "Force `import from` statements with multiple members and at least one alias (e.g., `import A as B`) to wrap such that every line contains exactly one member. For example, this formatting would be retained, rather than condensing to a single line:\n\n```python from .utils import ( test_directory as test_directory, test_id as test_id ) ```\n\nNote that this setting is only effective when combined with `combine-as-imports = true`. When `combine-as-imports` isn't enabled, every aliased `import from` will be given its own line, in which case, wrapping is not necessary.\n\nWhen using the formatter, ensure that `format.skip-magic-trailing-comma` is set to `false` (default) when enabling `force-wrap-aliases` to avoid that the formatter collapses members if they all fit on a single line.",
"type": [
"boolean",
"null"
@@ -1471,7 +1471,7 @@
}
},
"lines-after-imports": {
"description": "The number of blank lines to place after imports. Use `-1` for automatic determination.",
"description": "The number of blank lines to place after imports. Use `-1` for automatic determination.\n\nWhen using the formatter, only the values `-1`, `1`, and `2` are compatible because it enforces at least one empty and at most two empty lines after imports.",
"type": [
"integer",
"null"
@@ -1479,7 +1479,7 @@
"format": "int"
},
"lines-between-types": {
"description": "The number of lines to place between \"direct\" and `import from` imports.",
"description": "The number of lines to place between \"direct\" and `import from` imports.\n\nWhen using the formatter, only the values `0` and `1` are compatible because it preserves up to one empty line after imports in nested blocks.",
"type": [
"integer",
"null"
@@ -1559,7 +1559,7 @@
}
},
"split-on-trailing-comma": {
"description": "If a comma is placed after the last member in a multi-line import, then the imports will never be folded into one line.\n\nSee isort's [`split-on-trailing-comma`](https://pycqa.github.io/isort/docs/configuration/options.html#split-on-trailing-comma) option.",
"description": "If a comma is placed after the last member in a multi-line import, then the imports will never be folded into one line.\n\nSee isort's [`split-on-trailing-comma`](https://pycqa.github.io/isort/docs/configuration/options.html#split-on-trailing-comma) option.\n\nWhen using the formatter, ensure that `format.skip-magic-trailing-comma` is set to `false` (default) when enabling `split-on-trailing-comma` to avoid that the formatter removes the trailing commas.",
"type": [
"boolean",
"null"
@@ -1733,7 +1733,7 @@
}
},
"external": {
"description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
"description": "A list of rule codes or prefixes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.",
"type": [
"array",
"null"
@@ -2213,7 +2213,7 @@
]
},
"max-doc-length": {
"description": "The maximum line length to allow for [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) violations within documentation (`W505`), including standalone comments. By default, this is set to null which disables reporting violations.\n\nThe length is determined by the number of characters per line, except for lines containinAsian characters or emojis. For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.\n\nSee the [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) rule for more information.",
"description": "The maximum line length to allow for [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) violations within documentation (`W505`), including standalone comments. By default, this is set to null which disables reporting violations.\n\nThe length is determined by the number of characters per line, except for lines containing Asian characters or emojis. For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.\n\nSee the [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) rule for more information.",
"anyOf": [
{
"$ref": "#/definitions/LineLength"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "scripts"
version = "0.1.2"
version = "0.1.3"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]

View File

@@ -70,6 +70,12 @@ if [ ! -d "$dir/cpython/.git" ]; then
fi
git -C "$dir/cpython" checkout -q b75186f69edcf54615910a5cd707996144163ef7
# poetry itself
if [ ! -d "$dir/poetry/.git" ]; then
git clone --filter=tree:0 https://github.com/python-poetry/poetry "$dir/poetry"
fi
git -C "$dir/poetry" checkout -q 611033a7335f3c8e2b74dd58688fb9021cf84a5b
# Uncomment if you want to update the hashes
#for i in "$dir"/*/; do git -C "$i" switch main && git -C "$i" pull; done
#for i in "$dir"/*/; do echo "# $(basename "$i") $(git -C "$i" rev-parse HEAD)"; done