Compare commits

..

3 Commits

Author SHA1 Message Date
Amethyst Reese
f0fa410a02 Minimal prototype using regex 2026-01-12 18:20:12 -08:00
Amethyst Reese
06440dc5ba Update source kind from path mapping 2026-01-09 15:45:22 -08:00
Amethyst Reese
64fd7e900d Create new source types for markdown files 2026-01-08 17:08:23 -08:00
151 changed files with 2234 additions and 8431 deletions

View File

@@ -5,4 +5,4 @@ rustup component add clippy rustfmt
cargo install cargo-insta
cargo fetch
pip install maturin prek
pip install maturin pre-commit

View File

@@ -1,4 +1,4 @@
# Configuration for the actionlint tool, which we run via prek
# Configuration for the actionlint tool, which we run via pre-commit
# to verify the correctness of the syntax in our GitHub Actions workflows.
self-hosted-runner:

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

@@ -76,9 +76,9 @@
enabled: false,
},
{
groupName: "prek dependencies",
groupName: "pre-commit dependencies",
matchManagers: ["pre-commit"],
description: "Weekly update of prek dependencies",
description: "Weekly update of pre-commit dependencies",
},
{
groupName: "NPM Development dependencies",

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

@@ -769,8 +769,8 @@ jobs:
- name: "Remove wheels from cache"
run: rm -rf target/wheels
prek:
name: "prek"
pre-commit:
name: "pre-commit"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
timeout-minutes: 10
steps:
@@ -784,17 +784,17 @@ jobs:
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 24
- name: "Cache prek"
- name: "Cache pre-commit"
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.cache/prek
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run prek"
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run pre-commit"
run: |
echo '```console' > "$GITHUB_STEP_SUMMARY"
# Enable color output for prek and remove it for the summary
# Use --hook-stage=manual to enable slower hooks that are skipped by default
SKIP=cargo-fmt uvx prek run --all-files --show-diff-on-failure --color always --hook-stage manual | \
# Enable color output for pre-commit and remove it for the summary
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
SKIP=cargo-fmt uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
exit_code="${PIPESTATUS[0]}"
echo '```' >> "$GITHUB_STEP_SUMMARY"

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

@@ -21,62 +21,15 @@ exclude: |
)$
repos:
# Priority 0: Read-only hooks; hooks that modify disjoint file types.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-merge-conflict
priority: 0
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
hooks:
- id: validate-pyproject
priority: 0
- repo: https://github.com/crate-ci/typos
rev: v1.40.0
hooks:
- id: typos
priority: 0
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt --
language: system
types: [rust]
pass_filenames: false # This makes it a lot faster
priority: 0
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.7.4
hooks:
- id: prettier
types: [yaml]
priority: 0
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.19.0
hooks:
- id: zizmor
priority: 0
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.0
hooks:
- id: check-github-workflows
priority: 0
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
priority: 0
- repo: https://github.com/executablebooks/mdformat
rev: 1.0.0
@@ -91,20 +44,7 @@ repos:
docs/formatter/black\.md
| docs/\w+\.md
)$
priority: 0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
hooks:
- id: ruff-format
priority: 0
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
require_serial: true
priority: 1
# Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat).
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.47.0
hooks:
@@ -114,9 +54,7 @@ repos:
docs/formatter/black\.md
| docs/\w+\.md
)$
priority: 1
# Priority 2: blacken-docs runs after markdownlint-fix (both modify markdown).
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.20.0
hooks:
@@ -130,7 +68,48 @@ repos:
)$
additional_dependencies:
- black==25.12.0
priority: 2
- repo: https://github.com/crate-ci/typos
rev: v1.40.0
hooks:
- id: typos
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt --
language: system
types: [rust]
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
hooks:
- id: ruff-format
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
require_serial: true
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.7.4
hooks:
- id: prettier
types: [yaml]
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.19.0
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.0
hooks:
- id: check-github-workflows
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
@@ -140,16 +119,19 @@ repos:
- id: actionlint
stages:
# This hook is disabled by default, since it's quite slow.
# To run all hooks *including* this hook, use `uvx prek run -a --hook-stage=manual`.
# To run *just* this hook, use `uvx prek run -a actionlint --hook-stage=manual`.
# To run all hooks *including* this hook, use `uvx pre-commit run -a --hook-stage=manual`.
# To run *just* this hook, use `uvx pre-commit run -a actionlint --hook-stage=manual`.
- manual
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,
# but the integration only works if shellcheck is installed
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.11.1"
priority: 0
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck

View File

@@ -65,7 +65,6 @@ When working on ty, PR titles should start with `[ty]` and be tagged with the `t
- All changes must be tested. If you're not testing your changes, you're not done.
- Get your tests to pass. If you didn't run the tests, your code does not work.
- Follow existing code style. Check neighboring files for patterns.
- Always run `uvx prek run -a` at the end of a task.
- Always run `uvx pre-commit run -a` at the end of a task.
- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first.
- Avoid falling back to patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system.
- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability.

View File

@@ -53,12 +53,12 @@ cargo install cargo-insta
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
run Python utility commands.
You can optionally install hooks to automatically run the validation checks
You can optionally install pre-commit hooks to automatically run the validation checks
when making a commit:
```shell
uv tool install prek
prek install
uv tool install pre-commit
pre-commit install
```
We recommend [nextest](https://nexte.st/) to run Ruff's test suite (via `cargo nextest run`),
@@ -85,7 +85,7 @@ and that it passes both the lint and test validation checks:
```shell
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json
uvx prek run -a # Rust and Python formatting, Markdown and Python linting, etc.
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your pull request, but running them locally
@@ -381,7 +381,7 @@ Commit each step of this process separately for easier review.
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
- Square brackets (eg, `[ruff]` project name) will be automatically escaped by `prek`
- Square brackets (eg, `[ruff]` project name) will be automatically escaped by `pre-commit`
Additionally, for minor releases:

7
Cargo.lock generated
View File

@@ -3646,7 +3646,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 +3671,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 +4444,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

@@ -31,6 +31,7 @@ ruff_options_metadata = { workspace = true, features = ["serde"] }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_server = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }

View File

@@ -11,6 +11,7 @@ use itertools::Itertools;
use log::{error, warn};
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use regex::{Captures, Regex};
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, Severity, Span,
};
@@ -18,6 +19,7 @@ use ruff_linter::message::{EmitterContext, create_panic_diagnostic, render_diagn
use ruff_linter::settings::types::OutputFormat;
use ruff_notebook::NotebookIndex;
use ruff_python_parser::ParseError;
use ruff_python_trivia::textwrap::{dedent, indent};
use rustc_hash::{FxHashMap, FxHashSet};
use thiserror::Error;
use tracing::debug;
@@ -489,6 +491,66 @@ pub(crate) fn format_source(
formatted,
)))
}
SourceKind::Markdown(unformatted_document) => {
// adapted from blacken-docs
// https://github.com/adamchainz/blacken-docs/blob/fb107c1dce25f9206e29297aaa1ed7afc2980a5a/src/blacken_docs/__init__.py#L17
let code_block_regex = Regex::new(
r"(?imsx)
(?<before>
^(?<indent>\ *)```[^\S\r\n]*
(?:python|py|python3|py3)
(?:\ .*?)?\n
)
(?<code>.*?)
(?<after>
^\ *```[^\S\r\n]*$
)
",
)
.unwrap();
let mut changed = false;
let formatted_document =
code_block_regex.replace_all(unformatted_document, |capture: &Captures| {
let (original, [before, code_indent, unformatted_code, after]) =
capture.extract();
let unformatted_code = dedent(unformatted_code);
let options = settings.to_format_options(source_type, &unformatted_code, path);
let formatted_code = if let Some(_range) = range {
unimplemented!()
} else {
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
#[expect(clippy::redundant_closure_for_method_calls)]
format_module_source(&unformatted_code, options)
.map(|formatted| formatted.into_code())
};
// TODO: figure out how to properly raise errors from inside closure
if let Ok(formatted_code) = formatted_code {
if formatted_code.len() == unformatted_code.len()
&& formatted_code == *unformatted_code
{
original.to_string()
} else {
changed = true;
let formatted_code = indent(formatted_code.as_str(), code_indent);
format!("{before}{formatted_code}{after}")
}
} else {
original.to_string()
}
});
if changed {
Ok(FormattedSource::Formatted(SourceKind::Markdown(
formatted_document.to_string(),
)))
} else {
Ok(FormattedSource::Unchanged)
}
}
}
}

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

@@ -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

@@ -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

@@ -22,6 +22,8 @@ pub enum SourceKind {
Python(String),
/// The source contains a Jupyter notebook.
IpyNotebook(Box<Notebook>),
/// The source contains Markdown text.
Markdown(String),
}
impl SourceKind {
@@ -33,6 +35,7 @@ impl SourceKind {
match self {
SourceKind::IpyNotebook(notebook) => Some(notebook),
SourceKind::Python(_) => None,
SourceKind::Markdown(_) => None,
}
}
@@ -40,20 +43,36 @@ impl SourceKind {
match self {
SourceKind::Python(code) => Some(code),
SourceKind::IpyNotebook(_) => None,
SourceKind::Markdown(_) => None,
}
}
pub fn as_markdown(&self) -> Option<&str> {
match self {
SourceKind::Markdown(code) => Some(code),
SourceKind::Python(_) => None,
SourceKind::IpyNotebook(_) => None,
}
}
pub fn expect_python(self) -> String {
match self {
SourceKind::Python(code) => code,
SourceKind::IpyNotebook(_) => panic!("expected python code"),
_ => panic!("expected python code"),
}
}
pub fn expect_ipy_notebook(self) -> Notebook {
match self {
SourceKind::IpyNotebook(notebook) => *notebook,
SourceKind::Python(_) => panic!("expected ipy notebook"),
_ => panic!("expected ipy notebook"),
}
}
pub fn expect_markdown(self) -> String {
match self {
SourceKind::Markdown(code) => code,
_ => panic!("expected markdown text"),
}
}
@@ -66,6 +85,7 @@ impl SourceKind {
SourceKind::IpyNotebook(cloned)
}
SourceKind::Python(_) => SourceKind::Python(new_source),
SourceKind::Markdown(_) => SourceKind::Markdown(new_source),
}
}
@@ -74,20 +94,28 @@ impl SourceKind {
match self {
SourceKind::Python(source) => source,
SourceKind::IpyNotebook(notebook) => notebook.source_code(),
SourceKind::Markdown(source) => source,
}
}
/// Read the [`SourceKind`] from the given path. Returns `None` if the source is not a Python
/// source file.
pub fn from_path(path: &Path, source_type: PySourceType) -> Result<Option<Self>, SourceError> {
if source_type.is_ipynb() {
let notebook = Notebook::from_path(path)?;
Ok(notebook
.is_python_notebook()
.then_some(Self::IpyNotebook(Box::new(notebook))))
} else {
let contents = std::fs::read_to_string(path)?;
Ok(Some(Self::Python(contents)))
match source_type {
PySourceType::Ipynb => {
let notebook = Notebook::from_path(path)?;
Ok(notebook
.is_python_notebook()
.then_some(Self::IpyNotebook(Box::new(notebook))))
}
PySourceType::Markdown => {
let contents = std::fs::read_to_string(path)?;
Ok(Some(Self::Markdown(contents)))
}
PySourceType::Python | PySourceType::Stub => {
let contents = std::fs::read_to_string(path)?;
Ok(Some(Self::Python(contents)))
}
}
}
@@ -120,6 +148,10 @@ impl SourceKind {
notebook.write(writer)?;
Ok(())
}
SourceKind::Markdown(source) => {
writer.write_all(source.as_bytes())?;
Ok(())
}
}
}
@@ -140,6 +172,10 @@ impl SourceKind {
kind: DiffKind::IpyNotebook(src, dst),
path,
}),
(SourceKind::Markdown(src), SourceKind::Markdown(dst)) => Some(SourceKindDiff {
kind: DiffKind::Markdown(src, dst),
path,
}),
_ => None,
}
}
@@ -212,6 +248,17 @@ impl std::fmt::Display for SourceKindDiff<'_> {
writeln!(f)?;
}
DiffKind::Markdown(original, modified) => {
let mut diff = CodeDiff::new(original, modified);
let relative_path = self.path.map(fs::relativize_path);
if let Some(relative_path) = &relative_path {
diff.header(relative_path, relative_path);
}
writeln!(f, "{diff}")?;
}
}
Ok(())
@@ -222,6 +269,7 @@ impl std::fmt::Display for SourceKindDiff<'_> {
enum DiffKind<'a> {
Python(&'a str, &'a str),
IpyNotebook(&'a Notebook, &'a Notebook),
Markdown(&'a str, &'a str),
}
struct CodeDiff<'a> {

