Compare commits

...

33 Commits

Author SHA1 Message Date
Zanie
b3980c1113 Add must_use 2023-12-26 12:56:06 -06:00
Zanie
4f6e03ced8 Do not take explicit-preview-rules into account when selecting fixable rules 2023-12-26 12:43:35 -06:00
dependabot[bot]
29513398d2 Bump env_logger from 0.10.0 to 0.10.1 (#9275) 2023-12-25 09:03:35 -05:00
Charlie Marsh
fa78d2d97c Avoid adding return types to stub methods (#9277)
We should avoid adding `-> None` to stubs in `.pyi` files, along with a
few other cases. (We already ignore abstract methods.)

Closes https://github.com/astral-sh/ruff/issues/9270.
2023-12-25 09:03:24 -05:00
dependabot[bot]
8b70240fa2 Bump anyhow from 1.0.75 to 1.0.76 (#9274) 2023-12-25 08:12:40 -05:00
dependabot[bot]
e85e0d45f3 Bump ignore from 0.4.20 to 0.4.21 (#9273) 2023-12-25 08:12:36 -05:00
dependabot[bot]
9b43203575 Bump colored from 2.0.4 to 2.1.0 (#9271) 2023-12-25 08:12:28 -05:00
dependabot[bot]
bae3fa435d Bump pep440_rs from 0.3.12 to 0.4.0 (#9272) 2023-12-25 08:12:20 -05:00
Zanie Blue
6e65601055 Update some references to the old repo org (#9233)
Need https://github.com/pkgxdev/pantry/issues/4531 before we can update
at
af88ffc57e/docs/installation.md (L30-L31)
2023-12-24 20:02:49 +00:00
Charlie Marsh
9d6444138b Remove lexing and parsing from the linter benchmark (#9264)
## Summary

This PR adds some helper structs to the linter paths to enable passing
in the pre-computed tokens and parsed source code during benchmarking,
to remove lexing and parsing from the overall linter benchmark
measurement. We already remove parsing for the formatter, and we have
separate benchmarks for the lexer and the parser, so this should make it
much easier to measure linter performance changes.
2023-12-23 16:43:11 -05:00
Charlie Marsh
6d0c9c4e95 Avoid asyncio-dangling-task for nonlocal and global bindings (#9263)
Closes https://github.com/astral-sh/ruff/issues/9262.
2023-12-23 21:33:50 +00:00
Charlie Marsh
20def33fb7 Remove special pre-visit for module docstrings (#9261)
This ensures that we visit the module docstring like any other string.

Closes https://github.com/astral-sh/ruff/issues/9260.
2023-12-23 10:03:12 -05:00
Charlie Marsh
506ffade6c Remove unnecessary rule enabled check (#9259) 2023-12-23 12:45:22 +00:00
dependabot[bot]
5040fb8cec Bump thiserror from 1.0.50 to 1.0.51 (#9255) 2023-12-23 12:44:44 +00:00
Charlie Marsh
09ac0f9e72 Remove separate push method (#9258) 2023-12-23 12:36:40 +00:00
dependabot[bot]
34d7584ca3 Bump toml from 0.8.2 to 0.8.8 (#9253) 2023-12-23 07:35:24 -05:00
dependabot[bot]
097d0a4322 Bump proc-macro2 from 1.0.70 to 1.0.71 (#9250) 2023-12-23 07:35:18 -05:00
dependabot[bot]
9a672ec112 Bump cachedir from 0.3.0 to 0.3.1 (#9254) 2023-12-23 07:35:11 -05:00
dependabot[bot]
7a109164a6 Bump test-case from 3.2.1 to 3.3.1 (#9252) 2023-12-23 07:35:03 -05:00
Henry Schreiner
74dba3ee59 ci: group dependabot updates (#9249)
See https://github.com/scientific-python/cookie/pull/348.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2023-12-22 23:39:52 -06:00
Charlie Marsh
cdea7d71a3 Fix scoping for generators in named expressions in classes (#9248)
Closes https://github.com/astral-sh/ruff/issues/9230.
2023-12-22 18:06:40 +00:00
Charlie Marsh
bb86d359d4 Run cargo with --locked in CI (#9247)
This should ensure that CI fails if the lockfile is not up-to-date (see:
https://github.com/astral-sh/ruff/pull/9235).
2023-12-22 15:53:42 +00:00
Micha Reiser
9cc257ee7d Improve dummy_implementations preview style formatting (#9240) 2023-12-22 03:44:14 +00:00
Micha Reiser
a06723da2b Parenthesize multi-context managers (#9222) 2023-12-22 03:41:03 +00:00
Micha Reiser
fa2c37b411 Parenthesize long type annotations in annotated assignments (#9210) 2023-12-22 03:33:47 +00:00
Micha Reiser
3cc719bd74 Use named preview test functions (#9239) 2023-12-22 00:23:04 +00:00
Micha Reiser
d835b28d01 Show preview changes for tests with options (#9223) 2023-12-21 23:36:19 +00:00
Charlie Marsh
1e7bc1dffe Wrap subscripted dicts in parens for f-string conversion (#9238)
Closes https://github.com/astral-sh/ruff/issues/9227.
2023-12-21 21:51:50 +00:00
Charlie Marsh
e241c1c5df Make parent non-Optional in traverse_union (#9219)
## Summary

This protects callers from having to pass in `None`, and allows the
callback to operate as if it's always a union member.
2023-12-21 21:10:08 +00:00
Charlie Marsh
b0ae1199e8 Add a fix for never-union (#9218)
## Summary

Enables us to rewrite `Never | int` as `int`.
2023-12-21 21:01:09 +00:00
Charlie Marsh
a9ceef5b5d [ruff] Add never-union rule to detect redundant typing.NoReturn and typing.Never (#9217)
## Summary

Adds a rule to detect unions that include `typing.NoReturn` or
`typing.Never`. In such cases, the use of the bottom type is redundant.

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

## Test Plan

`cargo test`
2023-12-21 20:53:31 +00:00
Andrew Gallant
a3e06e5a9d update lock file from v0.1.9 release (#9235)
This should have been done before the actual release, so we add another
step to `CONTRIBUTING.md` to make sure it gets done in the future.

This doesn't fix https://github.com/astral-sh/ruff/issues/9234
completely, but it's a step in the right direction.
2023-12-21 15:47:30 -05:00
Zanie Blue
3c2b800d26 Clarify release workflow steps in CONTRIB guide (#9232)
We always recommend providing the SHA and since
https://github.com/astral-sh/ruff/pull/7279 it does not need to be the
latest commit on `main`.
2023-12-21 12:25:18 -06:00
94 changed files with 3371 additions and 1491 deletions

View File

@@ -5,6 +5,10 @@ updates:
schedule:
interval: "weekly"
labels: ["internal"]
groups:
actions:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/"

View File

@@ -95,9 +95,9 @@ jobs:
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- name: "Clippy (wasm)"
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
cargo-test-linux:
runs-on: ubuntu-latest

View File

@@ -73,7 +73,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel - x86_64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
@@ -112,7 +112,7 @@ jobs:
- name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1
with:
args: --release --target universal2-apple-darwin --out dist
args: --release --locked --target universal2-apple-darwin --out dist
- name: "Test wheel - universal2"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
@@ -161,7 +161,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel"
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
@@ -210,7 +210,7 @@ jobs:
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel"
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
@@ -269,7 +269,7 @@ jobs:
target: ${{ matrix.platform.target }}
manylinux: auto
docker-options: ${{ matrix.platform.maturin_docker_options }}
args: --release --out dist
args: --release --locked --out dist
- uses: uraimo/run-on-arch-action@v2
if: matrix.platform.arch != 'ppc64'
name: Test wheel
@@ -324,7 +324,7 @@ jobs:
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
@@ -379,7 +379,7 @@ jobs:
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --out dist
args: --release --locked --out dist
docker-options: ${{ matrix.platform.maturin_docker_options }}
- uses: uraimo/run-on-arch-action@v2
name: Test wheel

View File

@@ -326,16 +326,18 @@ We use an experimental in-house tool for managing releases.
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
1. Highlight any breaking changes in `BREAKING_CHANGES.md`
1. Run `cargo check`. This should update the lock file with new versions.
1. Create a pull request with the changelog and version updates
1. Merge the PR
1. Run the release workflow with the version number (without starting `v`) as input. Make sure
main has your merged PR as last commit
1. Run the [release workflow](https://github.com/astral-sh/ruff/actions/workflows/release.yaml) with:
- The new version number (without starting `v`)
- The commit hash of the merged release pull request on `main`
1. The release workflow will do the following:
1. Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or
uploaded anything, you can restart after pushing a fix.
1. Upload to PyPI.
1. Create and push the Git tag (as extracted from `pyproject.toml`). We create the Git tag only
after building the wheels and uploading to PyPI, since we can't delete or modify the tag ([#4468](https://github.com/charliermarsh/ruff/issues/4468)).
after building the wheels and uploading to PyPI, since we can't delete or modify the tag ([#4468](https://github.com/astral-sh/ruff/issues/4468)).
1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any
downstream jobs manually if needed.
@@ -344,7 +346,10 @@ We use an experimental in-house tool for managing releases.
1. Copy the changelog for the release into the GitHub release
- See previous releases for formatting of section headers
1. Generate the contributor list with `rooster contributors` and add to the release notes
1. If needed, [update the schemastore](https://github.com/charliermarsh/ruff/blob/main/scripts/update_schemastore.py)
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
1. Once run successfully, you should follow the link in the output to create a PR.
1. If needed, update the `ruff-lsp` and `ruff-vscode` repositories.
## Ecosystem CI

88
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.75"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
[[package]]
name = "argfile"
@@ -234,9 +234,9 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "cachedir"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e236bf5873ea57ec2877445297f4da008916bfae51567131acfc54a073d694f3"
checksum = "4703f3937077db8fa35bee3c8789343c1aec2585f0146f09d658d4ccc0e8d873"
dependencies = [
"tempfile",
]
@@ -434,11 +434,10 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "2.0.4"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"is-terminal",
"lazy_static",
"windows-sys 0.48.0",
]
@@ -736,9 +735,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "env_logger"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
"humantime",
"is-terminal",
@@ -809,7 +808,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.8"
version = "0.1.9"
dependencies = [
"anyhow",
"clap",
@@ -818,7 +817,7 @@ dependencies = [
"itertools 0.11.0",
"log",
"once_cell",
"pep440_rs",
"pep440_rs 0.4.0",
"pretty_assertions",
"regex",
"ruff_linter",
@@ -998,17 +997,16 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.20"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
dependencies = [
"crossbeam-deque",
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"regex-automata 0.4.3",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
@@ -1605,6 +1603,18 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "pep440_rs"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c29f9c43de378b4e4e0cd7dbcce0e5cfb80443de8c05620368b2948bc936a1"
dependencies = [
"once_cell",
"regex",
"serde",
"unicode-width",
]
[[package]]
name = "pep508_rs"
version = "0.2.1"
@@ -1612,7 +1622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0713d7bb861ca2b7d4c50a38e1f31a4b63a2e2df35ef1e5855cc29e108453e2"
dependencies = [
"once_cell",
"pep440_rs",
"pep440_rs 0.3.12",
"regex",
"serde",
"thiserror",
@@ -1794,9 +1804,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.70"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
dependencies = [
"unicode-ident",
]
@@ -1808,7 +1818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46d4a5e69187f23a29f8aa0ea57491d104ba541bc55f76552c2a74962aa20e04"
dependencies = [
"indexmap",
"pep440_rs",
"pep440_rs 0.3.12",
"pep508_rs",
"serde",
"toml",
@@ -2063,7 +2073,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.8"
version = "0.1.9"
dependencies = [
"annotate-snippets 0.9.2",
"anyhow",
@@ -2199,7 +2209,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.8"
version = "0.1.9"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2222,7 +2232,7 @@ dependencies = [
"once_cell",
"path-absolutize",
"pathdiff",
"pep440_rs",
"pep440_rs 0.4.0",
"pretty_assertions",
"pyproject-toml",
"quick-junit",
@@ -2452,7 +2462,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.8"
version = "0.1.9"
dependencies = [
"anyhow",
"clap",
@@ -2528,7 +2538,7 @@ dependencies = [
"log",
"once_cell",
"path-absolutize",
"pep440_rs",
"pep440_rs 0.4.0",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -2731,9 +2741,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.3"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
@@ -2958,9 +2968,9 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "test-case"
version = "3.2.1"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8f1e820b7f1d95a0cdbf97a5df9de10e1be731983ab943e56703ac1b8e9d425"
checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
dependencies = [
"test-case-macros",
]
@@ -2993,18 +3003,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.50"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.50"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
@@ -3093,9 +3103,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.2"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
dependencies = [
"serde",
"serde_spanned",
@@ -3105,18 +3115,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.3"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.20.2"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap",
"serde",

View File

@@ -12,15 +12,15 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
anyhow = { version = "1.0.69" }
anyhow = { version = "1.0.76" }
bitflags = { version = "2.4.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.7", features = ["derive"] }
colored = { version = "2.0.0" }
colored = { version = "2.1.0" }
filetime = { version = "0.2.23" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
ignore = { version = "0.4.20" }
ignore = { version = "0.4.21" }
insta = { version = "1.34.0", feature = ["filters", "glob"] }
is-macro = { version = "0.3.1" }
itertools = { version = "0.11.0" }
@@ -29,7 +29,7 @@ log = { version = "0.4.17" }
memchr = { version = "2.6.4" }
once_cell = { version = "1.19.0" }
path-absolutize = { version = "3.1.1" }
proc-macro2 = { version = "1.0.70" }
proc-macro2 = { version = "1.0.71" }
quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
@@ -43,9 +43,9 @@ static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.50" }
toml = { version = "0.8.2" }
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.51" }
toml = { version = "0.8.8" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

View File

@@ -23,7 +23,7 @@ configparser = { version = "3.0.3" }
itertools = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pep440_rs = { version = "0.4.0", features = ["serde"] }
regex = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }

View File

@@ -2,7 +2,7 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_linter::linter::lint_only;
use ruff_linter::linter::{lint_only, ParseSource};
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::PreviewMode;
@@ -10,6 +10,7 @@ use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{registry::Rule, RuleSelector};
use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, parse_program_tokens, Mode};
#[cfg(target_os = "windows")]
#[global_allocator]
@@ -53,7 +54,13 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
BenchmarkId::from_parameter(case.name()),
&case,
|b, case| {
let kind = SourceKind::Python(case.code().to_string());
// Tokenize the source.
let tokens = lexer::lex(case.code(), Mode::Module).collect::<Vec<_>>();
// Parse the source.
let ast =
parse_program_tokens(tokens.clone(), case.code(), case.name(), false).unwrap();
b.iter(|| {
let path = case.path();
let result = lint_only(
@@ -61,8 +68,12 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
None,
settings,
flags::Noqa::Enabled,
&kind,
&SourceKind::Python(case.code().to_string()),
PySourceType::from(path.as_path()),
ParseSource::Precomputed {
tokens: &tokens,
ast: &ast,
},
);
// Assert that file contains no parse errors

View File

@@ -33,7 +33,7 @@ anyhow = { workspace = true }
argfile = { version = "0.1.6" }
bincode = { version = "1.3.3" }
bitflags = { workspace = true }
cachedir = { version = "0.3.0" }
cachedir = { version = "0.3.1" }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
clap_complete_command = { version = "0.5.1" }

View File

@@ -12,7 +12,7 @@ use rustc_hash::FxHashMap;
use crate::cache::{Cache, FileCacheKey, LintCacheData};
use ruff_diagnostics::Diagnostic;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult};
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
use ruff_linter::logging::DisplayParseError;
use ruff_linter::message::Message;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
@@ -303,12 +303,28 @@ pub(crate) fn lint_path(
(result, fixed)
} else {
// If we fail to fix, lint the original source code.
let result = lint_only(path, package, settings, noqa, &source_kind, source_type);
let result = lint_only(
path,
package,
settings,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
(result, fixed)
}
} else {
let result = lint_only(path, package, settings, noqa, &source_kind, source_type);
let result = lint_only(
path,
package,
settings,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
(result, fixed)
};
@@ -444,6 +460,7 @@ pub(crate) fn lint_stdin(
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
@@ -462,6 +479,7 @@ pub(crate) fn lint_stdin(
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
(result, fixed)

View File

@@ -52,7 +52,7 @@ path-absolutize = { workspace = true, features = [
"use_unix_paths_on_wasm",
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pyproject-toml = { version = "0.8.1" }
quick-junit = { version = "0.3.5" }
regex = { workspace = true }

View File

@@ -212,3 +212,20 @@ def func(x: int):
raise ValueError
else:
return 1
from typing import overload
@overload
def overloaded(i: int) -> "int":
...
@overload
def overloaded(i: "str") -> "str":
...
def overloaded(i):
return i

View File

@@ -0,0 +1,9 @@
import a
"""Some other docstring."""
import b
"""Some other docstring."""
import c

View File

@@ -0,0 +1,5 @@
"""Docstring"""
"""Non-docstring"""
from __future__ import absolute_import

View File

@@ -0,0 +1,8 @@
"""Test for accessing class members within a generator."""
class Class:
items = []
if len(replacements := {item[1] for item in items}) > 1:
pass

View File

@@ -1,30 +1,28 @@
# These should change
x = u"Hello"
u"Hello"
u'world'
x = u"Hello" # UP025
print(u"Hello")
u'world' # UP025
print(u'world')
print(u"Hello") # UP025
print(u'world') # UP025
import foo
foo(u"Hello", U"world", a=u"Hello", b=u"world")
foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
# These should stay quoted they way they are
# Retain quotes when fixing.
x = u'hello' # UP025
x = u"""hello""" # UP025
x = u'''hello''' # UP025
x = u'Hello "World"' # UP025
x = u'hello'
x = u"""hello"""
x = u'''hello'''
x = u'Hello "World"'
# These should not change
u = "Hello"
u = u
u = "Hello" # OK
u = u # OK
def hello():
return"Hello"
return"Hello" # OK
f"foo"u"bar"
f"foo" u"bar"
f"foo"u"bar" # OK
f"foo" u"bar" # OK

View File

@@ -243,3 +243,12 @@ raise ValueError(
).format(a, b)
("{}" "{{{}}}").format(a, b)
# The dictionary should be parenthesized.
"{}".format({0: 1}[0])
# The dictionary should be parenthesized.
"{}".format({0: 1}.bar)
# The dictionary should be parenthesized.
"{}".format({0: 1}())

View File

@@ -152,3 +152,13 @@ async def f(x: bool):
t = asyncio.create_task(asyncio.sleep(1))
else:
t = None
# OK
async def f(x: bool):
global T
if x:
T = asyncio.create_task(asyncio.sleep(1))
else:
T = None

View File

@@ -0,0 +1,8 @@
from typing import Never, NoReturn, Union
Union[Never, int]
Union[NoReturn, int]
Never | int
NoReturn | int
Union[Union[Never, int], Union[NoReturn, int]]
Union[NoReturn, int, float]

View File

@@ -81,6 +81,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::DuplicateUnionMember,
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
Rule::NeverUnion,
]) {
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
@@ -100,6 +101,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NeverUnion) {
ruff::rules::never_union(checker, expr);
}
if checker.any_enabled(&[
Rule::SysVersionSlice3,
Rule::SysVersion2,
@@ -1154,6 +1159,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NeverUnion) {
ruff::rules::never_union(checker, expr);
}
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
if !checker.semantic.in_nested_union() {

View File

@@ -287,7 +287,18 @@ where
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
Stmt::Expr(ast::StmtExpr { value, .. })
if !self
.semantic
.flags
.intersects(SemanticModelFlags::MODULE_DOCSTRING)
&& value.is_string_literal_expr() =>
{
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
}
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
// Allow __future__ imports until we see a non-__future__ import.
if let Some("__future__") = module.as_deref() {
if names
@@ -301,9 +312,11 @@ where
}
}
Stmt::Import(_) => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
}
_ => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !(self.semantic.seen_import_boundary()
|| helpers::is_assignment_to_a_dunder(stmt)
@@ -1435,11 +1448,8 @@ where
impl<'a> Checker<'a> {
/// Visit a [`Module`]. Returns `true` if the module contains a module-level docstring.
fn visit_module(&mut self, python_ast: &'a Suite) -> bool {
fn visit_module(&mut self, python_ast: &'a Suite) {
analyze::module(python_ast, self);
let docstring = docstrings::extraction::docstring_from(python_ast);
docstring.is_some()
}
/// Visit a list of [`Comprehension`] nodes, assumed to be the comprehensions that compose a
@@ -1745,10 +1755,13 @@ impl<'a> Checker<'a> {
return;
}
// If the expression is the left-hand side of a walrus operator, then it's a named
// expression assignment.
if self
.semantic
.current_expressions()
.any(Expr::is_named_expr_expr)
.filter_map(Expr::as_named_expr_expr)
.any(|parent| parent.target.as_ref() == expr)
{
self.add_binding(id, expr.range(), BindingKind::NamedExprAssignment, flags);
return;
@@ -2003,14 +2016,8 @@ pub(crate) fn check_ast(
);
checker.bind_builtins();
// Check for module docstring.
let python_ast = if checker.visit_module(python_ast) {
&python_ast[1..]
} else {
python_ast
};
// Iterate over the AST.
checker.visit_module(python_ast);
checker.visit_body(python_ast);
// Visit any deferred syntax nodes. Take care to visit in order, such that we avoid adding

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_diagnostics::Diagnostic;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::TokenKind;
@@ -97,7 +97,7 @@ pub(crate) fn check_logical_lines(
indent_size,
) {
if settings.rules.enabled(kind.rule()) {
context.push(kind, range);
context.push_diagnostic(Diagnostic::new(kind, range));
}
}
@@ -123,18 +123,6 @@ impl<'a> LogicalLinesContext<'a> {
}
}
pub(crate) fn push<K: Into<DiagnosticKind>>(&mut self, kind: K, range: TextRange) {
let kind = kind.into();
if self.settings.rules.enabled(kind.rule()) {
self.diagnostics.push(Diagnostic {
kind,
range,
fix: None,
parent: None,
});
}
}
pub(crate) fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
if self.settings.rules.enabled(diagnostic.kind.rule()) {
self.diagnostics.push(diagnostic);

View File

@@ -901,6 +901,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -11,7 +11,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_notebook::Notebook;
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_ast::{PySourceType, Suite};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
@@ -73,7 +73,6 @@ pub struct FixerResult<'a> {
pub fn check_path(
path: &Path,
package: Option<&Path>,
tokens: Vec<LexResult>,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
@@ -82,6 +81,7 @@ pub fn check_path(
noqa: flags::Noqa,
source_kind: &SourceKind,
source_type: PySourceType,
tokens: TokenSource,
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
@@ -144,12 +144,8 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
match ruff_python_parser::parse_program_tokens(
tokens,
source_kind.source_code(),
&path.to_string_lossy(),
source_type.is_ipynb(),
) {
// Parse, if the AST wasn't pre-provided provided.
match tokens.into_ast_source(source_kind, source_type, path) {
Ok(python_ast) => {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
if use_ast {
@@ -325,7 +321,6 @@ pub fn add_noqa_to_path(
} = check_path(
path,
package,
tokens,
&locator,
&stylist,
&indexer,
@@ -334,6 +329,7 @@ pub fn add_noqa_to_path(
flags::Noqa::Disabled,
source_kind,
source_type,
TokenSource::Tokens(tokens),
);
// Log any parse errors.
@@ -365,10 +361,10 @@ pub fn lint_only(
noqa: flags::Noqa,
source_kind: &SourceKind,
source_type: PySourceType,
data: ParseSource,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(source_kind.source_code(), source_type.as_mode());
let tokens = data.into_token_source(source_kind, source_type);
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(source_kind.source_code());
@@ -391,7 +387,6 @@ pub fn lint_only(
let result = check_path(
path,
package,
tokens,
&locator,
&stylist,
&indexer,
@@ -400,6 +395,7 @@ pub fn lint_only(
noqa,
source_kind,
source_type,
tokens,
);
result.map(|(diagnostics, imports)| {
@@ -487,7 +483,6 @@ pub fn lint_fix<'a>(
let result = check_path(
path,
package,
tokens,
&locator,
&stylist,
&indexer,
@@ -496,6 +491,7 @@ pub fn lint_fix<'a>(
noqa,
&transformed,
source_type,
TokenSource::Tokens(tokens),
);
if iterations == 0 {
@@ -632,6 +628,95 @@ This indicates a bug in Ruff. If you could open an issue at:
}
}
#[derive(Debug, Clone)]
pub enum ParseSource<'a> {
/// Extract the tokens and AST from the given source code.
None,
/// Use the precomputed tokens and AST.
Precomputed {
tokens: &'a [LexResult],
ast: &'a Suite,
},
}
impl<'a> ParseSource<'a> {
/// Convert to a [`TokenSource`], tokenizing if necessary.
fn into_token_source(
self,
source_kind: &SourceKind,
source_type: PySourceType,
) -> TokenSource<'a> {
match self {
Self::None => TokenSource::Tokens(ruff_python_parser::tokenize(
source_kind.source_code(),
source_type.as_mode(),
)),
Self::Precomputed { tokens, ast } => TokenSource::Precomputed { tokens, ast },
}
}
}
#[derive(Debug, Clone)]
pub enum TokenSource<'a> {
/// Use the precomputed tokens to generate the AST.
Tokens(Vec<LexResult>),
/// Use the precomputed tokens and AST.
Precomputed {
tokens: &'a [LexResult],
ast: &'a Suite,
},
}
impl Deref for TokenSource<'_> {
type Target = [LexResult];
fn deref(&self) -> &Self::Target {
match self {
Self::Tokens(tokens) => tokens,
Self::Precomputed { tokens, .. } => tokens,
}
}
}
impl<'a> TokenSource<'a> {
/// Convert to an [`AstSource`], parsing if necessary.
fn into_ast_source(
self,
source_kind: &SourceKind,
source_type: PySourceType,
path: &Path,
) -> Result<AstSource<'a>, ParseError> {
match self {
Self::Tokens(tokens) => Ok(AstSource::Ast(ruff_python_parser::parse_program_tokens(
tokens,
source_kind.source_code(),
&path.to_string_lossy(),
source_type.is_ipynb(),
)?)),
Self::Precomputed { ast, .. } => Ok(AstSource::Precomputed(ast)),
}
}
}
#[derive(Debug, Clone)]
pub enum AstSource<'a> {
/// Extract the AST from the given source code.
Ast(Suite),
/// Use the precomputed AST.
Precomputed(&'a Suite),
}
impl Deref for AstSource<'_> {
type Target = Suite;
fn deref(&self) -> &Self::Target {
match self {
Self::Ast(ast) => ast,
Self::Precomputed(ast) => ast,
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;

View File

@@ -243,6 +243,17 @@ pub struct PreviewOptions {
pub require_explicit: bool,
}
impl PreviewOptions {
/// Return a copy with the same preview mode setting but require explicit disabled.
#[must_use]
pub fn without_require_explicit(&self) -> Self {
Self {
mode: self.mode,
require_explicit: false,
}
}
}
#[cfg(feature = "schemars")]
mod schema {
use itertools::Itertools;

View File

@@ -482,7 +482,6 @@ impl Violation for AnyType {
format!("Dynamically typed expressions (typing.Any) are disallowed in `{name}`")
}
}
fn is_none_returning(body: &[Stmt]) -> bool {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(body);
@@ -537,17 +536,41 @@ fn check_dynamically_typed<F>(
}
}
fn is_empty_body(body: &[Stmt]) -> bool {
body.iter().all(|stmt| match stmt {
Stmt::Pass(_) => true,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
matches!(
value.as_ref(),
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
)
/// Return `true` if a function appears to be a stub.
fn is_stub_function(function_def: &ast::StmtFunctionDef, checker: &Checker) -> bool {
/// Returns `true` if a function has an empty body.
fn is_empty_body(function_def: &ast::StmtFunctionDef) -> bool {
function_def.body.iter().all(|stmt| match stmt {
Stmt::Pass(_) => true,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
matches!(
value.as_ref(),
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
)
}
_ => false,
})
}
// Ignore functions with empty bodies in...
if is_empty_body(function_def) {
// Stub definitions (.pyi files)...
if checker.source_type.is_stub() {
return true;
}
_ => false,
})
// Abstract methods...
if visibility::is_abstract(&function_def.decorator_list, checker.semantic()) {
return true;
}
// Overload definitions...
if visibility::is_overload(&function_def.decorator_list, checker.semantic()) {
return true;
}
}
false
}
/// Generate flake8-annotation checks for a given `Definition`.
@@ -738,9 +761,7 @@ pub(crate) fn definition(
) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
@@ -771,9 +792,7 @@ pub(crate) fn definition(
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
@@ -843,25 +862,22 @@ pub(crate) fn definition(
match visibility {
visibility::Visibility::Public => {
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
let return_type =
if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
@@ -885,25 +901,22 @@ pub(crate) fn definition(
}
visibility::Visibility::Private => {
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
let return_type =
if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),

View File

@@ -1,54 +0,0 @@
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_semantic::SemanticModel;
/// Traverse a "union" type annotation, applying `func` to each union member.
/// Supports traversal of `Union` and `|` union expressions.
/// The function is called with each expression in the union (excluding declarations of nested unions)
/// and the parent expression (if any).
pub(super) fn traverse_union<'a, F>(
func: &mut F,
semantic: &SemanticModel,
expr: &'a Expr,
parent: Option<&'a Expr>,
) where
F: FnMut(&'a Expr, Option<&'a Expr>),
{
// Ex) x | y
if let Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
left,
right,
range: _,
}) = expr
{
// The union data structure usually looks like this:
// a | b | c -> (a | b) | c
//
// However, parenthesized expressions can coerce it into any structure:
// a | (b | c)
//
// So we have to traverse both branches in order (left, then right), to report members
// in the order they appear in the source code.
// Traverse the left then right arms
traverse_union(func, semantic, left, Some(expr));
traverse_union(func, semantic, right, Some(expr));
return;
}
// Ex) Union[x, y]
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if semantic.match_typing_expr(value, "Union") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
// Traverse each element of the tuple within the union recursively to handle cases
// such as `Union[..., Union[...]]
elts.iter()
.for_each(|elt| traverse_union(func, semantic, elt, Some(expr)));
return;
}
}
}
// Otherwise, call the function on expression
func(expr, parent);
}

View File

@@ -1,5 +1,4 @@
//! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/).
mod helpers;
pub(crate) mod rules;
#[cfg(test)]

View File

@@ -1,15 +1,16 @@
use ruff_python_ast::{self as ast, Expr};
use rustc_hash::FxHashSet;
use std::collections::HashSet;
use crate::checkers::ast::Checker;
use rustc_hash::FxHashSet;
use crate::rules::flake8_pyi::helpers::traverse_union;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for duplicate union members.
///
@@ -55,7 +56,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
let mut diagnostics: Vec<Diagnostic> = Vec::new();
// Adds a member to `literal_exprs` if it is a `Literal` annotation
let mut check_for_duplicate_members = |expr: &'a Expr, parent: Option<&'a Expr>| {
let mut check_for_duplicate_members = |expr: &'a Expr, parent: &'a Expr| {
// If we've already seen this union member, raise a violation.
if !seen_nodes.insert(expr.into()) {
let mut diagnostic = Diagnostic::new(
@@ -68,7 +69,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
// parent without the duplicate.
// If the parent node is not a `BinOp` we will not perform a fix
if let Some(parent @ Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
if let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = parent {
// Replace the parent with its non-duplicate child.
let child = if expr == left.as_ref() { right } else { left };
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
@@ -81,12 +82,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
};
// Traverse the union, collect all diagnostic members
traverse_union(
&mut check_for_duplicate_members,
checker.semantic(),
expr,
None,
);
traverse_union(&mut check_for_duplicate_members, checker.semantic(), expr);
// Add all diagnostics to the checker
checker.diagnostics.append(&mut diagnostics);

View File

@@ -1,14 +1,16 @@
use rustc_hash::FxHashSet;
use std::fmt;
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
use crate::{checkers::ast::Checker, rules::flake8_pyi::helpers::traverse_union};
/// ## What it does
/// Checks for the presence of redundant `Literal` types and builtin super
@@ -64,7 +66,7 @@ pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr
// Adds a member to `literal_exprs` for each value in a `Literal`, and any builtin types
// to `builtin_types_in_union`.
let mut func = |expr: &'a Expr, _| {
let mut func = |expr: &'a Expr, _parent: &'a Expr| {
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if checker.semantic().match_typing_expr(value, "Literal") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
@@ -82,7 +84,7 @@ pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr
builtin_types_in_union.insert(builtin_type);
};
traverse_union(&mut func, checker.semantic(), union, None);
traverse_union(&mut func, checker.semantic(), union);
for typing_literal_expr in typing_literal_exprs {
let Some(literal_type) = match_literal_type(typing_literal_expr) else {

View File

@@ -1,11 +1,10 @@
use ruff_python_ast::{Expr, Parameters};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Parameters};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_pyi::helpers::traverse_union;
/// ## What it does
/// Checks for union annotations that contain redundant numeric types (e.g.,
@@ -90,7 +89,7 @@ fn check_annotation(checker: &mut Checker, annotation: &Expr) {
let mut has_complex = false;
let mut has_int = false;
let mut func = |expr: &Expr, _parent: Option<&Expr>| {
let mut func = |expr: &Expr, _parent: &Expr| {
let Some(call_path) = checker.semantic().resolve_call_path(expr) else {
return;
};
@@ -103,7 +102,7 @@ fn check_annotation(checker: &mut Checker, annotation: &Expr) {
}
};
traverse_union(&mut func, checker.semantic(), annotation, None);
traverse_union(&mut func, checker.semantic(), annotation);
if has_complex {
if has_float {

View File

@@ -1,13 +1,12 @@
use ast::{ExprSubscript, Operator};
use ast::Operator;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::rules::flake8_pyi::helpers::traverse_union;
/// ## What it does
/// Checks for the presence of multiple literal types in a union.
///
@@ -62,7 +61,7 @@ fn concatenate_bin_ors(exprs: Vec<&Expr>) -> Expr {
})
}
fn make_union(subscript: &ExprSubscript, exprs: Vec<&Expr>) -> Expr {
fn make_union(subscript: &ast::ExprSubscript, exprs: Vec<&Expr>) -> Expr {
Expr::Subscript(ast::ExprSubscript {
value: subscript.value.clone(),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
@@ -108,7 +107,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
let mut total_literals = 0;
// Split members into `literal_exprs` if they are a `Literal` annotation and `other_exprs` otherwise
let mut collect_literal_expr = |expr: &'a Expr, _| {
let mut collect_literal_expr = |expr: &'a Expr, _parent: &'a Expr| {
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if checker.semantic().match_typing_expr(value, "Literal") {
total_literals += 1;
@@ -137,7 +136,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
};
// Traverse the union, collect all members, split out the literals from the rest.
traverse_union(&mut collect_literal_expr, checker.semantic(), expr, None);
traverse_union(&mut collect_literal_expr, checker.semantic(), expr);
let union_subscript = expr.as_subscript_expr();
if union_subscript.is_some_and(|subscript| {

View File

@@ -2,9 +2,10 @@ use ast::{ExprContext, Operator};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::{Ranged, TextRange};
use crate::{checkers::ast::Checker, rules::flake8_pyi::helpers::traverse_union};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the presence of multiple `type`s in a union.
@@ -82,7 +83,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
let mut type_exprs = Vec::new();
let mut other_exprs = Vec::new();
let mut collect_type_exprs = |expr: &'a Expr, _| {
let mut collect_type_exprs = |expr: &'a Expr, _parent: &'a Expr| {
let subscript = expr.as_subscript_expr();
if subscript.is_none() {
@@ -101,7 +102,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
}
};
traverse_union(&mut collect_type_exprs, checker.semantic(), union, None);
traverse_union(&mut collect_type_exprs, checker.semantic(), union);
if type_exprs.len() > 1 {
let type_members: Vec<String> = type_exprs

View File

@@ -35,7 +35,8 @@ mod tests {
#[test_case(Rule::LineTooLong, Path::new("E501_3.py"))]
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))]
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
#[test_case(Rule::MultipleStatementsOnOneLineColon, Path::new("E70.py"))]
@@ -65,7 +66,7 @@ mod tests {
}
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402.py:25:1: E402 Module level import not at top of file
E402_0.py:25:1: E402 Module level import not at top of file
|
23 | sys.path.insert(0, "some/path")
24 |
@@ -11,7 +11,7 @@ E402.py:25:1: E402 Module level import not at top of file
27 | import matplotlib
|
E402.py:27:1: E402 Module level import not at top of file
E402_0.py:27:1: E402 Module level import not at top of file
|
25 | import f
26 |
@@ -21,7 +21,7 @@ E402.py:27:1: E402 Module level import not at top of file
29 | matplotlib.use("Agg")
|
E402.py:31:1: E402 Module level import not at top of file
E402_0.py:31:1: E402 Module level import not at top of file
|
29 | matplotlib.use("Agg")
30 |
@@ -31,7 +31,7 @@ E402.py:31:1: E402 Module level import not at top of file
33 | __some__magic = 1
|
E402.py:35:1: E402 Module level import not at top of file
E402_0.py:35:1: E402 Module level import not at top of file
|
33 | __some__magic = 1
34 |
@@ -39,7 +39,7 @@ E402.py:35:1: E402 Module level import not at top of file
| ^^^^^^^^ E402
|
E402.py:45:1: E402 Module level import not at top of file
E402_0.py:45:1: E402 Module level import not at top of file
|
43 | import j
44 |
@@ -47,7 +47,7 @@ E402.py:45:1: E402 Module level import not at top of file
| ^^^^^^^^ E402
|
E402.py:45:11: E402 Module level import not at top of file
E402_0.py:45:11: E402 Module level import not at top of file
|
43 | import j
44 |

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402_1.py:5:1: E402 Module level import not at top of file
|
3 | """Some other docstring."""
4 |
5 | import b
| ^^^^^^^^ E402
6 |
7 | """Some other docstring."""
|
E402_1.py:9:1: E402 Module level import not at top of file
|
7 | """Some other docstring."""
8 |
9 | import c
| ^^^^^^^^ E402
|

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402.py:35:1: E402 Module level import not at top of file
E402_0.py:35:1: E402 Module level import not at top of file
|
33 | __some__magic = 1
34 |
@@ -9,7 +9,7 @@ E402.py:35:1: E402 Module level import not at top of file
| ^^^^^^^^ E402
|
E402.py:45:1: E402 Module level import not at top of file
E402_0.py:45:1: E402 Module level import not at top of file
|
43 | import j
44 |
@@ -17,7 +17,7 @@ E402.py:45:1: E402 Module level import not at top of file
| ^^^^^^^^ E402
|
E402.py:45:11: E402 Module level import not at top of file
E402_0.py:45:11: E402 Module level import not at top of file
|
43 | import j
44 |

View File

@@ -23,7 +23,7 @@ mod tests {
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use crate::linter::{check_path, LinterResult};
use crate::linter::{check_path, LinterResult, TokenSource};
use crate::registry::{AsRule, Linter, Rule};
use crate::rules::pyflakes;
use crate::settings::types::PreviewMode;
@@ -55,7 +55,8 @@ mod tests {
#[test_case(Rule::UnusedImport, Path::new("F401_20.py"))]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
#[test_case(Rule::LateFutureImport, Path::new("F404_0.py"))]
#[test_case(Rule::LateFutureImport, Path::new("F404_1.py"))]
#[test_case(Rule::UndefinedLocalWithImportStarUsage, Path::new("F405.py"))]
#[test_case(Rule::UndefinedLocalWithNestedImportStarUsage, Path::new("F406.py"))]
#[test_case(Rule::FutureFeatureNotDefined, Path::new("F407.py"))]
@@ -142,6 +143,7 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_21.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_22.ipynb"))]
#[test_case(Rule::UndefinedName, Path::new("F821_23.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_24.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]
@@ -558,7 +560,6 @@ mod tests {
} = check_path(
Path::new("<filename>"),
None,
tokens,
&locator,
&stylist,
&indexer,
@@ -567,6 +568,7 @@ mod tests {
flags::Noqa::Enabled,
&source_kind,
source_type,
TokenSource::Tokens(tokens),
);
diagnostics.sort_by_key(Ranged::start);
let actual = diagnostics

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F404.py:6:1: F404 `from __future__` imports must occur at the beginning of the file
F404_0.py:6:1: F404 `from __future__` imports must occur at the beginning of the file
|
4 | from collections import namedtuple
5 |

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F404_1.py:5:1: F404 `from __future__` imports must occur at the beginning of the file
|
3 | """Non-docstring"""
4 |
5 | from __future__ import absolute_import
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F404
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -7,7 +7,6 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of the known pre-Python 2.5 ternary syntax.
@@ -123,11 +122,9 @@ pub(crate) fn and_or_ternary(checker: &mut Checker, bool_op: &ExprBoolOp) {
},
bool_op.range,
);
if checker.enabled(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
ternary,
bool_op.range,
)));
}
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
ternary,
bool_op.range,
)));
checker.diagnostics.push(diagnostic);
}

View File

@@ -137,10 +137,9 @@ enum FormatContext {
Accessed,
}
/// Given an [`Expr`], format it for use in a formatted expression within an f-string.
fn formatted_expr<'a>(expr: &Expr, context: FormatContext, locator: &Locator<'a>) -> Cow<'a, str> {
let text = locator.slice(expr);
let parenthesize = match (context, expr) {
/// Returns `true` if the expression should be parenthesized when used in an f-string.
fn parenthesize(expr: &Expr, text: &str, context: FormatContext) -> bool {
match (context, expr) {
// E.g., `x + y` should be parenthesized in `f"{(x + y)[0]}"`.
(
FormatContext::Accessed,
@@ -173,9 +172,44 @@ fn formatted_expr<'a>(expr: &Expr, context: FormatContext, locator: &Locator<'a>
| Expr::SetComp(_)
| Expr::DictComp(_),
) => true,
(_, Expr::Subscript(ast::ExprSubscript { value, .. })) => {
matches!(
value.as_ref(),
Expr::GeneratorExp(_)
| Expr::Dict(_)
| Expr::Set(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
)
}
(_, Expr::Attribute(ast::ExprAttribute { value, .. })) => {
matches!(
value.as_ref(),
Expr::GeneratorExp(_)
| Expr::Dict(_)
| Expr::Set(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
)
}
(_, Expr::Call(ast::ExprCall { func, .. })) => {
matches!(
func.as_ref(),
Expr::GeneratorExp(_)
| Expr::Dict(_)
| Expr::Set(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
)
}
_ => false,
};
if parenthesize && !text.starts_with('(') && !text.ends_with(')') {
}
}
/// Given an [`Expr`], format it for use in a formatted expression within an f-string.
fn formatted_expr<'a>(expr: &Expr, context: FormatContext, locator: &Locator<'a>) -> Cow<'a, str> {
let text = locator.slice(expr);
if parenthesize(expr, text, context) && !(text.starts_with('(') && text.ends_with(')')) {
Cow::Owned(format!("({text})"))
} else {
Cow::Borrowed(text)

View File

@@ -1,284 +1,302 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP025.py:2:5: UP025 [*] Remove unicode literals from strings
UP025.py:1:1: UP025 [*] Remove unicode literals from strings
|
1 | # These should change
2 | x = u"Hello"
| ^^^^^^^^ UP025
3 |
4 | u'world'
|
= help: Remove unicode prefix
Safe fix
1 1 | # These should change
2 |-x = u"Hello"
2 |+x = "Hello"
3 3 |
4 4 | u'world'
5 5 |
UP025.py:4:1: UP025 [*] Remove unicode literals from strings
|
2 | x = u"Hello"
3 |
4 | u'world'
1 | u"Hello"
| ^^^^^^^^ UP025
5 |
6 | print(u"Hello")
2 |
3 | x = u"Hello" # UP025
|
= help: Remove unicode prefix
Safe fix
1 1 | # These should change
2 2 | x = u"Hello"
3 3 |
4 |-u'world'
4 |+'world'
5 5 |
6 6 | print(u"Hello")
7 7 |
1 |-u"Hello"
1 |+"Hello"
2 2 |
3 3 | x = u"Hello" # UP025
4 4 |
UP025.py:6:7: UP025 [*] Remove unicode literals from strings
UP025.py:3:5: UP025 [*] Remove unicode literals from strings
|
4 | u'world'
5 |
6 | print(u"Hello")
1 | u"Hello"
2 |
3 | x = u"Hello" # UP025
| ^^^^^^^^ UP025
4 |
5 | u'world' # UP025
|
= help: Remove unicode prefix
Safe fix
1 1 | u"Hello"
2 2 |
3 |-x = u"Hello" # UP025
3 |+x = "Hello" # UP025
4 4 |
5 5 | u'world' # UP025
6 6 |
UP025.py:5:1: UP025 [*] Remove unicode literals from strings
|
3 | x = u"Hello" # UP025
4 |
5 | u'world' # UP025
| ^^^^^^^^ UP025
6 |
7 | print(u"Hello") # UP025
|
= help: Remove unicode prefix
Safe fix
2 2 |
3 3 | x = u"Hello" # UP025
4 4 |
5 |-u'world' # UP025
5 |+'world' # UP025
6 6 |
7 7 | print(u"Hello") # UP025
8 8 |
UP025.py:7:7: UP025 [*] Remove unicode literals from strings
|
5 | u'world' # UP025
6 |
7 | print(u"Hello") # UP025
| ^^^^^^^^ UP025
7 |
8 | print(u'world')
8 |
9 | print(u'world') # UP025
|
= help: Remove unicode prefix
Safe fix
3 3 |
4 4 | u'world'
5 5 |
6 |-print(u"Hello")
6 |+print("Hello")
7 7 |
8 8 | print(u'world')
9 9 |
4 4 |
5 5 | u'world' # UP025
6 6 |
7 |-print(u"Hello") # UP025
7 |+print("Hello") # UP025
8 8 |
9 9 | print(u'world') # UP025
10 10 |
UP025.py:8:7: UP025 [*] Remove unicode literals from strings
UP025.py:9:7: UP025 [*] Remove unicode literals from strings
|
6 | print(u"Hello")
7 |
8 | print(u'world')
7 | print(u"Hello") # UP025
8 |
9 | print(u'world') # UP025
| ^^^^^^^^ UP025
9 |
10 | import foo
10 |
11 | import foo
|
= help: Remove unicode prefix
Safe fix
5 5 |
6 6 | print(u"Hello")
7 7 |
8 |-print(u'world')
8 |+print('world')
9 9 |
10 10 | import foo
11 11 |
6 6 |
7 7 | print(u"Hello") # UP025
8 8 |
9 |-print(u'world') # UP025
9 |+print('world') # UP025
10 10 |
11 11 | import foo
12 12 |
UP025.py:12:5: UP025 [*] Remove unicode literals from strings
UP025.py:13:5: UP025 [*] Remove unicode literals from strings
|
10 | import foo
11 |
12 | foo(u"Hello", U"world", a=u"Hello", b=u"world")
11 | import foo
12 |
13 | foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
| ^^^^^^^^ UP025
13 |
14 | # These should stay quoted they way they are
14 |
15 | # Retain quotes when fixing.
|
= help: Remove unicode prefix
Safe fix
9 9 |
10 10 | import foo
11 11 |
12 |-foo(u"Hello", U"world", a=u"Hello", b=u"world")
12 |+foo("Hello", U"world", a=u"Hello", b=u"world")
13 13 |
14 14 | # These should stay quoted they way they are
15 15 |
10 10 |
11 11 | import foo
12 12 |
13 |-foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
13 |+foo("Hello", U"world", a=u"Hello", b=u"world") # UP025
14 14 |
15 15 | # Retain quotes when fixing.
16 16 | x = u'hello' # UP025
UP025.py:12:15: UP025 [*] Remove unicode literals from strings
UP025.py:13:15: UP025 [*] Remove unicode literals from strings
|
10 | import foo
11 |
12 | foo(u"Hello", U"world", a=u"Hello", b=u"world")
11 | import foo
12 |
13 | foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
| ^^^^^^^^ UP025
13 |
14 | # These should stay quoted they way they are
14 |
15 | # Retain quotes when fixing.
|
= help: Remove unicode prefix
Safe fix
9 9 |
10 10 | import foo
11 11 |
12 |-foo(u"Hello", U"world", a=u"Hello", b=u"world")
12 |+foo(u"Hello", "world", a=u"Hello", b=u"world")
13 13 |
14 14 | # These should stay quoted they way they are
15 15 |
10 10 |
11 11 | import foo
12 12 |
13 |-foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
13 |+foo(u"Hello", "world", a=u"Hello", b=u"world") # UP025
14 14 |
15 15 | # Retain quotes when fixing.
16 16 | x = u'hello' # UP025
UP025.py:12:27: UP025 [*] Remove unicode literals from strings
UP025.py:13:27: UP025 [*] Remove unicode literals from strings
|
10 | import foo
11 |
12 | foo(u"Hello", U"world", a=u"Hello", b=u"world")
11 | import foo
12 |
13 | foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
| ^^^^^^^^ UP025
13 |
14 | # These should stay quoted they way they are
14 |
15 | # Retain quotes when fixing.
|
= help: Remove unicode prefix
Safe fix
9 9 |
10 10 | import foo
11 11 |
12 |-foo(u"Hello", U"world", a=u"Hello", b=u"world")
12 |+foo(u"Hello", U"world", a="Hello", b=u"world")
13 13 |
14 14 | # These should stay quoted they way they are
15 15 |
10 10 |
11 11 | import foo
12 12 |
13 |-foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
13 |+foo(u"Hello", U"world", a="Hello", b=u"world") # UP025
14 14 |
15 15 | # Retain quotes when fixing.
16 16 | x = u'hello' # UP025
UP025.py:12:39: UP025 [*] Remove unicode literals from strings
UP025.py:13:39: UP025 [*] Remove unicode literals from strings
|
10 | import foo
11 |
12 | foo(u"Hello", U"world", a=u"Hello", b=u"world")
11 | import foo
12 |
13 | foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
| ^^^^^^^^ UP025
13 |
14 | # These should stay quoted they way they are
14 |
15 | # Retain quotes when fixing.
|
= help: Remove unicode prefix
Safe fix
9 9 |
10 10 | import foo
11 11 |
12 |-foo(u"Hello", U"world", a=u"Hello", b=u"world")
12 |+foo(u"Hello", U"world", a=u"Hello", b="world")
13 13 |
14 14 | # These should stay quoted they way they are
15 15 |
10 10 |
11 11 | import foo
12 12 |
13 |-foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
13 |+foo(u"Hello", U"world", a=u"Hello", b="world") # UP025
14 14 |
15 15 | # Retain quotes when fixing.
16 16 | x = u'hello' # UP025
UP025.py:16:5: UP025 [*] Remove unicode literals from strings
|
14 | # These should stay quoted they way they are
15 |
16 | x = u'hello'
15 | # Retain quotes when fixing.
16 | x = u'hello' # UP025
| ^^^^^^^^ UP025
17 | x = u"""hello"""
18 | x = u'''hello'''
17 | x = u"""hello""" # UP025
18 | x = u'''hello''' # UP025
|
= help: Remove unicode prefix
Safe fix
13 13 |
14 14 | # These should stay quoted they way they are
15 15 |
16 |-x = u'hello'
16 |+x = 'hello'
17 17 | x = u"""hello"""
18 18 | x = u'''hello'''
19 19 | x = u'Hello "World"'
13 13 | foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
14 14 |
15 15 | # Retain quotes when fixing.
16 |-x = u'hello' # UP025
16 |+x = 'hello' # UP025
17 17 | x = u"""hello""" # UP025
18 18 | x = u'''hello''' # UP025
19 19 | x = u'Hello "World"' # UP025
UP025.py:17:5: UP025 [*] Remove unicode literals from strings
|
16 | x = u'hello'
17 | x = u"""hello"""
15 | # Retain quotes when fixing.
16 | x = u'hello' # UP025
17 | x = u"""hello""" # UP025
| ^^^^^^^^^^^^ UP025
18 | x = u'''hello'''
19 | x = u'Hello "World"'
18 | x = u'''hello''' # UP025
19 | x = u'Hello "World"' # UP025
|
= help: Remove unicode prefix
Safe fix
14 14 | # These should stay quoted they way they are
15 15 |
16 16 | x = u'hello'
17 |-x = u"""hello"""
17 |+x = """hello"""
18 18 | x = u'''hello'''
19 19 | x = u'Hello "World"'
14 14 |
15 15 | # Retain quotes when fixing.
16 16 | x = u'hello' # UP025
17 |-x = u"""hello""" # UP025
17 |+x = """hello""" # UP025
18 18 | x = u'''hello''' # UP025
19 19 | x = u'Hello "World"' # UP025
20 20 |
UP025.py:18:5: UP025 [*] Remove unicode literals from strings
|
16 | x = u'hello'
17 | x = u"""hello"""
18 | x = u'''hello'''
16 | x = u'hello' # UP025
17 | x = u"""hello""" # UP025
18 | x = u'''hello''' # UP025
| ^^^^^^^^^^^^ UP025
19 | x = u'Hello "World"'
19 | x = u'Hello "World"' # UP025
|
= help: Remove unicode prefix
Safe fix
15 15 |
16 16 | x = u'hello'
17 17 | x = u"""hello"""
18 |-x = u'''hello'''
18 |+x = '''hello'''
19 19 | x = u'Hello "World"'
15 15 | # Retain quotes when fixing.
16 16 | x = u'hello' # UP025
17 17 | x = u"""hello""" # UP025
18 |-x = u'''hello''' # UP025
18 |+x = '''hello''' # UP025
19 19 | x = u'Hello "World"' # UP025
20 20 |
21 21 | # These should not change
21 21 | u = "Hello" # OK
UP025.py:19:5: UP025 [*] Remove unicode literals from strings
|
17 | x = u"""hello"""
18 | x = u'''hello'''
19 | x = u'Hello "World"'
17 | x = u"""hello""" # UP025
18 | x = u'''hello''' # UP025
19 | x = u'Hello "World"' # UP025
| ^^^^^^^^^^^^^^^^ UP025
20 |
21 | # These should not change
21 | u = "Hello" # OK
|
= help: Remove unicode prefix
Safe fix
16 16 | x = u'hello'
17 17 | x = u"""hello"""
18 18 | x = u'''hello'''
19 |-x = u'Hello "World"'
19 |+x = 'Hello "World"'
16 16 | x = u'hello' # UP025
17 17 | x = u"""hello""" # UP025
18 18 | x = u'''hello''' # UP025
19 |-x = u'Hello "World"' # UP025
19 |+x = 'Hello "World"' # UP025
20 20 |
21 21 | # These should not change
22 22 | u = "Hello"
21 21 | u = "Hello" # OK
22 22 | u = u # OK
UP025.py:29:7: UP025 [*] Remove unicode literals from strings
UP025.py:27:7: UP025 [*] Remove unicode literals from strings
|
27 | return"Hello"
28 |
29 | f"foo"u"bar"
25 | return"Hello" # OK
26 |
27 | f"foo"u"bar" # OK
| ^^^^^^ UP025
30 | f"foo" u"bar"
28 | f"foo" u"bar" # OK
|
= help: Remove unicode prefix
Safe fix
26 26 | def hello():
27 27 | return"Hello"
28 28 |
29 |-f"foo"u"bar"
29 |+f"foo""bar"
30 30 | f"foo" u"bar"
24 24 | def hello():
25 25 | return"Hello" # OK
26 26 |
27 |-f"foo"u"bar" # OK
27 |+f"foo""bar" # OK
28 28 | f"foo" u"bar" # OK
UP025.py:30:8: UP025 [*] Remove unicode literals from strings
UP025.py:28:8: UP025 [*] Remove unicode literals from strings
|
29 | f"foo"u"bar"
30 | f"foo" u"bar"
27 | f"foo"u"bar" # OK
28 | f"foo" u"bar" # OK
| ^^^^^^ UP025
|
= help: Remove unicode prefix
Safe fix
27 27 | return"Hello"
28 28 |
29 29 | f"foo"u"bar"
30 |-f"foo" u"bar"
30 |+f"foo" "bar"
25 25 | return"Hello" # OK
26 26 |
27 27 | f"foo"u"bar" # OK
28 |-f"foo" u"bar" # OK
28 |+f"foo" "bar" # OK

View File

@@ -1141,6 +1141,7 @@ UP032_0.py:240:1: UP032 [*] Use f-string instead of `format` call
243 |+)
244 244 |
245 245 | ("{}" "{{{}}}").format(a, b)
246 246 |
UP032_0.py:245:1: UP032 [*] Use f-string instead of `format` call
|
@@ -1148,6 +1149,8 @@ UP032_0.py:245:1: UP032 [*] Use f-string instead of `format` call
244 |
245 | ("{}" "{{{}}}").format(a, b)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
246 |
247 | # The dictionary should be parenthesized.
|
= help: Convert to f-string
@@ -1157,5 +1160,63 @@ UP032_0.py:245:1: UP032 [*] Use f-string instead of `format` call
244 244 |
245 |-("{}" "{{{}}}").format(a, b)
245 |+(f"{a}" f"{{{b}}}")
246 246 |
247 247 | # The dictionary should be parenthesized.
248 248 | "{}".format({0: 1}[0])
UP032_0.py:248:1: UP032 [*] Use f-string instead of `format` call
|
247 | # The dictionary should be parenthesized.
248 | "{}".format({0: 1}[0])
| ^^^^^^^^^^^^^^^^^^^^^^ UP032
249 |
250 | # The dictionary should be parenthesized.
|
= help: Convert to f-string
Safe fix
245 245 | ("{}" "{{{}}}").format(a, b)
246 246 |
247 247 | # The dictionary should be parenthesized.
248 |-"{}".format({0: 1}[0])
248 |+f"{({0: 1}[0])}"
249 249 |
250 250 | # The dictionary should be parenthesized.
251 251 | "{}".format({0: 1}.bar)
UP032_0.py:251:1: UP032 [*] Use f-string instead of `format` call
|
250 | # The dictionary should be parenthesized.
251 | "{}".format({0: 1}.bar)
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
252 |
253 | # The dictionary should be parenthesized.
|
= help: Convert to f-string
Safe fix
248 248 | "{}".format({0: 1}[0])
249 249 |
250 250 | # The dictionary should be parenthesized.
251 |-"{}".format({0: 1}.bar)
251 |+f"{({0: 1}.bar)}"
252 252 |
253 253 | # The dictionary should be parenthesized.
254 254 | "{}".format({0: 1}())
UP032_0.py:254:1: UP032 [*] Use f-string instead of `format` call
|
253 | # The dictionary should be parenthesized.
254 | "{}".format({0: 1}())
| ^^^^^^^^^^^^^^^^^^^^^ UP032
|
= help: Convert to f-string
Safe fix
251 251 | "{}".format({0: 1}.bar)
252 252 |
253 253 | # The dictionary should be parenthesized.
254 |-"{}".format({0: 1}())
254 |+f"{({0: 1}())}"

View File

@@ -44,6 +44,7 @@ mod tests {
#[test_case(Rule::QuadraticListSummation, Path::new("RUF017_0.py"))]
#[test_case(Rule::AssignmentInAssert, Path::new("RUF018.py"))]
#[test_case(Rule::UnnecessaryKeyCheck, Path::new("RUF019.py"))]
#[test_case(Rule::NeverUnion, Path::new("RUF020.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -112,11 +112,15 @@ pub(crate) fn asyncio_dangling_binding(
for binding_id in scope.binding_ids() {
// If the binding itself is used, or it's not an assignment, skip it.
let binding = semantic.binding(binding_id);
if binding.is_used() || !binding.kind.is_assignment() {
if binding.is_used()
|| binding.is_global()
|| binding.is_nonlocal()
|| !binding.kind.is_assignment()
{
continue;
}
// Otherwise, any dangling tasks, including those that are shadowed, as in:
// Otherwise, flag any dangling tasks, including those that are shadowed, as in:
// ```python
// if x > 0:
// task = asyncio.create_task(make_request())
@@ -127,7 +131,11 @@ pub(crate) fn asyncio_dangling_binding(
std::iter::successors(Some(binding_id), |id| semantic.shadowed_binding(*id))
{
let binding = semantic.binding(binding_id);
if binding.is_used() || !binding.kind.is_assignment() {
if binding.is_used()
|| binding.is_global()
|| binding.is_nonlocal()
|| !binding.kind.is_assignment()
{
continue;
}

View File

@@ -9,7 +9,9 @@ pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use mutable_class_default::*;
pub(crate) use mutable_dataclass_default::*;
pub(crate) use never_union::*;
pub(crate) use pairwise_over_zipped::*;
pub(crate) use quadratic_list_summation::*;
pub(crate) use static_key_dict_comprehension::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
@@ -30,6 +32,7 @@ mod invalid_index_type;
mod invalid_pyproject_toml;
mod mutable_class_default;
mod mutable_dataclass_default;
mod never_union;
mod pairwise_over_zipped;
mod static_key_dict_comprehension;
mod unnecessary_iterable_allocation_for_first_element;
@@ -44,6 +47,5 @@ pub(crate) enum Context {
Docstring,
Comment,
}
pub(crate) use quadratic_list_summation::*;
mod quadratic_list_summation;

View File

@@ -0,0 +1,212 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `typing.NoReturn` and `typing.Never` in union types.
///
/// ## Why is this bad?
/// `typing.NoReturn` and `typing.Never` are special types, used to indicate
/// that a function never returns, or that a type has no values.
///
/// Including `typing.NoReturn` or `typing.Never` in a union type is redundant,
/// as, e.g., `typing.Never | T` is equivalent to `T`.
///
/// ## Example
/// ```python
/// from typing import Never
///
///
/// def func() -> Never | int:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def func() -> int:
/// ...
/// ```
///
/// ## Options
/// - [Python documentation: `typing.Never`](https://docs.python.org/3/library/typing.html#typing.Never)
/// - [Python documentation: `typing.NoReturn`](https://docs.python.org/3/library/typing.html#typing.NoReturn)
#[violation]
pub struct NeverUnion {
never_like: NeverLike,
union_like: UnionLike,
}
impl AlwaysFixableViolation for NeverUnion {
#[derive_message_formats]
fn message(&self) -> String {
let Self {
never_like,
union_like,
} = self;
match union_like {
UnionLike::BinOp => {
format!("`{never_like} | T` is equivalent to `T`")
}
UnionLike::TypingUnion => {
format!("`Union[{never_like}, T]` is equivalent to `T`")
}
}
}
fn fix_title(&self) -> String {
let Self { never_like, .. } = self;
format!("Remove `{never_like}`")
}
}
/// RUF020
pub(crate) fn never_union(checker: &mut Checker, expr: &Expr) {
match expr {
// Ex) `typing.NoReturn | int`
Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
left,
right,
range: _,
}) => {
// Analyze the left-hand side of the `|` operator.
if let Some(never_like) = NeverLike::from_expr(left, checker.semantic()) {
let mut diagnostic = Diagnostic::new(
NeverUnion {
never_like,
union_like: UnionLike::BinOp,
},
left.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
checker.locator().slice(right.as_ref()).to_string(),
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}
// Analyze the right-hand side of the `|` operator.
if let Some(never_like) = NeverLike::from_expr(right, checker.semantic()) {
let mut diagnostic = Diagnostic::new(
NeverUnion {
never_like,
union_like: UnionLike::BinOp,
},
right.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
checker.locator().slice(left.as_ref()).to_string(),
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}
}
// Ex) `typing.Union[typing.NoReturn, int]`
Expr::Subscript(ast::ExprSubscript {
value,
slice,
ctx: _,
range: _,
}) if checker.semantic().match_typing_expr(value, "Union") => {
let Expr::Tuple(ast::ExprTuple {
elts,
ctx: _,
range: _,
}) = slice.as_ref()
else {
return;
};
// Analyze each element of the `Union`.
for elt in elts {
if let Some(never_like) = NeverLike::from_expr(elt, checker.semantic()) {
// Collect the other elements of the `Union`.
let rest = elts
.iter()
.filter(|other| *other != elt)
.cloned()
.collect::<Vec<_>>();
// Ignore, e.g., `typing.Union[typing.NoReturn]`.
if rest.is_empty() {
return;
}
let mut diagnostic = Diagnostic::new(
NeverUnion {
never_like,
union_like: UnionLike::TypingUnion,
},
elt.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
if let [only] = rest.as_slice() {
// Ex) `typing.Union[typing.NoReturn, int]` -> `int`
checker.locator().slice(only).to_string()
} else {
// Ex) `typing.Union[typing.NoReturn, int, str]` -> `typing.Union[int, str]`
checker
.generator()
.expr(&Expr::Subscript(ast::ExprSubscript {
value: value.clone(),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts: rest,
ctx: ast::ExprContext::Load,
range: TextRange::default(),
})),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
}))
},
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}
}
}
_ => {}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UnionLike {
/// E.g., `typing.Union[int, str]`
TypingUnion,
/// E.g., `int | str`
BinOp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NeverLike {
/// E.g., `typing.NoReturn`
NoReturn,
/// E.g., `typing.Never`
Never,
}
impl NeverLike {
fn from_expr(expr: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> Option<Self> {
let call_path = semantic.resolve_call_path(expr)?;
if semantic.match_typing_call_path(&call_path, "NoReturn") {
Some(NeverLike::NoReturn)
} else if semantic.match_typing_call_path(&call_path, "Never") {
Some(NeverLike::Never)
} else {
None
}
}
}
impl std::fmt::Display for NeverLike {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NeverLike::NoReturn => f.write_str("NoReturn"),
NeverLike::Never => f.write_str("Never"),
}
}
}

View File

@@ -0,0 +1,137 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF020.py:3:7: RUF020 [*] `Union[Never, T]` is equivalent to `T`
|
1 | from typing import Never, NoReturn, Union
2 |
3 | Union[Never, int]
| ^^^^^ RUF020
4 | Union[NoReturn, int]
5 | Never | int
|
= help: Remove `Never`
Safe fix
1 1 | from typing import Never, NoReturn, Union
2 2 |
3 |-Union[Never, int]
3 |+int
4 4 | Union[NoReturn, int]
5 5 | Never | int
6 6 | NoReturn | int
RUF020.py:4:7: RUF020 [*] `Union[NoReturn, T]` is equivalent to `T`
|
3 | Union[Never, int]
4 | Union[NoReturn, int]
| ^^^^^^^^ RUF020
5 | Never | int
6 | NoReturn | int
|
= help: Remove `NoReturn`
Safe fix
1 1 | from typing import Never, NoReturn, Union
2 2 |
3 3 | Union[Never, int]
4 |-Union[NoReturn, int]
4 |+int
5 5 | Never | int
6 6 | NoReturn | int
7 7 | Union[Union[Never, int], Union[NoReturn, int]]
RUF020.py:5:1: RUF020 [*] `Never | T` is equivalent to `T`
|
3 | Union[Never, int]
4 | Union[NoReturn, int]
5 | Never | int
| ^^^^^ RUF020
6 | NoReturn | int
7 | Union[Union[Never, int], Union[NoReturn, int]]
|
= help: Remove `Never`
Safe fix
2 2 |
3 3 | Union[Never, int]
4 4 | Union[NoReturn, int]
5 |-Never | int
5 |+int
6 6 | NoReturn | int
7 7 | Union[Union[Never, int], Union[NoReturn, int]]
8 8 | Union[NoReturn, int, float]
RUF020.py:6:1: RUF020 [*] `NoReturn | T` is equivalent to `T`
|
4 | Union[NoReturn, int]
5 | Never | int
6 | NoReturn | int
| ^^^^^^^^ RUF020
7 | Union[Union[Never, int], Union[NoReturn, int]]
8 | Union[NoReturn, int, float]
|
= help: Remove `NoReturn`
Safe fix
3 3 | Union[Never, int]
4 4 | Union[NoReturn, int]
5 5 | Never | int
6 |-NoReturn | int
6 |+int
7 7 | Union[Union[Never, int], Union[NoReturn, int]]
8 8 | Union[NoReturn, int, float]
RUF020.py:7:13: RUF020 [*] `Union[Never, T]` is equivalent to `T`
|
5 | Never | int
6 | NoReturn | int
7 | Union[Union[Never, int], Union[NoReturn, int]]
| ^^^^^ RUF020
8 | Union[NoReturn, int, float]
|
= help: Remove `Never`
Safe fix
4 4 | Union[NoReturn, int]
5 5 | Never | int
6 6 | NoReturn | int
7 |-Union[Union[Never, int], Union[NoReturn, int]]
7 |+Union[int, Union[NoReturn, int]]
8 8 | Union[NoReturn, int, float]
RUF020.py:7:32: RUF020 [*] `Union[NoReturn, T]` is equivalent to `T`
|
5 | Never | int
6 | NoReturn | int
7 | Union[Union[Never, int], Union[NoReturn, int]]
| ^^^^^^^^ RUF020
8 | Union[NoReturn, int, float]
|
= help: Remove `NoReturn`
Safe fix
4 4 | Union[NoReturn, int]
5 5 | Never | int
6 6 | NoReturn | int
7 |-Union[Union[Never, int], Union[NoReturn, int]]
7 |+Union[Union[Never, int], int]
8 8 | Union[NoReturn, int, float]
RUF020.py:8:7: RUF020 [*] `Union[NoReturn, T]` is equivalent to `T`
|
6 | NoReturn | int
7 | Union[Union[Never, int], Union[NoReturn, int]]
8 | Union[NoReturn, int, float]
| ^^^^^^^^ RUF020
|
= help: Remove `NoReturn`
Safe fix
5 5 | Never | int
6 6 | NoReturn | int
7 7 | Union[Union[Never, int], Union[NoReturn, int]]
8 |-Union[NoReturn, int, float]
8 |+Union[int, float]

View File

@@ -21,7 +21,7 @@ use ruff_text_size::Ranged;
use crate::directives;
use crate::fix::{fix_file, FixResult};
use crate::linter::{check_path, LinterResult};
use crate::linter::{check_path, LinterResult, TokenSource};
use crate::message::{Emitter, EmitterContext, Message, TextEmitter};
use crate::packaging::detect_package_root;
use crate::registry::AsRule;
@@ -129,7 +129,6 @@ pub(crate) fn test_contents<'a>(
path,
path.parent()
.and_then(|parent| detect_package_root(parent, &settings.namespace_packages)),
tokens,
&locator,
&stylist,
&indexer,
@@ -138,6 +137,7 @@ pub(crate) fn test_contents<'a>(
flags::Noqa::Enabled,
source_kind,
source_type,
TokenSource::Tokens(tokens),
);
let source_has_errors = error.is_some();
@@ -195,7 +195,6 @@ pub(crate) fn test_contents<'a>(
} = check_path(
path,
None,
tokens,
&locator,
&stylist,
&indexer,
@@ -204,6 +203,7 @@ pub(crate) fn test_contents<'a>(
flags::Noqa::Enabled,
&transformed,
source_type,
TokenSource::Tokens(tokens),
);
if let Some(fixed_error) = fixed_error {

View File

@@ -1505,6 +1505,7 @@ pub fn pep_604_union(elts: &[Expr]) -> Expr {
}
}
/// Format the expression as a `typing.Optional`-style optional.
pub fn typing_optional(elt: Expr, binding: String) -> Expr {
Expr::Subscript(ast::ExprSubscript {
value: Box::new(Expr::Name(ast::ExprName {
@@ -1518,18 +1519,19 @@ pub fn typing_optional(elt: Expr, binding: String) -> Expr {
})
}
/// Format the expressions as a `typing.Union`-style union.
pub fn typing_union(elts: &[Expr], binding: String) -> Expr {
fn tuple(elts: &[Expr]) -> Expr {
fn tuple(elts: &[Expr], binding: String) -> Expr {
match elts {
[] => Expr::Tuple(ast::ExprTuple {
elts: vec![],
ctx: ExprContext::Load,
range: TextRange::default(),
}),
[Expr::Tuple(ast::ExprTuple { elts, .. })] => pep_604_union(elts),
[Expr::Tuple(ast::ExprTuple { elts, .. })] => typing_union(elts, binding),
[elt] => elt.clone(),
[rest @ .., elt] => Expr::BinOp(ast::ExprBinOp {
left: Box::new(tuple(rest)),
left: Box::new(tuple(rest, binding)),
op: Operator::BitOr,
right: Box::new(elt.clone()),
range: TextRange::default(),
@@ -1539,11 +1541,11 @@ pub fn typing_union(elts: &[Expr], binding: String) -> Expr {
Expr::Subscript(ast::ExprSubscript {
value: Box::new(Expr::Name(ast::ExprName {
id: binding,
id: binding.clone(),
range: TextRange::default(),
ctx: ExprContext::Load,
})),
slice: Box::new(tuple(elts)),
slice: Box::new(tuple(elts, binding)),
ctx: ExprContext::Load,
range: TextRange::default(),
})

View File

@@ -253,4 +253,23 @@ if True:
# empty line(s) at the end of the file due to nested function
if True:
def nested_trailing_function():
pass
pass
def overload1(): ... # trailing comment
def overload1(a: int): ...
def overload2(): ... # trailing comment
def overload2(a: int): ...
def overload3():
...
# trailing comment
def overload3(a: int): ...
def overload4():
...
# trailing comment
def overload4(a: int): ...

View File

@@ -0,0 +1,157 @@
x1: A[b] | EventHandler | EventSpec | list[EventHandler | EventSpec] | Other | More | AndMore | None = None
x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]"
x6: VeryLongClassNameWithAwkwardGenericSubtype[
integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer,
VeryLongClassNameWithAwkwardGenericSubtype,
str
] = True
x7: CustomTrainingJob | CustomContainerTrainingJob | CustomPythonPackageTrainingJob
x8: (
None
| datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset
) = None
x9: None | (
datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset
) = None
x10: (
aaaaaaaaaaaaaaaaaaaaaaaa[
bbbbbbbbbbb,
Subscript
| None
| datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset,
],
bbb[other],
) = None
x11: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] = None
x12: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x13: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x14: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] = None
x15: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x16: None | Literal[
"split",
"a bit longer",
"records",
"index",
"table",
"columns",
"values",
] = None
x17: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
]
class Test:
safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff
applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes.
string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation
##########
# Comments
leading: (
# Leading comment
None | dataset.ImageDataset
)
leading_with_value: (
# Leading comment
None
| dataset.ImageDataset
) = None
leading_open_parentheses: ( # Leading comment
None
| dataset.ImageDataset
)
leading_open_parentheses_with_value: ( # Leading comment
None
| dataset.ImageDataset
) = None
trailing: (
None | dataset.ImageDataset # trailing comment
)
trailing_with_value: (
None | dataset.ImageDataset # trailing comment
) = None
trailing_own_line: (
None | dataset.ImageDataset
# trailing own line
)
trailing_own_line_with_value: (
None | dataset.ImageDataset
# trailing own line
) = None
nested_comment: None | [
# a list of strings
str
] = None

View File

@@ -0,0 +1,9 @@
[
{
"target_version": "py38"
},
{
"target_version": "py39",
"preview": "enabled"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"target_version": "py39",
"preview": "enabled"
}
]

View File

@@ -0,0 +1,88 @@
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
# Black avoids parenthesizing the with because it can make all with items fit by just breaking
# around parentheses. We don't implement this optimisation because it makes it difficult to see where
# the different context managers start and end.
with cmd, xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
) as cmd, another, and_more as x:
pass
# Avoid parenthesizing single item context managers when splitting after the parentheses (can_omit_optional_parentheses)
# is sufficient
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method(): pass
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
# Black avoids parentheses here because it can make the entire with
# header fit without requiring parentheses to do so.
# We don't implement this optimisation because it very difficult to see where
# the different context managers start or end.
with cmd, xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
) as cmd, another, and_more as x:
pass
# Avoid parenthesizing single item context managers when splitting after the parentheses
# is sufficient
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method(): pass
# Parenthesize the with items if it makes them fit. Breaking the binary expression isn't
# necessary because the entire items fit just into the 88 character limit.
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
pass
# Black parenthesizes this binary expression but also preserves the parentheses of the first with-item.
# It does so because it prefers splitting already parenthesized context managers, even if it leads to more parentheses
# like in this case.
with (
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) as b,
c as d,
):
pass
if True:
with anyio.CancelScope(shield=True) if get_running_loop() else contextlib.nullcontext():
pass
with (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c):
pass

View File

@@ -529,7 +529,7 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Expr {
///
/// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820)
#[allow(clippy::if_same_then_else)]
fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool {
pub(crate) fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool {
let mut visitor = CanOmitOptionalParenthesesVisitor::new(context);
visitor.visit_subexpression(expr);
@@ -538,8 +538,8 @@ fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool
false
} else if visitor.max_precedence_count > 1 {
false
} else if visitor.max_precedence == OperatorPrecedence::None && expr.is_lambda_expr() {
// Micha: This seems to exclusively apply for lambda expressions where the body ends in a subscript.
} else if visitor.max_precedence == OperatorPrecedence::None {
// Micha: This seems to apply for lambda expressions where the body ends in a subscript.
// Subscripts are excluded by default because breaking them looks odd, but it seems to be fine for lambda expression.
//
// ```python
@@ -566,10 +566,19 @@ fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool
// ]
// )
// ```
//
// Another case are method chains:
// ```python
// xxxxxxxx.some_kind_of_method(
// some_argument=[
// "first",
// "second",
// "third",
// ]
// ).another_method(a)
// ```
true
} else if visitor.max_precedence == OperatorPrecedence::Attribute
&& (expr.is_lambda_expr() || expr.is_named_expr_expr())
{
} else if visitor.max_precedence == OperatorPrecedence::Attribute {
// A single method call inside a named expression (`:=`) or as the body of a lambda function:
// ```python
// kwargs["open_with"] = lambda path, _: fsspec.open(
@@ -1195,3 +1204,75 @@ impl From<Operator> for OperatorPrecedence {
}
}
}
/// Returns `true` if `expr` is an expression that can be split into multiple lines.
///
/// Returns `false` for expressions that are guaranteed to never split.
pub(crate) fn is_splittable_expression(expr: &Expr, context: &PyFormatContext) -> bool {
match expr {
// Single token expressions. They never have any split points.
Expr::NamedExpr(_)
| Expr::Name(_)
| Expr::NumberLiteral(_)
| Expr::BooleanLiteral(_)
| Expr::NoneLiteral(_)
| Expr::EllipsisLiteral(_)
| Expr::Slice(_)
| Expr::IpyEscapeCommand(_) => false,
// Expressions that insert split points when parenthesized.
Expr::Compare(_)
| Expr::BinOp(_)
| Expr::BoolOp(_)
| Expr::IfExp(_)
| Expr::GeneratorExp(_)
| Expr::Subscript(_)
| Expr::Await(_)
| Expr::ListComp(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
| Expr::YieldFrom(_) => true,
// Sequence types can split if they contain at least one element.
Expr::Tuple(tuple) => !tuple.elts.is_empty(),
Expr::Dict(dict) => !dict.values.is_empty(),
Expr::Set(set) => !set.elts.is_empty(),
Expr::List(list) => !list.elts.is_empty(),
Expr::UnaryOp(unary) => is_splittable_expression(unary.operand.as_ref(), context),
Expr::Yield(ast::ExprYield { value, .. }) => value.is_some(),
Expr::Call(ast::ExprCall {
arguments, func, ..
}) => {
!arguments.is_empty()
|| is_expression_parenthesized(
func.as_ref().into(),
context.comments().ranges(),
context.source(),
)
}
// String like literals can expand if they are implicit concatenated.
Expr::FString(fstring) => fstring.value.is_implicit_concatenated(),
Expr::StringLiteral(string) => string.value.is_implicit_concatenated(),
Expr::BytesLiteral(bytes) => bytes.value.is_implicit_concatenated(),
// Expressions that have no split points per se, but they contain nested sub expressions that might expand.
Expr::Lambda(ast::ExprLambda {
body: expression, ..
})
| Expr::Starred(ast::ExprStarred {
value: expression, ..
})
| Expr::Attribute(ast::ExprAttribute {
value: expression, ..
}) => {
is_expression_parenthesized(
expression.into(),
context.comments().ranges(),
context.source(),
) || is_splittable_expression(expression.as_ref(), context)
}
}
}

View File

@@ -1,5 +1,4 @@
use ruff_formatter::write;
use ruff_python_ast::WithItem;
use crate::comments::SourceComment;
@@ -8,6 +7,7 @@ use crate::expression::parentheses::{
is_expression_parenthesized, parenthesized, Parentheses, Parenthesize,
};
use crate::prelude::*;
use crate::preview::is_wrap_multiple_context_managers_in_parens_enabled;
#[derive(Default)]
pub struct FormatWithItem;
@@ -23,26 +23,49 @@ impl FormatNodeRule<WithItem> for FormatWithItem {
let comments = f.context().comments().clone();
let trailing_as_comments = comments.dangling(item);
// Prefer keeping parentheses for already parenthesized expressions over
// parenthesizing other nodes.
let parenthesize = if is_expression_parenthesized(
let is_parenthesized = is_expression_parenthesized(
context_expr.into(),
f.context().comments().ranges(),
f.context().source(),
) {
Parenthesize::IfBreaks
} else {
Parenthesize::IfRequired
};
);
write!(
f,
[maybe_parenthesize_expression(
context_expr,
item,
parenthesize
)]
)?;
// Remove the parentheses of the `with_items` if the with statement adds parentheses
if f.context().node_level().is_parenthesized()
&& is_wrap_multiple_context_managers_in_parens_enabled(f.context())
{
if is_parenthesized {
// ...except if the with item is parenthesized, then use this with item as a preferred breaking point
// or when it has comments, then parenthesize it to prevent comments from moving.
maybe_parenthesize_expression(
context_expr,
item,
Parenthesize::IfBreaksOrIfRequired,
)
.fmt(f)?;
} else {
context_expr
.format()
.with_options(Parentheses::Never)
.fmt(f)?;
}
} else {
// Prefer keeping parentheses for already parenthesized expressions over
// parenthesizing other nodes.
let parenthesize = if is_parenthesized {
Parenthesize::IfBreaks
} else {
Parenthesize::IfRequired
};
write!(
f,
[maybe_parenthesize_expression(
context_expr,
item,
parenthesize
)]
)?;
}
if let Some(optional_vars) = optional_vars {
write!(f, [space(), token("as"), space()])?;

View File

@@ -25,6 +25,11 @@ pub(crate) const fn is_prefer_splitting_right_hand_side_of_assignments_enabled(
context.is_preview()
}
/// Returns `true` if the [`parenthesize_long_type_hints`](https://github.com/astral-sh/ruff/issues/8894) preview style is enabled.
pub(crate) const fn is_parenthesize_long_type_hints_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
/// Returns `true` if the [`no_blank_line_before_class_docstring`] preview style is enabled.
///
/// [`no_blank_line_before_class_docstring`]: https://github.com/astral-sh/ruff/issues/8888
@@ -33,3 +38,22 @@ pub(crate) const fn is_no_blank_line_before_class_docstring_enabled(
) -> bool {
context.is_preview()
}
/// Returns `true` if the [`wrap_multiple_context_managers_in_parens`](https://github.com/astral-sh/ruff/issues/8889) preview style is enabled.
///
/// Unlike Black, we re-use the same preview style feature flag for [`improved_async_statements_handling`](https://github.com/astral-sh/ruff/issues/8890)
pub(crate) const fn is_wrap_multiple_context_managers_in_parens_enabled(
context: &PyFormatContext,
) -> bool {
context.is_preview()
}
/// Returns `true` if the [`module_docstring_newlines`](https://github.com/astral-sh/ruff/issues/7995) preview style is enabled.
pub(crate) const fn is_module_docstring_newlines_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
/// Returns `true` if the [`dummy_implementations`](https://github.com/astral-sh/ruff/issues/8357) preview style is enabled.
pub(crate) const fn is_dummy_implementations_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}

View File

@@ -11,6 +11,7 @@ use crate::comments::{
leading_alternate_branch_comments, trailing_comments, SourceComment, SuppressionKind,
};
use crate::prelude::*;
use crate::preview::is_dummy_implementations_enabled;
use crate::statement::suite::{contains_only_an_ellipsis, SuiteKind};
use crate::verbatim::write_suppressed_clause_header;
@@ -391,9 +392,13 @@ pub(crate) fn clause_body<'a>(
impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
// 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())
// In stable, stubs are only collapsed in stub files, in preview stubs in functions
// or classes are collapsed too
let should_collapse_stub = f.options().source_type().is_stub()
|| (is_dummy_implementations_enabled(f.context())
&& matches!(self.kind, SuiteKind::Function | SuiteKind::Class));
if should_collapse_stub
&& contains_only_an_ellipsis(self.body, f.context().comments())
&& self.trailing_comments.is_empty()
{

View File

@@ -2,9 +2,13 @@ use ruff_formatter::write;
use ruff_python_ast::StmtAnnAssign;
use crate::comments::{SourceComment, SuppressionKind};
use crate::expression::has_parentheses;
use crate::expression::parentheses::Parentheses;
use crate::expression::{has_parentheses, is_splittable_expression};
use crate::prelude::*;
use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled;
use crate::preview::{
is_parenthesize_long_type_hints_enabled,
is_prefer_splitting_right_hand_side_of_assignments_enabled,
};
use crate::statement::stmt_assign::{
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
};
@@ -27,7 +31,11 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
if let Some(value) = value {
if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context())
&& has_parentheses(annotation, f.context()).is_some()
// The `has_parentheses` check can be removed when stabilizing `is_parenthesize_long_type_hints`.
// because `is_splittable_expression` covers both.
&& (has_parentheses(annotation, f.context()).is_some()
|| (is_parenthesize_long_type_hints_enabled(f.context())
&& is_splittable_expression(annotation, f.context())))
{
FormatStatementsLastExpression::RightToLeft {
before_operator: AnyBeforeOperator::Expression(annotation),
@@ -37,10 +45,28 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
}
.fmt(f)?;
} else {
// Remove unnecessary parentheses around the annotation if the parenthesize long type hints preview style is enabled.
// Ensure we keep the parentheses if the annotation has any comments.
if is_parenthesize_long_type_hints_enabled(f.context()) {
if f.context().comments().has_leading(annotation.as_ref())
|| f.context().comments().has_trailing(annotation.as_ref())
{
annotation
.format()
.with_options(Parentheses::Always)
.fmt(f)?;
} else {
annotation
.format()
.with_options(Parentheses::Never)
.fmt(f)?;
}
} else {
annotation.format().fmt(f)?;
}
write!(
f,
[
annotation.format(),
space(),
token("="),
space(),
@@ -49,7 +75,19 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
)?;
}
} else {
annotation.format().fmt(f)?;
// Parenthesize the value and inline the comment if it is a "simple" type annotation, similar
// to what we do with the value.
// ```python
// class Test:
// safe_age: (
// Decimal # the user's age, used to determine if it's safe for them to use ruff
// )
// ```
if is_parenthesize_long_type_hints_enabled(f.context()) {
FormatStatementsLastExpression::left_to_right(annotation, item).fmt(f)?;
} else {
annotation.format().fmt(f)?;
}
}
if f.options().source_type().is_ipynb()

View File

@@ -9,11 +9,18 @@ use crate::comments::{
};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::parentheses::{
is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize,
is_expression_parenthesized, optional_parentheses, NeedsParentheses, OptionalParentheses,
Parentheses, Parenthesize,
};
use crate::expression::{
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
maybe_parenthesize_expression,
};
use crate::expression::{has_own_parentheses, has_parentheses, maybe_parenthesize_expression};
use crate::prelude::*;
use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled;
use crate::preview::{
is_parenthesize_long_type_hints_enabled,
is_prefer_splitting_right_hand_side_of_assignments_enabled,
};
use crate::statement::trailing_semicolon;
#[derive(Default)]
@@ -686,8 +693,17 @@ impl Format<PyFormatContext<'_>> for AnyBeforeOperator<'_> {
}
// Never parenthesize targets that come with their own parentheses, e.g. don't parenthesize lists or dictionary literals.
else if should_parenthesize_target(expression, f.context()) {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
if is_parenthesize_long_type_hints_enabled(f.context())
&& can_omit_optional_parentheses(expression, f.context())
{
optional_parentheses(&expression.format().with_options(Parentheses::Never))
.fmt(f)
} else {
parenthesize_if_expands(
&expression.format().with_options(Parentheses::Never),
)
.fmt(f)
}
} else {
expression.format().with_options(Parentheses::Never).fmt(f)
}

View File

@@ -6,17 +6,21 @@ use ruff_text_size::{Ranged, TextRange};
use crate::builders::parenthesize_if_expands;
use crate::comments::SourceComment;
use crate::expression::parentheses::parenthesized;
use crate::expression::can_omit_optional_parentheses;
use crate::expression::parentheses::{
is_expression_parenthesized, optional_parentheses, parenthesized,
};
use crate::other::commas;
use crate::prelude::*;
use crate::preview::is_wrap_multiple_context_managers_in_parens_enabled;
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
use crate::PyFormatOptions;
use crate::{PyFormatOptions, PythonVersion};
#[derive(Default)]
pub struct FormatStmtWith;
impl FormatNodeRule<StmtWith> for FormatStmtWith {
fn fmt_fields(&self, item: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> {
fn fmt_fields(&self, with_stmt: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> {
// The `with` statement can have one dangling comment on the open parenthesis, like:
// ```python
// with ( # comment
@@ -31,9 +35,10 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
// ...
// ```
let comments = f.context().comments().clone();
let dangling_comments = comments.dangling(item.as_any_node_ref());
let dangling_comments = comments.dangling(with_stmt.as_any_node_ref());
let partition_point = dangling_comments.partition_point(|comment| {
item.items
with_stmt
.items
.first()
.is_some_and(|with_item| with_item.start() > comment.start())
});
@@ -43,35 +48,26 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
f,
[
clause_header(
ClauseHeader::With(item),
ClauseHeader::With(with_stmt),
colon_comments,
&format_with(|f| {
write!(
f,
[
item.is_async
with_stmt
.is_async
.then_some(format_args![token("async"), space()]),
token("with"),
space()
]
)?;
if !parenthesized_comments.is_empty() {
let joined = format_with(|f: &mut PyFormatter| {
f.join_comma_separated(item.body.first().unwrap().start())
.nodes(&item.items)
.finish()
});
parenthesized("(", &joined, ")")
.with_dangling_comments(parenthesized_comments)
.fmt(f)?;
} else if should_parenthesize(item, f.options(), f.context())? {
parenthesize_if_expands(&format_with(|f| {
if parenthesized_comments.is_empty() {
let format_items = format_with(|f| {
let mut joiner =
f.join_comma_separated(item.body.first().unwrap().start());
f.join_comma_separated(with_stmt.body.first().unwrap().start());
for item in &item.items {
for item in &with_stmt.items {
joiner.entry_with_line_separator(
item,
&item.format(),
@@ -79,26 +75,48 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
);
}
joiner.finish()
}))
.fmt(f)?;
} else if let [item] = item.items.as_slice() {
// This is similar to `maybe_parenthesize_expression`, but we're not
// dealing with an expression here, it's a `WithItem`.
if comments.has_leading(item) || comments.has_trailing(item) {
parenthesized("(", &item.format(), ")").fmt(f)?;
} else {
item.format().fmt(f)?;
});
match should_parenthesize(with_stmt, f.options(), f.context())? {
ParenthesizeWith::Optional => {
optional_parentheses(&format_items).fmt(f)?;
}
ParenthesizeWith::IfExpands => {
parenthesize_if_expands(&format_items).fmt(f)?;
}
ParenthesizeWith::UnlessCommented => {
if let [item] = with_stmt.items.as_slice() {
// This is similar to `maybe_parenthesize_expression`, but we're not
// dealing with an expression here, it's a `WithItem`.
if comments.has_leading(item) || comments.has_trailing(item)
{
parenthesized("(", &item.format(), ")").fmt(f)?;
} else {
item.format().fmt(f)?;
}
} else {
f.join_with(format_args![token(","), space()])
.entries(with_stmt.items.iter().formatted())
.finish()?;
}
}
}
} else {
f.join_with(format_args![token(","), space()])
.entries(item.items.iter().formatted())
.finish()?;
let joined = format_with(|f: &mut PyFormatter| {
f.join_comma_separated(with_stmt.body.first().unwrap().start())
.nodes(&with_stmt.items)
.finish()
});
parenthesized("(", &joined, ")")
.with_dangling_comments(parenthesized_comments)
.fmt(f)?;
}
Ok(())
})
),
clause_body(&item.body, colon_comments)
clause_body(&with_stmt.body, colon_comments)
]
)
}
@@ -113,24 +131,79 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
}
}
/// Returns `true` if the `with` items should be parenthesized, if at least one item expands.
/// Determines whether the `with` items should be parenthesized (over parenthesizing each item),
/// and if so, which parenthesizing layout to use.
///
/// Black parenthesizes `with` items if there's more than one item and they're already
/// parenthesized, _or_ there's a single item with a trailing comma.
/// Parenthesize `with` items if
/// * The last item has a trailing comma (implying that the with items were parenthesized in the source)
/// * There's more than one item and they're already parenthesized
/// * There's more than one item, the [`wrap_multiple_context_managers_in_parens`](is_wrap_multiple_context_managers_in_parens) preview style is enabled,
/// and the target python version is >= 3.9
/// * There's a single non-parenthesized item. The function returns [`ParenthesizeWith::Optional`]
/// if the parentheses can be omitted if breaking around parenthesized sub-expressions is sufficient
/// to make the expression fit. It returns [`ParenthesizeWith::IfExpands`] otherwise.
/// * The only item is parenthesized and has comments.
fn should_parenthesize(
with: &StmtWith,
options: &PyFormatOptions,
context: &PyFormatContext,
) -> FormatResult<bool> {
) -> FormatResult<ParenthesizeWith> {
if has_magic_trailing_comma(with, options, context) {
return Ok(true);
return Ok(ParenthesizeWith::IfExpands);
}
if are_with_items_parenthesized(with, context)? {
return Ok(true);
let can_parenthesize = (is_wrap_multiple_context_managers_in_parens_enabled(context)
&& options.target_version() >= PythonVersion::Py39)
|| are_with_items_parenthesized(with, context)?;
if !can_parenthesize {
return Ok(ParenthesizeWith::UnlessCommented);
}
Ok(false)
if let [single] = with.items.as_slice() {
return Ok(
// If the with item itself has comments (not the context expression), then keep the parentheses
if context.comments().has_leading(single) || context.comments().has_trailing(single) {
ParenthesizeWith::IfExpands
}
// If it is the only expression and it has comments, then the with statement
// as well as the with item add parentheses
else if is_expression_parenthesized(
(&single.context_expr).into(),
context.comments().ranges(),
context.source(),
) {
// Preserve the parentheses around the context expression instead of parenthesizing the entire
// with items.
ParenthesizeWith::UnlessCommented
} else if is_wrap_multiple_context_managers_in_parens_enabled(context)
&& can_omit_optional_parentheses(&single.context_expr, context)
{
ParenthesizeWith::Optional
} else {
ParenthesizeWith::IfExpands
},
);
}
// Always parenthesize multiple items
Ok(ParenthesizeWith::IfExpands)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ParenthesizeWith {
/// Don't wrap the with items in parentheses except if it is a single item
/// and it has leading or trailing comment.
///
/// This is required because `are_with_items_parenthesized` cannot determine if
/// `with (expr)` is a parenthesized expression or a parenthesized with item.
UnlessCommented,
/// Wrap the with items in optional parentheses
Optional,
/// Wrap the with items in parentheses if they expand
IfExpands,
}
fn has_magic_trailing_comma(

View File

@@ -11,7 +11,10 @@ use crate::comments::{
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
use crate::expression::expr_string_literal::ExprStringLiteralKind;
use crate::prelude::*;
use crate::preview::is_no_blank_line_before_class_docstring_enabled;
use crate::preview::{
is_dummy_implementations_enabled, is_module_docstring_newlines_enabled,
is_no_blank_line_before_class_docstring_enabled,
};
use crate::statement::stmt_expr::FormatStmtExpr;
use crate::verbatim::{
suppressed_node, write_suppressed_statements_starting_with_leading_comment,
@@ -166,7 +169,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
&& self.kind == SuiteKind::Class
{
true
} else if f.options().preview().is_enabled()
} else if is_module_docstring_newlines_enabled(f.context())
&& self.kind == SuiteKind::TopLevel
&& DocstringStmt::try_from_statement(first.statement(), self.kind).is_some()
{
@@ -259,12 +262,31 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
f,
)?;
} else {
match self.kind {
SuiteKind::TopLevel => {
write!(f, [empty_line(), empty_line()])?;
}
SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => {
empty_line().fmt(f)?;
// Preserve empty lines after a stub implementation but don't insert a new one if there isn't any present in the source.
// This is useful when having multiple function overloads that should be grouped to getter by omitting new lines between them.
let is_preceding_stub_function_without_empty_line =
is_dummy_implementations_enabled(f.context())
&& following.is_function_def_stmt()
&& preceding
.as_function_def_stmt()
.is_some_and(|preceding_stub| {
contains_only_an_ellipsis(
&preceding_stub.body,
f.context().comments(),
) && lines_after_ignoring_end_of_line_trivia(
preceding_stub.end(),
f.context().source(),
) < 2
});
if !is_preceding_stub_function_without_empty_line {
match self.kind {
SuiteKind::TopLevel => {
write!(f, [empty_line(), empty_line()])?;
}
SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => {
empty_line().fmt(f)?;
}
}
}
}

View File

@@ -157,6 +157,39 @@ fn format() {
CodeFrame::new("python", &formatted_code)
)
.unwrap();
if options.preview().is_enabled() {
continue;
}
// 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_unchanged_ast(&content, formatted_preview, &options_preview, input_path);
ensure_stability_when_formatting_twice(
formatted_preview,
&options_preview,
input_path,
);
if formatted_code != formatted_preview {
// Having both snapshots makes it hard to see the difference, so we're keeping only
// diff.
writeln!(
snapshot,
"#### Preview changes\n{}",
CodeFrame::new(
"diff",
TextDiff::from_lines(formatted_code, formatted_preview)
.unified_diff()
.header("Stable", "Preview")
)
)
.unwrap();
}
}
} else {
// We want to capture the differences in the preview style in our fixtures

View File

@@ -95,36 +95,7 @@ def f(
```diff
--- Black
+++ Ruff
@@ -7,23 +7,13 @@
)
# "AnnAssign"s now also work
-z: (
- Loooooooooooooooooooooooong
- | Loooooooooooooooooooooooong
- | Loooooooooooooooooooooooong
- | Loooooooooooooooooooooooong
-)
-z: Short | Short2 | Short3 | Short4
-z: int
-z: int
+z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong
+z: (Short | Short2 | Short3 | Short4)
+z: (int)
+z: (int)
-z: (
- Loooooooooooooooooooooooong
- | Loooooooooooooooooooooooong
- | Loooooooooooooooooooooooong
- | Loooooooooooooooooooooooong
-) = 7
+z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7
z: Short | Short2 | Short3 | Short4 = 8
z: int = 2.3
z: int = foo()
@@ -63,7 +53,7 @@
@@ -63,7 +63,7 @@
# remove unnecessary paren
@@ -133,7 +104,7 @@ def f(
# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so.
@@ -72,12 +62,10 @@
@@ -72,12 +72,10 @@
def foo(
i: int,
@@ -150,7 +121,7 @@ def f(
*,
s: str,
) -> None:
@@ -88,7 +76,7 @@
@@ -88,7 +86,7 @@
async def foo(
q: str | None = Query(
None, title="Some long title", description="Some long description"
@@ -173,13 +144,23 @@ z = (
)
# "AnnAssign"s now also work
z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong
z: (Short | Short2 | Short3 | Short4)
z: (int)
z: (int)
z: (
Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
)
z: Short | Short2 | Short3 | Short4
z: int
z: int
z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7
z: (
Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
) = 7
z: Short | Short2 | Short3 | Short4 = 8
z: int = 2.3
z: int = foo()

View File

@@ -1,342 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_39.py
---
## Input
```python
with \
make_context_manager1() as cm1, \
make_context_manager2() as cm2, \
make_context_manager3() as cm3, \
make_context_manager4() as cm4 \
:
pass
# Leading comment
with \
make_context_manager1() as cm1, \
make_context_manager2(), \
make_context_manager3() as cm3, \
make_context_manager4() \
:
pass
with \
new_new_new1() as cm1, \
new_new_new2() \
:
pass
with (
new_new_new1() as cm1,
new_new_new2()
):
pass
# Leading comment.
with (
# First comment.
new_new_new1() as cm1,
# Second comment.
new_new_new2()
# Last comment.
):
pass
with \
this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2) as cm1, \
this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2, looong_arg3=looong_value3, looong_arg4=looong_value4) as cm2 \
:
pass
with mock.patch.object(
self.my_runner, "first_method", autospec=True
) as mock_run_adb, mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method() as cmd:
pass
async def func():
async with \
make_context_manager1() as cm1, \
make_context_manager2() as cm2, \
make_context_manager3() as cm3, \
make_context_manager4() as cm4 \
:
pass
async with some_function(
argument1, argument2, argument3="some_value"
) as some_cm, some_other_function(
argument1, argument2, argument3="some_value"
):
pass
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,19 +1,9 @@
-with (
- make_context_manager1() as cm1,
- make_context_manager2() as cm2,
- make_context_manager3() as cm3,
- make_context_manager4() as cm4,
-):
+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
# Leading comment
-with (
- make_context_manager1() as cm1,
- make_context_manager2(),
- make_context_manager3() as cm3,
- make_context_manager4(),
-):
+with make_context_manager1() as cm1, make_context_manager2(), make_context_manager3() as cm3, make_context_manager4():
pass
@@ -36,25 +26,21 @@
pass
-with (
- this_is_a_very_long_call(
- looong_arg1=looong_value1, looong_arg2=looong_value2
- ) as cm1,
- this_is_a_very_long_call(
- looong_arg1=looong_value1,
- looong_arg2=looong_value2,
- looong_arg3=looong_value3,
- looong_arg4=looong_value4,
- ) as cm2,
-):
+with this_is_a_very_long_call(
+ looong_arg1=looong_value1, looong_arg2=looong_value2
+) as cm1, this_is_a_very_long_call(
+ looong_arg1=looong_value1,
+ looong_arg2=looong_value2,
+ looong_arg3=looong_value3,
+ looong_arg4=looong_value4,
+) as cm2:
pass
-with (
- mock.patch.object(self.my_runner, "first_method", autospec=True) as mock_run_adb,
- mock.patch.object(
- self.my_runner, "second_method", autospec=True, return_value="foo"
- ),
+with mock.patch.object(
+ self.my_runner, "first_method", autospec=True
+) as mock_run_adb, mock.patch.object(
+ self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass
@@ -70,16 +56,10 @@
async def func():
- async with (
- make_context_manager1() as cm1,
- make_context_manager2() as cm2,
- make_context_manager3() as cm3,
- make_context_manager4() as cm4,
- ):
+ async with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
- async with (
- some_function(argument1, argument2, argument3="some_value") as some_cm,
- some_other_function(argument1, argument2, argument3="some_value"),
- ):
+ async with some_function(
+ argument1, argument2, argument3="some_value"
+ ) as some_cm, some_other_function(argument1, argument2, argument3="some_value"):
pass
```
## Ruff Output
```python
with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
# Leading comment
with make_context_manager1() as cm1, make_context_manager2(), make_context_manager3() as cm3, make_context_manager4():
pass
with new_new_new1() as cm1, new_new_new2():
pass
with new_new_new1() as cm1, new_new_new2():
pass
# Leading comment.
with (
# First comment.
new_new_new1() as cm1,
# Second comment.
new_new_new2(),
# Last comment.
):
pass
with this_is_a_very_long_call(
looong_arg1=looong_value1, looong_arg2=looong_value2
) as cm1, this_is_a_very_long_call(
looong_arg1=looong_value1,
looong_arg2=looong_value2,
looong_arg3=looong_value3,
looong_arg4=looong_value4,
) as cm2:
pass
with mock.patch.object(
self.my_runner, "first_method", autospec=True
) as mock_run_adb, mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method() as cmd:
pass
async def func():
async with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
async with some_function(
argument1, argument2, argument3="some_value"
) as some_cm, some_other_function(argument1, argument2, argument3="some_value"):
pass
```
## Black Output
```python
with (
make_context_manager1() as cm1,
make_context_manager2() as cm2,
make_context_manager3() as cm3,
make_context_manager4() as cm4,
):
pass
# Leading comment
with (
make_context_manager1() as cm1,
make_context_manager2(),
make_context_manager3() as cm3,
make_context_manager4(),
):
pass
with new_new_new1() as cm1, new_new_new2():
pass
with new_new_new1() as cm1, new_new_new2():
pass
# Leading comment.
with (
# First comment.
new_new_new1() as cm1,
# Second comment.
new_new_new2(),
# Last comment.
):
pass
with (
this_is_a_very_long_call(
looong_arg1=looong_value1, looong_arg2=looong_value2
) as cm1,
this_is_a_very_long_call(
looong_arg1=looong_value1,
looong_arg2=looong_value2,
looong_arg3=looong_value3,
looong_arg4=looong_value4,
) as cm2,
):
pass
with (
mock.patch.object(self.my_runner, "first_method", autospec=True) as mock_run_adb,
mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
),
):
pass
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method() as cmd:
pass
async def func():
async with (
make_context_manager1() as cm1,
make_context_manager2() as cm2,
make_context_manager3() as cm3,
make_context_manager4() as cm4,
):
pass
async with (
some_function(argument1, argument2, argument3="some_value") as some_cm,
some_other_function(argument1, argument2, argument3="some_value"),
):
pass
```

View File

@@ -1,79 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_autodetect_310.py
---
## Input
```python
# This file uses pattern matching introduced in Python 3.10.
match http_code:
case 404:
print("Not found")
with \
make_context_manager1() as cm1, \
make_context_manager2() as cm2, \
make_context_manager3() as cm3, \
make_context_manager4() as cm4 \
:
pass
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -6,10 +6,5 @@
print("Not found")
-with (
- make_context_manager1() as cm1,
- make_context_manager2() as cm2,
- make_context_manager3() as cm3,
- make_context_manager4() as cm4,
-):
+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
```
## Ruff Output
```python
# This file uses pattern matching introduced in Python 3.10.
match http_code:
case 404:
print("Not found")
with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
```
## Black Output
```python
# This file uses pattern matching introduced in Python 3.10.
match http_code:
case 404:
print("Not found")
with (
make_context_manager1() as cm1,
make_context_manager2() as cm2,
make_context_manager3() as cm3,
make_context_manager4() as cm4,
):
pass
```

View File

@@ -1,82 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_autodetect_311.py
---
## Input
```python
# This file uses except* clause in Python 3.11.
try:
some_call()
except* Error as e:
pass
with \
make_context_manager1() as cm1, \
make_context_manager2() as cm2, \
make_context_manager3() as cm3, \
make_context_manager4() as cm4 \
:
pass
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -7,10 +7,5 @@
pass
-with (
- make_context_manager1() as cm1,
- make_context_manager2() as cm2,
- make_context_manager3() as cm3,
- make_context_manager4() as cm4,
-):
+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
```
## Ruff Output
```python
# This file uses except* clause in Python 3.11.
try:
some_call()
except* Error as e:
pass
with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
```
## Black Output
```python
# This file uses except* clause in Python 3.11.
try:
some_call()
except* Error as e:
pass
with (
make_context_manager1() as cm1,
make_context_manager2() as cm2,
make_context_manager3() as cm3,
make_context_manager4() as cm4,
):
pass
```

View File

@@ -1,81 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_autodetect_39.py
---
## Input
```python
# This file uses parenthesized context managers introduced in Python 3.9.
with \
make_context_manager1() as cm1, \
make_context_manager2() as cm2, \
make_context_manager3() as cm3, \
make_context_manager4() as cm4 \
:
pass
with (
new_new_new1() as cm1,
new_new_new2()
):
pass
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,12 +1,7 @@
# This file uses parenthesized context managers introduced in Python 3.9.
-with (
- make_context_manager1() as cm1,
- make_context_manager2() as cm2,
- make_context_manager3() as cm3,
- make_context_manager4() as cm4,
-):
+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
```
## Ruff Output
```python
# This file uses parenthesized context managers introduced in Python 3.9.
with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
with new_new_new1() as cm1, new_new_new2():
pass
```
## Black Output
```python
# This file uses parenthesized context managers introduced in Python 3.9.
with (
make_context_manager1() as cm1,
make_context_manager2() as cm2,
make_context_manager3() as cm3,
make_context_manager4() as cm4,
):
pass
with new_new_new1() as cm1, new_new_new2():
pass
```

View File

@@ -1,276 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_dummy_implementations.py
---
## Input
```python
from typing import NoReturn, Protocol, Union, overload
class Empty:
...
def dummy(a): ...
async def other(b): ...
@overload
def a(arg: int) -> int: ...
@overload
def a(arg: str) -> str: ...
@overload
def a(arg: object) -> NoReturn: ...
def a(arg: Union[int, str, object]) -> Union[int, str]:
if not isinstance(arg, (int, str)):
raise TypeError
return arg
class Proto(Protocol):
def foo(self, a: int) -> int:
...
def bar(self, b: str) -> str: ...
def baz(self, c: bytes) -> str:
...
def dummy_two():
...
@dummy
def dummy_three():
...
def dummy_four():
...
@overload
def b(arg: int) -> int: ...
@overload
def b(arg: str) -> str: ...
@overload
def b(arg: object) -> NoReturn: ...
def b(arg: Union[int, str, object]) -> Union[int, str]:
if not isinstance(arg, (int, str)):
raise TypeError
return arg
def has_comment():
... # still a dummy
if some_condition:
...
if already_dummy: ...
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -5,15 +5,23 @@
def dummy(a): ...
+
+
async def other(b): ...
@overload
def a(arg: int) -> int: ...
+
+
@overload
def a(arg: str) -> str: ...
+
+
@overload
def a(arg: object) -> NoReturn: ...
+
+
def a(arg: Union[int, str, object]) -> Union[int, str]:
if not isinstance(arg, (int, str)):
raise TypeError
@@ -24,10 +32,13 @@
def foo(self, a: int) -> int: ...
def bar(self, b: str) -> str: ...
+
def baz(self, c: bytes) -> str: ...
def dummy_two(): ...
+
+
@dummy
def dummy_three(): ...
@@ -41,6 +52,8 @@
@overload
def b(arg: str) -> str: ...
+
+
@overload
def b(arg: object) -> NoReturn: ...
@@ -54,8 +67,6 @@
def has_comment(): ... # still a dummy
-if some_condition:
- ...
+if some_condition: ...
-if already_dummy:
- ...
+if already_dummy: ...
```
## Ruff Output
```python
from typing import NoReturn, Protocol, Union, overload
class Empty: ...
def dummy(a): ...
async def other(b): ...
@overload
def a(arg: int) -> int: ...
@overload
def a(arg: str) -> str: ...
@overload
def a(arg: object) -> NoReturn: ...
def a(arg: Union[int, str, object]) -> Union[int, str]:
if not isinstance(arg, (int, str)):
raise TypeError
return arg
class Proto(Protocol):
def foo(self, a: int) -> int: ...
def bar(self, b: str) -> str: ...
def baz(self, c: bytes) -> str: ...
def dummy_two(): ...
@dummy
def dummy_three(): ...
def dummy_four(): ...
@overload
def b(arg: int) -> int: ...
@overload
def b(arg: str) -> str: ...
@overload
def b(arg: object) -> NoReturn: ...
def b(arg: Union[int, str, object]) -> Union[int, str]:
if not isinstance(arg, (int, str)):
raise TypeError
return arg
def has_comment(): ... # still a dummy
if some_condition: ...
if already_dummy: ...
```
## Black Output
```python
from typing import NoReturn, Protocol, Union, overload
class Empty: ...
def dummy(a): ...
async def other(b): ...
@overload
def a(arg: int) -> int: ...
@overload
def a(arg: str) -> str: ...
@overload
def a(arg: object) -> NoReturn: ...
def a(arg: Union[int, str, object]) -> Union[int, str]:
if not isinstance(arg, (int, str)):
raise TypeError
return arg
class Proto(Protocol):
def foo(self, a: int) -> int: ...
def bar(self, b: str) -> str: ...
def baz(self, c: bytes) -> str: ...
def dummy_two(): ...
@dummy
def dummy_three(): ...
def dummy_four(): ...
@overload
def b(arg: int) -> int: ...
@overload
def b(arg: str) -> str: ...
@overload
def b(arg: object) -> NoReturn: ...
def b(arg: Union[int, str, object]) -> Union[int, str]:
if not isinstance(arg, (int, str)):
raise TypeError
return arg
def has_comment(): ... # still a dummy
if some_condition:
...
if already_dummy:
...
```

View File

@@ -535,6 +535,28 @@ def doctest_extra_indent3():
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -270,9 +270,11 @@
Examples
--------
- >>> df = pl.DataFrame(
- ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}
- ... )
+ >>> df = pl.DataFrame({
+ ... "foo": [1, 2, 3],
+ ... "bar": [6, 7, 8],
+ ... "ham": ["a", "b", "c"],
+ ... })
"""
```
### Output 2
```
indent-style = space
@@ -1139,6 +1161,28 @@ def doctest_extra_indent3():
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -270,9 +270,11 @@
Examples
--------
- >>> df = pl.DataFrame(
- ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}
- ... )
+ >>> df = pl.DataFrame({
+ ... "foo": [1, 2, 3],
+ ... "bar": [6, 7, 8],
+ ... "ham": ["a", "b", "c"],
+ ... })
"""
```
### Output 4
```
indent-style = tab
@@ -1876,4 +1920,26 @@ def doctest_extra_indent3():
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -700,9 +700,11 @@
Examples
--------
- >>> df = pl.DataFrame(
- ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}
- ... )
+ >>> df = pl.DataFrame({
+ ... "foo": [1, 2, 3],
+ ... "bar": [6, 7, 8],
+ ... "ham": ["a", "b", "c"],
+ ... })
"""
```

View File

@@ -319,6 +319,19 @@ trailing_preferred_quote_texts = [''' "''', ''' ""''', ''' """''', ''' """"''']
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -1,4 +1,5 @@
"' test"
+
'" test'
'" test'
```
### Output 2
```
indent-style = space
@@ -496,4 +509,17 @@ trailing_preferred_quote_texts = [''' "''', ''' ""''', ''' """''', ''' """"''']
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -1,4 +1,5 @@
"' test"
+
'" test'
'" test'
```

View File

@@ -259,7 +259,27 @@ if True:
# empty line(s) at the end of the file due to nested function
if True:
def nested_trailing_function():
pass```
pass
def overload1(): ... # trailing comment
def overload1(a: int): ...
def overload2(): ... # trailing comment
def overload2(a: int): ...
def overload3():
...
# trailing comment
def overload3(a: int): ...
def overload4():
...
# trailing comment
def overload4(a: int): ...
```
## Output
```python
@@ -546,6 +566,40 @@ if True:
def nested_trailing_function():
pass
def overload1():
... # trailing comment
def overload1(a: int):
...
def overload2():
... # trailing comment
def overload2(a: int):
...
def overload3():
...
# trailing comment
def overload3(a: int):
...
def overload4():
...
# trailing comment
def overload4(a: int):
...
```
@@ -569,6 +623,48 @@ if True:
def fakehttp():
@@ -283,20 +281,14 @@
pass
-def overload1():
- ... # trailing comment
+def overload1(): ... # trailing comment
+def overload1(a: int): ...
-def overload1(a: int):
- ...
+def overload2(): ... # trailing comment
-def overload2():
- ... # trailing comment
-
-
-def overload2(a: int):
- ...
+def overload2(a: int): ...
def overload3():
@@ -304,8 +296,7 @@
# trailing comment
-def overload3(a: int):
- ...
+def overload3(a: int): ...
def overload4():
@@ -313,5 +304,4 @@
# trailing comment
-def overload4(a: int):
- ...
+def overload4(a: int): ...
```

View File

@@ -156,6 +156,78 @@ def f():
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -1,13 +1,13 @@
"""
Black's `Preview.module_docstring_newlines`
"""
+
first_stmt_after_module_level_docstring = 1
class CachedRepository:
# Black's `Preview.dummy_implementations`
- def get_release_info(self):
- ...
+ def get_release_info(self): ...
def raw_docstring():
@@ -27,23 +27,22 @@
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
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = (
+ cccccccc.ccccccccccccc.cccccccc
+ )
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
- bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
- ] = cccccccc.ccccccccccccc().cccccccc
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = (
+ cccccccc.ccccccccccccc().cccccccc
+ )
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
- bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
- ] = cccccccc.ccccccccccccc(d).cccccccc
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = (
+ cccccccc.ccccccccccccc(d).cccccccc
+ )
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = (
cccccccc.ccccccccccccc(d).cccccccc + e
@@ -57,9 +56,9 @@
+ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
)
- self._cache: dict[
- DependencyCacheKey, list[list[DependencyPackage]]
- ] = collections.defaultdict(list)
- self._cached_dependencies_by_level: dict[
- int, list[DependencyCacheKey]
- ] = collections.defaultdict(list)
+ self._cache: dict[DependencyCacheKey, list[list[DependencyPackage]]] = (
+ collections.defaultdict(list)
+ )
+ self._cached_dependencies_by_level: dict[int, list[DependencyCacheKey]] = (
+ collections.defaultdict(list)
+ )
```
### Output 2
```
indent-style = space

View File

@@ -129,6 +129,19 @@ def docstring_single():
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -1,4 +1,5 @@
'single'
+
'double'
r'r single'
r'r double'
```
### Output 2
```
indent-style = space
@@ -201,6 +214,19 @@ def docstring_single():
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -1,4 +1,5 @@
"single"
+
"double"
r"r single"
r"r double"
```
### Output 3
```
indent-style = space
@@ -273,4 +299,17 @@ def docstring_single():
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -1,4 +1,5 @@
'single'
+
"double"
r'r single'
r"r double"
```

View File

@@ -84,6 +84,16 @@ class DefaultRunner:
JSONSerializable: TypeAlias = (
"str | int | float | bool | None | list | tuple | JSONMapping"
@@ -29,6 +29,6 @@
# 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

@@ -0,0 +1,451 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/long_type_annotations.py
---
## Input
```python
x1: A[b] | EventHandler | EventSpec | list[EventHandler | EventSpec] | Other | More | AndMore | None = None
x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]"
x6: VeryLongClassNameWithAwkwardGenericSubtype[
integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer,
VeryLongClassNameWithAwkwardGenericSubtype,
str
] = True
x7: CustomTrainingJob | CustomContainerTrainingJob | CustomPythonPackageTrainingJob
x8: (
None
| datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset
) = None
x9: None | (
datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset
) = None
x10: (
aaaaaaaaaaaaaaaaaaaaaaaa[
bbbbbbbbbbb,
Subscript
| None
| datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset,
],
bbb[other],
) = None
x11: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] = None
x12: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x13: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x14: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] = None
x15: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x16: None | Literal[
"split",
"a bit longer",
"records",
"index",
"table",
"columns",
"values",
] = None
x17: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
]
class Test:
safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff
applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes.
string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation
##########
# Comments
leading: (
# Leading comment
None | dataset.ImageDataset
)
leading_with_value: (
# Leading comment
None
| dataset.ImageDataset
) = None
leading_open_parentheses: ( # Leading comment
None
| dataset.ImageDataset
)
leading_open_parentheses_with_value: ( # Leading comment
None
| dataset.ImageDataset
) = None
trailing: (
None | dataset.ImageDataset # trailing comment
)
trailing_with_value: (
None | dataset.ImageDataset # trailing comment
) = None
trailing_own_line: (
None | dataset.ImageDataset
# trailing own line
)
trailing_own_line_with_value: (
None | dataset.ImageDataset
# trailing own line
) = None
nested_comment: None | [
# a list of strings
str
] = None
```
## Output
```python
x1: A[b] | EventHandler | EventSpec | list[
EventHandler | EventSpec
] | Other | More | AndMore | None = None
x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]"
x6: VeryLongClassNameWithAwkwardGenericSubtype[
integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer,
VeryLongClassNameWithAwkwardGenericSubtype,
str,
] = True
x7: CustomTrainingJob | CustomContainerTrainingJob | CustomPythonPackageTrainingJob
x8: (
None
| datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset
) = None
x9: None | (
datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset
) = None
x10: (
aaaaaaaaaaaaaaaaaaaaaaaa[
bbbbbbbbbbb,
Subscript
| None
| datasets.ImageDataset
| datasets.TabularDataset
| datasets.TextDataset
| datasets.VideoDataset,
],
bbb[other],
) = None
x11: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] = None
x12: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x13: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x14: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] = None
x15: [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
] | Other = None
x16: None | Literal[
"split",
"a bit longer",
"records",
"index",
"table",
"columns",
"values",
] = None
x17: None | [
datasets.ImageDataset,
datasets.TabularDataset,
datasets.TextDataset,
datasets.VideoDataset,
]
class Test:
safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff
applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes.
string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation
##########
# Comments
leading: (
# Leading comment
None | dataset.ImageDataset
)
leading_with_value: (
# Leading comment
None | dataset.ImageDataset
) = None
leading_open_parentheses: ( # Leading comment
None | dataset.ImageDataset
)
leading_open_parentheses_with_value: ( # Leading comment
None | dataset.ImageDataset
) = None
trailing: (
None | dataset.ImageDataset # trailing comment
)
trailing_with_value: (
None | dataset.ImageDataset # trailing comment
) = None
trailing_own_line: (
None | dataset.ImageDataset
# trailing own line
)
trailing_own_line_with_value: (
None | dataset.ImageDataset
# trailing own line
) = None
nested_comment: None | [
# a list of strings
str
] = None
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -1,8 +1,18 @@
-x1: A[b] | EventHandler | EventSpec | list[
- EventHandler | EventSpec
-] | Other | More | AndMore | None = None
+x1: (
+ A[b]
+ | EventHandler
+ | EventSpec
+ | list[EventHandler | EventSpec]
+ | Other
+ | More
+ | AndMore
+ | None
+) = None
-x2: "VeryLongClassNameWithAwkwardGenericSubtype[int] |" "VeryLongClassNameWithAwkwardGenericSubtype[str]"
+x2: (
+ "VeryLongClassNameWithAwkwardGenericSubtype[int] |"
+ "VeryLongClassNameWithAwkwardGenericSubtype[str]"
+)
x6: VeryLongClassNameWithAwkwardGenericSubtype[
integeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer,
@@ -48,12 +58,16 @@
datasets.VideoDataset,
] = None
-x12: None | [
- datasets.ImageDataset,
- datasets.TabularDataset,
- datasets.TextDataset,
- datasets.VideoDataset,
-] | Other = None
+x12: (
+ None
+ | [
+ datasets.ImageDataset,
+ datasets.TabularDataset,
+ datasets.TextDataset,
+ datasets.VideoDataset,
+ ]
+ | Other
+) = None
x13: [
@@ -75,27 +89,34 @@
datasets.VideoDataset,
] = None
-x15: [
- datasets.ImageDataset,
- datasets.TabularDataset,
- datasets.TextDataset,
- datasets.VideoDataset,
-] | [
- datasets.ImageDataset,
- datasets.TabularDataset,
- datasets.TextDataset,
- datasets.VideoDataset,
-] | Other = None
+x15: (
+ [
+ datasets.ImageDataset,
+ datasets.TabularDataset,
+ datasets.TextDataset,
+ datasets.VideoDataset,
+ ]
+ | [
+ datasets.ImageDataset,
+ datasets.TabularDataset,
+ datasets.TextDataset,
+ datasets.VideoDataset,
+ ]
+ | Other
+) = None
-x16: None | Literal[
- "split",
- "a bit longer",
- "records",
- "index",
- "table",
- "columns",
- "values",
-] = None
+x16: (
+ None
+ | Literal[
+ "split",
+ "a bit longer",
+ "records",
+ "index",
+ "table",
+ "columns",
+ "values",
+ ]
+) = None
x17: None | [
datasets.ImageDataset,
@@ -106,9 +127,13 @@
class Test:
- safe_age: Decimal # the user's age, used to determine if it's safe for them to use ruff
+ safe_age: (
+ Decimal # the user's age, used to determine if it's safe for them to use ruff
+ )
applied_fixes: int # the number of fixes that this user applied. Used for ranking the users with the most applied fixes.
- string_annotation: "Test" # a long comment after a quoted, runtime-only type annotation
+ string_annotation: (
+ "Test" # a long comment after a quoted, runtime-only type annotation
+ )
##########
```

View File

@@ -150,21 +150,23 @@ def quuz():
class Baz(Qux):
@@ -49,12 +44,10 @@
@@ -49,14 +44,8 @@
pass
-def bar():
- ...
+def bar(): ...
-
-
-def baz():
- ...
-
-
+def bar(): ...
+def baz(): ...
def quux():
"""Some docstring."""
```

View File

@@ -315,7 +315,21 @@ with Child(aaaaaaaaa, bbbbbbbbbbbbbbb, cccccc), Document(aaaaa, bbbbbbbbbb, dddd
pass
```
## Output
## Outputs
### Output 1
```
indent-style = space
line-width = 88
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Disabled
target_version = Py38
```
```python
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
pass
@@ -653,4 +667,414 @@ with Child(aaaaaaaaa, bbbbbbbbbbbbbbb, cccccc), Document(
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -295,8 +295,9 @@
pass
with (
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b,
+ (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ ) as b,
c as d,
):
pass
```
### Output 2
```
indent-style = space
line-width = 88
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Enabled
target_version = Py39
```
```python
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
):
pass
# trailing
with a, a: # after colon
pass
# trailing
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
):
pass
# trailing
with (
a, # a # comma
b, # c
): # colon
pass
with (
a as ( # a # as
# own line
b
), # b # comma
c, # c
): # colon
pass # body
# body trailing own
with (
a as ( # a # as
# own line
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) # b
):
pass
with (
a,
): # magic trailing comma
pass
with a: # should remove brackets
pass
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c
):
pass
# currently unparsable by black: https://github.com/psf/black/issues/3678
with (name_2 for name_0 in name_4):
pass
with (a, *b):
pass
with (
# leading comment
a
) as b:
pass
with (
# leading comment
a as b
):
pass
with (
a as b
# trailing comment
):
pass
with (
a as (
# leading comment
b
)
):
pass
with (
a as (
b
# trailing comment
)
):
pass
with (
a as b # trailing same line comment
# trailing own line comment
):
pass
with (
a # trailing same line comment
# trailing own line comment
) as b:
pass
with (
(
a
# trailing own line comment
) as ( # trailing as same line comment
b
) # trailing b same line comment
):
pass
with (
# comment
a
):
pass
with (
a # comment
):
pass
with (
a
# comment
):
pass
with (
# comment
a as b
):
pass
with (
a as b # comment
):
pass
with (
a as b
# comment
):
pass
with (
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccccccccccccccccccccccccccccccccccc",
dddddddddddddddddddddddddddddddd,
] as example1,
aaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ cccccccccccccccccccccccccccc
+ ddddddddddddddddd as example2,
CtxManager2() as example2,
CtxManager2() as example2,
CtxManager2() as example2,
):
pass
with (
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccccccccccccccccccccccccccccccccccc",
dddddddddddddddddddddddddddddddd,
] as example1,
aaaaaaaaaaaaaaaaaaaaaaaaaa
* bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
* cccccccccccccccccccccccccccc
+ ddddddddddddddddd as example2,
CtxManager222222222222222() as example2,
):
pass
# Comments on open parentheses
with ( # comment
CtxManager1() as example1,
CtxManager2() as example2,
CtxManager3() as example3,
):
pass
with ( # outer comment
( # inner comment
CtxManager1()
) as example1,
CtxManager2() as example2,
CtxManager3() as example3,
):
pass
with ( # outer comment
CtxManager()
) as example:
pass
with (
( # outer comment
CtxManager()
) as example,
( # inner comment
CtxManager2()
) as example2,
):
pass
with ( # outer comment
CtxManager1(),
CtxManager2(),
) as example:
pass
with ( # outer comment
( # inner comment
CtxManager1()
),
CtxManager2(),
) as example:
pass
# Breaking of with items.
with (
test as ( # bar # foo
# test
foo
)
):
pass
with (
test as (
# test
foo
)
):
pass
with (
test as ( # bar # foo # baz
# test
foo
)
):
pass
with a as b, c as d:
pass
with (
a as b,
# foo
c as d,
):
pass
with (
a as ( # foo
b
)
):
pass
with f(
a,
) as b:
pass
with (x := 1) as d:
pass
with x[
1,
2,
] as d:
pass
with (
f(
a,
) as b,
c as d,
):
pass
with (
f(
a,
) as b,
c as d,
):
pass
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) as b:
pass
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b
):
pass
with (
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) as b,
c as d,
):
pass
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b,
c as d,
):
pass
with (
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) as b,
c as d,
):
pass
with foo() as bar, baz() as bop:
pass
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
if True:
with (
anyio.CancelScope(shield=True)
and B
and [aaaaaaaa, bbbbbbbbbbbbb, cccccccccc, dddddddddddd, eeeeeeeeeeeee]
):
pass
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
with (
Child(aaaaaaaaa, bbbbbbbbbbbbbbb, cccccc),
Document(aaaaa, bbbbbbbbbb, ddddddddddddd),
):
pass
```

View File

@@ -0,0 +1,222 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with_39.py
---
## Input
```python
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
# Black avoids parenthesizing the with because it can make all with items fit by just breaking
# around parentheses. We don't implement this optimisation because it makes it difficult to see where
# the different context managers start and end.
with cmd, xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
) as cmd, another, and_more as x:
pass
# Avoid parenthesizing single item context managers when splitting after the parentheses (can_omit_optional_parentheses)
# is sufficient
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method(): pass
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
# Black avoids parentheses here because it can make the entire with
# header fit without requiring parentheses to do so.
# We don't implement this optimisation because it very difficult to see where
# the different context managers start or end.
with cmd, xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
) as cmd, another, and_more as x:
pass
# Avoid parenthesizing single item context managers when splitting after the parentheses
# is sufficient
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method(): pass
# Parenthesize the with items if it makes them fit. Breaking the binary expression isn't
# necessary because the entire items fit just into the 88 character limit.
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
pass
# Black parenthesizes this binary expression but also preserves the parentheses of the first with-item.
# It does so because it prefers splitting already parenthesized context managers, even if it leads to more parentheses
# like in this case.
with (
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) as b,
c as d,
):
pass
if True:
with anyio.CancelScope(shield=True) if get_running_loop() else contextlib.nullcontext():
pass
with (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c):
pass
```
## Outputs
### Output 1
```
indent-style = space
line-width = 88
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Enabled
target_version = Py39
```
```python
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
# Black avoids parenthesizing the with because it can make all with items fit by just breaking
# around parentheses. We don't implement this optimisation because it makes it difficult to see where
# the different context managers start and end.
with (
cmd,
xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
) as cmd,
another,
and_more as x,
):
pass
# Avoid parenthesizing single item context managers when splitting after the parentheses (can_omit_optional_parentheses)
# is sufficient
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method():
pass
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
# Black avoids parentheses here because it can make the entire with
# header fit without requiring parentheses to do so.
# We don't implement this optimisation because it very difficult to see where
# the different context managers start or end.
with (
cmd,
xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
) as cmd,
another,
and_more as x,
):
pass
# Avoid parenthesizing single item context managers when splitting after the parentheses
# is sufficient
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method():
pass
# Parenthesize the with items if it makes them fit. Breaking the binary expression isn't
# necessary because the entire items fit just into the 88 character limit.
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c
):
pass
# Black parenthesizes this binary expression but also preserves the parentheses of the first with-item.
# It does so because it prefers splitting already parenthesized context managers, even if it leads to more parentheses
# like in this case.
with (
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) as b,
c as d,
):
pass
if True:
with (
anyio.CancelScope(shield=True)
if get_running_loop()
else contextlib.nullcontext()
):
pass
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c
):
pass
```

View File

@@ -1293,7 +1293,7 @@ impl FusedIterator for Lexer<'_> {}
/// [lexer] implementation.
///
/// [lexer]: crate::lexer
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct LexicalError {
/// The type of error that occurred.
pub error: LexicalErrorType,
@@ -1309,7 +1309,7 @@ impl LexicalError {
}
/// Represents the different types of errors that can occur during lexing.
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub enum LexicalErrorType {
// TODO: Can probably be removed, the places it is used seem to be able
// to use the `UnicodeError` variant instead.

View File

@@ -409,7 +409,7 @@ pub(crate) fn concatenated_strings(
// TODO: consolidate these with ParseError
/// An error that occurred during parsing of an f-string.
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
struct FStringError {
/// The type of error that occurred.
pub(crate) error: FStringErrorType,
@@ -427,7 +427,7 @@ impl From<FStringError> for LexicalError {
}
/// Represents the different types of errors that can occur during parsing of an f-string.
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub enum FStringErrorType {
/// Expected a right brace after an opened left brace.
UnclosedLbrace,

View File

@@ -17,7 +17,7 @@ license = { workspace = true }
log = { workspace = true }
[dev-dependencies]
env_logger = "0.10.0"
env_logger = "0.10.1"
tempfile = "3.8.1"
insta = { workspace = true }

View File

@@ -333,6 +333,69 @@ pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> boo
})
}
/// Traverse a "union" type annotation, applying `func` to each union member.
///
/// Supports traversal of `Union` and `|` union expressions.
///
/// The function is called with each expression in the union (excluding declarations of nested
/// unions) and the parent expression.
pub fn traverse_union<'a, F>(func: &mut F, semantic: &SemanticModel, expr: &'a Expr)
where
F: FnMut(&'a Expr, &'a Expr),
{
fn inner<'a, F>(
func: &mut F,
semantic: &SemanticModel,
expr: &'a Expr,
parent: Option<&'a Expr>,
) where
F: FnMut(&'a Expr, &'a Expr),
{
// Ex) x | y
if let Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
left,
right,
range: _,
}) = expr
{
// The union data structure usually looks like this:
// a | b | c -> (a | b) | c
//
// However, parenthesized expressions can coerce it into any structure:
// a | (b | c)
//
// So we have to traverse both branches in order (left, then right), to report members
// in the order they appear in the source code.
// Traverse the left then right arms
inner(func, semantic, left, Some(expr));
inner(func, semantic, right, Some(expr));
return;
}
// Ex) Union[x, y]
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if semantic.match_typing_expr(value, "Union") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
// Traverse each element of the tuple within the union recursively to handle cases
// such as `Union[..., Union[...]]
elts.iter()
.for_each(|elt| inner(func, semantic, elt, Some(expr)));
return;
}
}
}
// Otherwise, call the function on expression, if it's not the top-level expression.
if let Some(parent) = parent {
func(expr, parent);
}
}
inner(func, semantic, expr, None);
}
/// Abstraction for a type checker, conservatively checks for the intended type(s).
trait TypeChecker {
/// Check annotation expression to match the intended type(s).

View File

@@ -1715,6 +1715,16 @@ bitflags! {
/// ```
const FUTURE_ANNOTATIONS = 1 << 15;
/// The model has traversed past the module docstring.
///
/// For example, the model could be visiting `x` in:
/// ```python
/// """Module docstring."""
///
/// x: int = 1
/// ```
const MODULE_DOCSTRING = 1 << 16;
/// The model is in a type parameter definition.
///
/// For example, the model could be visiting `Record` in:
@@ -1723,11 +1733,10 @@ bitflags! {
///
/// Record = TypeVar("Record")
///
const TYPE_PARAM_DEFINITION = 1 << 16;
const TYPE_PARAM_DEFINITION = 1 << 17;
/// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
/// The context is in any string type definition.
const STRING_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits()

View File

@@ -7,7 +7,7 @@ use wasm_bindgen::prelude::*;
use ruff_formatter::{FormatResult, Formatted, IndentStyle};
use ruff_linter::directives;
use ruff_linter::line_width::{IndentWidth, LineLength};
use ruff_linter::linter::{check_path, LinterResult};
use ruff_linter::linter::{check_path, LinterResult, TokenSource};
use ruff_linter::registry::AsRule;
use ruff_linter::settings::types::PythonVersion;
use ruff_linter::settings::{flags, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX};
@@ -182,7 +182,6 @@ impl Workspace {
} = check_path(
Path::new("<filename>"),
None,
tokens,
&locator,
&stylist,
&indexer,
@@ -191,6 +190,7 @@ impl Workspace {
flags::Noqa::Enabled,
&source_kind,
source_type,
TokenSource::Tokens(tokens),
);
let source_code = locator.to_source_code();

View File

@@ -32,7 +32,7 @@ glob = { workspace = true }
globset = { workspace = true }
once_cell = { workspace = true }
path-absolutize = { workspace = true }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pep440_rs = { version = "0.4.0", features = ["serde"] }
regex = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }

View File

@@ -714,7 +714,9 @@ impl LintConfiguration {
.collect();
// The fixable set keeps track of which rules are fixable.
let mut fixable_set: RuleSet = RuleSelector::All.rules(&preview).collect();
let mut fixable_set: RuleSet = RuleSelector::All
.rules(&preview.without_require_explicit())
.collect();
// Ignores normally only subtract from the current set of selected
// rules. By that logic the ignore in `select = [], ignore = ["E501"]`

View File

@@ -24,7 +24,7 @@ export default function SettingsEditor({
monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
schemas: [
{
uri: "https://raw.githubusercontent.com/charliermarsh/ruff/main/ruff.schema.json",
uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ruff.schema.json",
fileMatch: ["*"],
schema,
},

2
ruff.schema.json generated
View File

@@ -3429,6 +3429,8 @@
"RUF017",
"RUF018",
"RUF019",
"RUF02",
"RUF020",
"RUF1",
"RUF10",
"RUF100",