Compare commits
83 Commits
brent/lamb
...
dcreager/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4be76e812 | ||
|
|
b1e354bd99 | ||
|
|
e4a32ba644 | ||
|
|
ac2d07e83c | ||
|
|
8156b45173 | ||
|
|
d063c71177 | ||
|
|
c16ef709f6 | ||
|
|
04a3ec3689 | ||
|
|
1a86e13472 | ||
|
|
901e9cdf49 | ||
|
|
58fa1d71b6 | ||
|
|
d9fc0f08b4 | ||
|
|
09deeabda5 | ||
|
|
1436e688cc | ||
|
|
d6c34b98a5 | ||
|
|
1b50e032a4 | ||
|
|
687ed292f6 | ||
|
|
0554b1ca8a | ||
|
|
bbe42bc775 | ||
|
|
0d2cd84df4 | ||
|
|
665f68036c | ||
|
|
f5fb5c388a | ||
|
|
dbd72480a9 | ||
|
|
75c1a0ae55 | ||
|
|
7a546809c4 | ||
|
|
3065f8dbbc | ||
|
|
fb5b8c3653 | ||
|
|
efa2b5167f | ||
|
|
29acc1e860 | ||
|
|
698231a47a | ||
|
|
d63b4b0383 | ||
|
|
c5d654bce8 | ||
|
|
3e7e91724c | ||
|
|
2a2b719f00 | ||
|
|
ffb7bdd595 | ||
|
|
0a55327d64 | ||
|
|
008e9d06e1 | ||
|
|
8529d79a70 | ||
|
|
8599c7e5b3 | ||
|
|
5f501374c4 | ||
|
|
e9a5337136 | ||
|
|
05cf53aae8 | ||
|
|
6a26f86778 | ||
|
|
d0314131fb | ||
|
|
696d7a5d68 | ||
|
|
66e9d57797 | ||
|
|
87dafb8787 | ||
|
|
9e80e5a3a6 | ||
|
|
f9cc26aa12 | ||
|
|
d49c326309 | ||
|
|
e70fccbf25 | ||
|
|
90b32f3b3b | ||
|
|
99694b6e4a | ||
|
|
67e54fffe1 | ||
|
|
a01b0d7780 | ||
|
|
04ab9170d6 | ||
|
|
12e74ae894 | ||
|
|
d64b2f747c | ||
|
|
cd183c5e1f | ||
|
|
eb1957cd17 | ||
|
|
7e3dd0764a | ||
|
|
a6abd65c2c | ||
|
|
3d4b0559f1 | ||
|
|
2f6f3e1042 | ||
|
|
9dd666d677 | ||
|
|
a1d9cb5830 | ||
|
|
8a85a2961e | ||
|
|
43427abb61 | ||
|
|
84c3cecad6 | ||
|
|
e8e8180888 | ||
|
|
f5cf672ed4 | ||
|
|
6322f37015 | ||
|
|
d272a623d3 | ||
|
|
19c7994e90 | ||
|
|
725ae69773 | ||
|
|
d2c3996f4e | ||
|
|
988c38c013 | ||
|
|
164c2a6cc6 | ||
|
|
1bbe4f0d5e | ||
|
|
cd7354a5c6 | ||
|
|
ec48a47a88 | ||
|
|
43297d3455 | ||
|
|
4373974dd9 |
@@ -7,6 +7,10 @@ serial = { max-threads = 1 }
|
||||
filter = 'binary(file_watching)'
|
||||
test-group = 'serial'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = 'binary(e2e)'
|
||||
test-group = 'serial'
|
||||
|
||||
[profile.ci]
|
||||
# Print out output for failing tests as soon as they fail, and also at the end
|
||||
# of the run (for easy scrollability).
|
||||
|
||||
54
.github/workflows/ci.yaml
vendored
54
.github/workflows/ci.yaml
vendored
@@ -261,15 +261,15 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -319,19 +319,17 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
|
||||
run: cargo nextest run --cargo-profile profiling --all-features
|
||||
- name: "Run doctests"
|
||||
run: cargo test --doc --profile profiling --all-features
|
||||
|
||||
cargo-test-other:
|
||||
strategy:
|
||||
@@ -354,11 +352,11 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -464,7 +462,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
@@ -499,7 +497,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
@@ -534,7 +532,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
activate-environment: true
|
||||
@@ -640,7 +638,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -699,7 +697,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -750,7 +748,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -794,7 +792,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
python-version: 3.13
|
||||
activate-environment: true
|
||||
@@ -949,13 +947,13 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -963,7 +961,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@bb005fe1c1eea036d3894f02c049cb6b154a1c27 # v4.3.3
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: cargo codspeed run
|
||||
@@ -989,13 +987,13 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1003,7 +1001,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@bb005fe1c1eea036d3894f02c049cb6b154a1c27 # v4.3.3
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: cargo codspeed run
|
||||
@@ -1029,13 +1027,13 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1043,7 +1041,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@bb005fe1c1eea036d3894f02c049cb6b154a1c27 # v4.3.3
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
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
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
|
||||
4
.github/workflows/mypy_primer.yaml
vendored
4
.github/workflows/mypy_primer.yaml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
|
||||
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
15
.github/workflows/sync_typeshed.yaml
vendored
15
.github/workflows/sync_typeshed.yaml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
git config --global user.email '<>'
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -207,17 +207,22 @@ jobs:
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Update snapshots
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
cargo r \
|
||||
--profile=profiling \
|
||||
-p ty_completion_eval \
|
||||
-- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
|
||||
|
||||
# The `cargo insta` docs indicate that `--unreferenced=delete` might be a good option,
|
||||
# but from local testing it appears to just revert all changes made by `cargo insta test --accept`.
|
||||
#
|
||||
|
||||
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
|
||||
4
.github/workflows/ty-ecosystem-report.yaml
vendored
4
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.5
|
||||
|
||||
Released on 2025-11-13.
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Apply `SIM113` when index variable is of type `int` ([#21395](https://github.com/astral-sh/ruff/pull/21395))
|
||||
- \[`pydoclint`\] Fix false positive when Sphinx directives follow a "Raises" section (`DOC502`) ([#20535](https://github.com/astral-sh/ruff/pull/20535))
|
||||
- \[`pydoclint`\] Support NumPy-style comma-separated parameters (`DOC102`) ([#20972](https://github.com/astral-sh/ruff/pull/20972))
|
||||
- \[`refurb`\] Auto-fix annotated assignments (`FURB101`) ([#21278](https://github.com/astral-sh/ruff/pull/21278))
|
||||
- \[`ruff`\] Ignore `str()` when not used for simple conversion (`RUF065`) ([#21330](https://github.com/astral-sh/ruff/pull/21330))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix syntax error false positive on alternative `match` patterns ([#21362](https://github.com/astral-sh/ruff/pull/21362))
|
||||
- \[`flake8-simplify`\] Fix false positive for iterable initializers with generator arguments (`SIM222`) ([#21187](https://github.com/astral-sh/ruff/pull/21187))
|
||||
- \[`pyupgrade`\] Fix false positive on relative imports from local `.builtins` module (`UP029`) ([#21309](https://github.com/astral-sh/ruff/pull/21309))
|
||||
- \[`pyupgrade`\] Consistently set the deprecated tag (`UP035`) ([#21396](https://github.com/astral-sh/ruff/pull/21396))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`refurb`\] Detect empty f-strings (`FURB105`) ([#21348](https://github.com/astral-sh/ruff/pull/21348))
|
||||
|
||||
### CLI
|
||||
|
||||
- Add option to provide a reason to `--add-noqa` ([#21294](https://github.com/astral-sh/ruff/pull/21294))
|
||||
- Add upstream linter URL to `ruff linter --output-format=json` ([#21316](https://github.com/astral-sh/ruff/pull/21316))
|
||||
- Add color to `--help` ([#21337](https://github.com/astral-sh/ruff/pull/21337))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add a new "Opening a PR" section to the contribution guide ([#21298](https://github.com/astral-sh/ruff/pull/21298))
|
||||
- Added the PyScripter IDE to the list of "Who is using Ruff?" ([#21402](https://github.com/astral-sh/ruff/pull/21402))
|
||||
- Update PyCharm setup instructions ([#21409](https://github.com/astral-sh/ruff/pull/21409))
|
||||
- \[`flake8-annotations`\] Add link to `allow-star-arg-any` option (`ANN401`) ([#21326](https://github.com/astral-sh/ruff/pull/21326))
|
||||
|
||||
### Other changes
|
||||
|
||||
- \[`configuration`\] Improve error message when `line-length` exceeds `u16::MAX` ([#21329](https://github.com/astral-sh/ruff/pull/21329))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@njhearp](https://github.com/njhearp)
|
||||
- [@11happy](https://github.com/11happy)
|
||||
- [@hugovk](https://github.com/hugovk)
|
||||
- [@Gankra](https://github.com/Gankra)
|
||||
- [@ntBre](https://github.com/ntBre)
|
||||
- [@pyscripter](https://github.com/pyscripter)
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@MichaReiser](https://github.com/MichaReiser)
|
||||
- [@henryiii](https://github.com/henryiii)
|
||||
- [@charliecloudberry](https://github.com/charliecloudberry)
|
||||
|
||||
## 0.14.4
|
||||
|
||||
Released on 2025-11-06.
|
||||
|
||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -1238,9 +1238,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size-derive2"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f"
|
||||
checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"quote",
|
||||
@@ -1249,9 +1249,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size2"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9"
|
||||
checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
@@ -1575,9 +1575,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.18.2"
|
||||
version = "0.18.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
|
||||
checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
|
||||
dependencies = [
|
||||
"console 0.16.1",
|
||||
"portable-atomic",
|
||||
@@ -2606,9 +2606,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-junit"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed1a693391a16317257103ad06a88c6529ac640846021da7c435a06fffdacd7"
|
||||
checksum = "6ee9342d671fae8d66b3ae9fd7a9714dfd089c04d2a8b1ec0436ef77aee15e5f"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"indexmap",
|
||||
@@ -2621,9 +2621,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
version = "0.38.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2858,7 +2858,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -3115,7 +3115,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3470,7 +3470,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3586,7 +3586,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3610,12 +3610,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4521,6 +4521,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
|
||||
@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "05a9af7f554b64b8aadc2eeb6f2caf73d0408d09", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a885bb4c4c192741b8a17418fef81a71e33d111e", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
||||
@@ -147,8 +147,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.14.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.4/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.14.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.5/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -181,7 +181,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.14.4
|
||||
rev: v0.14.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
@@ -491,6 +491,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PyTorch](https://github.com/pytorch/pytorch)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [PyScripter](https://github.com/pyscripter/pyscripter)
|
||||
- [PyVista](https://github.com/pyvista/pyvista)
|
||||
- [Reflex](https://github.com/reflex-dev/reflex)
|
||||
- [River](https://github.com/online-ml/river)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -167,6 +167,7 @@ pub enum AnalyzeCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
#[expect(clippy::struct_excessive_bools)]
|
||||
pub struct AnalyzeGraphCommand {
|
||||
/// List of files or directories to include.
|
||||
#[clap(help = "List of files or directories to include [default: .]")]
|
||||
@@ -193,6 +194,12 @@ pub struct AnalyzeGraphCommand {
|
||||
/// Path to a virtual environment to use for resolving additional dependencies
|
||||
#[arg(long)]
|
||||
python: Option<PathBuf>,
|
||||
/// Include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks).
|
||||
/// Use `--no-type-checking-imports` to exclude imports that are only used for type checking.
|
||||
#[arg(long, overrides_with("no_type_checking_imports"))]
|
||||
type_checking_imports: bool,
|
||||
#[arg(long, overrides_with("type_checking_imports"), hide = true)]
|
||||
no_type_checking_imports: bool,
|
||||
}
|
||||
|
||||
// The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient
|
||||
@@ -839,6 +846,10 @@ impl AnalyzeGraphCommand {
|
||||
string_imports_min_dots: self.min_dots,
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
target_version: self.target_version.map(ast::PythonVersion::from),
|
||||
type_checking_imports: resolve_bool_arg(
|
||||
self.type_checking_imports,
|
||||
self.no_type_checking_imports,
|
||||
),
|
||||
..ExplicitConfigOverrides::default()
|
||||
};
|
||||
|
||||
@@ -1335,6 +1346,7 @@ struct ExplicitConfigOverrides {
|
||||
extension: Option<Vec<ExtensionPair>>,
|
||||
detect_string_imports: Option<bool>,
|
||||
string_imports_min_dots: Option<usize>,
|
||||
type_checking_imports: Option<bool>,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
@@ -1425,6 +1437,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
|
||||
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
|
||||
}
|
||||
if let Some(type_checking_imports) = &self.type_checking_imports {
|
||||
config.analyze.type_checking_imports = Some(*type_checking_imports);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ pub(crate) fn analyze_graph(
|
||||
let settings = resolver.resolve(path);
|
||||
let string_imports = settings.analyze.string_imports;
|
||||
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
|
||||
let type_checking_imports = settings.analyze.type_checking_imports;
|
||||
|
||||
// Skip excluded files.
|
||||
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
|
||||
@@ -167,6 +168,7 @@ pub(crate) fn analyze_graph(
|
||||
&path,
|
||||
package.as_deref(),
|
||||
string_imports,
|
||||
type_checking_imports,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to generate import map for {path}: {err}");
|
||||
|
||||
193
crates/ruff/tests/cli/analyze_graph.rs
Normal file
193
crates/ruff/tests/cli/analyze_graph.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use std::process::Command;
|
||||
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
#[test]
|
||||
fn type_checking_imports() -> anyhow::Result<()> {
|
||||
let test = AnalyzeTest::with_files([
|
||||
("ruff/__init__.py", ""),
|
||||
(
|
||||
"ruff/a.py",
|
||||
r#"
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ruff.b
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ruff.c
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"ruff/b.py",
|
||||
r#"
|
||||
if TYPE_CHECKING:
|
||||
from ruff import c
|
||||
"#,
|
||||
),
|
||||
("ruff/c.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(test.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py",
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
test.command()
|
||||
.arg("--no-type-checking-imports"),
|
||||
@r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_checking_imports_from_config() -> anyhow::Result<()> {
|
||||
let test = AnalyzeTest::with_files([
|
||||
("ruff/__init__.py", ""),
|
||||
(
|
||||
"ruff/a.py",
|
||||
r#"
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ruff.b
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ruff.c
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"ruff/b.py",
|
||||
r#"
|
||||
if TYPE_CHECKING:
|
||||
from ruff import c
|
||||
"#,
|
||||
),
|
||||
("ruff/c.py", ""),
|
||||
(
|
||||
"ruff.toml",
|
||||
r#"
|
||||
[analyze]
|
||||
type-checking-imports = false
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(test.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
test.write_file(
|
||||
"ruff.toml",
|
||||
r#"
|
||||
[analyze]
|
||||
type-checking-imports = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(test.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py",
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct AnalyzeTest {
|
||||
cli_test: CliTest,
|
||||
}
|
||||
|
||||
impl AnalyzeTest {
|
||||
pub(crate) fn new() -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
cli_test: CliTest::with_settings(|_, mut settings| {
|
||||
settings.add_filter(r#"\\\\"#, "/");
|
||||
settings
|
||||
})?,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_files<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_files(files)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
#[expect(unused)]
|
||||
fn with_file(path: impl AsRef<std::path::Path>, content: &str) -> anyhow::Result<Self> {
|
||||
let fixture = Self::new()?;
|
||||
fixture.write_file(path, content)?;
|
||||
Ok(fixture)
|
||||
}
|
||||
|
||||
fn command(&self) -> Command {
|
||||
let mut command = self.cli_test.command();
|
||||
command.arg("analyze").arg("graph").arg("--preview");
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AnalyzeTest {
|
||||
type Target = CliTest;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cli_test
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
mod analyze_graph;
|
||||
mod format;
|
||||
mod lint;
|
||||
|
||||
@@ -62,9 +63,7 @@ impl CliTest {
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
for file in files {
|
||||
case.write_file(file.0, file.1)?;
|
||||
}
|
||||
case.write_files(files)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
@@ -153,6 +152,16 @@ impl CliTest {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_files<'a>(
|
||||
&self,
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> Result<()> {
|
||||
for file in files {
|
||||
self.write_file(file.0, file.1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the path to the test directory root.
|
||||
pub(crate) fn root(&self) -> &Path {
|
||||
&self.project_dir
|
||||
|
||||
@@ -9,7 +9,6 @@ info:
|
||||
- concise
|
||||
- "--show-settings"
|
||||
- test.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -284,5 +283,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -12,7 +12,6 @@ info:
|
||||
- UP007
|
||||
- test.py
|
||||
- "-"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -286,5 +285,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -13,7 +13,6 @@ info:
|
||||
- UP007
|
||||
- test.py
|
||||
- "-"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -288,5 +287,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -14,7 +14,6 @@ info:
|
||||
- py310
|
||||
- test.py
|
||||
- "-"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -288,5 +287,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -11,7 +11,6 @@ info:
|
||||
- "--select"
|
||||
- UP007
|
||||
- foo/test.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -285,5 +284,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -11,7 +11,6 @@ info:
|
||||
- "--select"
|
||||
- UP007
|
||||
- foo/test.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -285,5 +284,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -283,5 +283,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -283,5 +283,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -9,7 +9,6 @@ info:
|
||||
- concise
|
||||
- test.py
|
||||
- "--show-settings"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -284,5 +283,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -396,5 +396,6 @@ analyze.target_version = 3.7
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -71,16 +71,13 @@ impl Display for Benchmark<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_project(db: &ProjectDatabase, max_diagnostics: usize) {
|
||||
fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usize) {
|
||||
let result = db.check();
|
||||
let diagnostics = result.len();
|
||||
|
||||
assert!(
|
||||
diagnostics > 1 && diagnostics <= max_diagnostics,
|
||||
"Expected between {} and {} diagnostics but got {}",
|
||||
1,
|
||||
max_diagnostics,
|
||||
diagnostics
|
||||
"Expected between 1 and {max_diagnostics} diagnostics on project '{project_name}' but got {diagnostics}",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY39,
|
||||
},
|
||||
1000,
|
||||
5000,
|
||||
);
|
||||
|
||||
static SYMPY: Benchmark = Benchmark::new(
|
||||
@@ -226,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
800,
|
||||
900,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
@@ -234,11 +231,11 @@ fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
bencher
|
||||
.with_inputs(|| benchmark.setup_iteration())
|
||||
.bench_local_refs(|db| {
|
||||
check_project(db, benchmark.max_diagnostics);
|
||||
check_project(db, benchmark.project.name, benchmark.max_diagnostics);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench(args=[&ALTAIR, &FREQTRADE, &PYDANTIC, &TANJUN], sample_size=2, sample_count=3)]
|
||||
#[bench(args=[&ALTAIR, &FREQTRADE, &TANJUN], sample_size=2, sample_count=3)]
|
||||
fn small(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
@@ -248,12 +245,12 @@ fn medium(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
#[bench(args=[&SYMPY], sample_size=1, sample_count=2)]
|
||||
#[bench(args=[&SYMPY, &PYDANTIC], sample_size=1, sample_count=2)]
|
||||
fn large(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
#[bench(args=[&PYDANTIC], sample_size=3, sample_count=8)]
|
||||
#[bench(args=[&ALTAIR], sample_size=3, sample_count=8)]
|
||||
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
|
||||
|
||||
@@ -261,7 +258,7 @@ fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
.with_inputs(|| benchmark.setup_iteration())
|
||||
.bench_local_values(|db| {
|
||||
thread_pool.install(|| {
|
||||
check_project(&db, benchmark.max_diagnostics);
|
||||
check_project(&db, benchmark.project.name, benchmark.max_diagnostics);
|
||||
db
|
||||
})
|
||||
});
|
||||
@@ -285,7 +282,7 @@ fn main() {
|
||||
// branch when looking up the ingredient index.
|
||||
{
|
||||
let db = TANJUN.setup_iteration();
|
||||
check_project(&db, TANJUN.max_diagnostics);
|
||||
check_project(&db, TANJUN.project.name, TANJUN.max_diagnostics);
|
||||
}
|
||||
|
||||
divan::main();
|
||||
|
||||
@@ -7,6 +7,7 @@ use ruff_source_file::LineIndex;
|
||||
|
||||
use crate::Db;
|
||||
use crate::files::{File, FilePath};
|
||||
use crate::system::System;
|
||||
|
||||
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||
@@ -15,7 +16,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
let _span = tracing::trace_span!("source_text", file = %path).entered();
|
||||
let mut read_error = None;
|
||||
|
||||
let kind = if is_notebook(file.path(db)) {
|
||||
let kind = if is_notebook(db.system(), path) {
|
||||
file.read_to_notebook(db)
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::debug!("Failed to read notebook '{path}': {error}");
|
||||
@@ -40,18 +41,17 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_notebook(path: &FilePath) -> bool {
|
||||
match path {
|
||||
FilePath::System(system) => system.extension().is_some_and(|extension| {
|
||||
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
|
||||
}),
|
||||
FilePath::SystemVirtual(system_virtual) => {
|
||||
system_virtual.extension().is_some_and(|extension| {
|
||||
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
|
||||
})
|
||||
}
|
||||
FilePath::Vendored(_) => false,
|
||||
}
|
||||
fn is_notebook(system: &dyn System, path: &FilePath) -> bool {
|
||||
let source_type = match path {
|
||||
FilePath::System(path) => system.source_type(path),
|
||||
FilePath::SystemVirtual(system_virtual) => system.virtual_path_source_type(system_virtual),
|
||||
FilePath::Vendored(_) => return false,
|
||||
};
|
||||
|
||||
let with_extension_fallback =
|
||||
source_type.or_else(|| PySourceType::try_from_extension(path.extension()?));
|
||||
|
||||
with_extension_fallback == Some(PySourceType::Ipynb)
|
||||
}
|
||||
|
||||
/// The source text of a file containing python code.
|
||||
|
||||
@@ -9,6 +9,7 @@ pub use os::OsSystem;
|
||||
|
||||
use filetime::FileTime;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -16,12 +17,11 @@ use std::{fmt, io};
|
||||
pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
|
||||
use walk_directory::WalkDirectoryBuilder;
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
|
||||
pub use self::path::{
|
||||
DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, SystemVirtualPath,
|
||||
SystemVirtualPathBuf, deduplicate_nested_paths,
|
||||
};
|
||||
use crate::file_revision::FileRevision;
|
||||
|
||||
mod memory_fs;
|
||||
#[cfg(feature = "os")]
|
||||
@@ -66,6 +66,35 @@ pub trait System: Debug + Sync + Send {
|
||||
/// See [dunce::canonicalize] for more information.
|
||||
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
|
||||
|
||||
/// Returns the source type for `path` if known or `None`.
|
||||
///
|
||||
/// The default is to always return `None`, assuming the system
|
||||
/// has no additional information and that the caller should
|
||||
/// rely on the file extension instead.
|
||||
///
|
||||
/// This is primarily used for the LSP integration to respect
|
||||
/// the chosen language (or the fact that it is a notebook) in
|
||||
/// the editor.
|
||||
fn source_type(&self, path: &SystemPath) -> Option<PySourceType> {
|
||||
let _ = path;
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the source type for `path` if known or `None`.
|
||||
///
|
||||
/// The default is to always return `None`, assuming the system
|
||||
/// has no additional information and that the caller should
|
||||
/// rely on the file extension instead.
|
||||
///
|
||||
/// This is primarily used for the LSP integration to respect
|
||||
/// the chosen language (or the fact that it is a notebook) in
|
||||
/// the editor.
|
||||
fn virtual_path_source_type(&self, path: &SystemVirtualPath) -> Option<PySourceType> {
|
||||
let _ = path;
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Reads the content of the file at `path` into a [`String`].
|
||||
fn read_to_string(&self, path: &SystemPath) -> Result<String>;
|
||||
|
||||
|
||||
@@ -14,14 +14,21 @@ pub(crate) struct Collector<'a> {
|
||||
string_imports: StringImports,
|
||||
/// The collected imports from the Python AST.
|
||||
imports: Vec<CollectedImport>,
|
||||
/// Whether to detect type checking imports
|
||||
type_checking_imports: bool,
|
||||
}
|
||||
|
||||
impl<'a> Collector<'a> {
|
||||
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: StringImports) -> Self {
|
||||
pub(crate) fn new(
|
||||
module_path: Option<&'a [String]>,
|
||||
string_imports: StringImports,
|
||||
type_checking_imports: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
module_path,
|
||||
string_imports,
|
||||
imports: Vec::new(),
|
||||
type_checking_imports,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +98,25 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
node_index: _,
|
||||
}) => {
|
||||
// Skip TYPE_CHECKING blocks if not requested
|
||||
if self.type_checking_imports || !is_type_checking_condition(test) {
|
||||
self.visit_body(body);
|
||||
}
|
||||
|
||||
for clause in elif_else_clauses {
|
||||
self.visit_elif_else_clause(clause);
|
||||
}
|
||||
}
|
||||
Stmt::FunctionDef(_)
|
||||
| Stmt::ClassDef(_)
|
||||
| Stmt::While(_)
|
||||
| Stmt::If(_)
|
||||
| Stmt::With(_)
|
||||
| Stmt::Match(_)
|
||||
| Stmt::Try(_)
|
||||
@@ -152,6 +174,30 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an expression is a `TYPE_CHECKING` condition.
|
||||
///
|
||||
/// Returns `true` for:
|
||||
/// - `TYPE_CHECKING`
|
||||
/// - `typing.TYPE_CHECKING`
|
||||
///
|
||||
/// NOTE: Aliased `TYPE_CHECKING`, i.e. `import typing.TYPE_CHECKING as TC; if TC: ...`
|
||||
/// will not be detected!
|
||||
fn is_type_checking_condition(expr: &Expr) -> bool {
|
||||
match expr {
|
||||
// `if TYPE_CHECKING:`
|
||||
Expr::Name(ast::ExprName { id, .. }) => id.as_str() == "TYPE_CHECKING",
|
||||
// `if typing.TYPE_CHECKING:`
|
||||
Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
|
||||
attr.as_str() == "TYPE_CHECKING"
|
||||
&& matches!(
|
||||
value.as_ref(),
|
||||
Expr::Name(ast::ExprName { id, .. }) if id.as_str() == "typing"
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CollectedImport {
|
||||
/// The import was part of an `import` statement.
|
||||
|
||||
@@ -30,6 +30,7 @@ impl ModuleImports {
|
||||
path: &SystemPath,
|
||||
package: Option<&SystemPath>,
|
||||
string_imports: StringImports,
|
||||
type_checking_imports: bool,
|
||||
) -> Result<Self> {
|
||||
// Parse the source code.
|
||||
let parsed = parse(source, ParseOptions::from(source_type))?;
|
||||
@@ -38,8 +39,12 @@ impl ModuleImports {
|
||||
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));
|
||||
|
||||
// Collect the imports.
|
||||
let imports =
|
||||
Collector::new(module_path.as_deref(), string_imports).collect(parsed.syntax());
|
||||
let imports = Collector::new(
|
||||
module_path.as_deref(),
|
||||
string_imports,
|
||||
type_checking_imports,
|
||||
)
|
||||
.collect(parsed.syntax());
|
||||
|
||||
// Resolve the imports.
|
||||
let mut resolved_imports = ModuleImports::default();
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Default, Clone, CacheKey)]
|
||||
#[derive(Debug, Clone, CacheKey)]
|
||||
pub struct AnalyzeSettings {
|
||||
pub exclude: FilePatternSet,
|
||||
pub preview: PreviewMode,
|
||||
@@ -14,6 +14,21 @@ pub struct AnalyzeSettings {
|
||||
pub string_imports: StringImports,
|
||||
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
|
||||
pub extension: ExtensionMapping,
|
||||
pub type_checking_imports: bool,
|
||||
}
|
||||
|
||||
impl Default for AnalyzeSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
exclude: FilePatternSet::default(),
|
||||
preview: PreviewMode::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
string_imports: StringImports::default(),
|
||||
include_dependencies: BTreeMap::default(),
|
||||
extension: ExtensionMapping::default(),
|
||||
type_checking_imports: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AnalyzeSettings {
|
||||
@@ -29,6 +44,7 @@ impl fmt::Display for AnalyzeSettings {
|
||||
self.string_imports,
|
||||
self.extension | debug,
|
||||
self.include_dependencies | debug,
|
||||
self.type_checking_imports,
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -46,7 +46,8 @@ def func():
|
||||
|
||||
|
||||
def func():
|
||||
# OK (index doesn't start at 0
|
||||
# SIM113
|
||||
# https://github.com/astral-sh/ruff/pull/21395
|
||||
idx = 10
|
||||
for x in range(5):
|
||||
g(x, idx)
|
||||
|
||||
@@ -371,6 +371,61 @@ class Foo:
|
||||
"""
|
||||
return
|
||||
|
||||
# DOC102 - Test case from issue #20959: comma-separated parameters
|
||||
def leq(x: object, y: object) -> bool:
|
||||
"""Compare two objects for loose equality.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x1, x2 : object
|
||||
Objects.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the objects are identical or equal.
|
||||
"""
|
||||
return x is y or x == y
|
||||
|
||||
|
||||
# OK - comma-separated parameters that match function signature
|
||||
def compare_values(x1: int, x2: int) -> bool:
|
||||
"""Compare two integer values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x1, x2 : int
|
||||
Values to compare.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if values are equal.
|
||||
"""
|
||||
return x1 == x2
|
||||
|
||||
|
||||
# DOC102 - mixed comma-separated and regular parameters
|
||||
def process_data(data, x1: str, x2: str) -> str:
|
||||
"""Process data with multiple string parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : list
|
||||
Input data to process.
|
||||
x1, x2 : str
|
||||
String parameters for processing.
|
||||
extra_param : str
|
||||
Extra parameter not in signature.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Processed result.
|
||||
"""
|
||||
return f"{x1}{x2}{len(data)}"
|
||||
|
||||
|
||||
# OK
|
||||
def baz(x: int) -> int:
|
||||
"""
|
||||
@@ -389,3 +444,21 @@ def baz(x: int) -> int:
|
||||
int
|
||||
"""
|
||||
return x
|
||||
|
||||
|
||||
# OK - comma-separated parameters without type annotations
|
||||
def add_numbers(a, b):
|
||||
"""
|
||||
Adds two numbers and returns the result.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
a, b
|
||||
The numbers to add.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The sum of the two numbers.
|
||||
"""
|
||||
return a + b
|
||||
|
||||
@@ -83,6 +83,37 @@ def calculate_speed(distance: float, time: float) -> float:
|
||||
raise
|
||||
|
||||
|
||||
# DOC502 regression for Sphinx directive after Raises (issue #18959)
|
||||
def foo():
|
||||
"""First line.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
some text
|
||||
|
||||
.. versionadded:: 0.7.0
|
||||
The ``init_kwargs`` argument.
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
|
||||
# DOC502 regression for following section with colons
|
||||
def example_with_following_section():
|
||||
"""Summary.
|
||||
|
||||
Returns:
|
||||
str: The resulting expression.
|
||||
|
||||
Raises:
|
||||
ValueError: If the unit is not valid.
|
||||
|
||||
Relation to `time_range_lookup`:
|
||||
- Handles the "start of" modifier.
|
||||
- Example: "start of month" → `DATETRUNC()`.
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
|
||||
# This should NOT trigger DOC502 because OSError is explicitly re-raised
|
||||
def f():
|
||||
"""Do nothing.
|
||||
|
||||
@@ -117,3 +117,33 @@ def calculate_speed(distance: float, time: float) -> float:
|
||||
except TypeError:
|
||||
print("Not a number? Shame on you!")
|
||||
raise
|
||||
|
||||
|
||||
# DOC502 regression for Sphinx directive after Raises (issue #18959)
|
||||
def foo():
|
||||
"""First line.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
some text
|
||||
|
||||
.. versionadded:: 0.7.0
|
||||
The ``init_kwargs`` argument.
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
# Make sure we don't bail out on a Sphinx directive in the description of one
|
||||
# of the exceptions
|
||||
def foo():
|
||||
"""First line.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
some text
|
||||
.. math:: e^{xception}
|
||||
ZeroDivisionError
|
||||
Will not be raised, DOC502
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
@@ -152,4 +152,13 @@ import json
|
||||
data = {"price": 100}
|
||||
|
||||
with open("test.json", "wb") as f:
|
||||
f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/21381
|
||||
with open("tmp_path/pyproject.toml", "w") as f:
|
||||
f.write(dedent(
|
||||
"""
|
||||
[project]
|
||||
other = 1.234
|
||||
""",
|
||||
))
|
||||
|
||||
@@ -132,3 +132,9 @@ class AWithQuotes:
|
||||
final_variable: 'Final[list[int]]' = []
|
||||
class_variable_without_subscript: 'ClassVar' = []
|
||||
final_variable_without_subscript: 'Final' = []
|
||||
|
||||
|
||||
# Reassignment of a ClassVar should not trigger RUF012
|
||||
class P:
|
||||
class_variable: ClassVar[list] = [10, 20, 30, 40, 50]
|
||||
class_variable = [*class_variable[0::1], *class_variable[2::3]]
|
||||
|
||||
@@ -860,23 +860,17 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
}
|
||||
|
||||
fn is_bound_parameter(&self, name: &str) -> bool {
|
||||
for scope in self.semantic.current_scopes() {
|
||||
match scope.kind {
|
||||
ScopeKind::Class(_) => return false,
|
||||
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. })
|
||||
| ScopeKind::Lambda(ast::ExprLambda {
|
||||
parameters: Some(parameters),
|
||||
..
|
||||
}) => return parameters.includes(name),
|
||||
ScopeKind::Lambda(_)
|
||||
| ScopeKind::Generator { .. }
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::Type
|
||||
| ScopeKind::DunderClassCell => {}
|
||||
match self.semantic.current_scope().kind {
|
||||
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. }) => {
|
||||
parameters.includes(name)
|
||||
}
|
||||
ScopeKind::Class(_)
|
||||
| ScopeKind::Lambda(_)
|
||||
| ScopeKind::Generator { .. }
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::Type
|
||||
| ScopeKind::DunderClassCell => false,
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ impl<'a> Importer<'a> {
|
||||
.into_edit(&required_import)
|
||||
} else {
|
||||
// Insert at the start of the file.
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
|
||||
.into_edit(&required_import)
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ impl<'a> Importer<'a> {
|
||||
Insertion::end_of_statement(stmt, self.source, self.stylist)
|
||||
} else {
|
||||
// Insert at the start of the file.
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
|
||||
};
|
||||
let add_import_edit = insertion.into_edit(&content);
|
||||
|
||||
@@ -498,7 +498,7 @@ impl<'a> Importer<'a> {
|
||||
Insertion::end_of_statement(stmt, self.source, self.stylist)
|
||||
} else {
|
||||
// Insert at the start of the file.
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
|
||||
};
|
||||
if insertion.is_inline() {
|
||||
Err(anyhow::anyhow!(
|
||||
|
||||
@@ -269,3 +269,8 @@ pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSett
|
||||
pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/21395
|
||||
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
|
||||
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
|
||||
#[test_case(Rule::EnumerateForLoop, Path::new("SIM113.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::preview::is_enumerate_for_loop_int_index_enabled;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, Int, Number, Operator, Stmt};
|
||||
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -11,6 +13,9 @@ use crate::checkers::ast::Checker;
|
||||
/// Checks for `for` loops with explicit loop-index variables that can be replaced
|
||||
/// with `enumerate()`.
|
||||
///
|
||||
/// In [preview], this rule checks for index variables initialized with any integer rather than only
|
||||
/// a literal zero.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When iterating over a sequence, it's often desirable to keep track of the
|
||||
/// index of each element alongside the element itself. Prefer the `enumerate`
|
||||
@@ -35,6 +40,8 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `enumerate`](https://docs.python.org/3/library/functions.html#enumerate)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.2.0")]
|
||||
pub(crate) struct EnumerateForLoop {
|
||||
@@ -82,17 +89,21 @@ pub(crate) fn enumerate_for_loop(checker: &Checker, for_stmt: &ast::StmtFor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure that the index variable was initialized to 0.
|
||||
// Ensure that the index variable was initialized to 0 (or instance of `int` if preview is enabled).
|
||||
let Some(value) = typing::find_binding_value(binding, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(
|
||||
if !(matches!(
|
||||
value,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: Number::Int(Int::ZERO),
|
||||
..
|
||||
})
|
||||
) {
|
||||
) || matches!(
|
||||
ResolvedPythonType::from(value),
|
||||
ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer))
|
||||
) && is_enumerate_for_loop_int_index_enabled(checker.settings()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:6:9
|
||||
|
|
||||
4 | for x in range(5):
|
||||
5 | g(x, idx)
|
||||
6 | idx += 1
|
||||
| ^^^^^^^^
|
||||
7 | h(x)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:17:9
|
||||
|
|
||||
15 | if g(x):
|
||||
16 | break
|
||||
17 | idx += 1
|
||||
| ^^^^^^^^
|
||||
18 | sum += h(x, idx)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:27:9
|
||||
|
|
||||
25 | g(x)
|
||||
26 | h(x, y)
|
||||
27 | idx += 1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:36:9
|
||||
|
|
||||
34 | for x in range(5):
|
||||
35 | sum += h(x, idx)
|
||||
36 | idx += 1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:44:9
|
||||
|
|
||||
42 | for x in range(5):
|
||||
43 | g(x, idx)
|
||||
44 | idx += 1
|
||||
| ^^^^^^^^
|
||||
45 | h(x)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:54:9
|
||||
|
|
||||
52 | for x in range(5):
|
||||
53 | g(x, idx)
|
||||
54 | idx += 1
|
||||
| ^^^^^^^^
|
||||
55 | h(x)
|
||||
|
|
||||
@@ -661,19 +661,31 @@ fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec<Paramet
|
||||
.is_some_and(|first_char| !first_char.is_whitespace())
|
||||
{
|
||||
if let Some(before_colon) = entry.split(':').next() {
|
||||
let param = before_colon.trim_end();
|
||||
let param_name = param.trim_start_matches('*');
|
||||
if is_identifier(param_name) {
|
||||
let param_start = line_start + indentation.text_len();
|
||||
let param_end = param_start + param.text_len();
|
||||
let param_line = before_colon.trim_end();
|
||||
|
||||
entries.push(ParameterEntry {
|
||||
name: param_name,
|
||||
range: TextRange::new(
|
||||
content_start + param_start,
|
||||
content_start + param_end,
|
||||
),
|
||||
});
|
||||
// Split on commas to handle comma-separated parameters
|
||||
let mut current_offset = TextSize::from(0);
|
||||
for param_part in param_line.split(',') {
|
||||
let param_part_trimmed = param_part.trim();
|
||||
let param_name = param_part_trimmed.trim_start_matches('*');
|
||||
if is_identifier(param_name) {
|
||||
// Calculate the position of this specific parameter part within the line
|
||||
// Account for leading whitespace that gets trimmed
|
||||
let param_start_in_line = current_offset
|
||||
+ (param_part.text_len() - param_part_trimmed.text_len());
|
||||
let param_start =
|
||||
line_start + indentation.text_len() + param_start_in_line;
|
||||
|
||||
entries.push(ParameterEntry {
|
||||
name: param_name,
|
||||
range: TextRange::at(
|
||||
content_start + param_start,
|
||||
param_part_trimmed.text_len(),
|
||||
),
|
||||
});
|
||||
}
|
||||
// Update offset for next iteration: add the part length plus comma length
|
||||
current_offset = current_offset + param_part.text_len() + ','.text_len();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,12 +722,30 @@ fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName
|
||||
/// ```
|
||||
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
|
||||
let mut entries: Vec<QualifiedName> = Vec::new();
|
||||
for potential in content.lines() {
|
||||
let Some(colon_idx) = potential.find(':') else {
|
||||
continue;
|
||||
};
|
||||
let entry = potential[..colon_idx].trim();
|
||||
entries.push(QualifiedName::user_defined(entry));
|
||||
let mut lines = content.lines().peekable();
|
||||
let Some(first) = lines.peek() else {
|
||||
return entries;
|
||||
};
|
||||
let indentation = &first[..first.len() - first.trim_start().len()];
|
||||
for potential in lines {
|
||||
if let Some(entry) = potential.strip_prefix(indentation) {
|
||||
if let Some(first_char) = entry.chars().next() {
|
||||
if !first_char.is_whitespace() {
|
||||
if let Some(colon_idx) = entry.find(':') {
|
||||
let entry = entry[..colon_idx].trim();
|
||||
if !entry.is_empty() {
|
||||
entries.push(QualifiedName::user_defined(entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we can't strip the expected indentation, check if this is a dedented line
|
||||
// (not blank) - if so, break early as we've reached the end of this section
|
||||
if !potential.trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
@@ -739,6 +769,12 @@ fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
|
||||
let indentation = &dashes[..dashes.len() - dashes.trim_start().len()];
|
||||
for potential in lines {
|
||||
if let Some(entry) = potential.strip_prefix(indentation) {
|
||||
// Check for Sphinx directives (lines starting with ..) - these indicate the end of the
|
||||
// section. In numpy-style, exceptions are dedented to the same level as sphinx
|
||||
// directives.
|
||||
if entry.starts_with("..") {
|
||||
break;
|
||||
}
|
||||
if let Some(first_char) = entry.chars().next() {
|
||||
if !first_char.is_whitespace() {
|
||||
entries.push(QualifiedName::user_defined(entry.trim_end()));
|
||||
|
||||
@@ -95,3 +95,23 @@ DOC502 Raised exception is not explicitly raised: `DivisionByZero`
|
||||
82 | return distance / time
|
||||
|
|
||||
help: Remove `DivisionByZero` from the docstring
|
||||
|
||||
DOC502 Raised exception is not explicitly raised: `ZeroDivisionError`
|
||||
--> DOC502_numpy.py:139:5
|
||||
|
|
||||
137 | # of the exceptions
|
||||
138 | def foo():
|
||||
139 | / """First line.
|
||||
140 | |
|
||||
141 | | Raises
|
||||
142 | | ------
|
||||
143 | | ValueError
|
||||
144 | | some text
|
||||
145 | | .. math:: e^{xception}
|
||||
146 | | ZeroDivisionError
|
||||
147 | | Will not be raised, DOC502
|
||||
148 | | """
|
||||
| |_______^
|
||||
149 | raise ValueError
|
||||
|
|
||||
help: Remove `ZeroDivisionError` from the docstring
|
||||
|
||||
@@ -187,3 +187,36 @@ DOC102 Documented parameter `a` is not in the function's signature
|
||||
302 | b
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `x1` is not in the function's signature
|
||||
--> DOC102_numpy.py:380:5
|
||||
|
|
||||
378 | Parameters
|
||||
379 | ----------
|
||||
380 | x1, x2 : object
|
||||
| ^^
|
||||
381 | Objects.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `x2` is not in the function's signature
|
||||
--> DOC102_numpy.py:380:9
|
||||
|
|
||||
378 | Parameters
|
||||
379 | ----------
|
||||
380 | x1, x2 : object
|
||||
| ^^
|
||||
381 | Objects.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `extra_param` is not in the function's signature
|
||||
--> DOC102_numpy.py:418:5
|
||||
|
|
||||
416 | x1, x2 : str
|
||||
417 | String parameters for processing.
|
||||
418 | extra_param : str
|
||||
| ^^^^^^^^^^^
|
||||
419 | Extra parameter not in signature.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
@@ -766,11 +766,12 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
|
||||
}
|
||||
|
||||
for operation in fixer.with_renames() {
|
||||
checker.report_diagnostic(
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
DeprecatedImport {
|
||||
deprecation: Deprecation::WithRename(operation),
|
||||
},
|
||||
import_from_stmt.range(),
|
||||
);
|
||||
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,15 @@ use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{
|
||||
self as ast, Expr, Stmt,
|
||||
relocate::relocate_expr,
|
||||
visitor::{self, Visitor},
|
||||
};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::rules::refurb::helpers::{FileOpen, find_file_opens};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use crate::{FixAvailability, Locator, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `open` and `write` that can be replaced by `pathlib`
|
||||
@@ -129,7 +127,7 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
|
||||
let open = self.candidates.remove(open);
|
||||
|
||||
if self.loop_counter == 0 {
|
||||
let suggestion = make_suggestion(&open, content, self.checker.generator());
|
||||
let suggestion = make_suggestion(&open, content, self.checker.locator());
|
||||
|
||||
let mut diagnostic = self.checker.report_diagnostic(
|
||||
WriteWholeFile {
|
||||
@@ -172,27 +170,21 @@ fn match_write_call(expr: &Expr) -> Option<(&Expr, &Expr)> {
|
||||
Some((&*attr.value, call.arguments.args.first()?))
|
||||
}
|
||||
|
||||
fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, generator: Generator) -> String {
|
||||
let name = ast::ExprName {
|
||||
id: open.mode.pathlib_method(),
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
};
|
||||
let mut arg = arg.clone();
|
||||
relocate_expr(&mut arg, TextRange::default());
|
||||
let call = ast::ExprCall {
|
||||
func: Box::new(name.into()),
|
||||
arguments: ast::Arguments {
|
||||
args: Box::new([arg]),
|
||||
keywords: open.keywords.iter().copied().cloned().collect(),
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
},
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
};
|
||||
generator.expr(&call.into())
|
||||
fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, locator: &Locator) -> String {
|
||||
let method_name = open.mode.pathlib_method();
|
||||
let arg_code = locator.slice(arg.range());
|
||||
|
||||
if open.keywords.is_empty() {
|
||||
format!("{method_name}({arg_code})")
|
||||
} else {
|
||||
format!(
|
||||
"{method_name}({arg_code}, {})",
|
||||
itertools::join(
|
||||
open.keywords.iter().map(|kw| locator.slice(kw.range())),
|
||||
", "
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_fix(
|
||||
|
||||
@@ -279,3 +279,34 @@ help: Replace with `Path("test.json")....`
|
||||
- with open("test.json", "wb") as f:
|
||||
- f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
155 + pathlib.Path("test.json").write_bytes(json.dumps(data, indent=4).encode("utf-8"))
|
||||
156 |
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
|
||||
--> FURB103.py:158:6
|
||||
|
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
159 | f.write(dedent(
|
||||
160 | """
|
||||
|
|
||||
help: Replace with `Path("tmp_path/pyproject.toml")....`
|
||||
148 |
|
||||
149 | # See: https://github.com/astral-sh/ruff/issues/20785
|
||||
150 | import json
|
||||
151 + import pathlib
|
||||
152 |
|
||||
153 | data = {"price": 100}
|
||||
154 |
|
||||
--------------------------------------------------------------------------------
|
||||
156 | f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
157 |
|
||||
158 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
- with open("tmp_path/pyproject.toml", "w") as f:
|
||||
- f.write(dedent(
|
||||
159 + pathlib.Path("tmp_path/pyproject.toml").write_text(dedent(
|
||||
160 | """
|
||||
161 | [project]
|
||||
162 | other = 1.234
|
||||
|
||||
@@ -209,3 +209,34 @@ help: Replace with `Path("test.json")....`
|
||||
- with open("test.json", "wb") as f:
|
||||
- f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
155 + pathlib.Path("test.json").write_bytes(json.dumps(data, indent=4).encode("utf-8"))
|
||||
156 |
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
|
||||
--> FURB103.py:158:6
|
||||
|
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
159 | f.write(dedent(
|
||||
160 | """
|
||||
|
|
||||
help: Replace with `Path("tmp_path/pyproject.toml")....`
|
||||
148 |
|
||||
149 | # See: https://github.com/astral-sh/ruff/issues/20785
|
||||
150 | import json
|
||||
151 + import pathlib
|
||||
152 |
|
||||
153 | data = {"price": 100}
|
||||
154 |
|
||||
--------------------------------------------------------------------------------
|
||||
156 | f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
157 |
|
||||
158 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
- with open("tmp_path/pyproject.toml", "w") as f:
|
||||
- f.write(dedent(
|
||||
159 + pathlib.Path("tmp_path/pyproject.toml").write_text(dedent(
|
||||
160 | """
|
||||
161 | [project]
|
||||
162 | other = 1.234
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_python_ast::{self as ast, Stmt};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Stmt};
|
||||
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -96,6 +97,9 @@ impl Violation for MutableClassDefault {
|
||||
|
||||
/// RUF012
|
||||
pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClassDef) {
|
||||
// Collect any `ClassVar`s we find in case they get reassigned later.
|
||||
let mut class_var_targets = FxHashSet::default();
|
||||
|
||||
for statement in &class_def.body {
|
||||
match statement {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
@@ -104,6 +108,12 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas
|
||||
value: Some(value),
|
||||
..
|
||||
}) => {
|
||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
if is_class_var_annotation(annotation, checker.semantic()) {
|
||||
class_var_targets.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_special_attribute(target)
|
||||
&& is_mutable_expr(value, checker.semantic())
|
||||
&& !is_class_var_annotation(annotation, checker.semantic())
|
||||
@@ -123,8 +133,12 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas
|
||||
}
|
||||
}
|
||||
Stmt::Assign(ast::StmtAssign { value, targets, .. }) => {
|
||||
if !targets.iter().all(is_special_attribute)
|
||||
&& is_mutable_expr(value, checker.semantic())
|
||||
if !targets.iter().all(|target| {
|
||||
is_special_attribute(target)
|
||||
|| target
|
||||
.as_name_expr()
|
||||
.is_some_and(|name| class_var_targets.contains(&name.id))
|
||||
}) && is_mutable_expr(value, checker.semantic())
|
||||
{
|
||||
// Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
|
||||
if has_default_copy_semantics(class_def, checker.semantic()) {
|
||||
|
||||
@@ -294,19 +294,33 @@ impl CellOffsets {
|
||||
}
|
||||
|
||||
/// Returns `true` if the given range contains a cell boundary.
|
||||
///
|
||||
/// A range starting at the cell boundary isn't considered to contain the cell boundary
|
||||
/// as it starts right after it. A range starting before a cell boundary
|
||||
/// and ending exactly at the boundary is considered to contain the cell boundary.
|
||||
///
|
||||
/// # Examples
|
||||
/// Cell 1:
|
||||
///
|
||||
/// ```py
|
||||
/// import c
|
||||
/// ```
|
||||
///
|
||||
/// Cell 2:
|
||||
///
|
||||
/// ```py
|
||||
/// import os
|
||||
/// ```
|
||||
///
|
||||
/// The range `import c`..`import os`, contains a cell boundary because it starts before cell 2 and ends in cell 2 (`end=cell2_boundary`).
|
||||
/// The `import os` contains no cell boundary because it starts at the start of cell 2 (at the cell boundary) but doesn't cross into another cell.
|
||||
pub fn has_cell_boundary(&self, range: TextRange) -> bool {
|
||||
self.binary_search_by(|offset| {
|
||||
if range.start() <= *offset {
|
||||
if range.end() < *offset {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
} else {
|
||||
std::cmp::Ordering::Less
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
let after_range_start = self.partition_point(|offset| *offset <= range.start());
|
||||
let Some(boundary) = self.get(after_range_start).copied() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
range.contains_inclusive(boundary)
|
||||
}
|
||||
|
||||
/// Returns an iterator over [`TextRange`]s covered by each cell.
|
||||
|
||||
@@ -39,7 +39,7 @@ impl NotebookIndex {
|
||||
|
||||
/// Returns an iterator over the starting rows of each cell (1-based).
|
||||
///
|
||||
/// This yields one entry per Python cell (skipping over Makrdown cell).
|
||||
/// This yields one entry per Python cell (skipping over Markdown cell).
|
||||
pub fn iter(&self) -> impl Iterator<Item = CellStart> + '_ {
|
||||
self.cell_starts.iter().copied()
|
||||
}
|
||||
@@ -47,7 +47,7 @@ impl NotebookIndex {
|
||||
/// Translates the given [`LineColumn`] based on the indexing table.
|
||||
///
|
||||
/// This will translate the row/column in the concatenated source code
|
||||
/// to the row/column in the Jupyter Notebook.
|
||||
/// to the row/column in the Jupyter Notebook cell.
|
||||
pub fn translate_line_column(&self, source_location: &LineColumn) -> LineColumn {
|
||||
LineColumn {
|
||||
line: self
|
||||
@@ -60,7 +60,7 @@ impl NotebookIndex {
|
||||
/// Translates the given [`SourceLocation`] based on the indexing table.
|
||||
///
|
||||
/// This will translate the line/character in the concatenated source code
|
||||
/// to the line/character in the Jupyter Notebook.
|
||||
/// to the line/character in the Jupyter Notebook cell.
|
||||
pub fn translate_source_location(&self, source_location: &SourceLocation) -> SourceLocation {
|
||||
SourceLocation {
|
||||
line: self
|
||||
|
||||
@@ -13,7 +13,7 @@ use thiserror::Error;
|
||||
|
||||
use ruff_diagnostics::{SourceMap, SourceMarker};
|
||||
use ruff_source_file::{NewlineWithTrailingNewline, OneIndexed, UniversalNewlineIterator};
|
||||
use ruff_text_size::TextSize;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::cell::CellOffsets;
|
||||
use crate::index::NotebookIndex;
|
||||
@@ -294,7 +294,7 @@ impl Notebook {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build and return the [`JupyterIndex`].
|
||||
/// Build and return the [`NotebookIndex`].
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
@@ -388,6 +388,21 @@ impl Notebook {
|
||||
&self.cell_offsets
|
||||
}
|
||||
|
||||
/// Returns the start offset of the cell at index `cell` in the concatenated
|
||||
/// text document.
|
||||
pub fn cell_offset(&self, cell: OneIndexed) -> Option<TextSize> {
|
||||
self.cell_offsets.get(cell.to_zero_indexed()).copied()
|
||||
}
|
||||
|
||||
/// Returns the text range in the concatenated document of the cell
|
||||
/// with index `cell`.
|
||||
pub fn cell_range(&self, cell: OneIndexed) -> Option<TextRange> {
|
||||
let start = self.cell_offsets.get(cell.to_zero_indexed()).copied()?;
|
||||
let end = self.cell_offsets.get(cell.to_zero_indexed() + 1).copied()?;
|
||||
|
||||
Some(TextRange::new(start, end))
|
||||
}
|
||||
|
||||
/// Return `true` if the notebook has a trailing newline, `false` otherwise.
|
||||
pub fn trailing_newline(&self) -> bool {
|
||||
self.trailing_newline
|
||||
|
||||
@@ -169,3 +169,53 @@ result = (
|
||||
# dangling before dot
|
||||
.b # trailing end-of-line
|
||||
)
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/19350
|
||||
variable = (
|
||||
(something) # a comment
|
||||
.first_method("some string")
|
||||
)
|
||||
|
||||
variable = (
|
||||
something # a commentdddddddddddddddddddddddddddddd
|
||||
.first_method("some string")
|
||||
)
|
||||
|
||||
if (
|
||||
(something) # a commentdddddddddddddddddddddddddddddd
|
||||
.first_method("some string")
|
||||
): pass
|
||||
|
||||
variable = (
|
||||
(something # a comment
|
||||
).first_method("some string")
|
||||
)
|
||||
|
||||
if (
|
||||
(something # a comment
|
||||
).first_method("some string") # second comment
|
||||
): pass
|
||||
|
||||
variable = ( # 1
|
||||
# 2
|
||||
(something) # 3
|
||||
# 4
|
||||
.first_method("some string") # 5
|
||||
# 6
|
||||
) # 7
|
||||
|
||||
|
||||
if (
|
||||
(something
|
||||
# trailing own line on value
|
||||
)
|
||||
.first_method("some string")
|
||||
): ...
|
||||
|
||||
variable = (
|
||||
(something
|
||||
# 1
|
||||
) # 2
|
||||
.first_method("some string")
|
||||
)
|
||||
|
||||
|
||||
@@ -294,3 +294,39 @@ if parent_body:
|
||||
# d
|
||||
# e
|
||||
#f
|
||||
|
||||
# Compare behavior with `while`/`else` comment placement
|
||||
|
||||
if True: pass
|
||||
# 1
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# 2
|
||||
else:
|
||||
pass
|
||||
|
||||
if True: pass
|
||||
# 3
|
||||
else:
|
||||
pass
|
||||
|
||||
if True: pass
|
||||
# 4
|
||||
else:
|
||||
pass
|
||||
|
||||
def foo():
|
||||
if True:
|
||||
pass
|
||||
# 5
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
first;second
|
||||
# 6
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -28,3 +28,37 @@ while (
|
||||
and anotherCondition or aThirdCondition # trailing third condition
|
||||
): # comment
|
||||
print("Do something")
|
||||
|
||||
while True: pass
|
||||
# 1
|
||||
else:
|
||||
pass
|
||||
|
||||
while True:
|
||||
pass
|
||||
# 2
|
||||
else:
|
||||
pass
|
||||
|
||||
while True: pass
|
||||
# 3
|
||||
else:
|
||||
pass
|
||||
|
||||
while True: pass
|
||||
# 4
|
||||
else:
|
||||
pass
|
||||
|
||||
def foo():
|
||||
while True:
|
||||
pass
|
||||
# 5
|
||||
else:
|
||||
pass
|
||||
|
||||
while True:
|
||||
first;second
|
||||
# 6
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -1042,4 +1042,33 @@ else: # trailing comment
|
||||
|
||||
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn while_else_indented_comment_between_branches() {
|
||||
let source = r"while True: pass
|
||||
# comment
|
||||
else:
|
||||
pass
|
||||
";
|
||||
let test_case = CommentsTestCase::from_code(source);
|
||||
|
||||
let comments = test_case.to_comments();
|
||||
|
||||
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn while_else_very_indented_comment_between_branches() {
|
||||
let source = r"while True:
|
||||
pass
|
||||
# comment
|
||||
else:
|
||||
pass
|
||||
";
|
||||
let test_case = CommentsTestCase::from_code(source);
|
||||
|
||||
let comments = test_case.to_comments();
|
||||
|
||||
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use ruff_python_trivia::{
|
||||
find_only_token_in_range, first_non_trivia_token, indentation_at_offset,
|
||||
};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::comments::visitor::{CommentPlacement, DecoratedComment};
|
||||
@@ -602,9 +602,35 @@ fn handle_own_line_comment_between_branches<'a>(
|
||||
// following branch or if it a trailing comment of the previous body's last statement.
|
||||
let comment_indentation = comment_indentation_after(preceding, comment.range(), source);
|
||||
|
||||
let preceding_indentation = indentation(source, &preceding)
|
||||
.unwrap_or_default()
|
||||
.text_len();
|
||||
let preceding_indentation = indentation(source, &preceding).map_or_else(
|
||||
// If `indentation` returns `None`, then there is leading
|
||||
// content before the preceding node. In this case, we
|
||||
// always treat the comment as being less-indented than the
|
||||
// preceding. For example:
|
||||
//
|
||||
// ```python
|
||||
// if True: pass
|
||||
// # leading on `else`
|
||||
// else:
|
||||
// pass
|
||||
// ```
|
||||
// Note we even do this if the comment is very indented
|
||||
// (which matches `black`'s behavior as of 2025.11.11)
|
||||
//
|
||||
// ```python
|
||||
// if True: pass
|
||||
// # leading on `else`
|
||||
// else:
|
||||
// pass
|
||||
// ```
|
||||
|| {
|
||||
comment_indentation
|
||||
// This can be any positive number - we just
|
||||
// want to hit the `Less` branch below
|
||||
+ TextSize::new(1)
|
||||
},
|
||||
ruff_text_size::TextLen::text_len,
|
||||
);
|
||||
|
||||
// Compare to the last statement in the body
|
||||
match comment_indentation.cmp(&preceding_indentation) {
|
||||
@@ -678,8 +704,41 @@ fn handle_own_line_comment_after_branch<'a>(
|
||||
preceding: AnyNodeRef<'a>,
|
||||
source: &str,
|
||||
) -> CommentPlacement<'a> {
|
||||
let Some(last_child) = preceding.last_child_in_body() else {
|
||||
return CommentPlacement::Default(comment);
|
||||
// If the preceding node has a body, we want the last child - e.g.
|
||||
//
|
||||
// ```python
|
||||
// if True:
|
||||
// def foo():
|
||||
// something
|
||||
// last_child
|
||||
// # comment
|
||||
// else:
|
||||
// pass
|
||||
// ```
|
||||
//
|
||||
// Otherwise, the preceding node may be the last statement in the body
|
||||
// of the preceding branch, in which case we can take it as our
|
||||
// `last_child` here - e.g.
|
||||
//
|
||||
// ```python
|
||||
// if True:
|
||||
// something
|
||||
// last_child
|
||||
// # comment
|
||||
// else:
|
||||
// pass
|
||||
// ```
|
||||
let last_child = match preceding.last_child_in_body() {
|
||||
Some(last) => last,
|
||||
None if comment.following_node().is_some_and(|following| {
|
||||
following.is_first_statement_in_alternate_body(comment.enclosing_node())
|
||||
}) =>
|
||||
{
|
||||
preceding
|
||||
}
|
||||
_ => {
|
||||
return CommentPlacement::Default(comment);
|
||||
}
|
||||
};
|
||||
|
||||
// We only care about the length because indentations with mixed spaces and tabs are only valid if
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||
expression: comments.debug(test_case.source_code)
|
||||
---
|
||||
{
|
||||
Node {
|
||||
kind: StmtWhile,
|
||||
range: 0..45,
|
||||
source: `while True: pass⏎`,
|
||||
}: {
|
||||
"leading": [],
|
||||
"dangling": [
|
||||
SourceComment {
|
||||
text: "# comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
"trailing": [],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||
expression: comments.debug(test_case.source_code)
|
||||
---
|
||||
{
|
||||
Node {
|
||||
kind: StmtPass,
|
||||
range: 16..20,
|
||||
source: `pass`,
|
||||
}: {
|
||||
"leading": [],
|
||||
"dangling": [],
|
||||
"trailing": [
|
||||
SourceComment {
|
||||
text: "# comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -179,7 +179,22 @@ impl NeedsParentheses for ExprAttribute {
|
||||
context.comments().ranges(),
|
||||
context.source(),
|
||||
) {
|
||||
OptionalParentheses::Never
|
||||
// We have to avoid creating syntax errors like
|
||||
// ```python
|
||||
// variable = (something) # trailing
|
||||
// .my_attribute
|
||||
// ```
|
||||
// See https://github.com/astral-sh/ruff/issues/19350
|
||||
if context
|
||||
.comments()
|
||||
.trailing(self.value.as_ref())
|
||||
.iter()
|
||||
.any(|comment| comment.line_position().is_end_of_line())
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else {
|
||||
OptionalParentheses::Never
|
||||
}
|
||||
} else {
|
||||
self.value.needs_parentheses(self.into(), context)
|
||||
}
|
||||
|
||||
@@ -175,6 +175,56 @@ result = (
|
||||
# dangling before dot
|
||||
.b # trailing end-of-line
|
||||
)
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/19350
|
||||
variable = (
|
||||
(something) # a comment
|
||||
.first_method("some string")
|
||||
)
|
||||
|
||||
variable = (
|
||||
something # a commentdddddddddddddddddddddddddddddd
|
||||
.first_method("some string")
|
||||
)
|
||||
|
||||
if (
|
||||
(something) # a commentdddddddddddddddddddddddddddddd
|
||||
.first_method("some string")
|
||||
): pass
|
||||
|
||||
variable = (
|
||||
(something # a comment
|
||||
).first_method("some string")
|
||||
)
|
||||
|
||||
if (
|
||||
(something # a comment
|
||||
).first_method("some string") # second comment
|
||||
): pass
|
||||
|
||||
variable = ( # 1
|
||||
# 2
|
||||
(something) # 3
|
||||
# 4
|
||||
.first_method("some string") # 5
|
||||
# 6
|
||||
) # 7
|
||||
|
||||
|
||||
if (
|
||||
(something
|
||||
# trailing own line on value
|
||||
)
|
||||
.first_method("some string")
|
||||
): ...
|
||||
|
||||
variable = (
|
||||
(something
|
||||
# 1
|
||||
) # 2
|
||||
.first_method("some string")
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -328,4 +378,54 @@ result = (
|
||||
# dangling before dot
|
||||
.b # trailing end-of-line
|
||||
)
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/19350
|
||||
variable = (
|
||||
(something) # a comment
|
||||
.first_method("some string")
|
||||
)
|
||||
|
||||
variable = something.first_method( # a commentdddddddddddddddddddddddddddddd
|
||||
"some string"
|
||||
)
|
||||
|
||||
if (
|
||||
(something) # a commentdddddddddddddddddddddddddddddd
|
||||
.first_method("some string")
|
||||
):
|
||||
pass
|
||||
|
||||
variable = (
|
||||
something # a comment
|
||||
).first_method("some string")
|
||||
|
||||
if (
|
||||
(
|
||||
something # a comment
|
||||
).first_method("some string") # second comment
|
||||
):
|
||||
pass
|
||||
|
||||
variable = ( # 1
|
||||
# 2
|
||||
(something) # 3
|
||||
# 4
|
||||
.first_method("some string") # 5
|
||||
# 6
|
||||
) # 7
|
||||
|
||||
|
||||
if (
|
||||
something
|
||||
# trailing own line on value
|
||||
).first_method("some string"):
|
||||
...
|
||||
|
||||
variable = (
|
||||
(
|
||||
something
|
||||
# 1
|
||||
) # 2
|
||||
.first_method("some string")
|
||||
)
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/form_feed.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -18,7 +17,7 @@ else:
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7624
|
||||
if symbol is not None:
|
||||
request["market"] = market["id"]
|
||||
# "remaining_volume": "0.0",
|
||||
# "remaining_volume": "0.0",
|
||||
else:
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -301,6 +300,42 @@ if parent_body:
|
||||
# d
|
||||
# e
|
||||
#f
|
||||
|
||||
# Compare behavior with `while`/`else` comment placement
|
||||
|
||||
if True: pass
|
||||
# 1
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# 2
|
||||
else:
|
||||
pass
|
||||
|
||||
if True: pass
|
||||
# 3
|
||||
else:
|
||||
pass
|
||||
|
||||
if True: pass
|
||||
# 4
|
||||
else:
|
||||
pass
|
||||
|
||||
def foo():
|
||||
if True:
|
||||
pass
|
||||
# 5
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
first;second
|
||||
# 6
|
||||
else:
|
||||
pass
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -607,6 +642,48 @@ if parent_body:
|
||||
# d
|
||||
# e
|
||||
# f
|
||||
|
||||
# Compare behavior with `while`/`else` comment placement
|
||||
|
||||
if True:
|
||||
pass
|
||||
# 1
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# 2
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# 3
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# 4
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def foo():
|
||||
if True:
|
||||
pass
|
||||
# 5
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
first
|
||||
second
|
||||
# 6
|
||||
else:
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -35,6 +34,40 @@ while (
|
||||
and anotherCondition or aThirdCondition # trailing third condition
|
||||
): # comment
|
||||
print("Do something")
|
||||
|
||||
while True: pass
|
||||
# 1
|
||||
else:
|
||||
pass
|
||||
|
||||
while True:
|
||||
pass
|
||||
# 2
|
||||
else:
|
||||
pass
|
||||
|
||||
while True: pass
|
||||
# 3
|
||||
else:
|
||||
pass
|
||||
|
||||
while True: pass
|
||||
# 4
|
||||
else:
|
||||
pass
|
||||
|
||||
def foo():
|
||||
while True:
|
||||
pass
|
||||
# 5
|
||||
else:
|
||||
pass
|
||||
|
||||
while True:
|
||||
first;second
|
||||
# 6
|
||||
else:
|
||||
pass
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -70,4 +103,44 @@ while (
|
||||
or aThirdCondition # trailing third condition
|
||||
): # comment
|
||||
print("Do something")
|
||||
|
||||
while True:
|
||||
pass
|
||||
# 1
|
||||
else:
|
||||
pass
|
||||
|
||||
while True:
|
||||
pass
|
||||
# 2
|
||||
else:
|
||||
pass
|
||||
|
||||
while True:
|
||||
pass
|
||||
# 3
|
||||
else:
|
||||
pass
|
||||
|
||||
while True:
|
||||
pass
|
||||
# 4
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def foo():
|
||||
while True:
|
||||
pass
|
||||
# 5
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
while True:
|
||||
first
|
||||
second
|
||||
# 6
|
||||
else:
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruff_python_parser::{TokenKind, Tokens};
|
||||
use ruff_python_trivia::is_python_whitespace;
|
||||
use ruff_python_trivia::{PythonWhitespace, textwrap::indent};
|
||||
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum Placement<'a> {
|
||||
@@ -37,7 +37,7 @@ pub struct Insertion<'a> {
|
||||
|
||||
impl<'a> Insertion<'a> {
|
||||
/// Create an [`Insertion`] to insert (e.g.) an import statement at the start of a given
|
||||
/// file, along with a prefix and suffix to use for the insertion.
|
||||
/// file or cell, along with a prefix and suffix to use for the insertion.
|
||||
///
|
||||
/// For example, given the following code:
|
||||
///
|
||||
@@ -49,7 +49,26 @@ impl<'a> Insertion<'a> {
|
||||
///
|
||||
/// The insertion returned will begin at the start of the `import os` statement, and will
|
||||
/// include a trailing newline.
|
||||
pub fn start_of_file(body: &[Stmt], contents: &str, stylist: &Stylist) -> Insertion<'static> {
|
||||
///
|
||||
/// If `within_range` is set, the insertion will be limited to the specified range. That is,
|
||||
/// the insertion is constrained to the given range rather than the start of the file.
|
||||
/// This is used for insertions in notebook cells where the source code and AST are for
|
||||
/// the entire notebook but the insertion should be constrained to a specific cell.
|
||||
pub fn start_of_file(
|
||||
body: &[Stmt],
|
||||
contents: &str,
|
||||
stylist: &Stylist,
|
||||
within_range: Option<TextRange>,
|
||||
) -> Insertion<'static> {
|
||||
let body = within_range
|
||||
.map(|range| {
|
||||
let start = body.partition_point(|stmt| stmt.start() < range.start());
|
||||
let end = body.partition_point(|stmt| stmt.end() <= range.end());
|
||||
|
||||
&body[start..end]
|
||||
})
|
||||
.unwrap_or(body);
|
||||
|
||||
// Skip over any docstrings.
|
||||
let mut location = if let Some(mut location) = match_docstring_end(body) {
|
||||
// If the first token after the docstring is a semicolon, insert after the semicolon as
|
||||
@@ -66,6 +85,10 @@ impl<'a> Insertion<'a> {
|
||||
|
||||
// Otherwise, advance to the next row.
|
||||
contents.full_line_end(location)
|
||||
} else if let Some(range) = within_range
|
||||
&& range.start() != TextSize::ZERO
|
||||
{
|
||||
range.start()
|
||||
} else {
|
||||
contents.bom_start_offset()
|
||||
};
|
||||
@@ -374,7 +397,12 @@ mod tests {
|
||||
fn insert(contents: &str) -> Result<Insertion<'_>> {
|
||||
let parsed = parse_module(contents)?;
|
||||
let stylist = Stylist::from_tokens(parsed.tokens(), contents);
|
||||
Ok(Insertion::start_of_file(parsed.suite(), contents, &stylist))
|
||||
Ok(Insertion::start_of_file(
|
||||
parsed.suite(),
|
||||
contents,
|
||||
&stylist,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
let contents = "";
|
||||
|
||||
@@ -2107,8 +2107,10 @@ pub trait SemanticSyntaxContext {
|
||||
|
||||
fn report_semantic_error(&self, error: SemanticSyntaxError);
|
||||
|
||||
/// Returns `true` if the visitor is inside a `for` or `while` loop.
|
||||
fn in_loop_context(&self) -> bool;
|
||||
|
||||
/// Returns `true` if `name` is a bound parameter in the current function or lambda scope.
|
||||
fn is_bound_parameter(&self, name: &str) -> bool;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -232,6 +232,9 @@ impl Configuration {
|
||||
include_dependencies: analyze
|
||||
.include_dependencies
|
||||
.unwrap_or(analyze_defaults.include_dependencies),
|
||||
type_checking_imports: analyze
|
||||
.type_checking_imports
|
||||
.unwrap_or(analyze_defaults.type_checking_imports),
|
||||
};
|
||||
|
||||
let lint = self.lint;
|
||||
@@ -1277,6 +1280,7 @@ pub struct AnalyzeConfiguration {
|
||||
pub detect_string_imports: Option<bool>,
|
||||
pub string_imports_min_dots: Option<usize>,
|
||||
pub include_dependencies: Option<BTreeMap<PathBuf, (PathBuf, Vec<String>)>>,
|
||||
pub type_checking_imports: Option<bool>,
|
||||
}
|
||||
|
||||
impl AnalyzeConfiguration {
|
||||
@@ -1303,6 +1307,7 @@ impl AnalyzeConfiguration {
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
}),
|
||||
type_checking_imports: options.type_checking_imports,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1317,6 +1322,7 @@ impl AnalyzeConfiguration {
|
||||
.string_imports_min_dots
|
||||
.or(config.string_imports_min_dots),
|
||||
include_dependencies: self.include_dependencies.or(config.include_dependencies),
|
||||
type_checking_imports: self.type_checking_imports.or(config.type_checking_imports),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3892,6 +3892,18 @@ pub struct AnalyzeOptions {
|
||||
"#
|
||||
)]
|
||||
pub include_dependencies: Option<BTreeMap<PathBuf, Vec<String>>>,
|
||||
/// Whether to include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks).
|
||||
/// When enabled (default), type-checking-only imports are included in the import graph.
|
||||
/// When disabled, they are excluded.
|
||||
#[option(
|
||||
default = "true",
|
||||
value_type = "bool",
|
||||
example = r#"
|
||||
# Exclude type-checking-only imports from the graph
|
||||
type-checking-imports = false
|
||||
"#
|
||||
)]
|
||||
pub type_checking_imports: Option<bool>,
|
||||
}
|
||||
|
||||
/// Like [`LintCommonOptions`], but with any `#[serde(flatten)]` fields inlined. This leads to far,
|
||||
|
||||
@@ -153,7 +153,7 @@ fn both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -171,14 +171,14 @@ fn both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -193,7 +193,7 @@ fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()>
|
||||
"###,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -211,14 +211,14 @@ fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()>
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -233,7 +233,7 @@ fn exit_zero_is_true() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -251,14 +251,14 @@ fn exit_zero_is_true() -> anyhow::Result<()> {
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -617,7 +617,7 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
|
||||
let _s = settings.bind_to_scope();
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=gitlab").arg("--warn").arg("unresolved-reference")
|
||||
.env("CI_PROJECT_DIR", case.project_dir), @r###"
|
||||
.env("CI_PROJECT_DIR", case.project_dir), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -655,14 +655,14 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
|
||||
},
|
||||
"end": {
|
||||
"line": 3,
|
||||
"column": 8
|
||||
"column": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
----- stderr -----
|
||||
"###);
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -677,15 +677,15 @@ fn github_diagnostics() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=github").arg("--warn").arg("unresolved-reference"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=github").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
::warning title=ty (unresolved-reference),file=<temp_dir>/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined
|
||||
::error title=ty (non-subscriptable),file=<temp_dir>/test.py,line=3,col=7,endLine=3,endColumn=8::test.py:3:7: non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
::error title=ty (non-subscriptable),file=<temp_dir>/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -323,6 +323,231 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This attempts to simulate the tangled web of symlinks that a homebrew install has
|
||||
/// which can easily confuse us if we're ever told to use it.
|
||||
///
|
||||
/// The main thing this is regression-testing is a panic in one *extremely* specific case
|
||||
/// that you have to try really hard to hit (but vscode, hilariously, did hit).
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn python_argument_trapped_in_a_symlink_factory() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
// This is the real python binary.
|
||||
(
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13",
|
||||
"",
|
||||
),
|
||||
// There's a real site-packages here (although it's basically empty).
|
||||
(
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages/foo.py",
|
||||
"",
|
||||
),
|
||||
// There's also a real site-packages here (although it's basically empty).
|
||||
("opt/homebrew/lib/python3.13/site-packages/bar.py", ""),
|
||||
// This has the real stdlib, but the site-packages in this dir is a symlink.
|
||||
(
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/abc.py",
|
||||
"",
|
||||
),
|
||||
// It's important that this our faux-homebrew not be in the same dir as our working directory
|
||||
// to reproduce the crash, don't ask me why.
|
||||
(
|
||||
"project/test.py",
|
||||
"\
|
||||
import foo
|
||||
import bar
|
||||
import colorama
|
||||
",
|
||||
),
|
||||
])?;
|
||||
|
||||
// many python symlinks pointing to a single real python (the longest path)
|
||||
case.write_symlink(
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13",
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3",
|
||||
)?;
|
||||
case.write_symlink(
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3",
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3",
|
||||
)?;
|
||||
case.write_symlink(
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3",
|
||||
"opt/homebrew/bin/python3",
|
||||
)?;
|
||||
// the "real" python's site-packages is a symlink to a different dir
|
||||
case.write_symlink(
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages",
|
||||
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages",
|
||||
)?;
|
||||
|
||||
// Try all 4 pythons with absolute paths to our fauxbrew install
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.arg("--python").arg(case.root().join("opt/homebrew/bin/python3")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `foo`
|
||||
--> test.py:1:8
|
||||
|
|
||||
1 | import foo
|
||||
| ^^^
|
||||
2 | import bar
|
||||
3 | import colorama
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `colorama`
|
||||
--> test.py:3:8
|
||||
|
|
||||
1 | import foo
|
||||
2 | import bar
|
||||
3 | import colorama
|
||||
| ^^^^^^^^
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `bar`
|
||||
--> test.py:2:8
|
||||
|
|
||||
1 | import foo
|
||||
2 | import bar
|
||||
| ^^^
|
||||
3 | import colorama
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `colorama`
|
||||
--> test.py:3:8
|
||||
|
|
||||
1 | import foo
|
||||
2 | import bar
|
||||
3 | import colorama
|
||||
| ^^^^^^^^
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `bar`
|
||||
--> test.py:2:8
|
||||
|
|
||||
1 | import foo
|
||||
2 | import bar
|
||||
| ^^^
|
||||
3 | import colorama
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `colorama`
|
||||
--> test.py:3:8
|
||||
|
|
||||
1 | import foo
|
||||
2 | import bar
|
||||
3 | import colorama
|
||||
| ^^^^^^^^
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `bar`
|
||||
--> test.py:2:8
|
||||
|
|
||||
1 | import foo
|
||||
2 | import bar
|
||||
| ^^^
|
||||
3 | import colorama
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `colorama`
|
||||
--> test.py:3:8
|
||||
|
|
||||
1 | import foo
|
||||
2 | import bar
|
||||
3 | import colorama
|
||||
| ^^^^^^^^
|
||||
|
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// On Unix systems, it's common for a Python installation at `.venv/bin/python` to only be a symlink
|
||||
/// to a system Python installation. We must be careful not to resolve the symlink too soon!
|
||||
/// If we do, we will incorrectly add the system installation's `site-packages` as a search path,
|
||||
|
||||
@@ -10,12 +10,14 @@ import-deprioritizes-type_check_only,main.py,1,1
|
||||
import-deprioritizes-type_check_only,main.py,2,1
|
||||
import-deprioritizes-type_check_only,main.py,3,2
|
||||
import-deprioritizes-type_check_only,main.py,4,3
|
||||
internal-typeshed-hidden,main.py,0,5
|
||||
none-completion,main.py,0,11
|
||||
import-keyword-completion,main.py,0,1
|
||||
internal-typeshed-hidden,main.py,0,4
|
||||
none-completion,main.py,0,2
|
||||
numpy-array,main.py,0,
|
||||
numpy-array,main.py,1,1
|
||||
object-attr-instance-methods,main.py,0,1
|
||||
object-attr-instance-methods,main.py,1,1
|
||||
pass-keyword-completion,main.py,0,1
|
||||
raise-uses-base-exception,main.py,0,2
|
||||
scope-existing-over-new-import,main.py,0,1
|
||||
scope-prioritize-closer,main.py,0,2
|
||||
@@ -23,4 +25,4 @@ scope-simple-long-identifier,main.py,0,1
|
||||
tstring-completions,main.py,0,1
|
||||
ty-extensions-lower-stdlib,main.py,0,8
|
||||
type-var-typing-over-ast,main.py,0,3
|
||||
type-var-typing-over-ast,main.py,1,277
|
||||
type-var-typing-over-ast,main.py,1,279
|
||||
|
||||
|
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = false
|
||||
@@ -0,0 +1 @@
|
||||
from collections im<CURSOR: import>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/import-keyword-completion/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/import-keyword-completion/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = false
|
||||
@@ -0,0 +1,3 @@
|
||||
match x:
|
||||
case int():
|
||||
pa<CURSOR: pass>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/pass-keyword-completion/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/pass-keyword-completion/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -855,7 +855,7 @@ fn convert_resolved_definitions_to_targets(
|
||||
}
|
||||
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
|
||||
// For file ranges, navigate to the specific range within the file
|
||||
crate::NavigationTarget::new(file_range.file(), file_range.range())
|
||||
crate::NavigationTarget::from(file_range)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -1592,6 +1592,111 @@ a = Test()
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_annotation() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
a: float<CURSOR> = 3.14
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: float = 3.14
|
||||
| ^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:661:7
|
||||
|
|
||||
660 | @disjoint_base
|
||||
661 | class float:
|
||||
| ^^^^^
|
||||
662 | """Convert a string or number to a floating-point number, if possible."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: float = 3.14
|
||||
| ^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_annotation() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
a: complex<CURSOR> = 3.14
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: complex = 3.14
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:661:7
|
||||
|
|
||||
660 | @disjoint_base
|
||||
661 | class float:
|
||||
| ^^^^^
|
||||
662 | """Convert a string or number to a floating-point number, if possible."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: complex = 3.14
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> stdlib/builtins.pyi:822:7
|
||||
|
|
||||
821 | @disjoint_base
|
||||
822 | class complex:
|
||||
| ^^^^^^^
|
||||
823 | """Create a complex number from a string or numbers.
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: complex = 3.14
|
||||
| ^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/astral-sh/ty/issues/1451>.
|
||||
/// We must ensure we respect re-import convention for stub files for
|
||||
/// imports in builtins.pyi.
|
||||
|
||||
@@ -199,13 +199,13 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:913:7
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
912 | @disjoint_base
|
||||
913 | class str(Sequence[str]):
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
914 | """str(object='') -> str
|
||||
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:1
|
||||
@@ -227,13 +227,13 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:913:7
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
912 | @disjoint_base
|
||||
913 | class str(Sequence[str]):
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
914 | """str(object='') -> str
|
||||
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:10
|
||||
@@ -334,13 +334,13 @@ mod tests {
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:913:7
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
912 | @disjoint_base
|
||||
913 | class str(Sequence[str]):
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
914 | """str(object='') -> str
|
||||
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:6
|
||||
@@ -368,13 +368,13 @@ mod tests {
|
||||
// is an int. Navigating to `str` would match pyright's behavior.
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:346:7
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
345 | @disjoint_base
|
||||
346 | class int:
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
347 | """int([x]) -> integer
|
||||
348 | int(x, base=10) -> integer
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:6
|
||||
@@ -401,13 +401,13 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:2918:7
|
||||
--> stdlib/builtins.pyi:2920:7
|
||||
|
|
||||
2917 | @disjoint_base
|
||||
2918 | class dict(MutableMapping[_KT, _VT]):
|
||||
2919 | @disjoint_base
|
||||
2920 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
2919 | """dict() -> new empty dictionary
|
||||
2920 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
2921 | """dict() -> new empty dictionary
|
||||
2922 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:5
|
||||
@@ -431,13 +431,13 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:913:7
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
912 | @disjoint_base
|
||||
913 | class str(Sequence[str]):
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
914 | """str(object='') -> str
|
||||
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
@@ -523,13 +523,13 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:913:7
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
912 | @disjoint_base
|
||||
913 | class str(Sequence[str]):
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
914 | """str(object='') -> str
|
||||
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:15
|
||||
@@ -570,13 +570,13 @@ f(**kwargs<CURSOR>)
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:913:7
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
912 | @disjoint_base
|
||||
913 | class str(Sequence[str]):
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
914 | """str(object='') -> str
|
||||
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
||||
@@ -2634,6 +2634,40 @@ def ab(a: int, *, c: int):
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_float_annotation() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: float<CURSOR> = 3.14
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
int | float
|
||||
---------------------------------------------
|
||||
Convert a string or number to a floating-point number, if possible.
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
int | float
|
||||
```
|
||||
---
|
||||
```text
|
||||
Convert a string or number to a floating-point number, if possible.
|
||||
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: float = 3.14
|
||||
| ^^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn hover(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
@@ -20,6 +20,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
@@ -76,6 +77,7 @@ impl<'a> Importer<'a> {
|
||||
parsed: &'a ParsedModuleRef,
|
||||
) -> Self {
|
||||
let imports = TopLevelImports::find(parsed);
|
||||
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
@@ -145,10 +147,14 @@ impl<'a> Importer<'a> {
|
||||
let request = request.avoid_conflicts(self.db, self.file, members);
|
||||
let mut symbol_text: Box<str> = request.member.into();
|
||||
let Some(response) = self.find(&request, members.at) else {
|
||||
let insertion = if let Some(future) = self.find_last_future_import() {
|
||||
let insertion = if let Some(future) = self.find_last_future_import(members.at) {
|
||||
Insertion::end_of_statement(future.stmt, self.source, self.stylist)
|
||||
} else {
|
||||
Insertion::start_of_file(self.parsed.suite(), self.source, self.stylist)
|
||||
let range = source_text(self.db, self.file)
|
||||
.as_notebook()
|
||||
.and_then(|notebook| notebook.cell_offsets().containing_range(members.at));
|
||||
|
||||
Insertion::start_of_file(self.parsed.suite(), self.source, self.stylist, range)
|
||||
};
|
||||
let import = insertion.into_edit(&request.to_string());
|
||||
if matches!(request.style, ImportStyle::Import) {
|
||||
@@ -209,6 +215,9 @@ impl<'a> Importer<'a> {
|
||||
available_at: TextSize,
|
||||
) -> Option<ImportResponse<'importer, 'a>> {
|
||||
let mut choice = None;
|
||||
let source = source_text(self.db, self.file);
|
||||
let notebook = source.as_notebook();
|
||||
|
||||
for import in &self.imports {
|
||||
// If the import statement comes after the spot where we
|
||||
// need the symbol, then we conservatively assume that
|
||||
@@ -226,7 +235,22 @@ impl<'a> Importer<'a> {
|
||||
if import.stmt.start() >= available_at {
|
||||
return choice;
|
||||
}
|
||||
|
||||
if let Some(response) = import.satisfies(self.db, self.file, request) {
|
||||
let partial = matches!(response.kind, ImportResponseKind::Partial { .. });
|
||||
|
||||
// The LSP doesn't support edits across cell boundaries.
|
||||
// Skip over imports that only partially satisfy the import
|
||||
// because they would require changes to the import (across cell boundaries).
|
||||
if partial
|
||||
&& let Some(notebook) = notebook
|
||||
&& notebook
|
||||
.cell_offsets()
|
||||
.has_cell_boundary(TextRange::new(import.stmt.start(), available_at))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if choice
|
||||
.as_ref()
|
||||
.is_none_or(|c| !c.kind.is_prioritized_over(&response.kind))
|
||||
@@ -247,9 +271,21 @@ impl<'a> Importer<'a> {
|
||||
}
|
||||
|
||||
/// Find the last `from __future__` import statement in the AST.
|
||||
fn find_last_future_import(&self) -> Option<&'a AstImport> {
|
||||
fn find_last_future_import(&self, at: TextSize) -> Option<&'a AstImport> {
|
||||
let source = source_text(self.db, self.file);
|
||||
let notebook = source.as_notebook();
|
||||
|
||||
self.imports
|
||||
.iter()
|
||||
.take_while(|import| import.stmt.start() <= at)
|
||||
// Skip over imports from other cells.
|
||||
.skip_while(|import| {
|
||||
notebook.is_some_and(|notebook| {
|
||||
notebook
|
||||
.cell_offsets()
|
||||
.has_cell_boundary(TextRange::new(import.stmt.start(), at))
|
||||
})
|
||||
})
|
||||
.take_while(|import| {
|
||||
import
|
||||
.stmt
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -132,6 +132,20 @@ impl NavigationTarget {
|
||||
pub fn full_range(&self) -> TextRange {
|
||||
self.full_range
|
||||
}
|
||||
|
||||
pub fn full_file_range(&self) -> FileRange {
|
||||
FileRange::new(self.file, self.full_range)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileRange> for NavigationTarget {
|
||||
fn from(value: FileRange) -> Self {
|
||||
Self {
|
||||
file: value.file(),
|
||||
focus_range: value.range(),
|
||||
full_range: value.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies the kind of reference operation.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Final, Literal, Never, assert_never
|
||||
from typing import Final, Literal, assert_never
|
||||
|
||||
from rich.console import Console
|
||||
from watchfiles import Change, watch
|
||||
@@ -27,15 +30,21 @@ DIRS_TO_WATCH: Final = (
|
||||
CRATE_ROOT.parent / "ty_test/src",
|
||||
)
|
||||
MDTEST_DIR: Final = CRATE_ROOT / "resources" / "mdtest"
|
||||
MDTEST_README: Final = CRATE_ROOT / "resources" / "README.md"
|
||||
|
||||
|
||||
class MDTestRunner:
|
||||
mdtest_executable: Path | None
|
||||
console: Console
|
||||
filters: list[str]
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, filters: list[str] | None = None) -> None:
|
||||
self.mdtest_executable = None
|
||||
self.console = Console()
|
||||
self.filters = [
|
||||
f.removesuffix(".md").replace("/", "_").replace("-", "_")
|
||||
for f in (filters or [])
|
||||
]
|
||||
|
||||
def _run_cargo_test(self, *, message_format: Literal["human", "json"]) -> str:
|
||||
return subprocess.check_output(
|
||||
@@ -117,13 +126,16 @@ class MDTestRunner:
|
||||
check=False,
|
||||
)
|
||||
|
||||
def _run_mdtests_for_file(self, markdown_file: Path) -> None:
|
||||
path_mangled = (
|
||||
def _mangle_path(self, markdown_file: Path) -> str:
|
||||
return (
|
||||
markdown_file.as_posix()
|
||||
.replace("/", "_")
|
||||
.replace("-", "_")
|
||||
.removesuffix(".md")
|
||||
)
|
||||
|
||||
def _run_mdtests_for_file(self, markdown_file: Path) -> None:
|
||||
path_mangled = self._mangle_path(markdown_file)
|
||||
test_name = f"mdtest__{path_mangled}"
|
||||
|
||||
output = self._run_mdtest(["--exact", test_name], capture_output=True)
|
||||
@@ -165,9 +177,19 @@ class MDTestRunner:
|
||||
|
||||
print(line)
|
||||
|
||||
def watch(self) -> Never:
|
||||
def watch(self):
|
||||
def keyboard_input() -> None:
|
||||
for _ in sys.stdin:
|
||||
# This is silly, but there is no other way to inject events into
|
||||
# the main `watch` loop. We use changes to the `README.md` file
|
||||
# as a trigger to re-run all mdtests:
|
||||
MDTEST_README.touch()
|
||||
|
||||
input_thread = threading.Thread(target=keyboard_input, daemon=True)
|
||||
input_thread.start()
|
||||
|
||||
self._recompile_tests("Compiling tests...", message_on_success=False)
|
||||
self._run_mdtest()
|
||||
self._run_mdtest(self.filters)
|
||||
self.console.print("[dim]Ready to watch for changes...[/dim]")
|
||||
|
||||
for changes in watch(*DIRS_TO_WATCH):
|
||||
@@ -179,6 +201,11 @@ class MDTestRunner:
|
||||
for change, path_str in changes:
|
||||
path = Path(path_str)
|
||||
|
||||
# See above: `README.md` changes trigger a full re-run of all tests
|
||||
if path == MDTEST_README:
|
||||
self._run_mdtest(self.filters)
|
||||
continue
|
||||
|
||||
match path.suffix:
|
||||
case ".rs":
|
||||
rust_code_has_changed = True
|
||||
@@ -214,12 +241,12 @@ class MDTestRunner:
|
||||
|
||||
if rust_code_has_changed:
|
||||
if self._recompile_tests("Rust code has changed, recompiling tests..."):
|
||||
self._run_mdtest()
|
||||
self._run_mdtest(self.filters)
|
||||
elif vendored_typeshed_has_changed:
|
||||
if self._recompile_tests(
|
||||
"Vendored typeshed has changed, recompiling tests..."
|
||||
):
|
||||
self._run_mdtest()
|
||||
self._run_mdtest(self.filters)
|
||||
elif new_md_files:
|
||||
files = " ".join(file.as_posix() for file in new_md_files)
|
||||
self._recompile_tests(
|
||||
@@ -231,8 +258,19 @@ class MDTestRunner:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="A runner for Markdown-based tests for ty"
|
||||
)
|
||||
parser.add_argument(
|
||||
"filters",
|
||||
nargs="*",
|
||||
help="Partial paths or mangled names, e.g., 'loops/for.md' or 'loops_for'",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
runner = MDTestRunner()
|
||||
runner = MDTestRunner(filters=args.filters)
|
||||
runner.watch()
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
# Documentation of two fuzzer panics involving comprehensions
|
||||
# Regression test for https://github.com/astral-sh/ruff/pull/20962
|
||||
# error message:
|
||||
# `place_by_id: execute: too many cycle iterations`
|
||||
|
||||
Type inference for comprehensions was added in <https://github.com/astral-sh/ruff/pull/20962>. It
|
||||
added two new fuzzer panics that are documented here for regression testing.
|
||||
|
||||
## Too many cycle iterations in `place_by_id`
|
||||
|
||||
<!-- expect-panic: too many cycle iterations -->
|
||||
|
||||
```py
|
||||
name_5(name_3)
|
||||
[0 for unique_name_0 in unique_name_1 for unique_name_2 in name_3]
|
||||
|
||||
@@ -34,4 +28,3 @@ else:
|
||||
@name_3
|
||||
async def name_5():
|
||||
pass
|
||||
```
|
||||
@@ -76,7 +76,18 @@ from ty_extensions import reveal_mro
|
||||
|
||||
class C(Annotated[int, "foo"]): ...
|
||||
|
||||
reveal_mro(C) # revealed: (<class 'C'>, <class 'int'>, <class 'object'>)
|
||||
# revealed: (<class 'C'>, <class 'int'>, <class 'object'>)
|
||||
reveal_mro(C)
|
||||
|
||||
class D(Annotated[list[str], "foo"]): ...
|
||||
|
||||
# revealed: (<class 'D'>, <class 'list[str]'>, <class 'MutableSequence[str]'>, <class 'Sequence[str]'>, <class 'Reversible[str]'>, <class 'Collection[str]'>, <class 'Iterable[str]'>, <class 'Container[str]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(D)
|
||||
|
||||
class E(Annotated[list["E"], "metadata"]): ...
|
||||
|
||||
# error: [revealed-type] "Revealed MRO: (<class 'E'>, <class 'list[E]'>, <class 'MutableSequence[E]'>, <class 'Sequence[E]'>, <class 'Reversible[E]'>, <class 'Collection[E]'>, <class 'Iterable[E]'>, <class 'Container[E]'>, typing.Protocol, typing.Generic, <class 'object'>)"
|
||||
reveal_mro(E)
|
||||
```
|
||||
|
||||
### Not parameterized
|
||||
|
||||
@@ -87,9 +87,23 @@ class Foo:
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||
class Qux(Foo, Bar, Baz):
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||
class Quux[_T](Foo, Bar, Baz):
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
type U = Foo
|
||||
# error: [unresolved-reference]
|
||||
type V = Bar
|
||||
type W = Baz
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
@@ -141,9 +155,23 @@ class Foo:
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||
class Qux(Foo, Bar, Baz):
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||
# error: [unresolved-reference] "Name `Bar` used when not defined"
|
||||
class Quux[_T](Foo, Bar, Baz):
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
type U = Foo
|
||||
# error: [unresolved-reference]
|
||||
type V = Bar
|
||||
type W = Baz
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
|
||||
@@ -396,3 +396,34 @@ B = NewType("B", list[Any])
|
||||
T = TypeVar("T")
|
||||
C = NewType("C", list[T]) # TODO: should be "error: [invalid-newtype]"
|
||||
```
|
||||
|
||||
## Forward references in stub files
|
||||
|
||||
Stubs natively support forward references, so patterns that would raise `NameError` at runtime are
|
||||
allowed in stub files:
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import NewType
|
||||
|
||||
N = NewType("N", A)
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from stub import N, A
|
||||
|
||||
n = N(A()) # fine
|
||||
|
||||
def f(x: A): ...
|
||||
|
||||
f(n) # fine
|
||||
|
||||
class Invalid: ...
|
||||
|
||||
bad = N(Invalid()) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user