Compare commits
32 Commits
david/remo
...
david/do-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
618fa922c7 | ||
|
|
42b297bf44 | ||
|
|
80b337669f | ||
|
|
1ce57edf33 | ||
|
|
1f8a74b5c6 | ||
|
|
c895b29f23 | ||
|
|
a9f4852956 | ||
|
|
c20f489484 | ||
|
|
6ae7e7ba6b | ||
|
|
c05b172266 | ||
|
|
10d23bb5e1 | ||
|
|
2d44ad2f8f | ||
|
|
2ce3aba458 | ||
|
|
b83ac5e234 | ||
|
|
c91b457044 | ||
|
|
673167a565 | ||
|
|
805d179dc0 | ||
|
|
f73ead11cb | ||
|
|
ebfb33c30b | ||
|
|
7d7237c660 | ||
|
|
542f080035 | ||
|
|
92eee816ed | ||
|
|
00c0c567dc | ||
|
|
f9688bd05c | ||
|
|
188c0dce29 | ||
|
|
c9b2bcbfb2 | ||
|
|
4e94b22815 | ||
|
|
0639da2552 | ||
|
|
caf48f4bfc | ||
|
|
71d711257a | ||
|
|
eb34d12151 | ||
|
|
56d630e303 |
8
.github/workflows/build-docker.yml
vendored
8
.github/workflows/build-docker.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
80
.github/workflows/ci.yaml
vendored
80
.github/workflows/ci.yaml
vendored
@@ -225,7 +225,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
@@ -245,21 +245,21 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -307,21 +307,21 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -340,15 +340,15 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -371,7 +371,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -423,7 +423,7 @@ jobs:
|
||||
with:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
@@ -446,7 +446,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -472,7 +472,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -506,7 +506,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
@@ -673,7 +673,7 @@ jobs:
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
workflow: "ci.yaml"
|
||||
check_artifacts: true
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- name: Fuzz
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
@@ -720,7 +720,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
@@ -743,8 +743,8 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -777,7 +777,7 @@ jobs:
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
@@ -786,7 +786,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -816,7 +816,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Run checks"
|
||||
@@ -886,7 +886,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -920,22 +920,22 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
- name: "Build benchmarks"
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark formatter lexer linter parser
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
|
||||
uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: cargo codspeed run
|
||||
@@ -955,22 +955,22 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
- name: "Build benchmarks"
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark ty
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench ty
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
|
||||
uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: cargo codspeed run
|
||||
@@ -989,14 +989,14 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
|
||||
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1004,7 +1004,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
|
||||
uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
|
||||
env:
|
||||
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
|
||||
# appear to provide much useful insight for our walltime benchmarks right now
|
||||
|
||||
4
.github/workflows/daily_fuzz.yaml
vendored
4
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,12 +34,12 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- name: Build ruff
|
||||
# A debug build means the script runs slower once it gets started,
|
||||
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
|
||||
|
||||
8
.github/workflows/mypy_primer.yaml
vendored
8
.github/workflows/mypy_primer.yaml
vendored
@@ -39,9 +39,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
@@ -82,9 +82,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
2
.github/workflows/publish-docs.yml
vendored
2
.github/workflows/publish-docs.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
6
.github/workflows/sync_typeshed.yaml
vendored
6
.github/workflows/sync_typeshed.yaml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
git config --global user.email '<>'
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -33,9 +33,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
4
.github/workflows/ty-ecosystem-report.yaml
vendored
4
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -29,9 +29,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
2
.github/workflows/typing_conformance.yaml
vendored
2
.github/workflows/typing_conformance.yaml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
path: typing
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.3
|
||||
|
||||
Released on 2025-10-02.
|
||||
|
||||
### Preview features
|
||||
|
||||
- Display diffs for `ruff format --check` and add support for different output formats ([#20443](https://github.com/astral-sh/ruff/pull/20443))
|
||||
- \[`pyflakes`\] Handle some common submodule import situations for `unused-import` (`F401`) ([#20200](https://github.com/astral-sh/ruff/pull/20200))
|
||||
- \[`ruff`\] Do not flag `%r` + `repr()` combinations (`RUF065`) ([#20600](https://github.com/astral-sh/ruff/pull/20600))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`cli`\] Add conflict between `--add-noqa` and `--diff` options ([#20642](https://github.com/astral-sh/ruff/pull/20642))
|
||||
- \[`pylint`\] Exempt required imports from `PLR0402` ([#20381](https://github.com/astral-sh/ruff/pull/20381))
|
||||
- \[`pylint`\] Fix missing `max-nested-blocks` in settings display ([#20574](https://github.com/astral-sh/ruff/pull/20574))
|
||||
- \[`pyupgrade`\] Prevent infinite loop with `I002` and `UP026` ([#20634](https://github.com/astral-sh/ruff/pull/20634))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-simplify`\] Improve help message clarity (`SIM105`) ([#20548](https://github.com/astral-sh/ruff/pull/20548))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add the *The Basics* title back to CONTRIBUTING.md ([#20624](https://github.com/astral-sh/ruff/pull/20624))
|
||||
- Fixed documentation for try_consider_else ([#20587](https://github.com/astral-sh/ruff/pull/20587))
|
||||
- \[`isort`\] Clarify dependency between `order-by-type` and `case-sensitive` settings ([#20559](https://github.com/astral-sh/ruff/pull/20559))
|
||||
- \[`pylint`\] Clarify fix safety to include left-hand hashability (`PLR6201`) ([#20518](https://github.com/astral-sh/ruff/pull/20518))
|
||||
|
||||
### Other changes
|
||||
|
||||
- \[`playground`\] Fix quick fixes for empty ranges in playground ([#20599](https://github.com/astral-sh/ruff/pull/20599))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@TaKO8Ki](https://github.com/TaKO8Ki)
|
||||
- [@ntBre](https://github.com/ntBre)
|
||||
- [@dylwil3](https://github.com/dylwil3)
|
||||
- [@MichaReiser](https://github.com/MichaReiser)
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@LilMonk](https://github.com/LilMonk)
|
||||
- [@mgiovani](https://github.com/mgiovani)
|
||||
- [@IDrokin117](https://github.com/IDrokin117)
|
||||
|
||||
## 0.13.2
|
||||
|
||||
Released on 2025-09-25.
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -2738,7 +2738,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2994,7 +2994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3348,7 +3348,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -4449,6 +4449,7 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"serde-wasm-bindgen",
|
||||
"tracing",
|
||||
"ty_ide",
|
||||
"ty_project",
|
||||
"ty_python_semantic",
|
||||
|
||||
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.13.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.13.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.13.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.13.3/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.13.2
|
||||
rev: v0.13.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -31,6 +31,7 @@ extend-ignore-re = [
|
||||
"typ",
|
||||
# TODO: Remove this once the `TYP` redirects are removed from `rule_redirects.rs`
|
||||
"TYP",
|
||||
"ntBre"
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::io::Write;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
|
||||
@@ -39,39 +40,46 @@ pub fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
let args = wild::args_os();
|
||||
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
|
||||
let args = match argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
|
||||
.context("Failed to read CLI arguments from files")
|
||||
{
|
||||
Ok(args) => args,
|
||||
Err(err) => return report_error(&err),
|
||||
};
|
||||
|
||||
let args = Args::parse_from(args);
|
||||
|
||||
match run(args) {
|
||||
Ok(code) => code.into(),
|
||||
Err(err) => {
|
||||
{
|
||||
// Exit "gracefully" on broken pipe errors.
|
||||
//
|
||||
// See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14
|
||||
for cause in err.chain() {
|
||||
if let Some(ioerr) = cause.downcast_ref::<std::io::Error>() {
|
||||
if ioerr.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
return ExitCode::from(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
|
||||
// This communicates that this isn't a linter error but ruff itself hard-errored for
|
||||
// some reason (e.g. failed to resolve the configuration)
|
||||
writeln!(stderr, "{}", "ruff failed".red().bold()).ok();
|
||||
// Currently we generally only see one error, but e.g. with io errors when resolving
|
||||
// the configuration it is help to chain errors ("resolving configuration failed" ->
|
||||
// "failed to read file: subdir/pyproject.toml")
|
||||
for cause in err.chain() {
|
||||
writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
|
||||
}
|
||||
}
|
||||
ExitStatus::Error.into()
|
||||
}
|
||||
Err(err) => report_error(&err),
|
||||
}
|
||||
}
|
||||
|
||||
fn report_error(err: &anyhow::Error) -> ExitCode {
|
||||
{
|
||||
// Exit "gracefully" on broken pipe errors.
|
||||
//
|
||||
// See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14
|
||||
for cause in err.chain() {
|
||||
if let Some(ioerr) = cause.downcast_ref::<std::io::Error>() {
|
||||
if ioerr.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
return ExitCode::from(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
|
||||
// This communicates that this isn't a linter error but ruff itself hard-errored for
|
||||
// some reason (e.g. failed to resolve the configuration)
|
||||
writeln!(stderr, "{}", "ruff failed".red().bold()).ok();
|
||||
// Currently we generally only see one error, but e.g. with io errors when resolving
|
||||
// the configuration it is help to chain errors ("resolving configuration failed" ->
|
||||
// "failed to read file: subdir/pyproject.toml")
|
||||
for cause in err.chain() {
|
||||
writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
|
||||
}
|
||||
}
|
||||
ExitStatus::Error.into()
|
||||
}
|
||||
|
||||
@@ -1688,6 +1688,27 @@ fn check_input_from_argfile() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Regression test for https://github.com/astral-sh/ruff/issues/20655
|
||||
fn missing_argfile_reports_error() {
|
||||
let mut cmd = RuffCheck::default().filename("@!.txt").build();
|
||||
insta::with_settings!({filters => vec![
|
||||
("The system cannot find the file specified.", "No such file or directory")
|
||||
]}, {
|
||||
assert_cmd_snapshot!(cmd, @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Failed to read CLI arguments from files
|
||||
Cause: failed to open file `!.txt`
|
||||
Cause: No such file or directory (os error 2)
|
||||
");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_hints_hidden_unsafe_fixes() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
|
||||
@@ -117,7 +117,7 @@ static COLOUR_SCIENCE: std::sync::LazyLock<Benchmark<'static>> = std::sync::Lazy
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY310,
|
||||
},
|
||||
500,
|
||||
600,
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -95,7 +95,10 @@ impl FileRoots {
|
||||
self.by_path.insert(route.clone(), root).unwrap();
|
||||
|
||||
// Insert a path that matches all subdirectories and files
|
||||
route.push_str("/{*filepath}");
|
||||
if !route.ends_with("/") {
|
||||
route.push('/');
|
||||
}
|
||||
route.push_str("{*filepath}");
|
||||
|
||||
self.by_path.insert(route, root).unwrap();
|
||||
self.roots.push(root);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
21
crates/ruff_linter/resources/test/fixtures/flake8_annotations/shadowed_builtins.py
vendored
Normal file
21
crates/ruff_linter/resources/test/fixtures/flake8_annotations/shadowed_builtins.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
from collections import UserString as str
|
||||
from typing import override
|
||||
|
||||
def foo():
|
||||
return "!"
|
||||
|
||||
def _foo():
|
||||
return "!"
|
||||
|
||||
class C:
|
||||
@override
|
||||
def __str__(self):
|
||||
return "!"
|
||||
|
||||
@staticmethod
|
||||
def foo():
|
||||
return "!"
|
||||
|
||||
@classmethod
|
||||
def bar(cls):
|
||||
return "!"
|
||||
26
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_9.py
vendored
Normal file
26
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_9.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
def f1(x=([],)):
|
||||
print(x)
|
||||
|
||||
|
||||
def f2(x=(x for x in "x")):
|
||||
print(x)
|
||||
|
||||
|
||||
def f3(x=((x for x in "x"),)):
|
||||
print(x)
|
||||
|
||||
|
||||
def f4(x=(z := [1, ])):
|
||||
print(x)
|
||||
|
||||
|
||||
def f5(x=([1, ])):
|
||||
print(x)
|
||||
|
||||
|
||||
def w1(x=(1,)):
|
||||
print(x)
|
||||
|
||||
|
||||
def w2(x=(z := 3)):
|
||||
print(x)
|
||||
@@ -0,0 +1,91 @@
|
||||
import asyncio
|
||||
import collections
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import shelve
|
||||
import types
|
||||
import weakref
|
||||
from collections.abc import (
|
||||
AsyncGenerator,
|
||||
AsyncIterable,
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
ByteString,
|
||||
Callable,
|
||||
Collection,
|
||||
Container,
|
||||
Coroutine,
|
||||
Generator,
|
||||
Iterable,
|
||||
Iterator,
|
||||
ItemsView,
|
||||
KeysView,
|
||||
Mapping,
|
||||
MappingView,
|
||||
MutableMapping,
|
||||
MutableSequence,
|
||||
MutableSet,
|
||||
Reversible,
|
||||
Sequence,
|
||||
Set,
|
||||
ValuesView,
|
||||
)
|
||||
|
||||
|
||||
def takes_preview_generics(
|
||||
future: asyncio.Future[int],
|
||||
task: asyncio.Task[str],
|
||||
deque_object: collections.deque[int],
|
||||
defaultdict_object: collections.defaultdict[str, int],
|
||||
ordered_dict: collections.OrderedDict[str, int],
|
||||
counter_obj: collections.Counter[str],
|
||||
chain_map: collections.ChainMap[str, int],
|
||||
context_manager: contextlib.AbstractContextManager[str],
|
||||
async_context_manager: contextlib.AbstractAsyncContextManager[int],
|
||||
dataclass_field: dataclasses.Field[int],
|
||||
cached_prop: functools.cached_property[int],
|
||||
partial_method: functools.partialmethod[int],
|
||||
path_like: os.PathLike[str],
|
||||
lifo_queue: queue.LifoQueue[int],
|
||||
regular_queue: queue.Queue[int],
|
||||
priority_queue: queue.PriorityQueue[int],
|
||||
simple_queue: queue.SimpleQueue[int],
|
||||
regex_pattern: re.Pattern[str],
|
||||
regex_match: re.Match[str],
|
||||
bsd_db_shelf: shelve.BsdDbShelf[str, int],
|
||||
db_filename_shelf: shelve.DbfilenameShelf[str, int],
|
||||
shelf_obj: shelve.Shelf[str, int],
|
||||
mapping_proxy: types.MappingProxyType[str, int],
|
||||
weak_key_dict: weakref.WeakKeyDictionary[object, int],
|
||||
weak_method: weakref.WeakMethod[int],
|
||||
weak_set: weakref.WeakSet[int],
|
||||
weak_value_dict: weakref.WeakValueDictionary[object, int],
|
||||
awaitable: Awaitable[int],
|
||||
coroutine: Coroutine[int, None, str],
|
||||
async_iterable: AsyncIterable[int],
|
||||
async_iterator: AsyncIterator[int],
|
||||
async_generator: AsyncGenerator[int, None],
|
||||
iterable: Iterable[int],
|
||||
iterator: Iterator[int],
|
||||
generator: Generator[int, None, None],
|
||||
reversible: Reversible[int],
|
||||
container: Container[int],
|
||||
collection: Collection[int],
|
||||
callable_obj: Callable[[int], str],
|
||||
set_obj: Set[int],
|
||||
mutable_set: MutableSet[int],
|
||||
mapping: Mapping[str, int],
|
||||
mutable_mapping: MutableMapping[str, int],
|
||||
sequence: Sequence[int],
|
||||
mutable_sequence: MutableSequence[int],
|
||||
byte_string: ByteString[int],
|
||||
mapping_view: MappingView[str, int],
|
||||
keys_view: KeysView[str],
|
||||
items_view: ItemsView[str, int],
|
||||
values_view: ValuesView[int],
|
||||
) -> None:
|
||||
...
|
||||
@@ -23,3 +23,9 @@ nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
def create_file_public_url(url, filename):
|
||||
return''.join([url, filename])
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/19837
|
||||
nok10 = "".join((foo, '"'))
|
||||
nok11 = ''.join((foo, "'"))
|
||||
nok12 = ''.join([foo, "'", '"'])
|
||||
nok13 = "".join([foo, "'", '"'])
|
||||
|
||||
@@ -8,7 +8,8 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::{
|
||||
is_optional_as_none_in_union_enabled, is_unnecessary_default_type_args_stubs_enabled,
|
||||
is_future_required_preview_generics_enabled, is_optional_as_none_in_union_enabled,
|
||||
is_unnecessary_default_type_args_stubs_enabled,
|
||||
};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{
|
||||
@@ -69,7 +70,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
&& checker.semantic.in_annotation()
|
||||
&& checker.semantic.in_runtime_evaluated_annotation()
|
||||
&& !checker.semantic.in_string_type_definition()
|
||||
&& typing::is_pep585_generic(value, &checker.semantic)
|
||||
&& typing::is_pep585_generic(
|
||||
value,
|
||||
&checker.semantic,
|
||||
is_future_required_preview_generics_enabled(checker.settings()),
|
||||
)
|
||||
{
|
||||
flake8_future_annotations::rules::future_required_type_annotation(
|
||||
checker,
|
||||
|
||||
@@ -200,6 +200,11 @@ pub(crate) const fn is_optional_as_none_in_union_enabled(settings: &LinterSettin
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/20659
|
||||
pub(crate) const fn is_future_required_preview_generics_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/18683
|
||||
pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
|
||||
settings: &LinterSettings,
|
||||
@@ -240,3 +245,17 @@ pub(crate) const fn is_a003_class_scope_shadowing_expansion_enabled(
|
||||
pub(crate) const fn is_refined_submodule_import_match_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// github.com/astral-sh/ruff/issues/20004
|
||||
pub(crate) const fn is_b006_check_guaranteed_mutable_expr_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// github.com/astral-sh/ruff/issues/20004
|
||||
pub(crate) const fn is_b006_unsafe_fix_preserve_assignment_expr_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -142,23 +142,25 @@ impl AutoPythonType {
|
||||
});
|
||||
Some((expr, vec![no_return_edit]))
|
||||
}
|
||||
AutoPythonType::Atom(python_type) => {
|
||||
let expr = type_expr(python_type)?;
|
||||
Some((expr, vec![]))
|
||||
}
|
||||
AutoPythonType::Atom(python_type) => type_expr(python_type, checker, at),
|
||||
AutoPythonType::Union(python_types) => {
|
||||
if target_version >= PythonVersion::PY310 {
|
||||
// Aggregate all the individual types (e.g., `int`, `float`).
|
||||
let mut all_edits = Vec::new();
|
||||
let names = python_types
|
||||
.iter()
|
||||
.sorted_unstable()
|
||||
.map(|python_type| type_expr(*python_type))
|
||||
.map(|python_type| {
|
||||
let (expr, mut edits) = type_expr(*python_type, checker, at)?;
|
||||
all_edits.append(&mut edits);
|
||||
Some(expr)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
|
||||
// Wrap in a bitwise union (e.g., `int | float`).
|
||||
let expr = pep_604_union(&names);
|
||||
|
||||
Some((expr, vec![]))
|
||||
Some((expr, all_edits))
|
||||
} else {
|
||||
let python_types = python_types
|
||||
.into_iter()
|
||||
@@ -167,7 +169,7 @@ impl AutoPythonType {
|
||||
|
||||
match python_types.as_slice() {
|
||||
[python_type, PythonType::None] | [PythonType::None, python_type] => {
|
||||
let element = type_expr(*python_type)?;
|
||||
let (element, mut edits) = type_expr(*python_type, checker, at)?;
|
||||
|
||||
// Ex) `Optional[int]`
|
||||
let (optional_edit, binding) = checker
|
||||
@@ -175,12 +177,18 @@ impl AutoPythonType {
|
||||
.import(at)
|
||||
.ok()?;
|
||||
let expr = typing_optional(element, Name::from(binding));
|
||||
Some((expr, vec![optional_edit]))
|
||||
edits.push(optional_edit);
|
||||
Some((expr, edits))
|
||||
}
|
||||
_ => {
|
||||
let mut all_edits = Vec::new();
|
||||
let elements = python_types
|
||||
.into_iter()
|
||||
.map(type_expr)
|
||||
.map(|python_type| {
|
||||
let (expr, mut edits) = type_expr(python_type, checker, at)?;
|
||||
all_edits.append(&mut edits);
|
||||
Some(expr)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
|
||||
// Ex) `Union[int, str]`
|
||||
@@ -189,7 +197,8 @@ impl AutoPythonType {
|
||||
.import(at)
|
||||
.ok()?;
|
||||
let expr = typing_union(&elements, Name::from(binding));
|
||||
Some((expr, vec![union_edit]))
|
||||
all_edits.push(union_edit);
|
||||
Some((expr, all_edits))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,26 +208,44 @@ impl AutoPythonType {
|
||||
}
|
||||
|
||||
/// Given a [`PythonType`], return an [`Expr`] that resolves to that type.
|
||||
fn type_expr(python_type: PythonType) -> Option<Expr> {
|
||||
fn name(name: &str) -> Expr {
|
||||
Expr::Name(ast::ExprName {
|
||||
id: name.into(),
|
||||
///
|
||||
/// If the [`Expr`] relies on importing any external symbols, those imports will be returned as
|
||||
/// additional edits.
|
||||
pub(crate) fn type_expr(
|
||||
python_type: PythonType,
|
||||
checker: &Checker,
|
||||
at: TextSize,
|
||||
) -> Option<(Expr, Vec<Edit>)> {
|
||||
fn name(name: &str, checker: &Checker, at: TextSize) -> Option<(Expr, Vec<Edit>)> {
|
||||
let (edit, binding) = checker
|
||||
.importer()
|
||||
.get_or_import_builtin_symbol(name, at, checker.semantic())
|
||||
.ok()?;
|
||||
let expr = Expr::Name(ast::ExprName {
|
||||
id: binding.into(),
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
ctx: ExprContext::Load,
|
||||
})
|
||||
});
|
||||
Some((expr, edit.map_or_else(Vec::new, |edit| vec![edit])))
|
||||
}
|
||||
|
||||
match python_type {
|
||||
PythonType::String => Some(name("str")),
|
||||
PythonType::Bytes => Some(name("bytes")),
|
||||
PythonType::Number(number) => match number {
|
||||
NumberLike::Integer => Some(name("int")),
|
||||
NumberLike::Float => Some(name("float")),
|
||||
NumberLike::Complex => Some(name("complex")),
|
||||
NumberLike::Bool => Some(name("bool")),
|
||||
},
|
||||
PythonType::None => Some(name("None")),
|
||||
PythonType::String => name("str", checker, at),
|
||||
PythonType::Bytes => name("bytes", checker, at),
|
||||
PythonType::Number(number) => {
|
||||
let symbol = match number {
|
||||
NumberLike::Integer => "int",
|
||||
NumberLike::Float => "float",
|
||||
NumberLike::Complex => "complex",
|
||||
NumberLike::Bool => "bool",
|
||||
};
|
||||
name(symbol, checker, at)
|
||||
}
|
||||
PythonType::None => {
|
||||
let expr = Expr::NoneLiteral(ast::ExprNoneLiteral::default());
|
||||
Some((expr, vec![]))
|
||||
}
|
||||
PythonType::Ellipsis => None,
|
||||
PythonType::Dict => None,
|
||||
PythonType::List => None,
|
||||
|
||||
@@ -229,4 +229,20 @@ mod tests {
|
||||
assert_diagnostics!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadowed_builtins() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_annotations/shadowed_builtins.py"),
|
||||
&LinterSettings::for_rules(vec![
|
||||
Rule::MissingReturnTypeUndocumentedPublicFunction,
|
||||
Rule::MissingReturnTypePrivateFunction,
|
||||
Rule::MissingReturnTypeSpecialMethod,
|
||||
Rule::MissingReturnTypeStaticMethod,
|
||||
Rule::MissingReturnTypeClassMethod,
|
||||
]),
|
||||
)?;
|
||||
assert_diagnostics!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::Definition;
|
||||
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType};
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_stdlib::typing::simple_magic_return_type;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::{Checker, DiagnosticGuard};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_annotations::helpers::auto_return_type;
|
||||
use crate::rules::flake8_annotations::helpers::{auto_return_type, type_expr};
|
||||
use crate::rules::ruff::typing::type_hint_resolves_to_any;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
@@ -825,11 +826,31 @@ pub(crate) fn definition(
|
||||
},
|
||||
function.identifier(),
|
||||
);
|
||||
if let Some(return_type) = return_type {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
|
||||
format!(" -> {return_type}"),
|
||||
function.parameters.end(),
|
||||
)));
|
||||
if let Some(return_type_str) = return_type {
|
||||
// Convert the simple return type to a proper expression that handles shadowed builtins
|
||||
let python_type = match return_type_str {
|
||||
"str" => PythonType::String,
|
||||
"bytes" => PythonType::Bytes,
|
||||
"int" => PythonType::Number(NumberLike::Integer),
|
||||
"float" => PythonType::Number(NumberLike::Float),
|
||||
"complex" => PythonType::Number(NumberLike::Complex),
|
||||
"bool" => PythonType::Number(NumberLike::Bool),
|
||||
"None" => PythonType::None,
|
||||
_ => return, // Unknown type, skip
|
||||
};
|
||||
|
||||
if let Some((expr, edits)) =
|
||||
type_expr(python_type, checker, function.parameters.start())
|
||||
{
|
||||
let return_type_expr = checker.generator().expr(&expr);
|
||||
diagnostic.set_fix(Fix::unsafe_edits(
|
||||
Edit::insertion(
|
||||
format!(" -> {return_type_expr}"),
|
||||
function.parameters.end(),
|
||||
),
|
||||
edits,
|
||||
));
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
|
||||
---
|
||||
ANN201 [*] Missing return type annotation for public function `foo`
|
||||
--> shadowed_builtins.py:4:5
|
||||
|
|
||||
2 | from typing import override
|
||||
3 |
|
||||
4 | def foo():
|
||||
| ^^^
|
||||
5 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
- def foo():
|
||||
5 + def foo() -> builtins.str:
|
||||
6 | return "!"
|
||||
7 |
|
||||
8 | def _foo():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN202 [*] Missing return type annotation for private function `_foo`
|
||||
--> shadowed_builtins.py:7:5
|
||||
|
|
||||
5 | return "!"
|
||||
6 |
|
||||
7 | def _foo():
|
||||
| ^^^^
|
||||
8 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
7 |
|
||||
- def _foo():
|
||||
8 + def _foo() -> builtins.str:
|
||||
9 | return "!"
|
||||
10 |
|
||||
11 | class C:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN204 [*] Missing return type annotation for special method `__str__`
|
||||
--> shadowed_builtins.py:12:9
|
||||
|
|
||||
10 | class C:
|
||||
11 | @override
|
||||
12 | def __str__(self):
|
||||
| ^^^^^^^
|
||||
13 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
--------------------------------------------------------------------------------
|
||||
10 |
|
||||
11 | class C:
|
||||
12 | @override
|
||||
- def __str__(self):
|
||||
13 + def __str__(self) -> builtins.str:
|
||||
14 | return "!"
|
||||
15 |
|
||||
16 | @staticmethod
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN205 [*] Missing return type annotation for staticmethod `foo`
|
||||
--> shadowed_builtins.py:16:9
|
||||
|
|
||||
15 | @staticmethod
|
||||
16 | def foo():
|
||||
| ^^^
|
||||
17 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
--------------------------------------------------------------------------------
|
||||
14 | return "!"
|
||||
15 |
|
||||
16 | @staticmethod
|
||||
- def foo():
|
||||
17 + def foo() -> builtins.str:
|
||||
18 | return "!"
|
||||
19 |
|
||||
20 | @classmethod
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN206 [*] Missing return type annotation for classmethod `bar`
|
||||
--> shadowed_builtins.py:20:9
|
||||
|
|
||||
19 | @classmethod
|
||||
20 | def bar(cls):
|
||||
| ^^^
|
||||
21 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
--------------------------------------------------------------------------------
|
||||
18 | return "!"
|
||||
19 |
|
||||
20 | @classmethod
|
||||
- def bar(cls):
|
||||
21 + def bar(cls) -> builtins.str:
|
||||
22 | return "!"
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -47,6 +47,7 @@ mod tests {
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_6.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_7.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_8.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_9.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.pyi"))]
|
||||
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))]
|
||||
@@ -83,6 +84,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Rule::MapWithoutExplicitStrict, Path::new("B912.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_2.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_3.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_4.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_5.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_6.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_7.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_8.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_9.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
|
||||
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.pyi"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -3,9 +3,8 @@ use std::fmt::Write;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::{self as ast, Expr, Parameter};
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::analyze::function_type::is_stub;
|
||||
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
|
||||
@@ -13,8 +12,11 @@ use ruff_python_trivia::{indentation_at_offset, textwrap};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::{
|
||||
is_b006_check_guaranteed_mutable_expr_enabled,
|
||||
is_b006_unsafe_fix_preserve_assignment_expr_enabled,
|
||||
};
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -111,8 +113,12 @@ pub(crate) fn mutable_argument_default(checker: &Checker, function_def: &ast::St
|
||||
.iter()
|
||||
.map(|target| QualifiedName::from_dotted_name(target))
|
||||
.collect();
|
||||
|
||||
if is_mutable_expr(default, checker.semantic())
|
||||
let is_mut_expr = if is_b006_check_guaranteed_mutable_expr_enabled(checker.settings()) {
|
||||
is_guaranteed_mutable_expr(default, checker.semantic())
|
||||
} else {
|
||||
is_mutable_expr(default, checker.semantic())
|
||||
};
|
||||
if is_mut_expr
|
||||
&& !parameter.annotation().is_some_and(|expr| {
|
||||
is_immutable_annotation(expr, checker.semantic(), extend_immutable_calls.as_slice())
|
||||
})
|
||||
@@ -120,35 +126,37 @@ pub(crate) fn mutable_argument_default(checker: &Checker, function_def: &ast::St
|
||||
let mut diagnostic = checker.report_diagnostic(MutableArgumentDefault, default.range());
|
||||
|
||||
// If the function body is on the same line as the function def, do not fix
|
||||
if let Some(fix) = move_initialization(
|
||||
function_def,
|
||||
¶meter.parameter,
|
||||
default,
|
||||
checker.semantic(),
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
checker.generator(),
|
||||
) {
|
||||
if let Some(fix) = move_initialization(function_def, parameter, default, checker) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the expression is guaranteed to create a mutable object.
|
||||
fn is_guaranteed_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
match expr {
|
||||
Expr::Generator(_) => true,
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
elts.iter().any(|e| is_guaranteed_mutable_expr(e, semantic))
|
||||
}
|
||||
Expr::Named(ast::ExprNamed { value, .. }) => is_guaranteed_mutable_expr(value, semantic),
|
||||
_ => is_mutable_expr(expr, semantic),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to move a mutable argument default initialization
|
||||
/// into the function body.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn move_initialization(
|
||||
function_def: &ast::StmtFunctionDef,
|
||||
parameter: &Parameter,
|
||||
parameter: &ParameterWithDefault,
|
||||
default: &Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
indexer: &Indexer,
|
||||
generator: Generator,
|
||||
checker: &Checker,
|
||||
) -> Option<Fix> {
|
||||
let indexer = checker.indexer();
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
let mut body = function_def.body.iter().peekable();
|
||||
|
||||
// Avoid attempting to fix single-line functions.
|
||||
@@ -157,25 +165,58 @@ fn move_initialization(
|
||||
return None;
|
||||
}
|
||||
|
||||
let range = match parenthesized_range(
|
||||
default.into(),
|
||||
parameter.into(),
|
||||
checker.comment_ranges(),
|
||||
checker.source(),
|
||||
) {
|
||||
Some(range) => range,
|
||||
None => default.range(),
|
||||
};
|
||||
// Set the default argument value to `None`.
|
||||
let default_edit = Edit::range_replacement("None".to_string(), default.range());
|
||||
let default_edit = Edit::range_replacement("None".to_string(), range);
|
||||
|
||||
// If the function is a stub, this is the only necessary edit.
|
||||
if is_stub(function_def, semantic) {
|
||||
if is_stub(function_def, checker.semantic()) {
|
||||
return Some(Fix::unsafe_edit(default_edit));
|
||||
}
|
||||
|
||||
// Add an `if`, to set the argument to its original value if still `None`.
|
||||
let mut content = String::new();
|
||||
let _ = write!(&mut content, "if {} is None:", parameter.name());
|
||||
let _ = write!(&mut content, "if {} is None:", parameter.parameter.name());
|
||||
content.push_str(stylist.line_ending().as_str());
|
||||
content.push_str(stylist.indentation());
|
||||
let _ = write!(
|
||||
&mut content,
|
||||
"{} = {}",
|
||||
parameter.name(),
|
||||
generator.expr(default)
|
||||
);
|
||||
if is_b006_unsafe_fix_preserve_assignment_expr_enabled(checker.settings()) {
|
||||
let annotation = if let Some(ann) = parameter.annotation() {
|
||||
format!(": {}", locator.slice(ann))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let _ = write!(
|
||||
&mut content,
|
||||
"{}{} = {}",
|
||||
parameter.parameter.name(),
|
||||
annotation,
|
||||
locator.slice(
|
||||
parenthesized_range(
|
||||
default.into(),
|
||||
parameter.into(),
|
||||
checker.comment_ranges(),
|
||||
checker.source()
|
||||
)
|
||||
.unwrap_or(default.range())
|
||||
)
|
||||
);
|
||||
} else {
|
||||
let _ = write!(
|
||||
&mut content,
|
||||
"{} = {}",
|
||||
parameter.name(),
|
||||
checker.generator().expr(default)
|
||||
);
|
||||
}
|
||||
|
||||
content.push_str(stylist.line_ending().as_str());
|
||||
|
||||
// Determine the indentation depth of the function body.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_9.py:17:11
|
||||
|
|
||||
17 | def f5(x=([1, ])):
|
||||
| ^^^^^
|
||||
18 | print(x)
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
14 | print(x)
|
||||
15 |
|
||||
16 |
|
||||
- def f5(x=([1, ])):
|
||||
17 + def f5(x=None):
|
||||
18 + if x is None:
|
||||
19 + x = [1]
|
||||
20 | print(x)
|
||||
21 |
|
||||
22 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_1.py:3:22
|
||||
|
|
||||
1 | # Docstring followed by a newline
|
||||
2 |
|
||||
3 | def foobar(foor, bar={}):
|
||||
| ^^
|
||||
4 | """
|
||||
5 | """
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
1 | # Docstring followed by a newline
|
||||
2 |
|
||||
- def foobar(foor, bar={}):
|
||||
3 + def foobar(foor, bar=None):
|
||||
4 | """
|
||||
5 | """
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_2.py:4:22
|
||||
|
|
||||
2 | # Regression test for https://github.com/astral-sh/ruff/issues/7155
|
||||
3 |
|
||||
4 | def foobar(foor, bar={}):
|
||||
| ^^
|
||||
5 | """
|
||||
6 | """
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
1 | # Docstring followed by whitespace with no newline
|
||||
2 | # Regression test for https://github.com/astral-sh/ruff/issues/7155
|
||||
3 |
|
||||
- def foobar(foor, bar={}):
|
||||
4 + def foobar(foor, bar=None):
|
||||
5 | """
|
||||
6 | """
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_3.py:4:22
|
||||
|
|
||||
4 | def foobar(foor, bar={}):
|
||||
| ^^
|
||||
5 | """
|
||||
6 | """
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
1 | # Docstring with no newline
|
||||
2 |
|
||||
3 |
|
||||
- def foobar(foor, bar={}):
|
||||
4 + def foobar(foor, bar=None):
|
||||
5 | """
|
||||
6 | """
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_4.py:7:26
|
||||
|
|
||||
6 | class FormFeedIndent:
|
||||
7 | def __init__(self, a=[]):
|
||||
| ^^
|
||||
8 | print(a)
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
4 |
|
||||
5 |
|
||||
6 | class FormFeedIndent:
|
||||
- def __init__(self, a=[]):
|
||||
7 + def __init__(self, a=None):
|
||||
8 + if a is None:
|
||||
9 + a = []
|
||||
10 | print(a)
|
||||
11 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:5:49
|
||||
|
|
||||
5 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
6 | import os
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
2 | # https://github.com/astral-sh/ruff/issues/7616
|
||||
3 |
|
||||
4 |
|
||||
- def import_module_wrong(value: dict[str, str] = {}):
|
||||
5 + def import_module_wrong(value: dict[str, str] = None):
|
||||
6 | import os
|
||||
7 + if value is None:
|
||||
8 + value: dict[str, str] = {}
|
||||
9 |
|
||||
10 |
|
||||
11 | def import_module_with_values_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:9:61
|
||||
|
|
||||
9 | def import_module_with_values_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
10 | import os
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
6 | import os
|
||||
7 |
|
||||
8 |
|
||||
- def import_module_with_values_wrong(value: dict[str, str] = {}):
|
||||
9 + def import_module_with_values_wrong(value: dict[str, str] = None):
|
||||
10 | import os
|
||||
11 |
|
||||
12 + if value is None:
|
||||
13 + value: dict[str, str] = {}
|
||||
14 | return 2
|
||||
15 |
|
||||
16 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:15:50
|
||||
|
|
||||
15 | def import_modules_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
16 | import os
|
||||
17 | import sys
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
12 | return 2
|
||||
13 |
|
||||
14 |
|
||||
- def import_modules_wrong(value: dict[str, str] = {}):
|
||||
15 + def import_modules_wrong(value: dict[str, str] = None):
|
||||
16 | import os
|
||||
17 | import sys
|
||||
18 | import itertools
|
||||
19 + if value is None:
|
||||
20 + value: dict[str, str] = {}
|
||||
21 |
|
||||
22 |
|
||||
23 | def from_import_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:21:54
|
||||
|
|
||||
21 | def from_import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
22 | from os import path
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
18 | import itertools
|
||||
19 |
|
||||
20 |
|
||||
- def from_import_module_wrong(value: dict[str, str] = {}):
|
||||
21 + def from_import_module_wrong(value: dict[str, str] = None):
|
||||
22 | from os import path
|
||||
23 + if value is None:
|
||||
24 + value: dict[str, str] = {}
|
||||
25 |
|
||||
26 |
|
||||
27 | def from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:25:55
|
||||
|
|
||||
25 | def from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
26 | from os import path
|
||||
27 | from sys import version_info
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
22 | from os import path
|
||||
23 |
|
||||
24 |
|
||||
- def from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
25 + def from_imports_module_wrong(value: dict[str, str] = None):
|
||||
26 | from os import path
|
||||
27 | from sys import version_info
|
||||
28 + if value is None:
|
||||
29 + value: dict[str, str] = {}
|
||||
30 |
|
||||
31 |
|
||||
32 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:30:66
|
||||
|
|
||||
30 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
31 | import os
|
||||
32 | from sys import version_info
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
27 | from sys import version_info
|
||||
28 |
|
||||
29 |
|
||||
- def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
|
||||
30 + def import_and_from_imports_module_wrong(value: dict[str, str] = None):
|
||||
31 | import os
|
||||
32 | from sys import version_info
|
||||
33 + if value is None:
|
||||
34 + value: dict[str, str] = {}
|
||||
35 |
|
||||
36 |
|
||||
37 | def import_docstring_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:35:59
|
||||
|
|
||||
35 | def import_docstring_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
36 | """Docstring"""
|
||||
37 | import os
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
32 | from sys import version_info
|
||||
33 |
|
||||
34 |
|
||||
- def import_docstring_module_wrong(value: dict[str, str] = {}):
|
||||
35 + def import_docstring_module_wrong(value: dict[str, str] = None):
|
||||
36 | """Docstring"""
|
||||
37 | import os
|
||||
38 + if value is None:
|
||||
39 + value: dict[str, str] = {}
|
||||
40 |
|
||||
41 |
|
||||
42 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:40:49
|
||||
|
|
||||
40 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
41 | """Docstring"""
|
||||
42 | import os; import sys
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
37 | import os
|
||||
38 |
|
||||
39 |
|
||||
- def import_module_wrong(value: dict[str, str] = {}):
|
||||
40 + def import_module_wrong(value: dict[str, str] = None):
|
||||
41 | """Docstring"""
|
||||
42 | import os; import sys
|
||||
43 + if value is None:
|
||||
44 + value: dict[str, str] = {}
|
||||
45 |
|
||||
46 |
|
||||
47 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:45:49
|
||||
|
|
||||
45 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
46 | """Docstring"""
|
||||
47 | import os; import sys; x = 1
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
42 | import os; import sys
|
||||
43 |
|
||||
44 |
|
||||
- def import_module_wrong(value: dict[str, str] = {}):
|
||||
45 + def import_module_wrong(value: dict[str, str] = None):
|
||||
46 | """Docstring"""
|
||||
47 + if value is None:
|
||||
48 + value: dict[str, str] = {}
|
||||
49 | import os; import sys; x = 1
|
||||
50 |
|
||||
51 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:50:49
|
||||
|
|
||||
50 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
51 | """Docstring"""
|
||||
52 | import os; import sys
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
47 | import os; import sys; x = 1
|
||||
48 |
|
||||
49 |
|
||||
- def import_module_wrong(value: dict[str, str] = {}):
|
||||
50 + def import_module_wrong(value: dict[str, str] = None):
|
||||
51 | """Docstring"""
|
||||
52 | import os; import sys
|
||||
53 + if value is None:
|
||||
54 + value: dict[str, str] = {}
|
||||
55 |
|
||||
56 |
|
||||
57 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:55:49
|
||||
|
|
||||
55 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
56 | import os; import sys
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
52 | import os; import sys
|
||||
53 |
|
||||
54 |
|
||||
- def import_module_wrong(value: dict[str, str] = {}):
|
||||
55 + def import_module_wrong(value: dict[str, str] = None):
|
||||
56 | import os; import sys
|
||||
57 + if value is None:
|
||||
58 + value: dict[str, str] = {}
|
||||
59 |
|
||||
60 |
|
||||
61 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:59:49
|
||||
|
|
||||
59 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
60 | import os; import sys; x = 1
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
56 | import os; import sys
|
||||
57 |
|
||||
58 |
|
||||
- def import_module_wrong(value: dict[str, str] = {}):
|
||||
59 + def import_module_wrong(value: dict[str, str] = None):
|
||||
60 + if value is None:
|
||||
61 + value: dict[str, str] = {}
|
||||
62 | import os; import sys; x = 1
|
||||
63 |
|
||||
64 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:63:49
|
||||
|
|
||||
63 | def import_module_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
64 | import os; import sys
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
60 | import os; import sys; x = 1
|
||||
61 |
|
||||
62 |
|
||||
- def import_module_wrong(value: dict[str, str] = {}):
|
||||
63 + def import_module_wrong(value: dict[str, str] = None):
|
||||
64 | import os; import sys
|
||||
65 + if value is None:
|
||||
66 + value: dict[str, str] = {}
|
||||
67 |
|
||||
68 |
|
||||
69 | def import_module_wrong(value: dict[str, str] = {}): import os
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:67:49
|
||||
|
|
||||
67 | def import_module_wrong(value: dict[str, str] = {}): import os
|
||||
| ^^
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
|
||||
B006 Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:70:49
|
||||
|
|
||||
70 | def import_module_wrong(value: dict[str, str] = {}): import os; import sys
|
||||
| ^^
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
|
||||
B006 Do not use mutable data structures for argument defaults
|
||||
--> B006_5.py:73:49
|
||||
|
|
||||
73 | def import_module_wrong(value: dict[str, str] = {}): \
|
||||
| ^^
|
||||
74 | import os
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_6.py:4:22
|
||||
|
|
||||
2 | # Same as B006_2.py, but import instead of docstring
|
||||
3 |
|
||||
4 | def foobar(foor, bar={}):
|
||||
| ^^
|
||||
5 | import os
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
1 | # Import followed by whitespace with no newline
|
||||
2 | # Same as B006_2.py, but import instead of docstring
|
||||
3 |
|
||||
- def foobar(foor, bar={}):
|
||||
- import os
|
||||
4 + def foobar(foor, bar=None):
|
||||
5 + import os
|
||||
6 + if bar is None:
|
||||
7 + bar = {}
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_7.py:4:22
|
||||
|
|
||||
2 | # Same as B006_3.py, but import instead of docstring
|
||||
3 |
|
||||
4 | def foobar(foor, bar={}):
|
||||
| ^^
|
||||
5 | import os
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
1 | # Import with no newline
|
||||
2 | # Same as B006_3.py, but import instead of docstring
|
||||
3 |
|
||||
- def foobar(foor, bar={}):
|
||||
- import os
|
||||
4 + def foobar(foor, bar=None):
|
||||
5 + import os
|
||||
6 + if bar is None:
|
||||
7 + bar = {}
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_8.py:1:19
|
||||
|
|
||||
1 | def foo(a: list = []):
|
||||
| ^^
|
||||
2 | raise NotImplementedError("")
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
- def foo(a: list = []):
|
||||
1 + def foo(a: list = None):
|
||||
2 | raise NotImplementedError("")
|
||||
3 |
|
||||
4 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_8.py:5:19
|
||||
|
|
||||
5 | def bar(a: dict = {}):
|
||||
| ^^
|
||||
6 | """ This one also has a docstring"""
|
||||
7 | raise NotImplementedError("and has some text in here")
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
2 | raise NotImplementedError("")
|
||||
3 |
|
||||
4 |
|
||||
- def bar(a: dict = {}):
|
||||
5 + def bar(a: dict = None):
|
||||
6 | """ This one also has a docstring"""
|
||||
7 | raise NotImplementedError("and has some text in here")
|
||||
8 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_8.py:10:19
|
||||
|
|
||||
10 | def baz(a: list = []):
|
||||
| ^^
|
||||
11 | """This one raises a different exception"""
|
||||
12 | raise IndexError()
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
7 | raise NotImplementedError("and has some text in here")
|
||||
8 |
|
||||
9 |
|
||||
- def baz(a: list = []):
|
||||
10 + def baz(a: list = None):
|
||||
11 | """This one raises a different exception"""
|
||||
12 + if a is None:
|
||||
13 + a: list = []
|
||||
14 | raise IndexError()
|
||||
15 |
|
||||
16 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_8.py:15:19
|
||||
|
|
||||
15 | def qux(a: list = []):
|
||||
| ^^
|
||||
16 | raise NotImplementedError
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
12 | raise IndexError()
|
||||
13 |
|
||||
14 |
|
||||
- def qux(a: list = []):
|
||||
15 + def qux(a: list = None):
|
||||
16 | raise NotImplementedError
|
||||
17 |
|
||||
18 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_8.py:19:20
|
||||
|
|
||||
19 | def quux(a: list = []):
|
||||
| ^^
|
||||
20 | raise NotImplemented
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
16 | raise NotImplementedError
|
||||
17 |
|
||||
18 |
|
||||
- def quux(a: list = []):
|
||||
19 + def quux(a: list = None):
|
||||
20 | raise NotImplemented
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_9.py:1:10
|
||||
|
|
||||
1 | def f1(x=([],)):
|
||||
| ^^^^^
|
||||
2 | print(x)
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
- def f1(x=([],)):
|
||||
1 + def f1(x=None):
|
||||
2 + if x is None:
|
||||
3 + x = ([],)
|
||||
4 | print(x)
|
||||
5 |
|
||||
6 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_9.py:5:10
|
||||
|
|
||||
5 | def f2(x=(x for x in "x")):
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
6 | print(x)
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
2 | print(x)
|
||||
3 |
|
||||
4 |
|
||||
- def f2(x=(x for x in "x")):
|
||||
5 + def f2(x=None):
|
||||
6 + if x is None:
|
||||
7 + x = (x for x in "x")
|
||||
8 | print(x)
|
||||
9 |
|
||||
10 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_9.py:9:10
|
||||
|
|
||||
9 | def f3(x=((x for x in "x"),)):
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
10 | print(x)
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
6 | print(x)
|
||||
7 |
|
||||
8 |
|
||||
- def f3(x=((x for x in "x"),)):
|
||||
9 + def f3(x=None):
|
||||
10 + if x is None:
|
||||
11 + x = ((x for x in "x"),)
|
||||
12 | print(x)
|
||||
13 |
|
||||
14 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_9.py:13:11
|
||||
|
|
||||
13 | def f4(x=(z := [1, ])):
|
||||
| ^^^^^^^^^^
|
||||
14 | print(x)
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
10 | print(x)
|
||||
11 |
|
||||
12 |
|
||||
- def f4(x=(z := [1, ])):
|
||||
13 + def f4(x=None):
|
||||
14 + if x is None:
|
||||
15 + x = (z := [1, ])
|
||||
16 | print(x)
|
||||
17 |
|
||||
18 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_9.py:17:11
|
||||
|
|
||||
17 | def f5(x=([1, ])):
|
||||
| ^^^^^
|
||||
18 | print(x)
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
14 | print(x)
|
||||
15 |
|
||||
16 |
|
||||
- def f5(x=([1, ])):
|
||||
17 + def f5(x=None):
|
||||
18 + if x is None:
|
||||
19 + x = ([1, ])
|
||||
20 | print(x)
|
||||
21 |
|
||||
22 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,462 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:63:25
|
||||
|
|
||||
63 | def this_is_wrong(value=[1, 2, 3]):
|
||||
| ^^^^^^^^^
|
||||
64 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
60 | # Flag mutable literals/comprehensions
|
||||
61 |
|
||||
62 |
|
||||
- def this_is_wrong(value=[1, 2, 3]):
|
||||
63 + def this_is_wrong(value=None):
|
||||
64 | ...
|
||||
65 |
|
||||
66 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:67:30
|
||||
|
|
||||
67 | def this_is_also_wrong(value={}):
|
||||
| ^^
|
||||
68 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
64 | ...
|
||||
65 |
|
||||
66 |
|
||||
- def this_is_also_wrong(value={}):
|
||||
67 + def this_is_also_wrong(value=None):
|
||||
68 | ...
|
||||
69 |
|
||||
70 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:73:52
|
||||
|
|
||||
71 | class Foo:
|
||||
72 | @staticmethod
|
||||
73 | def this_is_also_wrong_and_more_indented(value={}):
|
||||
| ^^
|
||||
74 | pass
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
70 |
|
||||
71 | class Foo:
|
||||
72 | @staticmethod
|
||||
- def this_is_also_wrong_and_more_indented(value={}):
|
||||
73 + def this_is_also_wrong_and_more_indented(value=None):
|
||||
74 | pass
|
||||
75 |
|
||||
76 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:77:31
|
||||
|
|
||||
77 | def multiline_arg_wrong(value={
|
||||
| _______________________________^
|
||||
78 | |
|
||||
79 | | }):
|
||||
| |_^
|
||||
80 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
74 | pass
|
||||
75 |
|
||||
76 |
|
||||
- def multiline_arg_wrong(value={
|
||||
-
|
||||
- }):
|
||||
77 + def multiline_arg_wrong(value=None):
|
||||
78 | ...
|
||||
79 |
|
||||
80 | def single_line_func_wrong(value = {}): ...
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:82:36
|
||||
|
|
||||
80 | ...
|
||||
81 |
|
||||
82 | def single_line_func_wrong(value = {}): ...
|
||||
| ^^
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:85:20
|
||||
|
|
||||
85 | def and_this(value=set()):
|
||||
| ^^^^^
|
||||
86 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
82 | def single_line_func_wrong(value = {}): ...
|
||||
83 |
|
||||
84 |
|
||||
- def and_this(value=set()):
|
||||
85 + def and_this(value=None):
|
||||
86 | ...
|
||||
87 |
|
||||
88 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:89:20
|
||||
|
|
||||
89 | def this_too(value=collections.OrderedDict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
90 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
86 | ...
|
||||
87 |
|
||||
88 |
|
||||
- def this_too(value=collections.OrderedDict()):
|
||||
89 + def this_too(value=None):
|
||||
90 | ...
|
||||
91 |
|
||||
92 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:93:32
|
||||
|
|
||||
93 | async def async_this_too(value=collections.defaultdict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
94 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
90 | ...
|
||||
91 |
|
||||
92 |
|
||||
- async def async_this_too(value=collections.defaultdict()):
|
||||
93 + async def async_this_too(value=None):
|
||||
94 | ...
|
||||
95 |
|
||||
96 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:97:26
|
||||
|
|
||||
97 | def dont_forget_me(value=collections.deque()):
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
98 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
94 | ...
|
||||
95 |
|
||||
96 |
|
||||
- def dont_forget_me(value=collections.deque()):
|
||||
97 + def dont_forget_me(value=None):
|
||||
98 | ...
|
||||
99 |
|
||||
100 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:102:46
|
||||
|
|
||||
101 | # N.B. we're also flagging the function call in the comprehension
|
||||
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
103 | pass
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
99 |
|
||||
100 |
|
||||
101 | # N.B. we're also flagging the function call in the comprehension
|
||||
- def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
102 + def list_comprehension_also_not_okay(default=None):
|
||||
103 | pass
|
||||
104 |
|
||||
105 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:106:46
|
||||
|
|
||||
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
107 | pass
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
103 | pass
|
||||
104 |
|
||||
105 |
|
||||
- def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
106 + def dict_comprehension_also_not_okay(default=None):
|
||||
107 | pass
|
||||
108 |
|
||||
109 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:110:45
|
||||
|
|
||||
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
111 | pass
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
107 | pass
|
||||
108 |
|
||||
109 |
|
||||
- def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
110 + def set_comprehension_also_not_okay(default=None):
|
||||
111 | pass
|
||||
112 |
|
||||
113 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:114:33
|
||||
|
|
||||
114 | def kwonlyargs_mutable(*, value=[]):
|
||||
| ^^
|
||||
115 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
111 | pass
|
||||
112 |
|
||||
113 |
|
||||
- def kwonlyargs_mutable(*, value=[]):
|
||||
114 + def kwonlyargs_mutable(*, value=None):
|
||||
115 | ...
|
||||
116 |
|
||||
117 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:239:20
|
||||
|
|
||||
237 | # B006 and B008
|
||||
238 | # We should handle arbitrary nesting of these B008.
|
||||
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
240 | pass
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
236 |
|
||||
237 | # B006 and B008
|
||||
238 | # We should handle arbitrary nesting of these B008.
|
||||
- def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
239 + def nested_combo(a=None):
|
||||
240 | pass
|
||||
241 |
|
||||
242 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:276:27
|
||||
|
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
| ^^
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
273 |
|
||||
274 |
|
||||
275 | def mutable_annotations(
|
||||
- a: list[int] | None = [],
|
||||
276 + a: list[int] | None = None,
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:277:35
|
||||
|
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
| ^^
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
274 |
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
- b: Optional[Dict[int, int]] = {},
|
||||
277 + b: Optional[Dict[int, int]] = None,
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
280 | ):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:278:62
|
||||
|
|
||||
276 | a: list[int] | None = [],
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
280 | ):
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
278 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
280 | ):
|
||||
281 | pass
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:279:80
|
||||
|
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^
|
||||
280 | ):
|
||||
281 | pass
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
276 | a: list[int] | None = [],
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
280 | ):
|
||||
281 | pass
|
||||
282 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:284:52
|
||||
|
|
||||
284 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
285 | """Docstring"""
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
281 | pass
|
||||
282 |
|
||||
283 |
|
||||
- def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
284 + def single_line_func_wrong(value: dict[str, str] = None):
|
||||
285 | """Docstring"""
|
||||
286 |
|
||||
287 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:288:52
|
||||
|
|
||||
288 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
289 | """Docstring"""
|
||||
290 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
285 | """Docstring"""
|
||||
286 |
|
||||
287 |
|
||||
- def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
288 + def single_line_func_wrong(value: dict[str, str] = None):
|
||||
289 | """Docstring"""
|
||||
290 | ...
|
||||
291 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:293:52
|
||||
|
|
||||
293 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
294 | """Docstring"""; ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
290 | ...
|
||||
291 |
|
||||
292 |
|
||||
- def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
293 + def single_line_func_wrong(value: dict[str, str] = None):
|
||||
294 | """Docstring"""; ...
|
||||
295 |
|
||||
296 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:297:52
|
||||
|
|
||||
297 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
298 | """Docstring"""; \
|
||||
299 | ...
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
294 | """Docstring"""; ...
|
||||
295 |
|
||||
296 |
|
||||
- def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
297 + def single_line_func_wrong(value: dict[str, str] = None):
|
||||
298 | """Docstring"""; \
|
||||
299 | ...
|
||||
300 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:302:52
|
||||
|
|
||||
302 | def single_line_func_wrong(value: dict[str, str] = {
|
||||
| ____________________________________________________^
|
||||
303 | | # This is a comment
|
||||
304 | | }):
|
||||
| |_^
|
||||
305 | """Docstring"""
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
299 | ...
|
||||
300 |
|
||||
301 |
|
||||
- def single_line_func_wrong(value: dict[str, str] = {
|
||||
- # This is a comment
|
||||
- }):
|
||||
302 + def single_line_func_wrong(value: dict[str, str] = None):
|
||||
303 | """Docstring"""
|
||||
304 |
|
||||
305 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
B006 Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:308:52
|
||||
|
|
||||
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
| ^^
|
||||
309 | : \
|
||||
310 | """Docstring"""
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
|
||||
B006 [*] Do not use mutable data structures for argument defaults
|
||||
--> B006_B008.py:313:52
|
||||
|
|
||||
313 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^
|
||||
314 | """Docstring without newline"""
|
||||
|
|
||||
help: Replace with `None`; initialize within function
|
||||
310 | """Docstring"""
|
||||
311 |
|
||||
312 |
|
||||
- def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
313 + def single_line_func_wrong(value: dict[str, str] = None):
|
||||
314 | """Docstring without newline"""
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -32,12 +32,15 @@ use crate::rules::flake8_comprehensions::fixes;
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as `reversed()` and `reverse=True` will
|
||||
/// yield different results in the event of custom sort keys or equality
|
||||
/// functions. Specifically, `reversed()` will reverse the order of the
|
||||
/// collection, while `sorted()` with `reverse=True` will perform a stable
|
||||
/// This rule's fix is marked as unsafe for `reversed()` cases, as `reversed()`
|
||||
/// and `reverse=True` will yield different results in the event of custom sort
|
||||
/// keys or equality functions. Specifically, `reversed()` will reverse the order
|
||||
/// of the collection, while `sorted()` with `reverse=True` will perform a stable
|
||||
/// reverse sort, which will preserve the order of elements that compare as
|
||||
/// equal.
|
||||
///
|
||||
/// The fix is marked as safe for `list()` cases, as removing `list()` around
|
||||
/// `sorted()` does not change the behavior.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnnecessaryCallAroundSorted {
|
||||
func: UnnecessaryFunction,
|
||||
|
||||
@@ -9,6 +9,7 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_diagnostics, settings};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
@@ -39,6 +40,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Path::new("no_future_import_uses_lowercase.py"))]
|
||||
#[test_case(Path::new("no_future_import_uses_preview_generics.py"))]
|
||||
#[test_case(Path::new("no_future_import_uses_union.py"))]
|
||||
#[test_case(Path::new("no_future_import_uses_union_inner.py"))]
|
||||
#[test_case(Path::new("ok_no_types.py"))]
|
||||
@@ -56,4 +58,19 @@ mod tests {
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("no_future_import_uses_preview_generics.py"))]
|
||||
fn fa102_preview(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("fa102_preview_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_future_annotations").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
unresolved_target_version: PythonVersion::PY37.into(),
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:42:19
|
||||
|
|
||||
40 | future: asyncio.Future[int],
|
||||
41 | task: asyncio.Task[str],
|
||||
42 | deque_object: collections.deque[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
43 | defaultdict_object: collections.defaultdict[str, int],
|
||||
44 | ordered_dict: collections.OrderedDict[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:43:25
|
||||
|
|
||||
41 | task: asyncio.Task[str],
|
||||
42 | deque_object: collections.deque[int],
|
||||
43 | defaultdict_object: collections.defaultdict[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
44 | ordered_dict: collections.OrderedDict[str, int],
|
||||
45 | counter_obj: collections.Counter[str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,851 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:40:13
|
||||
|
|
||||
39 | def takes_preview_generics(
|
||||
40 | future: asyncio.Future[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
41 | task: asyncio.Task[str],
|
||||
42 | deque_object: collections.deque[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:41:11
|
||||
|
|
||||
39 | def takes_preview_generics(
|
||||
40 | future: asyncio.Future[int],
|
||||
41 | task: asyncio.Task[str],
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
42 | deque_object: collections.deque[int],
|
||||
43 | defaultdict_object: collections.defaultdict[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:42:19
|
||||
|
|
||||
40 | future: asyncio.Future[int],
|
||||
41 | task: asyncio.Task[str],
|
||||
42 | deque_object: collections.deque[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
43 | defaultdict_object: collections.defaultdict[str, int],
|
||||
44 | ordered_dict: collections.OrderedDict[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:43:25
|
||||
|
|
||||
41 | task: asyncio.Task[str],
|
||||
42 | deque_object: collections.deque[int],
|
||||
43 | defaultdict_object: collections.defaultdict[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
44 | ordered_dict: collections.OrderedDict[str, int],
|
||||
45 | counter_obj: collections.Counter[str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:44:19
|
||||
|
|
||||
42 | deque_object: collections.deque[int],
|
||||
43 | defaultdict_object: collections.defaultdict[str, int],
|
||||
44 | ordered_dict: collections.OrderedDict[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
45 | counter_obj: collections.Counter[str],
|
||||
46 | chain_map: collections.ChainMap[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:45:18
|
||||
|
|
||||
43 | defaultdict_object: collections.defaultdict[str, int],
|
||||
44 | ordered_dict: collections.OrderedDict[str, int],
|
||||
45 | counter_obj: collections.Counter[str],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
46 | chain_map: collections.ChainMap[str, int],
|
||||
47 | context_manager: contextlib.AbstractContextManager[str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:46:16
|
||||
|
|
||||
44 | ordered_dict: collections.OrderedDict[str, int],
|
||||
45 | counter_obj: collections.Counter[str],
|
||||
46 | chain_map: collections.ChainMap[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
47 | context_manager: contextlib.AbstractContextManager[str],
|
||||
48 | async_context_manager: contextlib.AbstractAsyncContextManager[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:47:22
|
||||
|
|
||||
45 | counter_obj: collections.Counter[str],
|
||||
46 | chain_map: collections.ChainMap[str, int],
|
||||
47 | context_manager: contextlib.AbstractContextManager[str],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
48 | async_context_manager: contextlib.AbstractAsyncContextManager[int],
|
||||
49 | dataclass_field: dataclasses.Field[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:48:28
|
||||
|
|
||||
46 | chain_map: collections.ChainMap[str, int],
|
||||
47 | context_manager: contextlib.AbstractContextManager[str],
|
||||
48 | async_context_manager: contextlib.AbstractAsyncContextManager[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
49 | dataclass_field: dataclasses.Field[int],
|
||||
50 | cached_prop: functools.cached_property[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:49:22
|
||||
|
|
||||
47 | context_manager: contextlib.AbstractContextManager[str],
|
||||
48 | async_context_manager: contextlib.AbstractAsyncContextManager[int],
|
||||
49 | dataclass_field: dataclasses.Field[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
50 | cached_prop: functools.cached_property[int],
|
||||
51 | partial_method: functools.partialmethod[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:50:18
|
||||
|
|
||||
48 | async_context_manager: contextlib.AbstractAsyncContextManager[int],
|
||||
49 | dataclass_field: dataclasses.Field[int],
|
||||
50 | cached_prop: functools.cached_property[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
51 | partial_method: functools.partialmethod[int],
|
||||
52 | path_like: os.PathLike[str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:51:21
|
||||
|
|
||||
49 | dataclass_field: dataclasses.Field[int],
|
||||
50 | cached_prop: functools.cached_property[int],
|
||||
51 | partial_method: functools.partialmethod[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
52 | path_like: os.PathLike[str],
|
||||
53 | lifo_queue: queue.LifoQueue[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:52:16
|
||||
|
|
||||
50 | cached_prop: functools.cached_property[int],
|
||||
51 | partial_method: functools.partialmethod[int],
|
||||
52 | path_like: os.PathLike[str],
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
53 | lifo_queue: queue.LifoQueue[int],
|
||||
54 | regular_queue: queue.Queue[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:53:17
|
||||
|
|
||||
51 | partial_method: functools.partialmethod[int],
|
||||
52 | path_like: os.PathLike[str],
|
||||
53 | lifo_queue: queue.LifoQueue[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
54 | regular_queue: queue.Queue[int],
|
||||
55 | priority_queue: queue.PriorityQueue[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:54:20
|
||||
|
|
||||
52 | path_like: os.PathLike[str],
|
||||
53 | lifo_queue: queue.LifoQueue[int],
|
||||
54 | regular_queue: queue.Queue[int],
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
55 | priority_queue: queue.PriorityQueue[int],
|
||||
56 | simple_queue: queue.SimpleQueue[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:55:21
|
||||
|
|
||||
53 | lifo_queue: queue.LifoQueue[int],
|
||||
54 | regular_queue: queue.Queue[int],
|
||||
55 | priority_queue: queue.PriorityQueue[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
56 | simple_queue: queue.SimpleQueue[int],
|
||||
57 | regex_pattern: re.Pattern[str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:56:19
|
||||
|
|
||||
54 | regular_queue: queue.Queue[int],
|
||||
55 | priority_queue: queue.PriorityQueue[int],
|
||||
56 | simple_queue: queue.SimpleQueue[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
57 | regex_pattern: re.Pattern[str],
|
||||
58 | regex_match: re.Match[str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:57:20
|
||||
|
|
||||
55 | priority_queue: queue.PriorityQueue[int],
|
||||
56 | simple_queue: queue.SimpleQueue[int],
|
||||
57 | regex_pattern: re.Pattern[str],
|
||||
| ^^^^^^^^^^^^^^^
|
||||
58 | regex_match: re.Match[str],
|
||||
59 | bsd_db_shelf: shelve.BsdDbShelf[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:58:18
|
||||
|
|
||||
56 | simple_queue: queue.SimpleQueue[int],
|
||||
57 | regex_pattern: re.Pattern[str],
|
||||
58 | regex_match: re.Match[str],
|
||||
| ^^^^^^^^^^^^^
|
||||
59 | bsd_db_shelf: shelve.BsdDbShelf[str, int],
|
||||
60 | db_filename_shelf: shelve.DbfilenameShelf[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:59:19
|
||||
|
|
||||
57 | regex_pattern: re.Pattern[str],
|
||||
58 | regex_match: re.Match[str],
|
||||
59 | bsd_db_shelf: shelve.BsdDbShelf[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
60 | db_filename_shelf: shelve.DbfilenameShelf[str, int],
|
||||
61 | shelf_obj: shelve.Shelf[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:60:24
|
||||
|
|
||||
58 | regex_match: re.Match[str],
|
||||
59 | bsd_db_shelf: shelve.BsdDbShelf[str, int],
|
||||
60 | db_filename_shelf: shelve.DbfilenameShelf[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
61 | shelf_obj: shelve.Shelf[str, int],
|
||||
62 | mapping_proxy: types.MappingProxyType[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:61:16
|
||||
|
|
||||
59 | bsd_db_shelf: shelve.BsdDbShelf[str, int],
|
||||
60 | db_filename_shelf: shelve.DbfilenameShelf[str, int],
|
||||
61 | shelf_obj: shelve.Shelf[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
62 | mapping_proxy: types.MappingProxyType[str, int],
|
||||
63 | weak_key_dict: weakref.WeakKeyDictionary[object, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:62:20
|
||||
|
|
||||
60 | db_filename_shelf: shelve.DbfilenameShelf[str, int],
|
||||
61 | shelf_obj: shelve.Shelf[str, int],
|
||||
62 | mapping_proxy: types.MappingProxyType[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
63 | weak_key_dict: weakref.WeakKeyDictionary[object, int],
|
||||
64 | weak_method: weakref.WeakMethod[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:63:20
|
||||
|
|
||||
61 | shelf_obj: shelve.Shelf[str, int],
|
||||
62 | mapping_proxy: types.MappingProxyType[str, int],
|
||||
63 | weak_key_dict: weakref.WeakKeyDictionary[object, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
64 | weak_method: weakref.WeakMethod[int],
|
||||
65 | weak_set: weakref.WeakSet[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:64:18
|
||||
|
|
||||
62 | mapping_proxy: types.MappingProxyType[str, int],
|
||||
63 | weak_key_dict: weakref.WeakKeyDictionary[object, int],
|
||||
64 | weak_method: weakref.WeakMethod[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
65 | weak_set: weakref.WeakSet[int],
|
||||
66 | weak_value_dict: weakref.WeakValueDictionary[object, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:65:15
|
||||
|
|
||||
63 | weak_key_dict: weakref.WeakKeyDictionary[object, int],
|
||||
64 | weak_method: weakref.WeakMethod[int],
|
||||
65 | weak_set: weakref.WeakSet[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
66 | weak_value_dict: weakref.WeakValueDictionary[object, int],
|
||||
67 | awaitable: Awaitable[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:66:22
|
||||
|
|
||||
64 | weak_method: weakref.WeakMethod[int],
|
||||
65 | weak_set: weakref.WeakSet[int],
|
||||
66 | weak_value_dict: weakref.WeakValueDictionary[object, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
67 | awaitable: Awaitable[int],
|
||||
68 | coroutine: Coroutine[int, None, str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:67:16
|
||||
|
|
||||
65 | weak_set: weakref.WeakSet[int],
|
||||
66 | weak_value_dict: weakref.WeakValueDictionary[object, int],
|
||||
67 | awaitable: Awaitable[int],
|
||||
| ^^^^^^^^^^^^^^
|
||||
68 | coroutine: Coroutine[int, None, str],
|
||||
69 | async_iterable: AsyncIterable[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:68:16
|
||||
|
|
||||
66 | weak_value_dict: weakref.WeakValueDictionary[object, int],
|
||||
67 | awaitable: Awaitable[int],
|
||||
68 | coroutine: Coroutine[int, None, str],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
69 | async_iterable: AsyncIterable[int],
|
||||
70 | async_iterator: AsyncIterator[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:69:21
|
||||
|
|
||||
67 | awaitable: Awaitable[int],
|
||||
68 | coroutine: Coroutine[int, None, str],
|
||||
69 | async_iterable: AsyncIterable[int],
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
70 | async_iterator: AsyncIterator[int],
|
||||
71 | async_generator: AsyncGenerator[int, None],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:70:21
|
||||
|
|
||||
68 | coroutine: Coroutine[int, None, str],
|
||||
69 | async_iterable: AsyncIterable[int],
|
||||
70 | async_iterator: AsyncIterator[int],
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
71 | async_generator: AsyncGenerator[int, None],
|
||||
72 | iterable: Iterable[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:71:22
|
||||
|
|
||||
69 | async_iterable: AsyncIterable[int],
|
||||
70 | async_iterator: AsyncIterator[int],
|
||||
71 | async_generator: AsyncGenerator[int, None],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
72 | iterable: Iterable[int],
|
||||
73 | iterator: Iterator[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:72:15
|
||||
|
|
||||
70 | async_iterator: AsyncIterator[int],
|
||||
71 | async_generator: AsyncGenerator[int, None],
|
||||
72 | iterable: Iterable[int],
|
||||
| ^^^^^^^^^^^^^
|
||||
73 | iterator: Iterator[int],
|
||||
74 | generator: Generator[int, None, None],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:73:15
|
||||
|
|
||||
71 | async_generator: AsyncGenerator[int, None],
|
||||
72 | iterable: Iterable[int],
|
||||
73 | iterator: Iterator[int],
|
||||
| ^^^^^^^^^^^^^
|
||||
74 | generator: Generator[int, None, None],
|
||||
75 | reversible: Reversible[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:74:16
|
||||
|
|
||||
72 | iterable: Iterable[int],
|
||||
73 | iterator: Iterator[int],
|
||||
74 | generator: Generator[int, None, None],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
75 | reversible: Reversible[int],
|
||||
76 | container: Container[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:75:17
|
||||
|
|
||||
73 | iterator: Iterator[int],
|
||||
74 | generator: Generator[int, None, None],
|
||||
75 | reversible: Reversible[int],
|
||||
| ^^^^^^^^^^^^^^^
|
||||
76 | container: Container[int],
|
||||
77 | collection: Collection[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:76:16
|
||||
|
|
||||
74 | generator: Generator[int, None, None],
|
||||
75 | reversible: Reversible[int],
|
||||
76 | container: Container[int],
|
||||
| ^^^^^^^^^^^^^^
|
||||
77 | collection: Collection[int],
|
||||
78 | callable_obj: Callable[[int], str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:77:17
|
||||
|
|
||||
75 | reversible: Reversible[int],
|
||||
76 | container: Container[int],
|
||||
77 | collection: Collection[int],
|
||||
| ^^^^^^^^^^^^^^^
|
||||
78 | callable_obj: Callable[[int], str],
|
||||
79 | set_obj: Set[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:78:19
|
||||
|
|
||||
76 | container: Container[int],
|
||||
77 | collection: Collection[int],
|
||||
78 | callable_obj: Callable[[int], str],
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
79 | set_obj: Set[int],
|
||||
80 | mutable_set: MutableSet[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:79:14
|
||||
|
|
||||
77 | collection: Collection[int],
|
||||
78 | callable_obj: Callable[[int], str],
|
||||
79 | set_obj: Set[int],
|
||||
| ^^^^^^^^
|
||||
80 | mutable_set: MutableSet[int],
|
||||
81 | mapping: Mapping[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:80:18
|
||||
|
|
||||
78 | callable_obj: Callable[[int], str],
|
||||
79 | set_obj: Set[int],
|
||||
80 | mutable_set: MutableSet[int],
|
||||
| ^^^^^^^^^^^^^^^
|
||||
81 | mapping: Mapping[str, int],
|
||||
82 | mutable_mapping: MutableMapping[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:81:14
|
||||
|
|
||||
79 | set_obj: Set[int],
|
||||
80 | mutable_set: MutableSet[int],
|
||||
81 | mapping: Mapping[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
82 | mutable_mapping: MutableMapping[str, int],
|
||||
83 | sequence: Sequence[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:82:22
|
||||
|
|
||||
80 | mutable_set: MutableSet[int],
|
||||
81 | mapping: Mapping[str, int],
|
||||
82 | mutable_mapping: MutableMapping[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
83 | sequence: Sequence[int],
|
||||
84 | mutable_sequence: MutableSequence[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:83:15
|
||||
|
|
||||
81 | mapping: Mapping[str, int],
|
||||
82 | mutable_mapping: MutableMapping[str, int],
|
||||
83 | sequence: Sequence[int],
|
||||
| ^^^^^^^^^^^^^
|
||||
84 | mutable_sequence: MutableSequence[int],
|
||||
85 | byte_string: ByteString[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:84:23
|
||||
|
|
||||
82 | mutable_mapping: MutableMapping[str, int],
|
||||
83 | sequence: Sequence[int],
|
||||
84 | mutable_sequence: MutableSequence[int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
85 | byte_string: ByteString[int],
|
||||
86 | mapping_view: MappingView[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:85:18
|
||||
|
|
||||
83 | sequence: Sequence[int],
|
||||
84 | mutable_sequence: MutableSequence[int],
|
||||
85 | byte_string: ByteString[int],
|
||||
| ^^^^^^^^^^^^^^^
|
||||
86 | mapping_view: MappingView[str, int],
|
||||
87 | keys_view: KeysView[str],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:86:19
|
||||
|
|
||||
84 | mutable_sequence: MutableSequence[int],
|
||||
85 | byte_string: ByteString[int],
|
||||
86 | mapping_view: MappingView[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
87 | keys_view: KeysView[str],
|
||||
88 | items_view: ItemsView[str, int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:87:16
|
||||
|
|
||||
85 | byte_string: ByteString[int],
|
||||
86 | mapping_view: MappingView[str, int],
|
||||
87 | keys_view: KeysView[str],
|
||||
| ^^^^^^^^^^^^^
|
||||
88 | items_view: ItemsView[str, int],
|
||||
89 | values_view: ValuesView[int],
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:88:17
|
||||
|
|
||||
86 | mapping_view: MappingView[str, int],
|
||||
87 | keys_view: KeysView[str],
|
||||
88 | items_view: ItemsView[str, int],
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
89 | values_view: ValuesView[int],
|
||||
90 | ) -> None:
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
--> no_future_import_uses_preview_generics.py:89:18
|
||||
|
|
||||
87 | keys_view: KeysView[str],
|
||||
88 | items_view: ItemsView[str, int],
|
||||
89 | values_view: ValuesView[int],
|
||||
| ^^^^^^^^^^^^^^^
|
||||
90 | ) -> None:
|
||||
91 | ...
|
||||
|
|
||||
help: Add `from __future__ import annotations`
|
||||
1 + from __future__ import annotations
|
||||
2 | import asyncio
|
||||
3 | import collections
|
||||
4 | import contextlib
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -2,7 +2,7 @@ use ast::FStringFlags;
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags};
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags, str::Quote};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -115,6 +115,8 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<
|
||||
}
|
||||
|
||||
let mut f_string_elements = Vec::with_capacity(joinees.len() * 2);
|
||||
let mut has_single_quote = joiner.contains('\'');
|
||||
let mut has_double_quote = joiner.contains('"');
|
||||
let mut first = true;
|
||||
|
||||
for expr in joinees {
|
||||
@@ -126,14 +128,31 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<
|
||||
if !std::mem::take(&mut first) {
|
||||
f_string_elements.push(helpers::to_interpolated_string_literal_element(joiner));
|
||||
}
|
||||
f_string_elements.push(helpers::to_interpolated_string_element(expr)?);
|
||||
let element = helpers::to_interpolated_string_element(expr)?;
|
||||
if let ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
|
||||
value,
|
||||
..
|
||||
}) = &element
|
||||
{
|
||||
has_single_quote |= value.contains('\'');
|
||||
has_double_quote |= value.contains('"');
|
||||
}
|
||||
f_string_elements.push(element);
|
||||
}
|
||||
|
||||
let quote = flags.quote_style();
|
||||
let adjusted_quote = match quote {
|
||||
Quote::Single if has_single_quote && !has_double_quote => quote.opposite(),
|
||||
Quote::Double if has_double_quote && !has_single_quote => quote.opposite(),
|
||||
_ if has_double_quote && has_single_quote => return None,
|
||||
_ => quote,
|
||||
};
|
||||
|
||||
let node = ast::FString {
|
||||
elements: f_string_elements.into(),
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
flags,
|
||||
flags: flags.with_quote_style(adjusted_quote),
|
||||
};
|
||||
Some(node.into())
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@ FLY002 [*] Consider `f"{url}{filename}"` instead of string join
|
||||
24 | def create_file_public_url(url, filename):
|
||||
25 | return''.join([url, filename])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
26 |
|
||||
27 | # Regression test for: https://github.com/astral-sh/ruff/issues/19837
|
||||
|
|
||||
help: Replace with `f"{url}{filename}"`
|
||||
22 |
|
||||
@@ -160,4 +162,47 @@ help: Replace with `f"{url}{filename}"`
|
||||
24 | def create_file_public_url(url, filename):
|
||||
- return''.join([url, filename])
|
||||
25 + return f"{url}{filename}"
|
||||
26 |
|
||||
27 | # Regression test for: https://github.com/astral-sh/ruff/issues/19837
|
||||
28 | nok10 = "".join((foo, '"'))
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FLY002 [*] Consider `f'{foo}"'` instead of string join
|
||||
--> FLY002.py:28:9
|
||||
|
|
||||
27 | # Regression test for: https://github.com/astral-sh/ruff/issues/19837
|
||||
28 | nok10 = "".join((foo, '"'))
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
29 | nok11 = ''.join((foo, "'"))
|
||||
30 | nok12 = ''.join([foo, "'", '"'])
|
||||
|
|
||||
help: Replace with `f'{foo}"'`
|
||||
25 | return''.join([url, filename])
|
||||
26 |
|
||||
27 | # Regression test for: https://github.com/astral-sh/ruff/issues/19837
|
||||
- nok10 = "".join((foo, '"'))
|
||||
28 + nok10 = f'{foo}"'
|
||||
29 | nok11 = ''.join((foo, "'"))
|
||||
30 | nok12 = ''.join([foo, "'", '"'])
|
||||
31 | nok13 = "".join([foo, "'", '"'])
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FLY002 [*] Consider `f"{foo}'"` instead of string join
|
||||
--> FLY002.py:29:9
|
||||
|
|
||||
27 | # Regression test for: https://github.com/astral-sh/ruff/issues/19837
|
||||
28 | nok10 = "".join((foo, '"'))
|
||||
29 | nok11 = ''.join((foo, "'"))
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
30 | nok12 = ''.join([foo, "'", '"'])
|
||||
31 | nok13 = "".join([foo, "'", '"'])
|
||||
|
|
||||
help: Replace with `f"{foo}'"`
|
||||
26 |
|
||||
27 | # Regression test for: https://github.com/astral-sh/ruff/issues/19837
|
||||
28 | nok10 = "".join((foo, '"'))
|
||||
- nok11 = ''.join((foo, "'"))
|
||||
29 + nok11 = f"{foo}'"
|
||||
30 | nok12 = ''.join([foo, "'", '"'])
|
||||
31 | nok13 = "".join([foo, "'", '"'])
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -25,9 +25,11 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as the use of a `set` literal will
|
||||
/// error at runtime if the sequence contains unhashable elements (like lists
|
||||
/// or dictionaries). While Ruff will attempt to infer the hashability of the
|
||||
/// elements, it may not always be able to do so.
|
||||
/// error at runtime if either the element being tested for membership (the
|
||||
/// left-hand side) or any element of the sequence (the right-hand side)
|
||||
/// is unhashable (like lists or dictionaries). While Ruff will attempt to
|
||||
/// infer the hashability of both sides and skip the fix when it can determine
|
||||
/// that either side is unhashable, it may not always be able to do so.
|
||||
///
|
||||
/// ## References
|
||||
/// - [What’s New In Python 3.2](https://docs.python.org/3/whatsnew/3.2.html#optimizations)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use get_size2::{GetSize, StandardTracker};
|
||||
use ordermap::OrderSet;
|
||||
use ordermap::{OrderMap, OrderSet};
|
||||
|
||||
/// Returns the memory usage of the provided object, using a global tracker to avoid
|
||||
/// double-counting shared objects.
|
||||
@@ -18,3 +18,11 @@ pub fn heap_size<T: GetSize>(value: &T) -> usize {
|
||||
pub fn order_set_heap_size<T: GetSize, S>(set: &OrderSet<T, S>) -> usize {
|
||||
(set.capacity() * T::get_stack_size()) + set.iter().map(heap_size).sum::<usize>()
|
||||
}
|
||||
|
||||
/// An implementation of [`GetSize::get_heap_size`] for [`OrderMap`].
|
||||
pub fn order_map_heap_size<K: GetSize, V: GetSize, S>(map: &OrderMap<K, V, S>) -> usize {
|
||||
(map.capacity() * (K::get_stack_size() + V::get_stack_size()))
|
||||
+ (map.iter())
|
||||
.map(|(k, v)| heap_size(k) + heap_size(v))
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ use ruff_python_ast::{
|
||||
StmtAssign,
|
||||
};
|
||||
use ruff_python_stdlib::typing::{
|
||||
as_pep_585_generic, has_pep_585_generic, is_immutable_generic_type,
|
||||
is_immutable_non_generic_type, is_immutable_return_type, is_literal_member,
|
||||
is_mutable_return_type, is_pep_593_generic_member, is_pep_593_generic_type,
|
||||
is_standard_library_generic, is_standard_library_generic_member, is_standard_library_literal,
|
||||
is_typed_dict, is_typed_dict_member,
|
||||
as_pep_585_generic, is_immutable_generic_type, is_immutable_non_generic_type,
|
||||
is_immutable_return_type, is_literal_member, is_mutable_return_type, is_pep_593_generic_member,
|
||||
is_pep_593_generic_type, is_standard_library_generic, is_standard_library_generic_member,
|
||||
is_standard_library_literal, is_typed_dict, is_typed_dict_member,
|
||||
};
|
||||
use ruff_text_size::Ranged;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
@@ -148,14 +147,46 @@ pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<Module
|
||||
}
|
||||
|
||||
/// Return whether a given expression uses a PEP 585 standard library generic.
|
||||
pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
pub fn is_pep585_generic(
|
||||
expr: &Expr,
|
||||
semantic: &SemanticModel,
|
||||
include_preview_generics: bool,
|
||||
) -> bool {
|
||||
semantic
|
||||
.resolve_qualified_name(expr)
|
||||
.is_some_and(|qualified_name| {
|
||||
let [module, name] = qualified_name.segments() else {
|
||||
return false;
|
||||
};
|
||||
has_pep_585_generic(module, name)
|
||||
.is_some_and(|qualified_name| match qualified_name.segments() {
|
||||
["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"]
|
||||
| ["collections", "deque" | "defaultdict"] => true,
|
||||
["asyncio", "Future" | "Task"]
|
||||
| ["collections", "ChainMap" | "Counter" | "OrderedDict"]
|
||||
| [
|
||||
"contextlib",
|
||||
"AbstractAsyncContextManager" | "AbstractContextManager",
|
||||
]
|
||||
| ["dataclasses", "Field"]
|
||||
| ["functools", "cached_property" | "partialmethod"]
|
||||
| ["os", "PathLike"]
|
||||
| [
|
||||
"queue",
|
||||
"LifoQueue" | "PriorityQueue" | "Queue" | "SimpleQueue",
|
||||
]
|
||||
| ["re", "Match" | "Pattern"]
|
||||
| ["shelve", "BsdDbShelf" | "DbfilenameShelf" | "Shelf"]
|
||||
| ["types", "MappingProxyType"]
|
||||
| [
|
||||
"weakref",
|
||||
"WeakKeyDictionary" | "WeakMethod" | "WeakSet" | "WeakValueDictionary",
|
||||
]
|
||||
| [
|
||||
"collections",
|
||||
"abc",
|
||||
"AsyncGenerator" | "AsyncIterable" | "AsyncIterator" | "Awaitable" | "ByteString"
|
||||
| "Callable" | "Collection" | "Container" | "Coroutine" | "Generator" | "ItemsView"
|
||||
| "Iterable" | "Iterator" | "KeysView" | "Mapping" | "MappingView"
|
||||
| "MutableMapping" | "MutableSequence" | "MutableSet" | "Reversible" | "Sequence"
|
||||
| "Set" | "ValuesView",
|
||||
] => include_preview_generics,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -354,20 +354,6 @@ pub fn as_pep_585_generic(module: &str, member: &str) -> Option<ModuleMember> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a typing member, returns `true` if a generic equivalent exists in the Python standard
|
||||
/// library (e.g., `list` for `typing.List`), as introduced by [PEP 585].
|
||||
///
|
||||
/// [PEP 585]: https://peps.python.org/pep-0585/
|
||||
pub fn has_pep_585_generic(module: &str, member: &str) -> bool {
|
||||
// Constructed by taking every pattern from `as_pep_585_generic`, removing all but
|
||||
// the last element in each pattern, and de-duplicating the values.
|
||||
matches!(
|
||||
(module, member),
|
||||
("", "dict" | "frozenset" | "list" | "set" | "tuple" | "type")
|
||||
| ("collections", "deque" | "defaultdict")
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the expected return type for a magic method.
|
||||
///
|
||||
/// See: <https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
12
crates/ty/docs/cli.md
generated
12
crates/ty/docs/cli.md
generated
@@ -54,7 +54,8 @@ over all configuration files.</p>
|
||||
</dd><dt id="ty-check--exclude"><a href="#ty-check--exclude"><code>--exclude</code></a> <i>exclude</i></dt><dd><p>Glob patterns for files to exclude from type checking.</p>
|
||||
<p>Uses gitignore-style syntax to exclude files and directories from type checking. Supports patterns like <code>tests/</code>, <code>*.tmp</code>, <code>**/__pycache__/**</code>.</p>
|
||||
</dd><dt id="ty-check--exit-zero"><a href="#ty-check--exit-zero"><code>--exit-zero</code></a></dt><dd><p>Always use exit code 0, even when there are error-level diagnostics</p>
|
||||
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times)</p>
|
||||
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times).</p>
|
||||
<p>This is an advanced option that should usually only be used for first-party or third-party modules that are not installed into your Python environment in a conventional way. Use <code>--python</code> to point ty to your Python environment if it is in an unusual location.</p>
|
||||
</dd><dt id="ty-check--help"><a href="#ty-check--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help (see a summary with '-h')</p>
|
||||
</dd><dt id="ty-check--ignore"><a href="#ty-check--ignore"><code>--ignore</code></a> <i>rule</i></dt><dd><p>Disables the rule. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
|
||||
@@ -67,11 +68,10 @@ over all configuration files.</p>
|
||||
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
||||
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>
|
||||
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
|
||||
</dd><dt id="ty-check--python"><a href="#ty-check--python"><code>--python</code></a> <i>path</i></dt><dd><p>Path to the Python environment.</p>
|
||||
<p>ty uses the Python environment to resolve type information and third-party dependencies.</p>
|
||||
<p>If not specified, ty will attempt to infer it from the <code>VIRTUAL_ENV</code> or <code>CONDA_PREFIX</code> environment variables, or discover a <code>.venv</code> directory in the project root or working directory.</p>
|
||||
<p>If a path to a Python interpreter is provided, e.g., <code>.venv/bin/python3</code>, ty will attempt to find an environment two directories up from the interpreter's path, e.g., <code>.venv</code>. At this time, ty does not invoke the interpreter to determine the location of the environment. This means that ty will not resolve dynamic executables such as a shim.</p>
|
||||
<p>ty will search in the resolved environment's <code>site-packages</code> directories for type information and third-party imports.</p>
|
||||
</dd><dt id="ty-check--python"><a href="#ty-check--python"><code>--python</code></a>, <code>--venv</code> <i>path</i></dt><dd><p>Path to your project's Python environment or interpreter.</p>
|
||||
<p>ty uses your Python environment to resolve third-party imports in your code.</p>
|
||||
<p>If you're using a project management tool such as uv or you have an activated Conda or virtual environment, you should not generally need to specify this option.</p>
|
||||
<p>This option can be used to point to virtual or system Python environments.</p>
|
||||
</dd><dt id="ty-check--python-platform"><a href="#ty-check--python-platform"><code>--python-platform</code></a>, <code>--platform</code> <i>platform</i></dt><dd><p>Target platform to assume when resolving types.</p>
|
||||
<p>This is used to specialize the type of <code>sys.platform</code> and will affect the visibility of platform-specific functions and attributes. If the value is set to <code>all</code>, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.</p>
|
||||
</dd><dt id="ty-check--python-version"><a href="#ty-check--python-version"><code>--python-version</code></a>, <code>--target-version</code> <i>version</i></dt><dd><p>Python version to assume when resolving types.</p>
|
||||
|
||||
30
crates/ty/docs/configuration.md
generated
30
crates/ty/docs/configuration.md
generated
@@ -32,9 +32,14 @@ division-by-zero = "ignore"
|
||||
|
||||
### `extra-paths`
|
||||
|
||||
List of user-provided paths that should take first priority in the module resolution.
|
||||
Examples in other type checkers are mypy's `MYPYPATH` environment variable,
|
||||
or pyright's `stubPath` configuration setting.
|
||||
User-provided paths that should take first priority in module resolution.
|
||||
|
||||
This is an advanced option that should usually only be used for first-party or third-party
|
||||
modules that are not installed into your Python environment in a conventional way.
|
||||
Use the `python` option to specify the location of your Python environment.
|
||||
|
||||
This option is similar to mypy's `MYPYPATH` environment variable and pyright's `stubPath`
|
||||
configuration setting.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
@@ -51,12 +56,21 @@ extra-paths = ["./shared/my-search-path"]
|
||||
|
||||
### `python`
|
||||
|
||||
Path to the Python installation from which ty resolves type information and third-party dependencies.
|
||||
Path to your project's Python environment or interpreter.
|
||||
|
||||
ty will search in the path's `site-packages` directories for type information and
|
||||
third-party imports.
|
||||
ty uses the `site-packages` directory of your project's Python environment
|
||||
to resolve third-party (and, in some cases, first-party) imports in your code.
|
||||
|
||||
This option is commonly used to specify the path to a virtual environment.
|
||||
If you're using a project management tool such as uv, you should not generally need
|
||||
to specify this option, as commands such as `uv run` will set the `VIRTUAL_ENV`
|
||||
environment variable to point to your project's virtual environment. ty can also infer
|
||||
the location of your environment from an activated Conda environment, and will look for
|
||||
a `.venv` directory in the project root if none of the above apply.
|
||||
|
||||
Passing a path to a Python executable is supported, but passing a path to a dynamic executable
|
||||
(such as a shim) is not currently supported.
|
||||
|
||||
This option can be used to point to virtual or system Python environments.
|
||||
|
||||
**Default value**: `null`
|
||||
|
||||
@@ -66,7 +80,7 @@ This option is commonly used to specify the path to a virtual environment.
|
||||
|
||||
```toml
|
||||
[tool.ty.environment]
|
||||
python = "./.venv"
|
||||
python = "./custom-venv-location/.venv"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -35,11 +35,11 @@ ty also reads the following externally defined environment variables:
|
||||
|
||||
### `CONDA_DEFAULT_ENV`
|
||||
|
||||
Used to determine if an active Conda environment is the base environment or not.
|
||||
Used to determine the name of the active Conda environment.
|
||||
|
||||
### `CONDA_PREFIX`
|
||||
|
||||
Used to detect an activated Conda environment location.
|
||||
Used to detect the path of an active Conda environment.
|
||||
If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.
|
||||
|
||||
### `PYTHONPATH`
|
||||
@@ -64,3 +64,7 @@ Used to detect an activated virtual environment.
|
||||
|
||||
Path to user-level configuration directory on Unix systems.
|
||||
|
||||
### `_CONDA_ROOT`
|
||||
|
||||
Used to determine the root install path of Conda.
|
||||
|
||||
|
||||
@@ -51,22 +51,15 @@ pub(crate) struct CheckCommand {
|
||||
#[arg(long, value_name = "PROJECT")]
|
||||
pub(crate) project: Option<SystemPathBuf>,
|
||||
|
||||
/// Path to the Python environment.
|
||||
/// Path to your project's Python environment or interpreter.
|
||||
///
|
||||
/// ty uses the Python environment to resolve type information and third-party dependencies.
|
||||
/// ty uses your Python environment to resolve third-party imports in your code.
|
||||
///
|
||||
/// If not specified, ty will attempt to infer it from the `VIRTUAL_ENV` or `CONDA_PREFIX`
|
||||
/// environment variables, or discover a `.venv` directory in the project root or working
|
||||
/// directory.
|
||||
/// If you're using a project management tool such as uv or you have an activated Conda or virtual
|
||||
/// environment, you should not generally need to specify this option.
|
||||
///
|
||||
/// If a path to a Python interpreter is provided, e.g., `.venv/bin/python3`, ty will attempt to
|
||||
/// find an environment two directories up from the interpreter's path, e.g., `.venv`. At this
|
||||
/// time, ty does not invoke the interpreter to determine the location of the environment. This
|
||||
/// means that ty will not resolve dynamic executables such as a shim.
|
||||
///
|
||||
/// ty will search in the resolved environment's `site-packages` directories for type
|
||||
/// information and third-party imports.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
/// This option can be used to point to virtual or system Python environments.
|
||||
#[arg(long, value_name = "PATH", alias = "venv")]
|
||||
pub(crate) python: Option<SystemPathBuf>,
|
||||
|
||||
/// Custom directory to use for stdlib typeshed stubs.
|
||||
@@ -74,6 +67,10 @@ pub(crate) struct CheckCommand {
|
||||
pub(crate) typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// Additional path to use as a module-resolution source (can be passed multiple times).
|
||||
///
|
||||
/// This is an advanced option that should usually only be used for first-party or third-party
|
||||
/// modules that are not installed into your Python environment in a conventional way.
|
||||
/// Use `--python` to point ty to your Python environment if it is in an unusual location.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub(crate) extra_search_path: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
|
||||
@@ -943,9 +943,10 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
/// The `site-packages` directory is used by ty for external import.
|
||||
/// Ty does the following checks to discover the `site-packages` directory in the order:
|
||||
/// 1) If `VIRTUAL_ENV` environment variable is set
|
||||
/// 2) If `CONDA_PREFIX` environment variable is set (and .filename != `CONDA_DEFAULT_ENV`)
|
||||
/// 2) If `CONDA_PREFIX` environment variable is set (and .filename == `CONDA_DEFAULT_ENV`)
|
||||
/// 3) If a `.venv` directory exists at the project root
|
||||
/// 4) If `CONDA_PREFIX` environment variable is set (and .filename == `CONDA_DEFAULT_ENV`)
|
||||
/// 4) If `CONDA_PREFIX` environment variable is set (and .filename != `CONDA_DEFAULT_ENV`)
|
||||
/// or if `_CONDA_ROOT` is set (and `_CONDA_ROOT` == `CONDA_PREFIX`)
|
||||
///
|
||||
/// This test (and the next one) is aiming at validating the logic around these cases.
|
||||
///
|
||||
@@ -986,15 +987,14 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
/// │ └── site-packages
|
||||
/// │ └── package1
|
||||
/// │ └── __init__.py
|
||||
/// ├── conda-env
|
||||
/// │ └── lib
|
||||
/// │ └── python3.13
|
||||
/// │ └── site-packages
|
||||
/// │ └── package1
|
||||
/// │ └── __init__.py
|
||||
/// └── conda
|
||||
/// ├── lib
|
||||
/// │ └── python3.13
|
||||
/// │ └── site-packages
|
||||
/// │ └── package1
|
||||
/// │ └── __init__.py
|
||||
/// └── envs
|
||||
/// └── base
|
||||
/// └── conda-env
|
||||
/// └── lib
|
||||
/// └── python3.13
|
||||
/// └── site-packages
|
||||
@@ -1006,15 +1006,15 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
#[test]
|
||||
fn check_venv_resolution_with_working_venv() -> anyhow::Result<()> {
|
||||
let child_conda_package1_path = if cfg!(windows) {
|
||||
"conda-env/Lib/site-packages/package1/__init__.py"
|
||||
"conda/envs/conda-env/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
"conda/envs/conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let base_conda_package1_path = if cfg!(windows) {
|
||||
"conda/envs/base/Lib/site-packages/package1/__init__.py"
|
||||
"conda/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
|
||||
"conda//lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let working_venv_package1_path = if cfg!(windows) {
|
||||
@@ -1136,7 +1136,7 @@ home = ./
|
||||
// run with CONDA_PREFIX set, should find the child conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -1157,61 +1157,10 @@ home = ./
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find working venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
|
||||
// should find child active venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_DEFAULT_ENV", "base")
|
||||
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
|
||||
--> test.py:2:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
| ^^^^^^^^^^
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find working venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -1233,6 +1182,106 @@ home = ./
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
|
||||
// should find child active venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda"))
|
||||
.env("CONDA_DEFAULT_ENV", "base")
|
||||
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
|
||||
--> test.py:2:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
| ^^^^^^^^^^
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find ChildConda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
|
||||
.env("CONDA_DEFAULT_ENV", "conda-env"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with _CONDA_ROOT and CONDA_PREFIX (unequal!) set, should find ChildConda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
|
||||
.env("_CONDA_ROOT", "conda"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with _CONDA_ROOT and CONDA_PREFIX (equal!) set, should find BaseConda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda"))
|
||||
.env("_CONDA_ROOT", "conda"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `BaseConda`
|
||||
--> test.py:5:22
|
||||
|
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1242,15 +1291,15 @@ home = ./
|
||||
#[test]
|
||||
fn check_venv_resolution_without_working_venv() -> anyhow::Result<()> {
|
||||
let child_conda_package1_path = if cfg!(windows) {
|
||||
"conda-env/Lib/site-packages/package1/__init__.py"
|
||||
"conda/envs/conda-env/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
"conda/envs/conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let base_conda_package1_path = if cfg!(windows) {
|
||||
"conda/envs/base/Lib/site-packages/package1/__init__.py"
|
||||
"conda/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
|
||||
"conda/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let active_venv_package1_path = if cfg!(windows) {
|
||||
@@ -1398,7 +1447,7 @@ home = ./
|
||||
// run with CONDA_PREFIX set, should find the child conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -1419,22 +1468,21 @@ home = ./
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find base conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
error[unresolved-import]: Module `package1` has no member `BaseConda`
|
||||
--> test.py:5:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
@@ -1448,7 +1496,7 @@ home = ./
|
||||
// should find child active venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda"))
|
||||
.env("CONDA_DEFAULT_ENV", "base")
|
||||
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
|
||||
success: false
|
||||
@@ -1470,10 +1518,10 @@ home = ./
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find base conda
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal!) set, should find base conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -1494,6 +1542,55 @@ home = ./
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with _CONDA_ROOT and CONDA_PREFIX (unequal!) set, should find ChildConda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
|
||||
.env("_CONDA_ROOT", "conda"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with _CONDA_ROOT and CONDA_PREFIX (equal!) set, should find BaseConda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda"))
|
||||
.env("_CONDA_ROOT", "conda"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `BaseConda`
|
||||
--> test.py:5:22
|
||||
|
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1640,9 +1640,9 @@ quux.<CURSOR>
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
|
||||
bar :: Unknown | Literal[2]
|
||||
baz :: Unknown | Literal[3]
|
||||
foo :: Unknown | Literal[1]
|
||||
bar :: Unknown
|
||||
baz :: Unknown
|
||||
foo :: Unknown
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type[Quux]
|
||||
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
|
||||
@@ -1685,9 +1685,9 @@ quux.b<CURSOR>
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
|
||||
bar :: Unknown | Literal[2]
|
||||
baz :: Unknown | Literal[3]
|
||||
foo :: Unknown | Literal[1]
|
||||
bar :: Unknown
|
||||
baz :: Unknown
|
||||
foo :: Unknown
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type[Quux]
|
||||
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
|
||||
|
||||
@@ -550,9 +550,14 @@ pub struct EnvironmentOptions {
|
||||
)]
|
||||
pub python_platform: Option<RangedValue<PythonPlatform>>,
|
||||
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's `MYPYPATH` environment variable,
|
||||
/// or pyright's `stubPath` configuration setting.
|
||||
/// User-provided paths that should take first priority in module resolution.
|
||||
///
|
||||
/// This is an advanced option that should usually only be used for first-party or third-party
|
||||
/// modules that are not installed into your Python environment in a conventional way.
|
||||
/// Use the `python` option to specify the location of your Python environment.
|
||||
///
|
||||
/// This option is similar to mypy's `MYPYPATH` environment variable and pyright's `stubPath`
|
||||
/// configuration setting.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
@@ -576,18 +581,27 @@ pub struct EnvironmentOptions {
|
||||
)]
|
||||
pub typeshed: Option<RelativePathBuf>,
|
||||
|
||||
/// Path to the Python installation from which ty resolves type information and third-party dependencies.
|
||||
/// Path to your project's Python environment or interpreter.
|
||||
///
|
||||
/// ty will search in the path's `site-packages` directories for type information and
|
||||
/// third-party imports.
|
||||
/// ty uses the `site-packages` directory of your project's Python environment
|
||||
/// to resolve third-party (and, in some cases, first-party) imports in your code.
|
||||
///
|
||||
/// This option is commonly used to specify the path to a virtual environment.
|
||||
/// If you're using a project management tool such as uv, you should not generally need
|
||||
/// to specify this option, as commands such as `uv run` will set the `VIRTUAL_ENV`
|
||||
/// environment variable to point to your project's virtual environment. ty can also infer
|
||||
/// the location of your environment from an activated Conda environment, and will look for
|
||||
/// a `.venv` directory in the project root if none of the above apply.
|
||||
///
|
||||
/// Passing a path to a Python executable is supported, but passing a path to a dynamic executable
|
||||
/// (such as a shim) is not currently supported.
|
||||
///
|
||||
/// This option can be used to point to virtual or system Python environments.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"null"#,
|
||||
value_type = "str",
|
||||
example = r#"
|
||||
python = "./.venv"
|
||||
python = "./custom-venv-location/.venv"
|
||||
"#
|
||||
)]
|
||||
pub python: Option<RelativePathBuf>,
|
||||
|
||||
@@ -25,7 +25,8 @@ class C:
|
||||
|
||||
c_instance = C(1)
|
||||
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
@@ -35,7 +36,8 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
# something that we might want to change in the future.
|
||||
#
|
||||
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown
|
||||
|
||||
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
|
||||
@@ -153,7 +155,8 @@ reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
|
||||
# which is planned in https://github.com/astral-sh/ruff/issues/14297
|
||||
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
|
||||
|
||||
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
|
||||
# TODO: Should be `Unknown | None | Literal["a"]`
|
||||
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None
|
||||
```
|
||||
|
||||
#### Variable defined in non-`__init__` method
|
||||
@@ -175,12 +178,14 @@ class C:
|
||||
|
||||
c_instance = C(1)
|
||||
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown
|
||||
|
||||
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
|
||||
@@ -224,7 +229,8 @@ class C:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.x) # revealed: Unknown | int | str
|
||||
# TODO: Should be `Unknown | int | str`
|
||||
reveal_type(c_instance.x) # revealed: Unknown
|
||||
reveal_type(c_instance.y) # revealed: int
|
||||
reveal_type(c_instance.z) # revealed: int
|
||||
```
|
||||
@@ -238,8 +244,10 @@ class C:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Augmented assignments
|
||||
@@ -256,7 +264,8 @@ class C:
|
||||
|
||||
# TODO: Mypy and pyright do not support this, but it would be great if we could
|
||||
# infer `Unknown | str` here (`Weird` is not a possible type for the `w` attribute).
|
||||
reveal_type(C().w) # revealed: Unknown | Weird
|
||||
# TODO: Should be `Unknown | Weird`
|
||||
reveal_type(C().w) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in tuple unpackings
|
||||
@@ -280,12 +289,16 @@ reveal_type(c_instance.b1) # revealed: Unknown | Literal["a"]
|
||||
reveal_type(c_instance.c1) # revealed: Unknown | int
|
||||
reveal_type(c_instance.d1) # revealed: Unknown | str
|
||||
|
||||
reveal_type(c_instance.a2) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(c_instance.a2) # revealed: Unknown
|
||||
|
||||
reveal_type(c_instance.b2) # revealed: Unknown | Literal["a"]
|
||||
# TODO: Should be `Unknown | Literal["a"]`
|
||||
reveal_type(c_instance.b2) # revealed: Unknown
|
||||
|
||||
reveal_type(c_instance.c2) # revealed: Unknown | int
|
||||
reveal_type(c_instance.d2) # revealed: Unknown | str
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(c_instance.c2) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | str`
|
||||
reveal_type(c_instance.d2) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Starred assignments
|
||||
@@ -296,8 +309,10 @@ class C:
|
||||
self.a, *self.b = (1, 2, 3)
|
||||
|
||||
c_instance = C()
|
||||
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b) # revealed: Unknown | list[Literal[2, 3]]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | list[Literal[2, 3]]`
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in for-loop (unpacking)
|
||||
@@ -333,8 +348,10 @@ class C:
|
||||
for self.z in NonIterable():
|
||||
pass
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | int
|
||||
reveal_type(C().y) # revealed: Unknown | str
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | str`
|
||||
reveal_type(C().y) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in `with` statements
|
||||
@@ -354,7 +371,8 @@ class C:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.x) # revealed: Unknown | int | None
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
reveal_type(c_instance.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in `with` statements, but with unpacking
|
||||
@@ -374,8 +392,10 @@ class C:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.x) # revealed: Unknown | int | None
|
||||
reveal_type(c_instance.y) # revealed: Unknown | int
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
reveal_type(c_instance.x) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(c_instance.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in comprehensions
|
||||
@@ -462,8 +482,10 @@ c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a1) # revealed: str | None
|
||||
reveal_type(c_instance.a2) # revealed: str | None
|
||||
reveal_type(c_instance.b1) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b2) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(c_instance.b1) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(c_instance.b2) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Methods that does not use `self` as a first parameter
|
||||
@@ -506,6 +528,7 @@ class C:
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
|
||||
@@ -536,7 +559,8 @@ class C:
|
||||
def f(self) -> None:
|
||||
self.x = 1
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
And if `staticmethod` is fully qualified, that should also be recognized:
|
||||
@@ -601,14 +625,18 @@ class C:
|
||||
self.e = e
|
||||
|
||||
# TODO: this would ideally be `Unknown | Literal[1]`
|
||||
reveal_type(C(True).a) # revealed: Unknown | Literal[1, "a"]
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(C(True).a) # revealed: Unknown
|
||||
# TODO: this would ideally raise an `unresolved-attribute` error
|
||||
reveal_type(C(True).b) # revealed: Unknown | Literal[2]
|
||||
reveal_type(C(True).c) # revealed: Unknown | Literal[3] | str
|
||||
# TODO: Should be `Unknown | Literal[2]`
|
||||
reveal_type(C(True).b) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | Literal[3] | str`
|
||||
reveal_type(C(True).c) # revealed: Unknown
|
||||
# Ideally, this would just be `Unknown | Literal[5]`, but we currently do not
|
||||
# attempt to analyze control flow within methods more closely. All reachable
|
||||
# attribute assignments are considered, so `self.x = 4` is also included:
|
||||
reveal_type(C(True).d) # revealed: Unknown | Literal[4, 5]
|
||||
# TODO: Should be `Unknown | Literal[4, 5]`
|
||||
reveal_type(C(True).d) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C(True).e) # revealed: Unknown
|
||||
```
|
||||
@@ -626,8 +654,10 @@ class C:
|
||||
# This is because, it is not possible to access a partially-initialized object by normal means.
|
||||
self.y = 2
|
||||
|
||||
reveal_type(C(False).x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(C(False).y) # revealed: Unknown | Literal[2]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(C(False).x) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | Literal[2]`
|
||||
reveal_type(C(False).y) # revealed: Unknown
|
||||
|
||||
class C:
|
||||
def __init__(self, b: bytes) -> None:
|
||||
@@ -640,8 +670,10 @@ class C:
|
||||
|
||||
self.s = s
|
||||
|
||||
reveal_type(C(b"abc").b) # revealed: Unknown | bytes
|
||||
reveal_type(C(b"abc").s) # revealed: Unknown | str
|
||||
# TODO: Should be `Unknown | bytes`
|
||||
reveal_type(C(b"abc").b) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | str`
|
||||
reveal_type(C(b"abc").s) # revealed: Unknown
|
||||
|
||||
class C:
|
||||
def __init__(self, iter) -> None:
|
||||
@@ -654,8 +686,10 @@ class C:
|
||||
# but we consider the subsequent attributes to be definitely-bound.
|
||||
self.y = 2
|
||||
|
||||
reveal_type(C([]).x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(C([]).y) # revealed: Unknown | Literal[2]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(C([]).x) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | Literal[2]`
|
||||
reveal_type(C([]).y) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Diagnostics are reported for the right-hand side of attribute assignments
|
||||
@@ -745,13 +779,15 @@ class C:
|
||||
# for a more realistic example, let's actually call the method
|
||||
C.class_method()
|
||||
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
|
||||
# TODO: Should be `Unknown | Literal["value set in class method"]`
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
C.pure_class_variable = "overwritten on class"
|
||||
reveal_type(C.pure_class_variable) # revealed: Literal["overwritten on class"]
|
||||
|
||||
c_instance = C()
|
||||
reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
|
||||
# TODO: Should be `Unknown | Literal["value set in class method"]`
|
||||
reveal_type(c_instance.pure_class_variable) # revealed: Unknown
|
||||
|
||||
# TODO: should raise an error.
|
||||
c_instance.pure_class_variable = "value set on instance"
|
||||
@@ -1335,11 +1371,13 @@ def _(flag: bool):
|
||||
else:
|
||||
self.y = "b"
|
||||
|
||||
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(Foo().x) # revealed: Unknown
|
||||
|
||||
Foo().x = 2
|
||||
|
||||
reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"]
|
||||
# TODO: Should be `Unknown | Literal["a", "b"]`
|
||||
reveal_type(Foo().y) # revealed: Unknown
|
||||
Foo().y = "c"
|
||||
```
|
||||
|
||||
@@ -2223,7 +2261,8 @@ class C:
|
||||
def copy(self, other: "C"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute:
|
||||
@@ -2279,8 +2318,10 @@ class B:
|
||||
def copy(self, other: "A"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(B().x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(A().x) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(B().x) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | Literal[1]`
|
||||
reveal_type(A().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
This case additionally tests our union/intersection simplification logic:
|
||||
@@ -2311,7 +2352,8 @@ class Toggle:
|
||||
self.y = True
|
||||
|
||||
reveal_type(Toggle().x) # revealed: Literal[True]
|
||||
reveal_type(Toggle().y) # revealed: Unknown | Literal[True]
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(Toggle().y) # revealed: Unknown
|
||||
```
|
||||
|
||||
Make sure that the growing union of literals `Literal[0, 1, 2, ...]` collapses to `int` during
|
||||
@@ -2325,7 +2367,8 @@ class Counter:
|
||||
def increment(self: "Counter"):
|
||||
self.count = self.count + 1
|
||||
|
||||
reveal_type(Counter().count) # revealed: Unknown | int
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(Counter().count) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Builtin types attributes
|
||||
@@ -2426,7 +2469,8 @@ class C:
|
||||
def f(self, other: "C"):
|
||||
self.x = (other.x, 1)
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
|
||||
# TODO: Should be `Unknown | tuple[Divergent, Literal[1]]`
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
@@ -92,7 +92,8 @@ reveal_type(this_fails[0]) # revealed: Unknown
|
||||
However, the attached dunder method *can* be called if accessed directly:
|
||||
|
||||
```py
|
||||
reveal_type(this_fails.__getitem__(this_fails, 0)) # revealed: Unknown | str
|
||||
# TODO: Should be `Unknown | str`
|
||||
reveal_type(this_fails.__getitem__(this_fails, 0)) # revealed: Unknown
|
||||
```
|
||||
|
||||
The instance-level method is also not called when the class-level method is present:
|
||||
|
||||
@@ -139,8 +139,7 @@ reveal_type(f(A())) # revealed: A
|
||||
reveal_type(f(*(A(),))) # revealed: A
|
||||
|
||||
reveal_type(f(B())) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(B(),))) # revealed: Unknown
|
||||
reveal_type(f(*(B(),))) # revealed: A
|
||||
|
||||
# But, in this case, the arity check filters out the first overload, so we only have one match:
|
||||
reveal_type(f(B(), 1)) # revealed: B
|
||||
@@ -551,16 +550,13 @@ from overloaded import MyEnumSubclass, ActualEnum, f
|
||||
|
||||
def _(actual_enum: ActualEnum, my_enum_instance: MyEnumSubclass):
|
||||
reveal_type(f(actual_enum)) # revealed: Both
|
||||
# TODO: revealed: Both
|
||||
reveal_type(f(*(actual_enum,))) # revealed: Unknown
|
||||
reveal_type(f(*(actual_enum,))) # revealed: Both
|
||||
|
||||
reveal_type(f(ActualEnum.A)) # revealed: OnlyA
|
||||
# TODO: revealed: OnlyA
|
||||
reveal_type(f(*(ActualEnum.A,))) # revealed: Unknown
|
||||
reveal_type(f(*(ActualEnum.A,))) # revealed: OnlyA
|
||||
|
||||
reveal_type(f(ActualEnum.B)) # revealed: OnlyB
|
||||
# TODO: revealed: OnlyB
|
||||
reveal_type(f(*(ActualEnum.B,))) # revealed: Unknown
|
||||
reveal_type(f(*(ActualEnum.B,))) # revealed: OnlyB
|
||||
|
||||
reveal_type(f(my_enum_instance)) # revealed: MyEnumSubclass
|
||||
reveal_type(f(*(my_enum_instance,))) # revealed: MyEnumSubclass
|
||||
@@ -1097,12 +1093,10 @@ reveal_type(f(*(1,))) # revealed: str
|
||||
|
||||
def _(list_int: list[int], list_any: list[Any]):
|
||||
reveal_type(f(list_int)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
reveal_type(f(*(list_int,))) # revealed: int
|
||||
|
||||
reveal_type(f(list_any)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
reveal_type(f(*(list_any,))) # revealed: int
|
||||
```
|
||||
|
||||
### Single list argument (ambiguous)
|
||||
@@ -1136,8 +1130,7 @@ def _(list_int: list[int], list_any: list[Any]):
|
||||
# All materializations of `list[int]` are assignable to `list[int]`, so it matches the first
|
||||
# overload.
|
||||
reveal_type(f(list_int)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
reveal_type(f(*(list_int,))) # revealed: int
|
||||
|
||||
# All materializations of `list[Any]` are assignable to `list[int]` and `list[Any]`, but the
|
||||
# return type of first and second overloads are not equivalent, so the overload matching
|
||||
@@ -1170,25 +1163,21 @@ reveal_type(f("a")) # revealed: str
|
||||
reveal_type(f(*("a",))) # revealed: str
|
||||
|
||||
reveal_type(f((1, "b"))) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*((1, "b"),))) # revealed: Unknown
|
||||
reveal_type(f(*((1, "b"),))) # revealed: int
|
||||
|
||||
reveal_type(f((1, 2))) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*((1, 2),))) # revealed: Unknown
|
||||
reveal_type(f(*((1, 2),))) # revealed: int
|
||||
|
||||
def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]):
|
||||
# All materializations are assignable to first overload, so second and third overloads are
|
||||
# eliminated
|
||||
reveal_type(f(int_str)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(int_str,))) # revealed: Unknown
|
||||
reveal_type(f(*(int_str,))) # revealed: int
|
||||
|
||||
# All materializations are assignable to second overload, so the third overload is eliminated;
|
||||
# the return type of first and second overload is equivalent
|
||||
reveal_type(f(int_any)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(int_any,))) # revealed: Unknown
|
||||
reveal_type(f(*(int_any,))) # revealed: int
|
||||
|
||||
# All materializations of `tuple[Any, Any]` are assignable to the parameters of all the
|
||||
# overloads, but the return types aren't equivalent, so the overload matching is ambiguous
|
||||
@@ -1266,26 +1255,22 @@ def _(list_int: list[int], list_any: list[Any], int_str: tuple[int, str], int_an
|
||||
# All materializations of both argument types are assignable to the first overload, so the
|
||||
# second and third overloads are filtered out
|
||||
reveal_type(f(list_int, int_str)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int, int_str))) # revealed: Unknown
|
||||
reveal_type(f(*(list_int, int_str))) # revealed: A
|
||||
|
||||
# All materialization of first argument is assignable to first overload and for the second
|
||||
# argument, they're assignable to the second overload, so the third overload is filtered out
|
||||
reveal_type(f(list_int, int_any)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int, int_any))) # revealed: Unknown
|
||||
reveal_type(f(*(list_int, int_any))) # revealed: A
|
||||
|
||||
# All materialization of first argument is assignable to second overload and for the second
|
||||
# argument, they're assignable to the first overload, so the third overload is filtered out
|
||||
reveal_type(f(list_any, int_str)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_any, int_str))) # revealed: Unknown
|
||||
reveal_type(f(*(list_any, int_str))) # revealed: A
|
||||
|
||||
# All materializations of both arguments are assignable to the second overload, so the third
|
||||
# overload is filtered out
|
||||
reveal_type(f(list_any, int_any)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_any, int_any))) # revealed: Unknown
|
||||
reveal_type(f(*(list_any, int_any))) # revealed: A
|
||||
|
||||
# All materializations of first argument is assignable to the second overload and for the second
|
||||
# argument, they're assignable to the third overload, so no overloads are filtered out; the
|
||||
@@ -1316,8 +1301,7 @@ from overloaded import f
|
||||
|
||||
def _(literal: LiteralString, string: str, any: Any):
|
||||
reveal_type(f(literal)) # revealed: LiteralString
|
||||
# TODO: revealed: LiteralString
|
||||
reveal_type(f(*(literal,))) # revealed: Unknown
|
||||
reveal_type(f(*(literal,))) # revealed: LiteralString
|
||||
|
||||
reveal_type(f(string)) # revealed: str
|
||||
reveal_type(f(*(string,))) # revealed: str
|
||||
@@ -1355,12 +1339,10 @@ from overloaded import f
|
||||
|
||||
def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any):
|
||||
reveal_type(f(list_int)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
reveal_type(f(*(list_int,))) # revealed: A
|
||||
|
||||
reveal_type(f(list_str)) # revealed: str
|
||||
# TODO: Should be `str`
|
||||
reveal_type(f(*(list_str,))) # revealed: Unknown
|
||||
reveal_type(f(*(list_str,))) # revealed: str
|
||||
|
||||
reveal_type(f(list_any)) # revealed: Unknown
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
@@ -1561,12 +1543,10 @@ def _(any: Any):
|
||||
reveal_type(f(*(any,), flag=False)) # revealed: str
|
||||
|
||||
def _(args: tuple[Any, Literal[True]]):
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*args)) # revealed: Unknown
|
||||
reveal_type(f(*args)) # revealed: int
|
||||
|
||||
def _(args: tuple[Any, Literal[False]]):
|
||||
# TODO: revealed: str
|
||||
reveal_type(f(*args)) # revealed: Unknown
|
||||
reveal_type(f(*args)) # revealed: str
|
||||
```
|
||||
|
||||
### Argument type expansion
|
||||
@@ -1682,3 +1662,67 @@ def _(arg: tuple[A | B, Any]):
|
||||
reveal_type(f(arg)) # revealed: Unknown
|
||||
reveal_type(f(*(arg,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Bidirectional Type Inference
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Type inference accounts for parameter type annotations across all overloads.
|
||||
|
||||
```py
|
||||
from typing import TypedDict, overload
|
||||
|
||||
class T(TypedDict):
|
||||
x: int
|
||||
|
||||
@overload
|
||||
def f(a: list[T], b: int) -> int: ...
|
||||
@overload
|
||||
def f(a: list[dict[str, int]], b: str) -> str: ...
|
||||
def f(a: list[dict[str, int]] | list[T], b: int | str) -> int | str:
|
||||
return 1
|
||||
|
||||
def int_or_str() -> int | str:
|
||||
return 1
|
||||
|
||||
x = f([{"x": 1}], int_or_str())
|
||||
reveal_type(x) # revealed: int | str
|
||||
|
||||
# TODO: error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
# we currently incorrectly consider `list[dict[str, int]]` a subtype of `list[T]`
|
||||
f([{"y": 1}], int_or_str())
|
||||
```
|
||||
|
||||
Non-matching overloads do not produce diagnostics:
|
||||
|
||||
```py
|
||||
from typing import TypedDict, overload
|
||||
|
||||
class T(TypedDict):
|
||||
x: int
|
||||
|
||||
@overload
|
||||
def f(a: T, b: int) -> int: ...
|
||||
@overload
|
||||
def f(a: dict[str, int], b: str) -> str: ...
|
||||
def f(a: T | dict[str, int], b: int | str) -> int | str:
|
||||
return 1
|
||||
|
||||
x = f({"y": 1}, "a")
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import SupportsRound, overload
|
||||
|
||||
@overload
|
||||
def takes_str_or_float(x: str): ...
|
||||
@overload
|
||||
def takes_str_or_float(x: float): ...
|
||||
def takes_str_or_float(x: float | str): ...
|
||||
|
||||
takes_str_or_float(round(1.0))
|
||||
```
|
||||
|
||||
@@ -251,3 +251,59 @@ from ty_extensions import Intersection, Not
|
||||
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
|
||||
reveal_type(x) # revealed: Any & ~int
|
||||
```
|
||||
|
||||
## Bidirectional Type Inference
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Type inference accounts for parameter type annotations across all signatures in a union.
|
||||
|
||||
```py
|
||||
from typing import TypedDict, overload
|
||||
|
||||
class T(TypedDict):
|
||||
x: int
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def f(x: T) -> int:
|
||||
return 1
|
||||
else:
|
||||
def f(x: dict[str, int]) -> int:
|
||||
return 1
|
||||
x = f({"x": 1})
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# TODO: error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int]`"
|
||||
# we currently consider `TypedDict` instances to be subtypes of `dict`
|
||||
f({"y": 1})
|
||||
```
|
||||
|
||||
Diagnostics unrelated to the type-context are only reported once:
|
||||
|
||||
```py
|
||||
def f[T](x: T) -> list[T]:
|
||||
return [x]
|
||||
|
||||
def a(x: list[bool], y: list[bool]): ...
|
||||
def b(x: list[int], y: list[int]): ...
|
||||
def c(x: list[int], y: list[int]): ...
|
||||
def _(x: int):
|
||||
if x == 0:
|
||||
y = a
|
||||
elif x == 1:
|
||||
y = b
|
||||
else:
|
||||
y = c
|
||||
|
||||
if x == 0:
|
||||
z = True
|
||||
|
||||
y(f(True), [True])
|
||||
|
||||
# error: [possibly-unresolved-reference] "Name `z` used when possibly not defined"
|
||||
y(f(True), [z])
|
||||
```
|
||||
|
||||
@@ -28,6 +28,49 @@ class Point:
|
||||
self.x, self.y = other.x, other.y
|
||||
|
||||
p = Point()
|
||||
reveal_type(p.x) # revealed: Unknown | int
|
||||
reveal_type(p.y) # revealed: Unknown | int
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(p.x) # revealed: Unknown
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(p.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Implicit instance attributes
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/1111>:
|
||||
|
||||
```py
|
||||
def combine(*args) -> int:
|
||||
return 0
|
||||
|
||||
class C:
|
||||
def __init__(self: "C"):
|
||||
self.x1 = 0
|
||||
self.x2 = 0
|
||||
self.x3 = 0
|
||||
self.x4 = 0
|
||||
self.x5 = 0
|
||||
|
||||
def f1(self: "C"):
|
||||
self.x1 = combine(self.x2, self.x3, self.x4, self.x5)
|
||||
self.x2 = combine(self.x1, self.x3, self.x4, self.x5)
|
||||
self.x3 = combine(self.x1, self.x2, self.x4, self.x5)
|
||||
self.x4 = combine(self.x1, self.x2, self.x3, self.x5)
|
||||
self.x5 = combine(self.x1, self.x2, self.x3, self.x4)
|
||||
|
||||
def f2(self: "C"):
|
||||
self.x1 = combine(self.x2, self.x3, self.x4, self.x5)
|
||||
self.x2 = combine(self.x1, self.x3, self.x4, self.x5)
|
||||
self.x3 = combine(self.x1, self.x2, self.x4, self.x5)
|
||||
self.x4 = combine(self.x1, self.x2, self.x3, self.x5)
|
||||
self.x5 = combine(self.x1, self.x2, self.x3, self.x4)
|
||||
|
||||
def f3(self: "C"):
|
||||
self.x1 = combine(self.x2, self.x3, self.x4, self.x5)
|
||||
self.x2 = combine(self.x1, self.x3, self.x4, self.x5)
|
||||
self.x3 = combine(self.x1, self.x2, self.x4, self.x5)
|
||||
self.x4 = combine(self.x1, self.x2, self.x3, self.x5)
|
||||
self.x5 = combine(self.x1, self.x2, self.x3, self.x4)
|
||||
|
||||
# TODO: should be `Unknown | int`
|
||||
reveal_type(C().x1) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ def foo():
|
||||
global x
|
||||
def bar():
|
||||
# allowed, refers to `x` in the global scope
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
bar()
|
||||
del x # allowed, deletes `x` in the global scope (though we don't track that)
|
||||
```
|
||||
|
||||
@@ -127,7 +127,8 @@ c = C()
|
||||
|
||||
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data"]
|
||||
|
||||
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1]
|
||||
# TODO: Should be `Unknown | Literal["non-data", 1]`
|
||||
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
|
||||
|
||||
reveal_type(C.data_descriptor) # revealed: Unknown | Literal["data"]
|
||||
|
||||
@@ -172,7 +173,8 @@ def f1(flag: bool):
|
||||
def f(self):
|
||||
self.attr = "normal"
|
||||
|
||||
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
|
||||
# TODO: Should be `Unknown | Literal["data", "normal"]`
|
||||
reveal_type(C1().attr) # revealed: Unknown | Literal["data"]
|
||||
|
||||
# Assigning to the attribute also causes no `possibly-unbound` diagnostic:
|
||||
C1().attr = 1
|
||||
@@ -187,7 +189,8 @@ class C2:
|
||||
self.attr = "normal"
|
||||
attr = NonDataDescriptor()
|
||||
|
||||
reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
|
||||
# TODO: Should be `Unknown | Literal["non-data", "normal"]`
|
||||
reveal_type(C2().attr) # revealed: Unknown | Literal["non-data"]
|
||||
|
||||
# Assignments always go to the instance attribute in this case
|
||||
C2().attr = 1
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing_extensions import assert_type
|
||||
def _(x: int):
|
||||
assert_type(x, int) # fine
|
||||
assert_type(x, str) # error: [type-assertion-failure]
|
||||
assert_type(assert_type(x, int), int)
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
@@ -265,6 +265,41 @@ reveal_type(enum_members(Color))
|
||||
reveal_type(Color.red)
|
||||
```
|
||||
|
||||
Multiple aliases to the same member are also supported. This is a regression test for
|
||||
<https://github.com/astral-sh/ty/issues/1293>:
|
||||
|
||||
```py
|
||||
from ty_extensions import enum_members
|
||||
|
||||
class ManyAliases(Enum):
|
||||
real_member = "real_member"
|
||||
alias1 = "real_member"
|
||||
alias2 = "real_member"
|
||||
alias3 = "real_member"
|
||||
|
||||
other_member = "other_real_member"
|
||||
|
||||
# revealed: tuple[Literal["real_member"], Literal["other_member"]]
|
||||
reveal_type(enum_members(ManyAliases))
|
||||
|
||||
reveal_type(ManyAliases.real_member) # revealed: Literal[ManyAliases.real_member]
|
||||
reveal_type(ManyAliases.alias1) # revealed: Literal[ManyAliases.real_member]
|
||||
reveal_type(ManyAliases.alias2) # revealed: Literal[ManyAliases.real_member]
|
||||
reveal_type(ManyAliases.alias3) # revealed: Literal[ManyAliases.real_member]
|
||||
|
||||
reveal_type(ManyAliases.real_member.value) # revealed: Literal["real_member"]
|
||||
reveal_type(ManyAliases.real_member.name) # revealed: Literal["real_member"]
|
||||
|
||||
reveal_type(ManyAliases.alias1.value) # revealed: Literal["real_member"]
|
||||
reveal_type(ManyAliases.alias1.name) # revealed: Literal["real_member"]
|
||||
|
||||
reveal_type(ManyAliases.alias2.value) # revealed: Literal["real_member"]
|
||||
reveal_type(ManyAliases.alias2.name) # revealed: Literal["real_member"]
|
||||
|
||||
reveal_type(ManyAliases.alias3.value) # revealed: Literal["real_member"]
|
||||
reveal_type(ManyAliases.alias3.name) # revealed: Literal["real_member"]
|
||||
```
|
||||
|
||||
### Using `auto()`
|
||||
|
||||
```toml
|
||||
|
||||
@@ -530,3 +530,17 @@ age, name = team.employees[0]
|
||||
reveal_type(age) # revealed: Age
|
||||
reveal_type(name) # revealed: Name
|
||||
```
|
||||
|
||||
## `~T` is never assignable to `T`
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
from ty_extensions import Not
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(x: T, y: Not[T]) -> T:
|
||||
x = y # error: [invalid-assignment]
|
||||
y = x # error: [invalid-assignment]
|
||||
return x
|
||||
```
|
||||
|
||||
@@ -543,3 +543,14 @@ def _(x: int):
|
||||
|
||||
reveal_type(C().implicit_self(x)) # revealed: tuple[C, int]
|
||||
```
|
||||
|
||||
## `~T` is never assignable to `T`
|
||||
|
||||
```py
|
||||
from ty_extensions import Not
|
||||
|
||||
def f[T](x: T, y: Not[T]) -> T:
|
||||
x = y # error: [invalid-assignment]
|
||||
y = x # error: [invalid-assignment]
|
||||
return x
|
||||
```
|
||||
|
||||
@@ -25,8 +25,8 @@ reveal_type(y)
|
||||
# error: [possibly-missing-import] "Member `y` of module `maybe_unbound` may be missing"
|
||||
from maybe_unbound import x, y
|
||||
|
||||
reveal_type(x) # revealed: Unknown | Literal[3]
|
||||
reveal_type(y) # revealed: Unknown | Literal[3]
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## Maybe unbound annotated
|
||||
@@ -56,7 +56,7 @@ Importing an annotated name prefers the declared type over the inferred type:
|
||||
# error: [possibly-missing-import] "Member `y` of module `maybe_unbound_annotated` may be missing"
|
||||
from maybe_unbound_annotated import x, y
|
||||
|
||||
reveal_type(x) # revealed: Unknown | Literal[3]
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
|
||||
@@ -783,8 +783,8 @@ class A: ...
|
||||
```py
|
||||
from subexporter import *
|
||||
|
||||
# TODO: Should we avoid including `Unknown` for this case?
|
||||
reveal_type(__all__) # revealed: Unknown | list[Unknown | str]
|
||||
# TODO: we could potentially infer `list[str] | tuple[str, ...]` here
|
||||
reveal_type(__all__) # revealed: list[Unknown | str]
|
||||
|
||||
__all__.append("B")
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def __getattr__(name: str) -> int:
|
||||
import mixed_module
|
||||
|
||||
# Explicit attribute should take precedence
|
||||
reveal_type(mixed_module.explicit_attr) # revealed: Unknown | Literal["explicit"]
|
||||
reveal_type(mixed_module.explicit_attr) # revealed: Literal["explicit"]
|
||||
|
||||
# `__getattr__` should handle unknown attributes
|
||||
reveal_type(mixed_module.dynamic_attr) # revealed: str
|
||||
|
||||
@@ -103,7 +103,7 @@ x = "namespace"
|
||||
```py
|
||||
from foo import x
|
||||
|
||||
reveal_type(x) # revealed: Unknown | Literal["module"]
|
||||
reveal_type(x) # revealed: Literal["module"]
|
||||
|
||||
import foo.bar # error: [unresolved-import]
|
||||
```
|
||||
|
||||
@@ -144,8 +144,8 @@ X = (Y := 3) + 4
|
||||
```py
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: Unknown | Literal[7]
|
||||
reveal_type(Y) # revealed: Unknown | Literal[3]
|
||||
reveal_type(X) # revealed: Literal[7]
|
||||
reveal_type(Y) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
### Global-scope symbols defined in many other ways
|
||||
@@ -781,9 +781,9 @@ else:
|
||||
from exporter import *
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(A) # revealed: Unknown | Literal[1]
|
||||
reveal_type(A) # revealed: Literal[1]
|
||||
|
||||
reveal_type(B) # revealed: Unknown | Literal[2, 3]
|
||||
reveal_type(B) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
### Reachability constraints in the importing module
|
||||
@@ -804,7 +804,7 @@ if coinflip():
|
||||
from exporter import *
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(A) # revealed: Unknown | Literal[1]
|
||||
reveal_type(A) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Reachability constraints in the exporting module *and* the importing module
|
||||
|
||||
@@ -34,9 +34,10 @@ class _:
|
||||
[reveal_type(a.z) for _ in range(1)] # revealed: Literal[0]
|
||||
|
||||
def _():
|
||||
reveal_type(a.x) # revealed: Unknown | int | None
|
||||
reveal_type(a.x) # revealed: int | None
|
||||
reveal_type(a.y) # revealed: Unknown | None
|
||||
reveal_type(a.z) # revealed: Unknown | None
|
||||
# TODO: Should be `Unknown | None`
|
||||
reveal_type(a.z) # revealed: Unknown
|
||||
|
||||
if False:
|
||||
a = A()
|
||||
@@ -48,7 +49,8 @@ if True:
|
||||
a = A()
|
||||
reveal_type(a.x) # revealed: int | None
|
||||
reveal_type(a.y) # revealed: Unknown | None
|
||||
reveal_type(a.z) # revealed: Unknown | None
|
||||
# TODO: Should be `Unknown | None`
|
||||
reveal_type(a.z) # revealed: Unknown
|
||||
|
||||
a.x = 0
|
||||
a.y = 0
|
||||
@@ -61,7 +63,8 @@ class _:
|
||||
a = A()
|
||||
reveal_type(a.x) # revealed: int | None
|
||||
reveal_type(a.y) # revealed: Unknown | None
|
||||
reveal_type(a.z) # revealed: Unknown | None
|
||||
# TODO: Should be `Unknown | None`
|
||||
reveal_type(a.z) # revealed: Unknown
|
||||
|
||||
def cond() -> bool:
|
||||
return True
|
||||
@@ -75,9 +78,10 @@ class _:
|
||||
|
||||
if cond():
|
||||
a = A()
|
||||
reveal_type(a.x) # revealed: int | None | Unknown
|
||||
reveal_type(a.x) # revealed: int | None
|
||||
reveal_type(a.y) # revealed: Unknown | None
|
||||
reveal_type(a.z) # revealed: Unknown | None
|
||||
# TODO: Should be `Unknown | None`
|
||||
reveal_type(a.z) # revealed: Unknown
|
||||
|
||||
class _:
|
||||
a = A()
|
||||
@@ -85,7 +89,8 @@ class _:
|
||||
class Inner:
|
||||
reveal_type(a.x) # revealed: int | None
|
||||
reveal_type(a.y) # revealed: Unknown | None
|
||||
reveal_type(a.z) # revealed: Unknown | None
|
||||
# TODO: Should be `Unknown | None`
|
||||
reveal_type(a.z) # revealed: Unknown
|
||||
|
||||
a = A()
|
||||
# error: [unresolved-attribute]
|
||||
@@ -295,10 +300,10 @@ class C:
|
||||
|
||||
def _():
|
||||
# error: [possibly-missing-attribute]
|
||||
reveal_type(b.a.x[0]) # revealed: Unknown | int | None
|
||||
reveal_type(b.a.x[0]) # revealed: int | None
|
||||
# error: [possibly-missing-attribute]
|
||||
reveal_type(b.a.x) # revealed: Unknown | list[int | None]
|
||||
reveal_type(b.a) # revealed: Unknown | A | None
|
||||
reveal_type(b.a.x) # revealed: list[int | None]
|
||||
reveal_type(b.a) # revealed: A | None
|
||||
```
|
||||
|
||||
## Invalid assignments are not used for narrowing
|
||||
|
||||
@@ -167,11 +167,11 @@ if c.x is not None:
|
||||
|
||||
if c.x is not None:
|
||||
def _():
|
||||
reveal_type(c.x) # revealed: Unknown | int | None
|
||||
reveal_type(c.x) # revealed: int | None
|
||||
|
||||
def _():
|
||||
if c.x is not None:
|
||||
reveal_type(c.x) # revealed: (Unknown & ~None) | int
|
||||
reveal_type(c.x) # revealed: int
|
||||
```
|
||||
|
||||
## Subscript narrowing
|
||||
|
||||
@@ -86,7 +86,7 @@ class B:
|
||||
reveal_type(a.x) # revealed: Literal["a"]
|
||||
|
||||
def f():
|
||||
reveal_type(a.x) # revealed: Unknown | str | None
|
||||
reveal_type(a.x) # revealed: str | None
|
||||
|
||||
[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"]
|
||||
|
||||
@@ -96,7 +96,7 @@ class C:
|
||||
reveal_type(a.x) # revealed: str | None
|
||||
|
||||
def g():
|
||||
reveal_type(a.x) # revealed: Unknown | str | None
|
||||
reveal_type(a.x) # revealed: str | None
|
||||
|
||||
[reveal_type(a.x) for _ in range(1)] # revealed: str | None
|
||||
|
||||
@@ -109,7 +109,7 @@ class D:
|
||||
reveal_type(a.x) # revealed: Literal["a"]
|
||||
|
||||
def h():
|
||||
reveal_type(a.x) # revealed: Unknown | str | None
|
||||
reveal_type(a.x) # revealed: str | None
|
||||
|
||||
# TODO: should be `str | None`
|
||||
[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"]
|
||||
@@ -190,7 +190,7 @@ def f(x: str | None):
|
||||
reveal_type(g) # revealed: str
|
||||
|
||||
if a.x is not None:
|
||||
reveal_type(a.x) # revealed: (Unknown & ~None) | str
|
||||
reveal_type(a.x) # revealed: str
|
||||
|
||||
if l[0] is not None:
|
||||
reveal_type(l[0]) # revealed: str
|
||||
@@ -206,7 +206,7 @@ def f(x: str | None):
|
||||
reveal_type(g) # revealed: str
|
||||
|
||||
if a.x is not None:
|
||||
reveal_type(a.x) # revealed: (Unknown & ~None) | str
|
||||
reveal_type(a.x) # revealed: str
|
||||
|
||||
if l[0] is not None:
|
||||
reveal_type(l[0]) # revealed: str
|
||||
@@ -382,12 +382,12 @@ def f():
|
||||
if a.x is not None:
|
||||
def _():
|
||||
# Lazy nested scope narrowing is not performed on attributes/subscripts because it's difficult to track their changes.
|
||||
reveal_type(a.x) # revealed: Unknown | str | None
|
||||
reveal_type(a.x) # revealed: str | None
|
||||
|
||||
class D:
|
||||
reveal_type(a.x) # revealed: (Unknown & ~None) | str
|
||||
reveal_type(a.x) # revealed: str
|
||||
|
||||
[reveal_type(a.x) for _ in range(1)] # revealed: (Unknown & ~None) | str
|
||||
[reveal_type(a.x) for _ in range(1)] # revealed: str
|
||||
|
||||
if l[0] is not None:
|
||||
def _():
|
||||
@@ -473,11 +473,11 @@ def f():
|
||||
if a.x is not None:
|
||||
def _():
|
||||
if a.x != 1:
|
||||
reveal_type(a.x) # revealed: (Unknown & ~Literal[1]) | str | None
|
||||
reveal_type(a.x) # revealed: str | None
|
||||
|
||||
class D:
|
||||
if a.x != 1:
|
||||
reveal_type(a.x) # revealed: (Unknown & ~Literal[1] & ~None) | str
|
||||
reveal_type(a.x) # revealed: str
|
||||
|
||||
if l[0] is not None:
|
||||
def _():
|
||||
|
||||
@@ -155,11 +155,13 @@ class Foo:
|
||||
|
||||
foo = Foo()
|
||||
reveal_type(foo) # revealed: Foo
|
||||
reveal_type(foo.x) # revealed: Unknown | int | None
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
reveal_type(foo.x) # revealed: Unknown
|
||||
|
||||
foo1 = Foo(1)
|
||||
reveal_type(foo1) # revealed: Foo
|
||||
reveal_type(foo1.x) # revealed: Unknown | int | None
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
reveal_type(foo1.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Version specific
|
||||
|
||||
@@ -685,7 +685,8 @@ class HalfUnknownQux:
|
||||
def __init__(self, x: int) -> None:
|
||||
self.x = x
|
||||
|
||||
reveal_type(HalfUnknownQux(1).x) # revealed: Unknown | int
|
||||
# TODO: Should be `Unknown | int`
|
||||
reveal_type(HalfUnknownQux(1).x) # revealed: Unknown
|
||||
|
||||
static_assert(not is_subtype_of(HalfUnknownQux, HasX))
|
||||
static_assert(is_assignable_to(HalfUnknownQux, HasX))
|
||||
|
||||
@@ -263,7 +263,7 @@ if flag():
|
||||
x = 1
|
||||
|
||||
def f() -> None:
|
||||
reveal_type(x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
# Function only used inside this branch
|
||||
f()
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ if flag():
|
||||
chr: int = 1
|
||||
|
||||
def _():
|
||||
# TODO: Should ideally be `Unknown | Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)`
|
||||
reveal_type(abs) # revealed: Unknown | Literal[1]
|
||||
# TODO: Should ideally be `Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)`
|
||||
reveal_type(abs) # revealed: Literal[1]
|
||||
# TODO: Should ideally be `int | (def chr(i: SupportsIndex, /) -> str)`
|
||||
reveal_type(chr) # revealed: int
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ Function definitions are evaluated lazily.
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
|
||||
x = 2
|
||||
```
|
||||
@@ -283,7 +283,7 @@ x = 1
|
||||
|
||||
def _():
|
||||
class C:
|
||||
# revealed: Unknown | Literal[1]
|
||||
# revealed: Literal[1]
|
||||
[reveal_type(x) for _ in [1]]
|
||||
x = 2
|
||||
```
|
||||
@@ -389,7 +389,7 @@ x = int
|
||||
class C:
|
||||
var: ClassVar[x]
|
||||
|
||||
reveal_type(C.var) # revealed: Unknown | int | str
|
||||
reveal_type(C.var) # revealed: int | str
|
||||
|
||||
x = str
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ A name reference to a never-defined symbol in a function is implicitly a global
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Explicit global in function
|
||||
@@ -18,7 +18,7 @@ x = 1
|
||||
|
||||
def f():
|
||||
global x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Unassignable type in function
|
||||
@@ -201,7 +201,7 @@ x = 42
|
||||
|
||||
def f():
|
||||
global x
|
||||
reveal_type(x) # revealed: Unknown | Literal[42]
|
||||
reveal_type(x) # revealed: Literal[42]
|
||||
x = "56"
|
||||
reveal_type(x) # revealed: Literal["56"]
|
||||
```
|
||||
|
||||
@@ -73,10 +73,10 @@ __spec__ = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is no
|
||||
```py
|
||||
import module
|
||||
|
||||
reveal_type(module.__file__) # revealed: Unknown | None
|
||||
reveal_type(module.__file__) # revealed: None
|
||||
reveal_type(module.__path__) # revealed: list[str]
|
||||
reveal_type(module.__doc__) # revealed: Unknown
|
||||
reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None
|
||||
reveal_type(module.__spec__) # revealed: ModuleSpec | None
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(module.__warningregistry__) # revealed: Unknown
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_type.m
|
||||
3 | def _(x: int):
|
||||
4 | assert_type(x, int) # fine
|
||||
5 | assert_type(x, str) # error: [type-assertion-failure]
|
||||
6 | assert_type(assert_type(x, int), int)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
@@ -31,6 +32,7 @@ error[type-assertion-failure]: Argument does not have asserted type `str`
|
||||
| ^^^^^^^^^^^^-^^^^^^
|
||||
| |
|
||||
| Inferred type of argument is `int`
|
||||
6 | assert_type(assert_type(x, int), int)
|
||||
|
|
||||
info: `str` and `int` are not equivalent types
|
||||
info: rule `type-assertion-failure` is enabled by default
|
||||
|
||||
@@ -138,6 +138,11 @@ static_assert(is_equivalent_to(Any, Any | Intersection[Any, str]))
|
||||
static_assert(is_equivalent_to(Any, Intersection[str, Any] | Any))
|
||||
static_assert(is_equivalent_to(Any, Any | Intersection[Any, Not[None]]))
|
||||
static_assert(is_equivalent_to(Any, Intersection[Not[None], Any] | Any))
|
||||
|
||||
static_assert(is_equivalent_to(Any, Unknown | Intersection[Unknown, str]))
|
||||
static_assert(is_equivalent_to(Any, Intersection[str, Unknown] | Unknown))
|
||||
static_assert(is_equivalent_to(Any, Unknown | Intersection[Unknown, Not[None]]))
|
||||
static_assert(is_equivalent_to(Any, Intersection[Not[None], Unknown] | Unknown))
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
@@ -97,7 +97,8 @@ reveal_type(C().FINAL_A) # revealed: int
|
||||
reveal_type(C().FINAL_B) # revealed: Literal[1]
|
||||
reveal_type(C().FINAL_C) # revealed: int
|
||||
reveal_type(C().FINAL_D) # revealed: Literal[1]
|
||||
reveal_type(C().FINAL_E) # revealed: Literal[1]
|
||||
# TODO: Should be `Literal[1]`
|
||||
reveal_type(C().FINAL_E) # revealed: @Todo(implicit instance attribute)
|
||||
```
|
||||
|
||||
## Not modifiable
|
||||
|
||||
@@ -152,7 +152,7 @@ Person(name="Alice")
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
Person({"name": "Alice"})
|
||||
|
||||
# TODO: this should be an error, similar to the above
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
accepts_person({"name": "Alice"})
|
||||
# TODO: this should be an error, similar to the above
|
||||
house.owner = {"name": "Alice"}
|
||||
@@ -171,7 +171,7 @@ Person(name=None, age=30)
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
Person({"name": None, "age": 30})
|
||||
|
||||
# TODO: this should be an error, similar to the above
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
accepts_person({"name": None, "age": 30})
|
||||
# TODO: this should be an error, similar to the above
|
||||
house.owner = {"name": None, "age": 30}
|
||||
@@ -190,7 +190,7 @@ Person(name="Alice", age=30, extra=True)
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
Person({"name": "Alice", "age": 30, "extra": True})
|
||||
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
accepts_person({"name": "Alice", "age": 30, "extra": True})
|
||||
# TODO: this should be an error
|
||||
house.owner = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
||||
@@ -306,3 +306,74 @@ def _(c: BC, d: BD):
|
||||
reveal_type(c) # revealed: Literal[b""]
|
||||
reveal_type(d) # revealed: Literal[b""]
|
||||
```
|
||||
|
||||
## Unions of tuples
|
||||
|
||||
A union of a fixed-length tuple and a variable-length tuple must be collapsed to the variable-length
|
||||
element, never to the fixed-length element (`tuple[()] | tuple[Any, ...]` -> `tuple[Any, ...]`, not
|
||||
`tuple[()]`).
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(
|
||||
a: tuple[()] | tuple[int, ...],
|
||||
b: tuple[int, ...] | tuple[()],
|
||||
c: tuple[int] | tuple[str, ...],
|
||||
d: tuple[str, ...] | tuple[int],
|
||||
e: tuple[()] | tuple[Any, ...],
|
||||
f: tuple[Any, ...] | tuple[()],
|
||||
g: tuple[Any, ...] | tuple[Any | str, ...],
|
||||
h: tuple[Any | str, ...] | tuple[Any, ...],
|
||||
):
|
||||
reveal_type(a) # revealed: tuple[int, ...]
|
||||
reveal_type(b) # revealed: tuple[int, ...]
|
||||
reveal_type(c) # revealed: tuple[int] | tuple[str, ...]
|
||||
reveal_type(d) # revealed: tuple[str, ...] | tuple[int]
|
||||
reveal_type(e) # revealed: tuple[Any, ...]
|
||||
reveal_type(f) # revealed: tuple[Any, ...]
|
||||
reveal_type(g) # revealed: tuple[Any | str, ...]
|
||||
reveal_type(h) # revealed: tuple[Any | str, ...]
|
||||
```
|
||||
|
||||
## Unions of other generic containers
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Bivariant[T]: ...
|
||||
|
||||
class Covariant[T]:
|
||||
def get(self) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
class Contravariant[T]:
|
||||
def receive(self, input: T) -> None: ...
|
||||
|
||||
class Invariant[T]:
|
||||
mutable_attribute: T
|
||||
|
||||
def _(
|
||||
a: Bivariant[Any] | Bivariant[Any | str],
|
||||
b: Bivariant[Any | str] | Bivariant[Any],
|
||||
c: Covariant[Any] | Covariant[Any | str],
|
||||
d: Covariant[Any | str] | Covariant[Any],
|
||||
e: Contravariant[Any | str] | Contravariant[Any],
|
||||
f: Contravariant[Any] | Contravariant[Any | str],
|
||||
g: Invariant[Any] | Invariant[Any | str],
|
||||
h: Invariant[Any | str] | Invariant[Any],
|
||||
):
|
||||
reveal_type(a) # revealed: Bivariant[Any]
|
||||
reveal_type(b) # revealed: Bivariant[Any | str]
|
||||
reveal_type(c) # revealed: Covariant[Any | str]
|
||||
reveal_type(d) # revealed: Covariant[Any | str]
|
||||
reveal_type(e) # revealed: Contravariant[Any]
|
||||
reveal_type(f) # revealed: Contravariant[Any]
|
||||
reveal_type(g) # revealed: Invariant[Any] | Invariant[Any | str]
|
||||
reveal_type(h) # revealed: Invariant[Any | str] | Invariant[Any]
|
||||
```
|
||||
|
||||
@@ -1765,4 +1765,45 @@ not_a_directory
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
/// This is a regression test for mishandling of file root matching.
|
||||
///
|
||||
/// In particular, in some cases, `/` is added as a search root. This
|
||||
/// should in turn match everything. But the way we were setting up the
|
||||
/// wildcard for matching was incorrect for this one specific case. That in
|
||||
/// turn meant that the module resolver couldn't find an appropriate file
|
||||
/// root which in turn caused a panic.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/ty/issues/1277>
|
||||
#[test]
|
||||
fn root_directory_for_search_path_is_okay() {
|
||||
let project_directory = SystemPathBuf::from("/project");
|
||||
let installed_foo_module = project_directory.join("foo/__init__.py");
|
||||
|
||||
let mut db = TestDb::new();
|
||||
db.write_file(&installed_foo_module, "").unwrap();
|
||||
|
||||
db.files()
|
||||
.try_add_root(&db, SystemPath::new("/"), FileRootKind::Project);
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersionWithSource::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings::new(vec![project_directory])
|
||||
.to_search_paths(db.system(), db.vendored())
|
||||
.unwrap(),
|
||||
},
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
list_snapshot_filter(&db, |m| m.name(&db).as_str() == "foo"),
|
||||
@r#"
|
||||
[
|
||||
Module::File("foo", "first-party", "/project/foo/__init__.py", Package, None),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,11 +666,15 @@ struct ModuleNameIngredient<'db> {
|
||||
/// Returns `true` if the module name refers to a standard library module which can't be shadowed
|
||||
/// by a first-party module.
|
||||
///
|
||||
/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as the
|
||||
/// `types` module, which tends to be imported early in Python startup, so can't be consistently
|
||||
/// shadowed, and is important to type checking.
|
||||
/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as
|
||||
/// certain other modules that are involved in an import cycle with `builtins` (`types`,
|
||||
/// `typing_extensions`, etc.). This latter set of modules cannot be allowed to be shadowed by
|
||||
/// first-party or "extra-path" modules, or we risk panics in unexpected places due to being
|
||||
/// unable to resolve builtin symbols. This is similar behaviour to other type checkers such
|
||||
/// as mypy: <https://github.com/python/mypy/blob/3807423e9d98e678bf16b13ec8b4f909fe181908/mypy/build.py#L104-L117>
|
||||
pub(super) fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool {
|
||||
module_name == "types" || ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name)
|
||||
matches!(module_name, "types" | "typing_extensions")
|
||||
|| ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name)
|
||||
}
|
||||
|
||||
/// Given a module name and a list of search paths in which to lookup modules,
|
||||
|
||||
@@ -810,7 +810,7 @@ fn place_by_id<'db>(
|
||||
// modified externally, but those changes do not take effect. We therefore issue
|
||||
// a diagnostic if we see it being modified externally. In type inference, we
|
||||
// can assign a "narrow" type to it even if it is not *declared*. This means, we
|
||||
// do not have to call [`widen_type_for_undeclared_public_symbol`].
|
||||
// do not have to union with `Unknown`.
|
||||
//
|
||||
// `TYPE_CHECKING` is a special variable that should only be assigned `False`
|
||||
// at runtime, but is always considered `True` in type checking.
|
||||
@@ -822,18 +822,37 @@ fn place_by_id<'db>(
|
||||
)
|
||||
});
|
||||
|
||||
if scope.file(db).is_stub(db) || scope.scope(db).visibility().is_private() {
|
||||
// We generally trust module-level undeclared places in stubs and do not union
|
||||
// with `Unknown`. If we don't do this, simple aliases like `IOError = OSError` in
|
||||
// stubs would result in `IOError` being a union of `OSError` and `Unknown`, which
|
||||
// leads to all sorts of downstream problems. Similarly, type variables are often
|
||||
// defined as `_T = TypeVar("_T")`, without being declared.
|
||||
// Also, if the scope is private, such as a function scope,
|
||||
// meaning that the place cannot be rewritten from elsewhere, we do not union with `Unknown`.
|
||||
// Module-level globals can be mutated externally. A `MY_CONSTANT = 1` global might
|
||||
// be changed to `"some string"` from code outside of the module that we're looking
|
||||
// at, and so from a gradual-guarantee perspective, it makes sense to infer a type
|
||||
// of `Literal[1] | Unknown` for global symbols. This allows the code that does the
|
||||
// mutation to type check correctly, and for code that uses the global, it accurately
|
||||
// reflects the lack of knowledge about the type.
|
||||
//
|
||||
// However, external modifications (or modifications through `global` statements) that
|
||||
// would require a wider type are relatively rare. From a practical perspective, we can
|
||||
// therefore achieve a better user experience by trusting the inferred type. Users who
|
||||
// need the external mutation to work can always annotate the global with the wider
|
||||
// type. And everyone else benefits from more precise type inference.
|
||||
let is_module_global = scope.node(db).scope_kind().is_module();
|
||||
|
||||
// If the visibility of the scope is private (like for a function scope), we also do
|
||||
// not union with `Unknown`, because the symbol cannot be modified externally.
|
||||
let scope_has_private_visibility = scope.scope(db).visibility().is_private();
|
||||
|
||||
// We generally trust undeclared places in stubs and do not union with `Unknown`.
|
||||
let in_stub_file = scope.file(db).is_stub(db);
|
||||
|
||||
if is_considered_non_modifiable
|
||||
|| is_module_global
|
||||
|| scope_has_private_visibility
|
||||
|| in_stub_file
|
||||
{
|
||||
inferred.into()
|
||||
} else {
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable)
|
||||
// Widen the inferred type of undeclared public symbols by unioning with `Unknown`
|
||||
inferred
|
||||
.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty]))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -1585,29 +1604,6 @@ pub(crate) enum BoundnessAnalysis {
|
||||
BasedOnUnboundVisibility,
|
||||
}
|
||||
|
||||
/// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred`
|
||||
/// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is
|
||||
/// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses
|
||||
/// of symbols that have no declared type.
|
||||
fn widen_type_for_undeclared_public_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
inferred: Place<'db>,
|
||||
is_considered_non_modifiable: bool,
|
||||
) -> Place<'db> {
|
||||
// We special-case known-instance types here since symbols like `typing.Any` are typically
|
||||
// not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as
|
||||
// such.
|
||||
let is_known_instance = inferred
|
||||
.ignore_possibly_unbound()
|
||||
.is_some_and(|ty| matches!(ty, Type::SpecialForm(_) | Type::KnownInstance(_)));
|
||||
|
||||
if is_considered_non_modifiable || is_known_instance {
|
||||
inferred
|
||||
} else {
|
||||
inferred.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -614,28 +614,44 @@ pub(crate) enum CondaEnvironmentKind {
|
||||
impl CondaEnvironmentKind {
|
||||
/// Compute the kind of `CONDA_PREFIX` we have.
|
||||
///
|
||||
/// When the base environment is used, `CONDA_DEFAULT_ENV` will be set to a name, i.e., `base` or
|
||||
/// `root` which does not match the prefix, e.g. `/usr/local` instead of
|
||||
/// `/usr/local/conda/envs/<name>`.
|
||||
/// The base environment is typically stored in a location matching the `_CONDA_ROOT` path.
|
||||
///
|
||||
/// Additionally, when the base environment is active, `CONDA_DEFAULT_ENV` will be set to a
|
||||
/// name, e.g., `base`, which does not match the `CONDA_PREFIX`, e.g., `/usr/local` instead of
|
||||
/// `/usr/local/conda/envs/<name>`. Note that the name `CONDA_DEFAULT_ENV` is misleading, it's
|
||||
/// the active environment name, not a constant base environment name.
|
||||
fn from_prefix_path(system: &dyn System, path: &SystemPath) -> Self {
|
||||
// If we cannot read `CONDA_DEFAULT_ENV`, there's no way to know if the base environment
|
||||
let Ok(default_env) = system.env_var(EnvVars::CONDA_DEFAULT_ENV) else {
|
||||
return CondaEnvironmentKind::Child;
|
||||
};
|
||||
|
||||
// These are the expected names for the base environment
|
||||
if default_env != "base" && default_env != "root" {
|
||||
return CondaEnvironmentKind::Child;
|
||||
// If `_CONDA_ROOT` is set and matches `CONDA_PREFIX`, it's the base environment.
|
||||
if let Ok(conda_root) = system.env_var(EnvVars::CONDA_ROOT) {
|
||||
if path.as_str() == conda_root {
|
||||
return Self::Base;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(name) = path.file_name() else {
|
||||
return CondaEnvironmentKind::Child;
|
||||
// Next, we'll use a heuristic based on `CONDA_DEFAULT_ENV`
|
||||
let Ok(current_env) = system.env_var(EnvVars::CONDA_DEFAULT_ENV) else {
|
||||
return Self::Child;
|
||||
};
|
||||
|
||||
if name == default_env {
|
||||
CondaEnvironmentKind::Base
|
||||
// If the environment name is "base" or "root", treat it as a base environment
|
||||
//
|
||||
// These are the expected names for the base environment; and is retained for backwards
|
||||
// compatibility, but in a future breaking release we should remove this special-casing.
|
||||
if current_env == "base" || current_env == "root" {
|
||||
return Self::Base;
|
||||
}
|
||||
|
||||
// For other environment names, use the path-based logic
|
||||
let Some(name) = path.file_name() else {
|
||||
return Self::Child;
|
||||
};
|
||||
|
||||
// If the environment is in a directory matching the name of the environment, it's not
|
||||
// usually a base environment.
|
||||
if name == current_env {
|
||||
Self::Child
|
||||
} else {
|
||||
CondaEnvironmentKind::Child
|
||||
Self::Base
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ pub use crate::types::ide_support::{
|
||||
definitions_for_keyword_argument, definitions_for_name, find_active_signature_from_details,
|
||||
inlay_hint_function_argument_details,
|
||||
};
|
||||
use crate::types::infer::infer_unpack_types;
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
||||
use crate::types::signatures::{ParameterForm, walk_signature};
|
||||
@@ -1050,13 +1049,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn into_intersection(self) -> Option<IntersectionType<'db>> {
|
||||
match self {
|
||||
Type::Intersection(intersection_type) => Some(intersection_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub(crate) fn expect_union(self) -> UnionType<'db> {
|
||||
@@ -1420,43 +1412,9 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is a [subtype of] type `target`.
|
||||
/// Return true if this type is a subtype of type `target`.
|
||||
///
|
||||
/// For fully static types, this means that the set of objects represented by `self` is a
|
||||
/// subset of the objects represented by `target`.
|
||||
///
|
||||
/// For gradual types, it means that the union of all possible sets of values represented by
|
||||
/// `self` (the "top materialization" of `self`) is a subtype of the intersection of all
|
||||
/// possible sets of values represented by `target` (the "bottom materialization" of
|
||||
/// `target`). In other words, for all possible pairs of materializations `self'` and
|
||||
/// `target'`, `self'` is always a subtype of `target'`.
|
||||
///
|
||||
/// Note that this latter expansion of the subtyping relation to non-fully-static types is not
|
||||
/// described in the typing spec, but the primary use of the subtyping relation is for
|
||||
/// simplifying unions and intersections, and this expansion to gradual types is sound and
|
||||
/// allows us to better simplify many unions and intersections. This definition does mean the
|
||||
/// subtyping relation is not reflexive for non-fully-static types (e.g. `Any` is not a subtype
|
||||
/// of `Any`).
|
||||
///
|
||||
/// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
///
|
||||
/// There would be an even more general definition of subtyping for gradual types, allowing a
|
||||
/// type `S` to be a subtype of a type `T` if the top materialization of `S` (`S+`) is a
|
||||
/// subtype of `T+`, and the bottom materialization of `S` (`S-`) is a subtype of `T-`. This
|
||||
/// definition is attractive in that it would restore reflexivity of subtyping for all types,
|
||||
/// and would mean that gradual equivalence of `S` and `T` could be defined simply as `S <: T
|
||||
/// && T <: S`. It would also be sound, in that simplifying unions or intersections according
|
||||
/// to this definition of subtyping would still result in an equivalent type.
|
||||
///
|
||||
/// Unfortunately using this definition would break transitivity of subtyping when both nominal
|
||||
/// and structural types are involved, because Liskov enforcement for nominal types is based on
|
||||
/// assignability, so we can have class `A` with method `def meth(self) -> Any` and a subclass
|
||||
/// `B(A)` with method `def meth(self) -> int`. In this case, `A` would be a subtype of a
|
||||
/// protocol `P` with method `def meth(self) -> Any`, but `B` would not be a subtype of `P`,
|
||||
/// and yet `B` is (by nominal subtyping) a subtype of `A`, so we would have `B <: A` and `A <:
|
||||
/// P`, but not `B <: P`. Losing transitivity of subtyping is not tenable (it makes union and
|
||||
/// intersection simplification dependent on the order in which elements are added), so we do
|
||||
/// not use this more general definition of subtyping.
|
||||
/// See [`TypeRelation::Subtyping`] for more details.
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
self.when_subtype_of(db, target).is_always_satisfied()
|
||||
}
|
||||
@@ -1465,9 +1423,9 @@ impl<'db> Type<'db> {
|
||||
self.has_relation_to(db, target, TypeRelation::Subtyping)
|
||||
}
|
||||
|
||||
/// Return true if this type is [assignable to] type `target`.
|
||||
/// Return true if this type is assignable to type `target`.
|
||||
///
|
||||
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
/// See [`TypeRelation::Assignability`] for more details.
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
self.when_assignable_to(db, target).is_always_satisfied()
|
||||
}
|
||||
@@ -1476,6 +1434,14 @@ impl<'db> Type<'db> {
|
||||
self.has_relation_to(db, target, TypeRelation::Assignability)
|
||||
}
|
||||
|
||||
/// Return `true` if it would be redundant to add `self` to a union that already contains `other`.
|
||||
///
|
||||
/// See [`TypeRelation::Redundancy`] for more details.
|
||||
pub(crate) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
self.has_relation_to(db, other, TypeRelation::Redundancy)
|
||||
.is_always_satisfied()
|
||||
}
|
||||
|
||||
fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
@@ -1497,7 +1463,7 @@ impl<'db> Type<'db> {
|
||||
//
|
||||
// Note that we could do a full equivalence check here, but that would be both expensive
|
||||
// and unnecessary. This early return is only an optimisation.
|
||||
if (relation.is_assignability() || self.subtyping_is_always_reflexive()) && self == target {
|
||||
if (!relation.is_subtyping() || self.subtyping_is_always_reflexive()) && self == target {
|
||||
return ConstraintSet::from(true);
|
||||
}
|
||||
|
||||
@@ -1514,9 +1480,11 @@ impl<'db> Type<'db> {
|
||||
// It is a subtype of all other types.
|
||||
(Type::Never, _) => ConstraintSet::from(true),
|
||||
|
||||
// Dynamic is only a subtype of `object` and only a supertype of `Never`; both were
|
||||
// handled above. It's always assignable, though.
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => {
|
||||
// In some specific situations, `Any`/`Unknown`/`@Todo` can be simplified out of unions and intersections,
|
||||
// but this is not true for divergent types (and moving this case any lower down appears to cause
|
||||
// "too many cycle iterations" panics).
|
||||
(Type::Dynamic(DynamicType::Divergent(_)), _)
|
||||
| (_, Type::Dynamic(DynamicType::Divergent(_))) => {
|
||||
ConstraintSet::from(relation.is_assignability())
|
||||
}
|
||||
|
||||
@@ -1541,10 +1509,33 @@ impl<'db> Type<'db> {
|
||||
.has_relation_to_impl(db, right, relation, visitor)
|
||||
}
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
// TODO: Implement assignability and subtyping for TypedDict
|
||||
ConstraintSet::from(relation.is_assignability())
|
||||
}
|
||||
// Dynamic is only a subtype of `object` and only a supertype of `Never`; both were
|
||||
// handled above. It's always assignable, though.
|
||||
//
|
||||
// Union simplification sits in between subtyping and assignability. `Any <: T` only
|
||||
// holds true if `T` is also a dynamic type or a union that contains a dynamic type.
|
||||
// Similarly, `T <: Any` only holds true if `T` is a dynamic type or an intersection
|
||||
// that contains a dynamic type.
|
||||
(Type::Dynamic(_), _) => ConstraintSet::from(match relation {
|
||||
TypeRelation::Subtyping => false,
|
||||
TypeRelation::Assignability => true,
|
||||
TypeRelation::Redundancy => match target {
|
||||
Type::Dynamic(_) => true,
|
||||
Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic),
|
||||
_ => false,
|
||||
},
|
||||
}),
|
||||
(_, Type::Dynamic(_)) => ConstraintSet::from(match relation {
|
||||
TypeRelation::Subtyping => false,
|
||||
TypeRelation::Assignability => true,
|
||||
TypeRelation::Redundancy => match self {
|
||||
Type::Dynamic(_) => true,
|
||||
Type::Intersection(intersection) => {
|
||||
intersection.positive(db).iter().any(Type::is_dynamic)
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}),
|
||||
|
||||
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
|
||||
@@ -1567,7 +1558,7 @@ impl<'db> Type<'db> {
|
||||
(Type::Intersection(intersection), Type::NonInferableTypeVar(_))
|
||||
if intersection.negative(db).contains(&target) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
// Two identical typevars must always solve to the same type, so they are always
|
||||
@@ -1692,6 +1683,11 @@ impl<'db> Type<'db> {
|
||||
// TODO: Infer specializations here
|
||||
(Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => ConstraintSet::from(false),
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
// TODO: Implement assignability and subtyping for TypedDict
|
||||
ConstraintSet::from(relation.is_assignability())
|
||||
}
|
||||
|
||||
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
|
||||
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
|
||||
(left, Type::AlwaysFalsy) => ConstraintSet::from(left.bool(db).is_always_false()),
|
||||
@@ -4197,20 +4193,26 @@ impl<'db> Type<'db> {
|
||||
.into()
|
||||
}
|
||||
|
||||
Some(KnownFunction::AssertType) => Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("value")))
|
||||
.with_annotated_type(Type::any()),
|
||||
Parameter::positional_only(Some(Name::new_static("type")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
Some(KnownFunction::AssertType) => {
|
||||
let val_ty =
|
||||
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Invariant);
|
||||
|
||||
Binding::single(
|
||||
self,
|
||||
Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [val_ty])),
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("value")))
|
||||
.with_annotated_type(Type::TypeVar(val_ty)),
|
||||
Parameter::positional_only(Some(Name::new_static("type")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(Type::TypeVar(val_ty)),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
Some(KnownFunction::AssertNever) => {
|
||||
Binding::single(
|
||||
@@ -5206,15 +5208,6 @@ impl<'db> Type<'db> {
|
||||
.unwrap_or_else(|err| err.fallback_enter_type(db))
|
||||
}
|
||||
|
||||
/// Returns the type bound from a context manager with type `self`.
|
||||
///
|
||||
/// This method should only be used outside of type checking because it omits any errors.
|
||||
/// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead.
|
||||
fn aenter(self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.try_enter_with_mode(db, EvaluationMode::Async)
|
||||
.unwrap_or_else(|err| err.fallback_enter_type(db))
|
||||
}
|
||||
|
||||
/// Given the type of an object that is used as a context manager (i.e. in a `with` statement),
|
||||
/// return the return type of its `__enter__` or `__aenter__` method, which is bound to any potential targets.
|
||||
///
|
||||
@@ -5459,28 +5452,19 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
let new_specialization = new_call_outcome
|
||||
.and_then(Result::ok)
|
||||
.as_ref()
|
||||
.and_then(Bindings::single_element)
|
||||
.into_iter()
|
||||
.flat_map(CallableBinding::matching_overloads)
|
||||
.next()
|
||||
.and_then(|(_, binding)| binding.inherited_specialization())
|
||||
.filter(|specialization| {
|
||||
Some(specialization.generic_context(db)) == generic_context
|
||||
});
|
||||
let init_specialization = init_call_outcome
|
||||
.and_then(Result::ok)
|
||||
.as_ref()
|
||||
.and_then(Bindings::single_element)
|
||||
.into_iter()
|
||||
.flat_map(CallableBinding::matching_overloads)
|
||||
.next()
|
||||
.and_then(|(_, binding)| binding.inherited_specialization())
|
||||
.filter(|specialization| {
|
||||
Some(specialization.generic_context(db)) == generic_context
|
||||
});
|
||||
let specialize_constructor = |outcome: Option<Bindings<'db>>| {
|
||||
let (_, binding) = outcome
|
||||
.as_ref()?
|
||||
.single_element()?
|
||||
.matching_overloads()
|
||||
.next()?;
|
||||
binding.specialization()?.restrict(db, generic_context?)
|
||||
};
|
||||
|
||||
let new_specialization =
|
||||
specialize_constructor(new_call_outcome.and_then(Result::ok));
|
||||
let init_specialization =
|
||||
specialize_constructor(init_call_outcome.and_then(Result::ok));
|
||||
let specialization =
|
||||
combine_specializations(db, new_specialization, init_specialization);
|
||||
let specialized = specialization
|
||||
@@ -6771,13 +6755,11 @@ impl<'db> TypeMapping<'_, 'db> {
|
||||
db,
|
||||
context
|
||||
.variables(db)
|
||||
.iter()
|
||||
.filter(|var| !var.typevar(db).is_self(db))
|
||||
.copied(),
|
||||
.filter(|var| !var.typevar(db).is_self(db)),
|
||||
),
|
||||
TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances(
|
||||
db,
|
||||
context.variables(db).iter().map(|typevar| {
|
||||
context.variables(db).map(|typevar| {
|
||||
if typevar.typevar(db).is_self(db) {
|
||||
BoundTypeVarInstance::synthetic_self(
|
||||
db,
|
||||
@@ -6785,7 +6767,7 @@ impl<'db> TypeMapping<'_, 'db> {
|
||||
typevar.binding_context(db),
|
||||
)
|
||||
} else {
|
||||
*typevar
|
||||
typevar
|
||||
}
|
||||
}),
|
||||
),
|
||||
@@ -8993,16 +8975,138 @@ impl<'db> ConstructorCallError<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-exhaustive enumeration of relations that can exist between types.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum TypeRelation {
|
||||
/// The "subtyping" relation.
|
||||
///
|
||||
/// A [fully static] type `B` is a subtype of a fully static type `A` if and only if
|
||||
/// the set of possible runtime values represented by `B` is a subset of the set
|
||||
/// of possible runtime values represented by `A`.
|
||||
///
|
||||
/// For a pair of types `C` and `D` that may or may not be fully static,
|
||||
/// `D` can be said to be a subtype of `C` if every possible fully static
|
||||
/// [materialization] of `D` is a subtype of every possible fully static
|
||||
/// materialization of `C`. Another way of saying this is that `D` will be a
|
||||
/// subtype of `C` if and only if the union of all possible sets of values
|
||||
/// represented by `D` (the "top materialization" of `D`) is a subtype of the
|
||||
/// intersection of all possible sets of values represented by `C` (the "bottom
|
||||
/// materialization" of `C`). More concisely: `D <: C` iff `Top[D] <: Bottom[C]`.
|
||||
///
|
||||
/// For example, `list[Any]` can be said to be a subtype of `Sequence[object]`,
|
||||
/// because every possible fully static materialization of `list[Any]` (`list[int]`,
|
||||
/// `list[str]`, `list[bytes | bool]`, `list[SupportsIndex]`, etc.) would be
|
||||
/// considered a subtype of `Sequence[object]`.
|
||||
///
|
||||
/// Note that this latter expansion of the subtyping relation to non-fully-static
|
||||
/// types is not described in the typing spec, but this expansion to gradual types is
|
||||
/// sound and consistent with the principles laid out in the spec. This definition
|
||||
/// does mean the subtyping relation is not reflexive for non-fully-static types
|
||||
/// (e.g. `Any` is not a subtype of `Any`).
|
||||
///
|
||||
/// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type
|
||||
/// [materialization]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
||||
Subtyping,
|
||||
|
||||
/// The "assignability" relation.
|
||||
///
|
||||
/// The assignability relation between two types `A` and `B` dictates whether a
|
||||
/// type checker should emit an error when a value of type `B` is assigned to a
|
||||
/// variable declared as having type `A`.
|
||||
///
|
||||
/// For a pair of [fully static] types `A` and `B`, the assignability relation
|
||||
/// between `A` and `B` is the same as the subtyping relation.
|
||||
///
|
||||
/// Between a pair of `C` and `D` where either `C` or `D` is not fully static, the
|
||||
/// assignability relation may be more permissive than the subtyping relation. `D`
|
||||
/// can be said to be assignable to `C` if *some* possible fully static [materialization]
|
||||
/// of `D` is a subtype of *some* possible fully static materialization of `C`.
|
||||
/// Another way of saying this is that `D` will be assignable to `C` if and only if the
|
||||
/// intersection of all possible sets of values represented by `D` (the "bottom
|
||||
/// materialization" of `D`) is a subtype of the union of all possible sets of values
|
||||
/// represented by `C` (the "top materialization" of `C`).
|
||||
/// More concisely: `D <: C` iff `Bottom[D] <: Top[C]`.
|
||||
///
|
||||
/// For example, `Any` is not a subtype of `int`, because there are possible
|
||||
/// materializations of `Any` (e.g., `str`) that are not subtypes of `int`.
|
||||
/// `Any` is *assignable* to `int`, however, as there are *some* possible materializations
|
||||
/// of `Any` (such as `int` itself!) that *are* subtypes of `int`. `Any` cannot even
|
||||
/// be considered a subtype of itself, as two separate uses of `Any` in the same scope
|
||||
/// might materialize to different types between which there would exist no subtyping
|
||||
/// relation; nor is `Any` a subtype of `int | Any`, for the same reason. Nonetheless,
|
||||
/// `Any` is assignable to both `Any` and `int | Any`.
|
||||
///
|
||||
/// While `Any` can materialize to anything, the presence of `Any` in a type does not
|
||||
/// necessarily make it assignable to everything. For example, `list[Any]` is not
|
||||
/// assignable to `int`, because there are no possible fully static types we could
|
||||
/// substitute for `Any` in this type that would make it a subtype of `int`. For the
|
||||
/// same reason, a union such as `str | Any` is not assignable to `int`.
|
||||
///
|
||||
/// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type
|
||||
/// [materialization]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
||||
Assignability,
|
||||
|
||||
/// The "redundancy" relation.
|
||||
///
|
||||
/// The redundancy relation dictates whether the union `A | B` can be safely simplified
|
||||
/// to the type `A` without downstream consequences on ty's inference of types elsewhere.
|
||||
///
|
||||
/// For a pair of [fully static] types `A` and `B`, the redundancy relation between `A`
|
||||
/// and `B` is the same as the subtyping relation.
|
||||
///
|
||||
/// Between a pair of `C` and `D` where either `C` or `D` is not fully static, the
|
||||
/// redundancy relation sits in between the subtyping relation and the assignability relation.
|
||||
/// `D` can be said to be redundant in a union with `C` if the top materialization of the type
|
||||
/// `C | D` is equivalent to the top materialization of `C`, *and* the bottom materialization
|
||||
/// of `C | D` is equivalent to the bottom materialization of `C`.
|
||||
/// More concisely: `D <: C` iff `Top[C | D] == Top[C]` AND `Bottom[C | D] == Bottom[C]`.
|
||||
///
|
||||
/// Practically speaking, in most respects the redundancy relation is the same as the subtyping
|
||||
/// relation. It is redundant to add `bool` to a union that includes `int`, because `bool` is a
|
||||
/// subtype of `int`, so inference of attribute access or binary expressions on the union
|
||||
/// `int | bool` would always produce a type that represents the same set of possible sets of
|
||||
/// runtime values as if ty had inferred the attribute access or binary expression on `int`
|
||||
/// alone.
|
||||
///
|
||||
/// Where the redundancy relation differs from the subtyping relation is that there are a
|
||||
/// number of simplifications that can be made when simplifying unions that are not
|
||||
/// strictly permitted by the subtyping relation. For example, it is safe to avoid adding
|
||||
/// `Any` to a union that already includes `Any`, because `Any` already represents an
|
||||
/// unknown set of possible sets of runtime values that can materialize to any type in a
|
||||
/// gradual, permissive way. Inferring attribute access or binary expressions over
|
||||
/// `Any | Any` could never conceivably yield a type that represents a different set of
|
||||
/// possible sets of runtime values to inferring the same expression over `Any` alone;
|
||||
/// although `Any` is not a subtype of `Any`, top materialization of both `Any` and
|
||||
/// `Any | Any` is `object`, and the bottom materialization of both types is `Never`.
|
||||
///
|
||||
/// The same principle also applies to intersections that include `Any` being added to
|
||||
/// unions that include `Any`: for any type `A`, although naively distributing
|
||||
/// type-inference operations over `(Any & A) | Any` could produce types that have
|
||||
/// different displays to `Any`, `(Any & A) | Any` nonetheless has the same top
|
||||
/// materialization as `Any` and the same bottom materialization as `Any`, and thus it is
|
||||
/// redundant to add `Any & A` to a union that already includes `Any`.
|
||||
///
|
||||
/// Union simplification cannot use the assignability relation, meanwhile, as it is
|
||||
/// trivial to produce examples of cases where adding a type `B` to a union that includes
|
||||
/// `A` would impact downstream type inference, even where `B` is assignable to `A`. For
|
||||
/// example, `int` is assignable to `Any`, but attribute access over the union `int | Any`
|
||||
/// will yield very different results to attribute access over `Any` alone. The top
|
||||
/// materialization of `Any` and `int | Any` may be the same type (`object`), but the
|
||||
/// two differ in their bottom materializations (`Never` and `int`, respectively).
|
||||
///
|
||||
/// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type
|
||||
/// [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
||||
Redundancy,
|
||||
}
|
||||
|
||||
impl TypeRelation {
|
||||
pub(crate) const fn is_assignability(self) -> bool {
|
||||
matches!(self, TypeRelation::Assignability)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_subtyping(self) -> bool {
|
||||
matches!(self, TypeRelation::Subtyping)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
|
||||
@@ -502,18 +502,9 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
|
||||
if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) {
|
||||
if ty.is_equivalent_to(self.db, element_type)
|
||||
|| ty.is_subtype_of(self.db, element_type)
|
||||
|| ty.into_intersection().is_some_and(|intersection| {
|
||||
intersection.positive(self.db).contains(&element_type)
|
||||
})
|
||||
{
|
||||
if ty.is_redundant_with(self.db, element_type) {
|
||||
return;
|
||||
} else if element_type.is_subtype_of(self.db, ty)
|
||||
|| element_type
|
||||
.into_intersection()
|
||||
.is_some_and(|intersection| intersection.positive(self.db).contains(&ty))
|
||||
{
|
||||
} else if element_type.is_redundant_with(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
} else if ty_negated.is_subtype_of(self.db, element_type) {
|
||||
// We add `ty` to the union. We just checked that `~ty` is a subtype of an
|
||||
@@ -930,13 +921,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
// S & T = S if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_positive)
|
||||
|| existing_positive.is_equivalent_to(db, new_positive)
|
||||
{
|
||||
if existing_positive.is_redundant_with(db, new_positive) {
|
||||
return;
|
||||
}
|
||||
// same rule, reverse order
|
||||
if new_positive.is_subtype_of(db, *existing_positive) {
|
||||
if new_positive.is_redundant_with(db, *existing_positive) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
// A & B = Never if A and B are disjoint
|
||||
@@ -1027,9 +1016,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
// ~S & ~T = ~T if S <: T
|
||||
if existing_negative.is_subtype_of(db, new_negative)
|
||||
|| existing_negative.is_equivalent_to(db, new_negative)
|
||||
{
|
||||
if existing_negative.is_redundant_with(db, new_negative) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
// same rule, reverse order
|
||||
@@ -1090,7 +1077,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
// don't need to worry about finding any particular constraint more than once.
|
||||
let constraints = constraints.elements(db);
|
||||
let mut positive_constraint_count = 0;
|
||||
for positive in &self.positive {
|
||||
for (i, positive) in self.positive.iter().enumerate() {
|
||||
if i == typevar_index {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This linear search should be fine as long as we don't encounter typevars with
|
||||
// thousands of constraints.
|
||||
positive_constraint_count += constraints
|
||||
|
||||
@@ -32,11 +32,11 @@ use crate::types::tuple::{TupleLength, TupleType};
|
||||
use crate::types::{
|
||||
BoundMethodType, ClassLiteral, DataclassParams, FieldInstance, KnownBoundMethodType,
|
||||
KnownClass, KnownInstanceType, MemberLookupPolicy, PropertyInstanceType, SpecialFormType,
|
||||
TrackedConstraintSet, TypeAliasType, TypeContext, TypeMapping, UnionType,
|
||||
WrapperDescriptorKind, enums, ide_support, todo_type,
|
||||
TrackedConstraintSet, TypeAliasType, TypeContext, UnionBuilder, UnionType,
|
||||
WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, todo_type,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
|
||||
|
||||
/// Binding information for a possible union of callables. At a call site, the arguments must be
|
||||
/// compatible with _all_ of the types in the union for the call to be valid.
|
||||
@@ -1588,6 +1588,14 @@ impl<'db> CallableBinding<'db> {
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
matching_overload_indexes: &[usize],
|
||||
) {
|
||||
// The maximum number of parameters across all the overloads that are being considered
|
||||
// for filtering.
|
||||
let max_parameter_count = matching_overload_indexes
|
||||
.iter()
|
||||
.map(|&index| self.overloads[index].signature.parameters().len())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
// These are the parameter indexes that matches the arguments that participate in the
|
||||
// filtering process.
|
||||
//
|
||||
@@ -1595,41 +1603,67 @@ impl<'db> CallableBinding<'db> {
|
||||
// gradual equivalent to the parameter types at the same index for other overloads.
|
||||
let mut participating_parameter_indexes = HashSet::new();
|
||||
|
||||
// These only contain the top materialized argument types for the corresponding
|
||||
// participating parameter indexes.
|
||||
let mut top_materialized_argument_types = vec![];
|
||||
// The parameter types at each index for the first overload containing a parameter at
|
||||
// that index.
|
||||
let mut first_parameter_types: Vec<Option<Type<'db>>> = vec![None; max_parameter_count];
|
||||
|
||||
for (argument_index, argument_type) in arguments.iter_types().enumerate() {
|
||||
let mut first_parameter_type: Option<Type<'db>> = None;
|
||||
let mut participating_parameter_index = None;
|
||||
|
||||
'overload: for overload_index in matching_overload_indexes {
|
||||
for argument_index in 0..arguments.len() {
|
||||
for overload_index in matching_overload_indexes {
|
||||
let overload = &self.overloads[*overload_index];
|
||||
for parameter_index in &overload.argument_matches[argument_index].parameters {
|
||||
for ¶meter_index in &overload.argument_matches[argument_index].parameters {
|
||||
// TODO: For an unannotated `self` / `cls` parameter, the type should be
|
||||
// `typing.Self` / `type[typing.Self]`
|
||||
let current_parameter_type = overload.signature.parameters()[*parameter_index]
|
||||
let current_parameter_type = overload.signature.parameters()[parameter_index]
|
||||
.annotated_type()
|
||||
.unwrap_or(Type::unknown());
|
||||
let first_parameter_type = &mut first_parameter_types[parameter_index];
|
||||
if let Some(first_parameter_type) = first_parameter_type {
|
||||
if !first_parameter_type.is_equivalent_to(db, current_parameter_type) {
|
||||
participating_parameter_index = Some(*parameter_index);
|
||||
break 'overload;
|
||||
participating_parameter_indexes.insert(parameter_index);
|
||||
}
|
||||
} else {
|
||||
first_parameter_type = Some(current_parameter_type);
|
||||
*first_parameter_type = Some(current_parameter_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parameter_index) = participating_parameter_index {
|
||||
participating_parameter_indexes.insert(parameter_index);
|
||||
top_materialized_argument_types.push(argument_type.top_materialization(db));
|
||||
let mut union_argument_type_builders = std::iter::repeat_with(|| UnionBuilder::new(db))
|
||||
.take(max_parameter_count)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (argument_index, argument_type) in arguments.iter_types().enumerate() {
|
||||
for overload_index in matching_overload_indexes {
|
||||
let overload = &self.overloads[*overload_index];
|
||||
for (parameter_index, variadic_argument_type) in
|
||||
overload.argument_matches[argument_index].iter()
|
||||
{
|
||||
if !participating_parameter_indexes.contains(¶meter_index) {
|
||||
continue;
|
||||
}
|
||||
union_argument_type_builders[parameter_index].add_in_place(
|
||||
variadic_argument_type
|
||||
.unwrap_or(argument_type)
|
||||
.top_materialization(db),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let top_materialized_argument_type =
|
||||
Type::heterogeneous_tuple(db, top_materialized_argument_types);
|
||||
// These only contain the top materialized argument types for the corresponding
|
||||
// participating parameter indexes.
|
||||
let top_materialized_argument_type = Type::heterogeneous_tuple(
|
||||
db,
|
||||
union_argument_type_builders
|
||||
.into_iter()
|
||||
.filter_map(|builder| {
|
||||
if builder.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(builder.build())
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// A flag to indicate whether we've found the overload that makes the remaining overloads
|
||||
// unmatched for the given argument types.
|
||||
@@ -1640,15 +1674,22 @@ impl<'db> CallableBinding<'db> {
|
||||
self.overloads[*current_index].mark_as_unmatched_overload();
|
||||
continue;
|
||||
}
|
||||
let mut parameter_types = Vec::with_capacity(arguments.len());
|
||||
|
||||
let mut union_parameter_types = std::iter::repeat_with(|| UnionBuilder::new(db))
|
||||
.take(max_parameter_count)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// The number of parameters that have been skipped because they don't participate in
|
||||
// the filtering process. This is used to make sure the types are added to the
|
||||
// corresponding parameter index in `union_parameter_types`.
|
||||
let mut skipped_parameters = 0;
|
||||
|
||||
for argument_index in 0..arguments.len() {
|
||||
// The parameter types at the current argument index.
|
||||
let mut current_parameter_types = vec![];
|
||||
for overload_index in &matching_overload_indexes[..=upto] {
|
||||
let overload = &self.overloads[*overload_index];
|
||||
for parameter_index in &overload.argument_matches[argument_index].parameters {
|
||||
if !participating_parameter_indexes.contains(parameter_index) {
|
||||
// This parameter doesn't participate in the filtering process.
|
||||
skipped_parameters += 1;
|
||||
continue;
|
||||
}
|
||||
// TODO: For an unannotated `self` / `cls` parameter, the type should be
|
||||
@@ -1660,21 +1701,24 @@ impl<'db> CallableBinding<'db> {
|
||||
parameter_type =
|
||||
parameter_type.apply_specialization(db, specialization);
|
||||
}
|
||||
if let Some(inherited_specialization) = overload.inherited_specialization {
|
||||
parameter_type =
|
||||
parameter_type.apply_specialization(db, inherited_specialization);
|
||||
}
|
||||
current_parameter_types.push(parameter_type);
|
||||
union_parameter_types[parameter_index.saturating_sub(skipped_parameters)]
|
||||
.add_in_place(parameter_type);
|
||||
}
|
||||
}
|
||||
if current_parameter_types.is_empty() {
|
||||
continue;
|
||||
}
|
||||
parameter_types.push(UnionType::from_elements(db, current_parameter_types));
|
||||
}
|
||||
if top_materialized_argument_type
|
||||
.is_assignable_to(db, Type::heterogeneous_tuple(db, parameter_types))
|
||||
{
|
||||
|
||||
let parameter_types = Type::heterogeneous_tuple(
|
||||
db,
|
||||
union_parameter_types.into_iter().filter_map(|builder| {
|
||||
if builder.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(builder.build())
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if top_materialized_argument_type.is_assignable_to(db, parameter_types) {
|
||||
filter_remaining_overloads = true;
|
||||
}
|
||||
}
|
||||
@@ -1732,7 +1776,7 @@ impl<'db> CallableBinding<'db> {
|
||||
}
|
||||
|
||||
/// Returns the index of the matching overload in the form of [`MatchingOverloadIndex`].
|
||||
fn matching_overload_index(&self) -> MatchingOverloadIndex {
|
||||
pub(crate) fn matching_overload_index(&self) -> MatchingOverloadIndex {
|
||||
let mut matching_overloads = self.matching_overloads();
|
||||
match matching_overloads.next() {
|
||||
None => MatchingOverloadIndex::None,
|
||||
@@ -1750,8 +1794,15 @@ impl<'db> CallableBinding<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all overloads for this call binding, including overloads that did not match.
|
||||
pub(crate) fn overloads(&self) -> &[Binding<'db>] {
|
||||
self.overloads.as_slice()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all the overloads that matched for this call binding.
|
||||
pub(crate) fn matching_overloads(&self) -> impl Iterator<Item = (usize, &Binding<'db>)> {
|
||||
pub(crate) fn matching_overloads(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (usize, &Binding<'db>)> + Clone {
|
||||
self.overloads
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1935,7 +1986,7 @@ impl<'db> CallableBinding<'db> {
|
||||
for overload in overloads.iter().take(MAXIMUM_OVERLOADS) {
|
||||
diag.info(format_args!(
|
||||
" {}",
|
||||
overload.signature(context.db(), None).display(context.db())
|
||||
overload.signature(context.db()).display(context.db())
|
||||
));
|
||||
}
|
||||
if overloads.len() > MAXIMUM_OVERLOADS {
|
||||
@@ -1982,7 +2033,7 @@ enum OverloadCallReturnType<'db> {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MatchingOverloadIndex {
|
||||
pub(crate) enum MatchingOverloadIndex {
|
||||
/// No matching overloads found.
|
||||
None,
|
||||
|
||||
@@ -2396,7 +2447,6 @@ struct ArgumentTypeChecker<'a, 'db> {
|
||||
errors: &'a mut Vec<BindingError<'db>>,
|
||||
|
||||
specialization: Option<Specialization<'db>>,
|
||||
inherited_specialization: Option<Specialization<'db>>,
|
||||
}
|
||||
|
||||
impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
@@ -2418,7 +2468,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
call_expression_tcx,
|
||||
errors,
|
||||
specialization: None,
|
||||
inherited_specialization: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2450,9 +2499,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
}
|
||||
|
||||
fn infer_specialization(&mut self) {
|
||||
if self.signature.generic_context.is_none()
|
||||
&& self.signature.inherited_generic_context.is_none()
|
||||
{
|
||||
if self.signature.generic_context.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2464,9 +2511,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
if let Some(return_ty) = self.signature.return_ty
|
||||
&& let Some(call_expression_tcx) = self.call_expression_tcx.annotation
|
||||
{
|
||||
// Ignore any specialization errors here, because the type context is only used to
|
||||
// optionally widen the return type.
|
||||
let _ = builder.infer(return_ty, call_expression_tcx);
|
||||
match call_expression_tcx {
|
||||
// A type variable is not a useful type-context for expression inference, and applying it
|
||||
// to the return type can lead to confusing unions in nested generic calls.
|
||||
Type::TypeVar(_) => {}
|
||||
|
||||
_ => {
|
||||
// Ignore any specialization errors here, because the type context is only used as a hint
|
||||
// to infer a more assignable return type.
|
||||
let _ = builder.infer(return_ty, call_expression_tcx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let parameters = self.signature.parameters();
|
||||
@@ -2494,14 +2549,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
}
|
||||
|
||||
self.specialization = self.signature.generic_context.map(|gc| builder.build(gc));
|
||||
self.inherited_specialization = self.signature.inherited_generic_context.map(|gc| {
|
||||
// The inherited generic context is used when inferring the specialization of a generic
|
||||
// class from a constructor call. In this case (only), we promote any typevars that are
|
||||
// inferred as a literal to the corresponding instance type.
|
||||
builder
|
||||
.build(gc)
|
||||
.apply_type_mapping(self.db, &TypeMapping::PromoteLiterals)
|
||||
});
|
||||
}
|
||||
|
||||
fn check_argument_type(
|
||||
@@ -2518,11 +2565,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
argument_type = argument_type.apply_specialization(self.db, specialization);
|
||||
expected_ty = expected_ty.apply_specialization(self.db, specialization);
|
||||
}
|
||||
if let Some(inherited_specialization) = self.inherited_specialization {
|
||||
argument_type =
|
||||
argument_type.apply_specialization(self.db, inherited_specialization);
|
||||
expected_ty = expected_ty.apply_specialization(self.db, inherited_specialization);
|
||||
}
|
||||
// This is one of the few places where we want to check if there's _any_ specialization
|
||||
// where assignability holds; normally we want to check that assignability holds for
|
||||
// _all_ specializations.
|
||||
@@ -2694,8 +2736,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> (Option<Specialization<'db>>, Option<Specialization<'db>>) {
|
||||
(self.specialization, self.inherited_specialization)
|
||||
fn finish(self) -> Option<Specialization<'db>> {
|
||||
self.specialization
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2759,10 +2801,6 @@ pub(crate) struct Binding<'db> {
|
||||
/// The specialization that was inferred from the argument types, if the callable is generic.
|
||||
specialization: Option<Specialization<'db>>,
|
||||
|
||||
/// The specialization that was inferred for a class method's containing generic class, if it
|
||||
/// is being used to infer a specialization for the class.
|
||||
inherited_specialization: Option<Specialization<'db>>,
|
||||
|
||||
/// Information about which parameter(s) each argument was matched with, in argument source
|
||||
/// order.
|
||||
argument_matches: Box<[MatchedArgument<'db>]>,
|
||||
@@ -2787,7 +2825,6 @@ impl<'db> Binding<'db> {
|
||||
signature_type,
|
||||
return_ty: Type::unknown(),
|
||||
specialization: None,
|
||||
inherited_specialization: None,
|
||||
argument_matches: Box::from([]),
|
||||
variadic_argument_matched_to_variadic_parameter: false,
|
||||
parameter_tys: Box::from([]),
|
||||
@@ -2858,15 +2895,10 @@ impl<'db> Binding<'db> {
|
||||
checker.infer_specialization();
|
||||
|
||||
checker.check_argument_types();
|
||||
(self.specialization, self.inherited_specialization) = checker.finish();
|
||||
self.specialization = checker.finish();
|
||||
if let Some(specialization) = self.specialization {
|
||||
self.return_ty = self.return_ty.apply_specialization(db, specialization);
|
||||
}
|
||||
if let Some(inherited_specialization) = self.inherited_specialization {
|
||||
self.return_ty = self
|
||||
.return_ty
|
||||
.apply_specialization(db, inherited_specialization);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
|
||||
@@ -2877,8 +2909,8 @@ impl<'db> Binding<'db> {
|
||||
self.return_ty
|
||||
}
|
||||
|
||||
pub(crate) fn inherited_specialization(&self) -> Option<Specialization<'db>> {
|
||||
self.inherited_specialization
|
||||
pub(crate) fn specialization(&self) -> Option<Specialization<'db>> {
|
||||
self.specialization
|
||||
}
|
||||
|
||||
/// Returns the bound types for each parameter, in parameter source order, or `None` if no
|
||||
@@ -2940,7 +2972,6 @@ impl<'db> Binding<'db> {
|
||||
BindingSnapshot {
|
||||
return_ty: self.return_ty,
|
||||
specialization: self.specialization,
|
||||
inherited_specialization: self.inherited_specialization,
|
||||
argument_matches: self.argument_matches.clone(),
|
||||
parameter_tys: self.parameter_tys.clone(),
|
||||
errors: self.errors.clone(),
|
||||
@@ -2951,7 +2982,6 @@ impl<'db> Binding<'db> {
|
||||
let BindingSnapshot {
|
||||
return_ty,
|
||||
specialization,
|
||||
inherited_specialization,
|
||||
argument_matches,
|
||||
parameter_tys,
|
||||
errors,
|
||||
@@ -2959,7 +2989,6 @@ impl<'db> Binding<'db> {
|
||||
|
||||
self.return_ty = return_ty;
|
||||
self.specialization = specialization;
|
||||
self.inherited_specialization = inherited_specialization;
|
||||
self.argument_matches = argument_matches;
|
||||
self.parameter_tys = parameter_tys;
|
||||
self.errors = errors;
|
||||
@@ -2979,7 +3008,6 @@ impl<'db> Binding<'db> {
|
||||
fn reset(&mut self) {
|
||||
self.return_ty = Type::unknown();
|
||||
self.specialization = None;
|
||||
self.inherited_specialization = None;
|
||||
self.argument_matches = Box::from([]);
|
||||
self.parameter_tys = Box::from([]);
|
||||
self.errors.clear();
|
||||
@@ -2990,7 +3018,6 @@ impl<'db> Binding<'db> {
|
||||
struct BindingSnapshot<'db> {
|
||||
return_ty: Type<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
inherited_specialization: Option<Specialization<'db>>,
|
||||
argument_matches: Box<[MatchedArgument<'db>]>,
|
||||
parameter_tys: Box<[Option<Type<'db>>]>,
|
||||
errors: Vec<BindingError<'db>>,
|
||||
@@ -3030,7 +3057,6 @@ impl<'db> CallableBindingSnapshot<'db> {
|
||||
// ... and update the snapshot with the current state of the binding.
|
||||
snapshot.return_ty = binding.return_ty;
|
||||
snapshot.specialization = binding.specialization;
|
||||
snapshot.inherited_specialization = binding.inherited_specialization;
|
||||
snapshot
|
||||
.argument_matches
|
||||
.clone_from(&binding.argument_matches);
|
||||
@@ -3278,6 +3304,23 @@ impl<'db> BindingError<'db> {
|
||||
return;
|
||||
};
|
||||
|
||||
// Re-infer the argument type of call expressions, ignoring the type context for more
|
||||
// precise error messages.
|
||||
let provided_ty = match Self::get_argument_node(node, *argument_index) {
|
||||
None => *provided_ty,
|
||||
|
||||
// Ignore starred arguments, as those are difficult to re-infer.
|
||||
Some(
|
||||
ast::ArgOrKeyword::Arg(ast::Expr::Starred(_))
|
||||
| ast::ArgOrKeyword::Keyword(ast::Keyword { arg: None, .. }),
|
||||
) => *provided_ty,
|
||||
|
||||
Some(
|
||||
ast::ArgOrKeyword::Arg(value)
|
||||
| ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }),
|
||||
) => infer_isolated_expression(context.db(), context.scope(), value),
|
||||
};
|
||||
|
||||
let provided_ty_display = provided_ty.display(context.db());
|
||||
let expected_ty_display = expected_ty.display(context.db());
|
||||
|
||||
@@ -3325,7 +3368,7 @@ impl<'db> BindingError<'db> {
|
||||
}
|
||||
diag.info(format_args!(
|
||||
" {}",
|
||||
overload.signature(context.db(), None).display(context.db())
|
||||
overload.signature(context.db()).display(context.db())
|
||||
));
|
||||
}
|
||||
if overloads.len() > MAXIMUM_OVERLOADS {
|
||||
@@ -3613,22 +3656,29 @@ impl<'db> BindingError<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_node(node: ast::AnyNodeRef, argument_index: Option<usize>) -> ast::AnyNodeRef {
|
||||
fn get_node(node: ast::AnyNodeRef<'_>, argument_index: Option<usize>) -> ast::AnyNodeRef<'_> {
|
||||
// If we have a Call node and an argument index, report the diagnostic on the correct
|
||||
// argument node; otherwise, report it on the entire provided node.
|
||||
match Self::get_argument_node(node, argument_index) {
|
||||
Some(ast::ArgOrKeyword::Arg(expr)) => expr.into(),
|
||||
Some(ast::ArgOrKeyword::Keyword(expr)) => expr.into(),
|
||||
None => node,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_argument_node(
|
||||
node: ast::AnyNodeRef<'_>,
|
||||
argument_index: Option<usize>,
|
||||
) -> Option<ArgOrKeyword<'_>> {
|
||||
match (node, argument_index) {
|
||||
(ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => {
|
||||
match call_node
|
||||
(ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => Some(
|
||||
call_node
|
||||
.arguments
|
||||
.arguments_source_order()
|
||||
.nth(argument_index)
|
||||
.expect("argument index should not be out of range")
|
||||
{
|
||||
ast::ArgOrKeyword::Arg(expr) => expr.into(),
|
||||
ast::ArgOrKeyword::Keyword(keyword) => keyword.into(),
|
||||
}
|
||||
}
|
||||
_ => node,
|
||||
.expect("argument index should not be out of range"),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user