Compare commits

..

33 Commits

Author SHA1 Message Date
Micha Reiser
242a1bcccb Union builder perf improvements 2024-11-20 21:46:35 +01:00
Zanie Blue
3c52d2d1bd Improve the performance of the formatter instability check job (#14471)
We should probably get rid of this entirely and subsume it's
functionality in the normal ecosystem checks? I don't think we're using
the black comparison tests anymore, but maybe someone wants it?

There are a few major parts to this:

1. Making the formatter script idempotent, so it can be run repeatedly
and is robust to changing commits
2. Reducing the overhead of the git operations, minimizing the data
transfer
3. Parallelizing all the git operations by repository

This reduces the setup time from 80s to 16s (locally).

The initial motivation for idempotency was to include the repositories
in the GitHub Actions cache. I'm not sure it's worth it yet — they're
about 1GB and would consume our limited cache space. Regardless, it
improves correctness for local invocations.

The total runtime of the job is reduced from ~4m to ~3m.

I also made some cosmetic changes to the output paths and such.
2024-11-20 08:55:10 -06:00
Micha Reiser
942d6eeb9f Stabilize A004 (#14480) 2024-11-20 13:11:51 +01:00
Alex Waygood
4ccacc80f9 [ruff-0.8] [FAST] Further improve docs for fast-api-non-annotated-depencency (FAST002) (#14467) 2024-11-20 13:11:51 +01:00
Micha Reiser
b2bb119c6a Fix failing tests for Ruff 0.8 branch (#14482) 2024-11-20 13:11:51 +01:00
Alex Waygood
cef12f4925 [ruff-0.8] Spruce up docs for newly stabilised rules (#14466)
## Summary

- Expand some docs where they're unclear about the motivation, or assume
some knowledge that hasn't been introduced yet
- Add more links to external docs
- Rename PYI063 from `PrePep570PositionalArgument` to
`Pep484StylePositionalOnlyParameter`
- Rename the file `parenthesize_logical_operators.rs` to
`parenthesize_chained_operators.rs`, since the rule is called
`ParenthesizeChainedOperators`, not `ParenthesizeLogicalOperators`

## Test Plan

`cargo test`
2024-11-20 13:11:51 +01:00
Alex Waygood
aa7ac2ce0f [ruff-0.8] [ruff] Stabilise unsorted-dunder-all and unsorted-dunder-slots (#14468)
## Summary

These rules were implemented in January, have been very stable, and have
no open issues about them. They were highly requested by the community
prior to being implemented. Let's stabilise them!

## Test Plan

Ecosystem check on this PR.
2024-11-20 13:11:51 +01:00
Zanie Blue
70d9c90827 Use XDG (i.e. ~/.local/bin) instead of the Cargo home directory in the installer (#14457)
Closes https://github.com/astral-sh/ruff/issues/13927
2024-11-20 13:11:51 +01:00
Micha Reiser
adfa723464 Stabilize multiple rules (#14462) 2024-11-20 13:11:51 +01:00
Zanie Blue
844c07f1f0 Upgrade cargo-dist from 0.22.1 => 0.25.2-prerelease.3 (#14456)
Needed to prevent updater failures when doing
https://github.com/astral-sh/ruff/issues/13927

See 

- https://github.com/axodotdev/axoupdater/issues/210
- https://github.com/axodotdev/cargo-dist/pull/1538
- https://github.com/astral-sh/uv/pull/8958
2024-11-20 13:11:51 +01:00
Alex Waygood
11d20a1a51 [ruff-0.8] [ruff] Stabilise parenthesize-chained-operators (RUF021) (#14450) 2024-11-20 13:11:51 +01:00
Micha Reiser
e9079e7d95 Remove the deprecated E999 rule code (#14428) 2024-11-20 13:11:51 +01:00
Alex Waygood
c400725713 [ruff 0.8] [flake8-pytest-style] Remove deprecated rules PT004 and PT005 (#14385)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-20 13:11:51 +01:00
Alex Waygood
1081694140 [ruff 0.8] [flake8-annotations] Remove deprecated rules ANN101 and ANN102 (#14384)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-20 13:11:51 +01:00
Micha Reiser
52f526eb38 Warn instead of error when removed rules are used in ignore (#14435)
Closes https://github.com/astral-sh/ruff/issues/13505
2024-11-20 13:11:51 +01:00
David Salvisberg
dc05b38165 [ruff 0.8][flake8-type-checking] Rename TCH to TC (#14438)
Closes #9573
2024-11-20 13:11:51 +01:00
renovate[bot]
8c3c5ee5e3 Update Rust crate unicode-width to 0.2.0 (#13473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 13:11:51 +01:00
konsti
b46cc6ac0b Update pyproject-toml to support PEP 639 (#13902)
Fixes #13869
2024-11-20 13:11:51 +01:00
Dylan
8b925ea626 [pycodestyle] Stabilize behavior to ignore stub files in ambiguous-variable-name (E741) (#14405) 2024-11-20 13:11:51 +01:00
yataka
1b180c8342 Change default for Python version from 3.8 to 3.9 (#13896)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-20 13:11:51 +01:00
Dylan
afeb217452 [pyupgrade] Stabilize behavior to show diagnostic even when unfixable in printf-string-formatting (UP031) (#14406) 2024-11-20 13:11:51 +01:00
Dylan
c0b3dd3745 [ruff] Stabilize unsafe fix for zip-instead-of-pairwise (RUF007) (#14401)
This PR stabilizes the unsafe fix for [zip-instead-of-pairwise
(RUF007)](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/#zip-instead-of-pairwise-ruf007),
which replaces the use of zip with that of itertools.pairwise and has
been available under preview since version 0.5.7.

There are no open issues regarding RUF007 at the time of this writing.
2024-11-20 13:11:51 +01:00
Alex Waygood
5f6607bf54 [ruff 0.8] Remove deprecated rule UP027 (#14382) 2024-11-20 13:11:51 +01:00
Zanie Blue
a6deca44b5 Rename the parser fuzz job for consistency with the rest of CI (#14479) 2024-11-20 07:54:42 +00:00
Zanie Blue
0dbceccbc1 Only build the fuzz crate on main (#14478)
It's not actually doing anything per pull request and it's pretty slow?
xref #14469

It seems useful to build on `main` still to find build regressions? e.g.
https://github.com/astral-sh/ruff/issues/9368
2024-11-19 23:07:48 -06:00
Dhruv Manilawala
48680e10b6 Watch for changes to the generated file during documentation serve (#14476)
## Summary

Similar to https://github.com/astral-sh/uv/pull/9244, but we need to use
the `mkdocs.generated.yml` file because the `scripts/generate_mkdocs.py`
uses the `mkdocs.template.yml` to generate the final config.
2024-11-20 04:51:20 +00:00
Zanie Blue
b0c88a2a42 Only test release builds on main (#14475)
This is one of the slowest remaining jobs in the pull request CI. We
could use a larger runner for a trivial speed-up (in exchange for $$),
but I don't think this is going to break often enough to merit testing
on every pull request commit? It's not a required job, so I don't feel
strongly about it, but it feels like a bit of a waste of compute.

Originally added in https://github.com/astral-sh/ruff/pull/11182
2024-11-19 22:47:46 -06:00
InSync
b9c53a74f9 [pycodestyle] Exempt pytest.importorskip() calls (E402) (#14474)
## Summary

Resolves #13537.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-11-19 22:08:15 -05:00
cake-monotone
6a4d207db7 [red-knot] Refactoring the inference logic of lexicographic comparisons (#14422)
## Summary

closes #14279

### Limitations of the Current Implementation
#### Incorrect Error Propagation

In the current implementation of lexicographic comparisons, if the
result of an Eq operation is Ambiguous, the comparison stops
immediately, returning a bool instance. While this may yield correct
inferences, it fails to capture unsupported-operation errors that might
occur in subsequent comparisons.
```py
class A: ...

(int_instance(), A()) < (int_instance(), A())  # should error
```

#### Weak Inference in Specific Cases

> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
> Current result: `bool`
> Expected result: `Literal[False]`

`Eq` and `NotEq` have unique behavior in lexicographic comparisons
compared to other operators. Specifically:
- For `Eq`, if any non-equal pair exists within the tuples being
compared, we can immediately conclude that the tuples are not equal.
- For `NotEq`, if any equal pair exists, we can conclude that the tuples
are unequal.

```py
a = (str_instance(), int_instance(), "foo")

reveal_type(a == a)  # revealed: bool
reveal_type(a != a)  # revealed: bool

b = (str_instance(), int_instance(), "bar")

reveal_type(a == b)  # revealed: bool  # should be Literal[False]
reveal_type(a != b)  # revealed: bool  # should be Literal[True]
```
#### Incorrect Support for Non-Boolean Rich Comparisons

In CPython, aside from `==` and `!=`, tuple comparisons return a
non-boolean result as-is. Tuples do not convert the value into `bool`.

Note: If all pairwise `==` comparisons between elements in the tuples
return Truthy, the comparison then considers the tuples' lengths.
Regardless of the return type of the dunder methods, the final result
can still be a boolean.

```py
from __future__ import annotations

class A:
    def __eq__(self, o: object) -> str:
        return "hello"

    def __ne__(self, o: object) -> bytes:
        return b"world"

    def __lt__(self, o: A) -> float:
        return 3.14

a = (A(), A())

reveal_type(a == a)  # revealed: bool
reveal_type(a != a)  # revealed: bool
reveal_type(a < a)  # revealed: bool # should be: `float | Literal[False]`

```

### Key Changes
One of the major changes is that comparisons no longer end with a `bool`
result when a pairwise `Eq` result is `Ambiguous`. Instead, the function
attempts to infer all possible cases and unions the results. This
improvement allows for more robust type inference and better error
detection.

Additionally, as the function is now optimized for tuple comparisons,
the name has been changed from the more general
`infer_lexicographic_comparison` to `infer_tuple_rich_comparison`.

## Test Plan

mdtest included
2024-11-19 17:32:43 -08:00
Zanie Blue
42c35b6f44 Use larger GitHub runner for testing on Windows (#14461)
Reduces to 3m 50s (extra large) or 6m 5s (large) vs 9m 7s (standard)
2024-11-19 18:00:59 -06:00
Zanie Blue
9e79d64d62 Use Depot 16-core runner for testing on Linux (#14460)
Reduces Linux test CI to 1m 40s (16 core) or 2m 56s (8 core) to from 4m
25s. Times are approximate, as runner performance is pretty variable.

In uv, we use the 16 core runners.
2024-11-19 18:00:51 -06:00
Zanie Blue
582857f292 Use Depot 8-core runner for ecosystem tests (#14463)
I noticed this was exceedingly slow.

Reduces to 3m from 14m
2024-11-19 18:00:38 -06:00
Zanie Blue
9bbeb793e5 Specify the wasm-pack version explicitly (#14465)
There is an upstream bug with latest version detection
https://github.com/jetli/wasm-pack-action/issues/23

This causes random flakes of the wasm build job.
2024-11-19 18:00:27 -06:00
303 changed files with 2062 additions and 8748 deletions

View File

@@ -115,7 +115,7 @@ jobs:
cargo-test-linux:
name: "cargo test (linux)"
runs-on: ubuntu-latest
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
@@ -159,7 +159,7 @@ jobs:
cargo-test-windows:
name: "cargo test (windows)"
runs-on: windows-latest
runs-on: windows-latest-xlarge
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
@@ -197,6 +197,8 @@ jobs:
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: Swatinem/rust-cache@v2
- name: "Test ruff_wasm"
run: |
@@ -211,7 +213,7 @@ jobs:
name: "cargo build (release)"
runs-on: macos-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -255,11 +257,11 @@ jobs:
NEXTEST_PROFILE: "ci"
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
cargo-fuzz:
name: "cargo fuzz"
cargo-fuzz-build:
name: "cargo fuzz build"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
@@ -268,7 +270,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: "fuzz -> target"
cache-all-crates: "true"
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@main
with:
@@ -279,7 +280,7 @@ jobs:
- run: cargo fuzz build -s none
fuzz-parser:
name: "Fuzz the parser"
name: "fuzz parser"
runs-on: ubuntu-latest
needs:
- cargo-test-linux
@@ -332,7 +333,7 @@ jobs:
ecosystem:
name: "ecosystem"
runs-on: ubuntu-latest
runs-on: depot-ubuntu-latest-8
needs:
- cargo-test-linux
- determine_changes
@@ -562,12 +563,12 @@ jobs:
run: rustup show
- name: "Cache rust"
uses: Swatinem/rust-cache@v2
- name: "Formatter progress"
- name: "Run checks"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects
run: rm -r target/formatter-ecosystem
check-ruff-lsp:
name: "test ruff-lsp"

View File

@@ -1,4 +1,4 @@
# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
@@ -6,7 +6,7 @@
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with cargo-dist (archives, installers, hashes)
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
@@ -24,10 +24,10 @@ permissions:
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (cargo-dist-able) packages in the workspace with that version (this mode is
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
@@ -48,7 +48,7 @@ on:
type: string
jobs:
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-20.04"
outputs:
@@ -62,16 +62,16 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh"
- name: Cache cargo-dist
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/cargo-dist
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
@@ -79,8 +79,8 @@ jobs:
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
cargo dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "cargo dist ran successfully"
dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
@@ -124,12 +124,12 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
@@ -140,8 +140,8 @@ jobs:
- id: cargo-dist
shell: bash
run: |
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "cargo dist ran successfully"
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
@@ -174,12 +174,12 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
@@ -191,7 +191,7 @@ jobs:
- id: host
shell: bash
run: |
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"

67
Cargo.lock generated
View File

@@ -212,6 +212,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "boxcar"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
[[package]]
name = "bstr"
version = "1.11.0"
@@ -1935,18 +1941,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
[[package]]
name = "pep440_rs"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c29f9c43de378b4e4e0cd7dbcce0e5cfb80443de8c05620368b2948bc936a1"
dependencies = [
"once_cell",
"regex",
"serde",
"unicode-width 0.1.13",
]
[[package]]
name = "pep440_rs"
version = "0.7.2"
@@ -1956,22 +1950,29 @@ dependencies = [
"serde",
"unicode-width 0.2.0",
"unscanny",
"version-ranges",
]
[[package]]
name = "pep508_rs"
version = "0.3.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "910c513bea0f4f833122321c0f20e8c704e01de98692f6989c2ec21f43d88b1e"
checksum = "8c2feee999fa547bacab06a4881bacc74688858b92fa8ef1e206c748b0a76048"
dependencies = [
"boxcar",
"indexmap",
"itertools 0.13.0",
"once_cell",
"pep440_rs 0.4.0",
"pep440_rs",
"regex",
"rustc-hash 2.0.0",
"serde",
"smallvec",
"thiserror 1.0.67",
"tracing",
"unicode-width 0.1.13",
"unicode-width 0.2.0",
"url",
"urlencoding",
"version-ranges",
]
[[package]]
@@ -2135,14 +2136,15 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.9.0"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95c3dd745f99aa3c554b7bb00859f7d18c2f1d6afd749ccc86d60b61e702abd9"
checksum = "643af57c3f36ba90a8b53e972727d8092f7408a9ebfbaf4c3d2c17b07c58d835"
dependencies = [
"indexmap",
"pep440_rs 0.4.0",
"pep440_rs",
"pep508_rs",
"serde",
"thiserror 1.0.67",
"toml",
]
@@ -2370,7 +2372,7 @@ dependencies = [
"glob",
"insta",
"notify",
"pep440_rs 0.7.2",
"pep440_rs",
"rayon",
"red_knot_python_semantic",
"red_knot_vendored",
@@ -2674,7 +2676,7 @@ dependencies = [
"serde",
"static_assertions",
"tracing",
"unicode-width 0.1.13",
"unicode-width 0.2.0",
]
[[package]]
@@ -2729,7 +2731,7 @@ dependencies = [
"natord",
"path-absolutize",
"pathdiff",
"pep440_rs 0.7.2",
"pep440_rs",
"pyproject-toml",
"quick-junit",
"regex",
@@ -2760,7 +2762,7 @@ dependencies = [
"toml",
"typed-arena",
"unicode-normalization",
"unicode-width 0.1.13",
"unicode-width 0.2.0",
"unicode_names2",
"url",
]
@@ -3060,7 +3062,7 @@ dependencies = [
"matchit",
"path-absolutize",
"path-slash",
"pep440_rs 0.7.2",
"pep440_rs",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -3960,6 +3962,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf16_iter"
version = "1.0.5"
@@ -4007,6 +4015,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version-ranges"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8d079415ceb2be83fc355adbadafe401307d5c309c7e6ade6638e6f9f42f42d"
dependencies = [
"smallvec",
]
[[package]]
name = "version_check"
version = "0.9.4"

View File

@@ -111,7 +111,7 @@ pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.7.1" }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.79" }
pyproject-toml = { version = "0.9.0" }
pyproject-toml = { version = "0.13.4" }
quick-junit = { version = "0.5.0" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
@@ -151,7 +151,7 @@ tracing-tree = { version = "0.4.0" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.1.11" }
unicode-width = { version = "0.2.0" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
ureq = { version = "2.9.6" }
@@ -248,10 +248,10 @@ debug = 1
[profile.dist]
inherits = "release"
# Config for 'cargo dist'
# Config for 'dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.22.1"
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.25.2-prerelease.3"
# CI backends to support
ci = "github"
# The installers to generate for each app
@@ -282,13 +282,13 @@ targets = [
]
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
auto-includes = false
# Whether cargo-dist should create a GitHub Release or use an existing draft
# Whether dist should create a Github Release or use an existing draft
create-release = true
# Which actions to run on pull requests
pr-run-mode = "skip"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Which phase cargo-dist should use to create the GitHub release
# Which phase dist should use to create the GitHub release
github-release = "announce"
# Whether CI should include auto-generated code to build local artifacts
build-local-artifacts = false
@@ -297,14 +297,10 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi", "./publish-wasm"]
# Post-announce jobs to run in CI
post-announce-jobs = [
"./notify-dependents",
"./publish-docs",
"./publish-playground",
]
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
# Custom permissions for GitHub Jobs
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
# Whether to install an updater program
install-updater = false
# Path that installers should place binaries in
install-path = "CARGO_HOME"
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]

View File

@@ -238,8 +238,8 @@ exclude = [
line-length = 88
indent-width = 4
# Assume Python 3.8
target-version = "py38"
# Assume Python 3.9
target-version = "py39"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.

View File

@@ -58,7 +58,9 @@ reveal_type(c >= d) # revealed: Literal[True]
#### Results with Ambiguity
```py
def bool_instance() -> bool: ...
def bool_instance() -> bool:
return True
def int_instance() -> int:
return 42
@@ -134,23 +136,158 @@ reveal_type(c >= c) # revealed: Literal[True]
#### Non Boolean Rich Comparisons
Rich comparison methods defined in a class affect tuple comparisons as well. Proper type inference
should be possible even in cases where these methods return non-boolean types.
Note: Tuples use lexicographic comparisons. If the `==` result for all paired elements in the tuple
is True, the comparison then considers the tuples length. Regardless of the return type of the
dunder methods, the final result can still be a boolean value.
(+cpython: For tuples, `==` and `!=` always produce boolean results, regardless of the return type
of the dunder methods.)
```py
from __future__ import annotations
class A:
def __eq__(self, o) -> str: ...
def __ne__(self, o) -> int: ...
def __lt__(self, o) -> float: ...
def __le__(self, o) -> object: ...
def __gt__(self, o) -> tuple: ...
def __ge__(self, o) -> list: ...
def __eq__(self, o: object) -> str:
return "hello"
def __ne__(self, o: object) -> bytes:
return b"world"
def __lt__(self, o: A) -> float:
return 3.14
def __le__(self, o: A) -> complex:
return complex(0.5, -0.5)
def __gt__(self, o: A) -> tuple:
return (1, 2, 3)
def __ge__(self, o: A) -> list:
return [1, 2, 3]
a = (A(), A())
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: float | Literal[False]
reveal_type(a <= a) # revealed: complex | Literal[True]
reveal_type(a > a) # revealed: tuple | Literal[False]
reveal_type(a >= a) # revealed: list | Literal[True]
# If lexicographic comparison is finished before comparing A()
b = ("1_foo", A())
c = ("2_bar", A())
reveal_type(b == c) # revealed: Literal[False]
reveal_type(b != c) # revealed: Literal[True]
reveal_type(b < c) # revealed: Literal[True]
reveal_type(b <= c) # revealed: Literal[True]
reveal_type(b > c) # revealed: Literal[False]
reveal_type(b >= c) # revealed: Literal[False]
class B:
def __lt__(self, o: B) -> set:
return set()
reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
```
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
immediately conclude that the tuples are not equal. Conversely, for `NotEq`, if any non-equal pair
exists, we can determine that the tuples are unequal.
In contrast, with operators like `<` and `>`, the comparison must consider each pair of elements
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
```py
def str_instance() -> str:
return "hello"
def int_instance() -> int:
return 42
reveal_type("foo" == "bar") # revealed: Literal[False]
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False]
a = (str_instance(), int_instance(), "foo")
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
b = (str_instance(), int_instance(), "bar")
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
c = (str_instance(), int_instance(), "foo", "different_length")
reveal_type(a == c) # revealed: Literal[False]
reveal_type(a != c) # revealed: Literal[True]
reveal_type(a < c) # revealed: bool
reveal_type(a <= c) # revealed: bool
reveal_type(a > c) # revealed: bool
reveal_type(a >= c) # revealed: bool
```
#### Error Propagation
Errors occurring within a tuple comparison should propagate outward. However, if the tuple
comparison can clearly conclude before encountering an error, the error should not be raised.
```py
def int_instance() -> int:
return 42
def str_instance() -> str:
return "hello"
class A: ...
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
A() < A()
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
A() <= A()
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
A() > A()
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
A() >= A()
a = (0, int_instance(), A())
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a < a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a <= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a > a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a >= a) # revealed: Unknown
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
# and should terminate immediately.
b = (99999, int_instance(), A())
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
```
### Membership Test Comparisons

View File

@@ -4,6 +4,8 @@
def bool_instance() -> bool:
return True
class A: ...
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
@@ -33,4 +35,8 @@ reveal_type(e) # revealed: bool
f = (1, 2) < (1, "hello")
# TODO: should be Unknown, once operand type check is implemented
reveal_type(f) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
g = (bool_instance(), A()) < (bool_instance(), A())
reveal_type(g) # revealed: Unknown
```

View File

@@ -58,8 +58,7 @@ reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: bool
# emitting a lint diagnostic of some kind warning them about the probable error?
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: bool
# TODO: this should be `Literal[False]`; see #14279
reveal_type(sys.version_info == (3, 9, 1, "finallllll", 0)) # revealed: bool
reveal_type(sys.version_info == (3, 8, 1, "finallllll", 0)) # revealed: Literal[False]
```
## Imports and aliases

View File

@@ -31,7 +31,7 @@ use crate::{Db, FxOrderSet};
use smallvec::SmallVec;
pub(crate) struct UnionBuilder<'db> {
elements: Vec<Type<'db>>,
elements: SmallVec<[Type<'db>; 1]>,
db: &'db dyn Db,
}
@@ -39,7 +39,7 @@ impl<'db> UnionBuilder<'db> {
pub(crate) fn new(db: &'db dyn Db) -> Self {
Self {
db,
elements: vec![],
elements: SmallVec::new(),
}
}
@@ -48,13 +48,22 @@ impl<'db> UnionBuilder<'db> {
match ty {
Type::Union(union) => {
let new_elements = union.elements(self.db);
self.elements.reserve(new_elements.len());
for element in new_elements {
self = self.add(*element);
if self.elements.is_empty() {
self.elements.extend_from_slice(new_elements);
} else {
self.elements.reserve(new_elements.len());
for element in new_elements {
self = self.add(*element);
}
}
}
Type::Never => {}
_ => {
if self.elements.is_empty() {
self.elements.push(ty);
return self;
}
let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b))
} else {

View File

@@ -3643,16 +3643,16 @@ impl<'db> TypeInferenceBuilder<'db> {
let lhs_elements = lhs.elements(self.db);
let rhs_elements = rhs.elements(self.db);
let mut lexicographic_type_comparison =
|op| self.infer_lexicographic_type_comparison(lhs_elements, op, rhs_elements);
let mut tuple_rich_comparison =
|op| self.infer_tuple_rich_comparison(lhs_elements, op, rhs_elements);
match op {
ast::CmpOp::Eq => lexicographic_type_comparison(RichCompareOperator::Eq),
ast::CmpOp::NotEq => lexicographic_type_comparison(RichCompareOperator::Ne),
ast::CmpOp::Lt => lexicographic_type_comparison(RichCompareOperator::Lt),
ast::CmpOp::LtE => lexicographic_type_comparison(RichCompareOperator::Le),
ast::CmpOp::Gt => lexicographic_type_comparison(RichCompareOperator::Gt),
ast::CmpOp::GtE => lexicographic_type_comparison(RichCompareOperator::Ge),
ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq),
ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne),
ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt),
ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le),
ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt),
ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge),
ast::CmpOp::In | ast::CmpOp::NotIn => {
let mut eq_count = 0usize;
let mut not_eq_count = 0usize;
@@ -3685,8 +3685,7 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::CmpOp::Is | ast::CmpOp::IsNot => {
// - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool`
// - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool`
let eq_result = lexicographic_type_comparison(RichCompareOperator::Eq)
.expect(
let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect(
"infer_binary_type_comparison should never return None for `CmpOp::Eq`",
);
@@ -3751,53 +3750,80 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
/// Performs lexicographic comparison between two slices of types.
/// Simulates rich comparison between tuples and returns the inferred result.
/// This performs a lexicographic comparison, returning a union of all possible return types that could result from the comparison.
///
/// For lexicographic comparison, elements from both slices are compared pairwise using
/// `infer_binary_type_comparison`. If a conclusive result cannot be determined as a `BooleanLiteral`,
/// it returns `bool`. Returns `None` if the comparison is not supported.
fn infer_lexicographic_type_comparison(
/// basically it's based on cpython's `tuple_richcompare`
/// see `<https://github.com/python/cpython/blob/9d6366b60d01305fc5e45100e0cd13e358aa397d/Objects/tupleobject.c#L637>`
fn infer_tuple_rich_comparison(
&mut self,
left: &[Type<'db>],
op: RichCompareOperator,
right: &[Type<'db>],
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
// Compare paired elements from left and right slices
for (l_ty, r_ty) in left.iter().copied().zip(right.iter().copied()) {
let eq_result = self
let left_iter = left.iter().copied();
let right_iter = right.iter().copied();
let mut builder = UnionBuilder::new(self.db);
for (l_ty, r_ty) in left_iter.zip(right_iter) {
let pairwise_eq_result = self
.infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty)
.expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
match eq_result {
match pairwise_eq_result {
// If propagation is required, return the result as is
Type::Todo => return Ok(Type::Todo),
ty => match ty.bool(self.db) {
// Types are equal, continue to the next pair
// - AlwaysTrue : Continue to the next pair for lexicographic comparison
Truthiness::AlwaysTrue => continue,
// Types are not equal, perform the specified comparison and return the result
Truthiness::AlwaysFalse => {
return self.infer_binary_type_comparison(l_ty, op.into(), r_ty)
// - AlwaysFalse:
// Lexicographic comparisons will always terminate with this pair.
// Complete the comparison and return the result.
// - Ambiguous:
// Lexicographic comparisons might continue to the next pair (if eq_result is true),
// or terminate here (if eq_result is false).
// To account for cases where the comparison terminates here, add the pairwise comparison result to the union builder.
eq_truthiness @ (Truthiness::AlwaysFalse | Truthiness::Ambiguous) => {
let pairwise_compare_result = match op {
RichCompareOperator::Lt
| RichCompareOperator::Le
| RichCompareOperator::Gt
| RichCompareOperator::Ge => {
self.infer_binary_type_comparison(l_ty, op.into(), r_ty)?
}
// For `==` and `!=`, we already figure out the result from `pairwise_eq_result`
// NOTE: The CPython implementation does not account for non-boolean return types
// or cases where `!=` is not the negation of `==`, we also do not consider these cases.
RichCompareOperator::Eq => Type::BooleanLiteral(false),
RichCompareOperator::Ne => Type::BooleanLiteral(true),
};
builder = builder.add(pairwise_compare_result);
if eq_truthiness.is_ambiguous() {
continue;
}
return Ok(builder.build());
}
// If the intermediate result is ambiguous, we cannot determine the final result as BooleanLiteral.
// In this case, we simply return a bool instance.
Truthiness::Ambiguous => return Ok(KnownClass::Bool.to_instance(self.db)),
},
}
}
// At this point, the lengths of the two slices may be different, but the prefix of
// left and right slices is entirely identical.
// We return a comparison of the slice lengths based on the operator.
// if no more items to compare, we just compare sizes
let (left_len, right_len) = (left.len(), right.len());
Ok(Type::BooleanLiteral(match op {
builder = builder.add(Type::BooleanLiteral(match op {
RichCompareOperator::Eq => left_len == right_len,
RichCompareOperator::Ne => left_len != right_len,
RichCompareOperator::Lt => left_len < right_len,
RichCompareOperator::Le => left_len <= right_len,
RichCompareOperator::Gt => left_len > right_len,
RichCompareOperator::Ge => left_len >= right_len,
}))
}));
Ok(builder.build())
}
fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> {

View File

@@ -840,7 +840,7 @@ fn stdin_multiple_parse_error() {
#[test]
fn parse_error_not_included() {
// Select any rule except for `E999`, syntax error should still be shown.
// Parse errors are always shown
let mut cmd = RuffCheck::default().args(["--select=I"]).build();
assert_cmd_snapshot!(cmd
.pass_stdin("foo =\n"), @r"
@@ -859,27 +859,6 @@ fn parse_error_not_included() {
");
}
#[test]
fn deprecated_parse_error_selection() {
let mut cmd = RuffCheck::default().args(["--select=E999"]).build();
assert_cmd_snapshot!(cmd
.pass_stdin("foo =\n"), @r"
success: false
exit_code: 1
----- stdout -----
-:1:6: SyntaxError: Expected an expression
|
1 | foo =
| ^
|
Found 1 error.
----- stderr -----
warning: Rule `E999` is deprecated and will be removed in a future release. Syntax errors will always be shown regardless of whether this rule is selected or not.
");
}
#[test]
fn full_output_preview() {
let mut cmd = RuffCheck::default().args(["--preview"]).build();
@@ -1250,6 +1229,68 @@ fn removed_indirect() {
");
}
#[test]
fn removed_ignore_direct() {
let mut cmd = RuffCheck::default().args(["--ignore", "UP027"]).build();
assert_cmd_snapshot!(cmd, @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: The following rules have been removed and ignoring them has no effect:
- UP027
");
}
#[test]
fn removed_ignore_multiple_direct() {
let mut cmd = RuffCheck::default()
.args(["--ignore", "UP027", "--ignore", "PLR1706"])
.build();
assert_cmd_snapshot!(cmd, @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: The following rules have been removed and ignoring them has no effect:
- PLR1706
- UP027
");
}
#[test]
fn removed_ignore_remapped_direct() {
let mut cmd = RuffCheck::default().args(["--ignore", "PGH001"]).build();
assert_cmd_snapshot!(cmd, @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: `PGH001` has been remapped to `S307`.
");
}
#[test]
fn removed_ignore_indirect() {
// `PLR170` includes removed rules but should not select or warn
// since it is not a "direct" selection
let mut cmd = RuffCheck::default().args(["--ignore", "PLR170"]).build();
assert_cmd_snapshot!(cmd, @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
}
#[test]
fn redirect_direct() {
// Selection of a redirected rule directly should use the new rule and warn

View File

@@ -88,7 +88,6 @@ linter.rules.enabled = [
ambiguous-class-name (E742),
ambiguous-function-name (E743),
io-error (E902),
syntax-error (E999),
unused-import (F401),
import-shadowed-by-loop-var (F402),
undefined-local-with-import-star (F403),
@@ -150,7 +149,6 @@ linter.rules.should_fix = [
ambiguous-class-name (E742),
ambiguous-function-name (E743),
io-error (E902),
syntax-error (E999),
unused-import (F401),
import-shadowed-by-loop-var (F402),
undefined-local-with-import-star (F403),

View File

@@ -71,7 +71,7 @@ class Foo:
def foo(self: "Foo", a: int, b: int) -> int:
pass
# ANN101
# OK
def foo(self, a: int, b: int) -> int:
pass
@@ -125,12 +125,12 @@ class Foo:
def foo(cls: Type["Foo"], a: int, b: int) -> int:
pass
# ANN102
# OK
@classmethod
def foo(cls, a: int, b: int) -> int:
pass
# ANN101
# OK
def foo(self, /, a: int, b: int) -> int:
pass

View File

@@ -1,58 +0,0 @@
import abc
from abc import abstractmethod
import pytest
@pytest.fixture()
def _patch_something(mocker): # OK simple
mocker.patch("some.thing")
@pytest.fixture()
def _patch_something(mocker): # OK with return
if something:
return
mocker.patch("some.thing")
@pytest.fixture()
def _activate_context(): # OK with yield
with context:
yield
class BaseTest:
@pytest.fixture()
@abc.abstractmethod
def my_fixture(): # OK abstract with import abc
raise NotImplementedError
class BaseTest:
@pytest.fixture()
@abstractmethod
def my_fixture(): # OK abstract with from import
raise NotImplementedError
@pytest.fixture()
def my_fixture(): # OK ignoring yield from
yield from some_generator()
@pytest.fixture()
def my_fixture(): # OK ignoring yield value
yield 1
@pytest.fixture()
def patch_something(mocker): # Error simple
mocker.patch("some.thing")
@pytest.fixture()
def activate_context(): # Error with yield
with context:
yield

View File

@@ -1,57 +0,0 @@
import abc
from abc import abstractmethod
import pytest
@pytest.fixture()
def my_fixture(mocker): # OK with return
return 0
@pytest.fixture()
def activate_context(): # OK with yield
with get_context() as context:
yield context
@pytest.fixture()
def _any_fixture(mocker): # Ok nested function
def nested_function():
return 1
mocker.patch("...", nested_function)
class BaseTest:
@pytest.fixture()
@abc.abstractmethod
def _my_fixture(): # OK abstract with import abc
return NotImplemented
class BaseTest:
@pytest.fixture()
@abstractmethod
def _my_fixture(): # OK abstract with from import
return NotImplemented
@pytest.fixture()
def _my_fixture(mocker): # Error with return
return 0
@pytest.fixture()
def _activate_context(): # Error with yield
with get_context() as context:
yield context
@pytest.fixture()
def _activate_context(): # Error with conditional yield from
if some_condition:
with get_context() as context:
yield context
else:
yield from other_context()

View File

@@ -1,6 +1,6 @@
"""Tests to determine first-party import classification.
For typing-only import detection tests, see `TCH002.py`.
For typing-only import detection tests, see `TC002.py`.
"""

View File

@@ -2,49 +2,49 @@
def f():
import pandas as pd # TCH002
import pandas as pd # TC002
x: pd.DataFrame
def f():
from pandas import DataFrame # TCH002
from pandas import DataFrame # TC002
x: DataFrame
def f():
from pandas import DataFrame as df # TCH002
from pandas import DataFrame as df # TC002
x: df
def f():
import pandas as pd # TCH002
import pandas as pd # TC002
x: pd.DataFrame = 1
def f():
from pandas import DataFrame # TCH002
from pandas import DataFrame # TC002
x: DataFrame = 2
def f():
from pandas import DataFrame as df # TCH002
from pandas import DataFrame as df # TC002
x: df = 3
def f():
import pandas as pd # TCH002
import pandas as pd # TC002
x: "pd.DataFrame" = 1
def f():
import pandas as pd # TCH002
import pandas as pd # TC002
x = dict["pd.DataFrame", "pd.DataFrame"]
@@ -153,13 +153,13 @@ def f():
def f():
from pandas import DataFrame # noqa: TCH002
from pandas import DataFrame # noqa: TC002
x: DataFrame = 2
def f():
from pandas import ( # noqa: TCH002
from pandas import ( # noqa: TC002
DataFrame,
)

View File

@@ -1,6 +1,6 @@
"""Tests to determine standard library import classification.
For typing-only import detection tests, see `TCH002.py`.
For typing-only import detection tests, see `TC002.py`.
"""

View File

@@ -1,25 +1,25 @@
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
pass # TCH005
pass # TC005
if False:
pass # TCH005
pass # TC005
if 0:
pass # TCH005
pass # TC005
def example():
if TYPE_CHECKING:
pass # TCH005
pass # TC005
return
class Test:
if TYPE_CHECKING:
pass # TCH005
pass # TC005
x = 2
@@ -42,7 +42,7 @@ if 0:
from typing_extensions import TYPE_CHECKING
if TYPE_CHECKING:
pass # TCH005
pass # TC005
# https://github.com/astral-sh/ruff/issues/11368
if TYPE_CHECKING:

View File

@@ -0,0 +1,18 @@
from __future__ import annotations
from typing import TypeVar
x: "int" | str # TC010
x: ("int" | str) | "bool" # TC010
def func():
x: "int" | str # OK
z: list[str, str | "int"] = [] # TC010
type A = Value["int" | str] # OK
OldS = TypeVar('OldS', int | 'str', str) # TC010

View File

@@ -0,0 +1,16 @@
from typing import TypeVar
x: "int" | str # TC010
x: ("int" | str) | "bool" # TC010
def func():
x: "int" | str # OK
z: list[str, str | "int"] = [] # TC010
type A = Value["int" | str] # OK
OldS = TypeVar('OldS', int | 'str', str) # TC010

View File

@@ -1,18 +0,0 @@
from __future__ import annotations
from typing import TypeVar
x: "int" | str # TCH010
x: ("int" | str) | "bool" # TCH010
def func():
x: "int" | str # OK
z: list[str, str | "int"] = [] # TCH010
type A = Value["int" | str] # OK
OldS = TypeVar('OldS', int | 'str', str) # TCH010

View File

@@ -1,16 +0,0 @@
from typing import TypeVar
x: "int" | str # TCH010
x: ("int" | str) | "bool" # TCH010
def func():
x: "int" | str # OK
z: list[str, str | "int"] = [] # TCH010
type A = Value["int" | str] # OK
OldS = TypeVar('OldS', int | 'str', str) # TCH010

View File

@@ -7,10 +7,10 @@ import pydantic
from pydantic import BaseModel
if TYPE_CHECKING:
import datetime # TCH004
from array import array # TCH004
import datetime # TC004
from array import array # TC004
import pandas # TCH004
import pandas # TC004
import pyproj

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
import geopandas as gpd # TCH002
import geopandas as gpd # TC002
import pydantic
import pyproj # TCH002
import pyproj # TC002
from pydantic import BaseModel
import numpy

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import datetime
import pathlib
from uuid import UUID # TCH003
from uuid import UUID # TC003
import pydantic
from pydantic import BaseModel

View File

@@ -9,10 +9,10 @@ from attrs import frozen
import numpy
if TYPE_CHECKING:
import datetime # TCH004
from array import array # TCH004
import datetime # TC004
from array import array # TC004
import pandas # TCH004
import pandas # TC004
import pyproj

View File

@@ -7,7 +7,7 @@ import pandas
import pyproj
from attrs import frozen
import numpy # TCH002
import numpy # TC002
@attrs.define(auto_attribs=True)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import datetime
from array import array
from dataclasses import dataclass
from uuid import UUID # TCH003
from uuid import UUID # TC003
from collections.abc import Sequence
from pydantic import validate_call

View File

@@ -0,0 +1,8 @@
import pytest
pytest.importorskip("foo.bar")
import re
from sys import version
from numpy import *

View File

@@ -1,18 +0,0 @@
# Should change
foo, bar, baz = [fn(x) for x in items]
foo, bar, baz =[fn(x) for x in items]
foo, bar, baz = [fn(x) for x in items]
foo, bar, baz = [[i for i in fn(x)] for x in items]
foo, bar, baz = [
fn(x)
for x in items
]
# Should not change
foo = [fn(x) for x in items]
x, = [await foo for foo in bar]

View File

@@ -0,0 +1,6 @@
[project]
name = "hello-world"
version = "0.1.0"
# PEP 639 - passing
license = "MIT OR GPL-2.0-or-later OR (FSFUL AND BSD-2-Clause)"
license-files = ["LICEN[CS]E*", "AUTHORS*", "licenses/LICENSE.MIT"]

View File

@@ -1,5 +1,3 @@
# license-files is wrong here
# https://github.com/PyO3/maturin/issues/1615
[build-system]
requires = [ "maturin>=0.14", "numpy", "wheel", "patchelf",]
build-backend = "maturin"

View File

@@ -27,10 +27,8 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingTypeArgs,
Rule::MissingTypeCls,
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
]);
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);

View File

@@ -180,8 +180,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::RedundantNumericUnion) {
flake8_pyi::rules::redundant_numeric_union(checker, parameters);
}
if checker.enabled(Rule::PrePep570PositionalArgument) {
flake8_pyi::rules::pre_pep570_positional_argument(checker, function_def);
if checker.enabled(Rule::Pep484StylePositionalOnlyParameter) {
flake8_pyi::rules::pep_484_positional_parameter(checker, function_def);
}
if checker.enabled(Rule::DunderFunctionName) {
if let Some(diagnostic) = pep8_naming::rules::dunder_function_name(
@@ -293,8 +293,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
Rule::PytestFixtureIncorrectParenthesesStyle,
Rule::PytestFixturePositionalArgs,
Rule::PytestExtraneousScopeFunction,
Rule::PytestMissingFixtureNameUnderscore,
Rule::PytestIncorrectFixtureNameUnderscore,
Rule::PytestFixtureParamWithoutValue,
Rule::PytestDeprecatedYieldFixture,
Rule::PytestFixtureFinalizerCallback,
@@ -304,7 +302,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
]) {
flake8_pytest_style::rules::fixture(
checker,
stmt,
name,
parameters,
returns.as_deref(),
@@ -594,18 +591,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonAsciiImportName) {
pylint::rules::non_ascii_module_import(checker, alias);
}
// TODO(charlie): Remove when stabilizing A004.
if let Some(asname) = &alias.asname {
if checker.settings.preview.is_disabled()
&& checker.enabled(Rule::BuiltinVariableShadowing)
{
flake8_builtins::rules::builtin_variable_shadowing(
checker,
asname,
asname.range(),
);
}
}
if checker.enabled(Rule::Debugger) {
if let Some(diagnostic) =
flake8_debugger::rules::debugger_import(stmt, None, &alias.name)
@@ -915,19 +901,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt.range(),
));
}
} else {
// TODO(charlie): Remove when stabilizing A004.
if let Some(asname) = &alias.asname {
if checker.settings.preview.is_disabled()
&& checker.enabled(Rule::BuiltinVariableShadowing)
{
flake8_builtins::rules::builtin_variable_shadowing(
checker,
asname,
asname.range(),
);
}
}
}
if checker.enabled(Rule::RelativeImports) {
if let Some(diagnostic) = flake8_tidy_imports::rules::banned_relative_import(
@@ -1560,9 +1533,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker, stmt, targets, value,
);
}
if checker.enabled(Rule::UnpackedListComprehension) {
pyupgrade::rules::unpacked_list_comprehension(checker, targets, value);
}
if checker.enabled(Rule::PandasDfVariableName) {
if let Some(diagnostic) = pandas_vet::rules::assignment_to_df(targets) {
checker.diagnostics.push(diagnostic);

View File

@@ -453,6 +453,8 @@ impl<'a> Checker<'a> {
impl<'a> Visitor<'a> for Checker<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
let in_preview = self.settings.preview.is_enabled();
// Step 0: Pre-processing
self.semantic.push_node(stmt);
@@ -504,7 +506,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
|| helpers::in_nested_block(self.semantic.current_statements())
|| imports::is_matplotlib_activation(stmt, self.semantic())
|| imports::is_sys_path_modification(stmt, self.semantic())
|| imports::is_os_environ_modification(stmt, self.semantic()))
|| imports::is_os_environ_modification(stmt, self.semantic())
|| (in_preview && imports::is_pytest_importorskip(stmt, self.semantic())))
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}

View File

@@ -126,7 +126,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pycodestyle, "E742") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousClassName),
(Pycodestyle, "E743") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousFunctionName),
(Pycodestyle, "E902") => (RuleGroup::Stable, rules::pycodestyle::rules::IOError),
(Pycodestyle, "E999") => (RuleGroup::Deprecated, rules::pycodestyle::rules::SyntaxError),
#[allow(deprecated)]
(Pycodestyle, "E999") => (RuleGroup::Removed, rules::pycodestyle::rules::SyntaxError),
// pycodestyle warnings
(Pycodestyle, "W191") => (RuleGroup::Stable, rules::pycodestyle::rules::TabIndentation),
@@ -187,7 +188,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0131") => (RuleGroup::Stable, rules::pylint::rules::TypeBivariance),
(Pylint, "C0132") => (RuleGroup::Stable, rules::pylint::rules::TypeParamNameMismatch),
(Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots),
(Pylint, "C0206") => (RuleGroup::Preview, rules::pylint::rules::DictIndexMissingItems),
(Pylint, "C0206") => (RuleGroup::Stable, rules::pylint::rules::DictIndexMissingItems),
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
@@ -312,8 +313,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing),
(Flake8Builtins, "002") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinArgumentShadowing),
(Flake8Builtins, "003") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinAttributeShadowing),
// TODO(charlie): When stabilizing, remove preview gating for A001's treatment of imports.
(Flake8Builtins, "004") => (RuleGroup::Preview, rules::flake8_builtins::rules::BuiltinImportShadowing),
(Flake8Builtins, "004") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinImportShadowing),
(Flake8Builtins, "005") => (RuleGroup::Preview, rules::flake8_builtins::rules::BuiltinModuleShadowing),
(Flake8Builtins, "006") => (RuleGroup::Preview, rules::flake8_builtins::rules::BuiltinLambdaArgumentShadowing),
@@ -352,7 +352,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bugbear, "033") => (RuleGroup::Stable, rules::flake8_bugbear::rules::DuplicateValue),
(Flake8Bugbear, "034") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ReSubPositionalArgs),
(Flake8Bugbear, "035") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StaticKeyDictComprehension),
(Flake8Bugbear, "039") => (RuleGroup::Preview, rules::flake8_bugbear::rules::MutableContextvarDefault),
(Flake8Bugbear, "039") => (RuleGroup::Stable, rules::flake8_bugbear::rules::MutableContextvarDefault),
(Flake8Bugbear, "901") => (RuleGroup::Preview, rules::flake8_bugbear::rules::ReturnInGenerator),
(Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept),
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
@@ -428,8 +428,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Annotations, "001") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeFunctionArgument),
(Flake8Annotations, "002") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeArgs),
(Flake8Annotations, "003") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeKwargs),
(Flake8Annotations, "101") => (RuleGroup::Deprecated, rules::flake8_annotations::rules::MissingTypeSelf),
(Flake8Annotations, "102") => (RuleGroup::Deprecated, rules::flake8_annotations::rules::MissingTypeCls),
#[allow(deprecated)]
(Flake8Annotations, "101") => (RuleGroup::Removed, rules::flake8_annotations::rules::MissingTypeSelf),
#[allow(deprecated)]
(Flake8Annotations, "102") => (RuleGroup::Removed, rules::flake8_annotations::rules::MissingTypeCls),
(Flake8Annotations, "201") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeUndocumentedPublicFunction),
(Flake8Annotations, "202") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypePrivateFunction),
(Flake8Annotations, "204") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeSpecialMethod),
@@ -513,7 +515,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "024") => (RuleGroup::Stable, rules::pyupgrade::rules::OSErrorAlias),
(Pyupgrade, "025") => (RuleGroup::Stable, rules::pyupgrade::rules::UnicodeKindPrefix),
(Pyupgrade, "026") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedMockImport),
(Pyupgrade, "027") => (RuleGroup::Deprecated, rules::pyupgrade::rules::UnpackedListComprehension),
(Pyupgrade, "027") => (RuleGroup::Removed, rules::pyupgrade::rules::UnpackedListComprehension),
(Pyupgrade, "028") => (RuleGroup::Stable, rules::pyupgrade::rules::YieldInForLoop),
(Pyupgrade, "029") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryBuiltinImport),
(Pyupgrade, "030") => (RuleGroup::Stable, rules::pyupgrade::rules::FormatLiterals),
@@ -529,7 +531,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias),
(Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias),
(Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum),
(Pyupgrade, "043") => (RuleGroup::Preview, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs),
(Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs),
(Pyupgrade, "044") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP646Unpack),
// pydocstyle
@@ -788,16 +790,18 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(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::Preview, rules::flake8_pyi::rules::PrePep570PositionalArgument),
(Flake8Pyi, "064") => (RuleGroup::Preview, rules::flake8_pyi::rules::RedundantFinalLiteral),
(Flake8Pyi, "066") => (RuleGroup::Preview, rules::flake8_pyi::rules::BadVersionInfoOrder),
(Flake8Pyi, "063") => (RuleGroup::Stable, rules::flake8_pyi::rules::Pep484StylePositionalOnlyParameter),
(Flake8Pyi, "064") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantFinalLiteral),
(Flake8Pyi, "066") => (RuleGroup::Stable, rules::flake8_pyi::rules::BadVersionInfoOrder),
// flake8-pytest-style
(Flake8PytestStyle, "001") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle),
(Flake8PytestStyle, "002") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixturePositionalArgs),
(Flake8PytestStyle, "003") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestExtraneousScopeFunction),
(Flake8PytestStyle, "004") => (RuleGroup::Deprecated, rules::flake8_pytest_style::rules::PytestMissingFixtureNameUnderscore),
(Flake8PytestStyle, "005") => (RuleGroup::Deprecated, rules::flake8_pytest_style::rules::PytestIncorrectFixtureNameUnderscore),
#[allow(deprecated)]
(Flake8PytestStyle, "004") => (RuleGroup::Removed, rules::flake8_pytest_style::rules::PytestMissingFixtureNameUnderscore),
#[allow(deprecated)]
(Flake8PytestStyle, "005") => (RuleGroup::Removed, rules::flake8_pytest_style::rules::PytestIncorrectFixtureNameUnderscore),
(Flake8PytestStyle, "006") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestParametrizeNamesWrongType),
(Flake8PytestStyle, "007") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestParametrizeValuesWrongType),
(Flake8PytestStyle, "008") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestPatchWithLambda),
@@ -924,8 +928,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation),
// fastapi
(FastApi, "001") => (RuleGroup::Preview, rules::fastapi::rules::FastApiRedundantResponseModel),
(FastApi, "002") => (RuleGroup::Preview, rules::fastapi::rules::FastApiNonAnnotatedDependency),
(FastApi, "001") => (RuleGroup::Stable, rules::fastapi::rules::FastApiRedundantResponseModel),
(FastApi, "002") => (RuleGroup::Stable, rules::fastapi::rules::FastApiNonAnnotatedDependency),
(FastApi, "003") => (RuleGroup::Preview, rules::fastapi::rules::FastApiUnusedPathParameter),
// pydoclint
@@ -955,15 +959,15 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "018") => (RuleGroup::Stable, rules::ruff::rules::AssignmentInAssert),
(Ruff, "019") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryKeyCheck),
(Ruff, "020") => (RuleGroup::Stable, rules::ruff::rules::NeverUnion),
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll),
(Ruff, "023") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderSlots),
(Ruff, "021") => (RuleGroup::Stable, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "022") => (RuleGroup::Stable, rules::ruff::rules::UnsortedDunderAll),
(Ruff, "023") => (RuleGroup::Stable, rules::ruff::rules::UnsortedDunderSlots),
(Ruff, "024") => (RuleGroup::Stable, rules::ruff::rules::MutableFromkeysValue),
(Ruff, "026") => (RuleGroup::Stable, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync),
(Ruff, "030") => (RuleGroup::Preview, rules::ruff::rules::AssertWithPrintMessage),
(Ruff, "030") => (RuleGroup::Stable, rules::ruff::rules::AssertWithPrintMessage),
(Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript),
(Ruff, "032") => (RuleGroup::Preview, rules::ruff::rules::DecimalFromFloatLiteral),
(Ruff, "033") => (RuleGroup::Preview, rules::ruff::rules::PostInitDefault),

View File

@@ -155,7 +155,7 @@ pub enum Linter {
#[prefix = "TID"]
Flake8TidyImports,
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
#[prefix = "TCH"]
#[prefix = "TC"]
Flake8TypeChecking,
/// [flake8-gettext](https://pypi.org/project/flake8-gettext/)
#[prefix = "INT"]

View File

@@ -87,8 +87,8 @@ static REDIRECTS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(
("PDV90", "PD90"),
("PDV901", "PD901"),
// TODO(charlie): Remove by 2023-04-01.
("TYP", "TCH"),
("TYP001", "TCH001"),
("TYP", "TC"),
("TYP001", "TC001"),
// TODO(charlie): Remove by 2023-06-01.
("RUF004", "B026"),
("PIE802", "C419"),
@@ -98,7 +98,6 @@ static REDIRECTS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(
("T003", "FIX003"),
("T004", "FIX004"),
("RUF011", "B035"),
("TCH006", "TCH010"),
("TRY200", "B904"),
("PGH001", "S307"),
("PGH002", "G010"),
@@ -126,5 +125,14 @@ static REDIRECTS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(
("RUF025", "C420"),
// See: https://github.com/astral-sh/ruff/issues/13492
("TRY302", "TRY203"),
// TCH renamed to TC to harmonize with flake8 plugin
("TCH", "TC"),
("TCH001", "TC001"),
("TCH002", "TC002"),
("TCH003", "TC003"),
("TCH004", "TC004"),
("TCH005", "TC005"),
("TCH006", "TC010"),
("TCH010", "TC010"),
])
});

View File

@@ -25,4 +25,20 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
// FAST002 autofixes use `typing_extensions` on Python 3.8,
// since `typing.Annotated` was added in Python 3.9
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002.py"))]
fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}_py38", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings {
target_version: settings::types::PythonVersion::Py38,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -11,14 +11,18 @@ use crate::rules::fastapi::rules::is_fastapi_route;
use crate::settings::types::PythonVersion;
/// ## What it does
/// Identifies FastAPI routes with deprecated uses of `Depends`.
/// Identifies FastAPI routes with deprecated uses of `Depends` or similar.
///
/// ## Why is this bad?
/// The FastAPI documentation recommends the use of `Annotated` for defining
/// route dependencies and parameters, rather than using `Depends` directly
/// with a default value.
/// The [FastAPI documentation] recommends the use of [`typing.Annotated`] for
/// defining route dependencies and parameters, rather than using `Depends`,
/// `Query` or similar as a default value for a parameter. Using this approach
/// everywhere helps ensure consistency and clarity in defining dependencies
/// and parameters.
///
/// This approach is also suggested for various route parameters, including Body and Cookie, as it helps ensure consistency and clarity in defining dependencies and parameters.
/// `Annotated` was added to the `typing` module in Python 3.9; however,
/// the third-party [`typing_extensions`] package provides a backport that can be
/// used on older versions of Python.
///
/// ## Example
///
@@ -55,9 +59,14 @@ use crate::settings::types::PythonVersion;
/// async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
/// return commons
/// ```
///
/// [fastAPI documentation]: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/?h=annotated#advantages-of-annotated
/// [typing.Annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated
/// [typing_extensions]: https://typing-extensions.readthedocs.io/en/stable/
#[violation]
pub struct FastApiNonAnnotatedDependency;
pub struct FastApiNonAnnotatedDependency {
py_version: PythonVersion,
}
impl Violation for FastApiNonAnnotatedDependency {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
@@ -68,11 +77,16 @@ impl Violation for FastApiNonAnnotatedDependency {
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Annotated`".to_string())
let title = if self.py_version >= PythonVersion::Py39 {
"Replace with `typing.Annotated`"
} else {
"Replace with `typing_extensions.Annotated`"
};
Some(title.to_string())
}
}
/// RUF103
/// FAST002
pub(crate) fn fastapi_non_annotated_dependency(
checker: &mut Checker,
function_def: &ast::StmtFunctionDef,
@@ -135,7 +149,12 @@ fn create_diagnostic(
parameter: &ast::ParameterWithDefault,
safe_to_update: bool,
) {
let mut diagnostic = Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range);
let mut diagnostic = Diagnostic::new(
FastApiNonAnnotatedDependency {
py_version: checker.settings.target_version,
},
parameter.range,
);
if safe_to_update {
if let (Some(annotation), Some(default)) =

View File

@@ -73,7 +73,7 @@ impl AlwaysFixableViolation for FastApiRedundantResponseModel {
}
}
/// RUF102
/// FAST001
pub(crate) fn fastapi_redundant_response_model(
checker: &mut Checker,
function_def: &StmtFunctionDef,

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
snapshot_kind: text
---
FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
|
@@ -11,7 +10,7 @@ FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
25 | some_security_param: str = Security(get_oauth2_user),
26 | ):
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -40,7 +39,7 @@ FAST002.py:25:5: FAST002 [*] FastAPI dependency without `Annotated`
26 | ):
27 | pass
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -69,7 +68,7 @@ FAST002.py:32:5: FAST002 [*] FastAPI dependency without `Annotated`
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -98,7 +97,7 @@ FAST002.py:33:5: FAST002 [*] FastAPI dependency without `Annotated`
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -127,7 +126,7 @@ FAST002.py:34:5: FAST002 [*] FastAPI dependency without `Annotated`
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -156,7 +155,7 @@ FAST002.py:35:5: FAST002 [*] FastAPI dependency without `Annotated`
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -185,7 +184,7 @@ FAST002.py:36:5: FAST002 [*] FastAPI dependency without `Annotated`
37 | some_file_param: UploadFile = File(),
38 | some_form_param: str = Form(),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -214,7 +213,7 @@ FAST002.py:37:5: FAST002 [*] FastAPI dependency without `Annotated`
38 | some_form_param: str = Form(),
39 | ):
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -243,7 +242,7 @@ FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated`
39 | ):
40 | # do stuff
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -272,7 +271,7 @@ FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated`
48 | ):
49 | pass
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -301,7 +300,7 @@ FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
54 | skip: int = 0,
55 | limit: int = 10,
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@@ -330,4 +329,4 @@ FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
68 | ):
69 | pass
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`

View File

@@ -0,0 +1,332 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
---
FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
|
22 | @app.get("/items/")
23 | def get_items(
24 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
25 | some_security_param: str = Security(get_oauth2_user),
26 | ):
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
21 22 |
22 23 | @app.get("/items/")
23 24 | def get_items(
24 |- current_user: User = Depends(get_current_user),
25 |+ current_user: Annotated[User, Depends(get_current_user)],
25 26 | some_security_param: str = Security(get_oauth2_user),
26 27 | ):
27 28 | pass
FAST002.py:25:5: FAST002 [*] FastAPI dependency without `Annotated`
|
23 | def get_items(
24 | current_user: User = Depends(get_current_user),
25 | some_security_param: str = Security(get_oauth2_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
26 | ):
27 | pass
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
22 23 | @app.get("/items/")
23 24 | def get_items(
24 25 | current_user: User = Depends(get_current_user),
25 |- some_security_param: str = Security(get_oauth2_user),
26 |+ some_security_param: Annotated[str, Security(get_oauth2_user)],
26 27 | ):
27 28 | pass
28 29 |
FAST002.py:32:5: FAST002 [*] FastAPI dependency without `Annotated`
|
30 | @app.post("/stuff/")
31 | def do_stuff(
32 | some_query_param: str | None = Query(default=None),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
29 30 |
30 31 | @app.post("/stuff/")
31 32 | def do_stuff(
32 |- some_query_param: str | None = Query(default=None),
33 |+ some_query_param: Annotated[str | None, Query(default=None)],
33 34 | some_path_param: str = Path(),
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
FAST002.py:33:5: FAST002 [*] FastAPI dependency without `Annotated`
|
31 | def do_stuff(
32 | some_query_param: str | None = Query(default=None),
33 | some_path_param: str = Path(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
30 31 | @app.post("/stuff/")
31 32 | def do_stuff(
32 33 | some_query_param: str | None = Query(default=None),
33 |- some_path_param: str = Path(),
34 |+ some_path_param: Annotated[str, Path()],
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
FAST002.py:34:5: FAST002 [*] FastAPI dependency without `Annotated`
|
32 | some_query_param: str | None = Query(default=None),
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
31 32 | def do_stuff(
32 33 | some_query_param: str | None = Query(default=None),
33 34 | some_path_param: str = Path(),
34 |- some_body_param: str = Body("foo"),
35 |+ some_body_param: Annotated[str, Body("foo")],
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
37 38 | some_file_param: UploadFile = File(),
FAST002.py:35:5: FAST002 [*] FastAPI dependency without `Annotated`
|
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
32 33 | some_query_param: str | None = Query(default=None),
33 34 | some_path_param: str = Path(),
34 35 | some_body_param: str = Body("foo"),
35 |- some_cookie_param: str = Cookie(),
36 |+ some_cookie_param: Annotated[str, Cookie()],
36 37 | some_header_param: int = Header(default=5),
37 38 | some_file_param: UploadFile = File(),
38 39 | some_form_param: str = Form(),
FAST002.py:36:5: FAST002 [*] FastAPI dependency without `Annotated`
|
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
37 | some_file_param: UploadFile = File(),
38 | some_form_param: str = Form(),
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
33 34 | some_path_param: str = Path(),
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
36 |- some_header_param: int = Header(default=5),
37 |+ some_header_param: Annotated[int, Header(default=5)],
37 38 | some_file_param: UploadFile = File(),
38 39 | some_form_param: str = Form(),
39 40 | ):
FAST002.py:37:5: FAST002 [*] FastAPI dependency without `Annotated`
|
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
38 | some_form_param: str = Form(),
39 | ):
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
37 |- some_file_param: UploadFile = File(),
38 |+ some_file_param: Annotated[UploadFile, File()],
38 39 | some_form_param: str = Form(),
39 40 | ):
40 41 | # do stuff
FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated`
|
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
38 | some_form_param: str = Form(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
39 | ):
40 | # do stuff
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
37 38 | some_file_param: UploadFile = File(),
38 |- some_form_param: str = Form(),
39 |+ some_form_param: Annotated[str, Form()],
39 40 | ):
40 41 | # do stuff
41 42 | pass
FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated`
|
45 | skip: int,
46 | limit: int,
47 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
48 | ):
49 | pass
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
44 45 | def get_users(
45 46 | skip: int,
46 47 | limit: int,
47 |- current_user: User = Depends(get_current_user),
48 |+ current_user: Annotated[User, Depends(get_current_user)],
48 49 | ):
49 50 | pass
50 51 |
FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
|
51 | @app.get("/users/")
52 | def get_users(
53 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
54 | skip: int = 0,
55 | limit: int = 10,
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
50 51 |
51 52 | @app.get("/users/")
52 53 | def get_users(
53 |- current_user: User = Depends(get_current_user),
54 |+ current_user: Annotated[User, Depends(get_current_user)],
54 55 | skip: int = 0,
55 56 | limit: int = 10,
56 57 | ):
FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
|
65 | skip: int = 0,
66 | limit: int = 10,
67 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
68 | ):
69 | pass
|
= help: Replace with `typing_extensions.Annotated`

View File

@@ -24,8 +24,6 @@ mod tests {
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
@@ -52,8 +50,6 @@ mod tests {
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
@@ -80,8 +76,6 @@ mod tests {
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
])
},
)?;
@@ -161,8 +155,6 @@ mod tests {
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,

View File

@@ -110,9 +110,8 @@ impl Violation for MissingTypeKwargs {
}
}
/// ## Deprecation
/// This rule is commonly disabled because type checkers can infer this type without annotation.
/// It will be removed in a future release.
/// ## Removed
/// This rule has been removed because type checkers can infer this type without annotation.
///
/// ## What it does
/// Checks that instance method `self` arguments have type annotations.
@@ -139,21 +138,22 @@ impl Violation for MissingTypeKwargs {
/// def bar(self: "Foo"): ...
/// ```
#[violation]
pub struct MissingTypeSelf {
name: String,
}
#[deprecated(note = "ANN101 has been removed")]
pub struct MissingTypeSelf;
#[allow(deprecated)]
impl Violation for MissingTypeSelf {
#[derive_message_formats]
fn message(&self) -> String {
let Self { name } = self;
format!("Missing type annotation for `{name}` in method")
unreachable!("ANN101 has been removed");
}
fn message_formats() -> &'static [&'static str] {
&["Missing type annotation for `{name}` in method"]
}
}
/// ## Deprecation
/// This rule is commonly disabled because type checkers can infer this type without annotation.
/// It will be removed in a future release.
/// ## Removed
/// This rule has been removed because type checkers can infer this type without annotation.
///
/// ## What it does
/// Checks that class method `cls` arguments have type annotations.
@@ -182,15 +182,17 @@ impl Violation for MissingTypeSelf {
/// def bar(cls: Type["Foo"]): ...
/// ```
#[violation]
pub struct MissingTypeCls {
name: String,
}
#[deprecated(note = "ANN102 has been removed")]
pub struct MissingTypeCls;
#[allow(deprecated)]
impl Violation for MissingTypeCls {
#[derive_message_formats]
fn message(&self) -> String {
let Self { name } = self;
format!("Missing type annotation for `{name}` in classmethod")
unreachable!("ANN102 has been removed")
}
fn message_formats() -> &'static [&'static str] {
&["Missing type annotation for `{name}` in classmethod"]
}
}
@@ -594,7 +596,6 @@ pub(crate) fn definition(
// Keep track of whether we've seen any typed arguments or return values.
let mut has_any_typed_arg = false; // Any argument has been typed?
let mut has_typed_return = false; // Return value has been typed?
let mut has_typed_self_or_cls = false; // Has a typed `self` or `cls` argument?
// Temporary storage for diagnostics; we emit them at the end
// unless configured to suppress ANN* for declarations that are fully untyped.
@@ -697,43 +698,6 @@ pub(crate) fn definition(
}
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()) {
if let Some(ParameterWithDefault {
parameter,
default: _,
range: _,
}) = parameters
.posonlyargs
.first()
.or_else(|| parameters.args.first())
{
if parameter.annotation.is_none() {
if visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
name: parameter.name.to_string(),
},
parameter.range(),
));
}
} else {
if checker.enabled(Rule::MissingTypeSelf) {
diagnostics.push(Diagnostic::new(
MissingTypeSelf {
name: parameter.name.to_string(),
},
parameter.range(),
));
}
}
} else {
has_typed_self_or_cls = true;
}
}
}
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
has_typed_return = true;
@@ -927,13 +891,25 @@ pub(crate) fn definition(
}
}
}
if !checker.settings.flake8_annotations.ignore_fully_untyped {
return diagnostics;
}
// If settings say so, don't report any of the
// diagnostics gathered here if there were no type annotations at all.
if checker.settings.flake8_annotations.ignore_fully_untyped
&& !(has_any_typed_arg || has_typed_self_or_cls || has_typed_return)
if has_any_typed_arg
|| has_typed_return
|| (is_method
&& !visibility::is_staticmethod(decorator_list, checker.semantic())
&& parameters
.posonlyargs
.first()
.or_else(|| parameters.args.first())
.is_some_and(|first_param| first_param.parameter.annotation.is_some()))
{
vec![]
} else {
diagnostics
} else {
vec![]
}
}

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
snapshot_kind: text
---
annotation_presence.py:5:5: ANN201 [*] Missing return type annotation for public function `foo`
|
@@ -158,14 +157,6 @@ annotation_presence.py:65:39: ANN401 Dynamically typed expressions (typing.Any)
66 | pass
|
annotation_presence.py:75:13: ANN101 Missing type annotation for `self` in method
|
74 | # ANN101
75 | def foo(self, a: int, b: int) -> int:
| ^^^^ ANN101
76 | pass
|
annotation_presence.py:79:29: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
78 | # ANN401
@@ -214,23 +205,6 @@ annotation_presence.py:95:59: ANN401 Dynamically typed expressions (typing.Any)
96 | pass
|
annotation_presence.py:130:13: ANN102 Missing type annotation for `cls` in classmethod
|
128 | # ANN102
129 | @classmethod
130 | def foo(cls, a: int, b: int) -> int:
| ^^^ ANN102
131 | pass
|
annotation_presence.py:134:13: ANN101 Missing type annotation for `self` in method
|
133 | # ANN101
134 | def foo(self, /, a: int, b: int) -> int:
| ^^^^ ANN101
135 | pass
|
annotation_presence.py:149:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
148 | # ANN401
@@ -324,12 +298,3 @@ annotation_presence.py:165:9: ANN204 [*] Missing return type annotation for spec
165 |- def __init__(self):
165 |+ def __init__(self) -> None:
166 166 | print(f"{self.attr=}")
annotation_presence.py:165:18: ANN101 Missing type annotation for `self` in method
|
163 | # Regression test for: https://github.com/astral-sh/ruff/issues/7711
164 | class Class:
165 | def __init__(self):
| ^^^^ ANN101
166 | print(f"{self.attr=}")
|

View File

@@ -19,9 +19,9 @@ use crate::checkers::ast::Checker;
/// the `ContextVar`. If the object is modified, those modifications will persist
/// across calls, which can lead to unexpected behavior.
///
/// Instead, prefer to use immutable data structures; or, take `None` as a
/// default, and initialize a new mutable object inside for each call using the
/// `.set()` method.
/// Instead, prefer to use immutable data structures. Alternatively, take
/// `None` as a default, and initialize a new mutable object inside for each
/// call using the `.set()` method.
///
/// Types outside the standard library can be marked as immutable with the
/// [`lint.flake8-bugbear.extend-immutable-calls`] configuration option.

View File

@@ -16,8 +16,31 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// Builtins can be marked as exceptions to this rule via the
/// [`lint.flake8-builtins.builtins-ignorelist`] configuration option.
///
/// ## Example
/// ```python
/// from rich import print
///
/// print("Some message")
/// ```
///
/// Use instead:
/// ```python
/// from rich import print as rich_print
///
/// rich_print("Some message")
/// ```
///
/// or:
/// ```python
/// import rich
///
/// rich.print("Some message")
/// ```
///
/// ## Options
/// - `lint.flake8-builtins.builtins-ignorelist`
/// - `target-version`
///
#[violation]
pub struct BuiltinImportShadowing {
name: String,

View File

@@ -2,32 +2,6 @@
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
snapshot_kind: text
---
A001.py:1:16: A001 Variable `sum` is shadowing a Python builtin
|
1 | import some as sum
| ^^^ A001
2 | from some import other as int
3 | from directory import new as dir
|
A001.py:2:27: A001 Variable `int` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
| ^^^ A001
3 | from directory import new as dir
|
A001.py:3:30: A001 Variable `dir` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
3 | from directory import new as dir
| ^^^ A001
4 |
5 | print = 1
|
A001.py:5:1: A001 Variable `print` is shadowing a Python builtin
|
3 | from directory import new as dir

View File

@@ -2,22 +2,6 @@
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
snapshot_kind: text
---
A001.py:1:16: A001 Variable `sum` is shadowing a Python builtin
|
1 | import some as sum
| ^^^ A001
2 | from some import other as int
3 | from directory import new as dir
|
A001.py:2:27: A001 Variable `int` is shadowing a Python builtin
|
1 | import some as sum
2 | from some import other as int
| ^^^ A001
3 | from directory import new as dir
|
A001.py:5:1: A001 Variable `print` is shadowing a Python builtin
|
3 | from directory import new as dir

View File

@@ -66,8 +66,8 @@ mod tests {
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))]
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.py"))]
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))]
#[test_case(Rule::PrePep570PositionalArgument, Path::new("PYI063.py"))]
#[test_case(Rule::PrePep570PositionalArgument, Path::new("PYI063.pyi"))]
#[test_case(Rule::Pep484StylePositionalOnlyParameter, Path::new("PYI063.py"))]
#[test_case(Rule::Pep484StylePositionalOnlyParameter, Path::new("PYI063.pyi"))]
#[test_case(Rule::RedundantFinalLiteral, Path::new("PYI064.py"))]
#[test_case(Rule::RedundantFinalLiteral, Path::new("PYI064.pyi"))]
#[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.py"))]

View File

@@ -16,8 +16,9 @@ use crate::registry::Rule;
/// Comparing `sys.version_info` with `==` or `<=` has unexpected behavior
/// and can lead to bugs.
///
/// For example, `sys.version_info > (3, 8)` will also match `3.8.10`,
/// while `sys.version_info <= (3, 8)` will _not_ match `3.8.10`:
/// For example, `sys.version_info > (3, 8, 1)` will resolve to `True` if your
/// Python version is 3.8.1; meanwhile, `sys.version_info <= (3, 8)` will _not_
/// resolve to `True` if your Python version is 3.8.10:
///
/// ```python
/// >>> import sys
@@ -61,16 +62,20 @@ impl Violation for BadVersionInfoComparison {
}
/// ## What it does
/// Checks for if-else statements with `sys.version_info` comparisons that use
/// `<` comparators.
/// Checks for code that branches on `sys.version_info` comparisons where
/// branches corresponding to older Python versions come before branches
/// corresponding to newer Python versions.
///
/// ## Why is this bad?
/// As a convention, branches that correspond to newer Python versions should
/// come first when using `sys.version_info` comparisons. This makes it easier
/// to understand the desired behavior, which typically corresponds to the
/// latest Python versions.
/// come first. This makes it easier to understand the desired behavior, which
/// typically corresponds to the latest Python versions.
///
/// In [preview], this rule will also flag non-stub files.
/// This rule enforces the convention by checking for `if` tests that compare
/// `sys.version_info` with `<` rather than `>=`.
///
/// By default, this rule only applies to stub files.
/// In [preview], it will also flag this anti-pattern in non-stub files.
///
/// ## Example
///
@@ -101,7 +106,7 @@ pub struct BadVersionInfoOrder;
impl Violation for BadVersionInfoOrder {
#[derive_message_formats]
fn message(&self) -> String {
"Use `>=` when using `if`-`else` with `sys.version_info` comparisons".to_string()
"Put branches for newer Python versions first when branching on `sys.version_info` comparisons".to_string()
}
}

View File

@@ -8,12 +8,19 @@ use crate::checkers::ast::Checker;
use crate::settings::types::PythonVersion;
/// ## What it does
/// Checks for the presence of [PEP 484]-style positional-only arguments.
/// Checks for the presence of [PEP 484]-style positional-only parameters.
///
/// ## Why is this bad?
/// Historically, [PEP 484] recommended prefixing positional-only arguments
/// with a double underscore (`__`). However, [PEP 570] introduced a dedicated
/// syntax for positional-only arguments, which should be preferred.
/// Historically, [PEP 484] recommended prefixing parameter names with double
/// underscores (`__`) to indicate to a type checker that they were
/// positional-only. However, [PEP 570] (introduced in Python 3.8) introduced
/// dedicated syntax for positional-only arguments. If a forward slash (`/`) is
/// present in a function signature on Python 3.8+, all parameters prior to the
/// slash are interpreted as positional-only.
///
/// The new syntax should be preferred as it is more widely used, more concise
/// and more readable. It is also respected by Python at runtime, whereas the
/// old-style syntax was only understood by type checkers.
///
/// ## Example
///
@@ -27,15 +34,18 @@ use crate::settings::types::PythonVersion;
/// def foo(x: int, /) -> None: ...
/// ```
///
/// ## Options
/// - `target-version`
///
/// [PEP 484]: https://peps.python.org/pep-0484/#positional-only-arguments
/// [PEP 570]: https://peps.python.org/pep-0570
#[violation]
pub struct PrePep570PositionalArgument;
pub struct Pep484StylePositionalOnlyParameter;
impl Violation for PrePep570PositionalArgument {
impl Violation for Pep484StylePositionalOnlyParameter {
#[derive_message_formats]
fn message(&self) -> String {
"Use PEP 570 syntax for positional-only arguments".to_string()
"Use PEP 570 syntax for positional-only parameters".to_string()
}
fn fix_title(&self) -> Option<String> {
@@ -44,7 +54,7 @@ impl Violation for PrePep570PositionalArgument {
}
/// PYI063
pub(crate) fn pre_pep570_positional_argument(
pub(crate) fn pep_484_positional_parameter(
checker: &mut Checker,
function_def: &ast::StmtFunctionDef,
) {
@@ -79,18 +89,18 @@ pub(crate) fn pre_pep570_positional_argument(
));
if let Some(arg) = function_def.parameters.args.get(skip) {
if is_pre_pep570_positional_only(arg) {
if is_old_style_positional_only(arg) {
checker.diagnostics.push(Diagnostic::new(
PrePep570PositionalArgument,
Pep484StylePositionalOnlyParameter,
arg.identifier(),
));
}
}
}
/// Returns `true` if the [`ParameterWithDefault`] is an old-style positional-only argument (i.e.,
/// 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_pre_pep570_positional_only(arg: &ParameterWithDefault) -> bool {
fn is_old_style_positional_only(arg: &ParameterWithDefault) -> bool {
let arg_name = &arg.parameter.name;
arg_name.starts_with("__") && !arg_name.ends_with("__")
}

View File

@@ -11,18 +11,25 @@ use crate::Locator;
/// Checks for redundant `Final[Literal[...]]` annotations.
///
/// ## Why is this bad?
/// A `Final[Literal[...]]` annotation can be replaced with `Final`; the literal
/// use is unnecessary.
/// All constant variables annotated as `Final` are understood as implicitly
/// having `Literal` types by a type checker. As such, a `Final[Literal[...]]`
/// annotation can often be replaced with a bare `Final`, annotation, which
/// will have the same meaning to the type checker while being more concise and
/// more readable.
///
/// ## Example
///
/// ```pyi
/// from typing import Final, Literal
///
/// x: Final[Literal[42]]
/// y: Final[Literal[42]] = 42
/// ```
///
/// Use instead:
/// ```pyi
/// from typing import Final, Literal
///
/// x: Final = 42
/// y: Final = 42
/// ```

View File

@@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
snapshot_kind: text
---
PYI063.py:6:9: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:6:9: PYI063 Use PEP 570 syntax for positional-only parameters
|
4 | from typing import Self
5 |
@@ -13,7 +12,7 @@ PYI063.py:6:9: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:7:14: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:7:14: PYI063 Use PEP 570 syntax for positional-only parameters
|
6 | def bad(__x: int) -> None: ... # PYI063
7 | def also_bad(__x: int, __y: str) -> None: ... # PYI063
@@ -22,7 +21,7 @@ PYI063.py:7:14: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:8:15: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:8:15: PYI063 Use PEP 570 syntax for positional-only parameters
|
6 | def bad(__x: int) -> None: ... # PYI063
7 | def also_bad(__x: int, __y: str) -> None: ... # PYI063
@@ -33,7 +32,7 @@ PYI063.py:8:15: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:24:14: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:24:14: PYI063 Use PEP 570 syntax for positional-only parameters
|
22 | def bad(__self) -> None: ... # PYI063
23 | @staticmethod
@@ -44,7 +43,7 @@ PYI063.py:24:14: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:25:22: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:25:22: PYI063 Use PEP 570 syntax for positional-only parameters
|
23 | @staticmethod
24 | def bad2(__self) -> None: ... # PYI063
@@ -55,7 +54,7 @@ PYI063.py:25:22: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:26:25: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:26:25: PYI063 Use PEP 570 syntax for positional-only parameters
|
24 | def bad2(__self) -> None: ... # PYI063
25 | def bad3(__self, __x: int) -> None: ... # PYI063
@@ -66,7 +65,7 @@ PYI063.py:26:25: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:28:25: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:28:25: PYI063 Use PEP 570 syntax for positional-only parameters
|
26 | def still_bad(self, __x_: int) -> None: ... # PYI063
27 | @staticmethod
@@ -77,7 +76,7 @@ PYI063.py:28:25: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:30:23: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:30:23: PYI063 Use PEP 570 syntax for positional-only parameters
|
28 | def this_is_bad_too(__x: int) -> None: ... # PYI063
29 | @classmethod
@@ -88,7 +87,7 @@ PYI063.py:30:23: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:52:23: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:52:23: PYI063 Use PEP 570 syntax for positional-only parameters
|
50 | class Metaclass(type):
51 | @classmethod
@@ -99,7 +98,7 @@ PYI063.py:52:23: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.py:56:26: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.py:56:26: PYI063 Use PEP 570 syntax for positional-only parameters
|
54 | class Metaclass2(type):
55 | @classmethod

View File

@@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
snapshot_kind: text
---
PYI063.pyi:6:9: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:6:9: PYI063 Use PEP 570 syntax for positional-only parameters
|
4 | from typing import Self
5 |
@@ -13,7 +12,7 @@ PYI063.pyi:6:9: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:7:14: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:7:14: PYI063 Use PEP 570 syntax for positional-only parameters
|
6 | def bad(__x: int) -> None: ... # PYI063
7 | def also_bad(__x: int, __y: str) -> None: ... # PYI063
@@ -22,7 +21,7 @@ PYI063.pyi:7:14: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:8:15: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:8:15: PYI063 Use PEP 570 syntax for positional-only parameters
|
6 | def bad(__x: int) -> None: ... # PYI063
7 | def also_bad(__x: int, __y: str) -> None: ... # PYI063
@@ -33,7 +32,7 @@ PYI063.pyi:8:15: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:24:14: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:24:14: PYI063 Use PEP 570 syntax for positional-only parameters
|
22 | def bad(__self) -> None: ... # PYI063
23 | @staticmethod
@@ -44,7 +43,7 @@ PYI063.pyi:24:14: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:25:22: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:25:22: PYI063 Use PEP 570 syntax for positional-only parameters
|
23 | @staticmethod
24 | def bad2(__self) -> None: ... # PYI063
@@ -55,7 +54,7 @@ PYI063.pyi:25:22: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:26:25: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:26:25: PYI063 Use PEP 570 syntax for positional-only parameters
|
24 | def bad2(__self) -> None: ... # PYI063
25 | def bad3(__self, __x: int) -> None: ... # PYI063
@@ -66,7 +65,7 @@ PYI063.pyi:26:25: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:28:25: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:28:25: PYI063 Use PEP 570 syntax for positional-only parameters
|
26 | def still_bad(self, __x_: int) -> None: ... # PYI063 # TODO: doesn't get raised here
27 | @staticmethod
@@ -77,7 +76,7 @@ PYI063.pyi:28:25: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:30:23: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:30:23: PYI063 Use PEP 570 syntax for positional-only parameters
|
28 | def this_is_bad_too(__x: int) -> None: ... # PYI063
29 | @classmethod
@@ -88,7 +87,7 @@ PYI063.pyi:30:23: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:52:23: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:52:23: PYI063 Use PEP 570 syntax for positional-only parameters
|
50 | class Metaclass(type):
51 | @classmethod
@@ -99,7 +98,7 @@ PYI063.pyi:52:23: PYI063 Use PEP 570 syntax for positional-only arguments
|
= help: Add `/` to function signature
PYI063.pyi:56:26: PYI063 Use PEP 570 syntax for positional-only arguments
PYI063.pyi:56:26: PYI063 Use PEP 570 syntax for positional-only parameters
|
54 | class Metaclass2(type):
55 | @classmethod

View File

@@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
snapshot_kind: text
---
PYI066.pyi:3:4: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
PYI066.pyi:3:4: PYI066 Put branches for newer Python versions first when branching on `sys.version_info` comparisons
|
1 | import sys
2 |
@@ -12,7 +11,7 @@ PYI066.pyi:3:4: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` c
5 | else:
|
PYI066.pyi:8:4: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
PYI066.pyi:8:4: PYI066 Put branches for newer Python versions first when branching on `sys.version_info` comparisons
|
6 | def foo(x, *, bar=True): ...
7 |
@@ -22,7 +21,7 @@ PYI066.pyi:8:4: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` c
10 | elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
|
PYI066.pyi:10:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
PYI066.pyi:10:6: PYI066 Put branches for newer Python versions first when branching on `sys.version_info` comparisons
|
8 | if sys.version_info < (3, 8): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 8)"
9 | def bar(x): ...
@@ -32,7 +31,7 @@ PYI066.pyi:10:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info`
12 | elif sys.version_info < (3, 11): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
PYI066.pyi:12:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
PYI066.pyi:12:6: PYI066 Put branches for newer Python versions first when branching on `sys.version_info` comparisons
|
10 | elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
11 | def bar(x, *, bar=True): ...
@@ -42,7 +41,7 @@ PYI066.pyi:12:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info`
14 | else:
|
PYI066.pyi:20:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
PYI066.pyi:20:6: PYI066 Put branches for newer Python versions first when branching on `sys.version_info` comparisons
|
18 | if sys.version_info >= (3, 5):
19 | ...

View File

@@ -45,18 +45,6 @@ mod tests {
Settings::default(),
"PT003"
)]
#[test_case(
Rule::PytestMissingFixtureNameUnderscore,
Path::new("PT004.py"),
Settings::default(),
"PT004"
)]
#[test_case(
Rule::PytestIncorrectFixtureNameUnderscore,
Path::new("PT005.py"),
Settings::default(),
"PT005"
)]
#[test_case(
Rule::PytestParametrizeNamesWrongType,
Path::new("PT006.py"),

View File

@@ -1,7 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::name::UnqualifiedName;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@@ -167,8 +166,8 @@ impl AlwaysFixableViolation for PytestExtraneousScopeFunction {
}
}
/// ## Deprecation
/// Marking fixtures that do not return a value with an underscore
/// ## Removal
/// This rule has been removed because marking fixtures that do not return a value with an underscore
/// isn't a practice recommended by the pytest community.
///
/// ## What it does
@@ -216,20 +215,22 @@ impl AlwaysFixableViolation for PytestExtraneousScopeFunction {
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[violation]
pub struct PytestMissingFixtureNameUnderscore {
function: String,
}
#[deprecated(note = "PT004 has been removed")]
pub struct PytestMissingFixtureNameUnderscore;
#[allow(deprecated)]
impl Violation for PytestMissingFixtureNameUnderscore {
#[derive_message_formats]
fn message(&self) -> String {
let PytestMissingFixtureNameUnderscore { function } = self;
format!("Fixture `{function}` does not return anything, add leading underscore")
unreachable!("PT004 has been removed");
}
fn message_formats() -> &'static [&'static str] {
&["Fixture `{function}` does not return anything, add leading underscore"]
}
}
/// ## Deprecation
/// Marking fixtures that do not return a value with an underscore
/// ## Removal
/// This rule has been removed because marking fixtures that do not return a value with an underscore
/// isn't a practice recommended by the pytest community.
///
/// ## What it does
@@ -279,15 +280,17 @@ impl Violation for PytestMissingFixtureNameUnderscore {
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[violation]
pub struct PytestIncorrectFixtureNameUnderscore {
function: String,
}
#[deprecated(note = "PT005 has been removed")]
pub struct PytestIncorrectFixtureNameUnderscore;
#[allow(deprecated)]
impl Violation for PytestIncorrectFixtureNameUnderscore {
#[derive_message_formats]
fn message(&self) -> String {
let PytestIncorrectFixtureNameUnderscore { function } = self;
format!("Fixture `{function}` returns a value, remove leading underscore")
unreachable!("PT005 has been removed");
}
fn message_formats() -> &'static [&'static str] {
&["Fixture `{function}` returns a value, remove leading underscore"]
}
}
@@ -749,43 +752,14 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D
}
}
/// PT004, PT005, PT022
fn check_fixture_returns(
checker: &mut Checker,
stmt: &Stmt,
name: &str,
body: &[Stmt],
returns: Option<&Expr>,
) {
/// PT022
fn check_fixture_returns(checker: &mut Checker, name: &str, body: &[Stmt], returns: Option<&Expr>) {
let mut visitor = SkipFunctionsVisitor::default();
for stmt in body {
visitor.visit_stmt(stmt);
}
if checker.enabled(Rule::PytestIncorrectFixtureNameUnderscore)
&& visitor.has_return_with_value
&& name.starts_with('_')
{
checker.diagnostics.push(Diagnostic::new(
PytestIncorrectFixtureNameUnderscore {
function: name.to_string(),
},
stmt.identifier(),
));
} else if checker.enabled(Rule::PytestMissingFixtureNameUnderscore)
&& !visitor.has_return_with_value
&& !visitor.has_yield_from
&& !name.starts_with('_')
{
checker.diagnostics.push(Diagnostic::new(
PytestMissingFixtureNameUnderscore {
function: name.to_string(),
},
stmt.identifier(),
));
}
if checker.enabled(Rule::PytestUselessYieldFixture) {
let Some(stmt) = body.last() else {
return;
@@ -904,7 +878,6 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Decorator]) {
pub(crate) fn fixture(
checker: &mut Checker,
stmt: &Stmt,
name: &str,
parameters: &Parameters,
returns: Option<&Expr>,
@@ -924,12 +897,10 @@ pub(crate) fn fixture(
check_fixture_decorator_name(checker, decorator);
}
if (checker.enabled(Rule::PytestMissingFixtureNameUnderscore)
|| checker.enabled(Rule::PytestIncorrectFixtureNameUnderscore)
|| checker.enabled(Rule::PytestUselessYieldFixture))
if checker.enabled(Rule::PytestUselessYieldFixture)
&& !is_abstract(decorators, checker.semantic())
{
check_fixture_returns(checker, stmt, name, body, returns);
check_fixture_returns(checker, name, body, returns);
}
if checker.enabled(Rule::PytestFixtureFinalizerCallback) {

View File

@@ -1,20 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT004.py:51:5: PT004 Fixture `patch_something` does not return anything, add leading underscore
|
50 | @pytest.fixture()
51 | def patch_something(mocker): # Error simple
| ^^^^^^^^^^^^^^^ PT004
52 | mocker.patch("some.thing")
|
PT004.py:56:5: PT004 Fixture `activate_context` does not return anything, add leading underscore
|
55 | @pytest.fixture()
56 | def activate_context(): # Error with yield
| ^^^^^^^^^^^^^^^^ PT004
57 | with context:
58 | yield
|

View File

@@ -1,29 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT005.py:41:5: PT005 Fixture `_my_fixture` returns a value, remove leading underscore
|
40 | @pytest.fixture()
41 | def _my_fixture(mocker): # Error with return
| ^^^^^^^^^^^ PT005
42 | return 0
|
PT005.py:46:5: PT005 Fixture `_activate_context` returns a value, remove leading underscore
|
45 | @pytest.fixture()
46 | def _activate_context(): # Error with yield
| ^^^^^^^^^^^^^^^^^ PT005
47 | with get_context() as context:
48 | yield context
|
PT005.py:52:5: PT005 Fixture `_activate_context` returns a value, remove leading underscore
|
51 | @pytest.fixture()
52 | def _activate_context(): # Error with conditional yield from
| ^^^^^^^^^^^^^^^^^ PT005
53 | if some_condition:
54 | with get_context() as context:
|

View File

@@ -16,29 +16,29 @@ mod tests {
use crate::test::{test_path, test_snippet};
use crate::{assert_messages, settings};
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_1.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_10.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_11.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_12.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_13.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_14.pyi"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_15.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_16.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_17.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_2.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_3.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_4.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_5.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_6.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_7.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"))]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TC005.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_1.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_10.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_11.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_12.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_13.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_14.pyi"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_15.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_16.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_17.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_2.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_3.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_4.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_5.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_6.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_7.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_9.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TCH010_1.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TCH010_2.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_1.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_2.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TC001.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TC003.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("kw_only.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
@@ -46,7 +46,7 @@ mod tests {
Rule::TypingOnlyStandardLibraryImport,
Path::new("singledispatchmethod.py")
)]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TC002.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("singledispatch.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]

View File

@@ -46,7 +46,7 @@ impl AlwaysFixableViolation for EmptyTypeCheckingBlock {
}
}
/// TCH005
/// TC005
pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtIf) {
if !typing::is_type_checking_block(stmt, checker.semantic()) {
return;

View File

@@ -96,7 +96,7 @@ enum Action {
Ignore,
}
/// TCH004
/// TC004
pub(crate) fn runtime_import_in_type_checking_block(
checker: &Checker,
scope: &Scope,

View File

@@ -51,7 +51,7 @@ impl Violation for RuntimeStringUnion {
}
}
/// TCH006
/// TC006
pub(crate) fn runtime_string_union(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().in_type_definition() {
return;

View File

@@ -243,7 +243,7 @@ impl Violation for TypingOnlyStandardLibraryImport {
}
}
/// TCH001, TCH002, TCH003
/// TC001, TC002, TC003
pub(crate) fn typing_only_runtime_import(
checker: &Checker,
scope: &Scope,

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
TCH005.py:4:5: TCH005 [*] Found empty type-checking block
TC005.py:4:5: TC005 [*] Found empty type-checking block
|
3 | if TYPE_CHECKING:
4 | pass # TCH005
| ^^^^ TCH005
4 | pass # TC005
| ^^^^ TC005
|
= help: Delete empty type-checking block
@@ -14,55 +14,55 @@ TCH005.py:4:5: TCH005 [*] Found empty type-checking block
1 1 | from typing import TYPE_CHECKING, List
2 2 |
3 |-if TYPE_CHECKING:
4 |- pass # TCH005
4 |- pass # TC005
5 3 |
6 4 |
7 5 | if False:
TCH005.py:8:5: TCH005 [*] Found empty type-checking block
TC005.py:8:5: TC005 [*] Found empty type-checking block
|
7 | if False:
8 | pass # TCH005
| ^^^^ TCH005
8 | pass # TC005
| ^^^^ TC005
9 |
10 | if 0:
|
= help: Delete empty type-checking block
Safe fix
4 4 | pass # TCH005
4 4 | pass # TC005
5 5 |
6 6 |
7 |-if False:
8 |- pass # TCH005
8 |- pass # TC005
9 7 |
10 8 | if 0:
11 9 | pass # TCH005
11 9 | pass # TC005
TCH005.py:11:5: TCH005 [*] Found empty type-checking block
TC005.py:11:5: TC005 [*] Found empty type-checking block
|
10 | if 0:
11 | pass # TCH005
| ^^^^ TCH005
11 | pass # TC005
| ^^^^ TC005
|
= help: Delete empty type-checking block
Safe fix
7 7 | if False:
8 8 | pass # TCH005
8 8 | pass # TC005
9 9 |
10 |-if 0:
11 |- pass # TCH005
11 |- pass # TC005
12 10 |
13 11 |
14 12 | def example():
TCH005.py:16:9: TCH005 [*] Found empty type-checking block
TC005.py:16:9: TC005 [*] Found empty type-checking block
|
14 | def example():
15 | if TYPE_CHECKING:
16 | pass # TCH005
| ^^^^ TCH005
16 | pass # TC005
| ^^^^ TC005
17 | return
|
= help: Delete empty type-checking block
@@ -72,17 +72,17 @@ TCH005.py:16:9: TCH005 [*] Found empty type-checking block
13 13 |
14 14 | def example():
15 |- if TYPE_CHECKING:
16 |- pass # TCH005
16 |- pass # TC005
17 15 | return
18 16 |
19 17 |
TCH005.py:22:9: TCH005 [*] Found empty type-checking block
TC005.py:22:9: TC005 [*] Found empty type-checking block
|
20 | class Test:
21 | if TYPE_CHECKING:
22 | pass # TCH005
| ^^^^ TCH005
22 | pass # TC005
| ^^^^ TC005
23 | x = 2
|
= help: Delete empty type-checking block
@@ -92,16 +92,16 @@ TCH005.py:22:9: TCH005 [*] Found empty type-checking block
19 19 |
20 20 | class Test:
21 |- if TYPE_CHECKING:
22 |- pass # TCH005
22 |- pass # TC005
23 21 | x = 2
24 22 |
25 23 |
TCH005.py:45:5: TCH005 [*] Found empty type-checking block
TC005.py:45:5: TC005 [*] Found empty type-checking block
|
44 | if TYPE_CHECKING:
45 | pass # TCH005
| ^^^^ TCH005
45 | pass # TC005
| ^^^^ TC005
46 |
47 | # https://github.com/astral-sh/ruff/issues/11368
|
@@ -112,7 +112,7 @@ TCH005.py:45:5: TCH005 [*] Found empty type-checking block
42 42 | from typing_extensions import TYPE_CHECKING
43 43 |
44 |-if TYPE_CHECKING:
45 |- pass # TCH005
45 |- pass # TC005
46 44 |
47 45 | # https://github.com/astral-sh/ruff/issues/11368
48 46 | if TYPE_CHECKING:

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
exempt_modules.py:14:12: TCH002 [*] Move third-party import `flask` into a type-checking block
exempt_modules.py:14:12: TC002 [*] Move third-party import `flask` into a type-checking block
|
13 | def f():
14 | import flask
| ^^^^^ TCH002
| ^^^^^ TC002
15 |
16 | x: flask
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
<filename>:5:5: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
<filename>:5:5: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
4 | from pandas import (
5 | DataFrame, # DataFrame
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
6 | Series, # Series
7 | )
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
<filename>:7:5: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
<filename>:7:5: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
6 | from pandas import (
7 | DataFrame, # DataFrame
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
8 | Series, # Series
9 | )
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
<filename>:7:5: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
<filename>:7:5: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
6 | from pandas import (
7 | DataFrame, # DataFrame
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
8 | Series, # Series
9 | )
|
@@ -30,12 +30,12 @@ snapshot_kind: text
11 13 | def f(x: DataFrame, y: Series):
12 14 | pass
<filename>:8:5: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
<filename>:8:5: TC002 [*] Move third-party import `pandas.Series` into a type-checking block
|
6 | from pandas import (
7 | DataFrame, # DataFrame
8 | Series, # Series
| ^^^^^^ TCH002
| ^^^^^^ TC002
9 | )
|
= help: Move into type-checking block

View File

@@ -2,12 +2,12 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
<filename>:6:8: TCH003 [*] Move standard library import `os` into a type-checking block
<filename>:6:8: TC003 [*] Move standard library import `os` into a type-checking block
|
4 | from typing import TYPE_CHECKING
5 |
6 | import os, pandas
| ^^ TCH003
| ^^ TC003
7 |
8 | def f(x: os, y: pandas):
|
@@ -26,12 +26,12 @@ snapshot_kind: text
8 11 | def f(x: os, y: pandas):
9 12 | pass
<filename>:6:12: TCH002 [*] Move third-party import `pandas` into a type-checking block
<filename>:6:12: TC002 [*] Move third-party import `pandas` into a type-checking block
|
4 | from typing import TYPE_CHECKING
5 |
6 | import os, pandas
| ^^^^^^ TCH002
| ^^^^^^ TC002
7 |
8 | def f(x: os, y: pandas):
|

View File

@@ -2,12 +2,12 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
<filename>:6:8: TCH003 [*] Move standard library import `os` into a type-checking block
<filename>:6:8: TC003 [*] Move standard library import `os` into a type-checking block
|
4 | from typing import TYPE_CHECKING
5 |
6 | import os, sys
| ^^ TCH003
| ^^ TC003
7 |
8 | def f(x: os, y: sys):
|
@@ -25,12 +25,12 @@ snapshot_kind: text
8 10 | def f(x: os, y: sys):
9 11 | pass
<filename>:6:12: TCH003 [*] Move standard library import `sys` into a type-checking block
<filename>:6:12: TC003 [*] Move standard library import `sys` into a type-checking block
|
4 | from typing import TYPE_CHECKING
5 |
6 | import os, sys
| ^^^ TCH003
| ^^^ TC003
7 |
8 | def f(x: os, y: sys):
|

View File

@@ -2,12 +2,12 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
<filename>:4:18: TCH002 [*] Move third-party import `pandas` into a type-checking block
<filename>:4:18: TC002 [*] Move third-party import `pandas` into a type-checking block
|
2 | from __future__ import annotations
3 |
4 | import pandas as pd
| ^^ TCH002
| ^^ TC002
5 |
6 | def f(x: pd.DataFrame):
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
quote.py:57:28: TCH004 [*] Quote references to `pandas.DataFrame`. Import is in a type-checking block.
quote.py:57:28: TC004 [*] Quote references to `pandas.DataFrame`. Import is in a type-checking block.
|
56 | if TYPE_CHECKING:
57 | from pandas import DataFrame
| ^^^^^^^^^ TCH004
| ^^^^^^^^^ TC004
58 |
59 | def func(value: DataFrame):
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
quote.py:2:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
quote.py:2:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
1 | def f():
2 | from pandas import DataFrame
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
3 |
4 | def baz() -> DataFrame:
|
@@ -26,11 +26,11 @@ quote.py:2:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type
6 9 |
7 10 |
quote.py:9:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
quote.py:9:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
8 | def f():
9 | from pandas import DataFrame
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
10 |
11 | def baz() -> DataFrame[int]:
|
@@ -56,11 +56,11 @@ quote.py:9:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type
13 16 |
14 17 |
quote.py:16:22: TCH002 [*] Move third-party import `pandas` into a type-checking block
quote.py:16:22: TC002 [*] Move third-party import `pandas` into a type-checking block
|
15 | def f():
16 | import pandas as pd
| ^^ TCH002
| ^^ TC002
17 |
18 | def baz() -> pd.DataFrame:
|
@@ -86,11 +86,11 @@ quote.py:16:22: TCH002 [*] Move third-party import `pandas` into a type-checking
20 23 |
21 24 |
quote.py:23:22: TCH002 [*] Move third-party import `pandas` into a type-checking block
quote.py:23:22: TC002 [*] Move third-party import `pandas` into a type-checking block
|
22 | def f():
23 | import pandas as pd
| ^^ TCH002
| ^^ TC002
24 |
25 | def baz() -> pd.DataFrame.Extra:
|
@@ -116,11 +116,11 @@ quote.py:23:22: TCH002 [*] Move third-party import `pandas` into a type-checking
27 30 |
28 31 |
quote.py:30:22: TCH002 [*] Move third-party import `pandas` into a type-checking block
quote.py:30:22: TC002 [*] Move third-party import `pandas` into a type-checking block
|
29 | def f():
30 | import pandas as pd
| ^^ TCH002
| ^^ TC002
31 |
32 | def baz() -> pd.DataFrame | int:
|
@@ -146,11 +146,11 @@ quote.py:30:22: TCH002 [*] Move third-party import `pandas` into a type-checking
34 37 |
35 38 |
quote.py:38:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
quote.py:38:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
37 | def f():
38 | from pandas import DataFrame
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
39 |
40 | def baz() -> DataFrame():
|
@@ -176,12 +176,12 @@ quote.py:38:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a typ
42 45 |
43 46 |
quote.py:47:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
quote.py:47:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
45 | from typing import Literal
46 |
47 | from pandas import DataFrame
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
48 |
49 | def baz() -> DataFrame[Literal["int"]]:
|
@@ -207,11 +207,11 @@ quote.py:47:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a typ
51 54 |
52 55 |
quote.py:64:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
quote.py:64:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
63 | def f():
64 | from pandas import DataFrame, Series
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
65 |
66 | def baz() -> DataFrame | Series:
|
@@ -237,11 +237,11 @@ quote.py:64:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a typ
68 71 |
69 72 |
quote.py:64:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
quote.py:64:35: TC002 [*] Move third-party import `pandas.Series` into a type-checking block
|
63 | def f():
64 | from pandas import DataFrame, Series
| ^^^^^^ TCH002
| ^^^^^^ TC002
65 |
66 | def baz() -> DataFrame | Series:
|
@@ -267,11 +267,11 @@ quote.py:64:35: TCH002 [*] Move third-party import `pandas.Series` into a type-c
68 71 |
69 72 |
quote.py:71:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
quote.py:71:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
70 | def f():
71 | from pandas import DataFrame, Series
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
72 |
73 | def baz() -> (
|
@@ -310,11 +310,11 @@ quote.py:71:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a typ
86 86 |
87 87 |
quote.py:71:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
quote.py:71:35: TC002 [*] Move third-party import `pandas.Series` into a type-checking block
|
70 | def f():
71 | from pandas import DataFrame, Series
| ^^^^^^ TCH002
| ^^^^^^ TC002
72 |
73 | def baz() -> (
|
@@ -353,11 +353,11 @@ quote.py:71:35: TCH002 [*] Move third-party import `pandas.Series` into a type-c
86 86 |
87 87 |
quote.py:89:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
quote.py:89:24: TC002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
88 | def f():
89 | from pandas import DataFrame, Series
| ^^^^^^^^^ TCH002
| ^^^^^^^^^ TC002
90 |
91 | def func(self) -> DataFrame | list[Series]:
|
@@ -383,11 +383,11 @@ quote.py:89:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a typ
93 96 |
94 97 |
quote.py:89:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
quote.py:89:35: TC002 [*] Move third-party import `pandas.Series` into a type-checking block
|
88 | def f():
89 | from pandas import DataFrame, Series
| ^^^^^^ TCH002
| ^^^^^^ TC002
90 |
91 | def func(self) -> DataFrame | list[Series]:
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
quote2.py:2:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:2:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
1 | def f():
2 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
3 |
4 | def test_remove_inner_quotes_double(self, user: AbstractBaseUser["int"]):
|
@@ -26,11 +26,11 @@ quote2.py:2:44: TCH002 [*] Move third-party import `django.contrib.auth.models.A
6 9 |
7 10 |
quote2.py:9:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:9:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
8 | def f():
9 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
10 |
11 | def test_remove_inner_quotes_single(self, user: AbstractBaseUser['int']):
|
@@ -56,11 +56,11 @@ quote2.py:9:44: TCH002 [*] Move third-party import `django.contrib.auth.models.A
13 16 |
14 17 |
quote2.py:16:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:16:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
15 | def f():
16 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
17 |
18 | def test_remove_inner_quotes_mixed(self, user: AbstractBaseUser['int', "str"]):
|
@@ -86,12 +86,12 @@ quote2.py:16:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
20 23 |
21 24 |
quote2.py:25:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:25:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
23 | from typing import Annotated, Literal
24 |
25 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
26 |
27 | def test_literal_annotation_args_contain_quotes(self, type1: AbstractBaseUser[Literal["user", "admin"], Annotated["int", "1", 2]]):
|
@@ -117,12 +117,12 @@ quote2.py:25:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
29 32 |
30 33 |
quote2.py:34:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:34:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
32 | from typing import Literal
33 |
34 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
35 |
36 | def test_union_contain_inner_quotes(self, type1: AbstractBaseUser["int" | Literal["int"]]):
|
@@ -148,12 +148,12 @@ quote2.py:34:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
38 41 |
39 42 |
quote2.py:43:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:43:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
41 | from typing import Literal
42 |
43 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
44 |
45 | def test_inner_literal_mixed_quotes(user: AbstractBaseUser[Literal['user', "admin"]]):
|
@@ -179,12 +179,12 @@ quote2.py:43:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
47 50 |
48 51 |
quote2.py:52:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:52:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
50 | from typing import Literal
51 |
52 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
53 |
54 | def test_inner_literal_single_quote(user: AbstractBaseUser[Literal['int'], str]):
|
@@ -210,12 +210,12 @@ quote2.py:52:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
56 59 |
57 60 |
quote2.py:61:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:61:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
59 | from typing import Literal
60 |
61 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
62 |
63 | def test_mixed_quotes_literal(user: AbstractBaseUser[Literal['user'], "int"]):
|
@@ -241,12 +241,12 @@ quote2.py:61:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
65 68 |
66 69 |
quote2.py:70:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote2.py:70:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
68 | from typing import Annotated, Literal
69 |
70 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
71 |
72 | def test_annotated_literal_mixed_quotes(user: AbstractBaseUser[Annotated[str, "max_length=20", Literal['user', "admin"]]]):
|

View File

@@ -1,12 +1,12 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
quote3.py:4:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote3.py:4:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
2 | from typing import Literal, Union
3 |
4 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
5 |
6 | def test_union_literal_mixed_quotes(user: AbstractBaseUser[Union[Literal['active', "inactive"], str]]):
|
@@ -28,12 +28,12 @@ quote3.py:4:44: TCH002 [*] Move third-party import `django.contrib.auth.models.A
8 11 |
9 12 |
quote3.py:13:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote3.py:13:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
11 | from typing import Callable, Literal
12 |
13 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
14 |
15 | def test_callable_literal_mixed_quotes(callable_fn: AbstractBaseUser[Callable[["int", Literal['admin', "user"]], 'bool']]):
|
@@ -59,12 +59,12 @@ quote3.py:13:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
17 20 |
18 21 |
quote3.py:22:44: TCH002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
quote3.py:22:44: TC002 [*] Move third-party import `django.contrib.auth.models.AbstractBaseUser` into a type-checking block
|
20 | from typing import Annotated, Callable, Literal
21 |
22 | from django.contrib.auth.models import AbstractBaseUser
| ^^^^^^^^^^^^^^^^ TCH002
| ^^^^^^^^^^^^^^^^ TC002
23 |
24 | def test_callable_annotated_literal(callable_fn: AbstractBaseUser[Callable[[int, Annotated[str, Literal['active', "inactive"]]], bool]]):
|
@@ -90,12 +90,12 @@ quote3.py:22:44: TCH002 [*] Move third-party import `django.contrib.auth.models.
26 29 |
27 30 |
quote3.py:31:37: TCH002 [*] Move third-party import `django.contrib.auth.models` into a type-checking block
quote3.py:31:37: TC002 [*] Move third-party import `django.contrib.auth.models` into a type-checking block
|
29 | from typing import literal
30 |
31 | from django.contrib.auth import models
| ^^^^^^ TCH002
| ^^^^^^ TC002
32 |
33 | def test_attribute(arg: models.AbstractBaseUser["int"]):
|
@@ -121,12 +121,12 @@ quote3.py:31:37: TCH002 [*] Move third-party import `django.contrib.auth.models`
35 38 |
36 39 |
quote3.py:40:37: TCH002 [*] Move third-party import `django.contrib.auth.models` into a type-checking block
quote3.py:40:37: TC002 [*] Move third-party import `django.contrib.auth.models` into a type-checking block
|
38 | from typing import Literal
39 |
40 | from django.contrib.auth import models
| ^^^^^^ TCH002
| ^^^^^^ TC002
41 |
42 | def test_attribute_typing_literal(arg: models.AbstractBaseUser[Literal["admin"]]):
|
@@ -152,23 +152,23 @@ quote3.py:40:37: TCH002 [*] Move third-party import `django.contrib.auth.models`
44 47 |
45 48 |
quote3.py:59:29: TCH002 Move third-party import `third_party.Type` into a type-checking block
quote3.py:59:29: TC002 Move third-party import `third_party.Type` into a type-checking block
|
57 | def f():
58 | from typing import Literal
59 | from third_party import Type
| ^^^^ TCH002
| ^^^^ TC002
60 |
61 | def test_string_contains_opposite_quote_do_not_fix(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
|
= help: Move into type-checking block
quote3.py:67:29: TCH002 Move third-party import `third_party.Type` into a type-checking block
quote3.py:67:29: TC002 Move third-party import `third_party.Type` into a type-checking block
|
65 | def f():
66 | from typing import Literal
67 | from third_party import Type
| ^^^^ TCH002
| ^^^^ TC002
68 |
69 | def test_quote_contains_backslash(self, type1: Type[Literal["\n"]], type2: Type[Literal["\""]]):
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
TCH004_1.py:4:26: TCH004 [*] Move import `datetime.datetime` out of type-checking block. Import is used for more than type hinting.
TC004_1.py:4:26: TC004 [*] Move import `datetime.datetime` out of type-checking block. Import is used for more than type hinting.
|
3 | if TYPE_CHECKING:
4 | from datetime import datetime
| ^^^^^^^^ TCH004
| ^^^^^^^^ TC004
5 | x = datetime
|
= help: Move out of type-checking block

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
TCH004_11.py:4:24: TCH004 [*] Move import `typing.List` out of type-checking block. Import is used for more than type hinting.
TC004_11.py:4:24: TC004 [*] Move import `typing.List` out of type-checking block. Import is used for more than type hinting.
|
3 | if TYPE_CHECKING:
4 | from typing import List
| ^^^^ TCH004
| ^^^^ TC004
5 |
6 | __all__ = ("List",)
|

View File

@@ -2,11 +2,11 @@
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
TCH004_12.py:6:33: TCH004 [*] Move import `collections.abc.Callable` out of type-checking block. Import is used for more than type hinting.
TC004_12.py:6:33: TC004 [*] Move import `collections.abc.Callable` out of type-checking block. Import is used for more than type hinting.
|
5 | if TYPE_CHECKING:
6 | from collections.abc import Callable
| ^^^^^^^^ TCH004
| ^^^^^^^^ TC004
7 |
8 | AnyCallable: TypeAlias = Callable[..., Any]
|

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