View File

@@ -89,6 +89,8 @@ pub enum PySourceType {
Stub,
/// The source is a Jupyter notebook (`.ipynb`).
Ipynb,
/// The source is a Markdown file (`.md`).
Markdown,
}
impl PySourceType {
@@ -106,6 +108,7 @@ impl PySourceType {
"pyi" => Self::Stub,
"pyw" => Self::Python,
"ipynb" => Self::Ipynb,
"md" => Self::Markdown,
_ => return None,
};
@@ -134,6 +137,10 @@ impl PySourceType {
pub const fn is_ipynb(self) -> bool {
matches!(self, Self::Ipynb)
}
pub const fn is_markdown(self) -> bool {
matches!(self, Self::Markdown)
}
}
impl<P: AsRef<Path>> From<P> for PySourceType {

View File

@@ -1,133 +0,0 @@
class Simple:
x=1
x=2 # fmt: skip
x=3
class Semicolon:
x=1
x=2;x=3 # fmt: skip
x=4
class TrailingSemicolon:
x=1
x=2;x=3 ; # fmt: skip
x=4
class SemicolonNewLogicalLine:
x=1;
x=2;x=3 # fmt: skip
x=4
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
def foo(): y=1 # fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
x=1
x = [
'1',
'2',
] # fmt: skip
class MultiLineSemicolon:
x=1
x = [
'1',
'2',
]; x=2 # fmt: skip
class LineContinuationSemicolonAfter:
x=1
x = ['a']\
; y=1 # fmt: skip
class LineContinuationSemicolonBefore:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewline:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewlineAndComment:
x=1
x = ['a']; \
# 1
y=1 # fmt: skip
class RepeatedLineContinuation:
x=1
x = ['a']; \
\
\
y=1 # fmt: skip
class MultiLineSemicolonComments:
x=1
# 1
x = [ # 2
'1', # 3
'2',
# 4
]; x=2 # 5 # fmt: skip # 6
class DocstringSkipped:
'''This is a docstring''' # fmt: skip
x=1
class MultilineDocstringSkipped:
'''This is a docstring
''' # fmt: skip
x=1
class FirstStatementNewlines:
x=1 # fmt: skip
class ChainingSemicolons:
x=[
'1',
'2',
'3',
];x=1;x=[
'1',
'2',
'3'
];x=1;x=1 # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
class MixingCompound:
def foo(): bar(); import zoo # fmt: skip
# https://github.com/astral-sh/ruff/issues/17331
def main() -> None:
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
# https://github.com/astral-sh/ruff/issues/11430
print(); print() # noqa # fmt: skip

View File

@@ -1,22 +0,0 @@
# 0
'''Module docstring
multiple lines''' # 1 # fmt: skip # 2
import a;import b; from c import (
x,y,z
); import f # fmt: skip
x=1;x=2;x=3;x=4 # fmt: skip
# 1
x=[ # 2
'1', # 3
'2',
'3'
];x=1;x=1 # 4 # fmt: skip # 5
# 6
def foo(): x=[
'1',
'2',
];x=1 # fmt: skip

View File

@@ -1,68 +0,0 @@
class Simple:
# Range comprises skip range
x=1
<RANGE_START>x=2 <RANGE_END># fmt: skip
x=3
class Semicolon:
# Range is part of skip range
x=1
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
x=4
class FormatFirst:
x=1
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
x=4
class FormatMiddle:
x=1
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
x=5
class SemicolonNewLogicalLine:
# Range overlaps on right side
<RANGE_START>x=1;
x=2<RANGE_END>;x=3 # fmt: skip
x=4
class SemicolonNewLogicalLine:
# Range overlaps on left side
x=1;
x=2;<RANGE_START>x=3 # fmt: skip
x=4<RANGE_END>
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
# Range inside statement
x=1
x = <RANGE_START>[
'1',
'2',<RANGE_END>
] # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3<RANGE_START>
'2',
'3'
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
# 6

View File

@@ -334,7 +334,7 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLines {
PySourceType::Stub => {
write!(f, [empty_line()])
}
PySourceType::Python | PySourceType::Ipynb => {
PySourceType::Python | PySourceType::Ipynb | PySourceType::Markdown => {
write!(f, [empty_line(), empty_line()])
}
},

View File

@@ -24,6 +24,7 @@ pub use crate::options::{
};
use crate::range::is_logical_line;
pub use crate::shared_traits::{AsFormat, FormattedIter, FormattedIterExt, IntoFormat};
use crate::verbatim::suppressed_node;
pub(crate) mod builders;
pub mod cli;
@@ -60,39 +61,51 @@ where
let node_ref = AnyNodeRef::from(node);
let node_comments = comments.leading_dangling_trailing(node_ref);
leading_comments(node_comments.leading).fmt(f)?;
if self.is_suppressed(node_comments.trailing, f.context()) {
suppressed_node(node_ref).fmt(f)
} else {
leading_comments(node_comments.leading).fmt(f)?;
// Emit source map information for nodes that are valid "narrowing" targets
// in range formatting. Never emit source map information if they're disabled
// for performance reasons.
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
&& f.options().source_map_generation().is_enabled();
// Emit source map information for nodes that are valid "narrowing" targets
// in range formatting. Never emit source map information if they're disabled
// for performance reasons.
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
&& f.options().source_map_generation().is_enabled();
emit_source_position
.then_some(source_position(node.start()))
.fmt(f)?;
emit_source_position
.then_some(source_position(node.start()))
.fmt(f)?;
self.fmt_fields(node, f)?;
self.fmt_fields(node, f)?;
debug_assert!(
node_comments
.dangling
.iter()
.all(SourceComment::is_formatted),
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
);
debug_assert!(
node_comments
.dangling
.iter()
.all(SourceComment::is_formatted),
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
);
write!(
f,
[
emit_source_position.then_some(source_position(node.end())),
trailing_comments(node_comments.trailing)
]
)
write!(
f,
[
emit_source_position.then_some(source_position(node.end())),
trailing_comments(node_comments.trailing)
]
)
}
}
/// Formats the node's fields.
fn fmt_fields(&self, item: &N, f: &mut PyFormatter) -> FormatResult<()>;
fn is_suppressed(
&self,
_trailing_comments: &[SourceComment],
_context: &PyFormatContext,
) -> bool {
false
}
}
#[derive(Error, Debug, salsa::Update, PartialEq, Eq)]

View File

@@ -1,10 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::Decorator;
use ruff_text_size::Ranged;
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::verbatim::verbatim_text;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
@@ -12,27 +11,26 @@ pub struct FormatDecorator;
impl FormatNodeRule<Decorator> for FormatDecorator {
fn fmt_fields(&self, item: &Decorator, f: &mut PyFormatter) -> FormatResult<()> {
let comments = f.context().comments();
let trailing = comments.trailing(item);
let Decorator {
expression,
range: _,
node_index: _,
} = item;
if has_skip_comment(trailing, f.context().source()) {
comments.mark_verbatim_node_comments_formatted(item.into());
write!(
f,
[
token("@"),
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
]
)
}
verbatim_text(item.range()).fmt(f)
} else {
let Decorator {
expression,
range: _,
node_index: _,
} = item;
write!(
f,
[
token("@"),
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
]
)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::comments::Comments;
use crate::context::{IndentLevel, NodeLevel};
use crate::prelude::*;
use crate::statement::suite::{DocstringStmt, skip_range};
use crate::statement::suite::DocstringStmt;
use crate::verbatim::{ends_suppression, starts_suppression};
use crate::{FormatModuleError, PyFormatOptions, format_module_source};
@@ -251,29 +251,7 @@ impl<'ast> SourceOrderVisitor<'ast> for FindEnclosingNode<'_, 'ast> {
// We only visit statements that aren't suppressed that's why we don't need to track the suppression
// state in a stack. Assert that this assumption is safe.
debug_assert!(self.suppressed.is_no());
let mut iter = body.iter();
while let Some(stmt) = iter.next() {
// If the range intersects a skip range then we need to
// format the entire suite to properly handle the case
// where a `fmt: skip` affects multiple statements.
//
// For example, in the case
//
// ```
// <RANGE_START>x=1<RANGE_END>;x=2 # fmt: skip
// ```
//
// the statement `x=1` does not "know" that it is
// suppressed, but the suite does.
if let Some(verbatim_range) = skip_range(stmt, iter.as_slice(), self.context)
&& verbatim_range.intersect(self.range).is_some()
{
break;
}
self.visit_stmt(stmt);
}
walk_body(self, body);
self.suppressed = Suppressed::No;
}
}
@@ -583,7 +561,7 @@ impl NarrowRange<'_> {
}
pub(crate) const fn is_logical_line(node: AnyNodeRef) -> bool {
// Make sure to update [`FormatEnclosingNode`] when changing this.
// Make sure to update [`FormatEnclosingLine`] when changing this.
node.is_statement()
|| node.is_decorator()
|| node.is_except_handler()

View File

@@ -1,13 +1,14 @@
use ruff_formatter::write;
use ruff_python_ast::StmtAnnAssign;
use crate::comments::SourceComment;
use crate::expression::is_splittable_expression;
use crate::expression::parentheses::Parentheses;
use crate::prelude::*;
use crate::statement::stmt_assign::{
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
};
use crate::statement::trailing_semicolon;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtAnnAssign;
@@ -83,4 +84,12 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -2,9 +2,10 @@ use ruff_formatter::prelude::{space, token};
use ruff_formatter::write;
use ruff_python_ast::StmtAssert;
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtAssert;
@@ -40,4 +41,12 @@ impl FormatNodeRule<StmtAssert> for FormatStmtAssert {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -19,7 +19,6 @@ use crate::expression::{
maybe_parenthesize_expression,
};
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::prelude::*;
use crate::preview::is_parenthesize_lambda_bodies_enabled;
use crate::statement::trailing_semicolon;
use crate::string::StringLikeExtensions;
@@ -27,6 +26,7 @@ use crate::string::implicit::{
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
ImplicitConcatenatedLayout,
};
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtAssign;
@@ -104,6 +104,14 @@ impl FormatNodeRule<StmtAssign> for FormatStmtAssign {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}
/// Formats a single target with the equal operator.

View File

@@ -1,14 +1,15 @@
use ruff_formatter::write;
use ruff_python_ast::StmtAugAssign;
use crate::comments::SourceComment;
use crate::expression::parentheses::is_expression_parenthesized;
use crate::prelude::*;
use crate::statement::stmt_assign::{
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
has_target_own_parentheses,
};
use crate::statement::trailing_semicolon;
use crate::{AsFormat, FormatNodeRule};
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtAugAssign;
@@ -61,4 +62,12 @@ impl FormatNodeRule<StmtAugAssign> for FormatStmtAugAssign {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::StmtBreak;
use crate::prelude::*;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtBreak;
@@ -9,4 +10,12 @@ impl FormatNodeRule<StmtBreak> for FormatStmtBreak {
fn fmt_fields(&self, _item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> {
token("break").fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::StmtContinue;
use crate::prelude::*;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtContinue;
@@ -9,4 +10,12 @@ impl FormatNodeRule<StmtContinue> for FormatStmtContinue {
fn fmt_fields(&self, _item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> {
token("continue").fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -3,10 +3,10 @@ use ruff_python_ast::StmtDelete;
use ruff_text_size::Ranged;
use crate::builders::{PyFormatterExtensions, parenthesize_if_expands};
use crate::comments::dangling_node_comments;
use crate::comments::{SourceComment, dangling_node_comments};
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtDelete;
@@ -57,4 +57,12 @@ impl FormatNodeRule<StmtDelete> for FormatStmtDelete {
}
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,10 +1,12 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Expr, Operator, StmtExpr};
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::trailing_semicolon;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtExpr;
@@ -28,6 +30,14 @@ impl FormatNodeRule<StmtExpr> for FormatStmtExpr {
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}
const fn is_arithmetic_like(expression: &Expr) -> bool {

View File

@@ -1,6 +1,8 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::StmtGlobal;
use crate::comments::SourceComment;
use crate::has_skip_comment;
use crate::prelude::*;
#[derive(Default)]
@@ -45,4 +47,12 @@ impl FormatNodeRule<StmtGlobal> for FormatStmtGlobal {
)
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,7 +1,8 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::StmtImport;
use crate::prelude::*;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtImport;
@@ -20,4 +21,12 @@ impl FormatNodeRule<StmtImport> for FormatStmtImport {
});
write!(f, [token("import"), space(), names])
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -3,7 +3,9 @@ use ruff_python_ast::StmtImportFrom;
use ruff_text_size::Ranged;
use crate::builders::{PyFormatterExtensions, TrailingComma, parenthesize_if_expands};
use crate::comments::SourceComment;
use crate::expression::parentheses::parenthesized;
use crate::has_skip_comment;
use crate::other::identifier::DotDelimitedIdentifier;
use crate::prelude::*;
@@ -70,4 +72,12 @@ impl FormatNodeRule<StmtImportFrom> for FormatStmtImportFrom {
.fmt(f)
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,7 +1,8 @@
use ruff_python_ast::StmtIpyEscapeCommand;
use ruff_text_size::Ranged;
use crate::prelude::*;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtIpyEscapeCommand;
@@ -10,4 +11,12 @@ impl FormatNodeRule<StmtIpyEscapeCommand> for FormatStmtIpyEscapeCommand {
fn fmt_fields(&self, item: &StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
source_text_slice(item.range()).fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,6 +1,8 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::StmtNonlocal;
use crate::comments::SourceComment;
use crate::has_skip_comment;
use crate::prelude::*;
#[derive(Default)]
@@ -45,4 +47,12 @@ impl FormatNodeRule<StmtNonlocal> for FormatStmtNonlocal {
)
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::StmtPass;
use crate::prelude::*;
use crate::comments::SourceComment;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtPass;
@@ -9,4 +10,12 @@ impl FormatNodeRule<StmtPass> for FormatStmtPass {
fn fmt_fields(&self, _item: &StmtPass, f: &mut PyFormatter) -> FormatResult<()> {
token("pass").fmt(f)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,9 +1,10 @@
use ruff_formatter::write;
use ruff_python_ast::StmtRaise;
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtRaise;
@@ -42,4 +43,12 @@ impl FormatNodeRule<StmtRaise> for FormatStmtRaise {
}
Ok(())
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,9 +1,10 @@
use ruff_formatter::write;
use ruff_python_ast::{Expr, StmtReturn};
use crate::comments::SourceComment;
use crate::expression::expr_tuple::TupleParentheses;
use crate::prelude::*;
use crate::statement::stmt_assign::FormatStatementsLastExpression;
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtReturn;
@@ -42,4 +43,12 @@ impl FormatNodeRule<StmtReturn> for FormatStmtReturn {
None => Ok(()),
}
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -1,10 +1,11 @@
use ruff_formatter::write;
use ruff_python_ast::StmtTypeAlias;
use crate::prelude::*;
use crate::comments::SourceComment;
use crate::statement::stmt_assign::{
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
};
use crate::{has_skip_comment, prelude::*};
#[derive(Default)]
pub struct FormatStmtTypeAlias;
@@ -41,4 +42,12 @@ impl FormatNodeRule<StmtTypeAlias> for FormatStmtTypeAlias {
]
)
}
fn is_suppressed(
&self,
trailing_comments: &[SourceComment],
context: &PyFormatContext,
) -> bool {
has_skip_comment(trailing_comments, context.source())
}
}

View File

@@ -4,15 +4,11 @@ use ruff_formatter::{
use ruff_python_ast::helpers::is_compound_statement;
use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite};
use ruff_python_ast::{AnyNodeRef, StmtExpr};
use ruff_python_trivia::{
SimpleTokenKind, SimpleTokenizer, lines_after, lines_after_ignoring_end_of_line_trivia,
lines_before,
};
use ruff_python_trivia::{lines_after, lines_after_ignoring_end_of_line_trivia, lines_before};
use ruff_text_size::{Ranged, TextRange};
use crate::comments::{
Comments, LeadingDanglingTrailingComments, has_skip_comment, leading_comments,
trailing_comments,
Comments, LeadingDanglingTrailingComments, leading_comments, trailing_comments,
};
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
use crate::other::string_literal::StringLiteralKind;
@@ -20,9 +16,9 @@ use crate::prelude::*;
use crate::preview::{
is_allow_newline_after_block_open_enabled, is_blank_line_before_decorated_class_in_stub_enabled,
};
use crate::statement::trailing_semicolon;
use crate::statement::stmt_expr::FormatStmtExpr;
use crate::verbatim::{
write_skipped_statements, write_suppressed_statements_starting_with_leading_comment,
suppressed_node, write_suppressed_statements_starting_with_leading_comment,
write_suppressed_statements_starting_with_trailing_comment,
};
@@ -156,21 +152,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
let first_comments = comments.leading_dangling_trailing(first);
let (mut preceding, mut empty_line_after_docstring) = if let Some(verbatim_range) =
skip_range(first.statement(), iter.as_slice(), f.context())
{
let preceding =
write_skipped_statements(first.statement(), &mut iter, verbatim_range, f)?;
// Insert a newline after a module level docstring, but treat
// it as a docstring otherwise. See: https://github.com/psf/black/pull/3932.
let empty_line_after_docstring =
matches!(self.kind, SuiteKind::TopLevel | SuiteKind::Class)
&& DocstringStmt::try_from_statement(preceding, self.kind, f.context())
.is_some();
(preceding, empty_line_after_docstring)
} else if first_comments
let (mut preceding, mut empty_line_after_docstring) = if first_comments
.leading
.iter()
.any(|comment| comment.is_suppression_off_comment(source))
@@ -301,7 +283,9 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
PySourceType::Stub => {
empty_line().fmt(f)?;
}
PySourceType::Python | PySourceType::Ipynb => {
PySourceType::Python
| PySourceType::Ipynb
| PySourceType::Markdown => {
write!(f, [empty_line(), empty_line()])?;
}
},
@@ -342,7 +326,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
PySourceType::Stub => {
empty_line().fmt(f)?;
}
PySourceType::Python | PySourceType::Ipynb => {
PySourceType::Python | PySourceType::Ipynb | PySourceType::Markdown => {
write!(f, [empty_line(), empty_line()])?;
}
},
@@ -394,7 +378,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
PySourceType::Stub => {
empty_line().fmt(f)?;
}
PySourceType::Python | PySourceType::Ipynb => {
PySourceType::Python | PySourceType::Ipynb | PySourceType::Markdown => {
write!(f, [empty_line(), empty_line()])?;
}
},
@@ -409,10 +393,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
}
}
if let Some(verbatim_range) = skip_range(following, iter.as_slice(), f.context()) {
preceding = write_skipped_statements(following, &mut iter, verbatim_range, f)?;
preceding_comments = comments.leading_dangling_trailing(preceding);
} else if following_comments
if following_comments
.leading
.iter()
.any(|comment| comment.is_suppression_off_comment(source))
@@ -861,57 +842,61 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
let comments = f.context().comments().clone();
let node_comments = comments.leading_dangling_trailing(self.docstring);
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
let string_literal = self
.docstring
.as_expr_stmt()
.unwrap()
.value
.as_string_literal_expr()
.unwrap();
if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) {
suppressed_node(self.docstring).fmt(f)
} else {
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
let string_literal = self
.docstring
.as_expr_stmt()
.unwrap()
.value
.as_string_literal_expr()
.unwrap();
// We format the expression, but the statement carries the comments
write!(
f,
[
leading_comments(node_comments.leading),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.start())),
string_literal
.format()
.with_options(StringLiteralKind::Docstring),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.end())),
]
)?;
// We format the expression, but the statement carries the comments
write!(
f,
[
leading_comments(node_comments.leading),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.start())),
string_literal
.format()
.with_options(StringLiteralKind::Docstring),
f.options()
.source_map_generation()
.is_enabled()
.then_some(source_position(self.docstring.end())),
]
)?;
if self.suite_kind == SuiteKind::Class {
// Comments after class docstrings need a newline between the docstring and the
// comment (https://github.com/astral-sh/ruff/issues/7948).
// ```python
// class ModuleBrowser:
// """Browse module classes and functions in IDLE."""
// # ^ Insert a newline above here
//
// def __init__(self, master, path, *, _htest=False, _utest=False):
// pass
// ```
if let Some(own_line) = node_comments
.trailing
.iter()
.find(|comment| comment.line_position().is_own_line())
{
if lines_before(own_line.start(), f.context().source()) < 2 {
empty_line().fmt(f)?;
if self.suite_kind == SuiteKind::Class {
// Comments after class docstrings need a newline between the docstring and the
// comment (https://github.com/astral-sh/ruff/issues/7948).
// ```python
// class ModuleBrowser:
// """Browse module classes and functions in IDLE."""
// # ^ Insert a newline above here
//
// def __init__(self, master, path, *, _htest=False, _utest=False):
// pass
// ```
if let Some(own_line) = node_comments
.trailing
.iter()
.find(|comment| comment.line_position().is_own_line())
{
if lines_before(own_line.start(), f.context().source()) < 2 {
empty_line().fmt(f)?;
}
}
}
}
trailing_comments(node_comments.trailing).fmt(f)
trailing_comments(node_comments.trailing).fmt(f)
}
}
}
@@ -955,58 +940,6 @@ impl Format<PyFormatContext<'_>> for SuiteChildStatement<'_> {
}
}
pub(crate) fn skip_range(
first: &Stmt,
statements: &[Stmt],
context: &PyFormatContext,
) -> Option<TextRange> {
let start = first.start();
let mut last_statement = first;
let comments = context.comments();
let source = context.source();
for statement in statements {
if new_logical_line_between_statements(
source,
TextRange::new(last_statement.end(), statement.start()),
) {
break;
}
last_statement = statement;
}
if has_skip_comment(comments.trailing(last_statement), source) {
Some(TextRange::new(
start,
trailing_semicolon(last_statement.into(), source)
.map_or_else(|| last_statement.end(), ruff_text_size::TextRange::end),
))
} else {
None
}
}
fn new_logical_line_between_statements(source: &str, between_statement_range: TextRange) -> bool {
let mut tokenizer = SimpleTokenizer::new(source, between_statement_range).map(|tok| tok.kind());
while let Some(token) = tokenizer.next() {
match token {
SimpleTokenKind::Continuation => {
tokenizer.next();
}
SimpleTokenKind::Newline => {
return true;
}
// Since we are between statements, there are
// no non-trivia tokens, so there is no need to check
// for these and do an early return.
_ => {}
}
}
false
}
#[cfg(test)]
mod tests {
use ruff_formatter::format;

View File

@@ -2,7 +2,6 @@ use std::borrow::Cow;
use std::iter::FusedIterator;
use std::slice::Iter;
use itertools::PeekingNext;
use ruff_formatter::{FormatError, write};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::Stmt;
@@ -452,40 +451,6 @@ fn write_suppressed_statements<'a>(
}
}
#[cold]
pub(crate) fn write_skipped_statements<'a>(
first_skipped: &'a Stmt,
statements: &mut std::slice::Iter<'a, Stmt>,
verbatim_range: TextRange,
f: &mut PyFormatter,
) -> FormatResult<&'a Stmt> {
let comments = f.context().comments().clone();
comments.mark_verbatim_node_comments_formatted(first_skipped.into());
let mut preceding = first_skipped;
while let Some(prec) = statements.peeking_next(|next| next.end() <= verbatim_range.end()) {
comments.mark_verbatim_node_comments_formatted(prec.into());
preceding = prec;
}
let first_leading = comments.leading(first_skipped);
let preceding_trailing = comments.trailing(preceding);
// Write the outer comments and format the node as verbatim
write!(
f,
[
leading_comments(first_leading),
source_position(verbatim_range.start()),
verbatim_text(verbatim_range),
source_position(verbatim_range.end()),
trailing_comments(preceding_trailing)
]
)?;
Ok(preceding)
}
#[derive(Copy, Clone, Debug)]
enum InSuppression {
No,
@@ -928,6 +893,65 @@ impl Format<PyFormatContext<'_>> for VerbatimText {
}
}
/// Disables formatting for `node` and instead uses the same formatting as the node has in source.
///
/// The `node` gets indented as any formatted node to avoid syntax errors when the indentation string changes (e.g. from 2 spaces to 4).
/// The `node`s leading and trailing comments are formatted as usual, except if they fall into the suppressed node's range.
#[cold]
pub(crate) fn suppressed_node<'a, N>(node: N) -> FormatSuppressedNode<'a>
where
N: Into<AnyNodeRef<'a>>,
{
FormatSuppressedNode { node: node.into() }
}
pub(crate) struct FormatSuppressedNode<'a> {
node: AnyNodeRef<'a>,
}
impl Format<PyFormatContext<'_>> for FormatSuppressedNode<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let comments = f.context().comments().clone();
let node_comments = comments.leading_dangling_trailing(self.node);
// Mark all comments as formatted that fall into the node range
for comment in node_comments.leading {
if comment.start() > self.node.start() {
comment.mark_formatted();
}
}
for comment in node_comments.trailing {
if comment.start() < self.node.end() {
comment.mark_formatted();
}
}
// Some statements may end with a semicolon. Preserve the semicolon
let semicolon_range = self
.node
.is_statement()
.then(|| trailing_semicolon(self.node, f.context().source()))
.flatten();
let verbatim_range = semicolon_range.map_or(self.node.range(), |semicolon| {
TextRange::new(self.node.start(), semicolon.end())
});
comments.mark_verbatim_node_comments_formatted(self.node);
// Write the outer comments and format the node as verbatim
write!(
f,
[
leading_comments(node_comments.leading),
source_position(verbatim_range.start()),
verbatim_text(verbatim_range),
source_position(verbatim_range.end()),
trailing_comments(node_comments.trailing)
]
)
}
}
#[cold]
pub(crate) fn write_suppressed_clause_header(
header: ClauseHeader,

View File

@@ -1,299 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
class Simple:
x=1
x=2 # fmt: skip
x=3
class Semicolon:
x=1
x=2;x=3 # fmt: skip
x=4
class TrailingSemicolon:
x=1
x=2;x=3 ; # fmt: skip
x=4
class SemicolonNewLogicalLine:
x=1;
x=2;x=3 # fmt: skip
x=4
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
def foo(): y=1 # fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
x=1
x = [
'1',
'2',
] # fmt: skip
class MultiLineSemicolon:
x=1
x = [
'1',
'2',
]; x=2 # fmt: skip
class LineContinuationSemicolonAfter:
x=1
x = ['a']\
; y=1 # fmt: skip
class LineContinuationSemicolonBefore:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewline:
x=1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewlineAndComment:
x=1
x = ['a']; \
# 1
y=1 # fmt: skip
class RepeatedLineContinuation:
x=1
x = ['a']; \
\
\
y=1 # fmt: skip
class MultiLineSemicolonComments:
x=1
# 1
x = [ # 2
'1', # 3
'2',
# 4
]; x=2 # 5 # fmt: skip # 6
class DocstringSkipped:
'''This is a docstring''' # fmt: skip
x=1
class MultilineDocstringSkipped:
'''This is a docstring
''' # fmt: skip
x=1
class FirstStatementNewlines:
x=1 # fmt: skip
class ChainingSemicolons:
x=[
'1',
'2',
'3',
];x=1;x=[
'1',
'2',
'3'
];x=1;x=1 # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
class MixingCompound:
def foo(): bar(); import zoo # fmt: skip
# https://github.com/astral-sh/ruff/issues/17331
def main() -> None:
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
# https://github.com/astral-sh/ruff/issues/11430
print(); print() # noqa # fmt: skip
```
## Output
```python
class Simple:
x = 1
x=2 # fmt: skip
x = 3
class Semicolon:
x = 1
x=2;x=3 # fmt: skip
x = 4
class TrailingSemicolon:
x = 1
x=2;x=3 ; # fmt: skip
x = 4
class SemicolonNewLogicalLine:
x = 1
x=2;x=3 # fmt: skip
x = 4
class ManySemicolonOneLine:
x = 1
x=2;x=3;x=4 # fmt: skip
x = 5
class CompoundInSuite:
x = 1
def foo(): y=1 # fmt: skip
x = 2
class CompoundInSuiteNewline:
x = 1
def foo():
y=1 # fmt: skip
x = 2
class MultiLineSkip:
x = 1
x = [
'1',
'2',
] # fmt: skip
class MultiLineSemicolon:
x = 1
x = [
'1',
'2',
]; x=2 # fmt: skip
class LineContinuationSemicolonAfter:
x = 1
x = ['a']\
; y=1 # fmt: skip
class LineContinuationSemicolonBefore:
x = 1
x = ['a']; \
y=1 # fmt: skip
class LineContinuationSemicolonAndNewline:
x = 1
x = ["a"]
y=1 # fmt: skip
class LineContinuationSemicolonAndNewlineAndComment:
x = 1
x = ["a"]
# 1
y=1 # fmt: skip
class RepeatedLineContinuation:
x = 1
x = ['a']; \
\
\
y=1 # fmt: skip
class MultiLineSemicolonComments:
x = 1
# 1
x = [ # 2
'1', # 3
'2',
# 4
]; x=2 # 5 # fmt: skip # 6
class DocstringSkipped:
'''This is a docstring''' # fmt: skip
x = 1
class MultilineDocstringSkipped:
'''This is a docstring
''' # fmt: skip
x = 1
class FirstStatementNewlines:
x=1 # fmt: skip
class ChainingSemicolons:
x=[
'1',
'2',
'3',
];x=1;x=[
'1',
'2',
'3'
];x=1;x=1 # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
class MixingCompound:
def foo(): bar(); import zoo # fmt: skip
# https://github.com/astral-sh/ruff/issues/17331
def main() -> None:
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
# https://github.com/astral-sh/ruff/issues/11430
print(); print() # noqa # fmt: skip
```

View File

@@ -1,56 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
# 0
'''Module docstring
multiple lines''' # 1 # fmt: skip # 2
import a;import b; from c import (
x,y,z
); import f # fmt: skip
x=1;x=2;x=3;x=4 # fmt: skip
# 1
x=[ # 2
'1', # 3
'2',
'3'
];x=1;x=1 # 4 # fmt: skip # 5
# 6
def foo(): x=[
'1',
'2',
];x=1 # fmt: skip
```
## Output
```python
# 0
'''Module docstring
multiple lines''' # 1 # fmt: skip # 2
import a;import b; from c import (
x,y,z
); import f # fmt: skip
x=1;x=2;x=3;x=4 # fmt: skip
# 1
x=[ # 2
'1', # 3
'2',
'3'
];x=1;x=1 # 4 # fmt: skip # 5
# 6
def foo():
x=[
'1',
'2',
];x=1 # fmt: skip
```

View File

@@ -1,147 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
class Simple:
# Range comprises skip range
x=1
<RANGE_START>x=2 <RANGE_END># fmt: skip
x=3
class Semicolon:
# Range is part of skip range
x=1
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
x=4
class FormatFirst:
x=1
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
x=4
class FormatMiddle:
x=1
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
x=5
class SemicolonNewLogicalLine:
# Range overlaps on right side
<RANGE_START>x=1;
x=2<RANGE_END>;x=3 # fmt: skip
x=4
class SemicolonNewLogicalLine:
# Range overlaps on left side
x=1;
x=2;<RANGE_START>x=3 # fmt: skip
x=4<RANGE_END>
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
# Range inside statement
x=1
x = <RANGE_START>[
'1',
'2',<RANGE_END>
] # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3<RANGE_START>
'2',
'3'
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
# 6
```
## Output
```python
class Simple:
# Range comprises skip range
x=1
x=2 # fmt: skip
x=3
class Semicolon:
# Range is part of skip range
x=1
x=2;x=3 # fmt: skip
x=4
class FormatFirst:
x=1
x=2;x=3 # fmt: skip
x=4
class FormatMiddle:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class SemicolonNewLogicalLine:
# Range overlaps on right side
x = 1
x=2;x=3 # fmt: skip
x=4
class SemicolonNewLogicalLine:
# Range overlaps on left side
x=1;
x=2;x=3 # fmt: skip
x = 4
class ManySemicolonOneLine:
x=1
x=2;x=3;x=4 # fmt: skip
x=5
class CompoundInSuite:
x=1
def foo(): y=1 # fmt: skip
x=2
class CompoundInSuiteNewline:
x=1
def foo():
y=1 # fmt: skip
x=2
class MultiLineSkip:
# Range inside statement
x=1
x = [
'1',
'2',
] # fmt: skip
class LotsOfComments:
# 1
x=[ # 2
'1', # 3
'2',
'3'
] ;x=2;x=3 # 4 # fmt: skip # 5
# 6
```

View File

@@ -525,7 +525,7 @@ pub trait AsMode {
impl AsMode for PySourceType {
fn as_mode(&self) -> Mode {
match self {
PySourceType::Python | PySourceType::Stub => Mode::Module,
PySourceType::Python | PySourceType::Stub | PySourceType::Markdown => Mode::Module,
PySourceType::Ipynb => Mode::Ipython,
}
}

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

@@ -34,12 +34,12 @@ cargo install cargo-insta
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
run Python utility commands.
You can optionally install hooks to automatically run the validation checks
You can optionally install pre-commit hooks to automatically run the validation checks
when making a commit:
```shell
uv tool install prek
prek install
uv tool install pre-commit
pre-commit install
```
We recommend [nextest](https://nexte.st/) to run ty's test suite (via `cargo nextest run`),
@@ -66,7 +66,7 @@ and that it passes both the lint and test validation checks:
```shell
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
cargo test # Rust testing
uvx prek run -a # Rust and Python formatting, Markdown and Python linting, etc.
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your pull request, but running them locally

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

@@ -231,8 +231,7 @@ impl<'db> Definitions<'db> {
ty_python_semantic::types::TypeDefinition::Module(module) => {
ResolvedDefinition::Module(module.file(db)?)
}
ty_python_semantic::types::TypeDefinition::StaticClass(definition)
| ty_python_semantic::types::TypeDefinition::DynamicClass(definition)
ty_python_semantic::types::TypeDefinition::Class(definition)
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)

View File

@@ -554,17 +554,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

@@ -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

@@ -13,6 +13,54 @@ bool(1, 2)
bool(NotBool())
```
## Calls to `type()`
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
tests for the `__class__` attribute.)
```py
reveal_type(type(1)) # revealed: <class 'int'>
```
But a three-argument call to type creates a dynamic instance of the `type` class:
```py
class Base: ...
reveal_type(type("Foo", (), {})) # revealed: type
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
```
Other numbers of arguments are invalid
```py
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", ())
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (), {}, weird_other_arg=42)
```
The following calls are also invalid, due to incorrect argument types:
```py
class Base: ...
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
type(b"Foo", (), {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
type("Foo", Base, {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
type("Foo", (1, 2), {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
type("Foo", (Base,), {b"attr": 1})
```
## Calls to `str()`
### Valid calls

View File

@@ -1,939 +0,0 @@
# Calls to `type()`
## Single-argument form
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
tests for the `__class__` attribute.)
```py
reveal_type(type(1)) # revealed: <class 'int'>
```
## Three-argument form (dynamic class creation)
A three-argument call to `type()` creates a new class. We synthesize a class type using the name
from the first argument:
```py
class Base: ...
class Mixin: ...
# We synthesize a class type using the name argument
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
# With a single base class
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
# With multiple base classes
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
# The inferred type is assignable to type[Base] since Foo inherits from Base
tests: list[type[Base]] = []
testCaseClass = type("Foo", (Base,), {})
tests.append(testCaseClass) # No error - type[Foo] is assignable to type[Base]
```
The name can also be provided indirectly via a variable with a string literal type:
```py
name = "IndirectClass"
IndirectClass = type(name, (), {})
reveal_type(IndirectClass) # revealed: <class 'IndirectClass'>
# Works with base classes too
class Base: ...
base_name = "DerivedClass"
DerivedClass = type(base_name, (Base,), {})
reveal_type(DerivedClass) # revealed: <class 'DerivedClass'>
```
## Distinct class types
Each `type()` call produces a distinct class type, even if they have the same name and bases:
```py
from ty_extensions import static_assert, is_equivalent_to
class Base: ...
Foo1 = type("Foo", (Base,), {})
Foo2 = type("Foo", (Base,), {})
# Even though they have the same name and bases, they are distinct types
static_assert(not is_equivalent_to(Foo1, Foo2))
# Each instance is typed with its respective class
foo1 = Foo1()
foo2 = Foo2()
def takes_foo1(x: Foo1) -> None: ...
def takes_foo2(x: Foo2) -> None: ...
takes_foo1(foo1) # OK
takes_foo2(foo2) # OK
# error: [invalid-argument-type] "Argument to function `takes_foo1` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`"
takes_foo1(foo2)
# error: [invalid-argument-type] "Argument to function `takes_foo2` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`"
takes_foo2(foo1)
```
## Instances and attribute access
Instances of dynamic classes are typed with the synthesized class name. Attributes from all base
classes are accessible:
```py
class Base:
base_attr: int = 1
def base_method(self) -> str:
return "hello"
class Mixin:
mixin_attr: str = "mixin"
Foo = type("Foo", (Base,), {})
foo = Foo()
# Instance is typed with the synthesized class name
reveal_type(foo) # revealed: Foo
# Inherited attributes are accessible
reveal_type(foo.base_attr) # revealed: int
reveal_type(foo.base_method()) # revealed: str
# Multiple inheritance: attributes from all bases are accessible
Bar = type("Bar", (Base, Mixin), {})
bar = Bar()
reveal_type(bar.base_attr) # revealed: int
reveal_type(bar.mixin_attr) # revealed: str
```
Attributes from the namespace dict (third argument) are tracked:
```py
class Base: ...
Foo = type("Foo", (Base,), {"custom_attr": 42})
# Class attribute access
reveal_type(Foo.custom_attr) # revealed: Literal[42]
# Instance attribute access
foo = Foo()
reveal_type(foo.custom_attr) # revealed: Literal[42]
```
When the namespace dict is not a literal (e.g., passed as a parameter), attribute access returns
`Unknown` since we can't know what attributes might be defined:
```py
from typing import Any
class DynamicBase: ...
def f(attributes: dict[str, Any]):
X = type("X", (DynamicBase,), attributes)
reveal_type(X) # revealed: <class 'X'>
# Attribute access returns Unknown since the namespace is dynamic
reveal_type(X.foo) # revealed: Unknown
x = X()
reveal_type(x.bar) # revealed: Unknown
```
When a `TypedDict` is passed as the namespace argument, we synthesize a class type with the known
keys from the `TypedDict` as attributes. Since `TypedDict` instances are "open" (they can have
arbitrary additional string keys), unknown attributes return `Unknown`:
```py
from typing import TypedDict
class Namespace(TypedDict):
z: int
def g(attributes: Namespace):
Y = type("Y", (), attributes)
reveal_type(Y) # revealed: <class 'Y'>
# Known keys from the TypedDict are tracked as attributes
reveal_type(Y.z) # revealed: int
y = Y()
reveal_type(y.z) # revealed: int
# Unknown attributes return Unknown since TypedDicts are open
reveal_type(Y.unknown) # revealed: Unknown
reveal_type(y.unknown) # revealed: Unknown
```
## Closed TypedDicts (PEP-728)
TODO: We don't support the PEP-728 `closed=True` keyword argument to `TypedDict` yet. When we do, a
closed TypedDict namespace should NOT be marked as dynamic, and accessing unknown attributes should
emit an error instead of returning `Unknown`.
```py
from typing import TypedDict
class ClosedNamespace(TypedDict, closed=True):
x: int
y: str
def h(ns: ClosedNamespace):
X = type("X", (), ns)
reveal_type(X) # revealed: <class 'X'>
# Known keys from the TypedDict are tracked as attributes
reveal_type(X.x) # revealed: int
reveal_type(X.y) # revealed: str
x = X()
reveal_type(x.x) # revealed: int
reveal_type(x.y) # revealed: str
# TODO: Once we support `closed=True`, these should emit errors instead of returning Unknown
reveal_type(X.unknown) # revealed: Unknown
reveal_type(x.unknown) # revealed: Unknown
```
## Inheritance from dynamic classes
Regular classes can inherit from dynamic classes:
```py
class Base:
base_attr: int = 1
DynamicClass = type("DynamicClass", (Base,), {})
class Child(DynamicClass):
child_attr: str = "child"
child = Child()
# Attributes from the dynamic class's base are accessible
reveal_type(child.base_attr) # revealed: int
# The child class's own attributes are accessible
reveal_type(child.child_attr) # revealed: str
# Child instances are subtypes of DynamicClass instances
def takes_dynamic(x: DynamicClass) -> None: ...
takes_dynamic(child) # No error - Child is a subtype of DynamicClass
# isinstance narrows to the dynamic class instance type
def check_isinstance(x: object) -> None:
if isinstance(x, DynamicClass):
reveal_type(x) # revealed: DynamicClass
# Dynamic class inheriting from int narrows correctly with isinstance
IntSubclass = type("IntSubclass", (int,), {})
def check_int_subclass(x: IntSubclass | str) -> None:
if isinstance(x, int):
# IntSubclass inherits from int, so it's included in the narrowed type
reveal_type(x) # revealed: IntSubclass
else:
reveal_type(x) # revealed: str
```
## Disjointness
Dynamic classes are not considered disjoint from unrelated types (since a subclass could inherit
from both):
```py
class Base: ...
Foo = type("Foo", (Base,), {})
def check_disjointness(x: Foo | int) -> None:
if isinstance(x, int):
reveal_type(x) # revealed: int
else:
# Foo and int are not considered disjoint because `class C(Foo, int)` could exist.
reveal_type(x) # revealed: Foo & ~int
```
Disjointness also works for `type[]` of dynamic classes:
```py
from ty_extensions import is_disjoint_from, static_assert
# Dynamic classes with disjoint bases have disjoint type[] types.
IntClass = type("IntClass", (int,), {})
StrClass = type("StrClass", (str,), {})
static_assert(is_disjoint_from(type[IntClass], type[StrClass]))
static_assert(is_disjoint_from(type[StrClass], type[IntClass]))
# Dynamic classes that share a common base are not disjoint.
class Base: ...
Foo = type("Foo", (Base,), {})
Bar = type("Bar", (Base,), {})
static_assert(not is_disjoint_from(type[Foo], type[Bar]))
```
## Using dynamic classes with `super()`
Dynamic classes can be used as the pivot class in `super()` calls:
```py
class Base:
def method(self) -> int:
return 42
DynamicChild = type("DynamicChild", (Base,), {})
# Using dynamic class as pivot with dynamic class instance owner
fc = DynamicChild()
reveal_type(super(DynamicChild, fc)) # revealed: <super: <class 'DynamicChild'>, DynamicChild>
reveal_type(super(DynamicChild, fc).method()) # revealed: int
# Regular class inheriting from dynamic class
class RegularChild(DynamicChild):
pass
rc = RegularChild()
reveal_type(super(RegularChild, rc)) # revealed: <super: <class 'RegularChild'>, RegularChild>
reveal_type(super(RegularChild, rc).method()) # revealed: int
# Using dynamic class as pivot with regular class instance owner
reveal_type(super(DynamicChild, rc)) # revealed: <super: <class 'DynamicChild'>, RegularChild>
reveal_type(super(DynamicChild, rc).method()) # revealed: int
```
## Dynamic class inheritance chains
Dynamic classes can inherit from other dynamic classes:
```py
class Base:
base_attr: int = 1
# Create a dynamic class that inherits from a regular class.
Parent = type("Parent", (Base,), {})
reveal_type(Parent) # revealed: <class 'Parent'>
# Create a dynamic class that inherits from another dynamic class.
ChildCls = type("ChildCls", (Parent,), {})
reveal_type(ChildCls) # revealed: <class 'ChildCls'>
# Child instances have access to attributes from the entire inheritance chain.
child = ChildCls()
reveal_type(child) # revealed: ChildCls
reveal_type(child.base_attr) # revealed: int
# Child instances are subtypes of `Parent` instances.
def takes_parent(x: Parent) -> None: ...
takes_parent(child) # No error - `ChildCls` is a subtype of `Parent`
```
## Dataclass transform inheritance
Dynamic classes that inherit from a `@dataclass_transform()` decorated base class are recognized as
dataclass-like and have the synthesized `__dataclass_fields__` attribute:
```py
from dataclasses import Field
from typing_extensions import dataclass_transform
@dataclass_transform()
class DataclassBase:
"""Base class decorated with @dataclass_transform()."""
pass
# A dynamic class inheriting from a dataclass_transform base
DynamicModel = type("DynamicModel", (DataclassBase,), {})
# The dynamic class has __dataclass_fields__ synthesized
reveal_type(DynamicModel.__dataclass_fields__) # revealed: dict[str, Field[Any]]
```
## Applying `@dataclass` decorator directly
Applying the `@dataclass` decorator directly to a dynamic class is supported:
```py
from dataclasses import dataclass
Foo = type("Foo", (), {})
Foo = dataclass(Foo)
reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
```
## Generic base classes
Dynamic classes with generic base classes:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Container(Generic[T]):
value: T
# Dynamic class inheriting from a generic class specialization
IntContainer = type("IntContainer", (Container[int],), {})
reveal_type(IntContainer) # revealed: <class 'IntContainer'>
container = IntContainer()
reveal_type(container) # revealed: IntContainer
reveal_type(container.value) # revealed: int
```
## `type()` and `__class__` on dynamic instances
`type(instance)` returns the class of the dynamic instance:
```py
class Base: ...
Foo = type("Foo", (Base,), {})
foo = Foo()
# type() on an instance returns the class
reveal_type(type(foo)) # revealed: type[Foo]
```
`__class__` attribute access on dynamic instances:
```py
class Base: ...
Foo = type("Foo", (Base,), {})
foo = Foo()
# __class__ returns the class type
reveal_type(foo.__class__) # revealed: type[Foo]
```
`__class__` on the dynamic class itself returns the metaclass (consistent with static classes):
```py
class StaticClass: ...
DynamicClass = type("DynamicClass", (), {})
# Both static and dynamic classes have `type` as their metaclass
reveal_type(StaticClass.__class__) # revealed: <class 'type'>
reveal_type(DynamicClass.__class__) # revealed: <class 'type'>
```
## Subtype relationships
Dynamic instances are subtypes of `object`:
```py
class Base: ...
Foo = type("Foo", (Base,), {})
foo = Foo()
# All dynamic instances are subtypes of object
def takes_object(x: object) -> None: ...
takes_object(foo) # No error - Foo is a subtype of object
# Even dynamic classes with no explicit bases are subtypes of object
EmptyBases = type("EmptyBases", (), {})
empty = EmptyBases()
takes_object(empty) # No error
```
## Attributes from `builtins.type`
Attributes defined on `builtins.type` are accessible on dynamic classes:
```py
T = type("T", (), {})
# Inherited from `builtins.type`:
reveal_type(T.__dictoffset__) # revealed: int
reveal_type(T.__name__) # revealed: str
reveal_type(T.__bases__) # revealed: tuple[type, ...]
reveal_type(T.__mro__) # revealed: tuple[type, ...]
```
## Invalid calls
Other numbers of arguments are invalid:
```py
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Foo", ())) # revealed: Unknown
# TODO: the keyword arguments for `Foo`/`Bar`/`Baz` here are invalid
# (you cannot pass `metaclass=` to `type()`, and none of them have
# base classes with `__init_subclass__` methods),
# but `type[Unknown]` would be better than `Unknown` here
#
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: Unknown
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
# error: [no-matching-overload] "No overload of class `type` matches arguments"
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
```
The following calls are also invalid, due to incorrect argument types:
```py
class Base: ...
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`"
type(b"Foo", (), {})
# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found `<class 'Base'>`"
type("Foo", Base, {})
# error: 14 [invalid-base] "Invalid class base with type `Literal[1]`"
# error: 17 [invalid-base] "Invalid class base with type `Literal[2]`"
type("Foo", (1, 2), {})
# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
type("Foo", (Base,), {b"attr": 1})
```
## `type[...]` as base class
`type[...]` (SubclassOf) types cannot be used as base classes. When a `type[...]` is used in the
bases tuple, we emit a diagnostic and insert `Unknown` into the MRO. This gives exactly one
diagnostic about the unsupported base, rather than cascading errors:
```py
from ty_extensions import reveal_mro
class Base:
base_attr: int = 1
def f(x: type[Base]):
# error: [unsupported-base] "Unsupported class base"
Child = type("Child", (x,), {})
# The class is still created with `Unknown` in MRO, allowing attribute access
reveal_type(Child) # revealed: <class 'Child'>
reveal_mro(Child) # revealed: (<class 'Child'>, Unknown, <class 'object'>)
child = Child()
reveal_type(child) # revealed: Child
# Attributes from `Unknown` are accessible without further errors
reveal_type(child.base_attr) # revealed: Unknown
```
## MRO errors
MRO errors are detected and reported:
```py
class A: ...
# Duplicate bases are detected
# error: [duplicate-base] "Duplicate base class <class 'A'> in class `Dup`"
Dup = type("Dup", (A, A), {})
```
Unknown bases (from unresolved imports) don't trigger duplicate-base diagnostics, since we can't
know if they represent the same type:
```py
from unresolved_module import Bar, Baz # error: [unresolved-import]
# No duplicate-base error here - Bar and Baz are Unknown, and we can't
# know if they're the same type.
X = type("X", (Bar, Baz), {})
```
```py
class A: ...
class B(A): ...
class C(A): ...
# This creates an inconsistent MRO because D would need B before C (from first base)
# but also C before B (from second base inheritance through A)
class X(B, C): ...
class Y(C, B): ...
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Conflict` with bases `[<class 'X'>, <class 'Y'>]`"
Conflict = type("Conflict", (X, Y), {})
```
## Metaclass conflicts
Metaclass conflicts are detected and reported:
```py
class Meta1(type): ...
class Meta2(type): ...
class A(metaclass=Meta1): ...
class B(metaclass=Meta2): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`Bad`) must be a subclass of the metaclasses of all its bases, but `Meta1` (metaclass of base class `<class 'A'>`) and `Meta2` (metaclass of base class `<class 'B'>`) have no subclass relationship"
Bad = type("Bad", (A, B), {})
```
## `__slots__` in namespace dictionary
Functional classes can define `__slots__` in the namespace dictionary. Non-empty `__slots__` makes
the class a "disjoint base", which prevents it from being used alongside other disjoint bases in a
class hierarchy:
```py
# Functional class with non-empty __slots__
Slotted = type("Slotted", (), {"__slots__": ("x", "y")})
slotted = Slotted()
reveal_type(slotted) # revealed: Slotted
# Classes with empty __slots__ are not disjoint bases
EmptySlots = type("EmptySlots", (), {"__slots__": ()})
# Classes with no __slots__ are not disjoint bases
NoSlots = type("NoSlots", (), {})
# String __slots__ are treated as a single slot (non-empty)
StringSlots = type("StringSlots", (), {"__slots__": "x"})
```
Functional classes with non-empty `__slots__` cannot coexist with other disjoint bases:
```py
class RegularSlotted:
__slots__ = ("a",)
# error: [instance-layout-conflict]
class Conflict(
RegularSlotted,
type("FuncSlotted", (), {"__slots__": ("b",)}),
): ...
```
Two functional classes with non-empty `__slots__` also conflict:
```py
A = type("A", (), {"__slots__": ("x",)})
B = type("B", (), {"__slots__": ("y",)})
# error: [instance-layout-conflict]
class Conflict(
A,
B,
): ...
```
When the namespace dictionary is dynamic (not a literal), we can't determine if `__slots__` is
defined, so no diagnostic is emitted:
```py
from typing import Any
class SlottedBase:
__slots__ = ("a",)
def f(ns: dict[str, Any]):
# The namespace might or might not contain __slots__, so no error is emitted
Dynamic = type("Dynamic", (), ns)
# No error: we can't prove there's a conflict since ns might not have __slots__
class MaybeConflict(SlottedBase, Dynamic): ...
```
## Cyclic functional class definitions
Self-referential class definitions using `type()` are detected. The name being defined is referenced
in the bases tuple before it's available:
```pyi
# error: [unresolved-reference] "Name `X` used when not defined"
X = type("X", (X,), {})
```
## Dynamic class names (non-literal strings)
When the class name is not a string literal, we still create a class literal type but with a
placeholder name `<unknown>`:
```py
def make_class(name: str):
# When the name is a dynamic string, we use a placeholder name
cls = type(name, (), {})
reveal_type(cls) # revealed: <class '<unknown>'>
return cls
def make_classes(name1: str, name2: str):
cls1 = type(name1, (), {})
cls2 = type(name2, (), {})
def inner(x: cls1): ...
# error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:8`, found `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:9`"
inner(cls2())
```
When the name comes from a union of string literals, we also use a placeholder name:
```py
import random
name = "Foo" if random.random() > 0.5 else "Bar"
reveal_type(name) # revealed: Literal["Foo", "Bar"]
# We cannot determine which name will be used at runtime
cls = type(name, (), {})
reveal_type(cls) # revealed: <class '<unknown>'>
```
## Dynamic bases (variable tuple)
When the bases tuple is a function parameter with a non-literal tuple type, we still create a class
literal type but with `Unknown` in the MRO. This means instances are treated highly dynamically -
any attribute access returns `Unknown`:
```py
from ty_extensions import reveal_mro
class Base1: ...
class Base2: ...
def make_class(bases: tuple[type, ...]):
# Class literal is created with Unknown base in MRO
cls = type("Cls", bases, {})
reveal_type(cls) # revealed: <class 'Cls'>
reveal_mro(cls) # revealed: (<class 'Cls'>, Unknown, <class 'object'>)
# Instances have dynamic attribute access due to Unknown base
instance = cls()
reveal_type(instance) # revealed: Cls
reveal_type(instance.any_attr) # revealed: Unknown
reveal_type(instance.any_method()) # revealed: Unknown
return cls
```
When `bases` is a module-level variable holding a tuple of class literals, we can extract the base
classes:
```py
class Base:
attr: int = 1
bases = (Base,)
Cls = type("Cls", bases, {})
reveal_type(Cls) # revealed: <class 'Cls'>
instance = Cls()
reveal_type(instance.attr) # revealed: int
```
## Variadic arguments
Unpacking arguments with `*args` or `**kwargs`:
```py
class Base: ...
# Unpacking a tuple for bases
bases_tuple = (Base,)
Cls1 = type("Cls1", (*bases_tuple,), {})
reveal_type(Cls1) # revealed: <class 'Cls1'>
# Unpacking a dict for the namespace - the dict contents are not tracked anyway
namespace = {"attr": 1}
Cls2 = type("Cls2", (Base,), {**namespace})
reveal_type(Cls2) # revealed: <class 'Cls2'>
```
When `*args` or `**kwargs` fill an unknown number of parameters, we cannot determine which overload
of `type()` is being called:
```py
def f(*args, **kwargs):
# Completely dynamic: could be 1-arg or 3-arg form
A = type(*args, **kwargs)
reveal_type(A) # revealed: type[Unknown]
# Has a string first arg, but unknown additional args from *args
B = type("B", *args, **kwargs)
# TODO: `type[Unknown]` would cause fewer false positives
reveal_type(B) # revealed: <class 'str'>
# Has string and tuple, but unknown additional args
C = type("C", (), *args, **kwargs)
# TODO: `type[Unknown]` would cause fewer false positives
reveal_type(C) # revealed: type
# All three positional args provided, only **kwargs unknown
D = type("D", (), {}, **kwargs)
# TODO: `type[Unknown]` would cause fewer false positives
reveal_type(D) # revealed: type
```
## Explicit type annotations
When an explicit type annotation is provided, the inferred type is checked against it:
```py
# The annotation `type` is compatible with the inferred class literal type
T: type = type("T", (), {})
reveal_type(T) # revealed: <class 'T'>
# The annotation `type[Base]` is compatible with the inferred type
class Base: ...
Derived: type[Base] = type("Derived", (Base,), {})
reveal_type(Derived) # revealed: <class 'Derived'>
# Incompatible annotation produces an error
class Unrelated: ...
# error: [invalid-assignment]
Bad: type[Unrelated] = type("Bad", (Base,), {})
```
## Special base classes
Some special base classes work with dynamic class creation, but special semantics may not be fully
synthesized:
### Protocol bases
```py
# Protocol bases work - the class is created as a subclass of the protocol
from typing import Protocol
class MyProtocol(Protocol):
def method(self) -> int: ...
ProtoImpl = type("ProtoImpl", (MyProtocol,), {})
reveal_type(ProtoImpl) # revealed: <class 'ProtoImpl'>
instance = ProtoImpl()
reveal_type(instance) # revealed: ProtoImpl
```
### TypedDict bases
```py
# TypedDict bases work but TypedDict semantics aren't applied to dynamic subclasses
from typing_extensions import TypedDict
class MyDict(TypedDict):
name: str
age: int
DictSubclass = type("DictSubclass", (MyDict,), {})
reveal_type(DictSubclass) # revealed: <class 'DictSubclass'>
```
### NamedTuple bases
```py
# NamedTuple bases work but the dynamic subclass isn't recognized as a NamedTuple
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
Point3D = type("Point3D", (Point,), {})
reveal_type(Point3D) # revealed: <class 'Point3D'>
```
### Enum bases
```py
# Enum subclassing via type() is not supported - EnumMeta requires special dict handling
# that type() cannot provide. This applies even to empty enums.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
class EmptyEnum(Enum):
pass
# TODO: We should emit a diagnostic here - type() cannot create Enum subclasses
ExtendedColor = type("ExtendedColor", (Color,), {})
reveal_type(ExtendedColor) # revealed: <class 'ExtendedColor'>
# Even empty enums fail - EnumMeta requires special dict handling
# TODO: We should emit a diagnostic here too
ValidExtension = type("ValidExtension", (EmptyEnum,), {})
reveal_type(ValidExtension) # revealed: <class 'ValidExtension'>
```
## `__init_subclass__` keyword arguments
When a base class defines `__init_subclass__` with required arguments, those should be passed to
`type()`. This is not yet supported:
```py
class Base:
def __init_subclass__(cls, required_arg: str, **kwargs):
super().__init_subclass__(**kwargs)
cls.config = required_arg
# Regular class definition - this works and passes the argument
class Child(Base, required_arg="value"):
pass
# The dynamically assigned attribute has Unknown in its type
reveal_type(Child.config) # revealed: Unknown | str
# Dynamic class creation - keyword arguments are not yet supported
# TODO: This should work: type("DynamicChild", (Base,), {}, required_arg="value")
# error: [no-matching-overload]
DynamicChild = type("DynamicChild", (Base,), {}, required_arg="value")
```
## Empty bases tuple
When the bases tuple is empty, the class implicitly inherits from `object`:
```py
from ty_extensions import reveal_mro
EmptyBases = type("EmptyBases", (), {})
reveal_type(EmptyBases) # revealed: <class 'EmptyBases'>
reveal_mro(EmptyBases) # revealed: (<class 'EmptyBases'>, <class 'object'>)
instance = EmptyBases()
reveal_type(instance) # revealed: EmptyBases
# object methods are available
reveal_type(instance.__hash__()) # revealed: int
reveal_type(instance.__str__()) # revealed: str
```
## Custom metaclass via bases
When a base class has a custom metaclass, the dynamic class inherits that metaclass:
```py
class MyMeta(type):
custom_attr: str = "meta"
class Base(metaclass=MyMeta): ...
# Dynamic class inherits the metaclass from Base
Dynamic = type("Dynamic", (Base,), {})
reveal_type(Dynamic) # revealed: <class 'Dynamic'>
# Metaclass attributes are accessible on the class
reveal_type(Dynamic.custom_attr) # revealed: str
```

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,42 +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
```
## Dynamic class literals
Dynamic classes created with `type()` can be wrapped with `dataclass()` as a function:
```py
from dataclasses import dataclass
# Basic dynamic class wrapped with dataclass
DynamicFoo = type("DynamicFoo", (), {})
DynamicFoo = dataclass(DynamicFoo)
# The class is recognized as a dataclass
reveal_type(DynamicFoo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
# Can create instances
instance = DynamicFoo()
reveal_type(instance) # revealed: DynamicFoo
```
Dynamic classes that inherit from a dataclass base also work:
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
# Dynamic class inheriting from a dataclass
DynamicChild = type("DynamicChild", (Base,), {})
DynamicChild = dataclass(DynamicChild)
reveal_type(DynamicChild.__dataclass_fields__) # revealed: dict[str, Field[Any]]
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

@@ -38,125 +38,6 @@ reveal_type(s1 > s2) # revealed: bool
reveal_type(s1 >= s2) # revealed: bool
```
## Signature derived from source ordering method
When the source ordering method accepts a broader type (like `object`) for its `other` parameter,
the synthesized comparison methods should use the same signature. This allows comparisons with types
other than the class itself:
```py
from functools import total_ordering
@total_ordering
class Comparable:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
if isinstance(other, Comparable):
return self.value == other.value
if isinstance(other, int):
return self.value == other
return NotImplemented
def __lt__(self, other: object) -> bool:
if isinstance(other, Comparable):
return self.value < other.value
if isinstance(other, int):
return self.value < other
return NotImplemented
a = Comparable(10)
b = Comparable(20)
# Comparisons with the same type work.
reveal_type(a <= b) # revealed: bool
reveal_type(a >= b) # revealed: bool
# Comparisons with `int` also work because `__lt__` accepts `object`.
reveal_type(a <= 15) # revealed: bool
reveal_type(a >= 5) # revealed: bool
```
## Multiple ordering methods with different signatures
When multiple ordering methods are defined with different signatures, the decorator selects a "root"
method using the priority order: `__lt__` > `__le__` > `__gt__` > `__ge__`. Synthesized methods use
the signature from the highest-priority method. Methods that are explicitly defined are not
overridden.
```py
from functools import total_ordering
@total_ordering
class MultiSig:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
return True
# __lt__ accepts `object` (highest priority, used as root)
def __lt__(self, other: object) -> bool:
return True
# __gt__ only accepts `MultiSig` (not overridden by decorator)
def __gt__(self, other: "MultiSig") -> bool:
return True
a = MultiSig(10)
b = MultiSig(20)
# __le__ and __ge__ are synthesized with __lt__'s signature (accepts `object`)
reveal_type(a <= b) # revealed: bool
reveal_type(a <= 15) # revealed: bool
reveal_type(a >= b) # revealed: bool
reveal_type(a >= 15) # revealed: bool
# __gt__ keeps its original signature (only accepts MultiSig)
reveal_type(a > b) # revealed: bool
a > 15 # error: [unsupported-operator]
```
## Overloaded ordering method
When the source ordering method is overloaded, the synthesized comparison methods should preserve
all overloads:
```py
from functools import total_ordering
from typing import overload
@total_ordering
class Flexible:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
return True
@overload
def __lt__(self, other: "Flexible") -> bool: ...
@overload
def __lt__(self, other: int) -> bool: ...
def __lt__(self, other: "Flexible | int") -> bool:
if isinstance(other, Flexible):
return self.value < other.value
return self.value < other
a = Flexible(10)
b = Flexible(20)
# Synthesized __le__ preserves overloads from __lt__
reveal_type(a <= b) # revealed: bool
reveal_type(a <= 15) # revealed: bool
# Synthesized __ge__ also preserves overloads
reveal_type(a >= b) # revealed: bool
reveal_type(a >= 15) # revealed: bool
# But comparison with an unsupported type should still error
a <= "string" # error: [unsupported-operator]
```
## Using `__gt__` as the root comparison method
When a class defines `__eq__` and `__gt__`, the decorator synthesizes `__lt__`, `__le__`, and
@@ -246,41 +127,6 @@ reveal_type(c1 > c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Method precedence with inheritance
The decorator always prefers `__lt__` > `__le__` > `__gt__` > `__ge__`, regardless of whether the
method is defined locally or inherited. In this example, the inherited `__lt__` takes precedence
over the locally-defined `__gt__`:
```py
from functools import total_ordering
from typing import Literal
class Base:
def __lt__(self, other: "Base") -> Literal[True]:
return True
@total_ordering
class Child(Base):
# __gt__ is defined locally, but __lt__ (inherited) takes precedence
def __gt__(self, other: "Child") -> Literal[False]:
return False
c1 = Child()
c2 = Child()
# __lt__ is inherited from Base
reveal_type(c1 < c2) # revealed: Literal[True]
# __gt__ is defined locally on Child
reveal_type(c1 > c2) # revealed: Literal[False]
# __le__ and __ge__ are synthesized from __lt__ (the highest-priority method),
# even though __gt__ is defined locally on the class itself
reveal_type(c1 <= c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Explicitly-defined methods are not overridden
When a class explicitly defines multiple comparison methods, the decorator does not override them.
@@ -398,176 +244,3 @@ reveal_type(n1 > n2) # revealed: bool
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```
## Non-bool return type
When the root ordering method returns a non-bool type (like `int`), the synthesized methods return a
union of that type and `bool`. This is because `@total_ordering` generates methods like:
```python
def __le__(self, other):
return self < other or self == other
```
If `__lt__` returns `int`, then the synthesized `__le__` could return either `int` (from
`self < other`) or `bool` (from `self == other`). Since `bool` is a subtype of `int`, the union
simplifies to `int`:
```py
from functools import total_ordering
@total_ordering
class IntReturn:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
if not isinstance(other, IntReturn):
return NotImplemented
return self.value == other.value
def __lt__(self, other: "IntReturn") -> int:
return self.value - other.value
a = IntReturn(10)
b = IntReturn(20)
# User-defined __lt__ returns int.
reveal_type(a < b) # revealed: int
# Synthesized methods return int (the union int | bool simplifies to int
# because bool is a subtype of int in Python).
reveal_type(a <= b) # revealed: int
reveal_type(a > b) # revealed: int
reveal_type(a >= b) # revealed: int
```
When the root method returns a type that is not a supertype of `bool`, the union is preserved:
```py
from functools import total_ordering
@total_ordering
class StrReturn:
def __init__(self, value: str):
self.value = value
def __eq__(self, other: object) -> bool:
if not isinstance(other, StrReturn):
return NotImplemented
return self.value == other.value
def __lt__(self, other: "StrReturn") -> str:
return self.value
a = StrReturn("a")
b = StrReturn("b")
# User-defined __lt__ returns str.
reveal_type(a < b) # revealed: str
# Synthesized methods return str | bool.
reveal_type(a <= b) # revealed: str | bool
reveal_type(a > b) # revealed: str | bool
reveal_type(a >= b) # revealed: str | bool
```
## 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]
```
## Function call form with `type()`
When `total_ordering` is called on a class created with `type()`, the same validation is performed:
```py
from functools import total_ordering
def lt_impl(self, other) -> bool:
return True
# No error: the functional class defines `__lt__` in its namespace
ValidFunctional = total_ordering(type("ValidFunctional", (), {"__lt__": lt_impl}))
# error: [invalid-total-ordering]
InvalidFunctional = total_ordering(type("InvalidFunctional", (), {}))
```
## Inherited from functional class
When a class inherits from a functional class that defines an ordering method, `@total_ordering`
correctly detects it:
```py
from functools import total_ordering
def lt_impl(self, other) -> bool:
return True
def eq_impl(self, other) -> bool:
return True
# Functional class with __lt__ method
OrderedBase = type("OrderedBase", (), {"__lt__": lt_impl})
# A class inheriting from OrderedBase gets the ordering method
@total_ordering
class Ordered(OrderedBase):
def __eq__(self, other: object) -> bool:
return True
o1 = Ordered()
o2 = Ordered()
# Inherited __lt__ is available
reveal_type(o1 < o2) # revealed: bool
# @total_ordering synthesizes the other methods
reveal_type(o1 <= o2) # revealed: bool
reveal_type(o1 > o2) # revealed: bool
reveal_type(o1 >= o2) # revealed: bool
```
When the functional base class does not define any ordering method, `@total_ordering` emits an
error:
```py
from functools import total_ordering
# Functional class without ordering methods (invalid for @total_ordering)
NoOrderBase = type("NoOrderBase", (), {})
@total_ordering # error: [invalid-total-ordering]
class NoOrder(NoOrderBase):
def __eq__(self, other: object) -> bool:
return True
```

View File

@@ -870,102 +870,6 @@ static_assert(not has_member(F, "__match_args__"))
static_assert(not has_member(F(), "__weakref__"))
```
### Dynamic classes (created via `type()`)
Dynamic classes created using the three-argument form of `type()` support autocomplete for members
inherited from their base classes on the class object:
```py
from ty_extensions import has_member, static_assert
class Base:
base_attr: int = 1
def base_method(self) -> str:
return "hello"
class Mixin:
mixin_attr: str = "mixin"
# Dynamic class with a single base
DynamicSingle = type("DynamicSingle", (Base,), {})
# The class object has access to base class attributes
static_assert(has_member(DynamicSingle, "base_attr"))
static_assert(has_member(DynamicSingle, "base_method"))
# Dynamic class with multiple bases
DynamicMulti = type("DynamicMulti", (Base, Mixin), {})
static_assert(has_member(DynamicMulti, "base_attr"))
static_assert(has_member(DynamicMulti, "mixin_attr"))
```
Members from `object` and the `type` metaclass are available on the class object:
```py
from ty_extensions import has_member, static_assert
Dynamic = type("Dynamic", (), {})
# object members are available on the class
static_assert(has_member(Dynamic, "__doc__"))
static_assert(has_member(Dynamic, "__init__"))
# type metaclass members are available on the class
static_assert(has_member(Dynamic, "__name__"))
static_assert(has_member(Dynamic, "__bases__"))
static_assert(has_member(Dynamic, "__mro__"))
static_assert(has_member(Dynamic, "__subclasses__"))
```
Attributes from the namespace dict (third argument) are not tracked:
```py
from ty_extensions import has_member, static_assert
DynamicWithDict = type("DynamicWithDict", (), {"custom_attr": 42})
# TODO: these should pass -- namespace dict attributes are not yet available for autocomplete
static_assert(has_member(DynamicWithDict, "custom_attr")) # error: [static-assert-error]
static_assert(has_member(DynamicWithDict(), "custom_attr")) # error: [static-assert-error]
```
Dynamic classes inheriting from classes with custom metaclasses get metaclass members:
```py
from ty_extensions import has_member, static_assert
class MyMeta(type):
meta_attr: str = "meta"
class Base(metaclass=MyMeta):
base_attr: int = 1
Dynamic = type("Dynamic", (Base,), {})
# Metaclass attributes are available on the class
static_assert(has_member(Dynamic, "meta_attr"))
static_assert(has_member(Dynamic, "base_attr"))
```
However, instances of dynamic classes currently do not expose members for autocomplete:
```py
from ty_extensions import has_member, static_assert
class Base:
base_attr: int = 1
DynamicSingle = type("DynamicSingle", (Base,), {})
instance = DynamicSingle()
# TODO: these should pass; instance members should be available
static_assert(has_member(instance, "base_attr")) # error: [static-assert-error]
static_assert(has_member(instance, "__repr__")) # error: [static-assert-error]
static_assert(has_member(instance, "__hash__")) # error: [static-assert-error]
```
### Attributes not available at runtime
Typeshed includes some attributes in `object` that are not available for some (builtin) types. For

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

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