Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
4eaf24028d [ty] Consider a TypedDict type to be a subtype of itself 2025-09-04 15:57:23 +02:00
199 changed files with 4133 additions and 6649 deletions

View File

@@ -259,10 +259,6 @@ jobs:
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
if: ${{ needs.determine_changes.outputs.ty == 'true' }}
env:
@@ -321,10 +317,6 @@ jobs:
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
@@ -348,10 +340,6 @@ jobs:
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
@@ -453,7 +441,9 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
@@ -473,7 +463,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -674,7 +664,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -704,7 +694,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
- uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -744,7 +734,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -787,7 +777,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -919,7 +909,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: "Install Rust toolchain"
run: rustup show
@@ -952,7 +942,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: "Install Rust toolchain"
run: rustup show

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -39,7 +39,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -82,7 +82,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*

View File

@@ -65,7 +65,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -117,7 +117,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -155,7 +155,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- name: Setup git
run: |
git config --global user.name typeshedbot

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -1,44 +1,5 @@
# Breaking Changes
## 0.13.0
- **Several rules can now add `from __future__ import annotations` automatically**
`TC001`, `TC002`, `TC003`, `RUF013`, and `UP037` now add `from __future__ import annotations` as part of their fixes when the
`lint.future-annotations` setting is enabled. This allows the rules to move
more imports into `TYPE_CHECKING` blocks (`TC001`, `TC002`, and `TC003`),
use PEP 604 union syntax on Python versions before 3.10 (`RUF013`), and
unquote more annotations (`UP037`).
- **Full module paths are now used to verify first-party modules**
Ruff now checks that the full path to a module exists on disk before
categorizing it as a first-party import. This change makes first-party
import detection more accurate, helping to avoid false positives on local
directories with the same name as a third-party dependency, for example. See
the [FAQ
section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) on import categorization for more details.
- **Deprecated rules must now be selected by exact rule code**
Ruff will no longer activate deprecated rules selected by their group name
or prefix. As noted below, the two remaining deprecated rules were also
removed in this release, so this won't affect any current rules, but it will
still affect any deprecations in the future.
- **The deprecated macOS configuration directory fallback has been removed**
Ruff will no longer look for a user-level configuration file at
`~/Library/Application Support/ruff/ruff.toml` on macOS. This feature was
deprecated in v0.5 in favor of using the [XDG
specification](https://specifications.freedesktop.org/basedir-spec/latest/)
(usually resolving to `~/.config/ruff/ruff.toml`), like on Linux. The
fallback and accompanying deprecation warning have now been removed.
- **[`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name) (`PD901`) has been removed**
- **[`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance) (`UP038`) has been removed**
## 0.12.0
- **Detection of more syntax errors**

View File

@@ -1,119 +1,517 @@
# Changelog
## 0.13.0
## 0.12.11
Check out the [blog post](https://astral.sh/blog/ruff-v0.13.0) for a migration
### Preview features
- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
### Bug fixes
- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
### Rule changes
- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
### Documentation
- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
## 0.12.10
### Preview features
- \[`flake8-simplify`\] Implement fix for `maxsplit` without separator (`SIM905`) ([#19851](https://github.com/astral-sh/ruff/pull/19851))
- \[`flake8-use-pathlib`\] Add fixes for `PTH102` and `PTH103` ([#19514](https://github.com/astral-sh/ruff/pull/19514))
### Bug fixes
- \[`isort`\] Handle multiple continuation lines after module docstring (`I002`) ([#19818](https://github.com/astral-sh/ruff/pull/19818))
- \[`pyupgrade`\] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) ([#19769](https://github.com/astral-sh/ruff/pull/19769))
- \[`pyupgrade`\] Handle nested `Optional`s (`UP045`) ([#19770](https://github.com/astral-sh/ruff/pull/19770))
### Rule changes
- \[`pycodestyle`\] Make `E731` fix unsafe instead of display-only for class assignments ([#19700](https://github.com/astral-sh/ruff/pull/19700))
- \[`pyflakes`\] Add secondary annotation showing previous definition (`F811`) ([#19900](https://github.com/astral-sh/ruff/pull/19900))
### Documentation
- Fix description of global config file discovery strategy ([#19188](https://github.com/astral-sh/ruff/pull/19188))
- Update outdated links to <https://typing.python.org/en/latest/source/stubs.html> ([#19992](https://github.com/astral-sh/ruff/pull/19992))
- \[`flake8-annotations`\] Remove unused import in example (`ANN401`) ([#20000](https://github.com/astral-sh/ruff/pull/20000))
## 0.12.9
### Preview features
- \[`airflow`\] Add check for `airflow.secrets.cache.SecretCache` (`AIR301`) ([#17707](https://github.com/astral-sh/ruff/pull/17707))
- \[`ruff`\] Offer a safe fix for multi-digit zeros (`RUF064`) ([#19847](https://github.com/astral-sh/ruff/pull/19847))
### Bug fixes
- \[`flake8-blind-except`\] Fix `BLE001` false-positive on `raise ... from None` ([#19755](https://github.com/astral-sh/ruff/pull/19755))
- \[`flake8-comprehensions`\] Fix false positive for `C420` with attribute, subscript, or slice assignment targets ([#19513](https://github.com/astral-sh/ruff/pull/19513))
- \[`flake8-simplify`\] Fix handling of U+001C..U+001F whitespace (`SIM905`) ([#19849](https://github.com/astral-sh/ruff/pull/19849))
### Rule changes
- \[`pylint`\] Use lowercase hex characters to match the formatter (`PLE2513`) ([#19808](https://github.com/astral-sh/ruff/pull/19808))
### Documentation
- Fix `lint.future-annotations` link ([#19876](https://github.com/astral-sh/ruff/pull/19876))
### Other changes
- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
```diff
-unused.py:8:19: F401 [*] `pathlib` imported but unused
+F401 [*] `pathlib` imported but unused
+ --> unused.py:8:19
|
7 | # Unused, _not_ marked as required (due to the alias).
8 | import pathlib as non_alias
- | ^^^^^^^^^ F401
+ | ^^^^^^^^^
9 |
10 | # Unused, marked as required.
|
- = help: Remove unused import: `pathlib`
+help: Remove unused import: `pathlib`
```
For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
## 0.12.8
### Preview features
- \[`flake8-use-pathlib`\] Expand `PTH201` to check all `PurePath` subclasses ([#19440](https://github.com/astral-sh/ruff/pull/19440))
### Bug fixes
- \[`flake8-blind-except`\] Change `BLE001` to correctly parse exception tuples ([#19747](https://github.com/astral-sh/ruff/pull/19747))
- \[`flake8-errmsg`\] Exclude `typing.cast` from `EM101` ([#19656](https://github.com/astral-sh/ruff/pull/19656))
- \[`flake8-simplify`\] Fix raw string handling in `SIM905` for embedded quotes ([#19591](https://github.com/astral-sh/ruff/pull/19591))
- \[`flake8-import-conventions`\] Avoid false positives for NFKC-normalized `__debug__` import aliases in `ICN001` ([#19411](https://github.com/astral-sh/ruff/pull/19411))
- \[`isort`\] Fix syntax error after docstring ending with backslash (`I002`) ([#19505](https://github.com/astral-sh/ruff/pull/19505))
- \[`pylint`\] Mark `PLC0207` fixes as unsafe when `*args` unpacking is present ([#19679](https://github.com/astral-sh/ruff/pull/19679))
- \[`pyupgrade`\] Prevent infinite loop with `I002` (`UP010`, `UP035`) ([#19413](https://github.com/astral-sh/ruff/pull/19413))
- \[`ruff`\] Parenthesize generator expressions in f-strings (`RUF010`) ([#19434](https://github.com/astral-sh/ruff/pull/19434))
### Rule changes
- \[`eradicate`\] Don't flag `pyrefly` pragmas as unused code (`ERA001`) ([#19731](https://github.com/astral-sh/ruff/pull/19731))
### Documentation
- Replace "associative" with "commutative" in docs for `RUF036` ([#19706](https://github.com/astral-sh/ruff/pull/19706))
- Fix copy and line separator colors in dark mode ([#19630](https://github.com/astral-sh/ruff/pull/19630))
- Fix link to `typing` documentation ([#19648](https://github.com/astral-sh/ruff/pull/19648))
- \[`refurb`\] Make more examples error out-of-the-box ([#19695](https://github.com/astral-sh/ruff/pull/19695),[#19673](https://github.com/astral-sh/ruff/pull/19673),[#19672](https://github.com/astral-sh/ruff/pull/19672))
### Other changes
- Include column numbers in GitLab output format ([#19708](https://github.com/astral-sh/ruff/pull/19708))
- Always expand tabs to four spaces in diagnostics ([#19618](https://github.com/astral-sh/ruff/pull/19618))
- Update pre-commit's `ruff` id ([#19654](https://github.com/astral-sh/ruff/pull/19654))
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.5
### Preview features
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
### Bug fixes
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
### Rule changes
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
## 0.12.4
### Preview features
- \[`flake8-type-checking`, `pyupgrade`, `ruff`\] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) ([#19100](https://github.com/astral-sh/ruff/pull/19100))
- \[`flake8-use-pathlib`\] Add autofix for `PTH109` ([#19245](https://github.com/astral-sh/ruff/pull/19245))
- \[`pylint`\] Detect indirect `pathlib.Path` usages for `unspecified-encoding` (`PLW1514`) ([#19304](https://github.com/astral-sh/ruff/pull/19304))
### Bug fixes
- \[`flake8-bugbear`\] Fix `B017` false negatives for keyword exception arguments ([#19217](https://github.com/astral-sh/ruff/pull/19217))
- \[`flake8-use-pathlib`\] Fix false negative on direct `Path()` instantiation (`PTH210`) ([#19388](https://github.com/astral-sh/ruff/pull/19388))
- \[`flake8-django`\] Fix `DJ008` false positive for abstract models with type-annotated `abstract` field ([#19221](https://github.com/astral-sh/ruff/pull/19221))
- \[`isort`\] Fix `I002` import insertion after docstring with multiple string statements ([#19222](https://github.com/astral-sh/ruff/pull/19222))
- \[`isort`\] Treat form feed as valid whitespace before a semicolon ([#19343](https://github.com/astral-sh/ruff/pull/19343))
- \[`pydoclint`\] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) ([#19246](https://github.com/astral-sh/ruff/pull/19246))
- \[`refurb`\] `FURB164` fix should validate arguments and should usually be marked unsafe ([#19136](https://github.com/astral-sh/ruff/pull/19136))
### Rule changes
- \[`flake8-use-pathlib`\] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 ([#19331](https://github.com/astral-sh/ruff/pull/19331))
- \[`pep8_naming`\] Avoid false positives on standard library functions with uppercase names (`N802`) ([#18907](https://github.com/astral-sh/ruff/pull/18907))
- \[`pycodestyle`\] Handle brace escapes for t-strings in logical lines ([#19358](https://github.com/astral-sh/ruff/pull/19358))
- \[`pylint`\] Extend invalid string character rules to include t-strings ([#19355](https://github.com/astral-sh/ruff/pull/19355))
- \[`ruff`\] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ ([#19333](https://github.com/astral-sh/ruff/pull/19333))
### Documentation
- \[`flake8-type-checking`\] Make `TC010` docs example more realistic ([#19356](https://github.com/astral-sh/ruff/pull/19356))
- Make more documentation examples error out-of-the-box ([#19288](https://github.com/astral-sh/ruff/pull/19288),[#19272](https://github.com/astral-sh/ruff/pull/19272),[#19291](https://github.com/astral-sh/ruff/pull/19291),[#19296](https://github.com/astral-sh/ruff/pull/19296),[#19292](https://github.com/astral-sh/ruff/pull/19292),[#19295](https://github.com/astral-sh/ruff/pull/19295),[#19297](https://github.com/astral-sh/ruff/pull/19297),[#19309](https://github.com/astral-sh/ruff/pull/19309))
## 0.12.3
### Preview features
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
### Bug fixes
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
### Documentation
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
## 0.12.2
### Preview features
- \[`flake8-pyi`\] Expand `Optional[A]` to `A | None` (`PYI016`) ([#18572](https://github.com/astral-sh/ruff/pull/18572))
- \[`pyupgrade`\] Mark `UP008` fix safe if no comments are in range ([#18683](https://github.com/astral-sh/ruff/pull/18683))
### Bug fixes
- \[`flake8-comprehensions`\] Fix `C420` to prepend whitespace when needed ([#18616](https://github.com/astral-sh/ruff/pull/18616))
- \[`perflint`\] Fix `PERF403` panic on attribute or subscription loop variable ([#19042](https://github.com/astral-sh/ruff/pull/19042))
- \[`pydocstyle`\] Fix `D413` infinite loop for parenthesized docstring ([#18930](https://github.com/astral-sh/ruff/pull/18930))
- \[`pylint`\] Fix `PLW0108` autofix introducing a syntax error when the lambda's body contains an assignment expression ([#18678](https://github.com/astral-sh/ruff/pull/18678))
- \[`refurb`\] Fix false positive on empty tuples (`FURB168`) ([#19058](https://github.com/astral-sh/ruff/pull/19058))
- \[`ruff`\] Allow more `field` calls from `attrs` (`RUF009`) ([#19021](https://github.com/astral-sh/ruff/pull/19021))
- \[`ruff`\] Fix syntax error introduced for an empty string followed by a u-prefixed string (`UP025`) ([#18899](https://github.com/astral-sh/ruff/pull/18899))
### Rule changes
- \[`flake8-executable`\] Allow `uvx` in shebang line (`EXE003`) ([#18967](https://github.com/astral-sh/ruff/pull/18967))
- \[`pandas`\] Avoid flagging `PD002` if `pandas` is not imported ([#18963](https://github.com/astral-sh/ruff/pull/18963))
- \[`pyupgrade`\] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`, `UP045`) ([#18682](https://github.com/astral-sh/ruff/pull/18682))
### Documentation
- Document link between `import-outside-top-level (PLC0415)` and `lint.flake8-tidy-imports.banned-module-level-imports` ([#18733](https://github.com/astral-sh/ruff/pull/18733))
- Fix description of the `format.skip-magic-trailing-comma` example ([#19095](https://github.com/astral-sh/ruff/pull/19095))
- \[`airflow`\] Make `AIR302` example error out-of-the-box ([#18988](https://github.com/astral-sh/ruff/pull/18988))
- \[`airflow`\] Make `AIR312` example error out-of-the-box ([#18989](https://github.com/astral-sh/ruff/pull/18989))
- \[`flake8-annotations`\] Make `ANN401` example error out-of-the-box ([#18974](https://github.com/astral-sh/ruff/pull/18974))
- \[`flake8-async`\] Make `ASYNC100` example error out-of-the-box ([#18993](https://github.com/astral-sh/ruff/pull/18993))
- \[`flake8-async`\] Make `ASYNC105` example error out-of-the-box ([#19002](https://github.com/astral-sh/ruff/pull/19002))
- \[`flake8-async`\] Make `ASYNC110` example error out-of-the-box ([#18975](https://github.com/astral-sh/ruff/pull/18975))
- \[`flake8-async`\] Make `ASYNC210` example error out-of-the-box ([#18977](https://github.com/astral-sh/ruff/pull/18977))
- \[`flake8-async`\] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples error out-of-the-box ([#18978](https://github.com/astral-sh/ruff/pull/18978))
- \[`flake8-async`\] Make `ASYNC251` example error out-of-the-box ([#18990](https://github.com/astral-sh/ruff/pull/18990))
- \[`flake8-bandit`\] Make `S201` example error out-of-the-box ([#19017](https://github.com/astral-sh/ruff/pull/19017))
- \[`flake8-bandit`\] Make `S604` and `S609` examples error out-of-the-box ([#19049](https://github.com/astral-sh/ruff/pull/19049))
- \[`flake8-bugbear`\] Make `B028` example error out-of-the-box ([#19054](https://github.com/astral-sh/ruff/pull/19054))
- \[`flake8-bugbear`\] Make `B911` example error out-of-the-box ([#19051](https://github.com/astral-sh/ruff/pull/19051))
- \[`flake8-datetimez`\] Make `DTZ011` example error out-of-the-box ([#19055](https://github.com/astral-sh/ruff/pull/19055))
- \[`flake8-datetimez`\] Make `DTZ901` example error out-of-the-box ([#19056](https://github.com/astral-sh/ruff/pull/19056))
- \[`flake8-pyi`\] Make `PYI032` example error out-of-the-box ([#19061](https://github.com/astral-sh/ruff/pull/19061))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI014`, `PYI015`) ([#19097](https://github.com/astral-sh/ruff/pull/19097))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI042`) ([#19101](https://github.com/astral-sh/ruff/pull/19101))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI059`) ([#19080](https://github.com/astral-sh/ruff/pull/19080))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI062`) ([#19079](https://github.com/astral-sh/ruff/pull/19079))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT023`) ([#19104](https://github.com/astral-sh/ruff/pull/19104))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT030`) ([#19105](https://github.com/astral-sh/ruff/pull/19105))
- \[`flake8-quotes`\] Make example error out-of-the-box (`Q003`) ([#19106](https://github.com/astral-sh/ruff/pull/19106))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM110`) ([#19113](https://github.com/astral-sh/ruff/pull/19113))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM113`) ([#19109](https://github.com/astral-sh/ruff/pull/19109))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM401`) ([#19110](https://github.com/astral-sh/ruff/pull/19110))
- \[`pyflakes`\] Fix backslash in docs (`F621`) ([#19098](https://github.com/astral-sh/ruff/pull/19098))
- \[`pylint`\] Fix `PLC0415` example ([#18970](https://github.com/astral-sh/ruff/pull/18970))
## 0.12.1
### Preview features
- \[`flake8-errmsg`\] Extend `EM101` to support byte strings ([#18867](https://github.com/astral-sh/ruff/pull/18867))
- \[`flake8-use-pathlib`\] Add autofix for `PTH202` ([#18763](https://github.com/astral-sh/ruff/pull/18763))
- \[`pygrep-hooks`\] Add `AsyncMock` methods to `invalid-mock-access` (`PGH005`) ([#18547](https://github.com/astral-sh/ruff/pull/18547))
- \[`pylint`\] Ignore `__init__.py` files in (`PLC0414`) ([#18400](https://github.com/astral-sh/ruff/pull/18400))
- \[`ruff`\] Trigger `RUF037` for empty string and byte strings ([#18862](https://github.com/astral-sh/ruff/pull/18862))
- [formatter] Fix missing blank lines before decorated classes in `.pyi` files ([#18888](https://github.com/astral-sh/ruff/pull/18888))
### Bug fixes
- Avoid generating diagnostics with per-file ignores ([#18801](https://github.com/astral-sh/ruff/pull/18801))
- Handle parenthesized arguments in `remove_argument` ([#18805](https://github.com/astral-sh/ruff/pull/18805))
- \[`flake8-logging`\] Avoid false positive for `exc_info=True` outside `logger.exception` (`LOG014`) ([#18737](https://github.com/astral-sh/ruff/pull/18737))
- \[`flake8-pytest-style`\] Enforce `pytest` import for decorators ([#18779](https://github.com/astral-sh/ruff/pull/18779))
- \[`flake8-pytest-style`\] Mark autofix for `PT001` and `PT023` as unsafe if there's comments in the decorator ([#18792](https://github.com/astral-sh/ruff/pull/18792))
- \[`flake8-pytest-style`\] `PT001`/`PT023` fix makes syntax error on parenthesized decorator ([#18782](https://github.com/astral-sh/ruff/pull/18782))
- \[`flake8-raise`\] Make fix unsafe if it deletes comments (`RSE102`) ([#18788](https://github.com/astral-sh/ruff/pull/18788))
- \[`flake8-simplify`\] Fix `SIM911` autofix creating a syntax error ([#18793](https://github.com/astral-sh/ruff/pull/18793))
- \[`flake8-simplify`\] Fix false negatives for shadowed bindings (`SIM910`, `SIM911`) ([#18794](https://github.com/astral-sh/ruff/pull/18794))
- \[`flake8-simplify`\] Preserve original behavior for `except ()` and bare `except` (`SIM105`) ([#18213](https://github.com/astral-sh/ruff/pull/18213))
- \[`flake8-pyi`\] Fix `PYI041`'s fix causing `TypeError` with `None | None | ...` ([#18637](https://github.com/astral-sh/ruff/pull/18637))
- \[`perflint`\] Fix `PERF101` autofix creating a syntax error and mark autofix as unsafe if there are comments in the `list` call expr ([#18803](https://github.com/astral-sh/ruff/pull/18803))
- \[`perflint`\] Fix false negative in `PERF401` ([#18866](https://github.com/astral-sh/ruff/pull/18866))
- \[`pylint`\] Avoid flattening nested `min`/`max` when outer call has single argument (`PLW3301`) ([#16885](https://github.com/astral-sh/ruff/pull/16885))
- \[`pylint`\] Fix `PLC2801` autofix creating a syntax error ([#18857](https://github.com/astral-sh/ruff/pull/18857))
- \[`pylint`\] Mark `PLE0241` autofix as unsafe if there's comments in the base classes ([#18832](https://github.com/astral-sh/ruff/pull/18832))
- \[`pylint`\] Suppress `PLE2510`/`PLE2512`/`PLE2513`/`PLE2514`/`PLE2515` autofix if the text contains an odd number of backslashes ([#18856](https://github.com/astral-sh/ruff/pull/18856))
- \[`refurb`\] Detect more exotic float literals in `FURB164` ([#18925](https://github.com/astral-sh/ruff/pull/18925))
- \[`refurb`\] Fix `FURB163` autofix creating a syntax error for `yield` expressions ([#18756](https://github.com/astral-sh/ruff/pull/18756))
- \[`refurb`\] Mark `FURB129` autofix as unsafe if there's comments in the `readlines` call ([#18858](https://github.com/astral-sh/ruff/pull/18858))
- \[`ruff`\] Fix false positives and negatives in `RUF010` ([#18690](https://github.com/astral-sh/ruff/pull/18690))
- Fix casing of `analyze.direction` variant names ([#18892](https://github.com/astral-sh/ruff/pull/18892))
### Rule changes
- Fix f-string interpolation escaping in generated fixes ([#18882](https://github.com/astral-sh/ruff/pull/18882))
- \[`flake8-return`\] Mark `RET501` fix unsafe if comments are inside ([#18780](https://github.com/astral-sh/ruff/pull/18780))
- \[`flake8-async`\] Fix detection for large integer sleep durations in `ASYNC116` rule ([#18767](https://github.com/astral-sh/ruff/pull/18767))
- \[`flake8-async`\] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments ([#18753](https://github.com/astral-sh/ruff/pull/18753))
- \[`flake8-bugbear`\] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments ([#18755](https://github.com/astral-sh/ruff/pull/18755))
- \[`flake8-comprehension`\] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension ([#18768](https://github.com/astral-sh/ruff/pull/18768))
- \[`flake8-comprehensions`\] Handle template strings for comprehension fixes ([#18710](https://github.com/astral-sh/ruff/pull/18710))
- \[`flake8-future-annotations`\] Add autofix (`FA100`) ([#18903](https://github.com/astral-sh/ruff/pull/18903))
- \[`pyflakes`\] Mark `F504`/`F522`/`F523` autofix as unsafe if there's a call with side effect ([#18839](https://github.com/astral-sh/ruff/pull/18839))
- \[`pylint`\] Allow fix with comments and document performance implications (`PLW3301`) ([#18936](https://github.com/astral-sh/ruff/pull/18936))
- \[`pylint`\] Detect more exotic `NaN` literals in `PLW0177` ([#18630](https://github.com/astral-sh/ruff/pull/18630))
- \[`pylint`\] Fix `PLC1802` autofix creating a syntax error and mark autofix as unsafe if there's comments in the `len` call ([#18836](https://github.com/astral-sh/ruff/pull/18836))
- \[`pyupgrade`\] Extend version detection to include `sys.version_info.major` (`UP036`) ([#18633](https://github.com/astral-sh/ruff/pull/18633))
- \[`ruff`\] Add lint rule `RUF064` for calling `chmod` with non-octal integers ([#18541](https://github.com/astral-sh/ruff/pull/18541))
- \[`ruff`\] Added `cls.__dict__.get('__annotations__')` check (`RUF063`) ([#18233](https://github.com/astral-sh/ruff/pull/18233))
- \[`ruff`\] Frozen `dataclass` default should be valid (`RUF009`) ([#18735](https://github.com/astral-sh/ruff/pull/18735))
### Server
- Consider virtual path for various server actions ([#18910](https://github.com/astral-sh/ruff/pull/18910))
### Documentation
- Add fix safety sections ([#18940](https://github.com/astral-sh/ruff/pull/18940),[#18841](https://github.com/astral-sh/ruff/pull/18841),[#18802](https://github.com/astral-sh/ruff/pull/18802),[#18837](https://github.com/astral-sh/ruff/pull/18837),[#18800](https://github.com/astral-sh/ruff/pull/18800),[#18415](https://github.com/astral-sh/ruff/pull/18415),[#18853](https://github.com/astral-sh/ruff/pull/18853),[#18842](https://github.com/astral-sh/ruff/pull/18842))
- Use updated pre-commit id ([#18718](https://github.com/astral-sh/ruff/pull/18718))
- \[`perflint`\] Small docs improvement to `PERF401` ([#18786](https://github.com/astral-sh/ruff/pull/18786))
- \[`pyupgrade`\]: Use `super()`, not `__super__` in error messages (`UP008`) ([#18743](https://github.com/astral-sh/ruff/pull/18743))
- \[`flake8-pie`\] Small docs fix to `PIE794` ([#18829](https://github.com/astral-sh/ruff/pull/18829))
- \[`flake8-pyi`\] Correct `collections-named-tuple` example to use PascalCase assignment ([#16884](https://github.com/astral-sh/ruff/pull/16884))
- \[`flake8-pie`\] Add note on type checking benefits to `unnecessary-dict-kwargs` (`PIE804`) ([#18666](https://github.com/astral-sh/ruff/pull/18666))
- \[`pycodestyle`\] Clarify PEP 8 relationship to `whitespace-around-operator` rules ([#18870](https://github.com/astral-sh/ruff/pull/18870))
### Other changes
- Disallow newlines in format specifiers of single quoted f- or t-strings ([#18708](https://github.com/astral-sh/ruff/pull/18708))
- \[`flake8-logging`\] Add fix safety section to `LOG002` ([#18840](https://github.com/astral-sh/ruff/pull/18840))
- \[`pyupgrade`\] Add fix safety section to `UP010` ([#18838](https://github.com/astral-sh/ruff/pull/18838))
## 0.12.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration
guide and overview of the changes!
### Breaking changes
- **Several rules can now add `from __future__ import annotations` automatically**
- **Detection of more syntax errors**
`TC001`, `TC002`, `TC003`, `RUF013`, and `UP037` now add `from __future__ import annotations` as part of their fixes when the
`lint.future-annotations` setting is enabled. This allows the rules to move
more imports into `TYPE_CHECKING` blocks (`TC001`, `TC002`, and `TC003`),
use PEP 604 union syntax on Python versions before 3.10 (`RUF013`), and
unquote more annotations (`UP037`).
Ruff now detects version-related syntax errors, such as the use of the `match`
statement on Python versions before 3.10, and syntax errors emitted by
CPython's compiler, such as irrefutable `match` patterns before the final
`case` arm.
- **Full module paths are now used to verify first-party modules**
- **New default Python version handling for syntax errors**
Ruff now checks that the full path to a module exists on disk before
categorizing it as a first-party import. This change makes first-party
import detection more accurate, helping to avoid false positives on local
directories with the same name as a third-party dependency, for example. See
the [FAQ
section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) on import categorization for more details.
Ruff will default to the *latest* supported Python version (3.13) when
checking for the version-related syntax errors mentioned above to prevent
false positives in projects without a Python version configured. The default
in all other cases, like applying lint rules, is unchanged and remains at the
minimum supported Python version (3.9).
- **Deprecated rules must now be selected by exact rule code**
- **Updated f-string formatting**
Ruff will no longer activate deprecated rules selected by their group name
or prefix. As noted below, the two remaining deprecated rules were also
removed in this release, so this won't affect any current rules, but it will
still affect any deprecations in the future.
Ruff now formats multi-line f-strings with format specifiers to avoid adding a
line break after the format specifier. This addresses a change to the Python
grammar in version 3.13.4 that made such a line break a syntax error.
- **The deprecated macOS configuration directory fallback has been removed**
- **`rust-toolchain.toml` is no longer included in source distributions**
Ruff will no longer look for a user-level configuration file at
`~/Library/Application Support/ruff/ruff.toml` on macOS. This feature was
deprecated in v0.5 in favor of using the [XDG
specification](https://specifications.freedesktop.org/basedir-spec/latest/)
(usually resolving to `~/.config/ruff/ruff.toml`), like on Linux. The
fallback and accompanying deprecation warning have now been removed.
The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
minimum supported Rust version (MSRV) for development and building release
artifacts. However, when present in source distributions, it would also cause
downstream package maintainers to pull in the same Rust toolchain, even if
their available toolchain was MSRV-compatible.
### Removed Rules
The following rules have been removed:
- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name) (`PD901`)
- [`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance) (`UP038`)
- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
(`S320`)
### Deprecated Rules
The following rules have been deprecated:
- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/)
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`airflow-dag-no-schedule-argument`](https://docs.astral.sh/ruff/rules/airflow-dag-no-schedule-argument)
(`AIR002`)
- [`airflow3-removal`](https://docs.astral.sh/ruff/rules/airflow3-removal) (`AIR301`)
- [`airflow3-moved-to-provider`](https://docs.astral.sh/ruff/rules/airflow3-moved-to-provider)
(`AIR302`)
- [`airflow3-suggested-update`](https://docs.astral.sh/ruff/rules/airflow3-suggested-update)
(`AIR311`)
- [`airflow3-suggested-to-move-to-provider`](https://docs.astral.sh/ruff/rules/airflow3-suggested-to-move-to-provider)
(`AIR312`)
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever) (`ASYNC116`)
- [`f-string-number-format`](https://docs.astral.sh/ruff/rules/f-string-number-format) (`FURB116`)
- [`os-symlink`](https://docs.astral.sh/ruff/rules/os-symlink) (`PTH211`)
- [`generic-not-last-base-class`](https://docs.astral.sh/ruff/rules/generic-not-last-base-class)
(`PYI059`)
- [`redundant-none-literal`](https://docs.astral.sh/ruff/rules/redundant-none-literal) (`PYI061`)
- [`pytest-raises-ambiguous-pattern`](https://docs.astral.sh/ruff/rules/pytest-raises-ambiguous-pattern)
(`RUF043`)
- [`unused-unpacked-variable`](https://docs.astral.sh/ruff/rules/unused-unpacked-variable)
(`RUF059`)
- [`useless-class-metaclass-type`](https://docs.astral.sh/ruff/rules/useless-class-metaclass-type)
(`UP050`)
- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`)
- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`)
- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`)
- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`)
- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`)
- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`)
- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`)
- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`)
- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`)
- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`)
- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`)
- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`)
- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`)
- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`)
- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`)
- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`)
- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`)
- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`)
- [`non-pep604-annotation-optional`] (`UP045`)
- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`)
- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`)
- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`)
The following behaviors have been stabilized:
- [`assert-raises-exception`](https://docs.astral.sh/ruff/rules/assert-raises-exception) (`B017`)
now checks for direct calls to `unittest.TestCase.assert_raises` and `pytest.raises` instead of
only the context manager forms.
- [`missing-trailing-comma`](https://docs.astral.sh/ruff/rules/missing-trailing-comma) (`COM812`)
and [`prohibited-trailing-comma`](https://docs.astral.sh/ruff/rules/prohibited-trailing-comma)
(`COM819`) now check for trailing commas in PEP 695 type parameter lists.
- [`raw-string-in-exception`](https://docs.astral.sh/ruff/rules/raw-string-in-exception) (`EM101`)
now also checks for byte strings in exception messages.
- [`invalid-mock-access`](https://docs.astral.sh/ruff/rules/invalid-mock-access) (`PGH005`) now
checks for `AsyncMock` methods like `not_awaited` in addition to the synchronous variants.
- [`useless-import-alias`](https://docs.astral.sh/ruff/rules/useless-import-alias) (`PLC0414`) no
longer applies to `__init__.py` files, where it conflicted with one of the suggested fixes for
[`unused-import`](https://docs.astral.sh/ruff/rules/unused-import) (`F401`).
- [`bidirectional-unicode`](https://docs.astral.sh/ruff/rules/bidirectional-unicode) (`PLE2502`) now
also checks for U+061C (Arabic Letter Mark).
- The fix for
[`multiple-with-statements`](https://docs.astral.sh/ruff/rules/multiple-with-statements)
(`SIM117`) is now marked as always safe.
- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in
addition to list literals and variables.
- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe.
- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify
expressions to use `or` instead of an `if` expression, where possible.
- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well
as inline comments.
- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings,
as well as lists and tuples of literal strings, as trusted input.
- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that
include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to
plain `bool` annotations.
- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules.
`UP007` now applies only to `typing.Union`, while
[`non-pep604-annotation-optional`] (`UP045`) checks for use of
`typing.Optional`. `UP045` has also been stabilized in this release, but you
may need to update existing `include`, `ignore`, or `noqa` settings to
accommodate this change.
### Preview features
- \[`pyupgrade`\] Enable `UP043` in stub files ([#20027](https://github.com/astral-sh/ruff/pull/20027))
- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368))
- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664))
### Bug fixes
- \[`pyupgrade`\] Apply `UP008` only when the `__class__` cell exists ([#19424](https://github.com/astral-sh/ruff/pull/19424))
- \[`ruff`\] Fix empty f-string detection in `in-empty-collection` (`RUF060`) ([#20249](https://github.com/astral-sh/ruff/pull/20249))
- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706))
- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542))
- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673))
- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704))
- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611))
- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428))
- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472))
- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479))
- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688))
- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592))
- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614))
- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598))
- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631))
- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509))
- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665))
### Rule changes
- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408))
- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532))
- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311))
- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545))
- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478))
- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645))
### Server
- Add support for using uv as an alternative formatter backend ([#19665](https://github.com/astral-sh/ruff/pull/19665))
- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627))
### Documentation
- \[`pep8-naming`\] Fix formatting of `__all__` (`N816`) ([#20301](https://github.com/astral-sh/ruff/pull/20301))
## 0.12.x
See [changelogs/0.12.x](./changelogs/0.12.x.md)
- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709))
- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491))
- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613))
- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615))
## 0.11.x
@@ -158,3 +556,12 @@ See [changelogs/0.2.x](./changelogs/0.2.x.md)
## 0.1.x
See [changelogs/0.1.x](./changelogs/0.1.x.md)
[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument
[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation
[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp
[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional
[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union
[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for
[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true
[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa

135
Cargo.lock generated
View File

@@ -257,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.4"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
[[package]]
name = "bitvec"
@@ -408,9 +408,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.47"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.47"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [
"anstream",
"anstyle",
@@ -461,9 +461,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.47"
version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
dependencies = [
"heck",
"proc-macro2",
@@ -603,7 +603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -612,7 +612,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1035,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1241,7 +1241,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"ignore",
"walkdir",
]
@@ -1521,7 +1521,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"inotify-sys",
"libc",
]
@@ -1537,9 +1537,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.43.2"
version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [
"console 0.15.11",
"globset",
@@ -1617,7 +1617,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1681,7 +1681,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1728,9 +1728,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
[[package]]
name = "js-sys"
version = "0.3.78"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1770,9 +1770,9 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libcst"
version = "1.8.4"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052ef5d9fc958a51aeebdf3713573b36c6fd6eed0bf0e60e204d2c0f8cf19b9f"
checksum = "ae28ddc5b90c3e3146a21d051ca095cbc8d932ad8714cf65ddf71a9abb35684c"
dependencies = [
"annotate-snippets",
"libcst_derive",
@@ -1785,9 +1785,9 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "1.8.4"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a91a751afee92cbdd59d4bc6754c7672712eec2d30a308f23de4e3287b2929cb"
checksum = "dc2de5c2f62bcf8a4f7290b1854388b262c4b68f1db1a3ee3ef6d4c1319b00a3"
dependencies = [
"quote",
"syn",
@@ -1809,7 +1809,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"libc",
"redox_syscall",
]
@@ -1850,9 +1850,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.28"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lsp-server"
@@ -2014,7 +2014,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2026,7 +2026,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2054,7 +2054,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"fsevent-sys",
"inotify",
"kqueue",
@@ -2659,7 +2659,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
]
[[package]]
@@ -2721,13 +2721,13 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.13.0"
version = "0.12.11"
dependencies = [
"anyhow",
"argfile",
"assert_fs",
"bincode 2.0.1",
"bitflags 2.9.4",
"bitflags 2.9.3",
"cachedir",
"clap",
"clap_complete_command",
@@ -2977,11 +2977,11 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.13.0"
version = "0.12.11"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.9.4",
"bitflags 2.9.3",
"clap",
"colored 3.0.0",
"fern",
@@ -3086,7 +3086,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.9.4",
"bitflags 2.9.3",
"compact_str",
"get-size2",
"is-macro",
@@ -3174,7 +3174,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"itertools 0.14.0",
"ruff_python_ast",
"unic-ucd-category",
@@ -3185,7 +3185,7 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
"bitflags 2.9.3",
"bstr",
"compact_str",
"get-size2",
@@ -3210,7 +3210,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"insta",
"is-macro",
"ruff_cache",
@@ -3231,7 +3231,7 @@ dependencies = [
name = "ruff_python_stdlib"
version = "0.0.0"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"unicode-ident",
]
@@ -3315,7 +3315,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.13.0"
version = "0.12.11"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3408,11 +3408,11 @@ version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -3805,7 +3805,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -4218,7 +4218,7 @@ dependencies = [
name = "ty_ide"
version = "0.0.0"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
"camino",
"get-size2",
"insta",
@@ -4283,7 +4283,7 @@ name = "ty_python_semantic"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
"bitflags 2.9.3",
"bitvec",
"camino",
"colored 3.0.0",
@@ -4336,7 +4336,7 @@ name = "ty_server"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
"bitflags 2.9.3",
"crossbeam",
"dunce",
"insta",
@@ -4379,7 +4379,7 @@ name = "ty_test"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
"bitflags 2.9.3",
"camino",
"colored 3.0.0",
"insta",
@@ -4738,22 +4738,21 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.101"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.101"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
@@ -4765,9 +4764,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.51"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
@@ -4778,9 +4777,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.101"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4788,9 +4787,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.101"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
@@ -4801,18 +4800,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.101"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.51"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
dependencies = [
"js-sys",
"minicov",
@@ -4823,9 +4822,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.51"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [
"proc-macro2",
"quote",
@@ -4834,9 +4833,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.78"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4878,7 +4877,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5117,7 +5116,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.9.3",
]
[[package]]

View File

@@ -115,7 +115,7 @@ jiff = { version = "0.2.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "1.0.0" }
libc = { version = "0.2.153" }
libcst = { version = "1.8.4", default-features = false }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
lsp-server = { version = "0.7.6" }
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
@@ -251,14 +251,6 @@ rest_pat_in_fully_bound_structs = "warn"
redundant_clone = "warn"
debug_assert_with_mut_call = "warn"
unused_peekable = "warn"
# This lint sometimes flags code whose `if` and `else`
# bodies could be flipped when a `!` operator is removed.
# While perhaps sometimes a good idea, it is also often
# not a good idea due to other factors impacting
# readability. For example, if flipping the bodies results
# in the `if` being an order of magnitude bigger than the
# `else`, then some might consider that harder to read.
if_not_else = "allow"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.13.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.0/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.11/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.11/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.0
rev: v0.12.11
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,551 +0,0 @@
# Changelog 0.12.x
## 0.12.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration
guide and overview of the changes!
### Breaking changes
- **Detection of more syntax errors**
Ruff now detects version-related syntax errors, such as the use of the `match`
statement on Python versions before 3.10, and syntax errors emitted by
CPython's compiler, such as irrefutable `match` patterns before the final
`case` arm.
- **New default Python version handling for syntax errors**
Ruff will default to the *latest* supported Python version (3.13) when
checking for the version-related syntax errors mentioned above to prevent
false positives in projects without a Python version configured. The default
in all other cases, like applying lint rules, is unchanged and remains at the
minimum supported Python version (3.9).
- **Updated f-string formatting**
Ruff now formats multi-line f-strings with format specifiers to avoid adding a
line break after the format specifier. This addresses a change to the Python
grammar in version 3.13.4 that made such a line break a syntax error.
- **`rust-toolchain.toml` is no longer included in source distributions**
The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
minimum supported Rust version (MSRV) for development and building release
artifacts. However, when present in source distributions, it would also cause
downstream package maintainers to pull in the same Rust toolchain, even if
their available toolchain was MSRV-compatible.
### Removed Rules
The following rules have been removed:
- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
(`S320`)
### Deprecated Rules
The following rules have been deprecated:
- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/)
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`)
- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`)
- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`)
- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`)
- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`)
- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`)
- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`)
- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`)
- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`)
- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`)
- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`)
- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`)
- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`)
- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`)
- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`)
- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`)
- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`)
- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`)
- [`non-pep604-annotation-optional`] (`UP045`)
- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`)
- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`)
- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`)
The following behaviors have been stabilized:
- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in
addition to list literals and variables.
- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe.
- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify
expressions to use `or` instead of an `if` expression, where possible.
- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well
as inline comments.
- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings,
as well as lists and tuples of literal strings, as trusted input.
- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that
include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to
plain `bool` annotations.
- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules.
`UP007` now applies only to `typing.Union`, while
[`non-pep604-annotation-optional`] (`UP045`) checks for use of
`typing.Optional`. `UP045` has also been stabilized in this release, but you
may need to update existing `include`, `ignore`, or `noqa` settings to
accommodate this change.
### Preview features
- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368))
- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664))
### Bug fixes
- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706))
- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542))
- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673))
- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704))
- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611))
- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428))
- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472))
- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479))
- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688))
- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592))
- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614))
- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598))
- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631))
- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509))
- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665))
### Rule changes
- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408))
- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532))
- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311))
- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545))
- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478))
- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645))
### Server
- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627))
### Documentation
- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709))
- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491))
- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613))
- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615))
## 0.12.1
### Preview features
- \[`flake8-errmsg`\] Extend `EM101` to support byte strings ([#18867](https://github.com/astral-sh/ruff/pull/18867))
- \[`flake8-use-pathlib`\] Add autofix for `PTH202` ([#18763](https://github.com/astral-sh/ruff/pull/18763))
- \[`pygrep-hooks`\] Add `AsyncMock` methods to `invalid-mock-access` (`PGH005`) ([#18547](https://github.com/astral-sh/ruff/pull/18547))
- \[`pylint`\] Ignore `__init__.py` files in (`PLC0414`) ([#18400](https://github.com/astral-sh/ruff/pull/18400))
- \[`ruff`\] Trigger `RUF037` for empty string and byte strings ([#18862](https://github.com/astral-sh/ruff/pull/18862))
- [formatter] Fix missing blank lines before decorated classes in `.pyi` files ([#18888](https://github.com/astral-sh/ruff/pull/18888))
### Bug fixes
- Avoid generating diagnostics with per-file ignores ([#18801](https://github.com/astral-sh/ruff/pull/18801))
- Handle parenthesized arguments in `remove_argument` ([#18805](https://github.com/astral-sh/ruff/pull/18805))
- \[`flake8-logging`\] Avoid false positive for `exc_info=True` outside `logger.exception` (`LOG014`) ([#18737](https://github.com/astral-sh/ruff/pull/18737))
- \[`flake8-pytest-style`\] Enforce `pytest` import for decorators ([#18779](https://github.com/astral-sh/ruff/pull/18779))
- \[`flake8-pytest-style`\] Mark autofix for `PT001` and `PT023` as unsafe if there's comments in the decorator ([#18792](https://github.com/astral-sh/ruff/pull/18792))
- \[`flake8-pytest-style`\] `PT001`/`PT023` fix makes syntax error on parenthesized decorator ([#18782](https://github.com/astral-sh/ruff/pull/18782))
- \[`flake8-raise`\] Make fix unsafe if it deletes comments (`RSE102`) ([#18788](https://github.com/astral-sh/ruff/pull/18788))
- \[`flake8-simplify`\] Fix `SIM911` autofix creating a syntax error ([#18793](https://github.com/astral-sh/ruff/pull/18793))
- \[`flake8-simplify`\] Fix false negatives for shadowed bindings (`SIM910`, `SIM911`) ([#18794](https://github.com/astral-sh/ruff/pull/18794))
- \[`flake8-simplify`\] Preserve original behavior for `except ()` and bare `except` (`SIM105`) ([#18213](https://github.com/astral-sh/ruff/pull/18213))
- \[`flake8-pyi`\] Fix `PYI041`'s fix causing `TypeError` with `None | None | ...` ([#18637](https://github.com/astral-sh/ruff/pull/18637))
- \[`perflint`\] Fix `PERF101` autofix creating a syntax error and mark autofix as unsafe if there are comments in the `list` call expr ([#18803](https://github.com/astral-sh/ruff/pull/18803))
- \[`perflint`\] Fix false negative in `PERF401` ([#18866](https://github.com/astral-sh/ruff/pull/18866))
- \[`pylint`\] Avoid flattening nested `min`/`max` when outer call has single argument (`PLW3301`) ([#16885](https://github.com/astral-sh/ruff/pull/16885))
- \[`pylint`\] Fix `PLC2801` autofix creating a syntax error ([#18857](https://github.com/astral-sh/ruff/pull/18857))
- \[`pylint`\] Mark `PLE0241` autofix as unsafe if there's comments in the base classes ([#18832](https://github.com/astral-sh/ruff/pull/18832))
- \[`pylint`\] Suppress `PLE2510`/`PLE2512`/`PLE2513`/`PLE2514`/`PLE2515` autofix if the text contains an odd number of backslashes ([#18856](https://github.com/astral-sh/ruff/pull/18856))
- \[`refurb`\] Detect more exotic float literals in `FURB164` ([#18925](https://github.com/astral-sh/ruff/pull/18925))
- \[`refurb`\] Fix `FURB163` autofix creating a syntax error for `yield` expressions ([#18756](https://github.com/astral-sh/ruff/pull/18756))
- \[`refurb`\] Mark `FURB129` autofix as unsafe if there's comments in the `readlines` call ([#18858](https://github.com/astral-sh/ruff/pull/18858))
- \[`ruff`\] Fix false positives and negatives in `RUF010` ([#18690](https://github.com/astral-sh/ruff/pull/18690))
- Fix casing of `analyze.direction` variant names ([#18892](https://github.com/astral-sh/ruff/pull/18892))
### Rule changes
- Fix f-string interpolation escaping in generated fixes ([#18882](https://github.com/astral-sh/ruff/pull/18882))
- \[`flake8-return`\] Mark `RET501` fix unsafe if comments are inside ([#18780](https://github.com/astral-sh/ruff/pull/18780))
- \[`flake8-async`\] Fix detection for large integer sleep durations in `ASYNC116` rule ([#18767](https://github.com/astral-sh/ruff/pull/18767))
- \[`flake8-async`\] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments ([#18753](https://github.com/astral-sh/ruff/pull/18753))
- \[`flake8-bugbear`\] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments ([#18755](https://github.com/astral-sh/ruff/pull/18755))
- \[`flake8-comprehension`\] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension ([#18768](https://github.com/astral-sh/ruff/pull/18768))
- \[`flake8-comprehensions`\] Handle template strings for comprehension fixes ([#18710](https://github.com/astral-sh/ruff/pull/18710))
- \[`flake8-future-annotations`\] Add autofix (`FA100`) ([#18903](https://github.com/astral-sh/ruff/pull/18903))
- \[`pyflakes`\] Mark `F504`/`F522`/`F523` autofix as unsafe if there's a call with side effect ([#18839](https://github.com/astral-sh/ruff/pull/18839))
- \[`pylint`\] Allow fix with comments and document performance implications (`PLW3301`) ([#18936](https://github.com/astral-sh/ruff/pull/18936))
- \[`pylint`\] Detect more exotic `NaN` literals in `PLW0177` ([#18630](https://github.com/astral-sh/ruff/pull/18630))
- \[`pylint`\] Fix `PLC1802` autofix creating a syntax error and mark autofix as unsafe if there's comments in the `len` call ([#18836](https://github.com/astral-sh/ruff/pull/18836))
- \[`pyupgrade`\] Extend version detection to include `sys.version_info.major` (`UP036`) ([#18633](https://github.com/astral-sh/ruff/pull/18633))
- \[`ruff`\] Add lint rule `RUF064` for calling `chmod` with non-octal integers ([#18541](https://github.com/astral-sh/ruff/pull/18541))
- \[`ruff`\] Added `cls.__dict__.get('__annotations__')` check (`RUF063`) ([#18233](https://github.com/astral-sh/ruff/pull/18233))
- \[`ruff`\] Frozen `dataclass` default should be valid (`RUF009`) ([#18735](https://github.com/astral-sh/ruff/pull/18735))
### Server
- Consider virtual path for various server actions ([#18910](https://github.com/astral-sh/ruff/pull/18910))
### Documentation
- Add fix safety sections ([#18940](https://github.com/astral-sh/ruff/pull/18940),[#18841](https://github.com/astral-sh/ruff/pull/18841),[#18802](https://github.com/astral-sh/ruff/pull/18802),[#18837](https://github.com/astral-sh/ruff/pull/18837),[#18800](https://github.com/astral-sh/ruff/pull/18800),[#18415](https://github.com/astral-sh/ruff/pull/18415),[#18853](https://github.com/astral-sh/ruff/pull/18853),[#18842](https://github.com/astral-sh/ruff/pull/18842))
- Use updated pre-commit id ([#18718](https://github.com/astral-sh/ruff/pull/18718))
- \[`perflint`\] Small docs improvement to `PERF401` ([#18786](https://github.com/astral-sh/ruff/pull/18786))
- \[`pyupgrade`\]: Use `super()`, not `__super__` in error messages (`UP008`) ([#18743](https://github.com/astral-sh/ruff/pull/18743))
- \[`flake8-pie`\] Small docs fix to `PIE794` ([#18829](https://github.com/astral-sh/ruff/pull/18829))
- \[`flake8-pyi`\] Correct `collections-named-tuple` example to use PascalCase assignment ([#16884](https://github.com/astral-sh/ruff/pull/16884))
- \[`flake8-pie`\] Add note on type checking benefits to `unnecessary-dict-kwargs` (`PIE804`) ([#18666](https://github.com/astral-sh/ruff/pull/18666))
- \[`pycodestyle`\] Clarify PEP 8 relationship to `whitespace-around-operator` rules ([#18870](https://github.com/astral-sh/ruff/pull/18870))
### Other changes
- Disallow newlines in format specifiers of single quoted f- or t-strings ([#18708](https://github.com/astral-sh/ruff/pull/18708))
- \[`flake8-logging`\] Add fix safety section to `LOG002` ([#18840](https://github.com/astral-sh/ruff/pull/18840))
- \[`pyupgrade`\] Add fix safety section to `UP010` ([#18838](https://github.com/astral-sh/ruff/pull/18838))
## 0.12.2
### Preview features
- \[`flake8-pyi`\] Expand `Optional[A]` to `A | None` (`PYI016`) ([#18572](https://github.com/astral-sh/ruff/pull/18572))
- \[`pyupgrade`\] Mark `UP008` fix safe if no comments are in range ([#18683](https://github.com/astral-sh/ruff/pull/18683))
### Bug fixes
- \[`flake8-comprehensions`\] Fix `C420` to prepend whitespace when needed ([#18616](https://github.com/astral-sh/ruff/pull/18616))
- \[`perflint`\] Fix `PERF403` panic on attribute or subscription loop variable ([#19042](https://github.com/astral-sh/ruff/pull/19042))
- \[`pydocstyle`\] Fix `D413` infinite loop for parenthesized docstring ([#18930](https://github.com/astral-sh/ruff/pull/18930))
- \[`pylint`\] Fix `PLW0108` autofix introducing a syntax error when the lambda's body contains an assignment expression ([#18678](https://github.com/astral-sh/ruff/pull/18678))
- \[`refurb`\] Fix false positive on empty tuples (`FURB168`) ([#19058](https://github.com/astral-sh/ruff/pull/19058))
- \[`ruff`\] Allow more `field` calls from `attrs` (`RUF009`) ([#19021](https://github.com/astral-sh/ruff/pull/19021))
- \[`ruff`\] Fix syntax error introduced for an empty string followed by a u-prefixed string (`UP025`) ([#18899](https://github.com/astral-sh/ruff/pull/18899))
### Rule changes
- \[`flake8-executable`\] Allow `uvx` in shebang line (`EXE003`) ([#18967](https://github.com/astral-sh/ruff/pull/18967))
- \[`pandas`\] Avoid flagging `PD002` if `pandas` is not imported ([#18963](https://github.com/astral-sh/ruff/pull/18963))
- \[`pyupgrade`\] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`, `UP045`) ([#18682](https://github.com/astral-sh/ruff/pull/18682))
### Documentation
- Document link between `import-outside-top-level (PLC0415)` and `lint.flake8-tidy-imports.banned-module-level-imports` ([#18733](https://github.com/astral-sh/ruff/pull/18733))
- Fix description of the `format.skip-magic-trailing-comma` example ([#19095](https://github.com/astral-sh/ruff/pull/19095))
- \[`airflow`\] Make `AIR302` example error out-of-the-box ([#18988](https://github.com/astral-sh/ruff/pull/18988))
- \[`airflow`\] Make `AIR312` example error out-of-the-box ([#18989](https://github.com/astral-sh/ruff/pull/18989))
- \[`flake8-annotations`\] Make `ANN401` example error out-of-the-box ([#18974](https://github.com/astral-sh/ruff/pull/18974))
- \[`flake8-async`\] Make `ASYNC100` example error out-of-the-box ([#18993](https://github.com/astral-sh/ruff/pull/18993))
- \[`flake8-async`\] Make `ASYNC105` example error out-of-the-box ([#19002](https://github.com/astral-sh/ruff/pull/19002))
- \[`flake8-async`\] Make `ASYNC110` example error out-of-the-box ([#18975](https://github.com/astral-sh/ruff/pull/18975))
- \[`flake8-async`\] Make `ASYNC210` example error out-of-the-box ([#18977](https://github.com/astral-sh/ruff/pull/18977))
- \[`flake8-async`\] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples error out-of-the-box ([#18978](https://github.com/astral-sh/ruff/pull/18978))
- \[`flake8-async`\] Make `ASYNC251` example error out-of-the-box ([#18990](https://github.com/astral-sh/ruff/pull/18990))
- \[`flake8-bandit`\] Make `S201` example error out-of-the-box ([#19017](https://github.com/astral-sh/ruff/pull/19017))
- \[`flake8-bandit`\] Make `S604` and `S609` examples error out-of-the-box ([#19049](https://github.com/astral-sh/ruff/pull/19049))
- \[`flake8-bugbear`\] Make `B028` example error out-of-the-box ([#19054](https://github.com/astral-sh/ruff/pull/19054))
- \[`flake8-bugbear`\] Make `B911` example error out-of-the-box ([#19051](https://github.com/astral-sh/ruff/pull/19051))
- \[`flake8-datetimez`\] Make `DTZ011` example error out-of-the-box ([#19055](https://github.com/astral-sh/ruff/pull/19055))
- \[`flake8-datetimez`\] Make `DTZ901` example error out-of-the-box ([#19056](https://github.com/astral-sh/ruff/pull/19056))
- \[`flake8-pyi`\] Make `PYI032` example error out-of-the-box ([#19061](https://github.com/astral-sh/ruff/pull/19061))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI014`, `PYI015`) ([#19097](https://github.com/astral-sh/ruff/pull/19097))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI042`) ([#19101](https://github.com/astral-sh/ruff/pull/19101))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI059`) ([#19080](https://github.com/astral-sh/ruff/pull/19080))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI062`) ([#19079](https://github.com/astral-sh/ruff/pull/19079))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT023`) ([#19104](https://github.com/astral-sh/ruff/pull/19104))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT030`) ([#19105](https://github.com/astral-sh/ruff/pull/19105))
- \[`flake8-quotes`\] Make example error out-of-the-box (`Q003`) ([#19106](https://github.com/astral-sh/ruff/pull/19106))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM110`) ([#19113](https://github.com/astral-sh/ruff/pull/19113))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM113`) ([#19109](https://github.com/astral-sh/ruff/pull/19109))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM401`) ([#19110](https://github.com/astral-sh/ruff/pull/19110))
- \[`pyflakes`\] Fix backslash in docs (`F621`) ([#19098](https://github.com/astral-sh/ruff/pull/19098))
- \[`pylint`\] Fix `PLC0415` example ([#18970](https://github.com/astral-sh/ruff/pull/18970))
## 0.12.3
### Preview features
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
### Bug fixes
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
### Documentation
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
## 0.12.4
### Preview features
- \[`flake8-type-checking`, `pyupgrade`, `ruff`\] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) ([#19100](https://github.com/astral-sh/ruff/pull/19100))
- \[`flake8-use-pathlib`\] Add autofix for `PTH109` ([#19245](https://github.com/astral-sh/ruff/pull/19245))
- \[`pylint`\] Detect indirect `pathlib.Path` usages for `unspecified-encoding` (`PLW1514`) ([#19304](https://github.com/astral-sh/ruff/pull/19304))
### Bug fixes
- \[`flake8-bugbear`\] Fix `B017` false negatives for keyword exception arguments ([#19217](https://github.com/astral-sh/ruff/pull/19217))
- \[`flake8-use-pathlib`\] Fix false negative on direct `Path()` instantiation (`PTH210`) ([#19388](https://github.com/astral-sh/ruff/pull/19388))
- \[`flake8-django`\] Fix `DJ008` false positive for abstract models with type-annotated `abstract` field ([#19221](https://github.com/astral-sh/ruff/pull/19221))
- \[`isort`\] Fix `I002` import insertion after docstring with multiple string statements ([#19222](https://github.com/astral-sh/ruff/pull/19222))
- \[`isort`\] Treat form feed as valid whitespace before a semicolon ([#19343](https://github.com/astral-sh/ruff/pull/19343))
- \[`pydoclint`\] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) ([#19246](https://github.com/astral-sh/ruff/pull/19246))
- \[`refurb`\] `FURB164` fix should validate arguments and should usually be marked unsafe ([#19136](https://github.com/astral-sh/ruff/pull/19136))
### Rule changes
- \[`flake8-use-pathlib`\] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 ([#19331](https://github.com/astral-sh/ruff/pull/19331))
- \[`pep8_naming`\] Avoid false positives on standard library functions with uppercase names (`N802`) ([#18907](https://github.com/astral-sh/ruff/pull/18907))
- \[`pycodestyle`\] Handle brace escapes for t-strings in logical lines ([#19358](https://github.com/astral-sh/ruff/pull/19358))
- \[`pylint`\] Extend invalid string character rules to include t-strings ([#19355](https://github.com/astral-sh/ruff/pull/19355))
- \[`ruff`\] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ ([#19333](https://github.com/astral-sh/ruff/pull/19333))
### Documentation
- \[`flake8-type-checking`\] Make `TC010` docs example more realistic ([#19356](https://github.com/astral-sh/ruff/pull/19356))
- Make more documentation examples error out-of-the-box ([#19288](https://github.com/astral-sh/ruff/pull/19288),[#19272](https://github.com/astral-sh/ruff/pull/19272),[#19291](https://github.com/astral-sh/ruff/pull/19291),[#19296](https://github.com/astral-sh/ruff/pull/19296),[#19292](https://github.com/astral-sh/ruff/pull/19292),[#19295](https://github.com/astral-sh/ruff/pull/19295),[#19297](https://github.com/astral-sh/ruff/pull/19297),[#19309](https://github.com/astral-sh/ruff/pull/19309))
## 0.12.5
### Preview features
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
### Bug fixes
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
### Rule changes
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.8
### Preview features
- \[`flake8-use-pathlib`\] Expand `PTH201` to check all `PurePath` subclasses ([#19440](https://github.com/astral-sh/ruff/pull/19440))
### Bug fixes
- \[`flake8-blind-except`\] Change `BLE001` to correctly parse exception tuples ([#19747](https://github.com/astral-sh/ruff/pull/19747))
- \[`flake8-errmsg`\] Exclude `typing.cast` from `EM101` ([#19656](https://github.com/astral-sh/ruff/pull/19656))
- \[`flake8-simplify`\] Fix raw string handling in `SIM905` for embedded quotes ([#19591](https://github.com/astral-sh/ruff/pull/19591))
- \[`flake8-import-conventions`\] Avoid false positives for NFKC-normalized `__debug__` import aliases in `ICN001` ([#19411](https://github.com/astral-sh/ruff/pull/19411))
- \[`isort`\] Fix syntax error after docstring ending with backslash (`I002`) ([#19505](https://github.com/astral-sh/ruff/pull/19505))
- \[`pylint`\] Mark `PLC0207` fixes as unsafe when `*args` unpacking is present ([#19679](https://github.com/astral-sh/ruff/pull/19679))
- \[`pyupgrade`\] Prevent infinite loop with `I002` (`UP010`, `UP035`) ([#19413](https://github.com/astral-sh/ruff/pull/19413))
- \[`ruff`\] Parenthesize generator expressions in f-strings (`RUF010`) ([#19434](https://github.com/astral-sh/ruff/pull/19434))
### Rule changes
- \[`eradicate`\] Don't flag `pyrefly` pragmas as unused code (`ERA001`) ([#19731](https://github.com/astral-sh/ruff/pull/19731))
### Documentation
- Replace "associative" with "commutative" in docs for `RUF036` ([#19706](https://github.com/astral-sh/ruff/pull/19706))
- Fix copy and line separator colors in dark mode ([#19630](https://github.com/astral-sh/ruff/pull/19630))
- Fix link to `typing` documentation ([#19648](https://github.com/astral-sh/ruff/pull/19648))
- \[`refurb`\] Make more examples error out-of-the-box ([#19695](https://github.com/astral-sh/ruff/pull/19695),[#19673](https://github.com/astral-sh/ruff/pull/19673),[#19672](https://github.com/astral-sh/ruff/pull/19672))
### Other changes
- Include column numbers in GitLab output format ([#19708](https://github.com/astral-sh/ruff/pull/19708))
- Always expand tabs to four spaces in diagnostics ([#19618](https://github.com/astral-sh/ruff/pull/19618))
- Update pre-commit's `ruff` id ([#19654](https://github.com/astral-sh/ruff/pull/19654))
## 0.12.9
### Preview features
- \[`airflow`\] Add check for `airflow.secrets.cache.SecretCache` (`AIR301`) ([#17707](https://github.com/astral-sh/ruff/pull/17707))
- \[`ruff`\] Offer a safe fix for multi-digit zeros (`RUF064`) ([#19847](https://github.com/astral-sh/ruff/pull/19847))
### Bug fixes
- \[`flake8-blind-except`\] Fix `BLE001` false-positive on `raise ... from None` ([#19755](https://github.com/astral-sh/ruff/pull/19755))
- \[`flake8-comprehensions`\] Fix false positive for `C420` with attribute, subscript, or slice assignment targets ([#19513](https://github.com/astral-sh/ruff/pull/19513))
- \[`flake8-simplify`\] Fix handling of U+001C..U+001F whitespace (`SIM905`) ([#19849](https://github.com/astral-sh/ruff/pull/19849))
### Rule changes
- \[`pylint`\] Use lowercase hex characters to match the formatter (`PLE2513`) ([#19808](https://github.com/astral-sh/ruff/pull/19808))
### Documentation
- Fix `lint.future-annotations` link ([#19876](https://github.com/astral-sh/ruff/pull/19876))
### Other changes
- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
```diff
-unused.py:8:19: F401 [*] `pathlib` imported but unused
+F401 [*] `pathlib` imported but unused
+ --> unused.py:8:19
|
7 | # Unused, _not_ marked as required (due to the alias).
8 | import pathlib as non_alias
- | ^^^^^^^^^ F401
+ | ^^^^^^^^^
9 |
10 | # Unused, marked as required.
|
- = help: Remove unused import: `pathlib`
+help: Remove unused import: `pathlib`
```
For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
## 0.12.10
### Preview features
- \[`flake8-simplify`\] Implement fix for `maxsplit` without separator (`SIM905`) ([#19851](https://github.com/astral-sh/ruff/pull/19851))
- \[`flake8-use-pathlib`\] Add fixes for `PTH102` and `PTH103` ([#19514](https://github.com/astral-sh/ruff/pull/19514))
### Bug fixes
- \[`isort`\] Handle multiple continuation lines after module docstring (`I002`) ([#19818](https://github.com/astral-sh/ruff/pull/19818))
- \[`pyupgrade`\] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) ([#19769](https://github.com/astral-sh/ruff/pull/19769))
- \[`pyupgrade`\] Handle nested `Optional`s (`UP045`) ([#19770](https://github.com/astral-sh/ruff/pull/19770))
### Rule changes
- \[`pycodestyle`\] Make `E731` fix unsafe instead of display-only for class assignments ([#19700](https://github.com/astral-sh/ruff/pull/19700))
- \[`pyflakes`\] Add secondary annotation showing previous definition (`F811`) ([#19900](https://github.com/astral-sh/ruff/pull/19900))
### Documentation
- Fix description of global config file discovery strategy ([#19188](https://github.com/astral-sh/ruff/pull/19188))
- Update outdated links to <https://typing.python.org/en/latest/source/stubs.html> ([#19992](https://github.com/astral-sh/ruff/pull/19992))
- \[`flake8-annotations`\] Remove unused import in example (`ANN401`) ([#20000](https://github.com/astral-sh/ruff/pull/20000))
## 0.12.11
### Preview features
- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
### Bug fixes
- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
### Rule changes
- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
### Documentation
- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
## 0.12.12
### Preview features
- Show fixes by default ([#19919](https://github.com/astral-sh/ruff/pull/19919))
- \[`airflow`\] Convert `DatasetOrTimeSchedule(datasets=...)` to `AssetOrTimeSchedule(assets=...)` (`AIR311`) ([#20202](https://github.com/astral-sh/ruff/pull/20202))
- \[`airflow`\] Improve the `AIR002` error message ([#20173](https://github.com/astral-sh/ruff/pull/20173))
- \[`airflow`\] Move `airflow.operators.postgres_operator.Mapping` from `AIR302` to `AIR301` ([#20172](https://github.com/astral-sh/ruff/pull/20172))
- \[`flake8-async`\] Implement `blocking-input` rule (`ASYNC250`) ([#20122](https://github.com/astral-sh/ruff/pull/20122))
- \[`flake8-use-pathlib`\] Make `PTH119` and `PTH120` fixes unsafe because they can change behavior ([#20118](https://github.com/astral-sh/ruff/pull/20118))
- \[`pylint`\] Add U+061C to `PLE2502` ([#20106](https://github.com/astral-sh/ruff/pull/20106))
- \[`ruff`\] Fix false negative for empty f-strings in `deque` calls (`RUF037`) ([#20109](https://github.com/astral-sh/ruff/pull/20109))
### Bug fixes
- Less confidently mark f-strings as empty when inferring truthiness ([#20152](https://github.com/astral-sh/ruff/pull/20152))
- \[`fastapi`\] Fix false positive for paths with spaces around parameters (`FAST003`) ([#20077](https://github.com/astral-sh/ruff/pull/20077))
- \[`flake8-comprehensions`\] Skip `C417` when lambda contains `yield`/`yield from` ([#20201](https://github.com/astral-sh/ruff/pull/20201))
- \[`perflint`\] Handle tuples in dictionary comprehensions (`PERF403`) ([#19934](https://github.com/astral-sh/ruff/pull/19934))
### Rule changes
- \[`pycodestyle`\] Preserve return type annotation for `ParamSpec` (`E731`) ([#20108](https://github.com/astral-sh/ruff/pull/20108))
### Documentation
- Add fix safety sections to docs ([#17490](https://github.com/astral-sh/ruff/pull/17490),[#17499](https://github.com/astral-sh/ruff/pull/17499))
[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument
[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation
[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp
[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional
[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union
[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for
[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true
[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.13.0"
version = "0.12.11"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1489,8 +1489,6 @@ fn deprecated_direct() {
#[test]
fn deprecated_multiple_direct() {
// Multiple deprecated rules selected by exact code should be included
// but a warning should be displayed
let mut cmd = RuffCheck::default()
.args(["--select", "RUF920", "--select", "RUF921"])
.build();
@@ -1518,10 +1516,16 @@ fn deprecated_indirect() {
// since it is not a "direct" selection
let mut cmd = RuffCheck::default().args(["--select", "RUF92"]).build();
assert_cmd_snapshot!(cmd, @r"
success: true
exit_code: 0
success: false
exit_code: 1
----- stdout -----
All checks passed!
RUF920 Hey this is a deprecated test rule.
--> -:1:1
RUF921 Hey this is another deprecated test rule.
--> -:1:1
Found 2 errors.
----- stderr -----
");
@@ -2151,10 +2155,16 @@ extend-safe-fixes = ["RUF9"]
RUF903 Hey this is a stable test rule with a display only fix.
--> -:1:1
RUF920 Hey this is a deprecated test rule.
--> -:1:1
RUF921 Hey this is another deprecated test rule.
--> -:1:1
RUF950 Hey this is a test rule that was redirected from another.
--> -:1:1
Found 5 errors.
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----

View File

@@ -5780,6 +5780,28 @@ match 42: # invalid-syntax
Ok(())
}
#[test]
fn future_annotations_preview_warning() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "lint.future-annotations = true"])
.args(["--select", "F"])
.arg("--no-preview")
.arg("-")
.pass_stdin("1"),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: The `lint.future-annotations` setting will have no effect because `preview` is disabled
",
);
}
#[test]
fn up045_nested_optional_flatten_all() {
let contents = "\

View File

@@ -55,10 +55,6 @@ either a redundant alias or, if already present in the file, an `__all__` entry.
to remove third-party and standard library imports -- the fix is unsafe because the module's
interface changes.
See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)
for more details on how Ruff
determines whether an import is first or third-party.
## Example
```python
@@ -87,6 +83,11 @@ else:
print("numpy is not installed")
```
## Preview
When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
the criterion for determining whether an import is first-party
is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
## Options
- `lint.ignore-init-module-imports`
- `lint.pyflakes.allowed-unused-imports`

View File

@@ -95,7 +95,7 @@ exit_code: 1
"rules": [
{
"fullDescription": {
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
},
"help": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"

View File

@@ -454,26 +454,24 @@ impl Diagnostic {
/// Computes the start source location for the message.
///
/// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
/// or if the span has no range.
pub fn ruff_start_location(&self) -> Option<LineColumn> {
Some(
self.ruff_source_file()?
.to_source_code()
.line_column(self.range()?.start()),
)
/// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the
/// span has no range.
pub fn expect_ruff_start_location(&self) -> LineColumn {
self.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.expect_range().start())
}
/// Computes the end source location for the message.
///
/// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
/// or if the span has no range.
pub fn ruff_end_location(&self) -> Option<LineColumn> {
Some(
self.ruff_source_file()?
.to_source_code()
.line_column(self.range()?.end()),
)
/// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the
/// span has no range.
pub fn expect_ruff_end_location(&self) -> LineColumn {
self.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.expect_range().end())
}
/// Returns the [`SourceFile`] which the message belongs to.
@@ -503,18 +501,13 @@ impl Diagnostic {
/// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
///
/// Panics if either diagnostic has no primary span, or if its file is not a `SourceFile`.
/// Panics if either diagnostic has no primary span, if the span has no range, or if its file is
/// not a `SourceFile`.
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
let a = (
self.expect_ruff_source_file(),
self.range().map(|r| r.start()),
);
let b = (
(self.expect_ruff_source_file(), self.expect_range().start()).cmp(&(
other.expect_ruff_source_file(),
other.range().map(|r| r.start()),
);
a.cmp(&b)
other.expect_range().start(),
))
}
}

View File

@@ -81,19 +81,14 @@ impl IndentStyle {
pub const fn is_space(&self) -> bool {
matches!(self, IndentStyle::Space)
}
/// Returns the string representation of the indent style.
pub const fn as_str(&self) -> &'static str {
match self {
IndentStyle::Tab => "tab",
IndentStyle::Space => "space",
}
}
}
impl std::fmt::Display for IndentStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
match self {
IndentStyle::Tab => std::write!(f, "tab"),
IndentStyle::Space => std::write!(f, "space"),
}
}
}

View File

@@ -139,16 +139,4 @@ impl LineEnding {
LineEnding::CarriageReturn => "\r",
}
}
/// Returns the string used to configure this line ending.
///
/// See [`LineEnding::as_str`] for the actual string representation of the line ending.
#[inline]
pub const fn as_setting_str(&self) -> &'static str {
match self {
LineEnding::LineFeed => "lf",
LineEnding::CarriageReturnLineFeed => "crlf",
LineEnding::CarriageReturn => "cr",
}
}
}

View File

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

View File

@@ -141,133 +141,3 @@ class ExampleWithKeywords:
def method3(self):
super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
# See: https://github.com/astral-sh/ruff/issues/19357
# Must be detected
class ParentD:
def f(self):
print("D")
class ChildD1(ParentD):
def f(self):
if False: __class__ # Python injects __class__ into scope
builtins.super(ChildD1, self).f()
class ChildD2(ParentD):
def f(self):
if False: super # Python injects __class__ into scope
builtins.super(ChildD2, self).f()
class ChildD3(ParentD):
def f(self):
builtins.super(ChildD3, self).f()
super # Python injects __class__ into scope
import builtins as builtins_alias
class ChildD4(ParentD):
def f(self):
builtins_alias.super(ChildD4, self).f()
super # Python injects __class__ into scope
class ChildD5(ParentD):
def f(self):
super = 1
super # Python injects __class__ into scope
builtins.super(ChildD5, self).f()
class ChildD6(ParentD):
def f(self):
super: "Any"
__class__ # Python injects __class__ into scope
builtins.super(ChildD6, self).f()
class ChildD7(ParentD):
def f(self):
def x():
__class__ # Python injects __class__ into scope
builtins.super(ChildD7, self).f()
class ChildD8(ParentD):
def f(self):
def x():
super = 1
super # Python injects __class__ into scope
builtins.super(ChildD8, self).f()
class ChildD9(ParentD):
def f(self):
def x():
__class__ = 1
__class__ # Python injects __class__ into scope
builtins.super(ChildD9, self).f()
class ChildD10(ParentD):
def f(self):
def x():
__class__ = 1
super # Python injects __class__ into scope
builtins.super(ChildD10, self).f()
# Must be ignored
class ParentI:
def f(self):
print("I")
class ChildI1(ParentI):
def f(self):
builtins.super(ChildI1, self).f() # no __class__ in the local scope
class ChildI2(ParentI):
def b(self):
x = __class__
if False: super
def f(self):
self.b()
builtins.super(ChildI2, self).f() # no __class__ in the local scope
class ChildI3(ParentI):
def f(self):
if False: super
def x(_):
builtins.super(ChildI3, self).f() # no __class__ in the local scope
x(None)
class ChildI4(ParentI):
def f(self):
super: "str"
builtins.super(ChildI4, self).f() # no __class__ in the local scope
class ChildI5(ParentI):
def f(self):
super = 1
__class__ = 3
builtins.super(ChildI5, self).f() # no __class__ in the local scope
class ChildI6(ParentI):
def f(self):
__class__ = None
__class__
builtins.super(ChildI6, self).f() # no __class__ in the local scope
class ChildI7(ParentI):
def f(self):
__class__ = None
super
builtins.super(ChildI7, self).f()
class ChildI8(ParentI):
def f(self):
__class__: "Any"
super
builtins.super(ChildI8, self).f()
class ChildI9(ParentI):
def f(self):
class A:
def foo(self):
if False: super
if False: __class__
builtins.super(ChildI9, self).f()

View File

@@ -1,59 +0,0 @@
from collections.abc import Generator, AsyncGenerator
def func() -> Generator[int, None, None]:
yield 42
def func() -> Generator[int, None]:
yield 42
def func() -> Generator[int]:
yield 42
def func() -> Generator[int, int, int]:
foo = yield 42
return foo
def func() -> Generator[int, int, None]:
_ = yield 42
return None
def func() -> Generator[int, None, int]:
yield 42
return 42
async def func() -> AsyncGenerator[int, None]:
yield 42
async def func() -> AsyncGenerator[int]:
yield 42
async def func() -> AsyncGenerator[int, int]:
foo = yield 42
return foo
from typing import Generator, AsyncGenerator
def func() -> Generator[str, None, None]:
yield "hello"
async def func() -> AsyncGenerator[str, None]:
yield "hello"
async def func() -> AsyncGenerator[ # type: ignore
str,
None
]:
yield "hello"

View File

@@ -42,7 +42,3 @@ b"a" in bytes("a", "utf-8")
1 in set(set([1]))
'' in {""}
frozenset() in {frozenset()}
# https://github.com/astral-sh/ruff/issues/20238
"b" in f"" "" # Error
"b" in f"" "x" # OK

View File

@@ -8,7 +8,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::{
is_optional_as_none_in_union_enabled, is_unnecessary_default_type_args_stubs_enabled,
is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled,
};
use crate::registry::Rule;
use crate::rules::{
@@ -142,10 +142,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}
if checker.is_rule_enabled(Rule::UnnecessaryDefaultTypeArgs) {
if checker.target_version() >= PythonVersion::PY313
|| is_unnecessary_default_type_args_stubs_enabled(checker.settings())
&& checker.semantic().in_stub_file()
{
if checker.target_version() >= PythonVersion::PY313 {
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
}
}
@@ -1295,7 +1292,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::NonOctalPermissions) {
ruff::rules::non_octal_permissions(checker, call);
}
if checker.is_rule_enabled(Rule::AssertRaisesException) {
if checker.is_rule_enabled(Rule::AssertRaisesException)
&& is_assert_raises_exception_call_enabled(checker.settings())
{
flake8_bugbear::rules::assert_raises_exception_call(checker, call);
}
}

View File

@@ -334,7 +334,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncFunctionWithTimeout),
(Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncBusyWait),
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
(Flake8Async, "116") => (RuleGroup::Stable, rules::flake8_async::rules::LongSleepNotForever),
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
(Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction),
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
@@ -563,7 +563,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "035") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedImport),
(Pyupgrade, "036") => (RuleGroup::Stable, rules::pyupgrade::rules::OutdatedVersionBlock),
(Pyupgrade, "037") => (RuleGroup::Stable, rules::pyupgrade::rules::QuotedAnnotation),
(Pyupgrade, "038") => (RuleGroup::Removed, rules::pyupgrade::rules::NonPEP604Isinstance),
(Pyupgrade, "038") => (RuleGroup::Deprecated, rules::pyupgrade::rules::NonPEP604Isinstance),
(Pyupgrade, "039") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryClassParentheses),
(Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias),
(Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias),
@@ -574,7 +574,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "046") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericClass),
(Pyupgrade, "047") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericFunction),
(Pyupgrade, "049") => (RuleGroup::Stable, rules::pyupgrade::rules::PrivateTypeParameter),
(Pyupgrade, "050") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessClassMetaclassType),
(Pyupgrade, "050") => (RuleGroup::Preview, rules::pyupgrade::rules::UselessClassMetaclassType),
// pydocstyle
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
@@ -773,7 +773,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(PandasVet, "013") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotStack),
(PandasVet, "015") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfPdMerge),
(PandasVet, "101") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck),
(PandasVet, "901") => (RuleGroup::Removed, rules::pandas_vet::rules::PandasDfVariableName),
(PandasVet, "901") => (RuleGroup::Deprecated, rules::pandas_vet::rules::PandasDfVariableName),
// flake8-errmsg
(Flake8ErrMsg, "101") => (RuleGroup::Stable, rules::flake8_errmsg::rules::RawStringInException),
@@ -830,8 +830,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "056") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
(Flake8Pyi, "058") => (RuleGroup::Stable, rules::flake8_pyi::rules::GeneratorReturnFromIterMethod),
(Flake8Pyi, "057") => (RuleGroup::Stable, rules::flake8_pyi::rules::ByteStringUsage),
(Flake8Pyi, "059") => (RuleGroup::Stable, rules::flake8_pyi::rules::GenericNotLastBaseClass),
(Flake8Pyi, "061") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantNoneLiteral),
(Flake8Pyi, "059") => (RuleGroup::Preview, rules::flake8_pyi::rules::GenericNotLastBaseClass),
(Flake8Pyi, "061") => (RuleGroup::Preview, rules::flake8_pyi::rules::RedundantNoneLiteral),
(Flake8Pyi, "062") => (RuleGroup::Stable, rules::flake8_pyi::rules::DuplicateLiteralMember),
(Flake8Pyi, "063") => (RuleGroup::Stable, rules::flake8_pyi::rules::Pep484StylePositionalOnlyParameter),
(Flake8Pyi, "064") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantFinalLiteral),
@@ -956,7 +956,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob),
(Flake8UsePathlib, "208") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsListdir),
(Flake8UsePathlib, "210") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::InvalidPathlibWithSuffix),
(Flake8UsePathlib, "211") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsSymlink),
(Flake8UsePathlib, "211") => (RuleGroup::Preview, rules::flake8_use_pathlib::rules::OsSymlink),
// flake8-logging-format
(Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat),
@@ -1032,7 +1032,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
(Ruff, "040") => (RuleGroup::Stable, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
(Ruff, "041") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryNestedLiteral),
(Ruff, "043") => (RuleGroup::Stable, rules::ruff::rules::PytestRaisesAmbiguousPattern),
(Ruff, "043") => (RuleGroup::Preview, rules::ruff::rules::PytestRaisesAmbiguousPattern),
(Ruff, "045") => (RuleGroup::Preview, rules::ruff::rules::ImplicitClassVarInDataclass),
(Ruff, "046") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryCastToInt),
(Ruff, "047") => (RuleGroup::Preview, rules::ruff::rules::NeedlessElse),
@@ -1046,7 +1046,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
(Ruff, "057") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryRound),
(Ruff, "058") => (RuleGroup::Stable, rules::ruff::rules::StarmapZip),
(Ruff, "059") => (RuleGroup::Stable, rules::ruff::rules::UnusedUnpackedVariable),
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection),
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
@@ -1106,11 +1106,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// airflow
(Airflow, "001") => (RuleGroup::Stable, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
(Airflow, "002") => (RuleGroup::Stable, rules::airflow::rules::AirflowDagNoScheduleArgument),
(Airflow, "301") => (RuleGroup::Stable, rules::airflow::rules::Airflow3Removal),
(Airflow, "302") => (RuleGroup::Stable, rules::airflow::rules::Airflow3MovedToProvider),
(Airflow, "311") => (RuleGroup::Stable, rules::airflow::rules::Airflow3SuggestedUpdate),
(Airflow, "312") => (RuleGroup::Stable, rules::airflow::rules::Airflow3SuggestedToMoveToProvider),
(Airflow, "002") => (RuleGroup::Preview, rules::airflow::rules::AirflowDagNoScheduleArgument),
(Airflow, "301") => (RuleGroup::Preview, rules::airflow::rules::Airflow3Removal),
(Airflow, "302") => (RuleGroup::Preview, rules::airflow::rules::Airflow3MovedToProvider),
(Airflow, "311") => (RuleGroup::Preview, rules::airflow::rules::Airflow3SuggestedUpdate),
(Airflow, "312") => (RuleGroup::Preview, rules::airflow::rules::Airflow3SuggestedToMoveToProvider),
// perflint
(Perflint, "101") => (RuleGroup::Stable, rules::perflint::rules::UnnecessaryListCast),
@@ -1137,7 +1137,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "105") => (RuleGroup::Stable, rules::refurb::rules::PrintEmptyString),
(Refurb, "110") => (RuleGroup::Preview, rules::refurb::rules::IfExpInsteadOfOrOperator),
(Refurb, "113") => (RuleGroup::Preview, rules::refurb::rules::RepeatedAppend),
(Refurb, "116") => (RuleGroup::Stable, rules::refurb::rules::FStringNumberFormat),
(Refurb, "116") => (RuleGroup::Preview, rules::refurb::rules::FStringNumberFormat),
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
(Refurb, "122") => (RuleGroup::Stable, rules::refurb::rules::ForLoopWrites),
(Refurb, "129") => (RuleGroup::Stable, rules::refurb::rules::ReadlinesInFor),

View File

@@ -19,7 +19,7 @@ impl Emitter for GithubEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for diagnostic in diagnostics {
let source_location = diagnostic.ruff_start_location().unwrap_or_default();
let source_location = diagnostic.expect_ruff_start_location();
let filename = diagnostic.expect_ruff_filename();
let location = if context.is_notebook(&filename) {
// We can't give a reasonable location for the structured formats,
@@ -29,7 +29,7 @@ impl Emitter for GithubEmitter {
source_location
};
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
let end_location = diagnostic.expect_ruff_end_location();
write!(
writer,

View File

@@ -105,7 +105,7 @@ fn group_diagnostics_by_filename(
.or_insert_with(Vec::new)
.push(MessageWithLocation {
message: diagnostic,
start_location: diagnostic.ruff_start_location().unwrap_or_default(),
start_location: diagnostic.expect_ruff_start_location(),
});
}
grouped_messages

View File

@@ -158,8 +158,8 @@ struct SarifResult<'a> {
impl<'a> SarifResult<'a> {
#[cfg(not(target_arch = "wasm32"))]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let start_location = message.expect_ruff_start_location();
let end_location = message.expect_ruff_end_location();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),
@@ -178,8 +178,8 @@ impl<'a> SarifResult<'a> {
#[cfg(target_arch = "wasm32")]
#[expect(clippy::unnecessary_wraps)]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let start_location = message.expect_ruff_start_location();
let end_location = message.expect_ruff_end_location();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),

View File

@@ -81,7 +81,7 @@ expression: value
"rules": [
{
"fullDescription": {
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
},
"help": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
@@ -119,7 +119,7 @@ expression: value
},
{
"fullDescription": {
"text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Fix safety\n\nThis rule's fix is marked as unsafe because removing an unused variable assignment may\ndelete comments that are attached to the assignment.\n\n## See also\n\nThis rule does not apply to bindings in unpacked assignments (e.g. `x, y = 1, 2`). See\n[`unused-unpacked-variable`][RUF059] for this case.\n\n## Options\n- `lint.dummy-variable-rgx`\n\n[RUF059]: https://docs.astral.sh/ruff/rules/unused-unpacked-variable/\n"
"text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Fix safety\n\nThis rule's fix is marked as unsafe because removing an unused variable assignment may\ndelete comments that are attached to the assignment.\n\n## Options\n- `lint.dummy-variable-rgx`\n"
},
"help": {
"text": "Local variable `{name}` is assigned to but never used"

View File

@@ -11,6 +11,11 @@ pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/16565
pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// Rule-specific behavior
// https://github.com/astral-sh/ruff/pull/15541
@@ -195,11 +200,35 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) ->
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18208
pub(crate) const fn is_multiple_with_statements_fix_safe_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18400
pub(crate) const fn is_ignore_init_files_in_useless_alias_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18572
pub(crate) const fn is_optional_as_none_in_union_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18547
pub(crate) const fn is_invalid_async_mock_access_check_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18867
pub(crate) const fn is_raise_exception_byte_string_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18683
pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
settings: &LinterSettings,
@@ -207,14 +236,27 @@ pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19063
pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19100
pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19390
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19851
pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20027
pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled(
settings: &LinterSettings,
) -> bool {
// https://github.com/astral-sh/ruff/pull/20106
pub(crate) const fn is_bidi_forbid_arabic_letter_mark_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -214,8 +214,10 @@ impl RuleSelector {
RuleGroup::Preview => {
preview_enabled && (self.is_exact() || !preview_require_explicit)
}
// Deprecated rules are excluded by default unless explicitly selected
RuleGroup::Deprecated => !preview_enabled && self.is_exact(),
// Deprecated rules are excluded in preview mode and with 'All' option unless explicitly selected
RuleGroup::Deprecated => {
(!preview_enabled || self.is_exact()) && !matches!(self, RuleSelector::All)
}
// Removed rules are included if explicitly selected but will error downstream
RuleGroup::Removed => self.is_exact(),
}

View File

@@ -13,13 +13,13 @@ use ruff_text_size::TextRange;
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of Airflow functions and values that have been moved to its providers
/// (e.g., `apache-airflow-providers-fab`).
/// Checks for uses of Airflow functions and values that have been moved to it providers.
/// (e.g., apache-airflow-providers-fab)
///
/// ## Why is this bad?
/// Airflow 3.0 moved various deprecated functions, members, and other
/// values to its providers. The user needs to install the corresponding provider and replace
/// the original usage with the one in the provider.
/// the original usage with the one in the provider
///
/// ## Example
/// ```python

View File

@@ -23,7 +23,7 @@ use ruff_text_size::TextRange;
/// ## Why is this bad?
/// Airflow 3.0 removed various deprecated functions, members, and other
/// values. Some have more modern replacements. Others are considered too niche
/// and not worth continued maintenance in Airflow.
/// and not worth to be maintained in Airflow.
///
/// ## Example
/// ```python

View File

@@ -17,9 +17,9 @@ use ruff_text_size::TextRange;
/// ## Why is this bad?
/// Airflow 3.0 removed various deprecated functions, members, and other
/// values. Some have more modern replacements. Others are considered too niche
/// and not worth continued maintenance in Airflow.
/// and not worth to be maintained in Airflow.
/// Even though these symbols still work fine on Airflow 3.0, they are expected to be removed in a future version.
/// Where available, users should replace the removed functionality with the new alternatives.
/// The user is suggested to replace the original usage with the new ones.
///
/// ## Example
/// ```python

View File

@@ -79,6 +79,7 @@ impl Violation for FastApiUnusedPathParameter {
function_name,
is_positional,
} = self;
#[expect(clippy::if_not_else)]
if !is_positional {
format!(
"Parameter `{arg_name}` appears in route path, but not in `{function_name}` signature"

View File

@@ -16,6 +16,8 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::settings::types::PreviewMode;
use ruff_python_ast::PythonVersion;
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))]
@@ -175,4 +177,23 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,41 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017 Do not assert blind exception: `Exception`
--> B017_1.py:20:9
|
18 | class Foobar(unittest.TestCase):
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | self.assertRaises(BaseException, something_else)
|
B017 Do not assert blind exception: `BaseException`
--> B017_1.py:21:9
|
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
21 | self.assertRaises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
B017 Do not assert blind exception: `Exception`
--> B017_1.py:25:5
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | pytest.raises(BaseException, something_else)
|
B017 Do not assert blind exception: `BaseException`
--> B017_1.py:26:5
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
26 | pytest.raises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 |
28 | pytest.raises(Exception, something_else, match="hello")
|

View File

@@ -0,0 +1,79 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017 Do not assert blind exception: `Exception`
--> B017_0.py:23:14
|
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
23 | with self.assertRaises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
24 | raise Exception("Evil I say!")
|
B017 Do not assert blind exception: `BaseException`
--> B017_0.py:27:14
|
26 | def also_evil_raises(self) -> None:
27 | with self.assertRaises(BaseException):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | raise Exception("Evil I say!")
|
B017 Do not assert blind exception: `Exception`
--> B017_0.py:45:10
|
44 | def test_pytest_raises():
45 | with pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^
46 | raise ValueError("Hello")
|
B017 Do not assert blind exception: `Exception`
--> B017_0.py:48:10
|
46 | raise ValueError("Hello")
47 |
48 | with pytest.raises(Exception), pytest.raises(ValueError):
| ^^^^^^^^^^^^^^^^^^^^^^^^
49 | raise ValueError("Hello")
|
B017 Do not assert blind exception: `Exception`
--> B017_0.py:57:36
|
55 | raise ValueError("This is also fine")
56 |
57 | with contextlib.nullcontext(), pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^
58 | raise ValueError("Multiple context managers")
|
B017 Do not assert blind exception: `Exception`
--> B017_0.py:62:10
|
61 | def test_pytest_raises_keyword():
62 | with pytest.raises(expected_exception=Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 | raise ValueError("Should be flagged")
|
B017 Do not assert blind exception: `Exception`
--> B017_0.py:68:18
|
66 | class TestKwargs(unittest.TestCase):
67 | def test_method(self):
68 | with self.assertRaises(exception=Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
69 | raise ValueError("Should be flagged")
|
B017 Do not assert blind exception: `BaseException`
--> B017_0.py:71:18
|
69 | raise ValueError("Should be flagged")
70 |
71 | with self.assertRaises(exception=BaseException):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72 | raise ValueError("Should be flagged")
|

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B017 Do not assert blind exception: `Exception`
--> B017_1.py:20:9
|
18 | class Foobar(unittest.TestCase):
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | self.assertRaises(BaseException, something_else)
|
B017 Do not assert blind exception: `BaseException`
--> B017_1.py:21:9
|
19 | def call_form_raises(self) -> None:
20 | self.assertRaises(Exception, something_else)
21 | self.assertRaises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
B017 Do not assert blind exception: `Exception`
--> B017_1.py:25:5
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | pytest.raises(BaseException, something_else)
|
B017 Do not assert blind exception: `BaseException`
--> B017_1.py:26:5
|
24 | def test_pytest_call_form() -> None:
25 | pytest.raises(Exception, something_else)
26 | pytest.raises(BaseException, something_else)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 |
28 | pytest.raises(Exception, something_else, match="hello")
|

View File

@@ -10,7 +10,7 @@ mod tests {
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
use crate::{assert_diagnostics, assert_diagnostics_diff, settings};
#[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
@@ -27,4 +27,28 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
fn preview_rules(path: &Path) -> Result<()> {
let snapshot = format!("preview_diff__{}", path.to_string_lossy());
let rules = vec![
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
];
let settings_before = settings::LinterSettings::for_rules(rules.clone());
let settings_after = settings::LinterSettings {
preview: crate::settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rules(rules)
};
assert_diagnostics_diff!(
snapshot,
Path::new("flake8_commas").join(path).as_path(),
&settings_before,
&settings_after
);
Ok(())
}
}

View File

@@ -5,6 +5,8 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::preview::is_trailing_comma_type_params_enabled;
use crate::settings::LinterSettings;
use crate::{AlwaysFixableViolation, Violation};
use crate::{Edit, Fix};
@@ -296,7 +298,7 @@ pub(crate) fn trailing_commas(
}
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack);
let context = update_context(token, prev, prev_prev, &mut stack, lint_context.settings());
check_token(token, prev, prev_prev, context, locator, lint_context);
@@ -415,6 +417,7 @@ fn update_context(
prev: SimpleToken,
prev_prev: SimpleToken,
stack: &mut Vec<Context>,
settings: &LinterSettings,
) -> Context {
let new_context = match token.ty {
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
@@ -424,11 +427,19 @@ fn update_context(
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket => match (prev.ty, prev_prev.ty) {
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
Context::new(ContextType::TypeParameters)
TokenType::OpeningSquareBracket if is_trailing_comma_type_params_enabled(settings) => {
match (prev.ty, prev_prev.ty) {
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
Context::new(ContextType::TypeParameters)
}
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
Context::new(ContextType::Subscript)
}
_ => Context::new(ContextType::List),
}
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
}
TokenType::OpeningSquareBracket => match prev.ty {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)
}
_ => Context::new(ContextType::List),

View File

@@ -939,111 +939,3 @@ help: Add trailing comma
644 | )
645 |
646 | assert False, f"<- This is not a trailing comma"
COM812 [*] Trailing comma missing
--> COM81.py:655:6
|
654 | type X[
655 | T
| ^
656 | ] = T
657 | def f[
|
help: Add trailing comma
652 | }"""
653 |
654 | type X[
- T
655 + T,
656 | ] = T
657 | def f[
658 | T
COM812 [*] Trailing comma missing
--> COM81.py:658:6
|
656 | ] = T
657 | def f[
658 | T
| ^
659 | ](): pass
660 | class C[
|
help: Add trailing comma
655 | T
656 | ] = T
657 | def f[
- T
658 + T,
659 | ](): pass
660 | class C[
661 | T
COM812 [*] Trailing comma missing
--> COM81.py:661:6
|
659 | ](): pass
660 | class C[
661 | T
| ^
662 | ]: pass
|
help: Add trailing comma
658 | T
659 | ](): pass
660 | class C[
- T
661 + T,
662 | ]: pass
663 |
664 | type X[T,] = T
COM819 [*] Trailing comma prohibited
--> COM81.py:664:9
|
662 | ]: pass
663 |
664 | type X[T,] = T
| ^
665 | def f[T,](): pass
666 | class C[T,]: pass
|
help: Remove trailing comma
661 | T
662 | ]: pass
663 |
- type X[T,] = T
664 + type X[T] = T
665 | def f[T,](): pass
666 | class C[T,]: pass
COM819 [*] Trailing comma prohibited
--> COM81.py:665:8
|
664 | type X[T,] = T
665 | def f[T,](): pass
| ^
666 | class C[T,]: pass
|
help: Remove trailing comma
662 | ]: pass
663 |
664 | type X[T,] = T
- def f[T,](): pass
665 + def f[T](): pass
666 | class C[T,]: pass
COM819 [*] Trailing comma prohibited
--> COM81.py:666:10
|
664 | type X[T,] = T
665 | def f[T,](): pass
666 | class C[T,]: pass
| ^
|
help: Remove trailing comma
663 |
664 | type X[T,] = T
665 | def f[T,](): pass
- class C[T,]: pass
666 + class C[T]: pass

View File

@@ -0,0 +1,124 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 6
--- Added ---
COM812 [*] Trailing comma missing
--> COM81.py:655:6
|
654 | type X[
655 | T
| ^
656 | ] = T
657 | def f[
|
help: Add trailing comma
652 | }"""
653 |
654 | type X[
- T
655 + T,
656 | ] = T
657 | def f[
658 | T
COM812 [*] Trailing comma missing
--> COM81.py:658:6
|
656 | ] = T
657 | def f[
658 | T
| ^
659 | ](): pass
660 | class C[
|
help: Add trailing comma
655 | T
656 | ] = T
657 | def f[
- T
658 + T,
659 | ](): pass
660 | class C[
661 | T
COM812 [*] Trailing comma missing
--> COM81.py:661:6
|
659 | ](): pass
660 | class C[
661 | T
| ^
662 | ]: pass
|
help: Add trailing comma
658 | T
659 | ](): pass
660 | class C[
- T
661 + T,
662 | ]: pass
663 |
664 | type X[T,] = T
COM819 [*] Trailing comma prohibited
--> COM81.py:664:9
|
662 | ]: pass
663 |
664 | type X[T,] = T
| ^
665 | def f[T,](): pass
666 | class C[T,]: pass
|
help: Remove trailing comma
661 | T
662 | ]: pass
663 |
- type X[T,] = T
664 + type X[T] = T
665 | def f[T,](): pass
666 | class C[T,]: pass
COM819 [*] Trailing comma prohibited
--> COM81.py:665:8
|
664 | type X[T,] = T
665 | def f[T,](): pass
| ^
666 | class C[T,]: pass
|
help: Remove trailing comma
662 | ]: pass
663 |
664 | type X[T,] = T
- def f[T,](): pass
665 + def f[T](): pass
666 | class C[T,]: pass
COM819 [*] Trailing comma prohibited
--> COM81.py:666:10
|
664 | type X[T,] = T
665 | def f[T,](): pass
666 | class C[T,]: pass
| ^
|
help: Remove trailing comma
663 |
664 | type X[T,] = T
665 | def f[T,](): pass
- class C[T,]: pass
666 + class C[T]: pass

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 0

View File

@@ -9,6 +9,7 @@ mod tests {
use anyhow::Result;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
@@ -46,14 +47,15 @@ mod tests {
}
#[test]
fn string_exception() -> Result<()> {
fn preview_string_exception() -> Result<()> {
let diagnostics = test_path(
Path::new("flake8_errmsg/EM101_byte_string.py"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(Rule::RawStringInException)
},
)?;
assert_diagnostics!(diagnostics);
assert_diagnostics!("preview", diagnostics);
Ok(())
}
}

View File

@@ -7,12 +7,16 @@ use ruff_text_size::Ranged;
use crate::Locator;
use crate::checkers::ast::Checker;
use crate::preview::is_raise_exception_byte_string_enabled;
use crate::registry::Rule;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for the use of string literals in exception constructors.
///
/// In [preview], this rule checks for byte string literals in
/// exception constructors.
///
/// ## Why is this bad?
/// Python includes the `raise` in the default traceback (and formatters
/// like Rich and IPython do too).
@@ -47,6 +51,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// raise RuntimeError(msg)
/// RuntimeError: 'Some value' is incorrect
/// ```
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct RawStringInException;
@@ -212,7 +218,9 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
// Check for byte string literals.
Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => {
if checker.settings().rules.enabled(Rule::RawStringInException) {
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length {
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length
&& is_raise_exception_byte_string_enabled(checker.settings())
{
let mut diagnostic =
checker.report_diagnostic(RawStringInException, first.range());
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::{self as ast, ParameterWithDefault};
use ruff_python_semantic::analyze::function_type;
use crate::Violation;
@@ -85,9 +85,16 @@ pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast
function_type::FunctionType::Method | function_type::FunctionType::ClassMethod
));
if let Some(param) = function_def.parameters.args.get(skip) {
if param.uses_pep_484_positional_only_convention() {
checker.report_diagnostic(Pep484StylePositionalOnlyParameter, param.identifier());
if let Some(arg) = function_def.parameters.args.get(skip) {
if is_old_style_positional_only(arg) {
checker.report_diagnostic(Pep484StylePositionalOnlyParameter, arg.identifier());
}
}
}
/// Returns `true` if the [`ParameterWithDefault`] is an old-style positional-only parameter (i.e.,
/// its name starts with `__` and does not end with `__`).
fn is_old_style_positional_only(param: &ParameterWithDefault) -> bool {
let arg_name = param.name();
arg_name.starts_with("__") && !arg_name.ends_with("__")
}

View File

@@ -59,6 +59,7 @@ mod tests {
Ok(())
}
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -10,6 +10,7 @@ use super::fix_with;
use crate::Fix;
use crate::checkers::ast::Checker;
use crate::fix::edits::fits;
use crate::preview::is_multiple_with_statements_fix_safe_enabled;
use crate::{FixAvailability, Violation};
/// ## What it does
@@ -44,8 +45,15 @@ use crate::{FixAvailability, Violation};
/// pass
/// ```
///
/// ## Fix safety
///
/// This fix is marked as always unsafe unless [preview] mode is enabled, in which case it is always
/// marked as safe. Note that the fix is unavailable if it would remove comments (in either case).
///
/// ## References
/// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct MultipleWithStatements;
@@ -187,7 +195,11 @@ pub(crate) fn multiple_with_statements(
checker.settings().tab_size,
)
}) {
Ok(Some(Fix::safe_edit(edit)))
if is_multiple_with_statements_fix_safe_enabled(checker.settings()) {
Ok(Some(Fix::safe_edit(edit)))
} else {
Ok(Some(Fix::unsafe_edit(edit)))
}
} else {
Ok(None)
}

View File

@@ -20,6 +20,7 @@ help: Combine `with` statements
4 |
5 | # SIM117
6 | with A():
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:7:1
@@ -45,6 +46,7 @@ help: Combine `with` statements
10 |
11 | # SIM117
12 | with A() as a:
note: This is an unsafe fix and may change runtime behavior
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:13:1
@@ -82,6 +84,7 @@ help: Combine `with` statements
22 |
23 | # OK
24 | with A() as a:
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:47:1
@@ -104,6 +107,7 @@ help: Combine `with` statements
49 |
50 | while True:
51 | # SIM117
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:53:5
@@ -140,6 +144,7 @@ help: Combine `with` statements
64 | "this for some reason")
65 |
66 | # SIM117
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:68:1
@@ -166,6 +171,7 @@ help: Combine `with` statements
73 |
74 | # SIM117
75 | with A() as a:
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:76:1
@@ -197,6 +203,7 @@ help: Combine `with` statements
81 |
82 | # SIM117
83 | with (
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:84:1
@@ -230,6 +237,7 @@ help: Combine `with` statements
90 |
91 | # SIM117 (auto-fixable)
92 | with A("01ß9💣28901ß9💣28901ß9💣289") as a:
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:95:1
@@ -252,6 +260,7 @@ help: Combine `with` statements
97 |
98 | # SIM117 (not auto-fixable too long)
99 | with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
note: This is an unsafe fix and may change runtime behavior
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:100:1
@@ -310,6 +319,7 @@ help: Combine `with` statements
136 |
137 | # Allow cascading for some statements.
138 | import anyio
note: This is an unsafe fix and may change runtime behavior
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:163:1
@@ -329,3 +339,4 @@ help: Combine `with` statements
- pass
163 + async with asyncio.timeout(1), A(), B():
164 + pass
note: This is an unsafe fix and may change runtime behavior

View File

@@ -0,0 +1,331 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:2:1
|
1 | # SIM117
2 | / with A() as a:
3 | | with B() as b:
| |__________________^
4 | print("hello")
|
help: Combine `with` statements
1 | # SIM117
- with A() as a:
- with B() as b:
- print("hello")
2 + with A() as a, B() as b:
3 + print("hello")
4 |
5 | # SIM117
6 | with A():
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:7:1
|
6 | # SIM117
7 | / with A():
8 | | with B():
| |_____________^
9 | with C():
10 | print("hello")
|
help: Combine `with` statements
4 | print("hello")
5 |
6 | # SIM117
- with A():
- with B():
- with C():
- print("hello")
7 + with A(), B():
8 + with C():
9 + print("hello")
10 |
11 | # SIM117
12 | with A() as a:
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:13:1
|
12 | # SIM117
13 | / with A() as a:
14 | | # Unfixable due to placement of this comment.
15 | | with B() as b:
| |__________________^
16 | print("hello")
|
help: Combine `with` statements
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:19:1
|
18 | # SIM117
19 | / with A() as a:
20 | | with B() as b:
| |__________________^
21 | # Fixable due to placement of this comment.
22 | print("hello")
|
help: Combine `with` statements
16 | print("hello")
17 |
18 | # SIM117
- with A() as a:
- with B() as b:
- # Fixable due to placement of this comment.
- print("hello")
19 + with A() as a, B() as b:
20 + # Fixable due to placement of this comment.
21 + print("hello")
22 |
23 | # OK
24 | with A() as a:
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:47:1
|
46 | # SIM117
47 | / async with A() as a:
48 | | async with B() as b:
| |________________________^
49 | print("hello")
|
help: Combine `with` statements
44 | print("hello")
45 |
46 | # SIM117
- async with A() as a:
- async with B() as b:
- print("hello")
47 + async with A() as a, B() as b:
48 + print("hello")
49 |
50 | while True:
51 | # SIM117
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:53:5
|
51 | while True:
52 | # SIM117
53 | / with A() as a:
54 | | with B() as b:
| |______________________^
55 | """this
56 | is valid"""
|
help: Combine `with` statements
50 |
51 | while True:
52 | # SIM117
- with A() as a:
- with B() as b:
- """this
53 + with A() as a, B() as b:
54 + """this
55 | is valid"""
56 |
- """the indentation on
57 + """the indentation on
58 | this line is significant"""
59 |
- "this is" \
60 + "this is" \
61 | "allowed too"
62 |
- ("so is"
63 + ("so is"
64 | "this for some reason")
65 |
66 | # SIM117
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:68:1
|
67 | # SIM117
68 | / with (
69 | | A() as a,
70 | | B() as b,
71 | | ):
72 | | with C() as c:
| |__________________^
73 | print("hello")
|
help: Combine `with` statements
67 | # SIM117
68 | with (
69 | A() as a,
- B() as b,
70 + B() as b,C() as c
71 | ):
- with C() as c:
- print("hello")
72 + print("hello")
73 |
74 | # SIM117
75 | with A() as a:
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:76:1
|
75 | # SIM117
76 | / with A() as a:
77 | | with (
78 | | B() as b,
79 | | C() as c,
80 | | ):
| |______^
81 | print("hello")
|
help: Combine `with` statements
73 | print("hello")
74 |
75 | # SIM117
- with A() as a:
- with (
- B() as b,
- C() as c,
- ):
- print("hello")
76 + with (
77 + A() as a, B() as b,
78 + C() as c,
79 + ):
80 + print("hello")
81 |
82 | # SIM117
83 | with (
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:84:1
|
83 | # SIM117
84 | / with (
85 | | A() as a,
86 | | B() as b,
87 | | ):
88 | | with (
89 | | C() as c,
90 | | D() as d,
91 | | ):
| |______^
92 | print("hello")
|
help: Combine `with` statements
83 | # SIM117
84 | with (
85 | A() as a,
- B() as b,
86 + B() as b,C() as c,
87 + D() as d,
88 | ):
- with (
- C() as c,
- D() as d,
- ):
- print("hello")
89 + print("hello")
90 |
91 | # SIM117 (auto-fixable)
92 | with A("01ß9💣28901ß9💣28901ß9💣289") as a:
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:95:1
|
94 | # SIM117 (auto-fixable)
95 | / with A("01ß9💣28901ß9💣28901ß9💣289") as a:
96 | | with B("01ß9💣28901ß9💣28901ß9💣289") as b:
| |__________________________________________________^
97 | print("hello")
|
help: Combine `with` statements
92 | print("hello")
93 |
94 | # SIM117 (auto-fixable)
- with A("01ß9💣28901ß9💣28901ß9💣289") as a:
- with B("01ß9💣28901ß9💣28901ß9💣289") as b:
- print("hello")
95 + with A("01ß9💣28901ß9💣28901ß9💣289") as a, B("01ß9💣28901ß9💣28901ß9💣289") as b:
96 + print("hello")
97 |
98 | # SIM117 (not auto-fixable too long)
99 | with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:100:1
|
99 | # SIM117 (not auto-fixable too long)
100 | / with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
101 | | with B("01ß9💣28901ß9💣28901ß9💣289") as b:
| |__________________________________________________^
102 | print("hello")
|
help: Combine `with` statements
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:106:5
|
104 | # From issue #3025.
105 | async def main():
106 | / async with A() as a: # SIM117.
107 | | async with B() as b:
| |____________________________^
108 | print("async-inside!")
|
help: Combine `with` statements
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:126:1
|
125 | # SIM117
126 | / with A() as a:
127 | | with B() as b:
| |__________________^
128 | type ListOrSet[T] = list[T] | set[T]
|
help: Combine `with` statements
123 | f(b2, c2, d2)
124 |
125 | # SIM117
- with A() as a:
- with B() as b:
- type ListOrSet[T] = list[T] | set[T]
126 + with A() as a, B() as b:
127 + type ListOrSet[T] = list[T] | set[T]
128 |
- class ClassA[T: str]:
- def method1(self) -> T:
- ...
129 + class ClassA[T: str]:
130 + def method1(self) -> T:
131 + ...
132 |
- f" something { my_dict["key"] } something else "
133 + f" something { my_dict["key"] } something else "
134 |
- f"foo {f"bar {x}"} baz"
135 + f"foo {f"bar {x}"} baz"
136 |
137 | # Allow cascading for some statements.
138 | import anyio
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
--> SIM117.py:163:1
|
162 | # Do not suppress combination, if a context manager is already combined with another.
163 | / async with asyncio.timeout(1), A():
164 | | async with B():
| |___________________^
165 | pass
|
help: Combine `with` statements
160 | pass
161 |
162 | # Do not suppress combination, if a context manager is already combined with another.
- async with asyncio.timeout(1), A():
- async with B():
- pass
163 + async with asyncio.timeout(1), A(), B():
164 + pass

View File

@@ -66,7 +66,7 @@ impl TypingReference {
}
// prefer `from __future__ import annotations` to quoting
if settings.future_annotations
if settings.future_annotations()
&& !reference.in_typing_only_annotation()
&& reference.in_runtime_evaluated_annotation()
{

View File

@@ -14,6 +14,7 @@ mod tests {
use test_case::test_case;
use crate::registry::{Linter, Rule};
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_snippet};
use crate::{assert_diagnostics, settings};
@@ -85,6 +86,7 @@ mod tests {
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
future_annotations: true,
preview: PreviewMode::Enabled,
// also enable quoting annotations to check the interaction. the future import
// should take precedence.
flake8_type_checking: super::settings::Settings {

View File

@@ -11,10 +11,12 @@ use crate::checkers::ast::{Checker, DiagnosticGuard};
use crate::codes::Rule;
use crate::fix;
use crate::importer::ImportedMembers;
use crate::preview::is_full_path_match_source_strategy_enabled;
use crate::rules::flake8_type_checking::helpers::{
TypingReference, filter_contained, quote_annotation,
};
use crate::rules::flake8_type_checking::imports::ImportBinding;
use crate::rules::isort::categorize::MatchSourceStrategy;
use crate::rules::isort::{ImportSection, ImportType, categorize};
use crate::{Fix, FixAvailability, Violation};
@@ -38,13 +40,6 @@ use crate::{Fix, FixAvailability, Violation};
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
/// as such.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be
/// moved into an `if TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if
/// both settings are enabled.
///
///
/// ## Example
/// ```python
/// from __future__ import annotations
@@ -70,6 +65,18 @@ use crate::{Fix, FixAvailability, Violation};
/// return len(sized)
/// ```
///
///
/// ## Preview
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
/// the criterion for determining whether an import is first-party
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-third-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be moved into an `if
/// TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
/// enabled.
///
/// ## Options
/// - `lint.flake8-type-checking.quote-annotations`
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
@@ -121,12 +128,6 @@ impl Violation for TypingOnlyFirstPartyImport {
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
/// as such.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be
/// moved into an `if TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if
/// both settings are enabled.
///
/// ## Example
/// ```python
/// from __future__ import annotations
@@ -152,6 +153,17 @@ impl Violation for TypingOnlyFirstPartyImport {
/// return len(df)
/// ```
///
/// ## Preview
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
/// the criterion for determining whether an import is first-party
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-first-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be moved into an `if
/// TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
/// enabled.
///
/// ## Options
/// - `lint.flake8-type-checking.quote-annotations`
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
@@ -203,12 +215,6 @@ impl Violation for TypingOnlyThirdPartyImport {
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
/// as such.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be
/// moved into an `if TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if
/// both settings are enabled.
///
/// ## Example
/// ```python
/// from __future__ import annotations
@@ -234,6 +240,15 @@ impl Violation for TypingOnlyThirdPartyImport {
/// return str(path)
/// ```
///
/// ## Preview
///
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, if
/// [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would enable an import to be moved into an `if
/// TYPE_CHECKING:` block. This takes precedence over the
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
/// enabled.
///
/// ## Options
/// - `lint.flake8-type-checking.quote-annotations`
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
@@ -282,7 +297,7 @@ pub(crate) fn typing_only_runtime_import(
// If we can't add a `__future__` import and in un-strict mode, don't flag typing-only
// imports that are implicitly loaded by way of a valid runtime import.
if !checker.settings().future_annotations
if !checker.settings().future_annotations()
&& !checker.settings().flake8_type_checking.strict
&& runtime_imports
.iter()
@@ -332,6 +347,13 @@ pub(crate) fn typing_only_runtime_import(
let source_name = import.source_name().join(".");
// Categorize the import, using coarse-grained categorization.
let match_source_strategy =
if is_full_path_match_source_strategy_enabled(checker.settings()) {
MatchSourceStrategy::FullPath
} else {
MatchSourceStrategy::Root
};
let import_type = match categorize(
&source_name,
qualified_name.is_unresolved_import(),
@@ -343,6 +365,7 @@ pub(crate) fn typing_only_runtime_import(
checker.settings().isort.no_sections,
&checker.settings().isort.section_order,
&checker.settings().isort.default_section,
match_source_strategy,
) {
ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => {
ImportType::FirstParty

View File

@@ -129,6 +129,7 @@ mod tests {
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
#[test_case(Rule::OsSymlink, Path::new("PTH211.py"))]
fn preview_flake8_use_pathlib(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -0,0 +1,152 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:5:1
|
5 | os.symlink("usr/bin/python", "tmp/python")
| ^^^^^^^^^^
6 | os.symlink(b"usr/bin/python", b"tmp/python")
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
help: Replace with `Path(...).symlink_to(...)`
2 | from pathlib import Path
3 |
4 |
- os.symlink("usr/bin/python", "tmp/python")
5 + Path("tmp/python").symlink_to("usr/bin/python")
6 | os.symlink(b"usr/bin/python", b"tmp/python")
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 |
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:6:1
|
5 | os.symlink("usr/bin/python", "tmp/python")
6 | os.symlink(b"usr/bin/python", b"tmp/python")
| ^^^^^^^^^^
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
help: Replace with `Path(...).symlink_to(...)`
3 |
4 |
5 | os.symlink("usr/bin/python", "tmp/python")
- os.symlink(b"usr/bin/python", b"tmp/python")
6 + Path(b"tmp/python").symlink_to(b"usr/bin/python")
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 |
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:9:1
|
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 |
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
| ^^^^^^^^^^
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
help: Replace with `Path(...).symlink_to(...)`
6 | os.symlink(b"usr/bin/python", b"tmp/python")
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 |
- os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
9 + Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True)
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
12 |
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:10:1
|
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
| ^^^^^^^^^^
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
help: Replace with `Path(...).symlink_to(...)`
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
8 |
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
- os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
10 + Path(b"tmp/python").symlink_to(b"usr/bin/python", target_is_directory=True)
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
12 |
13 | fd = os.open(".", os.O_RDONLY)
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:17:1
|
15 | os.close(fd)
16 |
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
| ^^^^^^^^^^
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
help: Replace with `Path(...).symlink_to(...)`
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:18:1
|
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
| ^^^^^^^^^^
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
help: Replace with `Path(...).symlink_to(...)`
15 | os.close(fd)
16 |
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
- os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
18 + Path("tmp/python").symlink_to("usr/bin/python")
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
21 |
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:20:1
|
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
| ^^^^^^^^^^
21 |
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
help: Replace with `Path(...).symlink_to(...)`
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
19 |
- os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
20 + Path("tmp/python").symlink_to("usr/bin/python")
21 |
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:22:1
|
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
21 |
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
| ^^^^^^^^^^
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
help: Replace with `Path(...).symlink_to(...)`
19 |
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
21 |
- os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
22 + Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True)
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
--> PTH211.py:23:1
|
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
| ^^^^^^^^^^
|
help: Replace with `Path(...).symlink_to(...)`

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::fmt;
use std::fs;
use std::iter;
use std::path::{Path, PathBuf};
@@ -100,6 +101,7 @@ pub(crate) fn categorize<'a>(
no_sections: bool,
section_order: &'a [ImportSection],
default_section: &'a ImportSection,
match_source_strategy: MatchSourceStrategy,
) -> &'a ImportSection {
let module_base = module_name.split('.').next().unwrap();
let (mut import_type, mut reason) = {
@@ -127,7 +129,7 @@ pub(crate) fn categorize<'a>(
&ImportSection::Known(ImportType::FirstParty),
Reason::SamePackage,
)
} else if let Some(src) = match_sources(src, module_name) {
} else if let Some(src) = match_sources(src, module_name, match_source_strategy) {
(
&ImportSection::Known(ImportType::FirstParty),
Reason::SourceMatch(src),
@@ -159,29 +161,61 @@ fn same_package(package: Option<PackageRoot<'_>>, module_base: &str) -> bool {
/// Returns the source path with respect to which the module `name`
/// should be considered first party, or `None` if no path is found.
///
/// The [`MatchSourceStrategy`] is the criterion used to decide whether
/// the module path matches a given source directory.
///
/// # Examples
///
/// - The module named `foo` will match `[SRC]` if `[SRC]/foo` is a directory
/// - The module named `foo` will match `[SRC]` if `[SRC]/foo` is a directory,
/// no matter the strategy.
///
/// - The module named `foo.baz` will match `[SRC]` only if `[SRC]/foo/baz`
/// is a directory, or `[SRC]/foo/baz.py` exists,
/// or `[SRC]/foo/baz.pyi` exists.
fn match_sources<'a>(paths: &'a [PathBuf], name: &str) -> Option<&'a Path> {
let relative_path: PathBuf = name.split('.').collect();
relative_path.components().next()?;
for root in paths {
let candidate = root.join(&relative_path);
if candidate.is_dir() {
return Some(root);
/// - With `match_source_strategy == MatchSourceStrategy::Root`, the module
/// named `foo.baz` will match `[SRC]` if `[SRC]/foo` is a
/// directory or `[SRC]/foo.py` exists.
///
/// - With `match_source_stratgy == MatchSourceStrategy::FullPath`, the module
/// named `foo.baz` will match `[SRC]` only if `[SRC]/foo/baz` is a directory,
/// or `[SRC]/foo/baz.py` exists or `[SRC]/foo/baz.pyi` exists.
fn match_sources<'a>(
paths: &'a [PathBuf],
name: &str,
match_source_strategy: MatchSourceStrategy,
) -> Option<&'a Path> {
match match_source_strategy {
MatchSourceStrategy::Root => {
let base = name.split('.').next()?;
for path in paths {
if let Ok(metadata) = fs::metadata(path.join(base)) {
if metadata.is_dir() {
return Some(path);
}
}
if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) {
if metadata.is_file() {
return Some(path);
}
}
}
None
}
if ["py", "pyi"]
.into_iter()
.any(|extension| candidate.with_extension(extension).is_file())
{
return Some(root);
MatchSourceStrategy::FullPath => {
let relative_path: PathBuf = name.split('.').collect();
relative_path.components().next()?;
for root in paths {
let candidate = root.join(&relative_path);
if candidate.is_dir() {
return Some(root);
}
if ["py", "pyi"]
.into_iter()
.any(|extension| candidate.with_extension(extension).is_file())
{
return Some(root);
}
}
None
}
}
None
}
#[expect(clippy::too_many_arguments)]
@@ -195,6 +229,7 @@ pub(crate) fn categorize_imports<'a>(
no_sections: bool,
section_order: &'a [ImportSection],
default_section: &'a ImportSection,
match_source_strategy: MatchSourceStrategy,
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default();
// Categorize `Stmt::Import`.
@@ -210,6 +245,7 @@ pub(crate) fn categorize_imports<'a>(
no_sections,
section_order,
default_section,
match_source_strategy,
);
block_by_type
.entry(import_type)
@@ -230,6 +266,7 @@ pub(crate) fn categorize_imports<'a>(
no_sections,
section_order,
default_section,
match_source_strategy,
);
block_by_type
.entry(classification)
@@ -250,6 +287,7 @@ pub(crate) fn categorize_imports<'a>(
no_sections,
section_order,
default_section,
match_source_strategy,
);
block_by_type
.entry(classification)
@@ -270,6 +308,7 @@ pub(crate) fn categorize_imports<'a>(
no_sections,
section_order,
default_section,
match_source_strategy,
);
block_by_type
.entry(classification)
@@ -424,9 +463,25 @@ impl fmt::Display for KnownModules {
}
}
/// Rule to determine whether a module path matches
/// a relative path from a source directory.
#[derive(Debug, Clone, Copy)]
pub(crate) enum MatchSourceStrategy {
/// Matches if first term in module path is found in file system
///
/// # Example
/// Module is `foo.bar.baz` and `[SRC]/foo` exists
Root,
/// Matches only if full module path is reflected in file system
///
/// # Example
/// Module is `foo.bar.baz` and `[SRC]/foo/bar/baz` exists
FullPath,
}
#[cfg(test)]
mod tests {
use crate::rules::isort::categorize::match_sources;
use crate::rules::isort::categorize::{MatchSourceStrategy, match_sources};
use std::fs;
use std::path::{Path, PathBuf};
@@ -467,17 +522,49 @@ mod tests {
let paths = vec![project_dir.clone()];
// Test with Root strategy
assert_eq!(
match_sources(&paths, "mypackage"),
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
assert_eq!(
match_sources(&paths, "mypackage.module1"),
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
assert_eq!(match_sources(&paths, "mypackage.nonexistent",), None);
assert_eq!(
match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
assert_eq!(
match_sources(&paths, "nonexistent", MatchSourceStrategy::Root),
None
);
// Test with FullPath strategy
assert_eq!(
match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath),
Some(project_dir.as_path())
);
assert_eq!(
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
Some(project_dir.as_path())
);
// Differs in behavior from [`MatchSourceStrategy::Root`]
assert_eq!(
match_sources(
&paths,
"mypackage.nonexistent",
MatchSourceStrategy::FullPath
),
None
);
}
/// Tests a src-based Python package layout:
@@ -501,12 +588,39 @@ mod tests {
let paths = vec![src_dir.clone()];
// Test with Root strategy
assert_eq!(
match_sources(&paths, "mypackage.module1"),
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
Some(src_dir.as_path())
);
assert_eq!(match_sources(&paths, "mypackage.nonexistent"), None);
assert_eq!(
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root),
Some(src_dir.as_path())
);
assert_eq!(
match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root),
Some(src_dir.as_path())
);
// Test with FullPath strategy
assert_eq!(
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
Some(src_dir.as_path())
);
// Differs in behavior from [`MatchSourceStrategy::Root`]
assert_eq!(
match_sources(
&paths,
"mypackage.nonexistent",
MatchSourceStrategy::FullPath
),
None
);
}
/// Tests a nested package layout:
@@ -533,13 +647,35 @@ mod tests {
let paths = vec![project_dir.clone()];
// Test with Root strategy
assert_eq!(
match_sources(&paths, "mypackage.subpackage.module2"),
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
assert_eq!(
match_sources(&paths, "mypackage.subpackage.nonexistent"),
match_sources(&paths, "mypackage.subpackage", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
// Test with FullPath strategy
assert_eq!(
match_sources(
&paths,
"mypackage.subpackage.module2",
MatchSourceStrategy::FullPath
),
Some(project_dir.as_path())
);
// Differs in behavior from [`MatchSourceStrategy::Root`]
assert_eq!(
match_sources(
&paths,
"mypackage.subpackage.nonexistent",
MatchSourceStrategy::FullPath
),
None
);
}
@@ -563,17 +699,52 @@ mod tests {
create_file(project_dir.join("namespace/package1/module1.py"));
let paths = vec![project_dir.clone()];
// Test with Root strategy
assert_eq!(
match_sources(&paths, "namespace.package1"),
match_sources(&paths, "namespace", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
assert_eq!(
match_sources(&paths, "namespace.package1.module1"),
match_sources(&paths, "namespace.package1", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
assert_eq!(match_sources(&paths, "namespace.package2.module1"), None);
assert_eq!(
match_sources(
&paths,
"namespace.package2.module1",
MatchSourceStrategy::Root
),
Some(project_dir.as_path())
);
// Test with FullPath strategy
assert_eq!(
match_sources(&paths, "namespace.package1", MatchSourceStrategy::FullPath),
Some(project_dir.as_path())
);
assert_eq!(
match_sources(
&paths,
"namespace.package1.module1",
MatchSourceStrategy::FullPath
),
Some(project_dir.as_path())
);
// Differs in behavior from [`MatchSourceStrategy::Root`]
assert_eq!(
match_sources(
&paths,
"namespace.package2.module1",
MatchSourceStrategy::FullPath
),
None
);
}
/// Tests a package with type stubs (.pyi files):
@@ -593,11 +764,12 @@ mod tests {
create_file(project_dir.join("mypackage/__init__.py"));
create_file(project_dir.join("mypackage/module1.pyi")); // Only create .pyi file, not .py
// Test with FullPath strategy
let paths = vec![project_dir.clone()];
// Module "mypackage.module1" should match project_dir using .pyi file
assert_eq!(
match_sources(&paths, "mypackage.module1"),
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
Some(project_dir.as_path())
);
}
@@ -624,17 +796,30 @@ mod tests {
create_file(project_dir.join("mypackage/feature/__init__.py"));
create_file(project_dir.join("mypackage/feature/submodule.py"));
// Test with Root strategy
let paths = vec![project_dir.clone()];
// Module "mypackage.feature" should match project_dir (matches the file first)
assert_eq!(
match_sources(&paths, "mypackage.feature", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
// Test with FullPath strategy
// Module "mypackage.feature" should match project_dir
assert_eq!(
match_sources(&paths, "mypackage.feature"),
match_sources(&paths, "mypackage.feature", MatchSourceStrategy::FullPath),
Some(project_dir.as_path())
);
// Module "mypackage.feature.submodule" should match project_dir
assert_eq!(
match_sources(&paths, "mypackage.feature.submodule"),
match_sources(
&paths,
"mypackage.feature.submodule",
MatchSourceStrategy::FullPath
),
Some(project_dir.as_path())
);
}
@@ -672,13 +857,13 @@ mod tests {
// Module "package1" should match project1_dir
assert_eq!(
match_sources(&paths, "package1"),
match_sources(&paths, "package1", MatchSourceStrategy::Root),
Some(project1_dir.as_path())
);
// Module "package2" should match project2_dir
assert_eq!(
match_sources(&paths, "package2"),
match_sources(&paths, "package2", MatchSourceStrategy::Root),
Some(project2_dir.as_path())
);
@@ -687,7 +872,7 @@ mod tests {
// Module "package1" should still match project1_dir
assert_eq!(
match_sources(&paths_reversed, "package1"),
match_sources(&paths_reversed, "package1", MatchSourceStrategy::Root),
Some(project1_dir.as_path())
);
}
@@ -700,7 +885,8 @@ mod tests {
///
/// In theory this should never happen since we expect
/// module names to have been normalized by the time we
/// call `match_sources`.
/// call `match_sources`. But it is worth noting that the
/// behavior is different depending on the [`MatchSourceStrategy`]
#[test]
fn test_empty_module_name() {
let temp_dir = tempdir().unwrap();
@@ -708,9 +894,16 @@ mod tests {
create_dir(project_dir.join("mypackage"));
let paths = vec![project_dir];
let paths = vec![project_dir.clone()];
assert_eq!(match_sources(&paths, ""), None);
assert_eq!(
match_sources(&paths, "", MatchSourceStrategy::Root),
Some(project_dir.as_path())
);
assert_eq!(
match_sources(&paths, "", MatchSourceStrategy::FullPath),
None
);
}
/// Tests behavior with an empty list of source paths
@@ -718,6 +911,14 @@ mod tests {
fn test_empty_paths() {
let paths: Vec<PathBuf> = vec![];
assert_eq!(match_sources(&paths, "mypackage"), None);
// Empty paths should return None
assert_eq!(
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
None
);
assert_eq!(
match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath),
None
);
}
}

View File

@@ -5,8 +5,8 @@ use std::path::PathBuf;
use annotate::annotate_imports;
use block::{Block, Trailer};
pub(crate) use categorize::categorize;
use categorize::categorize_imports;
pub use categorize::{ImportSection, ImportType};
use categorize::{MatchSourceStrategy, categorize_imports};
use comments::Comment;
use normalize::normalize_imports;
use order::order_imports;
@@ -76,6 +76,7 @@ pub(crate) fn format_imports(
source_type: PySourceType,
target_version: PythonVersion,
settings: &Settings,
match_source_strategy: MatchSourceStrategy,
tokens: &Tokens,
) -> String {
let trailer = &block.trailer;
@@ -103,6 +104,7 @@ pub(crate) fn format_imports(
package,
target_version,
settings,
match_source_strategy,
);
if !block_output.is_empty() && !output.is_empty() {
@@ -159,6 +161,7 @@ fn format_import_block(
package: Option<PackageRoot<'_>>,
target_version: PythonVersion,
settings: &Settings,
match_source_strategy: MatchSourceStrategy,
) -> String {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum LineInsertion {
@@ -179,6 +182,7 @@ fn format_import_block(
settings.no_sections,
&settings.section_order,
&settings.default_section,
match_source_strategy,
);
let mut output = String::new();

View File

@@ -14,7 +14,9 @@ use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::line_width::LineWidthBuilder;
use crate::package::PackageRoot;
use crate::preview::is_full_path_match_source_strategy_enabled;
use crate::rules::isort::block::Block;
use crate::rules::isort::categorize::MatchSourceStrategy;
use crate::rules::isort::{comments, format_imports};
use crate::settings::LinterSettings;
use crate::{Edit, Fix, FixAvailability, Violation};
@@ -38,6 +40,12 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// import pandas
/// ```
///
/// ## Preview
/// When [`preview`](https://docs.astral.sh/ruff/preview/) mode is enabled, Ruff applies a stricter criterion
/// for determining whether an import should be classified as first-party.
/// Specifically, for an import of the form `import foo.bar.baz`, Ruff will
/// check that `foo/bar`, relative to a [user-specified `src`](https://docs.astral.sh/ruff/settings/#src) directory, contains either
/// the directory `baz` or else a file with the name `baz.py` or `baz.pyi`.
#[derive(ViolationMetadata)]
pub(crate) struct UnsortedImports;
@@ -121,6 +129,12 @@ pub(crate) fn organize_imports(
trailing_lines_end(block.imports.last().unwrap(), locator.contents())
};
let match_source_strategy = if is_full_path_match_source_strategy_enabled(settings) {
MatchSourceStrategy::FullPath
} else {
MatchSourceStrategy::Root
};
// Generate the sorted import block.
let expected = format_imports(
block,
@@ -134,6 +148,7 @@ pub(crate) fn organize_imports(
source_type,
target_version,
&settings.isort,
match_source_strategy,
tokens,
);

View File

@@ -4,9 +4,9 @@ use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
/// ## Removed
/// ## Deprecated
///
/// This rule has been removed as it's highly opinionated and overly strict in most cases.
/// This rule has been deprecated as it's highly opinionated and overly strict in most cases.
///
/// ## What it does
/// Checks for assignments to the variable `df`.

View File

@@ -141,6 +141,7 @@ impl Violation for InvalidFirstArgumentNameForClassMethod {
#[derive_message_formats]
// The first string below is what shows up in the documentation
// in the rule table, and it is the more common case.
#[expect(clippy::if_not_else)]
fn message(&self) -> String {
if !self.is_new {
"First argument of a class method should be named `cls`".to_string()

View File

@@ -18,8 +18,8 @@ use crate::rules::pep8_naming::helpers;
/// > (Lets hope that these variables are meant for use inside one module
/// > only.) The conventions are about the same as those for functions.
/// >
/// > Modules that are designed for use via `from M import *` should use the
/// > `__all__` mechanism to prevent exporting globals, or use the older
/// > Modules that are designed for use via from M import * should use the
/// > __all__ mechanism to prevent exporting globals, or use the older
/// > convention of prefixing such globals with an underscore (which you might
/// > want to do to indicate these globals are “module non-public”).
/// >

View File

@@ -15,8 +15,11 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix;
use crate::preview::is_dunder_init_fix_unused_import_enabled;
use crate::preview::{
is_dunder_init_fix_unused_import_enabled, is_full_path_match_source_strategy_enabled,
};
use crate::registry::Rule;
use crate::rules::isort::categorize::MatchSourceStrategy;
use crate::rules::{isort, isort::ImportSection, isort::ImportType};
use crate::{Applicability, Fix, FixAvailability, Violation};
@@ -60,10 +63,6 @@ use crate::{Applicability, Fix, FixAvailability, Violation};
/// to remove third-party and standard library imports -- the fix is unsafe because the module's
/// interface changes.
///
/// See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)
/// for more details on how Ruff
/// determines whether an import is first or third-party.
///
/// ## Example
///
/// ```python
@@ -92,6 +91,11 @@ use crate::{Applicability, Fix, FixAvailability, Violation};
/// print("numpy is not installed")
/// ```
///
/// ## Preview
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
/// the criterion for determining whether an import is first-party
/// is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
///
/// ## Options
/// - `lint.ignore-init-module-imports`
/// - `lint.pyflakes.allowed-unused-imports`
@@ -227,6 +231,11 @@ enum UnusedImportContext {
fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
let source_name = import.source_name().join(".");
let match_source_strategy = if is_full_path_match_source_strategy_enabled(checker.settings()) {
MatchSourceStrategy::FullPath
} else {
MatchSourceStrategy::Root
};
let category = isort::categorize(
&source_name,
import.qualified_name().is_unresolved_import(),
@@ -238,6 +247,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
checker.settings().isort.no_sections,
&checker.settings().isort.section_order,
&checker.settings().isort.default_section,
match_source_strategy,
);
matches! {
category,

View File

@@ -43,15 +43,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// This rule's fix is marked as unsafe because removing an unused variable assignment may
/// delete comments that are attached to the assignment.
///
/// ## See also
///
/// This rule does not apply to bindings in unpacked assignments (e.g. `x, y = 1, 2`). See
/// [`unused-unpacked-variable`][RUF059] for this case.
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [RUF059]: https://docs.astral.sh/ruff/rules/unused-unpacked-variable/
#[derive(ViolationMetadata)]
pub(crate) struct UnusedVariable {
pub name: String,

View File

@@ -10,6 +10,7 @@ mod tests {
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
@@ -29,4 +30,22 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::InvalidMockAccess, Path::new("PGH005_0.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pygrep_hooks").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -5,6 +5,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_invalid_async_mock_access_check_enabled;
#[derive(Debug, PartialEq, Eq)]
enum Reason {
@@ -61,16 +62,18 @@ pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) {
| "assert_has_calls"
| "assert_not_called"
);
let is_uncalled_async_mock_method = matches!(
attr.as_str(),
"assert_awaited"
| "assert_awaited_once"
| "assert_awaited_with"
| "assert_awaited_once_with"
| "assert_any_await"
| "assert_has_awaits"
| "assert_not_awaited"
);
let is_uncalled_async_mock_method =
is_invalid_async_mock_access_check_enabled(checker.settings())
&& matches!(
attr.as_str(),
"assert_awaited"
| "assert_awaited_once"
| "assert_awaited_with"
| "assert_awaited_once_with"
| "assert_any_await"
| "assert_has_awaits"
| "assert_not_awaited"
);
if is_uncalled_mock_method || is_uncalled_async_mock_method {
checker.report_diagnostic(
InvalidMockAccess {
@@ -101,16 +104,18 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) {
| "has_calls"
| "not_called"
);
let is_missing_async_mock_method = matches!(
attr.as_str(),
"awaited"
| "awaited_once"
| "awaited_with"
| "awaited_once_with"
| "any_await"
| "has_awaits"
| "not_awaited"
);
let is_missing_async_mock_method =
is_invalid_async_mock_access_check_enabled(checker.settings())
&& matches!(
attr.as_str(),
"awaited"
| "awaited_once"
| "awaited_with"
| "awaited_once_with"
| "any_await"
| "has_awaits"
| "not_awaited"
);
if is_missing_mock_method || is_missing_async_mock_method {
checker.report_diagnostic(
InvalidMockAccess {

View File

@@ -98,112 +98,3 @@ PGH005 Mock method should be called: `assert_called_once_with`
13 |
14 | # OK
|
PGH005 Non-existent mock method: `not_awaited`
--> PGH005_0.py:26:8
|
24 | # =================
25 | # Errors
26 | assert my_mock.not_awaited()
| ^^^^^^^^^^^^^^^^^^^^^
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
|
PGH005 Non-existent mock method: `awaited_once_with`
--> PGH005_0.py:27:8
|
25 | # Errors
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
|
PGH005 Non-existent mock method: `not_awaited`
--> PGH005_0.py:28:8
|
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
| ^^^^^^^^^^^^^^^^^^^
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
|
PGH005 Non-existent mock method: `awaited_once_with`
--> PGH005_0.py:29:8
|
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
|
PGH005 Mock method should be called: `assert_not_awaited`
--> PGH005_0.py:30:1
|
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
|
PGH005 Mock method should be called: `assert_awaited`
--> PGH005_0.py:31:1
|
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
| ^^^^^^^^^^^^^^^^^^^^^^
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
|
PGH005 Mock method should be called: `assert_awaited_once_with`
--> PGH005_0.py:32:1
|
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
|
PGH005 Mock method should be called: `assert_awaited_once_with`
--> PGH005_0.py:33:1
|
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
|
PGH005 Mock method should be called: `assert_awaited_once_with`
--> PGH005_0.py:34:1
|
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 | assert my_mock.awaited
|
PGH005 Non-existent mock method: `awaited`
--> PGH005_0.py:35:8
|
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
| ^^^^^^^^^^^^^^^
36 |
37 | # OK
|

View File

@@ -0,0 +1,209 @@
---
source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs
---
PGH005 Non-existent mock method: `not_called`
--> PGH005_0.py:4:8
|
2 | # ============
3 | # Errors
4 | assert my_mock.not_called()
| ^^^^^^^^^^^^^^^^^^^^
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
|
PGH005 Non-existent mock method: `called_once_with`
--> PGH005_0.py:5:8
|
3 | # Errors
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
|
PGH005 Non-existent mock method: `not_called`
--> PGH005_0.py:6:8
|
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
| ^^^^^^^^^^^^^^^^^^
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
|
PGH005 Non-existent mock method: `called_once_with`
--> PGH005_0.py:7:8
|
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^
8 | my_mock.assert_not_called
9 | my_mock.assert_called
|
PGH005 Mock method should be called: `assert_not_called`
--> PGH005_0.py:8:1
|
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
| ^^^^^^^^^^^^^^^^^^^^^^^^^
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
|
PGH005 Mock method should be called: `assert_called`
--> PGH005_0.py:9:1
|
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
9 | my_mock.assert_called
| ^^^^^^^^^^^^^^^^^^^^^
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
|
PGH005 Mock method should be called: `assert_called_once_with`
--> PGH005_0.py:10:1
|
8 | my_mock.assert_not_called
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
|
PGH005 Mock method should be called: `assert_called_once_with`
--> PGH005_0.py:11:1
|
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 | MyMock.assert_called_once_with
|
PGH005 Mock method should be called: `assert_called_once_with`
--> PGH005_0.py:12:1
|
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 |
14 | # OK
|
PGH005 Non-existent mock method: `not_awaited`
--> PGH005_0.py:26:8
|
24 | # =================
25 | # Errors
26 | assert my_mock.not_awaited()
| ^^^^^^^^^^^^^^^^^^^^^
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
|
PGH005 Non-existent mock method: `awaited_once_with`
--> PGH005_0.py:27:8
|
25 | # Errors
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
|
PGH005 Non-existent mock method: `not_awaited`
--> PGH005_0.py:28:8
|
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
| ^^^^^^^^^^^^^^^^^^^
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
|
PGH005 Non-existent mock method: `awaited_once_with`
--> PGH005_0.py:29:8
|
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
|
PGH005 Mock method should be called: `assert_not_awaited`
--> PGH005_0.py:30:1
|
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
|
PGH005 Mock method should be called: `assert_awaited`
--> PGH005_0.py:31:1
|
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
| ^^^^^^^^^^^^^^^^^^^^^^
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
|
PGH005 Mock method should be called: `assert_awaited_once_with`
--> PGH005_0.py:32:1
|
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
|
PGH005 Mock method should be called: `assert_awaited_once_with`
--> PGH005_0.py:33:1
|
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
|
PGH005 Mock method should be called: `assert_awaited_once_with`
--> PGH005_0.py:34:1
|
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 | assert my_mock.awaited
|
PGH005 Non-existent mock method: `awaited`
--> PGH005_0.py:35:8
|
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
| ^^^^^^^^^^^^^^^
36 |
37 | # OK
|

View File

@@ -252,6 +252,30 @@ mod tests {
Ok(())
}
#[test_case(Rule::BidirectionalUnicode, Path::new("bidirectional_unicode.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pylint").join(path).as_path(),
&LinterSettings {
pylint: pylint::settings::Settings {
allow_dunder_method_names: FxHashSet::from_iter([
"__special_custom_magic__".to_string()
]),
..pylint::settings::Settings::default()
},
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test]
fn continue_in_finally() -> Result<()> {
let diagnostics = test_path(
@@ -420,6 +444,19 @@ mod tests {
Ok(())
}
#[test]
fn preview_useless_import_alias() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/import_aliasing_2/__init__.py"),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(Rule::UselessImportAlias)
},
)?;
assert_diagnostics!(diagnostics);
Ok(())
}
#[test]
fn import_outside_top_level_with_banned() -> Result<()> {
let diagnostics = test_path(

View File

@@ -1,9 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_source_file::Line;
use crate::{Violation, checkers::ast::LintContext};
use crate::{
Violation, checkers::ast::LintContext, preview::is_bidi_forbid_arabic_letter_mark_enabled,
};
const BIDI_UNICODE: [char; 11] = [
const BIDI_UNICODE: [char; 10] = [
'\u{202A}', //{LEFT-TO-RIGHT EMBEDDING}
'\u{202B}', //{RIGHT-TO-LEFT EMBEDDING}
'\u{202C}', //{POP DIRECTIONAL FORMATTING}
@@ -17,7 +19,6 @@ const BIDI_UNICODE: [char; 11] = [
// https://peps.python.org/pep-0672/
// so the list above might not be complete
'\u{200F}', //{RIGHT-TO-LEFT MARK}
'\u{061C}', //{ARABIC LETTER MARK}
// We don't use
// "\u200E" # \n{LEFT-TO-RIGHT MARK}
// as this is the default for latin files and can't be used
@@ -61,7 +62,12 @@ impl Violation for BidirectionalUnicode {
/// PLE2502
pub(crate) fn bidirectional_unicode(line: &Line, context: &LintContext) {
if line.contains(BIDI_UNICODE) {
if line.contains(BIDI_UNICODE)
|| (is_bidi_forbid_arabic_letter_mark_enabled(context.settings())
&& line.contains(
'\u{061C}', //{ARABIC LETTER MARK}
))
{
context.report_diagnostic(BidirectionalUnicode, line.full_range());
}
}

View File

@@ -4,11 +4,13 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_ignore_init_files_in_useless_alias_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for import aliases that do not rename the original package.
/// This rule does not apply in `__init__.py` files.
///
/// In [preview] this rule does not apply in `__init__.py` files.
///
/// ## Why is this bad?
/// The import alias is redundant and should be removed to avoid confusion.
@@ -33,6 +35,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ```python
/// import numpy
/// ```
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct UselessImportAlias {
required_import_conflict: bool,
@@ -43,6 +47,7 @@ impl Violation for UselessImportAlias {
#[derive_message_formats]
fn message(&self) -> String {
#[expect(clippy::if_not_else)]
if !self.required_import_conflict {
"Import alias does not rename original package".to_string()
} else {
@@ -69,7 +74,9 @@ pub(crate) fn useless_import_alias(checker: &Checker, alias: &Alias) {
}
// A re-export in __init__.py is probably intentional.
if checker.path().ends_with("__init__.py") {
if checker.path().ends_with("__init__.py")
&& is_ignore_init_files_in_useless_alias_enabled(checker.settings())
{
return;
}

View File

@@ -1,4 +1,18 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
PLC0414 [*] Import alias does not rename original package
--> __init__.py:1:8
|
1 | import collections as collections
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | from collections import OrderedDict as OrderedDict
3 | from . import foo as foo
|
help: Remove import alias
- import collections as collections
1 + import collections
2 | from collections import OrderedDict as OrderedDict
3 | from . import foo as foo
4 | from .foo import bar as bar
note: This is an unsafe fix and may change runtime behavior

View File

@@ -21,16 +21,6 @@ PLE2502 Contains control characters that can permit obfuscated code
7 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:8:1
|
7 | # E2502
8 | another = "x؜" * 50 # "؜x" is assigned
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 |
10 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:11:1
|

View File

@@ -0,0 +1,52 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:2:1
|
1 | # E2502
2 | print("שלום")
| ^^^^^^^^^^^^^
3 |
4 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:5:1
|
4 | # E2502
5 | example = "x" * 100 # "x" is assigned
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 |
7 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:8:1
|
7 | # E2502
8 | another = "x؜" * 50 # "؜x" is assigned
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 |
10 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:11:1
|
10 | # E2502
11 | if access_level != "none": # Check if admin ' and access_level != 'user
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 | print("You are an admin.")
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:17:1
|
15 | # E2502
16 | def subtract_funds(account: str, amount: int):
17 | """Subtract funds from bank account then """
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 | return
19 | bank[account] -= amount
|

View File

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

View File

@@ -147,6 +147,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
future_annotations: true,
..settings::LinterSettings::for_rule(rule_code)
},
@@ -356,19 +357,4 @@ mod tests {
2 | from pipes import quote, Template
");
}
#[test]
fn unnecessary_default_type_args_stubs_py312_preview() -> Result<()> {
let snapshot = format!("{}__preview", "UP043.pyi");
let diagnostics = test_path(
Path::new("pyupgrade/UP043.pyi"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY312.into(),
..settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -164,7 +164,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
stack.push(&mut string.left);
stack.push(&mut string.right);
}
libcst_native::String::Formatted(_) | libcst_native::String::Templated(_) => {}
libcst_native::String::Formatted(_) => {}
}
}
}

View File

@@ -102,7 +102,7 @@ impl AlwaysFixableViolation for QuotedAnnotation {
/// UP037
pub(crate) fn quoted_annotation(checker: &Checker, annotation: &str, range: TextRange) {
let add_future_import = checker.settings().future_annotations
let add_future_import = checker.settings().future_annotations()
&& checker.semantic().in_runtime_evaluated_annotation();
if !(checker.semantic().in_typing_only_annotation() || add_future_import) {

View File

@@ -1,8 +1,6 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
@@ -96,22 +94,14 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
};
// Find the enclosing function definition (if any).
let Some(
func_stmt @ Stmt::FunctionDef(ast::StmtFunctionDef {
parameters: parent_parameters,
..
}),
) = parents.find(|stmt| stmt.is_function_def_stmt())
let Some(Stmt::FunctionDef(ast::StmtFunctionDef {
parameters: parent_parameters,
..
})) = parents.find(|stmt| stmt.is_function_def_stmt())
else {
return;
};
if is_builtins_super(checker.semantic(), call)
&& !has_local_dunder_class_var_ref(checker.semantic(), func_stmt)
{
return;
}
// Extract the name of the first argument to the enclosing function.
let Some(parent_arg) = parent_parameters.args.first() else {
return;
@@ -203,67 +193,3 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
fn is_super_call_with_arguments(call: &ast::ExprCall, checker: &Checker) -> bool {
checker.semantic().match_builtin_expr(&call.func, "super") && !call.arguments.is_empty()
}
/// Returns `true` if the function contains load references to `__class__` or `super` without
/// local binding.
///
/// This indicates that the function relies on the implicit `__class__` cell variable created by
/// Python when `super()` is called without arguments, making it unsafe to remove `super()` parameters.
fn has_local_dunder_class_var_ref(semantic: &SemanticModel, func_stmt: &Stmt) -> bool {
if semantic.current_scope().has("__class__") {
return false;
}
let mut finder = ClassCellReferenceFinder::new();
finder.visit_stmt(func_stmt);
finder.found()
}
/// Returns `true` if the call is to the built-in `builtins.super` function.
fn is_builtins_super(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
semantic
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["builtins", "super"]))
}
/// A [`Visitor`] that searches for implicit reference to `__class__` cell,
/// excluding nested class definitions.
#[derive(Debug)]
struct ClassCellReferenceFinder {
has_class_cell: bool,
}
impl ClassCellReferenceFinder {
pub(crate) fn new() -> Self {
ClassCellReferenceFinder {
has_class_cell: false,
}
}
pub(crate) fn found(&self) -> bool {
self.has_class_cell
}
}
impl<'a> Visitor<'a> for ClassCellReferenceFinder {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::ClassDef(_) => {}
_ => {
if !self.has_class_cell {
walk_stmt(self, stmt);
}
}
}
}
fn visit_expr(&mut self, expr: &'a Expr) {
if expr.as_name_expr().is_some_and(|name| {
matches!(name.id.as_str(), "super" | "__class__") && name.ctx.is_load()
}) {
self.has_class_cell = true;
return;
}
walk_expr(self, expr);
}
}

View File

@@ -8,7 +8,6 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// ## What it does
/// Checks for unnecessary default type arguments for `Generator` and
/// `AsyncGenerator` on Python 3.13+.
/// In [preview], this rule will also apply to stub files.
///
/// ## Why is this bad?
/// Python 3.13 introduced the ability for type parameters to specify default
@@ -60,8 +59,6 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// - [Annotating generators and coroutines](https://docs.python.org/3/library/typing.html#annotating-generators-and-coroutines)
/// - [Python documentation: `typing.Generator`](https://docs.python.org/3/library/typing.html#typing.Generator)
/// - [Python documentation: `typing.AsyncGenerator`](https://docs.python.org/3/library/typing.html#typing.AsyncGenerator)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct UnnecessaryDefaultTypeArgs;

View File

@@ -33,8 +33,8 @@ impl CallKind {
}
}
/// ## Removed
/// This rule was removed as using [PEP 604] syntax in `isinstance` and `issubclass` calls
/// ## Deprecation
/// This rule was deprecated as using [PEP 604] syntax in `isinstance` and `issubclass` calls
/// isn't recommended practice, and it incorrectly suggests that other typing syntaxes like [PEP 695]
/// would be supported by `isinstance` and `issubclass`. Using the [PEP 604] syntax
/// is also slightly slower.

View File

@@ -146,6 +146,25 @@ help: Remove `super()` parameters
95 | # see: https://github.com/astral-sh/ruff/issues/18684
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:107:23
|
105 | class C:
106 | def f(self):
107 | builtins.super(C, self)
| ^^^^^^^^^
|
help: Remove `super()` parameters
104 |
105 | class C:
106 | def f(self):
- builtins.super(C, self)
107 + builtins.super()
108 |
109 |
110 | # see: https://github.com/astral-sh/ruff/issues/18533
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:113:14
|
@@ -275,8 +294,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
help: Remove `super()` parameters
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
@@ -284,213 +301,4 @@ help: Remove `super()` parameters
142 | def method3(self):
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 + super().some_method() # Should be fixed - no keywords
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
146 | # Must be detected
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:154:23
|
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
154 | builtins.super(ChildD1, self).f()
| ^^^^^^^^^^^^^^^
155 |
156 | class ChildD2(ParentD):
|
help: Remove `super()` parameters
151 | class ChildD1(ParentD):
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
- builtins.super(ChildD1, self).f()
154 + builtins.super().f()
155 |
156 | class ChildD2(ParentD):
157 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:159:23
|
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
159 | builtins.super(ChildD2, self).f()
| ^^^^^^^^^^^^^^^
160 |
161 | class ChildD3(ParentD):
|
help: Remove `super()` parameters
156 | class ChildD2(ParentD):
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
- builtins.super(ChildD2, self).f()
159 + builtins.super().f()
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:163:23
|
161 | class ChildD3(ParentD):
162 | def f(self):
163 | builtins.super(ChildD3, self).f()
| ^^^^^^^^^^^^^^^
164 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
- builtins.super(ChildD3, self).f()
163 + builtins.super().f()
164 | super # Python injects __class__ into scope
165 |
166 | import builtins as builtins_alias
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:169:29
|
167 | class ChildD4(ParentD):
168 | def f(self):
169 | builtins_alias.super(ChildD4, self).f()
| ^^^^^^^^^^^^^^^
170 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
166 | import builtins as builtins_alias
167 | class ChildD4(ParentD):
168 | def f(self):
- builtins_alias.super(ChildD4, self).f()
169 + builtins_alias.super().f()
170 | super # Python injects __class__ into scope
171 |
172 | class ChildD5(ParentD):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:176:23
|
174 | super = 1
175 | super # Python injects __class__ into scope
176 | builtins.super(ChildD5, self).f()
| ^^^^^^^^^^^^^^^
177 |
178 | class ChildD6(ParentD):
|
help: Remove `super()` parameters
173 | def f(self):
174 | super = 1
175 | super # Python injects __class__ into scope
- builtins.super(ChildD5, self).f()
176 + builtins.super().f()
177 |
178 | class ChildD6(ParentD):
179 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:182:23
|
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
182 | builtins.super(ChildD6, self).f()
| ^^^^^^^^^^^^^^^
183 |
184 | class ChildD7(ParentD):
|
help: Remove `super()` parameters
179 | def f(self):
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD6, self).f()
182 + builtins.super().f()
183 |
184 | class ChildD7(ParentD):
185 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:188:23
|
186 | def x():
187 | __class__ # Python injects __class__ into scope
188 | builtins.super(ChildD7, self).f()
| ^^^^^^^^^^^^^^^
189 |
190 | class ChildD8(ParentD):
|
help: Remove `super()` parameters
185 | def f(self):
186 | def x():
187 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD7, self).f()
188 + builtins.super().f()
189 |
190 | class ChildD8(ParentD):
191 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:195:23
|
193 | super = 1
194 | super # Python injects __class__ into scope
195 | builtins.super(ChildD8, self).f()
| ^^^^^^^^^^^^^^^
196 |
197 | class ChildD9(ParentD):
|
help: Remove `super()` parameters
192 | def x():
193 | super = 1
194 | super # Python injects __class__ into scope
- builtins.super(ChildD8, self).f()
195 + builtins.super().f()
196 |
197 | class ChildD9(ParentD):
198 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:202:23
|
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
202 | builtins.super(ChildD9, self).f()
| ^^^^^^^^^^^^^^^
203 |
204 | class ChildD10(ParentD):
|
help: Remove `super()` parameters
199 | def x():
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD9, self).f()
202 + builtins.super().f()
203 |
204 | class ChildD10(ParentD):
205 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:209:23
|
207 | __class__ = 1
208 | super # Python injects __class__ into scope
209 | builtins.super(ChildD10, self).f()
| ^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
206 | def x():
207 | __class__ = 1
208 | super # Python injects __class__ into scope
- builtins.super(ChildD10, self).f()
209 + builtins.super().f()
210 |
211 |
212 | # Must be ignored
note: This is an unsafe fix and may change runtime behavior

View File

@@ -139,6 +139,24 @@ help: Remove `super()` parameters
94 |
95 | # see: https://github.com/astral-sh/ruff/issues/18684
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:107:23
|
105 | class C:
106 | def f(self):
107 | builtins.super(C, self)
| ^^^^^^^^^
|
help: Remove `super()` parameters
104 |
105 | class C:
106 | def f(self):
- builtins.super(C, self)
107 + builtins.super()
108 |
109 |
110 | # see: https://github.com/astral-sh/ruff/issues/18533
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:113:14
|
@@ -268,8 +286,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
help: Remove `super()` parameters
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
@@ -277,202 +293,3 @@ help: Remove `super()` parameters
142 | def method3(self):
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 + super().some_method() # Should be fixed - no keywords
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
146 | # Must be detected
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:154:23
|
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
154 | builtins.super(ChildD1, self).f()
| ^^^^^^^^^^^^^^^
155 |
156 | class ChildD2(ParentD):
|
help: Remove `super()` parameters
151 | class ChildD1(ParentD):
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
- builtins.super(ChildD1, self).f()
154 + builtins.super().f()
155 |
156 | class ChildD2(ParentD):
157 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:159:23
|
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
159 | builtins.super(ChildD2, self).f()
| ^^^^^^^^^^^^^^^
160 |
161 | class ChildD3(ParentD):
|
help: Remove `super()` parameters
156 | class ChildD2(ParentD):
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
- builtins.super(ChildD2, self).f()
159 + builtins.super().f()
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:163:23
|
161 | class ChildD3(ParentD):
162 | def f(self):
163 | builtins.super(ChildD3, self).f()
| ^^^^^^^^^^^^^^^
164 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
- builtins.super(ChildD3, self).f()
163 + builtins.super().f()
164 | super # Python injects __class__ into scope
165 |
166 | import builtins as builtins_alias
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:169:29
|
167 | class ChildD4(ParentD):
168 | def f(self):
169 | builtins_alias.super(ChildD4, self).f()
| ^^^^^^^^^^^^^^^
170 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
166 | import builtins as builtins_alias
167 | class ChildD4(ParentD):
168 | def f(self):
- builtins_alias.super(ChildD4, self).f()
169 + builtins_alias.super().f()
170 | super # Python injects __class__ into scope
171 |
172 | class ChildD5(ParentD):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:176:23
|
174 | super = 1
175 | super # Python injects __class__ into scope
176 | builtins.super(ChildD5, self).f()
| ^^^^^^^^^^^^^^^
177 |
178 | class ChildD6(ParentD):
|
help: Remove `super()` parameters
173 | def f(self):
174 | super = 1
175 | super # Python injects __class__ into scope
- builtins.super(ChildD5, self).f()
176 + builtins.super().f()
177 |
178 | class ChildD6(ParentD):
179 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:182:23
|
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
182 | builtins.super(ChildD6, self).f()
| ^^^^^^^^^^^^^^^
183 |
184 | class ChildD7(ParentD):
|
help: Remove `super()` parameters
179 | def f(self):
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD6, self).f()
182 + builtins.super().f()
183 |
184 | class ChildD7(ParentD):
185 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:188:23
|
186 | def x():
187 | __class__ # Python injects __class__ into scope
188 | builtins.super(ChildD7, self).f()
| ^^^^^^^^^^^^^^^
189 |
190 | class ChildD8(ParentD):
|
help: Remove `super()` parameters
185 | def f(self):
186 | def x():
187 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD7, self).f()
188 + builtins.super().f()
189 |
190 | class ChildD8(ParentD):
191 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:195:23
|
193 | super = 1
194 | super # Python injects __class__ into scope
195 | builtins.super(ChildD8, self).f()
| ^^^^^^^^^^^^^^^
196 |
197 | class ChildD9(ParentD):
|
help: Remove `super()` parameters
192 | def x():
193 | super = 1
194 | super # Python injects __class__ into scope
- builtins.super(ChildD8, self).f()
195 + builtins.super().f()
196 |
197 | class ChildD9(ParentD):
198 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:202:23
|
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
202 | builtins.super(ChildD9, self).f()
| ^^^^^^^^^^^^^^^
203 |
204 | class ChildD10(ParentD):
|
help: Remove `super()` parameters
199 | def x():
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD9, self).f()
202 + builtins.super().f()
203 |
204 | class ChildD10(ParentD):
205 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:209:23
|
207 | __class__ = 1
208 | super # Python injects __class__ into scope
209 | builtins.super(ChildD10, self).f()
| ^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
206 | def x():
207 | __class__ = 1
208 | super # Python injects __class__ into scope
- builtins.super(ChildD10, self).f()
209 + builtins.super().f()
210 |
211 |
212 | # Must be ignored

View File

@@ -1,128 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:4:15
|
4 | def func() -> Generator[int, None, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
5 | yield 42
|
help: Remove default type arguments
1 | from collections.abc import Generator, AsyncGenerator
2 |
3 |
- def func() -> Generator[int, None, None]:
4 + def func() -> Generator[int]:
5 | yield 42
6 |
7 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:8:15
|
8 | def func() -> Generator[int, None]:
| ^^^^^^^^^^^^^^^^^^^^
9 | yield 42
|
help: Remove default type arguments
5 | yield 42
6 |
7 |
- def func() -> Generator[int, None]:
8 + def func() -> Generator[int]:
9 | yield 42
10 |
11 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:21:15
|
21 | def func() -> Generator[int, int, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^
22 | _ = yield 42
23 | return None
|
help: Remove default type arguments
18 | return foo
19 |
20 |
- def func() -> Generator[int, int, None]:
21 + def func() -> Generator[int, int]:
22 | _ = yield 42
23 | return None
24 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:31:21
|
31 | async def func() -> AsyncGenerator[int, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^
32 | yield 42
|
help: Remove default type arguments
28 | return 42
29 |
30 |
- async def func() -> AsyncGenerator[int, None]:
31 + async def func() -> AsyncGenerator[int]:
32 | yield 42
33 |
34 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:47:15
|
47 | def func() -> Generator[str, None, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
48 | yield "hello"
|
help: Remove default type arguments
44 | from typing import Generator, AsyncGenerator
45 |
46 |
- def func() -> Generator[str, None, None]:
47 + def func() -> Generator[str]:
48 | yield "hello"
49 |
50 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:51:21
|
51 | async def func() -> AsyncGenerator[str, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^
52 | yield "hello"
|
help: Remove default type arguments
48 | yield "hello"
49 |
50 |
- async def func() -> AsyncGenerator[str, None]:
51 + async def func() -> AsyncGenerator[str]:
52 | yield "hello"
53 |
54 |
UP043 [*] Unnecessary default type arguments
--> UP043.pyi:55:21
|
55 | async def func() -> AsyncGenerator[ # type: ignore
| _____________________^
56 | | str,
57 | | None
58 | | ]:
| |_^
59 | yield "hello"
|
help: Remove default type arguments
52 | yield "hello"
53 |
54 |
- async def func() -> AsyncGenerator[ # type: ignore
- str,
- None
- ]:
55 + async def func() -> AsyncGenerator[str]:
56 | yield "hello"
note: This is an unsafe fix and may change runtime behavior

View File

@@ -85,7 +85,6 @@ mod tests {
#[test_case(Rule::InvalidAssertMessageLiteralArgument, Path::new("RUF040.py"))]
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))]
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))]
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))]
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_CR.py"))]
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_LF.py"))]
@@ -536,6 +535,7 @@ mod tests {
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))]
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_2.py"))]
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_3.py"))]
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
@@ -649,6 +649,7 @@ mod tests {
let diagnostics = test_path(
Path::new("ruff").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
future_annotations: true,
unresolved_target_version: PythonVersion::PY39.into(),
..settings::LinterSettings::for_rule(rule_code)

View File

@@ -19,10 +19,6 @@ use crate::rules::ruff::typing::type_hint_explicitly_allows_none;
/// Checks for the use of implicit `Optional` in type annotations when the
/// default parameter value is `None`.
///
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
/// annotations` will be added if doing so would allow using the `|` operator on
/// a Python version before 3.10.
///
/// ## Why is this bad?
/// Implicit `Optional` is prohibited by [PEP 484]. It is confusing and
/// inconsistent with the rest of the type system.
@@ -77,6 +73,12 @@ use crate::rules::ruff::typing::type_hint_explicitly_allows_none;
/// - `target-version`
/// - `lint.future-annotations`
///
/// ## Preview
///
/// When [preview] is enabled, if [`lint.future-annotations`] is set to `true`,
/// `from __future__ import annotations` will be added if doing so would allow using the `|`
/// operator on a Python version before 3.10.
///
/// ## Fix safety
///
/// This fix is always marked as unsafe because it can change the behavior of code that relies on
@@ -215,7 +217,7 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
};
let conversion_type = if checker.target_version() >= PythonVersion::PY310
|| checker.settings().future_annotations
|| checker.settings().future_annotations()
{
ConversionType::BinOpOr
} else {

View File

@@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, CmpOp, Expr, helpers::is_empty_f_string};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -75,7 +75,10 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
Expr::Dict(ast::ExprDict { items, .. }) => items.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
Expr::FString(s) => is_empty_f_string(s),
Expr::FString(s) => s
.value
.elements()
.all(|elt| elt.as_literal().is_some_and(|elt| elt.is_empty())),
Expr::Call(ast::ExprCall {
func,
arguments,

View File

@@ -27,8 +27,7 @@ use crate::rules::flake8_pytest_style::rules::is_pytest_raises;
/// do_thing_that_raises()
/// ```
///
/// If the pattern is intended to be a regular expression, use a raw string to signal this
/// intention:
/// Use instead:
///
/// ```python
/// import pytest
@@ -38,7 +37,7 @@ use crate::rules::flake8_pytest_style::rules::is_pytest_raises;
/// do_thing_that_raises()
/// ```
///
/// Alternatively, escape any regex metacharacters with `re.escape`:
/// Alternatively:
///
/// ```python
/// import pytest
@@ -49,7 +48,7 @@ use crate::rules::flake8_pytest_style::rules::is_pytest_raises;
/// do_thing_that_raises()
/// ```
///
/// or directly with backslashes:
/// or:
///
/// ```python
/// import pytest

View File

@@ -36,15 +36,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// return x
/// ```
///
/// ## See also
///
/// This rule applies only to unpacked assignments. For regular assignments, see
/// [`unused-variable`][F841].
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [F841]: https://docs.astral.sh/ruff/rules/unused-variable/
#[derive(ViolationMetadata)]
pub(crate) struct UnusedUnpackedVariable {
pub name: String,

View File

@@ -251,12 +251,3 @@ RUF060 Unnecessary membership test on empty collection
25 |
26 | # OK
|
RUF060 Unnecessary membership test on empty collection
--> RUF060.py:47:1
|
46 | # https://github.com/astral-sh/ruff/issues/20238
47 | "b" in f"" "" # Error
| ^^^^^^^^^^^^^
48 | "b" in f"" "x" # OK
|

View File

@@ -475,6 +475,11 @@ impl LinterSettings {
.is_match(path)
.map_or(self.unresolved_target_version, TargetVersion::from)
}
pub fn future_annotations(&self) -> bool {
// TODO(brent) we can just access the field directly once this is stabilized.
self.future_annotations && crate::preview::is_add_future_annotations_imports_enabled(self)
}
}
impl Default for LinterSettings {

View File

@@ -3219,6 +3219,7 @@ impl<'a> IntoIterator for &'a Box<Parameters> {
/// Used by `Arguments` original type.
///
/// NOTE: This type is different from original Python AST.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct ParameterWithDefault {
@@ -3240,14 +3241,6 @@ impl ParameterWithDefault {
pub fn annotation(&self) -> Option<&Expr> {
self.parameter.annotation()
}
/// Return `true` if the parameter name uses the pre-PEP-570 convention
/// (specified in PEP 484) to indicate to a type checker that it should be treated
/// as positional-only.
pub fn uses_pep_484_positional_only_convention(&self) -> bool {
let name = self.name();
name.starts_with("__") && !name.ends_with("__")
}
}
/// An AST node used to represent the arguments passed to a function call or class definition.

View File

@@ -252,20 +252,15 @@ impl QuoteStyle {
pub const fn is_preserve(self) -> bool {
matches!(self, QuoteStyle::Preserve)
}
/// Returns the string representation of the quote style.
pub const fn as_str(&self) -> &'static str {
match self {
QuoteStyle::Single => "single",
QuoteStyle::Double => "double",
QuoteStyle::Preserve => "preserve",
}
}
}
impl fmt::Display for QuoteStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
match self {
Self::Single => write!(f, "single"),
Self::Double => write!(f, "double"),
Self::Preserve => write!(f, "preserve"),
}
}
}
@@ -307,10 +302,10 @@ impl MagicTrailingComma {
impl fmt::Display for MagicTrailingComma {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
MagicTrailingComma::Respect => "respect",
MagicTrailingComma::Ignore => "ignore",
})
match self {
Self::Respect => write!(f, "respect"),
Self::Ignore => write!(f, "ignore"),
}
}
}

View File

@@ -50,8 +50,5 @@ insta = { workspace = true }
[target.'cfg(target_vendor = "apple")'.dependencies]
libc = { workspace = true }
[features]
test-uv = []
[lints]
workspace = true

View File

@@ -1,52 +1,18 @@
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use anyhow::Context;
use ruff_formatter::{FormatOptions, PrintedRange};
use ruff_formatter::PrintedRange;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{FormatModuleError, PyFormatOptions, format_module_source};
use ruff_source_file::LineIndex;
use ruff_python_formatter::{FormatModuleError, format_module_source};
use ruff_text_size::TextRange;
use ruff_workspace::FormatterSettings;
use crate::edit::TextDocument;
/// The backend to use for formatting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum FormatBackend {
/// Use the built-in Ruff formatter.
///
/// The formatter version will match the LSP version.
#[default]
Internal,
/// Use uv for formatting.
///
/// The formatter version may differ from the LSP version.
Uv,
}
pub(crate) fn format(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: &Path,
backend: FormatBackend,
) -> crate::Result<Option<String>> {
match backend {
FormatBackend::Uv => format_external(document, source_type, formatter_settings, path),
FormatBackend::Internal => format_internal(document, source_type, formatter_settings, path),
}
}
/// Format using the built-in Ruff formatter.
fn format_internal(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: &Path,
) -> crate::Result<Option<String>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
@@ -69,44 +35,12 @@ fn format_internal(
}
}
/// Format using an external uv command.
fn format_external(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: &Path,
) -> crate::Result<Option<String>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
let uv_command = UvFormatCommand::from(format_options);
uv_command.format_document(document.contents(), path)
}
pub(crate) fn format_range(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: &Path,
backend: FormatBackend,
) -> crate::Result<Option<PrintedRange>> {
match backend {
FormatBackend::Uv => {
format_range_external(document, source_type, formatter_settings, range, path)
}
FormatBackend::Internal => {
format_range_internal(document, source_type, formatter_settings, range, path)
}
}
}
/// Format range using the built-in Ruff formatter
fn format_range_internal(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: &Path,
) -> crate::Result<Option<PrintedRange>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
@@ -129,198 +63,6 @@ fn format_range_internal(
}
}
/// Format range using an external command, i.e., `uv`.
fn format_range_external(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: &Path,
) -> crate::Result<Option<PrintedRange>> {
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
let uv_command = UvFormatCommand::from(format_options);
// Format the range using uv and convert the result to `PrintedRange`
match uv_command.format_range(document.contents(), range, path, document.index())? {
Some(formatted) => Ok(Some(PrintedRange::new(formatted, range))),
None => Ok(None),
}
}
/// Builder for uv format commands
#[derive(Debug)]
pub(crate) struct UvFormatCommand {
options: PyFormatOptions,
}
impl From<PyFormatOptions> for UvFormatCommand {
fn from(options: PyFormatOptions) -> Self {
Self { options }
}
}
impl UvFormatCommand {
/// Build the command with all necessary arguments
fn build_command(
&self,
path: &Path,
range_with_index: Option<(TextRange, &LineIndex, &str)>,
) -> Command {
let mut command = Command::new("uv");
command.arg("format");
command.arg("--");
let target_version = format!(
"py{}{}",
self.options.target_version().major,
self.options.target_version().minor
);
// Add only the formatting options that the CLI supports
command.arg("--target-version");
command.arg(&target_version);
command.arg("--line-length");
command.arg(self.options.line_width().to_string());
if self.options.preview().is_enabled() {
command.arg("--preview");
}
// Pass other formatting options via --config
command.arg("--config");
command.arg(format!(
"format.indent-style = '{}'",
self.options.indent_style()
));
command.arg("--config");
command.arg(format!("indent-width = {}", self.options.indent_width()));
command.arg("--config");
command.arg(format!(
"format.quote-style = '{}'",
self.options.quote_style()
));
command.arg("--config");
command.arg(format!(
"format.line-ending = '{}'",
self.options.line_ending().as_setting_str()
));
command.arg("--config");
command.arg(format!(
"format.skip-magic-trailing-comma = {}",
match self.options.magic_trailing_comma() {
ruff_python_formatter::MagicTrailingComma::Respect => "false",
ruff_python_formatter::MagicTrailingComma::Ignore => "true",
}
));
if let Some((range, line_index, source)) = range_with_index {
// The CLI expects line:column format
let start_pos = line_index.line_column(range.start(), source);
let end_pos = line_index.line_column(range.end(), source);
let range_str = format!(
"{}:{}-{}:{}",
start_pos.line.get(),
start_pos.column.get(),
end_pos.line.get(),
end_pos.column.get()
);
command.arg("--range");
command.arg(&range_str);
}
command.arg("--stdin-filename");
command.arg(path.to_string_lossy().as_ref());
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
command
}
/// Execute the format command on the given source.
pub(crate) fn format(
&self,
source: &str,
path: &Path,
range_with_index: Option<(TextRange, &LineIndex)>,
) -> crate::Result<Option<String>> {
let mut command =
self.build_command(path, range_with_index.map(|(r, idx)| (r, idx, source)));
let mut child = match command.spawn() {
Ok(child) => child,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
anyhow::bail!("uv was not found; is it installed and on the PATH?")
}
Err(err) => return Err(err).context("Failed to spawn uv"),
};
let mut stdin = child
.stdin
.take()
.context("Failed to get stdin from format subprocess")?;
stdin
.write_all(source.as_bytes())
.context("Failed to write to stdin")?;
drop(stdin);
let result = child
.wait_with_output()
.context("Failed to get output from format subprocess")?;
if !result.status.success() {
let stderr = String::from_utf8_lossy(&result.stderr);
// We don't propagate format errors due to invalid syntax
if stderr.contains("Failed to parse") {
tracing::warn!("Unable to format document: {}", stderr);
return Ok(None);
}
// Special-case for when `uv format` is not available
if stderr.contains("unrecognized subcommand 'format'") {
anyhow::bail!(
"The installed version of uv does not support `uv format`; upgrade to a newer version"
);
}
anyhow::bail!("Failed to format document: {}", stderr);
}
let formatted = String::from_utf8(result.stdout)
.context("Failed to parse stdout from format subprocess as utf-8")?;
if formatted == source {
Ok(None)
} else {
Ok(Some(formatted))
}
}
/// Format the entire document.
pub(crate) fn format_document(
&self,
source: &str,
path: &Path,
) -> crate::Result<Option<String>> {
self.format(source, path, None)
}
/// Format a specific range.
pub(crate) fn format_range(
&self,
source: &str,
range: TextRange,
path: &Path,
line_index: &LineIndex,
) -> crate::Result<Option<String>> {
self.format(source, path, Some((range, line_index)))
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
@@ -332,7 +74,7 @@ mod tests {
use ruff_workspace::FormatterSettings;
use crate::TextDocument;
use crate::format::{FormatBackend, format, format_range};
use crate::format::{format, format_range};
#[test]
fn format_per_file_version() {
@@ -356,7 +98,6 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
..Default::default()
},
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -379,7 +120,6 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
..Default::default()
},
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -428,7 +168,6 @@ sys.exit(
},
range,
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -452,7 +191,6 @@ sys.exit(
},
range,
Path::new("test.py"),
FormatBackend::Internal,
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
@@ -466,279 +204,4 @@ sys.exit(
Ok(())
}
#[cfg(feature = "test-uv")]
mod uv_tests {
use super::*;
#[test]
fn test_uv_format_document() {
let document = TextDocument::new(
r#"
def hello( x,y ,z ):
return x+y +z
def world( ):
pass
"#
.to_string(),
0,
);
let result = format(
&document,
PySourceType::Python,
&FormatterSettings::default(),
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
// uv should format this to a consistent style
assert_snapshot!(result, @r#"
def hello(x, y, z):
return x + y + z
def world():
pass
"#);
}
#[test]
fn test_uv_format_range() -> anyhow::Result<()> {
let document = TextDocument::new(
r#"
def messy_function( a, b,c ):
return a+b+c
def another_function(x,y,z):
result=x+y+z
return result
"#
.to_string(),
0,
);
// Find the range of the second function
let start = document.contents().find("def another_function").unwrap();
let end = document.contents().find("return result").unwrap() + "return result".len();
let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);
let result = format_range(
&document,
PySourceType::Python,
&FormatterSettings::default(),
range,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting range with uv")
.expect("Expected formatting changes");
assert_snapshot!(result.as_code(), @r#"
def messy_function( a, b,c ):
return a+b+c
def another_function(x, y, z):
result = x + y + z
return result
"#);
Ok(())
}
#[test]
fn test_uv_format_with_line_length() {
use ruff_formatter::LineWidth;
let document = TextDocument::new(
r#"
def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_parameter_name_3):
return very_long_parameter_name_1 + very_long_parameter_name_2 + very_long_parameter_name_3
"#
.to_string(),
0,
);
// Test with shorter line length
let formatter_settings = FormatterSettings {
line_width: LineWidth::try_from(60).unwrap(),
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
// With line length 60, the function should be wrapped
assert_snapshot!(result, @r#"
def hello(
very_long_parameter_name_1,
very_long_parameter_name_2,
very_long_parameter_name_3,
):
return (
very_long_parameter_name_1
+ very_long_parameter_name_2
+ very_long_parameter_name_3
)
"#);
}
#[test]
fn test_uv_format_with_indent_style() {
use ruff_formatter::IndentStyle;
let document = TextDocument::new(
r#"
def hello():
if True:
print("Hello")
if False:
print("World")
"#
.to_string(),
0,
);
// Test with tabs instead of spaces
let formatter_settings = FormatterSettings {
indent_style: IndentStyle::Tab,
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
// Should have formatting changes (spaces to tabs)
assert_snapshot!(result, @r#"
def hello():
if True:
print("Hello")
if False:
print("World")
"#);
}
#[test]
fn test_uv_format_syntax_error() {
let document = TextDocument::new(
r#"
def broken(:
pass
"#
.to_string(),
0,
);
// uv should return None for syntax errors (as indicated by the TODO comment)
let result = format(
&document,
PySourceType::Python,
&FormatterSettings::default(),
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors from format function");
// Should return None since the syntax is invalid
assert_eq!(result, None, "Expected None for syntax error");
}
#[test]
fn test_uv_format_with_quote_style() {
use ruff_python_formatter::QuoteStyle;
let document = TextDocument::new(
r#"
x = "hello"
y = 'world'
z = '''multi
line'''
"#
.to_string(),
0,
);
// Test with single quotes
let formatter_settings = FormatterSettings {
quote_style: QuoteStyle::Single,
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
assert_snapshot!(result, @r#"
x = 'hello'
y = 'world'
z = """multi
line"""
"#);
}
#[test]
fn test_uv_format_with_magic_trailing_comma() {
use ruff_python_formatter::MagicTrailingComma;
let document = TextDocument::new(
r#"
foo = [
1,
2,
3,
]
bar = [1, 2, 3,]
"#
.to_string(),
0,
);
// Test with ignore magic trailing comma
let formatter_settings = FormatterSettings {
magic_trailing_comma: MagicTrailingComma::Ignore,
..Default::default()
};
let result = format(
&document,
PySourceType::Python,
&formatter_settings,
Path::new("test.py"),
FormatBackend::Uv,
)
.expect("Expected no errors when formatting with uv")
.expect("Expected formatting changes");
assert_snapshot!(result, @r#"
foo = [1, 2, 3]
bar = [1, 2, 3]
"#);
}
}
}

View File

@@ -33,10 +33,6 @@ impl super::BackgroundDocumentRequestHandler for Format {
pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes> {
let mut fixes = Fixes::default();
let query = snapshot.query();
let backend = snapshot
.client_settings()
.editor_settings()
.format_backend();
match snapshot.query() {
DocumentQuery::Notebook { notebook, .. } => {
@@ -45,7 +41,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
.map(|url| (url.clone(), notebook.cell_document_by_uri(url).unwrap()))
{
if let Some(changes) =
format_text_document(text_document, query, snapshot.encoding(), true, backend)?
format_text_document(text_document, query, snapshot.encoding(), true)?
{
fixes.insert(url, changes);
}
@@ -53,7 +49,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
}
DocumentQuery::Text { document, .. } => {
if let Some(changes) =
format_text_document(document, query, snapshot.encoding(), false, backend)?
format_text_document(document, query, snapshot.encoding(), false)?
{
fixes.insert(snapshot.query().make_key().into_url(), changes);
}
@@ -72,16 +68,11 @@ pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::Form
.context("Failed to get text document for the format request")
.unwrap();
let query = snapshot.query();
let backend = snapshot
.client_settings()
.editor_settings()
.format_backend();
format_text_document(
text_document,
query,
snapshot.encoding(),
query.as_notebook().is_some(),
backend,
)
}
@@ -90,7 +81,6 @@ fn format_text_document(
query: &DocumentQuery,
encoding: PositionEncoding,
is_notebook: bool,
backend: crate::format::FormatBackend,
) -> Result<super::FormatResponse> {
let settings = query.settings();
let file_path = query.virtual_file_path();
@@ -111,7 +101,6 @@ fn format_text_document(
query.source_type(),
&settings.formatter,
&file_path,
backend,
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
let Some(mut formatted) = formatted else {

Some files were not shown because too many files have changed in this diff Show More