Compare commits

..

6 Commits

Author SHA1 Message Date
Aria Desires
901322f9de more cascady 2026-01-09 12:44:49 -05:00
Aria Desires
a59bf83854 checkpoint working 2026-01-09 12:29:35 -05:00
Aria Desires
4b569cea74 cleanup 2026-01-09 10:23:20 -05:00
Aria Desires
26bf64b9ef fixup 2026-01-07 21:57:44 -05:00
Aria Desires
f87146ea54 try optimization 2026-01-07 21:53:05 -05:00
Aria Desires
b0abf9808e Rework module resolution to be breadth-first instead of depth-first 2026-01-07 20:37:19 -05:00
142 changed files with 1915 additions and 5038 deletions

View File

@@ -5,4 +5,5 @@
[rules]
possibly-unresolved-reference = "warn"
possibly-missing-import = "warn"
unused-ignore-comment = "warn"
division-by-zero = "warn"

View File

@@ -51,7 +51,6 @@ jobs:
- name: "Build sdist"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
command: sdist
args: --out dist
- name: "Test sdist"
@@ -82,7 +81,6 @@ jobs:
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
target: x86_64
args: --release --locked --out dist
- name: "Upload wheels"
@@ -125,7 +123,6 @@ jobs:
- name: "Build wheels - aarch64"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
target: aarch64
args: --release --locked --out dist
- name: "Test wheel - aarch64"
@@ -182,7 +179,6 @@ jobs:
- name: "Build wheels"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
env:
@@ -236,7 +232,6 @@ jobs:
- name: "Build wheels"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
target: ${{ matrix.target }}
manylinux: auto
args: --release --locked --out dist
@@ -313,7 +308,6 @@ jobs:
- name: "Build wheels"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
target: ${{ matrix.platform.target }}
manylinux: auto
docker-options: ${{ matrix.platform.maturin_docker_options }}
@@ -380,7 +374,6 @@ jobs:
- name: "Build wheels"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --locked --out dist
@@ -446,7 +439,6 @@ jobs:
- name: "Build wheels"
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
maturin-version: v1.9.6
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --locked --out dist

View File

@@ -1,58 +0,0 @@
# Build ruff_wasm for npm.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist`.
name: "Build wasm"
on:
workflow_call:
inputs:
plan:
required: true
type: string
pull_request:
paths:
- .github/workflows/build-wasm.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
build:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
strategy:
matrix:
target: [web, bundler, nodejs]
fail-fast: false
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
with:
version: v0.13.1
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Run wasm-pack build"
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm
- name: "Rename generated package"
run: | # Replace the package name w/ jq
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- name: "Upload wasm artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: artifacts-wasm-${{ matrix.target }}
path: crates/ruff_wasm/pkg

View File

@@ -1,18 +1,25 @@
# Publish ruff_wasm to npm.
# Build and publish ruff-api for wasm.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish
# job within `cargo-dist`.
name: "Publish wasm"
name: "Build and publish wasm"
on:
workflow_dispatch:
workflow_call:
inputs:
plan:
required: true
type: string
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
publish:
ruff_wasm:
runs-on: ubuntu-latest
permissions:
contents: read
@@ -22,19 +29,31 @@ jobs:
target: [web, bundler, nodejs]
fail-fast: false
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
name: artifacts-wasm-${{ matrix.target }}
path: pkg
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
with:
version: v0.13.1
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Run wasm-pack build"
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm
- name: "Rename generated package"
run: | # Replace the package name w/ jq
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: "Publish (dry-run)"
if: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
run: npm publish --dry-run pkg
run: npm publish --dry-run crates/ruff_wasm/pkg
- name: "Publish"
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
run: npm publish --provenance --access public pkg
run: npm publish --provenance --access public crates/ruff_wasm/pkg
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -112,22 +112,12 @@ jobs:
"contents": "read"
"packages": "write"
custom-build-wasm:
needs:
- plan
if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }}
uses: ./.github/workflows/build-wasm.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- custom-build-binaries
- custom-build-docker
- custom-build-wasm
runs-on: "depot-ubuntu-latest-4"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -175,10 +165,9 @@ jobs:
- plan
- custom-build-binaries
- custom-build-docker
- custom-build-wasm
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') && (needs.custom-build-wasm.result == 'skipped' || needs.custom-build-wasm.result == 'success') }}
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "depot-ubuntu-latest-4"

View File

@@ -40,12 +40,12 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with:
enable-cache: true
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
lookup-only: false
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- name: Install Rust toolchain
run: rustup show

View File

@@ -125,7 +125,6 @@ repos:
args:
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
- "-ignore=SC2016" # another shellcheck lint: seems to have false positives?
language: golang # means renovate will also update `additional_dependencies`
additional_dependencies:
# actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions
# and checks these with shellcheck. This is arguably its most useful feature,

View File

@@ -1,63 +1,5 @@
# Changelog
## 0.14.11
Released on 2026-01-08.
### Preview features
- Consolidate diagnostics for matched disable/enable suppression comments ([#22099](https://github.com/astral-sh/ruff/pull/22099))
- Report diagnostics for invalid/unmatched range suppression comments ([#21908](https://github.com/astral-sh/ruff/pull/21908))
- \[`airflow`\] Passing positional argument into `airflow.lineage.hook.HookLineageCollector.create_asset` is not allowed (`AIR303`) ([#22046](https://github.com/astral-sh/ruff/pull/22046))
- \[`refurb`\] Mark `FURB192` fix as always unsafe ([#22210](https://github.com/astral-sh/ruff/pull/22210))
- \[`ruff`\] Add `non-empty-init-module` (`RUF067`) ([#22143](https://github.com/astral-sh/ruff/pull/22143))
### Bug fixes
- Fix GitHub format for multi-line diagnostics ([#22108](https://github.com/astral-sh/ruff/pull/22108))
- \[`flake8-unused-arguments`\] Mark `**kwargs` in `TypeVar` as used (`ARG001`) ([#22214](https://github.com/astral-sh/ruff/pull/22214))
### Rule changes
- Add `help:` subdiagnostics for several Ruff rules that can sometimes appear to disagree with `ty` ([#22331](https://github.com/astral-sh/ruff/pull/22331))
- \[`pylint`\] Demote `PLW1510` fix to display-only ([#22318](https://github.com/astral-sh/ruff/pull/22318))
- \[`pylint`\] Ignore identical members (`PLR1714`) ([#22220](https://github.com/astral-sh/ruff/pull/22220))
- \[`pylint`\] Improve diagnostic range for `PLC0206` ([#22312](https://github.com/astral-sh/ruff/pull/22312))
- \[`ruff`\] Improve fix title for `RUF102` invalid rule code ([#22100](https://github.com/astral-sh/ruff/pull/22100))
- \[`flake8-simplify`\]: Avoid unnecessary builtins import for `SIM105` ([#22358](https://github.com/astral-sh/ruff/pull/22358))
### Configuration
- Allow Python 3.15 as valid `target-version` value in preview ([#22419](https://github.com/astral-sh/ruff/pull/22419))
- Check `required-version` before parsing rules ([#22410](https://github.com/astral-sh/ruff/pull/22410))
- Include configured `src` directories when resolving graphs ([#22451](https://github.com/astral-sh/ruff/pull/22451))
### Documentation
- Update `T201` suggestion to not use root logger to satisfy `LOG015` ([#22059](https://github.com/astral-sh/ruff/pull/22059))
- Fix `iter` example in unsafe fixes doc ([#22118](https://github.com/astral-sh/ruff/pull/22118))
- \[`flake8_print`\] better suggestion for `basicConfig` in `T201` docs ([#22101](https://github.com/astral-sh/ruff/pull/22101))
- \[`pylint`\] Restore the fix safety docs for `PLW0133` ([#22211](https://github.com/astral-sh/ruff/pull/22211))
- Fix Jupyter notebook discovery info for editors ([#22447](https://github.com/astral-sh/ruff/pull/22447))
### Contributors
- [@charliermarsh](https://github.com/charliermarsh)
- [@ntBre](https://github.com/ntBre)
- [@cenviity](https://github.com/cenviity)
- [@njhearp](https://github.com/njhearp)
- [@cbachhuber](https://github.com/cbachhuber)
- [@jelle-openai](https://github.com/jelle-openai)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@ValdonVitija](https://github.com/ValdonVitija)
- [@BurntSushi](https://github.com/BurntSushi)
- [@Jkhall81](https://github.com/Jkhall81)
- [@PeterJCLaw](https://github.com/PeterJCLaw)
- [@harupy](https://github.com/harupy)
- [@amyreese](https://github.com/amyreese)
- [@sjyangkevin](https://github.com/sjyangkevin)
- [@woodruffw](https://github.com/woodruffw)
## 0.14.10
Released on 2025-12-18.

14
Cargo.lock generated
View File

@@ -2912,7 +2912,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.11"
version = "0.14.10"
dependencies = [
"anyhow",
"argfile",
@@ -2928,7 +2928,6 @@ dependencies = [
"filetime",
"globwalk",
"ignore",
"indexmap",
"indoc",
"insta",
"insta-cmd",
@@ -3172,7 +3171,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.11"
version = "0.14.10"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3530,7 +3529,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.11"
version = "0.14.10"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3646,7 +3645,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
dependencies = [
"boxcar",
"compact_str",
@@ -3671,12 +3670,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
[[package]]
name = "salsa-macros"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
dependencies = [
"proc-macro2",
"quote",
@@ -4444,7 +4443,6 @@ version = "0.0.0"
dependencies = [
"bitflags 2.10.0",
"camino",
"compact_str",
"get-size2",
"insta",
"itertools 0.14.0",

View File

@@ -150,7 +150,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 = "9860ff6ca0f1f8f3a8d6b832020002790b501254", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "309c249088fdeef0129606fa34ec2eefc74736ff", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -150,8 +150,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.11/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.11/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.10/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.10/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -184,7 +184,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.11
rev: v0.14.10
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.11"
version = "0.14.10"
publish = true
authors = { workspace = true }
edition = { workspace = true }
@@ -48,7 +48,6 @@ colored = { workspace = true }
filetime = { workspace = true }
globwalk = { workspace = true }
ignore = { workspace = true }
indexmap = { workspace = true }
is-macro = { workspace = true }
itertools = { workspace = true }
jiff = { workspace = true }

View File

@@ -2,7 +2,6 @@ use crate::args::{AnalyzeGraphArgs, ConfigArguments};
use crate::resolve::resolve;
use crate::{ExitStatus, resolve_default_files};
use anyhow::Result;
use indexmap::IndexSet;
use log::{debug, warn};
use path_absolutize::CWD;
use ruff_db::system::{SystemPath, SystemPathBuf};
@@ -12,7 +11,7 @@ use ruff_linter::source_kind::SourceKind;
use ruff_linter::{warn_user, warn_user_once};
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{ResolvedFile, match_exclusion, python_files_in_path};
use rustc_hash::{FxBuildHasher, FxHashMap};
use rustc_hash::FxHashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
@@ -60,34 +59,17 @@ pub(crate) fn analyze_graph(
})
.collect::<FxHashMap<_, _>>();
// Create a database from the source roots, combining configured `src` paths with detected
// package roots. Configured paths are added first so they take precedence, and duplicates
// are removed.
let mut src_roots: IndexSet<SystemPathBuf, FxBuildHasher> = IndexSet::default();
// Add configured `src` paths first (for precedence), filtering to only include existing
// directories.
src_roots.extend(
pyproject_config
.settings
.linter
.src
.iter()
.filter(|path| path.is_dir())
.filter_map(|path| SystemPathBuf::from_path_buf(path.clone()).ok()),
);
// Add detected package roots.
src_roots.extend(
package_roots
.values()
.filter_map(|package| package.as_deref())
.filter_map(|path| path.parent())
.filter_map(|path| SystemPathBuf::from_path_buf(path.to_path_buf()).ok()),
);
// Create a database from the source roots.
let src_roots = package_roots
.values()
.filter_map(|package| package.as_deref())
.filter_map(|package| package.parent())
.map(Path::to_path_buf)
.filter_map(|path| SystemPathBuf::from_path_buf(path).ok())
.collect();
let db = ModuleDb::from_src_roots(
src_roots.into_iter().collect(),
src_roots,
pyproject_config
.settings
.analyze

View File

@@ -29,10 +29,10 @@ pub(crate) fn show_settings(
bail!("No files found under the given path");
};
let (settings, config_path) = resolver.resolve_with_path(&path);
let settings = resolver.resolve(&path);
writeln!(writer, "Resolved settings for: \"{}\"", path.display())?;
if let Some(settings_path) = config_path {
if let Some(settings_path) = pyproject_config.path.as_ref() {
writeln!(writer, "Settings path: \"{}\"", settings_path.display())?;
}
write!(writer, "{settings}")?;

View File

@@ -714,121 +714,6 @@ fn notebook_basic() -> Result<()> {
Ok(())
}
/// Test that the `src` configuration option is respected.
///
/// This is useful for monorepos where there are multiple source directories that need to be
/// included in the module resolution search path.
#[test]
fn src_option() -> Result<()> {
let tempdir = TempDir::new()?;
let root = ChildPath::new(tempdir.path());
// Create a lib directory with a package.
root.child("lib")
.child("mylib")
.child("__init__.py")
.write_str("def helper(): pass")?;
// Create an app directory with a file that imports from mylib.
root.child("app").child("__init__.py").write_str("")?;
root.child("app")
.child("main.py")
.write_str("from mylib import helper")?;
// Without src configured, the import from mylib won't resolve.
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().arg("app").current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"app/__init__.py": [],
"app/main.py": []
}
----- stderr -----
"#);
});
// With src = ["lib"], the import should resolve.
root.child("ruff.toml").write_str(indoc::indoc! {r#"
src = ["lib"]
"#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().arg("app").current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"app/__init__.py": [],
"app/main.py": [
"lib/mylib/__init__.py"
]
}
----- stderr -----
"#);
});
Ok(())
}
/// Test that glob patterns in `src` are expanded.
#[test]
fn src_glob_expansion() -> Result<()> {
let tempdir = TempDir::new()?;
let root = ChildPath::new(tempdir.path());
// Create multiple lib directories with packages.
root.child("libs")
.child("lib_a")
.child("pkg_a")
.child("__init__.py")
.write_str("def func_a(): pass")?;
root.child("libs")
.child("lib_b")
.child("pkg_b")
.child("__init__.py")
.write_str("def func_b(): pass")?;
// Create an app that imports from both packages.
root.child("app").child("__init__.py").write_str("")?;
root.child("app")
.child("main.py")
.write_str("from pkg_a import func_a\nfrom pkg_b import func_b")?;
// Use a glob pattern to include all lib directories.
root.child("ruff.toml").write_str(indoc::indoc! {r#"
src = ["libs/*"]
"#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().arg("app").current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"app/__init__.py": [],
"app/main.py": [
"libs/lib_a/pkg_a/__init__.py",
"libs/lib_b/pkg_b/__init__.py"
]
}
----- stderr -----
"#);
});
Ok(())
}
#[test]
fn notebook_with_magic() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -1126,35 +1126,6 @@ import os
Ok(())
}
#[test]
fn required_version_fails_to_parse() -> Result<()> {
let fixture = CliTest::with_file(
"ruff.toml",
r#"
required-version = "pikachu"
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Failed to load configuration `[TMP]/ruff.toml`
Cause: Failed to parse [TMP]/ruff.toml
Cause: TOML parse error at line 2, column 20
|
2 | required-version = "pikachu"
| ^^^^^^^^^
Failed to parse version: Unexpected end of version specifier, expected operator:
pikachu
^^^^^^^
"#);
Ok(())
}
#[test]
fn required_version_exact_mismatch() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
@@ -1166,10 +1137,10 @@ required-version = "0.1.0"
"#,
)?;
let mut settings = insta::Settings::clone_current();
settings.add_filter(version, "[VERSION]");
settings.bind(|| {
assert_cmd_snapshot!(fixture
insta::with_settings!({
filters => vec![(version, "[VERSION]")]
}, {
assert_cmd_snapshot!(fixture
.check_command()
.arg("--config")
.arg("ruff.toml")
@@ -1183,7 +1154,6 @@ import os
----- stderr -----
ruff failed
Cause: Failed to load configuration `[TMP]/ruff.toml`
Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
");
});
@@ -1242,10 +1212,10 @@ required-version = ">{version}"
),
)?;
let mut settings = insta::Settings::clone_current();
settings.add_filter(version, "[VERSION]");
settings.bind(|| {
assert_cmd_snapshot!(fixture
insta::with_settings!({
filters => vec![(version, "[VERSION]")]
}, {
assert_cmd_snapshot!(fixture
.check_command()
.arg("--config")
.arg("ruff.toml")
@@ -1259,48 +1229,6 @@ import os
----- stderr -----
ruff failed
Cause: Failed to load configuration `[TMP]/ruff.toml`
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
");
});
Ok(())
}
#[test]
fn required_version_precedes_rule_validation() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let fixture = CliTest::with_file(
"ruff.toml",
&format!(
r#"
required-version = ">{version}"
[lint]
select = ["RUF999"]
"#
),
)?;
let mut settings = insta::Settings::clone_current();
settings.add_filter(version, "[VERSION]");
settings.bind(|| {
assert_cmd_snapshot!(fixture
.check_command()
.arg("--config")
.arg("ruff.toml")
.arg("-")
.pass_stdin(r#"
import os
"#), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Failed to load configuration `[TMP]/ruff.toml`
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
");
});

View File

@@ -16,7 +16,6 @@ success: true
exit_code: 0
----- stdout -----
Resolved settings for: "[TMP]/foo/test.py"
Settings path: "[TMP]/foo/pyproject.toml"
# General Settings
cache_dir = "[TMP]/foo/.ruff_cache"

View File

@@ -50,56 +50,6 @@ ignore = [
Ok(())
}
#[test]
fn display_settings_from_nested_directory() -> anyhow::Result<()> {
let tempdir = TempDir::new().context("Failed to create temp directory.")?;
// Tempdir path's on macos are symlinks, which doesn't play nicely with
// our snapshot filtering.
let project_dir =
dunce::canonicalize(tempdir.path()).context("Failed to canonical tempdir path.")?;
// Root pyproject.toml.
std::fs::write(
project_dir.join("pyproject.toml"),
r#"
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ["E", "F"]
"#,
)?;
// Create a subdirectory with its own pyproject.toml.
let subdir = project_dir.join("subdir");
std::fs::create_dir(&subdir)?;
std::fs::write(
subdir.join("pyproject.toml"),
r#"
[tool.ruff]
line-length = 120
[tool.ruff.lint]
select = ["E", "F", "I"]
"#,
)?;
std::fs::write(subdir.join("test.py"), r#"import os"#).context("Failed to write test.py.")?;
insta::with_settings!({filters => vec![
(&*tempdir_filter(&project_dir), "<temp_dir>/"),
(r#"\\(\w\w|\s|\.|")"#, "/$1"),
]}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-settings", "subdir/test.py"])
.current_dir(&project_dir));
});
Ok(())
}
fn tempdir_filter(project_dir: &Path) -> String {
format!(r#"{}\\?/?"#, regex::escape(project_dir.to_str().unwrap()))
}

View File

@@ -1,410 +0,0 @@
---
source: crates/ruff/tests/show_settings.rs
info:
program: ruff
args:
- check
- "--show-settings"
- subdir/test.py
---
success: true
exit_code: 0
----- stdout -----
Resolved settings for: "<temp_dir>/subdir/test.py"
Settings path: "<temp_dir>/subdir/pyproject.toml"
# General Settings
cache_dir = "<temp_dir>/subdir/.ruff_cache"
fix = false
fix_only = false
output_format = full
show_fixes = false
unsafe_fixes = hint
# File Resolver Settings
file_resolver.exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"dist",
"node_modules",
"site-packages",
"venv",
]
file_resolver.extend_exclude = []
file_resolver.force_exclude = false
file_resolver.include = [
"*.py",
"*.pyi",
"*.ipynb",
"**/pyproject.toml",
]
file_resolver.extend_include = []
file_resolver.respect_gitignore = true
file_resolver.project_root = "<temp_dir>/subdir"
# Linter Settings
linter.exclude = []
linter.project_root = "<temp_dir>/subdir"
linter.rules.enabled = [
unsorted-imports (I001),
missing-required-import (I002),
mixed-spaces-and-tabs (E101),
multiple-imports-on-one-line (E401),
module-import-not-at-top-of-file (E402),
line-too-long (E501),
multiple-statements-on-one-line-colon (E701),
multiple-statements-on-one-line-semicolon (E702),
useless-semicolon (E703),
none-comparison (E711),
true-false-comparison (E712),
not-in-test (E713),
not-is-test (E714),
type-comparison (E721),
bare-except (E722),
lambda-assignment (E731),
ambiguous-variable-name (E741),
ambiguous-class-name (E742),
ambiguous-function-name (E743),
io-error (E902),
unused-import (F401),
import-shadowed-by-loop-var (F402),
undefined-local-with-import-star (F403),
late-future-import (F404),
undefined-local-with-import-star-usage (F405),
undefined-local-with-nested-import-star-usage (F406),
future-feature-not-defined (F407),
percent-format-invalid-format (F501),
percent-format-expected-mapping (F502),
percent-format-expected-sequence (F503),
percent-format-extra-named-arguments (F504),
percent-format-missing-argument (F505),
percent-format-mixed-positional-and-named (F506),
percent-format-positional-count-mismatch (F507),
percent-format-star-requires-sequence (F508),
percent-format-unsupported-format-character (F509),
string-dot-format-invalid-format (F521),
string-dot-format-extra-named-arguments (F522),
string-dot-format-extra-positional-arguments (F523),
string-dot-format-missing-arguments (F524),
string-dot-format-mixing-automatic (F525),
f-string-missing-placeholders (F541),
multi-value-repeated-key-literal (F601),
multi-value-repeated-key-variable (F602),
expressions-in-star-assignment (F621),
multiple-starred-expressions (F622),
assert-tuple (F631),
is-literal (F632),
invalid-print-syntax (F633),
if-tuple (F634),
break-outside-loop (F701),
continue-outside-loop (F702),
yield-outside-function (F704),
return-outside-function (F706),
default-except-not-last (F707),
forward-annotation-syntax-error (F722),
redefined-while-unused (F811),
undefined-name (F821),
undefined-export (F822),
undefined-local (F823),
unused-variable (F841),
unused-annotation (F842),
raise-not-implemented (F901),
]
linter.rules.should_fix = [
unsorted-imports (I001),
missing-required-import (I002),
mixed-spaces-and-tabs (E101),
multiple-imports-on-one-line (E401),
module-import-not-at-top-of-file (E402),
line-too-long (E501),
multiple-statements-on-one-line-colon (E701),
multiple-statements-on-one-line-semicolon (E702),
useless-semicolon (E703),
none-comparison (E711),
true-false-comparison (E712),
not-in-test (E713),
not-is-test (E714),
type-comparison (E721),
bare-except (E722),
lambda-assignment (E731),
ambiguous-variable-name (E741),
ambiguous-class-name (E742),
ambiguous-function-name (E743),
io-error (E902),
unused-import (F401),
import-shadowed-by-loop-var (F402),
undefined-local-with-import-star (F403),
late-future-import (F404),
undefined-local-with-import-star-usage (F405),
undefined-local-with-nested-import-star-usage (F406),
future-feature-not-defined (F407),
percent-format-invalid-format (F501),
percent-format-expected-mapping (F502),
percent-format-expected-sequence (F503),
percent-format-extra-named-arguments (F504),
percent-format-missing-argument (F505),
percent-format-mixed-positional-and-named (F506),
percent-format-positional-count-mismatch (F507),
percent-format-star-requires-sequence (F508),
percent-format-unsupported-format-character (F509),
string-dot-format-invalid-format (F521),
string-dot-format-extra-named-arguments (F522),
string-dot-format-extra-positional-arguments (F523),
string-dot-format-missing-arguments (F524),
string-dot-format-mixing-automatic (F525),
f-string-missing-placeholders (F541),
multi-value-repeated-key-literal (F601),
multi-value-repeated-key-variable (F602),
expressions-in-star-assignment (F621),
multiple-starred-expressions (F622),
assert-tuple (F631),
is-literal (F632),
invalid-print-syntax (F633),
if-tuple (F634),
break-outside-loop (F701),
continue-outside-loop (F702),
yield-outside-function (F704),
return-outside-function (F706),
default-except-not-last (F707),
forward-annotation-syntax-error (F722),
redefined-while-unused (F811),
undefined-name (F821),
undefined-export (F822),
undefined-local (F823),
unused-variable (F841),
unused-annotation (F842),
raise-not-implemented (F901),
]
linter.per_file_ignores = {}
linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = []
linter.unresolved_target_version = none
linter.per_file_target_version = {}
linter.preview = disabled
linter.explicit_preview_rules = false
linter.extension = ExtensionMapping({})
linter.allowed_confusables = []
linter.builtins = []
linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
linter.external = []
linter.ignore_init_module_imports = true
linter.logger_objects = []
linter.namespace_packages = []
linter.src = [
"<temp_dir>/subdir",
"<temp_dir>/subdir/src",
]
linter.tab_size = 4
linter.line_length = 120
linter.task_tags = [
TODO,
FIXME,
XXX,
]
linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins
linter.flake8_annotations.mypy_init_return = false
linter.flake8_annotations.suppress_dummy_args = false
linter.flake8_annotations.suppress_none_returning = false
linter.flake8_annotations.allow_star_arg_any = false
linter.flake8_annotations.ignore_fully_untyped = false
linter.flake8_bandit.hardcoded_tmp_directory = [
/tmp,
/var/tmp,
/dev/shm,
]
linter.flake8_bandit.check_typed_exception = false
linter.flake8_bandit.extend_markup_names = []
linter.flake8_bandit.allowed_markup_calls = []
linter.flake8_bugbear.extend_immutable_calls = []
linter.flake8_builtins.allowed_modules = []
linter.flake8_builtins.ignorelist = []
linter.flake8_builtins.strict_checking = false
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0
linter.flake8_gettext.function_names = [
_,
gettext,
ngettext,
]
linter.flake8_implicit_str_concat.allow_multiline = true
linter.flake8_import_conventions.aliases = {
altair = alt,
holoviews = hv,
matplotlib = mpl,
matplotlib.pyplot = plt,
networkx = nx,
numpy = np,
numpy.typing = npt,
pandas = pd,
panel = pn,
plotly.express = px,
polars = pl,
pyarrow = pa,
seaborn = sns,
tensorflow = tf,
tkinter = tk,
xml.etree.ElementTree = ET,
}
linter.flake8_import_conventions.banned_aliases = {}
linter.flake8_import_conventions.banned_from = []
linter.flake8_pytest_style.fixture_parentheses = false
linter.flake8_pytest_style.parametrize_names_type = tuple
linter.flake8_pytest_style.parametrize_values_type = list
linter.flake8_pytest_style.parametrize_values_row_type = tuple
linter.flake8_pytest_style.raises_require_match_for = [
BaseException,
Exception,
ValueError,
OSError,
IOError,
EnvironmentError,
socket.error,
]
linter.flake8_pytest_style.raises_extend_require_match_for = []
linter.flake8_pytest_style.mark_parentheses = false
linter.flake8_quotes.inline_quotes = double
linter.flake8_quotes.multiline_quotes = double
linter.flake8_quotes.docstring_quotes = double
linter.flake8_quotes.avoid_escape = true
linter.flake8_self.ignore_names = [
_make,
_asdict,
_replace,
_fields,
_field_defaults,
_name_,
_value_,
]
linter.flake8_tidy_imports.ban_relative_imports = "parents"
linter.flake8_tidy_imports.banned_api = {}
linter.flake8_tidy_imports.banned_module_level_imports = []
linter.flake8_type_checking.strict = false
linter.flake8_type_checking.exempt_modules = [
typing,
typing_extensions,
]
linter.flake8_type_checking.runtime_required_base_classes = []
linter.flake8_type_checking.runtime_required_decorators = []
linter.flake8_type_checking.quote_annotations = false
linter.flake8_unused_arguments.ignore_variadic_names = false
linter.isort.required_imports = []
linter.isort.combine_as_imports = false
linter.isort.force_single_line = false
linter.isort.force_sort_within_sections = false
linter.isort.detect_same_package = true
linter.isort.case_sensitive = false
linter.isort.force_wrap_aliases = false
linter.isort.force_to_top = []
linter.isort.known_modules = {}
linter.isort.order_by_type = true
linter.isort.relative_imports_order = furthest_to_closest
linter.isort.single_line_exclusions = []
linter.isort.split_on_trailing_comma = true
linter.isort.classes = []
linter.isort.constants = []
linter.isort.variables = []
linter.isort.no_lines_before = []
linter.isort.lines_after_imports = -1
linter.isort.lines_between_types = 0
linter.isort.forced_separate = []
linter.isort.section_order = [
known { type = future },
known { type = standard_library },
known { type = third_party },
known { type = first_party },
known { type = local_folder },
]
linter.isort.default_section = known { type = third_party }
linter.isort.no_sections = false
linter.isort.from_first = false
linter.isort.length_sort = false
linter.isort.length_sort_straight = false
linter.mccabe.max_complexity = 10
linter.pep8_naming.ignore_names = [
setUp,
tearDown,
setUpClass,
tearDownClass,
setUpModule,
tearDownModule,
asyncSetUp,
asyncTearDown,
setUpTestData,
failureException,
longMessage,
maxDiff,
]
linter.pep8_naming.classmethod_decorators = []
linter.pep8_naming.staticmethod_decorators = []
linter.pycodestyle.max_line_length = 120
linter.pycodestyle.max_doc_length = none
linter.pycodestyle.ignore_overlong_task_comments = false
linter.pyflakes.extend_generics = []
linter.pyflakes.allowed_unused_imports = []
linter.pylint.allow_magic_value_types = [
str,
bytes,
]
linter.pylint.allow_dunder_method_names = []
linter.pylint.max_args = 5
linter.pylint.max_positional_args = 5
linter.pylint.max_returns = 6
linter.pylint.max_bool_expr = 5
linter.pylint.max_branches = 12
linter.pylint.max_statements = 50
linter.pylint.max_public_methods = 20
linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []
formatter.unresolved_target_version = 3.10
formatter.per_file_target_version = {}
formatter.preview = disabled
formatter.line_width = 120
formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
# Analyze Settings
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -15,7 +15,7 @@ use ruff_db::files::{File, system_path_to_file};
use ruff_db::source::source_text;
use ruff_db::system::{InMemorySystem, MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem};
use ruff_python_ast::PythonVersion;
use ty_project::metadata::options::{AnalysisOptions, EnvironmentOptions, Options};
use ty_project::metadata::options::{EnvironmentOptions, Options};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ChangedKind};
use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata};
@@ -67,7 +67,6 @@ fn tomllib_path(file: &TestFile) -> SystemPathBuf {
SystemPathBuf::from("src").join(file.name())
}
#[expect(clippy::needless_update)]
fn setup_tomllib_case() -> Case {
let system = TestSystem::default();
let fs = system.memory_file_system().clone();
@@ -86,10 +85,6 @@ fn setup_tomllib_case() -> Case {
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
..EnvironmentOptions::default()
}),
analysis: Some(AnalysisOptions {
respect_type_ignore_comments: Some(false),
..AnalysisOptions::default()
}),
..Options::default()
});
@@ -760,7 +755,7 @@ fn datetype(criterion: &mut Criterion) {
max_dep_date: "2025-07-04",
python_version: PythonVersion::PY313,
},
4,
2,
);
bench_project(&benchmark, criterion);

View File

@@ -71,8 +71,6 @@ impl Display for Benchmark<'_> {
}
}
#[track_caller]
#[expect(clippy::cast_precision_loss)]
fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usize) {
let result = db.check();
let diagnostics = result.len();
@@ -81,12 +79,6 @@ fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usiz
diagnostics > 1 && diagnostics <= max_diagnostics,
"Expected between 1 and {max_diagnostics} diagnostics on project '{project_name}' but got {diagnostics}",
);
if (max_diagnostics - diagnostics) as f64 / max_diagnostics as f64 > 0.10 {
tracing::warn!(
"The expected diagnostics for project `{project_name}` can be reduced: expected {max_diagnostics} but got {diagnostics}"
);
}
}
static ALTAIR: Benchmark = Benchmark::new(
@@ -109,7 +101,7 @@ static ALTAIR: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
850,
1000,
);
static COLOUR_SCIENCE: Benchmark = Benchmark::new(
@@ -128,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310,
},
350,
1070,
);
static FREQTRADE: Benchmark = Benchmark::new(
@@ -171,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
3800,
4000,
);
static PYDANTIC: Benchmark = Benchmark::new(
@@ -189,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
3200,
7000,
);
static SYMPY: Benchmark = Benchmark::new(
@@ -202,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13400,
13116,
);
static TANJUN: Benchmark = Benchmark::new(
@@ -215,7 +207,7 @@ static TANJUN: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
110,
320,
);
static STATIC_FRAME: Benchmark = Benchmark::new(
@@ -231,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
1700,
1100,
);
#[track_caller]

View File

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

View File

@@ -52,7 +52,6 @@ impl InvalidRuleCodeKind {
pub(crate) struct InvalidRuleCode {
pub(crate) rule_code: String,
pub(crate) kind: InvalidRuleCodeKind,
pub(crate) whole_comment: bool,
}
impl AlwaysFixableViolation for InvalidRuleCode {
@@ -66,11 +65,7 @@ impl AlwaysFixableViolation for InvalidRuleCode {
}
fn fix_title(&self) -> String {
if self.whole_comment {
format!("Remove the {} comment", self.kind.as_str())
} else {
format!("Remove the rule code `{}`", self.rule_code)
}
"Remove the rule code".to_string()
}
}
@@ -127,7 +122,6 @@ fn all_codes_invalid_diagnostic(
.collect::<Vec<_>>()
.join(", "),
kind: InvalidRuleCodeKind::Noqa,
whole_comment: true,
},
directive.range(),
)
@@ -145,7 +139,6 @@ fn some_codes_are_invalid_diagnostic(
InvalidRuleCode {
rule_code: invalid_code.to_string(),
kind: InvalidRuleCodeKind::Noqa,
whole_comment: false,
},
invalid_code.range(),
)

View File

@@ -52,25 +52,6 @@ impl UnusedNOQAKind {
/// foo.bar()
/// ```
///
/// ## Conflict with other linters
/// When using `RUF100` with the `--fix` option, Ruff may remove trailing comments
/// that follow a `# noqa` directive on the same line, as it interprets the
/// remainder of the line as a description for the suppression.
///
/// To prevent Ruff from removing suppressions for other tools (like `pylint`
/// or `mypy`), separate them with a second `#` character:
///
/// ```python
/// # Bad: Ruff --fix will remove the pylint comment
/// def visit_ImportFrom(self, node): # noqa: N802, pylint: disable=invalid-name
/// pass
///
///
/// # Good: Ruff will preserve the pylint comment
/// def visit_ImportFrom(self, node): # noqa: N802 # pylint: disable=invalid-name
/// pass
/// ```
///
/// ## Options
/// - `lint.external`
///

View File

@@ -10,7 +10,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
3 | # External code
4 | import re # noqa: V123
|
help: Remove the `# noqa` comment
help: Remove the rule code
1 | # Invalid code
- import os # noqa: INVALID123
2 + import os
@@ -28,7 +28,7 @@ RUF102 [*] Invalid rule code in `# noqa`: V123
5 | # Valid noqa
6 | import sys # noqa: E402
|
help: Remove the `# noqa` comment
help: Remove the rule code
1 | # Invalid code
2 | import os # noqa: INVALID123
3 | # External code
@@ -48,7 +48,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID456
8 | from itertools import product # Preceeding comment # noqa: INVALID789
9 | # Succeeding comment
|
help: Remove the rule code `INVALID456`
help: Remove the rule code
4 | import re # noqa: V123
5 | # Valid noqa
6 | import sys # noqa: E402
@@ -68,7 +68,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID789
9 | # Succeeding comment
10 | import math # noqa: INVALID000 # Succeeding comment
|
help: Remove the `# noqa` comment
help: Remove the rule code
5 | # Valid noqa
6 | import sys # noqa: E402
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
@@ -88,7 +88,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID000
11 | # Mixed valid and invalid
12 | from typing import List # noqa: F401, INVALID123
|
help: Remove the `# noqa` comment
help: Remove the rule code
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
8 | from itertools import product # Preceeding comment # noqa: INVALID789
9 | # Succeeding comment
@@ -108,7 +108,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
13 | # Test for multiple invalid
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
|
help: Remove the rule code `INVALID123`
help: Remove the rule code
9 | # Succeeding comment
10 | import math # noqa: INVALID000 # Succeeding comment
11 | # Mixed valid and invalid
@@ -128,7 +128,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID100
15 | # Test for preserving valid codes when fixing
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
help: Remove the rule code `INVALID100`
help: Remove the rule code
11 | # Mixed valid and invalid
12 | from typing import List # noqa: F401, INVALID123
13 | # Test for multiple invalid
@@ -148,7 +148,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID200
15 | # Test for preserving valid codes when fixing
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
help: Remove the rule code `INVALID200`
help: Remove the rule code
11 | # Mixed valid and invalid
12 | from typing import List # noqa: F401, INVALID123
13 | # Test for multiple invalid
@@ -168,7 +168,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID300
17 | # Test for mixed code types
18 | import json # noqa: E402, INVALID400, V100
|
help: Remove the rule code `INVALID300`
help: Remove the rule code
13 | # Test for multiple invalid
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
15 | # Test for preserving valid codes when fixing
@@ -188,7 +188,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID400
19 | # Test for rule redirects
20 | import pandas as pd # noqa: TCH002
|
help: Remove the rule code `INVALID400`
help: Remove the rule code
15 | # Test for preserving valid codes when fixing
16 | from itertools import chain # noqa: E402, INVALID300, F401
17 | # Test for mixed code types
@@ -207,7 +207,7 @@ RUF102 [*] Invalid rule code in `# noqa`: V100
19 | # Test for rule redirects
20 | import pandas as pd # noqa: TCH002
|
help: Remove the rule code `V100`
help: Remove the rule code
15 | # Test for preserving valid codes when fixing
16 | from itertools import chain # noqa: E402, INVALID300, F401
17 | # Test for mixed code types

View File

@@ -10,7 +10,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
3 | # External code
4 | import re # noqa: V123
|
help: Remove the `# noqa` comment
help: Remove the rule code
1 | # Invalid code
- import os # noqa: INVALID123
2 + import os
@@ -28,7 +28,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID456
8 | from itertools import product # Preceeding comment # noqa: INVALID789
9 | # Succeeding comment
|
help: Remove the rule code `INVALID456`
help: Remove the rule code
4 | import re # noqa: V123
5 | # Valid noqa
6 | import sys # noqa: E402
@@ -48,7 +48,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID789
9 | # Succeeding comment
10 | import math # noqa: INVALID000 # Succeeding comment
|
help: Remove the `# noqa` comment
help: Remove the rule code
5 | # Valid noqa
6 | import sys # noqa: E402
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
@@ -68,7 +68,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID000
11 | # Mixed valid and invalid
12 | from typing import List # noqa: F401, INVALID123
|
help: Remove the `# noqa` comment
help: Remove the rule code
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
8 | from itertools import product # Preceeding comment # noqa: INVALID789
9 | # Succeeding comment
@@ -88,7 +88,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
13 | # Test for multiple invalid
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
|
help: Remove the rule code `INVALID123`
help: Remove the rule code
9 | # Succeeding comment
10 | import math # noqa: INVALID000 # Succeeding comment
11 | # Mixed valid and invalid
@@ -108,7 +108,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID100
15 | # Test for preserving valid codes when fixing
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
help: Remove the rule code `INVALID100`
help: Remove the rule code
11 | # Mixed valid and invalid
12 | from typing import List # noqa: F401, INVALID123
13 | # Test for multiple invalid
@@ -128,7 +128,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID200
15 | # Test for preserving valid codes when fixing
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
help: Remove the rule code `INVALID200`
help: Remove the rule code
11 | # Mixed valid and invalid
12 | from typing import List # noqa: F401, INVALID123
13 | # Test for multiple invalid
@@ -148,7 +148,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID300
17 | # Test for mixed code types
18 | import json # noqa: E402, INVALID400, V100
|
help: Remove the rule code `INVALID300`
help: Remove the rule code
13 | # Test for multiple invalid
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
15 | # Test for preserving valid codes when fixing
@@ -168,7 +168,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID400
19 | # Test for rule redirects
20 | import pandas as pd # noqa: TCH002
|
help: Remove the rule code `INVALID400`
help: Remove the rule code
15 | # Test for preserving valid codes when fixing
16 | from itertools import chain # noqa: E402, INVALID300, F401
17 | # Test for mixed code types

View File

@@ -552,7 +552,7 @@ RUF102 [*] Invalid rule code in suppression: YF829
97 | # ruff: enable[YF829]
| -----
|
help: Remove the suppression comment
help: Remove the rule code
90 |
91 | def f():
92 | # Unknown rule codes
@@ -578,7 +578,7 @@ RUF102 [*] Invalid rule code in suppression: RQW320
| ------
97 | # ruff: enable[YF829]
|
help: Remove the rule code `RQW320`
help: Remove the rule code
91 | def f():
92 | # Unknown rule codes
93 | # ruff: disable[YF829]

View File

@@ -607,21 +607,13 @@ impl TryFrom<String> for RequiredVersion {
type Error = pep440_rs::VersionSpecifiersParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl FromStr for RequiredVersion {
type Err = pep440_rs::VersionSpecifiersParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
// Treat `0.3.1` as `==0.3.1`, for backwards compatibility.
if let Ok(version) = pep440_rs::Version::from_str(value) {
if let Ok(version) = pep440_rs::Version::from_str(&value) {
Ok(Self(VersionSpecifiers::from(
VersionSpecifier::equals_version(version),
)))
} else {
Ok(Self(VersionSpecifiers::from_str(value)?))
Ok(Self(VersionSpecifiers::from_str(&value)?))
}
}
}

View File

@@ -91,12 +91,6 @@ pub(crate) struct Suppression {
comments: DisableEnableComments,
}
impl Suppression {
fn codes(&self) -> &[TextRange] {
&self.comments.disable_comment().codes
}
}
#[derive(Debug)]
pub(crate) enum DisableEnableComments {
/// An implicitly closed disable comment without a matching enable comment.
@@ -207,7 +201,6 @@ impl Suppressions {
InvalidRuleCode {
rule_code: suppression.code.to_string(),
kind: InvalidRuleCodeKind::Suppression,
whole_comment: suppression.codes().len() == 1,
},
);
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.14.11"
version = "0.14.10"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -7,6 +7,7 @@ use std::collections::BTreeMap;
use std::env::VarError;
use std::num::{NonZeroU8, NonZeroU16};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{Context, Result, anyhow};
use glob::{GlobError, Paths, PatternError, glob};
@@ -35,7 +36,8 @@ use ruff_linter::settings::{
DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, TASK_TAGS, TargetVersion,
};
use ruff_linter::{
RuleSelector, fs, warn_user_once, warn_user_once_by_id, warn_user_once_by_message,
RUFF_PKG_VERSION, RuleSelector, fs, warn_user_once, warn_user_once_by_id,
warn_user_once_by_message,
};
use ruff_python_ast as ast;
use ruff_python_formatter::{
@@ -51,7 +53,6 @@ use crate::options::{
Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions,
McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions,
validate_required_version,
};
use crate::settings::{
EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, INCLUDE_PREVIEW, LineEnding,
@@ -154,7 +155,13 @@ pub struct Configuration {
impl Configuration {
pub fn into_settings(self, project_root: &Path) -> Result<Settings> {
if let Some(required_version) = &self.required_version {
validate_required_version(required_version)?;
let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION)
.expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
if !required_version.contains(&ruff_pkg_version) {
return Err(anyhow!(
"Required version `{required_version}` does not match the running version `{RUFF_PKG_VERSION}`"
));
}
}
let linter_target_version = TargetVersion(self.target_version);

View File

@@ -1,19 +1,15 @@
use anyhow::Result;
use regex::Regex;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use serde::de::{self};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::str::FromStr;
use strum::IntoEnumIterator;
use unicode_normalization::UnicodeNormalization;
use crate::settings::LineEnding;
use ruff_formatter::IndentStyle;
use ruff_graph::Direction;
use ruff_linter::RUFF_PKG_VERSION;
use ruff_linter::line_width::{IndentWidth, LineLength};
use ruff_linter::rules::flake8_import_conventions::settings::BannedAliases;
use ruff_linter::rules::flake8_pytest_style::settings::SettingsError;
@@ -560,17 +556,6 @@ pub struct LintOptions {
pub future_annotations: Option<bool>,
}
pub fn validate_required_version(required_version: &RequiredVersion) -> anyhow::Result<()> {
let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION)
.expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
if !required_version.contains(&ruff_pkg_version) {
return Err(anyhow::anyhow!(
"Required version `{required_version}` does not match the running version `{RUFF_PKG_VERSION}`"
));
}
Ok(())
}
/// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`].
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(transparent)]

View File

@@ -5,13 +5,12 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use log::debug;
use pep440_rs::{Operator, Version, VersionSpecifiers};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use ruff_linter::settings::types::{PythonVersion, RequiredVersion};
use ruff_linter::settings::types::PythonVersion;
use crate::options::{Options, validate_required_version};
use crate::options::Options;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools {
@@ -41,38 +40,20 @@ impl Pyproject {
}
}
fn parse_toml<P: AsRef<Path>, T: DeserializeOwned>(path: P, table_path: &[&str]) -> Result<T> {
/// Parse a `ruff.toml` file.
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
let path = path.as_ref();
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?;
// Parse the TOML document once into a spanned representation so we can:
// - Inspect `required-version` without triggering strict deserialization errors.
// - Deserialize with precise spans (line/column and excerpt) on errors.
let root = toml::de::DeTable::parse(&contents)
.with_context(|| format!("Failed to parse {}", path.display()))?;
check_required_version(root.get_ref(), table_path)?;
let deserializer = toml::de::Deserializer::from(root);
T::deserialize(deserializer)
.map_err(|mut err| {
// `Deserializer::from` doesn't have access to the original input, but we do.
// Attach it so TOML errors include line/column and a source excerpt.
err.set_input(Some(&contents));
err
})
.with_context(|| format!("Failed to parse {}", path.display()))
}
/// Parse a `ruff.toml` file.
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
parse_toml(path, &[])
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
}
/// Parse a `pyproject.toml` file.
fn parse_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
parse_toml(path, &["tool", "ruff"])
let path = path.as_ref();
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?;
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
}
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
@@ -117,33 +98,6 @@ pub fn find_settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
Ok(None)
}
fn check_required_version(value: &toml::de::DeTable, table_path: &[&str]) -> Result<()> {
let mut current = value;
for key in table_path {
let Some(next) = current.get(*key) else {
return Ok(());
};
let toml::de::DeValue::Table(next) = next.get_ref() else {
return Ok(());
};
current = next;
}
let required_version = current
.get("required-version")
.and_then(|value| value.get_ref().as_str());
let Some(required_version) = required_version else {
return Ok(());
};
// If it doesn't parse, we just fall through to normal parsing; it will give a nicer error message.
if let Ok(required_version) = required_version.parse::<RequiredVersion>() {
validate_required_version(&required_version)?;
}
Ok(())
}
/// Derive target version from `required-version` in `pyproject.toml`, if
/// such a file exists in an ancestor directory.
pub fn find_fallback_target_version<P: AsRef<Path>>(path: P) -> Option<PythonVersion> {

View File

@@ -102,8 +102,8 @@ impl Relativity {
#[derive(Debug)]
pub struct Resolver<'a> {
pyproject_config: &'a PyprojectConfig,
/// All [`Settings`] that have been added to the resolver, along with their config file paths.
settings: Vec<(Settings, PathBuf)>,
/// All [`Settings`] that have been added to the resolver.
settings: Vec<Settings>,
/// A router from path to index into the `settings` vector.
router: Router<usize>,
}
@@ -146,8 +146,8 @@ impl<'a> Resolver<'a> {
}
/// Add a resolved [`Settings`] under a given [`PathBuf`] scope.
fn add(&mut self, path: &Path, settings: Settings, config_path: PathBuf) {
self.settings.push((settings, config_path));
fn add(&mut self, path: &Path, settings: Settings) {
self.settings.push(settings);
// Normalize the path to use `/` separators and escape the '{' and '}' characters,
// which matchit uses for routing parameters.
@@ -172,27 +172,13 @@ impl<'a> Resolver<'a> {
/// Return the appropriate [`Settings`] for a given [`Path`].
pub fn resolve(&self, path: &Path) -> &Settings {
self.resolve_with_path(path).0
}
/// Return the appropriate [`Settings`] and config file path for a given [`Path`].
pub fn resolve_with_path(&self, path: &Path) -> (&Settings, Option<&Path>) {
match self.pyproject_config.strategy {
PyprojectDiscoveryStrategy::Fixed => (
&self.pyproject_config.settings,
self.pyproject_config.path.as_deref(),
),
PyprojectDiscoveryStrategy::Fixed => &self.pyproject_config.settings,
PyprojectDiscoveryStrategy::Hierarchical => self
.router
.at(path.to_slash_lossy().as_ref())
.map(|Match { value, .. }| {
let (settings, config_path) = &self.settings[*value];
(settings, Some(config_path.as_path()))
})
.unwrap_or((
&self.pyproject_config.settings,
self.pyproject_config.path.as_deref(),
)),
.map(|Match { value, .. }| &self.settings[*value])
.unwrap_or(&self.pyproject_config.settings),
}
}
@@ -269,8 +255,7 @@ impl<'a> Resolver<'a> {
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
pub fn settings(&self) -> impl Iterator<Item = &Settings> {
std::iter::once(&self.pyproject_config.settings)
.chain(self.settings.iter().map(|(settings, _)| settings))
std::iter::once(&self.pyproject_config.settings).chain(&self.settings)
}
}
@@ -394,17 +379,17 @@ pub fn resolve_configuration(
/// Extract the project root (scope) and [`Settings`] from a given
/// `pyproject.toml`.
fn resolve_scoped_settings(
pyproject: &Path,
fn resolve_scoped_settings<'a>(
pyproject: &'a Path,
transformer: &dyn ConfigurationTransformer,
origin: ConfigurationOrigin,
) -> Result<(PathBuf, Settings)> {
) -> Result<(&'a Path, Settings)> {
let relativity = Relativity::from(origin);
let configuration = resolve_configuration(pyproject, transformer, origin)?;
let project_root = relativity.resolve(pyproject);
let settings = configuration.into_settings(project_root)?;
Ok((project_root.to_path_buf(), settings))
Ok((project_root, settings))
}
/// Extract the [`Settings`] from a given `pyproject.toml` and process the
@@ -470,7 +455,7 @@ pub fn python_files_in_path<'a>(
transformer,
ConfigurationOrigin::Ancestor,
)?;
resolver.add(&root, settings, pyproject);
resolver.add(root, settings);
// We found the closest configuration.
break;
}
@@ -662,11 +647,7 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> {
ConfigurationOrigin::Ancestor,
) {
Ok((root, settings)) => {
self.global
.resolver
.write()
.unwrap()
.add(&root, settings, pyproject);
self.global.resolver.write().unwrap().add(root, settings);
}
Err(err) => {
self.local_error = Err(err);
@@ -786,7 +767,7 @@ pub fn python_file_at_path(
if let Some(pyproject) = settings_toml(ancestor)? {
let (root, settings) =
resolve_scoped_settings(&pyproject, transformer, ConfigurationOrigin::Unknown)?;
resolver.add(&root, settings, pyproject);
resolver.add(root, settings);
break;
}
}

View File

@@ -467,7 +467,7 @@ def test(): -> "int":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L54" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L50" target="_blank">View source</a>
</small>
@@ -1047,7 +1047,7 @@ class D(Generic[U, T]): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-ignore-comment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L79" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L75" target="_blank">View source</a>
</small>
@@ -2895,7 +2895,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
## `unused-ignore-comment`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unused-ignore-comment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L25" target="_blank">View source</a>
@@ -2904,11 +2904,11 @@ Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.
**What it does**
Checks for `ty: ignore` or `type: ignore` directives that are no longer applicable.
Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable.
**Why is this bad?**
A `ty: ignore` directive that no longer matches any diagnostic violations is likely
A `type: ignore` directive that no longer matches any diagnostic violations is likely
included by mistake, and should be removed to avoid confusion.
**Examples**
@@ -2923,11 +2923,6 @@ Use instead:
a = 20 / 2
```
**Options**
Set [`analysis.respect-type-ignore-comments`](https://docs.astral.sh/ty/reference/configuration/#respect-type-ignore-comments)
to `false` to prevent this rule from reporting unused `type: ignore` comments.
## `useless-overload-body`
<small>

View File

@@ -1,10 +1,9 @@
name,file,index,rank
auto-import-includes-modules,main.py,0,1
auto-import-includes-modules,main.py,1,2
auto-import-includes-modules,main.py,1,7
auto-import-includes-modules,main.py,2,1
auto-import-skips-current-module,main.py,0,1
class-arg-completion,main.py,0,1
exact-over-fuzzy,main.py,0,1
fstring-completions,main.py,0,1
higher-level-symbols-preferred,main.py,0,
higher-level-symbols-preferred,main.py,1,1
@@ -17,10 +16,8 @@ import-deprioritizes-type_check_only,main.py,3,2
import-deprioritizes-type_check_only,main.py,4,3
import-keyword-completion,main.py,0,1
internal-typeshed-hidden,main.py,0,2
local-over-auto-import,main.py,0,1
modules-over-other-symbols,main.py,0,1
none-completion,main.py,0,1
numpy-array,main.py,0,57
numpy-array,main.py,0,159
numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
object-attr-instance-methods,main.py,1,1
@@ -29,12 +26,7 @@ raise-uses-base-exception,main.py,0,1
scope-existing-over-new-import,main.py,0,1
scope-prioritize-closer,main.py,0,2
scope-simple-long-identifier,main.py,0,1
third-party-over-stdlib,main.py,0,1
tighter-over-looser-scope,main.py,0,3
tstring-completions,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,1
typing-gets-priority,main.py,0,1
typing-gets-priority,main.py,1,1
typing-gets-priority,main.py,2,1
typing-gets-priority,main.py,3,1
typing-gets-priority,main.py,4,1
ty-extensions-lower-stdlib,main.py,0,9
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,253
1 name file index rank
2 auto-import-includes-modules main.py 0 1
3 auto-import-includes-modules main.py 1 2 7
4 auto-import-includes-modules main.py 2 1
5 auto-import-skips-current-module main.py 0 1
6 class-arg-completion main.py 0 1
exact-over-fuzzy main.py 0 1
7 fstring-completions main.py 0 1
8 higher-level-symbols-preferred main.py 0
9 higher-level-symbols-preferred main.py 1 1
16 import-deprioritizes-type_check_only main.py 4 3
17 import-keyword-completion main.py 0 1
18 internal-typeshed-hidden main.py 0 2
local-over-auto-import main.py 0 1
modules-over-other-symbols main.py 0 1
19 none-completion main.py 0 1
20 numpy-array main.py 0 57 159
21 numpy-array main.py 1 1
22 object-attr-instance-methods main.py 0 1
23 object-attr-instance-methods main.py 1 1
26 scope-existing-over-new-import main.py 0 1
27 scope-prioritize-closer main.py 0 2
28 scope-simple-long-identifier main.py 0 1
third-party-over-stdlib main.py 0 1
tighter-over-looser-scope main.py 0 3
29 tstring-completions main.py 0 1
30 ty-extensions-lower-stdlib main.py 0 1 9
31 typing-gets-priority type-var-typing-over-ast main.py 0 1 3
32 typing-gets-priority type-var-typing-over-ast main.py 1 1 253
typing-gets-priority main.py 2 1
typing-gets-priority main.py 3 1
typing-gets-priority main.py 4 1

View File

@@ -540,17 +540,16 @@ fn copy_project(src_dir: &SystemPath, dst_dir: &SystemPath) -> anyhow::Result<Ve
std::fs::create_dir_all(dst_dir).with_context(|| dst_dir.to_string())?;
let mut cursors = vec![];
let it = walkdir::WalkDir::new(src_dir.as_std_path())
.into_iter()
.filter_entry(|dent| {
!dent
.file_name()
.to_str()
.is_some_and(|name| name.starts_with('.'))
});
for result in it {
for result in walkdir::WalkDir::new(src_dir.as_std_path()) {
let dent =
result.with_context(|| format!("failed to get directory entry from {src_dir}"))?;
if dent
.file_name()
.to_str()
.is_some_and(|name| name.starts_with('.'))
{
continue;
}
let src = SystemPath::from_std_path(dent.path()).ok_or_else(|| {
anyhow::anyhow!("path `{}` is not valid UTF-8", dent.path().display())

View File

@@ -1,3 +0,0 @@
pattern = 1
pttn = 1
pttn<CURSOR: pttn>

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = true

View File

@@ -1,11 +0,0 @@
def foo(x):
# We specifically want the local `x` to be
# suggested first here, and NOT an `x` or an
# `X` from some other module (via auto-import).
# We'd also like this to come before `except`,
# which is a keyword that contains `x`, but is
# not an exact match (where as `x` is). `except`
# also isn't legal in this context, although
# that sort of context sensitivity is a bit
# trickier.
return x<CURSOR: x>

View File

@@ -1,5 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -1,8 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = true

View File

@@ -1 +0,0 @@
os<CURSOR: os>

View File

@@ -1,5 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -1,78 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "regex"
version = "2025.11.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
{ url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
{ url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
{ url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
{ url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
{ url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
{ url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
{ url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
{ url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
{ url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
{ url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
{ url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
{ url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
{ url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
{ url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
{ url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
{ url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
{ url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
{ url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
{ url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
{ url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
{ url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
{ url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
{ url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
{ url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
{ url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
{ url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
{ url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
{ url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
{ url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
{ url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
{ url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
{ url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
{ url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
{ url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
{ url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
{ url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
{ url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
{ url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
{ url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
{ url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
{ url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
{ url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
{ url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
]
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "regex" },
]
[package.metadata]
requires-dist = [{ name = "regex", specifier = ">=2025.11.3" }]

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = true

View File

@@ -1,12 +0,0 @@
# This test was originally written to
# check that a third party dependency
# gets priority over stdlib. But it was
# not clearly the right choice[1].
#
# 2026-01-09: We stuck with it for now,
# but it seems likely that we'll want
# to regress this task in favor of
# another.
#
# [1]: https://github.com/astral-sh/ruff/pull/22460#discussion_r2676343225
fullma<CURSOR: regex.fullmatch>

View File

@@ -1,7 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"regex>=2025.11.3",
]

View File

@@ -1,78 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "regex"
version = "2025.11.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
{ url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
{ url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
{ url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
{ url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
{ url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
{ url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
{ url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
{ url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
{ url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
{ url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
{ url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
{ url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
{ url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
{ url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
{ url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
{ url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
{ url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
{ url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
{ url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
{ url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
{ url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
{ url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
{ url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
{ url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
{ url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
{ url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
{ url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
{ url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
{ url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
{ url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
{ url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
{ url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
{ url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
{ url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
{ url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
{ url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
{ url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
{ url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
{ url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
{ url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
{ url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
{ url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
{ url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
]
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "regex" },
]
[package.metadata]
requires-dist = [{ name = "regex", specifier = ">=2025.11.3" }]

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = true

View File

@@ -1,10 +0,0 @@
scope1 = 1
def x():
scope2 = 1
def xx():
scope3 = 1
def xxx():
# We specifically want `scope3` to be
# suggested first here, since that's in
# the "tighter" scope.
scope<CURSOR: scope3>

View File

@@ -1,5 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -1,8 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@@ -0,0 +1,12 @@
# This one demands that `TypeVa` complete to `typing.TypeVar`
# even though there is also an `ast.TypeVar`. Getting this one
# right seems tricky, and probably requires module-specific
# heuristics.
#
# ref: https://github.com/astral-sh/ty/issues/1274#issuecomment-3345884227
TypeVa<CURSOR: typing.TypeVar>
# This is a similar case of `ctypes.cast` being preferred over
# `typing.cast`. Maybe `typing` should just get a slightly higher
# weight than most other stdlib modules?
cas<CURSOR: typing.cast>

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = true

View File

@@ -1,19 +0,0 @@
# Many of these came from discussion in:
# <https://github.com/astral-sh/ty/issues/1274>
# We should prefer `typing` over `asyncio` here.
class Foo(Protoco<CURSOR: typing.Protocol>): ...
# We should prefer `typing` over `ty_extensions`
# or `typing_extensions`.
reveal_<CURSOR: typing.reveal_type>
# We should prefer `typing` over `ast`.
TypeVa<CURSOR: typing.TypeVar>
# We should prefer `typing` over `ctypes`.
cast<CURSOR: typing.cast>
# We should prefer a non-stdlib project import
# over a stdlib `typing` import.
NoRetur<CURSOR: sub1.NoReturn>

View File

@@ -1,5 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -1 +0,0 @@
NoReturn = 1

View File

@@ -1,8 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@@ -27,7 +27,6 @@ ty_project = { workspace = true, features = ["testing"] }
ty_python_semantic = { workspace = true }
ty_vendored = { workspace = true }
compact_str = { workspace = true }
get-size2 = { workspace = true }
itertools = { workspace = true }
rayon = { workspace = true }

View File

@@ -64,11 +64,7 @@ pub fn all_symbols<'db>(
continue;
}
s.spawn(move |_| {
let symbols_for_file_span = tracing::debug_span!(
parent: all_symbols_span,
"symbols_for_file_global_only",
path = %file.path(&*db),
);
let symbols_for_file_span = tracing::debug_span!(parent: all_symbols_span, "symbols_for_file_global_only", ?file);
let _entered = symbols_for_file_span.entered();
if query.is_match_symbol_name(module.name(&*db)) {
@@ -98,13 +94,11 @@ pub fn all_symbols<'db>(
let key1 = (
s1.name_in_file()
.unwrap_or_else(|| s1.module().name(db).as_str()),
s1.module().name(db).as_str(),
s1.file.path(db).as_str(),
);
let key2 = (
s2.name_in_file()
.unwrap_or_else(|| s2.module().name(db).as_str()),
s2.module().name(db).as_str(),
s2.file.path(db).as_str(),
);
key1.cmp(&key2)

File diff suppressed because it is too large Load Diff

View File

@@ -34,8 +34,8 @@ pub struct ParameterDetails<'db> {
pub name: String,
/// The parameter label in the signature (e.g., "param1: str")
pub label: String,
/// The annotated type of the parameter. If no annotation was provided, this is `Unknown`.
pub ty: Type<'db>,
/// The annotated type of the parameter, if any
pub ty: Option<Type<'db>>,
/// Documentation specific to the parameter, typically extracted from the
/// function's docstring
pub documentation: Option<String>,
@@ -237,7 +237,7 @@ fn create_parameters_from_offsets<'db>(
docstring: Option<&Docstring>,
parameter_names: &[String],
parameter_kinds: &[ParameterKind],
parameter_types: &[Type<'db>],
parameter_types: &[Option<Type<'db>>],
) -> Vec<ParameterDetails<'db>> {
// Extract parameter documentation from the function's docstring if available.
let param_docs = if let Some(docstring) = docstring {
@@ -264,11 +264,12 @@ fn create_parameters_from_offsets<'db>(
parameter_kinds.get(i),
Some(ParameterKind::PositionalOnly { .. })
);
let ty = parameter_types.get(i).copied().flatten();
ParameterDetails {
name: param_name.to_string(),
label,
ty: parameter_types[i],
ty,
documentation: param_docs.get(param_name).cloned(),
is_positional_only,
}

View File

@@ -325,6 +325,10 @@ impl ModulePath {
relative_path: relative_path.with_extension("py"),
})
}
pub(crate) fn into_search_path(self) -> SearchPath {
self.search_path
}
}
impl PartialEq<SystemPathBuf> for ModulePath {
@@ -554,17 +558,10 @@ impl SearchPath {
)
}
/// Is this search path in "first party" code? i.e., The
/// end user's project code.
pub fn is_first_party(&self) -> bool {
matches!(&*self.0, SearchPathInner::FirstParty(_))
}
/// Is the module in a site-packages directory?
pub fn is_site_packages(&self) -> bool {
matches!(&*self.0, SearchPathInner::SitePackages(_))
}
fn is_valid_extension(&self, extension: &str) -> bool {
if self.is_standard_library() {
extension == "pyi"

View File

@@ -32,11 +32,8 @@ specifies ty's implementation of Python's import resolution algorithm.
*/
use std::borrow::Cow;
use std::fmt;
use std::iter::FusedIterator;
use std::str::Split;
use compact_str::format_compact;
use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_db::files::{File, FilePath, FileRootKind};
@@ -1102,6 +1099,79 @@ fn desperately_resolve_name(
resolve_name_impl(db, name, mode, search_paths.iter().flatten())
}
#[derive(Debug, Clone, Copy)]
enum ResolvedModule {
NamespacePackage,
LegacyNamespacePackage(File),
RegularPackage(File),
Module(File),
}
#[derive(Debug, Clone)]
struct ModuleResolutionCandidate {
path: ModulePath,
module: ResolvedModule,
py_typed: PyTyped,
}
impl ModuleResolutionCandidate {
// Is this some kind of namespace package?
fn is_any_namespace_package(&self) -> bool {
match self.module {
ResolvedModule::NamespacePackage => true,
ResolvedModule::LegacyNamespacePackage(_) => true,
ResolvedModule::RegularPackage(_) => false,
ResolvedModule::Module(_) => false,
}
}
// This is the module we were actually interested in resolving, complete the resolution
fn into_resolved_name(self) -> ResolvedName {
match self.module {
ResolvedModule::NamespacePackage => ResolvedName::NamespacePackage,
// legacy namespace packages behave like regular packages when they're the target of the resolution
ResolvedModule::LegacyNamespacePackage(file) => {
ResolvedName::FileModule(ResolvedFileModule {
kind: ModuleKind::Package,
search_path: self.path.into_search_path(),
file,
})
}
ResolvedModule::RegularPackage(file) => ResolvedName::FileModule(ResolvedFileModule {
kind: ModuleKind::Package,
search_path: self.path.into_search_path(),
file,
}),
ResolvedModule::Module(file) => ResolvedName::FileModule(ResolvedFileModule {
kind: ModuleKind::Module,
search_path: self.path.into_search_path(),
file,
}),
}
}
fn missing_submodule_is_terminal(&self) -> bool {
if matches!(self.py_typed, PyTyped::Partial) {
return false;
}
// Only regular packages are truly terminal, as a later `foo/__init__.py`
// can shadow `foo.py`. Both shadow namespace packages.
matches!(self.module, ResolvedModule::RegularPackage(_))
}
fn to_str<'a>(&self, db: &'a dyn Db) -> Cow<'a, str> {
match self.module {
ResolvedModule::NamespacePackage => {
Cow::Owned(self.path.to_system_path().unwrap_or_default().to_string())
}
ResolvedModule::LegacyNamespacePackage(file) => Cow::Borrowed(file.path(db).as_str()),
ResolvedModule::RegularPackage(file) => Cow::Borrowed(file.path(db).as_str()),
ResolvedModule::Module(file) => Cow::Borrowed(file.path(db).as_str()),
}
}
}
fn resolve_name_impl<'a>(
db: &dyn Db,
name: &ModuleName,
@@ -1109,109 +1179,250 @@ fn resolve_name_impl<'a>(
search_paths: impl Iterator<Item = &'a SearchPath>,
) -> Option<ResolvedName> {
let python_version = db.python_version();
let resolver_state = ResolverContext::new(db, python_version, mode);
let context = ResolverContext::new(db, python_version, mode);
let is_non_shadowable = mode.is_non_shadowable(python_version.minor, name.as_str());
let mut stub_name = None;
let name = RelaxedModuleName::new(name);
let stub_name = name.to_stub_package();
let mut is_namespace_package = false;
let mut cur_candidates = search_paths
.filter_map(|search_path| {
// When a builtin module is imported, standard module resolution is bypassed:
// the module name always resolves to the stdlib module,
// even if there's a module of the same name in the first-party root
// (which would normally result in the stdlib module being overridden).
// TODO: offer a diagnostic if there is a first-party module of the same name
if is_non_shadowable && !search_path.is_standard_library() {
return None;
}
for search_path in search_paths {
// When a builtin module is imported, standard module resolution is bypassed:
// the module name always resolves to the stdlib module,
// even if there's a module of the same name in the first-party root
// (which would normally result in the stdlib module being overridden).
// TODO: offer a diagnostic if there is a first-party module of the same name
if is_non_shadowable && !search_path.is_standard_library() {
continue;
}
Some(ModuleResolutionCandidate {
path: search_path.to_module_path(),
module: ResolvedModule::NamespacePackage,
py_typed: PyTyped::Untyped,
})
})
.collect::<Vec<_>>();
let mut next_candidates = vec![];
if !search_path.is_standard_library() && resolver_state.mode.stubs_allowed() {
match resolve_name_in_search_path(&resolver_state, &stub_name, search_path) {
Ok((package_kind, _, ResolvedName::FileModule(module))) => {
if package_kind.is_root() && module.kind.is_module() {
// FIXME?: because we have to search every candidate on each step of this loop,
// in theory we can search them all in parallel. However we need to join the parallelism
// at the end of each iteration, and after the first iteration in 99% of cases we will have
// reduced down to a single candidate, so maybe meh?
let mut is_root = true;
for component in name.components() {
// Search for the next component in every search-path
for mut candidate in cur_candidates.drain(..) {
// On the first iteration, look for `mypackage-stubs` as well
// Optimization: stdlib never has these `-stubs`
if is_root
&& context.mode.stubs_allowed()
&& !candidate.path.search_path().is_standard_library()
{
let stub_name = stub_name.get_or_insert_with(|| format!("{component}-stubs"));
let mut stub_candidate = candidate.clone();
if resolve_name_in_search_path(&context, &mut stub_candidate, stub_name).is_ok() {
// `mypackage-stubs.py(i)` is not a valid result
if matches!(stub_candidate.module, ResolvedModule::Module(_)) {
tracing::trace!(
"Search path `{search_path}` contains a module \
named `{stub_name}` but a standalone module isn't a valid stub."
"Search path `{}` contains a module \
named `{stub_name}` but a standalone module isn't a valid stub.",
candidate.path.search_path()
);
} else {
return Some(ResolvedName::FileModule(module));
let shadows_all = stub_candidate.missing_submodule_is_terminal();
next_candidates.push(stub_candidate);
if shadows_all {
break;
}
}
}
Ok((_, _, ResolvedName::NamespacePackage)) => {
is_namespace_package = true;
}
Err((PackageKind::Root, _)) => {
tracing::trace!(
"Search path `{search_path}` contains no stub package named `{stub_name}`."
);
}
Err((PackageKind::Regular, PyTyped::Partial)) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: \
`{name}` but it is a partial package, keep going."
);
// stub exists, but the module doesn't. But this is a partial package,
// fall through to looking for a non-stub package
}
Err((PackageKind::Regular, _)) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: `{name}`"
);
// stub exists, but the module doesn't.
return None;
}
Err((PackageKind::Namespace, _)) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: \
`{name}` but it is a namespace package, keep going."
);
// stub exists, but the module doesn't. But this is a namespace package,
// fall through to looking for a non-stub package
}
if resolve_name_in_search_path(&context, &mut candidate, component).is_err() {
if candidate.missing_submodule_is_terminal() {
// Everything after this package should be shadowed out by this failure
// But the previous results are still in play because they would have
// shadowed this one out anyway.
break;
}
continue;
}
let shadows_all = candidate.missing_submodule_is_terminal();
next_candidates.push(candidate);
if shadows_all {
break;
}
}
match resolve_name_in_search_path(&resolver_state, &name, search_path) {
Ok((_, _, ResolvedName::FileModule(module))) => {
return Some(ResolvedName::FileModule(module));
// Now that we have several candidates, we need to reject candidates that are shadowed.
// There are only two valid situations where we should proceed into the next iteration
// with multiple candidates:
//
// * All the candidates are namespace packages
// * `mypackage-stubs` is a candidate with `PyTyped::Partial`
//
// The existence of a single non-namespace package will shadow
// all namespace packages *regardless of search-path order*.
//
// Similarly, the existence of a single regular package will shadow
// all modules (mymod.py) *regardless of search-path order*.
//
// This is implemented with the `retain` that follows.
//
// We can't do this "delete all namespace packages" eagerly because we want a
// `PyTyped::Partial` regular package to shadow namespace packages after it.
// (FIXME: I guess we could just set a flag not to add them...)
// First record what kinds of things we found
let mut found_regular_package = None;
let mut found_module = None;
let mut found_legacy_namespace_package = None;
for candidate in &next_candidates {
match (candidate.module, candidate.py_typed) {
(ResolvedModule::LegacyNamespacePackage(file), _) => {
found_legacy_namespace_package = Some(file);
}
(ResolvedModule::RegularPackage(file), PyTyped::Untyped | PyTyped::Full) => {
found_regular_package = Some(file);
}
(ResolvedModule::Module(file), PyTyped::Untyped | PyTyped::Full) => {
found_module = Some(file);
}
_ => {}
}
Ok((_, _, ResolvedName::NamespacePackage)) => {
is_namespace_package = true;
}
next_candidates.retain(|candidate| {
if let Some(_legacy) = found_legacy_namespace_package && !matches!(candidate.module, ResolvedModule::LegacyNamespacePackage(_)) {
// TODO: it would be nice to emit a warning about this but we just assume it's fine
}
// Regular packages shadow anything that isn't a regular package independent of order
if let Some(package) = found_regular_package && !matches!(candidate.module, ResolvedModule::RegularPackage(_)) {
tracing::trace!("Discarding namespace package `{}` because a regular package of the same name was found: {}",
candidate.to_str(db),
package.path(db).as_str(),
);
return false;
}
// Modules shadow namespace packages independent of order
if let Some(module) = found_module && candidate.is_any_namespace_package() {
tracing::trace!("Discarding namespace package `{}` because a module of the same name was found: {}",
candidate.to_str(db),
module.path(db).as_str(),
);
return false;
}
true
});
if next_candidates.is_empty() {
return None;
}
// Advance to the next level of candidates while reusing allocations
// (we used `drain` so cur_candidates is empty)
std::mem::swap(&mut cur_candidates, &mut next_candidates);
is_root = false;
}
// We now have a list of candidates that are all correct answers, and we just need to take the
// Best one. Because of the filtering we've done in the loop, and sorting stub-packages to come
// first, this is in fact just "the first one".
cur_candidates
.into_iter()
.next()
.map(ModuleResolutionCandidate::into_resolved_name)
}
/// Attempts to resolve a module name in a particular search path.
///
/// `search_path` should be the directory to start looking for the module.
///
/// `name` should be a complete non-empty module name, e.g, `foo` or
/// `foo.bar.baz`.
///
/// Upon success, this returns the kind of the parent package (root, regular
/// package or namespace package) along with the resolved details of the
/// module: its kind (single-file module or package), the search path in
/// which it was found (guaranteed to be equal to the one given) and the
/// corresponding `File`.
///
/// Upon error, the kind of the parent package is returned.
fn resolve_name_in_search_path(
context: &ResolverContext,
candidate: &mut ModuleResolutionCandidate,
module_name: &str,
) -> Result<(), ()> {
if matches!(candidate.module, ResolvedModule::Module(_)) {
tracing::trace!(
"The non-package {} cannot have child",
candidate.to_str(context.db)
);
return Err(());
}
let package_path = &mut candidate.path;
package_path.push(module_name);
// Check for a regular package first (highest priority)
package_path.push("__init__");
if let Some(init) = resolve_file_module(package_path, context) {
// Remove the `__init__` component for any potential next step
package_path.pop();
candidate.py_typed = package_path
.py_typed(context)
.inherit_parent(candidate.py_typed);
if is_legacy_namespace_package(package_path, context, init) {
candidate.module = ResolvedModule::LegacyNamespacePackage(init);
} else {
candidate.module = ResolvedModule::RegularPackage(init);
}
return Ok(());
}
// Check for a file module next
package_path.pop();
if let Some(file_module) = resolve_file_module(package_path, context) {
candidate.module = ResolvedModule::Module(file_module);
return Ok(());
}
// Last resort, check if a folder with the given name exists. If so,
// then this is a namespace package. We need to skip this check for
// typeshed because the `resolve_file_module` can also return `None` if the
// `__init__.py` exists but isn't available for the current Python version.
// Let's assume that the `xml` module is only available on Python 3.11+ and
// we're resolving for Python 3.10:
//
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
// the file exists but the module isn't available for the current Python
// version.
// * The check here would now return `true` because the `xml` directory
// exists, resulting in a false positive for a namespace package.
//
// Since typeshed doesn't use any namespace packages today (May 2025),
// simply skip this check which also helps performance. If typeshed
// ever uses namespace packages, ensure that this check also takes the
// `VERSIONS` file into consideration.
if !package_path.search_path().is_standard_library() && package_path.is_directory(context) {
if let Some(path) = package_path.to_system_path() {
let system = context.db.system();
if system.case_sensitivity().is_case_sensitive()
|| system.path_exists_case_sensitive(
&path,
package_path.search_path().as_system_path().unwrap(),
)
{
candidate.py_typed = package_path
.py_typed(context)
.inherit_parent(candidate.py_typed);
candidate.module = ResolvedModule::NamespacePackage;
return Ok(());
}
Err(kind) => match kind {
(PackageKind::Root, _) => {
tracing::trace!(
"Search path `{search_path}` contains no package named `{name}`."
);
}
(PackageKind::Regular, PyTyped::Partial) => {
tracing::trace!(
"Package in `{search_path}` doesn't contain module: \
`{name}` but it is a partial package, keep going."
);
}
(PackageKind::Regular, _) => {
// For regular packages, don't search the next search path. All files of that
// package must be in the same location
tracing::trace!("Package in `{search_path}` doesn't contain module: `{name}`");
return None;
}
(PackageKind::Namespace, _) => {
tracing::trace!(
"Package in `{search_path}` doesn't contain module: \
`{name}` but it is a namespace package, keep going."
);
}
},
}
}
if is_namespace_package {
return Some(ResolvedName::NamespacePackage);
}
None
Err(())
}
#[derive(Debug)]
@@ -1234,101 +1445,6 @@ struct ResolvedFileModule {
file: File,
}
/// Attempts to resolve a module name in a particular search path.
///
/// `search_path` should be the directory to start looking for the module.
///
/// `name` should be a complete non-empty module name, e.g, `foo` or
/// `foo.bar.baz`.
///
/// Upon success, this returns the kind of the parent package (root, regular
/// package or namespace package) along with the resolved details of the
/// module: its kind (single-file module or package), the search path in
/// which it was found (guaranteed to be equal to the one given) and the
/// corresponding `File`.
///
/// Upon error, the kind of the parent package is returned.
fn resolve_name_in_search_path(
context: &ResolverContext,
name: &RelaxedModuleName,
search_path: &SearchPath,
) -> Result<(PackageKind, PyTyped, ResolvedName), (PackageKind, PyTyped)> {
let mut components = name.components();
let module_name = components.next_back().unwrap();
let resolved_package = resolve_package(search_path, components, context)?;
let mut package_path = resolved_package.path;
package_path.push(module_name);
// Check for a regular package first (highest priority)
package_path.push("__init__");
if let Some(regular_package) = resolve_file_module(&package_path, context) {
return Ok((
resolved_package.kind,
resolved_package.typed,
ResolvedName::FileModule(ResolvedFileModule {
search_path: search_path.clone(),
kind: ModuleKind::Package,
file: regular_package,
}),
));
}
// Check for a file module next
package_path.pop();
if let Some(file_module) = resolve_file_module(&package_path, context) {
return Ok((
resolved_package.kind,
resolved_package.typed,
ResolvedName::FileModule(ResolvedFileModule {
file: file_module,
kind: ModuleKind::Module,
search_path: search_path.clone(),
}),
));
}
// Last resort, check if a folder with the given name exists. If so,
// then this is a namespace package. We need to skip this check for
// typeshed because the `resolve_file_module` can also return `None` if the
// `__init__.py` exists but isn't available for the current Python version.
// Let's assume that the `xml` module is only available on Python 3.11+ and
// we're resolving for Python 3.10:
//
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
// the file exists but the module isn't available for the current Python
// version.
// * The check here would now return `true` because the `xml` directory
// exists, resulting in a false positive for a namespace package.
//
// Since typeshed doesn't use any namespace packages today (May 2025),
// simply skip this check which also helps performance. If typeshed
// ever uses namespace packages, ensure that this check also takes the
// `VERSIONS` file into consideration.
if !search_path.is_standard_library() && package_path.is_directory(context) {
if let Some(path) = package_path.to_system_path() {
let system = context.db.system();
if system.case_sensitivity().is_case_sensitive()
|| system.path_exists_case_sensitive(
&path,
package_path.search_path().as_system_path().unwrap(),
)
{
return Ok((
resolved_package.kind,
resolved_package.typed,
ResolvedName::NamespacePackage,
));
}
}
}
Err((resolved_package.kind, resolved_package.typed))
}
/// If `module` exists on disk with either a `.pyi` or `.py` extension,
/// return the [`File`] corresponding to that path.
///
@@ -1366,90 +1482,6 @@ pub(super) fn resolve_file_module(
Some(file)
}
/// Attempt to resolve the parent package of a module.
///
/// `module_search_path` should be the directory to start looking for the
/// parent package.
///
/// `components` should be the full module name of the parent package. This
/// specifically should not include the basename of the module. So e.g.,
/// for `foo.bar.baz`, `components` should be `[foo, bar]`. It follows that
/// `components` may be empty (in which case, the parent package is the root).
///
/// Upon success, the path to the package and its "kind" (root, regular or
/// namespace) is returned. Upon error, the kind of the package is still
/// returned based on how many components were found and whether `__init__.py`
/// is present.
fn resolve_package<'a, 'db, I>(
module_search_path: &SearchPath,
components: I,
resolver_state: &ResolverContext<'db>,
) -> Result<ResolvedPackage, (PackageKind, PyTyped)>
where
I: Iterator<Item = &'a str>,
{
let mut package_path = module_search_path.to_module_path();
// `true` if inside a folder that is a namespace package (has no `__init__.py`).
// Namespace packages are special because they can be spread across multiple search paths.
// https://peps.python.org/pep-0420/
let mut in_namespace_package = false;
// `true` if resolving a sub-package. For example, `true` when resolving `bar` of `foo.bar`.
let mut in_sub_package = false;
let mut typed = package_path.py_typed(resolver_state);
// For `foo.bar.baz`, test that `foo` and `bar` both contain a `__init__.py`.
for folder in components {
package_path.push(folder);
typed = package_path.py_typed(resolver_state).inherit_parent(typed);
let is_regular_package = package_path.is_regular_package(resolver_state);
if is_regular_package {
// This is the only place where we need to consider the existence of legacy namespace
// packages, as we are explicitly searching for the *parent* package of the module
// we actually want. Here, such a package should be treated as a PEP-420 ("modern")
// namespace package. In all other contexts it acts like a normal package and needs
// no special handling.
in_namespace_package = is_legacy_namespace_package(&package_path, resolver_state);
} else if package_path.is_directory(resolver_state)
// Pure modules hide namespace packages with the same name
&& resolve_file_module(&package_path, resolver_state).is_none()
{
// A directory without an `__init__.py(i)` is a namespace package,
// continue with the next folder.
in_namespace_package = true;
} else if in_namespace_package {
// Package not found but it is part of a namespace package.
return Err((PackageKind::Namespace, typed));
} else if in_sub_package {
// A regular sub package wasn't found.
return Err((PackageKind::Regular, typed));
} else {
// We couldn't find `foo` for `foo.bar.baz`, search the next search path.
return Err((PackageKind::Root, typed));
}
in_sub_package = true;
}
let kind = if in_namespace_package {
PackageKind::Namespace
} else if in_sub_package {
PackageKind::Regular
} else {
PackageKind::Root
};
Ok(ResolvedPackage {
kind,
path: package_path,
typed,
})
}
/// Determines whether a package is a legacy namespace package.
///
/// Before PEP 420 introduced implicit namespace packages, the ecosystem developed
@@ -1479,19 +1511,14 @@ where
/// we will just get confused if you mess it up).
fn is_legacy_namespace_package(
package_path: &ModulePath,
resolver_state: &ResolverContext,
context: &ResolverContext,
init: File,
) -> bool {
// Just an optimization, the stdlib and typeshed are never legacy namespace packages
if package_path.search_path().is_standard_library() {
return false;
}
let mut package_path = package_path.clone();
package_path.push("__init__");
let Some(init) = resolve_file_module(&package_path, resolver_state) else {
return false;
};
// This is all syntax-only analysis so it *could* be fooled but it's really unlikely.
//
// The benefit of being syntax-only is speed and avoiding circular dependencies
@@ -1499,44 +1526,13 @@ fn is_legacy_namespace_package(
//
// The downside is if you write slightly different syntax we will fail to detect the idiom,
// but hey, this is better than nothing!
let parsed = ruff_db::parsed::parsed_module(resolver_state.db, init);
let parsed = ruff_db::parsed::parsed_module(context.db, init);
let mut visitor = LegacyNamespacePackageVisitor::default();
visitor.visit_body(parsed.load(resolver_state.db).suite());
visitor.visit_body(parsed.load(context.db).suite());
visitor.is_legacy_namespace_package
}
#[derive(Debug)]
struct ResolvedPackage {
path: ModulePath,
kind: PackageKind,
typed: PyTyped,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum PackageKind {
/// A root package or module. E.g. `foo` in `foo.bar.baz` or just `foo`.
Root,
/// A regular sub-package where the parent contains an `__init__.py`.
///
/// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`.
Regular,
/// A sub-package in a namespace package. A namespace package is a package
/// without an `__init__.py`.
///
/// For example, `bar` in `foo.bar` if the `foo` directory contains no
/// `__init__.py`.
Namespace,
}
impl PackageKind {
pub(crate) const fn is_root(self) -> bool {
matches!(self, PackageKind::Root)
}
}
/// Info about the `py.typed` file for this package
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub(crate) enum PyTyped {
@@ -1587,34 +1583,6 @@ impl<'db> ResolverContext<'db> {
}
}
/// A [`ModuleName`] but with relaxed semantics to allow `<package>-stubs.path`
#[derive(Debug)]
struct RelaxedModuleName(compact_str::CompactString);
impl RelaxedModuleName {
fn new(name: &ModuleName) -> Self {
Self(name.as_str().into())
}
fn components(&self) -> Split<'_, char> {
self.0.split('.')
}
fn to_stub_package(&self) -> Self {
if let Some((package, rest)) = self.0.split_once('.') {
Self(format_compact!("{package}-stubs.{rest}"))
} else {
Self(format_compact!("{package}-stubs", package = self.0))
}
}
}
impl fmt::Display for RelaxedModuleName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// Detects if a module contains a statement of the form:
/// ```python
/// __path__ = pkgutil.extend_path(__path__, __name__)
@@ -1926,14 +1894,12 @@ mod tests {
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
functools: 3.8- # Top-level single-file module
xml: 3.8-3.8 # Namespace package on py38 only
";
const STDLIB: &[FileSpec] = &[
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("functools.pyi", ""),
("xml/etree.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
@@ -1946,7 +1912,7 @@ mod tests {
.with_python_version(PythonVersion::PY38)
.build();
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
let existing_modules = create_module_names(&["asyncio", "functools"]);
for module_name in existing_modules {
let resolved_module =
resolve_module_confident(&db, &module_name).unwrap_or_else(|| {
@@ -1970,16 +1936,12 @@ mod tests {
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
collections: 3.9- # 'Regular' package on py39+
importlib: 3.9- # Namespace package on py39+
xml: 3.8-3.8 # Namespace package on 3.8 only
";
const STDLIB: &[FileSpec] = &[
("collections/__init__.pyi", ""),
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("importlib/abc.pyi", ""),
("xml/etree.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
@@ -1992,13 +1954,7 @@ mod tests {
.with_python_version(PythonVersion::PY38)
.build();
let nonexisting_modules = create_module_names(&[
"collections",
"importlib",
"importlib.abc",
"xml",
"asyncio.tasks",
]);
let nonexisting_modules = create_module_names(&["collections", "asyncio.tasks"]);
for module_name in nonexisting_modules {
assert!(
@@ -2015,7 +1971,6 @@ mod tests {
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
collections: 3.9- # 'Regular' package on py39+
functools: 3.8- # Top-level single-file module
importlib: 3.9- # Namespace package on py39+
";
const STDLIB: &[FileSpec] = &[
@@ -2023,7 +1978,6 @@ mod tests {
("asyncio/tasks.pyi", ""),
("collections/__init__.pyi", ""),
("functools.pyi", ""),
("importlib/abc.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
@@ -2036,13 +1990,8 @@ mod tests {
.with_python_version(PythonVersion::PY39)
.build();
let existing_modules = create_module_names(&[
"asyncio",
"functools",
"importlib.abc",
"collections",
"asyncio.tasks",
]);
let existing_modules =
create_module_names(&["asyncio", "functools", "collections", "asyncio.tasks"]);
for module_name in existing_modules {
let resolved_module =
@@ -2444,7 +2393,7 @@ mod tests {
fn adding_file_to_search_path_with_lower_priority_does_not_invalidate_query() {
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "functools: 3.8-",
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
stdlib_files: &[("functools/__init__.pyi", "def update_wrapper(): ...")],
};
let TestCase {
@@ -2458,7 +2407,7 @@ mod tests {
.build();
let functools_module_name = ModuleName::new_static("functools").unwrap();
let stdlib_functools_path = stdlib.join("functools.pyi");
let stdlib_functools_path = stdlib.join("functools/__init__.pyi");
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
@@ -2470,7 +2419,7 @@ mod tests {
// Adding a file to site-packages does not invalidate the query,
// since site-packages takes lower priority in the module resolution
db.clear_salsa_events();
let site_packages_functools_path = site_packages.join("functools.py");
let site_packages_functools_path = site_packages.join("functools/__init__.py");
db.write_file(&site_packages_functools_path, "f: int")
.unwrap();
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();

View File

@@ -40,22 +40,25 @@ impl ExcludeFilter {
}
fn matches(&self, path: &SystemPath, mode: GlobFilterCheckMode, directory: bool) -> bool {
// If the path is excluded, return `ignore`
if self.ignore.matched(path, directory).is_ignore() {
return true;
}
match mode {
GlobFilterCheckMode::TopDown => {
// No hit or an allow hit means the file or directory is not excluded.
false
match self.ignore.matched(path, directory) {
// No hit or an allow hit means the file or directory is not excluded.
Match::None | Match::Allow => false,
Match::Ignore => true,
}
}
GlobFilterCheckMode::Adhoc => {
// If the path is allowlisted or there's no hit, try the parent to ensure we don't return false
// for a folder where there's an exclude for a parent.
path.ancestors()
.skip(1)
.any(|ancestor| self.ignore.matched(ancestor, true).is_ignore())
for ancestor in path.ancestors() {
match self.ignore.matched(ancestor, directory) {
// If the path is allowlisted or there's no hit, try the parent to ensure we don't return false
// for a folder where there's an exclude for a parent.
Match::None | Match::Allow => {}
Match::Ignore => return true,
}
}
false
}
}
}
@@ -121,7 +124,7 @@ struct Gitignore {
set: GlobSet,
globs: Vec<IgnoreGlob>,
#[get_size(ignore)]
matches: Arc<Pool<Vec<usize>>>,
matches: Option<Arc<Pool<Vec<usize>>>>,
}
impl Gitignore {
@@ -137,7 +140,7 @@ impl Gitignore {
return Match::None;
}
let mut matches = self.matches.get();
let mut matches = self.matches.as_ref().unwrap().get();
let candidate = Candidate::new(path);
self.set.matches_candidate_into(&candidate, &mut matches);
for &i in matches.iter().rev() {
@@ -184,12 +187,6 @@ enum Match {
Allow,
}
impl Match {
const fn is_ignore(self) -> bool {
matches!(self, Match::Ignore)
}
}
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
struct IgnoreGlob {
/// The pattern that was originally parsed.
@@ -235,7 +232,7 @@ impl GitignoreBuilder {
Ok(Gitignore {
set,
globs: self.globs.clone(),
matches: Arc::new(Pool::new(Vec::new)),
matches: Some(Arc::new(Pool::new(Vec::new))),
})
}

View File

@@ -1,11 +1,9 @@
use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
use regex_automata::dfa;
use regex_automata::dfa::Automaton;
use regex_automata::util::pool::Pool;
use ruff_db::system::SystemPath;
use std::fmt::Formatter;
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
use std::sync::Arc;
use tracing::warn;
use crate::glob::portable::AbsolutePortableGlobPattern;
@@ -39,8 +37,6 @@ pub(crate) struct IncludeFilter {
literal_pattern_indices: Box<[usize]>,
#[get_size(size_fn = dfa_memory_usage)]
dfa: Option<dfa::dense::DFA<Vec<u32>>>,
#[get_size(ignore)]
matches: Arc<Pool<Vec<usize>>>,
}
#[allow(clippy::ref_option)]
@@ -61,14 +57,13 @@ impl IncludeFilter {
};
}
let mut matches = self.matches.get();
self.glob_set.matches_into(path, &mut matches);
let matches = self.glob_set.matches(path);
if matches.is_empty() {
MatchFile::No
} else {
for match_index in matches.iter() {
if self.literal_pattern_indices.contains(match_index) {
for match_index in matches {
if self.literal_pattern_indices.contains(&match_index) {
return MatchFile::Literal;
}
}
@@ -296,7 +291,6 @@ impl IncludeFilterBuilder {
dfa,
literal_pattern_indices: self.literal_pattern_indices.into(),
original_patterns: self.original_patterns.into(),
matches: Arc::new(Pool::new(Vec::new)),
})
}
}

View File

@@ -1292,7 +1292,7 @@ pub struct AnalysisOptions {
respect-type-ignore-comments = false
"#
)]
pub respect_type_ignore_comments: Option<bool>,
respect_type_ignore_comments: Option<bool>,
}
impl AnalysisOptions {

View File

@@ -359,51 +359,6 @@ reveal_type(GenericCircle[int].bar()) # revealed: GenericCircle[int]
reveal_type(GenericCircle.baz(1)) # revealed: GenericShape[Literal[1]]
```
### Calling `super()` in overridden methods with `Self` return type
This is a regression test for <https://github.com/astral-sh/ty/issues/2122>.
When a child class overrides a parent method with a `Self` return type and calls `super().method()`,
the return type should be the child's `Self` type variable, not the concrete child class type.
```py
from typing import Self
class Parent:
def copy(self) -> Self:
return self
class Child(Parent):
def copy(self) -> Self:
result = super().copy()
reveal_type(result) # revealed: Self@copy
return result
# When called on concrete types, Self is substituted correctly.
reveal_type(Child().copy()) # revealed: Child
```
The same applies to classmethods with `Self` return types:
```py
from typing import Self
class Parent:
@classmethod
def create(cls) -> Self:
return cls()
class Child(Parent):
@classmethod
def create(cls) -> Self:
result = super().create()
reveal_type(result) # revealed: Self@create
return result
# When called on concrete types, Self is substituted correctly.
reveal_type(Child.create()) # revealed: Child
```
## Attributes
TODO: The use of `Self` to annotate the `next_node` attribute should be

View File

@@ -117,15 +117,3 @@ def _():
result = yield from retrieve().__await__()
reveal_type(result) # revealed: int
```
## Un-annotated async functions
An `async def` with no annotated return type is still known to return `CoroutineType` of `Unknown`,
not just `Unknown`:
```py
async def f():
pass
reveal_type(f()) # revealed: CoroutineType[Any, Any, Unknown]
```

File diff suppressed because one or more lines are too long

View File

@@ -168,13 +168,13 @@ class A:
class B(A):
def __init__(self, a: int):
reveal_type(super()) # revealed: <super: <class 'B'>, Self@__init__>
reveal_type(super()) # revealed: <super: <class 'B'>, B>
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
super().__init__(a)
@classmethod
def f(cls):
reveal_type(super()) # revealed: <super: <class 'B'>, type[Self@f]>
reveal_type(super()) # revealed: <super: <class 'B'>, <class 'B'>>
super().f()
super(B, B(42)).__init__(42)
@@ -229,16 +229,16 @@ class Foo[T]:
reveal_type(super())
def method4(self: Self):
# revealed: <super: <class 'Foo'>, Self@method4>
# revealed: <super: <class 'Foo'>, Foo[T@Foo]>
reveal_type(super())
def method5[S: Foo[int]](self: S, other: S) -> S:
# revealed: <super: <class 'Foo'>, S@method5>
# revealed: <super: <class 'Foo'>, Foo[int]>
reveal_type(super())
return self
def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
# revealed: <super: <class 'Foo'>, S@method6> | <super: <class 'Foo'>, S@method6>
# revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
reveal_type(super())
return self
@@ -265,19 +265,6 @@ class Foo[T]:
# revealed: Unknown
reveal_type(super())
return self
# TypeVar bounded by `type[Foo]` rather than `Foo`
# TODO: Should error on signature - `self` is annotated as a class type, not an instance type
def method11[S: type[Foo[int]]](self: S, other: S) -> S:
# Delegates to the bound to resolve the super type
reveal_type(super()) # revealed: <super: <class 'Foo'>, <class 'Foo[int]'>>
return self
# TypeVar bounded by `type[Foo]`, used in `type[T]` position
# TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass
# Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed
@classmethod
def method12[S: type[Foo[int]]](cls: type[S]) -> S:
reveal_type(super()) # revealed: <super: <class 'Foo'>, Unknown>
raise NotImplementedError
type Alias = Bar
@@ -372,15 +359,15 @@ from __future__ import annotations
class A:
def test(self):
reveal_type(super()) # revealed: <super: <class 'A'>, Self@test>
reveal_type(super()) # revealed: <super: <class 'A'>, A>
class B:
def test(self):
reveal_type(super()) # revealed: <super: <class 'B'>, Self@test>
reveal_type(super()) # revealed: <super: <class 'B'>, B>
class C(A.B):
def test(self):
reveal_type(super()) # revealed: <super: <class 'C'>, Self@test>
reveal_type(super()) # revealed: <super: <class 'C'>, C>
def inner(t: C):
reveal_type(super()) # revealed: <super: <class 'B'>, C>
@@ -658,7 +645,7 @@ class A:
class B(A):
def __init__(self, a: int):
super().__init__(a)
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, Self@__init__>` has no attribute `a`"
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
super().a
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
@@ -683,58 +670,3 @@ reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(k
# error: [not-subscriptable] "Cannot subscript object of type `<super: <class 'B'>, B>` with no `__getitem__` method"
super(B, B())[0]
```
## Subclass Using Concrete Type Instead of `Self`
When a parent class uses `Self` in a parameter type and a subclass overrides it with a concrete
type, passing that parameter to `super().__init__()` is a type error. This is because `Self` in the
parent could represent a further subclass. The fix is to use `Self` consistently in the subclass.
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
from collections.abc import Mapping
from typing import Self
class Parent:
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
self.children = children
class Child(Parent):
def __init__(self, children: Mapping[str, Child] | None = None) -> None:
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `Mapping[str, Self@__init__] | None`, found `Mapping[str, Child] | None`"
super().__init__(children)
# The fix is to use `Self` consistently in the subclass:
class Parent2:
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
self.children = children
class Child2(Parent2):
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
super().__init__(children) # OK
```
## Super in Protocol Classes
Using `super()` in a class that inherits from `typing.Protocol` (similar to beartype's caching
Protocol):
```py
from typing import Protocol, Generic, TypeVar
_T_co = TypeVar("_T_co", covariant=True)
class MyProtocol(Protocol, Generic[_T_co]):
def __class_getitem__(cls, item):
# Accessing parent's __class_getitem__ through super()
reveal_type(super()) # revealed: <super: <class 'MyProtocol'>, type[Self@__class_getitem__]>
parent_method = super().__class_getitem__
reveal_type(parent_method) # revealed: @Todo(super in generic class)
return parent_method(item)
```

View File

@@ -1032,125 +1032,4 @@ reveal_type(asdict(p)) # revealed: dict[str, Any]
reveal_type(replace(p, name="Bob")) # revealed: Person
```
## Calling decorator function directly with a class argument
When a function decorated with `@dataclass_transform()` is called directly with a class argument
(not used as a decorator), it should return the class with the dataclass transformation applied.
### Basic case
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
return cls
class A:
x: int
B = my_dataclass(A)
reveal_type(B) # revealed: <class 'A'>
B(1)
```
### Function with additional parameters
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T], *, order: bool = False) -> type[T]:
return cls
class A:
x: int
B = my_dataclass(A, order=True)
reveal_type(B) # revealed: <class 'A'>
reveal_type(B(1) < B(2)) # revealed: bool
```
### Overloaded decorator function
When the decorator function has overloads (one for direct class application, one for returning a
decorator), calling it with a class should return the class type.
```py
from typing_extensions import dataclass_transform, Callable, overload
@overload
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]: ...
@overload
def my_dataclass[T]() -> Callable[[type[T]], type[T]]: ...
def my_dataclass[T](cls: type[T] | None = None) -> type[T] | Callable[[type[T]], type[T]]:
raise NotImplementedError
class A:
x: int
B = my_dataclass(A)
reveal_type(B) # revealed: <class 'A'>
B(1)
```
### Passing a specialized generic class
When calling a `@dataclass_transform()` decorated function with a specialized generic class, the
specialization should be preserved.
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
return cls
class A[T]:
x: T
B = my_dataclass(A[int])
reveal_type(B) # revealed: <class 'A[int]'>
B(1)
```
### Decorator factory with class parameter
When a `@dataclass_transform()` decorated function takes a class as a parameter but is used as a
decorator factory (returns a decorator), the dataclass behavior should be applied to the decorated
class, not to the parameter class.
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def hydrated_dataclass[T](target: type[T], *, frozen: bool = False):
def decorator[U](cls: type[U]) -> type[U]:
return cls
return decorator
class Target:
pass
decorator = hydrated_dataclass(Target)
reveal_type(decorator) # revealed: <decorator produced by dataclass-like function>
@hydrated_dataclass(Target)
class Model:
x: int
# Model should be a dataclass-like class with x as a field
Model(x=1)
reveal_type(Model.__init__) # revealed: (self: Model, x: int) -> None
```
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform

View File

@@ -1682,7 +1682,9 @@ def sequence4(cls: type) -> type:
class Foo: ...
ordered_foo = dataclass(order=True)(Foo)
reveal_type(ordered_foo) # revealed: <class 'Foo'>
reveal_type(ordered_foo()) # revealed: Foo
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
reveal_type(ordered_foo) # revealed: type[Foo] & Any
# TODO: should be `Foo & Any`
reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call)
# TODO: should be `Any`
reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call)
```

View File

@@ -244,36 +244,3 @@ reveal_type(n1 > n2) # revealed: bool
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```
## Function call form
When `total_ordering` is called as a function (not as a decorator), the same validation is
performed:
```py
from functools import total_ordering
class NoOrderingMethod:
def __eq__(self, other: object) -> bool:
return True
# error: [invalid-total-ordering]
InvalidOrderedClass = total_ordering(NoOrderingMethod)
```
When the class does define an ordering method, no error is emitted:
```py
from functools import total_ordering
class HasOrderingMethod:
def __eq__(self, other: object) -> bool:
return True
def __lt__(self, other: "HasOrderingMethod") -> bool:
return True
# No error (class defines `__lt__`).
ValidOrderedClass = total_ordering(HasOrderingMethod)
reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod]
```

View File

@@ -949,29 +949,3 @@ class Builder(Generic[TMsg]):
def _handler(self, stream: Stream[Msg]) -> Stream[Msg]:
return stream
```
## Regressions
### Only consider fully static types as pivots for transitivity
This is a regression test for [ty#2371]. When working with constraint sets, we track transitive
relationships between the constraints in the set. For instance, in `S ≤ int ∧ int ≤ T`, we can infer
that `S ≤ T`. However, we should only consider fully static types when looking for a "pivot" for
this kind of transitive relationship. The same pattern does not hold for `S ≤ Any ∧ Any ≤ T`;
because the two `Any`s can materialize to different types, we cannot infer that `S ≤ T`.
We have lower level tests of this in [`type_properties/implies_subtype_of.md`][implies_subtype_of].
`functools.reduce` has a signature that exercises this behavior, as well, so we also include this
regression test.
```py
from functools import reduce
def _(keys: list[str]):
# TODO: revealed: int
# revealed: Unknown | Literal[0]
reveal_type(reduce(lambda total, k: total + len(k), keys, 0))
```
[implies_subtype_of]: ../../type_properties/implies_subtype_of.md
[ty#2371]: https://github.com/astral-sh/ty/issues/2371

View File

@@ -783,20 +783,3 @@ class Container[**P]:
# error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
return self.method(f)
```
## `ParamSpec` inference with un-annotated return type
Regression test for an issue where `ParamSpec` inference failed when the callable we were inferring
from did not have an annotated return type.
```py
from typing import Callable
def infer_paramspec[**P](func: Callable[P, None]) -> Callable[P, None]:
return func
def f(x: int, y: str):
pass
reveal_type(infer_paramspec(f)) # revealed: (x: int, y: str) -> None
```

View File

@@ -361,52 +361,6 @@ def f(union: A | B):
static_assert(not has_member(union, "only_on_b"))
```
Unless one of the elements of the union is `Any`, thus making it dynamic. In which case, we consider
items on the intersection of the non-`Any` elements:
```py
from typing import Any
from ty_extensions import has_member, static_assert
class A:
on_both: int = 1
only_on_a: str = "a"
class B:
on_both: int = 2
only_on_b: str = "b"
def f(union: Any | A):
static_assert(has_member(union, "on_both"))
static_assert(has_member(union, "only_on_a"))
def g(union: Any | A | B):
static_assert(has_member(union, "on_both"))
static_assert(not has_member(union, "only_on_a"))
static_assert(not has_member(union, "only_on_b"))
```
Similarly, unioning with an intersection involving `Any` is treated the same as if it was just
unioned with `Any`:
```py
from typing import Any
from ty_extensions import Intersection, has_member, static_assert
class A:
on_both: int = 1
only_on_a: str = "a"
class B:
on_both: int = 2
only_on_b: str = "b"
def f(x: Intersection[Any, A] | B):
static_assert(has_member(x, "on_both"))
static_assert(not has_member(x, "only_on_a"))
static_assert(has_member(x, "only_on_b"))
```
### Intersections
#### Only positive types

View File

@@ -341,11 +341,11 @@ class Impl:
```py
from foo.bar.both import Both
from foo.bar.impl import Impl
from foo.bar.fake import Fake # error: "Cannot resolve"
from foo.bar.impl import Impl # error: [unresolved-import]
from foo.bar.fake import Fake # error: [unresolved-import]
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: str
reveal_type(Impl().impl) # revealed: Unknown
reveal_type(Fake().fake) # revealed: Unknown
```

View File

@@ -191,8 +191,7 @@ reveal_type(Hexagon().area) # revealed: Unknown
The runtime package is a regular package but the stubs are namespace packages. Pyright skips the
stub package if the "regular" package isn't a namespace package. I'm not aware that the behavior
here is specified, and using the stubs without probing the runtime package first requires slightly
fewer lookups.
here is specified, but we currently agree with pyright here.
```toml
[environment]
@@ -202,17 +201,13 @@ extra-paths = ["/packages"]
`/packages/shapes-stubs/polygons/pentagon.pyi`:
```pyi
class Pentagon:
sides: int
area: float
class Pentagon: ...
```
`/packages/shapes-stubs/polygons/hexagon.pyi`:
```pyi
class Hexagon:
sides: int
area: float
class Hexagon: ...
```
`/packages/shapes/__init__.py`:
@@ -228,13 +223,17 @@ class Hexagon:
`/packages/shapes/polygons/pentagon.py`:
```py
class Pentagon: ...
class Pentagon:
sides: int
area: float
```
`/packages/shapes/polygons/hexagon.py`:
```py
class Hexagon: ...
class Hexagon:
sides: int
area: float
```
`main.py`:

View File

@@ -140,11 +140,7 @@ If a child class's method definition is Liskov-compatible with the method defini
class, Liskov compatibility must also nonetheless be checked with respect to the method definition
on its grandparent class. This is because type checkers will treat the child class as a subtype of
the grandparent class just as much as they treat it as a subtype of the parent class, so
substitutability with respect to the grandparent class is just as important.
However, if the parent class itself already has an LSP violation with an ancestor, we do not report
the same violation for the child class. This is because the child class cannot fix the violation
without introducing a new, worse violation against its immediate parent's contract.
substitutability with respect to the grandparent class is just as important:
<!-- snapshot-diagnostics -->
@@ -160,31 +156,13 @@ class Parent(Grandparent):
def method(self, x: str) -> None: ... # error: [invalid-method-override]
class Child(Parent):
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`.
# However, since `Parent.method` already violates LSP with `Grandparent.method`,
# we don't report the same violation for `Child` -- it's inherited from `Parent`.
def method(self, x: str) -> None: ...
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
def method(self, x: str) -> None: ... # error: [invalid-method-override]
class OtherChild(Parent):
# compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
def method(self, x: int) -> None: ... # error: [invalid-method-override]
class ChildWithNewViolation(Parent):
# incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
# We report the violation against the immediate parent (`Parent`), not the grandparent.
def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
class GrandparentWithReturnType:
def method(self) -> int: ...
class ParentWithReturnType(GrandparentWithReturnType):
def method(self) -> str: ... # error: [invalid-method-override]
class ChildWithReturnType(ParentWithReturnType):
# Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
# but not with `ParentWithReturnType.method`. We report against the immediate parent.
def method(self) -> int: ... # error: [invalid-method-override]
class GradualParent(Grandparent):
def method(self, x: Any) -> None: ...
@@ -212,9 +190,8 @@ class C(B):
foo = get
class D(C):
# compatible with `C.get` and `B.get`, but not with `A.get`.
# Since `B.get` already violates LSP with `A.get`, we don't report for `D`.
def get(self, my_default): ...
# compatible with `C.get` and `B.get`, but not with `A.get`
def get(self, my_default): ... # error: [invalid-method-override]
```
## Non-generic methods on generic classes work as expected

View File

@@ -1,6 +1,6 @@
# Disjointness of two types implies that neither is a subtype of the other
# Tuple pair is assignable to their union
This is a regression test for <https://github.com/astral-sh/ty/issues/2236>.
Regression test for <https://github.com/astral-sh/ty/issues/2236>.
```toml
[environment]

View File

@@ -22,39 +22,21 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
8 |
9 | class Child(Parent):
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`.
11 | # However, since `Parent.method` already violates LSP with `Grandparent.method`,
12 | # we don't report the same violation for `Child` -- it's inherited from `Parent`.
13 | def method(self, x: str) -> None: ...
14 |
15 | class OtherChild(Parent):
16 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
17 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
18 |
19 | class ChildWithNewViolation(Parent):
20 | # incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
21 | # We report the violation against the immediate parent (`Parent`), not the grandparent.
22 | def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
23 |
24 | class GrandparentWithReturnType:
25 | def method(self) -> int: ...
26 |
27 | class ParentWithReturnType(GrandparentWithReturnType):
28 | def method(self) -> str: ... # error: [invalid-method-override]
29 |
30 | class ChildWithReturnType(ParentWithReturnType):
31 | # Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
32 | # but not with `ParentWithReturnType.method`. We report against the immediate parent.
33 | def method(self) -> int: ... # error: [invalid-method-override]
34 |
35 | class GradualParent(Grandparent):
36 | def method(self, x: Any) -> None: ...
37 |
38 | class ThirdChild(GradualParent):
39 | # `GradualParent.method` is compatible with the signature of `Grandparent.method`,
40 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
41 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
42 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
12 |
13 | class OtherChild(Parent):
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
16 |
17 | class GradualParent(Grandparent):
18 | def method(self, x: Any) -> None: ...
19 |
20 | class ThirdChild(GradualParent):
21 | # `GradualParent.method` is compatible with the signature of `Grandparent.method`,
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
```
## other_stub.pyi
@@ -74,9 +56,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
12 | foo = get
13 |
14 | class D(C):
15 | # compatible with `C.get` and `B.get`, but not with `A.get`.
16 | # Since `B.get` already violates LSP with `A.get`, we don't report for `D`.
17 | def get(self, my_default): ...
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
16 | def get(self, my_default): ... # error: [invalid-method-override]
```
# Diagnostics
@@ -102,14 +83,38 @@ info: rule `invalid-method-override` is enabled by default
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:17:9
--> src/stub.pyi:11:9
|
15 | class OtherChild(Parent):
16 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
17 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
9 | class Child(Parent):
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
12 |
13 | class OtherChild(Parent):
|
::: src/stub.pyi:4:9
|
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
| ---------------------------- `Grandparent.method` defined here
5 |
6 | class Parent(Grandparent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:15:9
|
13 | class OtherChild(Parent):
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method`
18 |
19 | class ChildWithNewViolation(Parent):
16 |
17 | class GradualParent(Grandparent):
|
::: src/stub.pyi:7:9
|
@@ -126,75 +131,11 @@ info: rule `invalid-method-override` is enabled by default
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:22:9
--> src/stub.pyi:24:9
|
20 | # incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
21 | # We report the violation against the immediate parent (`Parent`), not the grandparent.
22 | def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method`
23 |
24 | class GrandparentWithReturnType:
|
::: src/stub.pyi:7:9
|
6 | class Parent(Grandparent):
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ---------------------------- `Parent.method` defined here
8 |
9 | class Child(Parent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:25:9
|
24 | class GrandparentWithReturnType:
25 | def method(self) -> int: ...
| ------------------- `GrandparentWithReturnType.method` defined here
26 |
27 | class ParentWithReturnType(GrandparentWithReturnType):
28 | def method(self) -> str: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `GrandparentWithReturnType.method`
29 |
30 | class ChildWithReturnType(ParentWithReturnType):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:28:9
|
27 | class ParentWithReturnType(GrandparentWithReturnType):
28 | def method(self) -> str: ... # error: [invalid-method-override]
| ------------------- `ParentWithReturnType.method` defined here
29 |
30 | class ChildWithReturnType(ParentWithReturnType):
31 | # Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
32 | # but not with `ParentWithReturnType.method`. We report against the immediate parent.
33 | def method(self) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentWithReturnType.method`
34 |
35 | class GradualParent(Grandparent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:42:9
|
40 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
41 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
42 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
|
::: src/stub.pyi:4:9
@@ -228,3 +169,25 @@ info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `get`
--> src/other_stub.pyi:16:9
|
14 | class D(C):
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
16 | def get(self, my_default): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get`
|
::: src/other_stub.pyi:2:9
|
1 | class A:
2 | def get(self, default): ...
| ------------------ `A.get` defined here
3 |
4 | class B(A):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -304,7 +304,7 @@ info: rule `duplicate-base` is enabled by default
```
```
warning[unused-ignore-comment]: Unused blanket `type: ignore` directive
info[unused-ignore-comment]: Unused blanket `type: ignore` directive
--> src/mdtest_snippet.py:72:9
|
70 | A,
@@ -356,7 +356,7 @@ info: rule `duplicate-base` is enabled by default
```
```
warning[unused-ignore-comment]: Unused blanket `type: ignore` directive
info[unused-ignore-comment]: Unused blanket `type: ignore` directive
--> src/mdtest_snippet.py:81:13
|
79 | ):

View File

@@ -22,13 +22,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
7 |
8 | class B(A):
9 | def __init__(self, a: int):
10 | reveal_type(super()) # revealed: <super: <class 'B'>, Self@__init__>
10 | reveal_type(super()) # revealed: <super: <class 'B'>, B>
11 | reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
12 | super().__init__(a)
13 |
14 | @classmethod
15 | def f(cls):
16 | reveal_type(super()) # revealed: <super: <class 'B'>, type[Self@f]>
16 | reveal_type(super()) # revealed: <super: <class 'B'>, <class 'B'>>
17 | super().f()
18 |
19 | super(B, B(42)).__init__(42)
@@ -78,16 +78,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
63 | reveal_type(super())
64 |
65 | def method4(self: Self):
66 | # revealed: <super: <class 'Foo'>, Self@method4>
66 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
67 | reveal_type(super())
68 |
69 | def method5[S: Foo[int]](self: S, other: S) -> S:
70 | # revealed: <super: <class 'Foo'>, S@method5>
70 | # revealed: <super: <class 'Foo'>, Foo[int]>
71 | reveal_type(super())
72 | return self
73 |
74 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
75 | # revealed: <super: <class 'Foo'>, S@method6> | <super: <class 'Foo'>, S@method6>
75 | # revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
76 | reveal_type(super())
77 | return self
78 |
@@ -114,48 +114,35 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
99 | # revealed: Unknown
100 | reveal_type(super())
101 | return self
102 | # TypeVar bounded by `type[Foo]` rather than `Foo`
103 | # TODO: Should error on signature - `self` is annotated as a class type, not an instance type
104 | def method11[S: type[Foo[int]]](self: S, other: S) -> S:
105 | # Delegates to the bound to resolve the super type
106 | reveal_type(super()) # revealed: <super: <class 'Foo'>, <class 'Foo[int]'>>
107 | return self
108 | # TypeVar bounded by `type[Foo]`, used in `type[T]` position
109 | # TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass
110 | # Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed
111 | @classmethod
112 | def method12[S: type[Foo[int]]](cls: type[S]) -> S:
113 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Unknown>
114 | raise NotImplementedError
115 |
116 | type Alias = Bar
102 |
103 | type Alias = Bar
104 |
105 | class Bar:
106 | def method(self: Alias):
107 | # revealed: <super: <class 'Bar'>, Bar>
108 | reveal_type(super())
109 |
110 | def pls_dont_call_me(self: Never):
111 | # revealed: <super: <class 'Bar'>, Unknown>
112 | reveal_type(super())
113 |
114 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
115 | # revealed: <super: <class 'Bar'>, Bar>
116 | reveal_type(super())
117 |
118 | class Bar:
119 | def method(self: Alias):
120 | # revealed: <super: <class 'Bar'>, Bar>
118 | class P(Protocol):
119 | def method(self: P):
120 | # revealed: <super: <class 'P'>, P>
121 | reveal_type(super())
122 |
123 | def pls_dont_call_me(self: Never):
124 | # revealed: <super: <class 'Bar'>, Unknown>
125 | reveal_type(super())
126 |
127 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
128 | # revealed: <super: <class 'Bar'>, Bar>
129 | reveal_type(super())
130 |
131 | class P(Protocol):
132 | def method(self: P):
133 | # revealed: <super: <class 'P'>, P>
134 | reveal_type(super())
135 |
136 | class E(enum.Enum):
137 | X = 1
138 |
139 | def method(self: E):
140 | match self:
141 | case E.X:
142 | # revealed: <super: <class 'E'>, E>
143 | reveal_type(super())
123 | class E(enum.Enum):
124 | X = 1
125 |
126 | def method(self: E):
127 | match self:
128 | case E.X:
129 | # revealed: <super: <class 'E'>, E>
130 | reveal_type(super())
```
# Diagnostics
@@ -218,7 +205,6 @@ error[invalid-super-argument]: `S@method10` is a type variable with an abstract/
100 | reveal_type(super())
| ^^^^^^^
101 | return self
102 | # TypeVar bounded by `type[Foo]` rather than `Foo`
|
info: Type variable `S` has upper bound `(...) -> str`
info: rule `invalid-super-argument` is enabled by default

View File

@@ -27,7 +27,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.m
# Diagnostics
```
warning[unused-ignore-comment]: Unused `ty: ignore` directive
info[unused-ignore-comment]: Unused `ty: ignore` directive
--> src/mdtest_snippet.py:2:13
|
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
@@ -47,7 +47,7 @@ help: Remove the unused suppression comment
```
```
warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment'
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment'
--> src/mdtest_snippet.py:6:26
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
@@ -70,7 +70,7 @@ help: Remove the unused suppression code
```
```
warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-reference'
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-reference'
--> src/mdtest_snippet.py:6:64
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
@@ -93,7 +93,7 @@ help: Remove the unused suppression code
```
```
warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'
--> src/mdtest_snippet.py:9:26
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"

View File

@@ -32,7 +32,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore
# Diagnostics
```
warning[unused-ignore-comment]: Unused `ty: ignore` directive
info[unused-ignore-comment]: Unused `ty: ignore` directive
--> src/mdtest_snippet.py:10:9
|
8 | a = (3
@@ -55,7 +55,7 @@ help: Remove the unused suppression comment
```
```
warning[unused-ignore-comment]: Unused `ty: ignore` directive
info[unused-ignore-comment]: Unused `ty: ignore` directive
--> src/mdtest_snippet.py:14:21
|
12 | a = (3

View File

@@ -537,43 +537,4 @@ def identity2[T](t: T) -> T:
return t
```
## Transitivity
### Transitivity can propagate across typevars
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
def concrete_pivot[T, U]():
# If [int ≤ T ∧ T ≤ U], then [int ≤ U] must be true as well.
constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(T, U, object)
static_assert(constraints.implies_subtype_of(int, U))
```
### Transitivity can propagate across fully static concrete types
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
def concrete_pivot[T, U]():
# If [T ≤ int ∧ int ≤ U], then [T ≤ U] must be true as well.
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(int, U, object)
static_assert(constraints.implies_subtype_of(T, U))
```
### Transitivity cannot propagate across non-fully-static concrete types
```py
from typing import Any, Never
from ty_extensions import ConstraintSet, static_assert
def concrete_pivot[T, U]():
# If [T ≤ Any ∧ Any ≤ U], then the two `Any`s might materialize to different types. That means
# [T ≤ U] is NOT necessarily true.
constraints = ConstraintSet.range(Never, T, Any) & ConstraintSet.range(Any, U, object)
static_assert(not constraints.implies_subtype_of(T, U))
```
[subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence

View File

@@ -736,29 +736,6 @@ static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal[""]]], No
static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
```
## Callable types with Unknown/missing return type
See <https://github.com/astral-sh/ty/issues/2363>, a property test failure involving
`~type & ~((...) -> Unknown)` not being assignable to `~type`. Since `~type & ~Callable` is a subset
of `~type`, the intersection should be assignable to `~type`.
The root cause was that we failed to properly materialize a `Callable[..., Unknown]` type when the
`Unknown` return type originated from a missing annotation.
```py
from ty_extensions import static_assert, is_assignable_to, Intersection, Not, Unknown, CallableTypeOf
from typing import Callable
# `Callable[..., Unknown]` has explicit Unknown return type
static_assert(is_assignable_to(Intersection[Not[type], Not[Callable[..., Unknown]]], Not[type]))
# Function with no return annotation (has implicit Unknown return type internally)
def no_return_annotation(*args, **kwargs): ...
# `CallableTypeOf[no_return_annotation]` has `returns: None` internally (no annotation)
static_assert(is_assignable_to(Intersection[Not[type], Not[CallableTypeOf[no_return_annotation]]], Not[type]))
```
## Intersections with non-fully-static negated elements
A type can be _assignable_ to an intersection containing negated elements only if the _bottom_

View File

@@ -36,7 +36,6 @@ com2ann
comtypes
core
cpython
cryptography
cwltool
dacite
dd-trace-py
@@ -80,7 +79,6 @@ mypy-protobuf
mypy_primer
nionutils
nox
numpy-stl
openlibrary
operator
optuna

View File

@@ -156,10 +156,9 @@ impl<'db> DefinedPlace<'db> {
/// bound_or_declared: Place::Defined(DefinedPlace { ty: Literal[1], origin: TypeOrigin::Inferred, definedness: Definedness::PossiblyUndefined, .. }),
/// non_existent: Place::Undefined,
/// ```
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) enum Place<'db> {
Defined(DefinedPlace<'db>),
#[default]
Undefined,
}
@@ -593,7 +592,6 @@ type DeclaredTypeAndConflictingTypes<'db> = (
);
/// The result of looking up a declared type from declarations; see [`place_from_declarations`].
#[derive(Debug, Default)]
pub(crate) struct PlaceFromDeclarationsResult<'db> {
place_and_quals: PlaceAndQualifiers<'db>,
conflicting_types: Option<Box<indexmap::set::Slice<Type<'db>>>>,
@@ -643,12 +641,21 @@ impl<'db> PlaceFromDeclarationsResult<'db> {
/// that this comes with a [`CLASS_VAR`] type qualifier.
///
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) struct PlaceAndQualifiers<'db> {
pub(crate) place: Place<'db>,
pub(crate) qualifiers: TypeQualifiers,
}
impl Default for PlaceAndQualifiers<'_> {
fn default() -> Self {
PlaceAndQualifiers {
place: Place::Undefined,
qualifiers: TypeQualifiers::empty(),
}
}
}
impl<'db> PlaceAndQualifiers<'db> {
/// Constructor that creates a [`PlaceAndQualifiers`] instance with a [`TodoType`] type
/// and no qualifiers.
@@ -662,7 +669,10 @@ impl<'db> PlaceAndQualifiers<'db> {
}
pub(crate) fn unbound() -> Self {
Self::default()
PlaceAndQualifiers {
place: Place::Undefined,
qualifiers: TypeQualifiers::empty(),
}
}
pub(crate) fn is_undefined(&self) -> bool {
@@ -1557,7 +1567,11 @@ fn place_from_declarations_impl<'db>(
}
}
} else {
PlaceFromDeclarationsResult::default()
PlaceFromDeclarationsResult {
place_and_quals: Place::Undefined.into(),
conflicting_types: None,
first_declaration: None,
}
}
}
@@ -1657,7 +1671,7 @@ mod implicit_globals {
Place::Defined(
DefinedPlace::new(KnownClass::Dict.to_specialized_instance(
db,
&[Type::any(), KnownClass::Int.to_instance(db)],
[Type::any(), KnownClass::Int.to_instance(db)],
))
.with_definedness(Definedness::PossiblyUndefined),
)
@@ -1673,10 +1687,10 @@ mod implicit_globals {
[Parameter::positional_only(Some(Name::new_static("format")))
.with_annotated_type(KnownClass::Int.to_instance(db))],
),
KnownClass::Dict.to_specialized_instance(
Some(KnownClass::Dict.to_specialized_instance(
db,
&[KnownClass::Str.to_instance(db), Type::any()],
),
[KnownClass::Str.to_instance(db), Type::any()],
)),
);
Place::Defined(
DefinedPlace::new(Type::function_like_callable(db, signature))
@@ -1879,7 +1893,7 @@ mod tests {
let ty1 = Type::IntLiteral(1);
let ty2 = Type::IntLiteral(2);
let unbound = || PlaceAndQualifiers::default();
let unbound = || Place::Undefined.with_qualifiers(TypeQualifiers::empty());
let possibly_unbound_ty1 = || {
Place::Defined(DefinedPlace {

View File

@@ -781,13 +781,6 @@ mod tests {
.find_map(|constrained_binding| constrained_binding.binding.definition())
}
fn first_public_declaration(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
self.end_of_scope_symbol_declarations(symbol)
.find_map(|declaration_with_constraint| {
declaration_with_constraint.declaration.definition()
})
}
fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option<Definition<'_>> {
self.bindings_at_use(use_id)
.find_map(|constrained_binding| constrained_binding.binding.definition())
@@ -840,19 +833,10 @@ mod tests {
#[test]
fn annotation_only() {
let TestCase { db, file } = test_case("x: int");
let scope = global_scope(&db, file);
let global_table = place_table(&db, scope);
let global_table = place_table(&db, global_scope(&db, file));
assert_eq!(names(global_table), vec!["int", "x"]);
let use_def = use_def_map(&db, scope);
let declaration = use_def
.first_public_declaration(global_table.symbol_id("x").expect("symbol to exist"))
.unwrap();
assert!(matches!(
declaration.kind(&db),
DefinitionKind::AnnotatedAssignment(_)
));
// TODO record definition
}
#[test]

View File

@@ -899,7 +899,9 @@ impl ReachabilityConstraints {
let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator
.fold((true, true), |(none, all), overload| {
let overload_returns_never =
overload.return_ty.is_equivalent_to(db, Type::Never);
overload.return_ty.is_some_and(|return_type| {
return_type.is_equivalent_to(db, Type::Never)
});
(
none && !overload_returns_never,

View File

@@ -427,6 +427,12 @@ pub struct Completion<'db> {
pub builtin: bool,
}
impl<'db> Completion<'db> {
pub fn is_type_check_only(&self, db: &'db dyn Db) -> bool {
self.ty.is_some_and(|ty| ty.is_type_check_only(db))
}
}
pub trait HasType {
/// Returns the inferred type of `self`.
///

View File

@@ -24,10 +24,10 @@ use crate::{Db, declare_lint, lint::LintId};
declare_lint! {
/// ## What it does
/// Checks for `ty: ignore` or `type: ignore` directives that are no longer applicable.
/// Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable.
///
/// ## Why is this bad?
/// A `ty: ignore` directive that no longer matches any diagnostic violations is likely
/// A `type: ignore` directive that no longer matches any diagnostic violations is likely
/// included by mistake, and should be removed to avoid confusion.
///
/// ## Examples
@@ -40,14 +40,10 @@ declare_lint! {
/// ```py
/// a = 20 / 2
/// ```
///
/// ## Options
/// Set [`analysis.respect-type-ignore-comments`](https://docs.astral.sh/ty/reference/configuration/#respect-type-ignore-comments)
/// to `false` to prevent this rule from reporting unused `type: ignore` comments.
pub static UNUSED_IGNORE_COMMENT = {
summary: "detects unused `ty: ignore` and `type: ignore` comments",
summary: "detects unused `type: ignore` comments",
status: LintStatus::stable("0.0.1-alpha.1"),
default_level: Level::Warn,
default_level: Level::Ignore,
}
}

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