Compare commits

...

46 Commits

Author SHA1 Message Date
Alex Waygood
c872fe4c08 generalize fast path more...? 2026-01-10 15:26:09 +00:00
Alex Waygood
f497167798 use an FxOrderSet in UnionType 2026-01-10 14:07:13 +00:00
Charlie Marsh
046c5a46d8 [ty] Support dataclass_transform as a function call (#22378)
## Summary

Instead of just as a decorator.

Closes https://github.com/astral-sh/ty/issues/2319.
2026-01-10 08:45:45 -05:00
Micha Reiser
cfed34334c Update mypy primer pin (#22490) 2026-01-10 14:00:00 +01:00
Charlie Marsh
11cc324449 [ty] Detect invalid @total_ordering applications in non-decorator contexts (#22486)
## Summary

E.g., `ValidOrderedClass = total_ordering(HasOrderingMethod)`.
2026-01-09 19:37:58 -05:00
Charlie Marsh
c88e1a0663 [ty] Avoid emitting Liskov repeated violations from grandparent to child (#22484)
## Summary

If parent violates LSP against grandparent, and child has the same
violation (but matches parent), we no longer flag the LSP violation on
child, since it can't be fixed without violating parent.

If parent violates LSP against grandparent, and child violates LSP
against both parent and grandparent, we emit two diagnostics (one for
each violation).

If parent violates LSP against grandparent, and child violates LSP
against parent (but not grandparent), we flag it.

Closes https://github.com/astral-sh/ty/issues/2000.
2026-01-09 19:27:57 -05:00
William Woodruff
2c7ac17b1e Remove two old zizmor ignore comments (#22485) 2026-01-09 18:02:44 -05:00
Matthias Schoettle
a0f2cd0ded Add language: golang to actionlint pre-commit hook (#22483) 2026-01-09 19:30:00 +00:00
Alex Waygood
dc61104726 [ty] Derive Default in a few more places in place.rs (#22481) 2026-01-09 18:37:36 +00:00
Jason K Hall
f40c578ffb [ruff] document RUF100 trailing comment fix behavior (#22479)
## Summary
Ruff's `--fix` for `RUF100` can inadvertently remove trailing comments
(e.g., `pylint` or `mypy` suppressions) by interpreting them as
descriptions. This PR adds a "Conflict with other linters" section to
the rule documentation to clarify this behavior and provide the
double-hash (`# noqa # pylint`) workaround.

## Fixes
Fixes #20762
2026-01-09 09:01:09 -08:00
Leo Hayashi
baaf6966f6 [ruff] Split WASM build and publish into separate workflows (#12387) (#22476)
Co-authored-by: Hayashi Reo <reo@wantedly.com>
2026-01-09 17:41:13 +01:00
Zanie Blue
a6df4a3be7 Add llms.txt support for documentation (#22463)
Matching uv.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 09:56:41 -06:00
Micha Reiser
1094009790 Fix configuration path in --show-settings (#22478) 2026-01-09 16:28:57 +01:00
Andrew Gallant
10eb3d52d5 [ty] Rank top-level module symbols above most other symbols
This makes it so, e.g., `os<CURSOR>` will suggest the top-level stdlib
`os` module even if there is an `os` symbol elsewhere in your project.

The way this is done is somewhat overwrought, but it's done to avoid
suggesting top-level modules over other symbols already in scope.

Fixes astral-sh/issues#1852
2026-01-09 10:11:37 -05:00
Andrew Gallant
91d24ebb92 [ty] Give exact completion matches very high ranking
Aside from ruling out "impossible" completions in the current context,
we give completions with an exact match the highest possible ranking.
2026-01-09 09:55:32 -05:00
Andrew Gallant
e68fba20a9 [ty] Remove type-var-typing-over-ast evaluation task
This is now strictly redundant with the `typing-gets-priority` task.
2026-01-09 09:55:32 -05:00
Andrew Gallant
64117c1146 [ty] Improve completion ranking based on origin of symbols
Part of this was already done, but it was half-assed. We now look at the
search path that a symbol came from and centralize a symbol's origin
classification.

The preference ordering here is maybe not the right one, but we can
iterate as users give us feedback. Note also that the preference
ordering based on the origin is pretty low in the relevance sorting.
This means that other more specific criteria will and can override this.

This results in some nice improvements to our evaluation tasks.
2026-01-09 09:55:32 -05:00
Andrew Gallant
2196ef3a33 [ty] Add package priority completion evaluation tasks
This commit adds two new tests. One checks that a symbol in the current
project gets priority over a symbol in the standard library. Another
checks that a symbol in a third party dependency gets priority over a
symbol in the standard library. We don't get either of these right
today.

Note that these comparisons are done ceteris paribus. A symbol from the
standard library could still be ranked above a symbol elsewhere.
(Although I believe currently this is somewhat rare.)
2026-01-09 09:55:32 -05:00
Andrew Gallant
c36397031b [ty] Actually ignore hidden directories in completion evaluation
I apparently don't know how to use my own API. Previously,
we would skip, e.g., `.venv`, but still descend into it.

This was annoying in practice because I sometimes have an
environment in one of the truth task directories. The eval
command should ignore that entirely, but it ended up
choking on it without properly ignoring hidden files
and directories.
2026-01-09 09:55:32 -05:00
Andrew Gallant
eef34958f9 [ty] More sensible sorting for results returned by all_symbols
Previously, we would only sort by name and file path (unless the symbol
refers to an entire module). This led to somewhat confusing ordering,
since the file path is absolute and can be anything.

Sorting by module name after symbol name gives a more predictable
ordering in common practice.

This is mostly just an improvement for debugging purposes, i.e., when
looking at the output of `all_symbols` directly. This mostly shouldn't
impact completion ordering since completions do their own ranking.
2026-01-09 09:55:32 -05:00
Andrew Gallant
f1bd5f1941 [ty] Rank unimported symbols from typing higher
This addresses a number of minor annoyances where users really want
imports from `typing` most of the time.

For example:
https://github.com/astral-sh/ty/issues/1274#issuecomment-3345884227
2026-01-09 09:55:32 -05:00
Andrew Gallant
56862f8241 [ty] Refactor ranking
This moves the information we want to use to rank completions into
`Completion` itself. (This was the primary motivation for creating a
`CompletionBuilder` in the first place.)

The principal advantage here is that we now only need to compute the
relevance information for each completion exactly once. Previously, we
were computing it on every comparison, which might end up doing
redundant work for a not insignifcant number of completions.

The relevance information is also specifically constructed from the
builder so that, in the future, we might choose to short-circuit
construction if we know we'll never send it back to the client (e.g.,
its ranking is worse than the lowest ranked completion). But we don't
implement that optimization yet.
2026-01-09 09:55:32 -05:00
Andrew Gallant
e9cc2f6f42 [ty] Refactor completion construction to use a builder
I want to be able to attach extra data to each `Completion`, but not
burden callers with the need to construct it. This commit helps get us
to that point by requiring callers to use a `CompletionBuilder` for
construction instead of a `Completion` itself.

I think this will also help in the future if it proves to be the case
that we can improve performance by delaying work until we actually build
a `Completion`, which might only happen if we know we won't throw it
out. But we aren't quite there yet.

This also lets us tighten things up a little bit and makes completion
construction less noisy. The downside is that callers no longer need to
consider "every" completion field.

There should not be any behavior changes here.
2026-01-09 09:55:32 -05:00
Andrew Gallant
8dd56d4264 [ty] Rename internal completion Rank type to Relevance
I think "relevance" captures the meaning of this type more precisely.
2026-01-09 09:55:32 -05:00
Andrew Gallant
d4c1b0ccc7 [ty] Add evaluation tasks for a few cases
I went through https://github.com/astral-sh/ty/issues/1274 and tried to
extract what I could into eval tasks. Some of the suggestions from that
issue have already been done, but most haven't.

This captures the status quo.
2026-01-09 09:55:32 -05:00
Micha Reiser
e61657ff3c [ty] Enable unused-type-ignore-comment by default (#22474) 2026-01-09 10:58:43 +00:00
Micha Reiser
ba5dd5837c [ty] Pass slice to specialize (#22421) 2026-01-09 09:45:39 +01:00
Micha Reiser
c5f6a74da5 [ty] Fix goto definition for relative imports in third-party files (#22457) 2026-01-09 09:30:31 +01:00
Micha Reiser
b3cde98cd1 [ty] Update salsa (#22473) 2026-01-09 09:21:45 +01:00
Carl Meyer
f9f7a6901b Complete minor TODO in ty_python_semantic crate (#22468)
This TODO is very old -- we have long since recorded this definition.
Updating the test to actually assert the declaration requires a new
helper method for declarations, to complement the existing
`first_public_binding` helper.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-08 16:04:34 -08:00
Dylan
c920cf8cdb Bump 0.14.11 (#22462) 2026-01-08 12:51:47 -06:00
Micha Reiser
bb757b5a79 [ty] Don't show diagnostics for excluded files (#22455) 2026-01-08 18:27:28 +01:00
Charlie Marsh
1f49e8ef51 Include configured src directories when resolving graphs (#22451)
## Summary

This PR augments the detected source paths with the user-configured
`src` when computing roots for `ruff analyze graph`.
2026-01-08 15:19:15 +00:00
Douglas Creager
701f5134ab [ty] Only consider fully static pivots when deriving transitive constraints (#22444)
When working with constraint sets, we track transitive relationships
between the constraints in the set. For instance, in `S ≤ int ∧ int ≤
T`, we can infer that `S ≤ T`. However, we should only consider fully
static types when looking for a "pivot" for this kind of transitive
relationship. The same pattern does not hold for `S ≤ Any ∧ Any ≤ T`;
because the two `Any`s can materialize to different types, we cannot
infer that `S ≤ T`.

Fixes https://github.com/astral-sh/ty/issues/2371
2026-01-08 09:31:55 -05:00
Micha Reiser
eea9ad8352 Pin maturin version (#22454) 2026-01-08 12:39:51 +01:00
Alex Waygood
eeac2bd3ee [ty] Optimize union building for unions with many enum-literal members (#22363) 2026-01-08 10:50:04 +00:00
Jason K Hall
7319c37f4e docs: fix jupyter notebook discovery info for editors (#22447)
Resolves #21892

## Summary

This PR updates `docs/editors/features.md` to clarify that Jupyter
Notebooks are now included by default as of version 0.6.0.
2026-01-08 11:52:01 +05:30
Amethyst Reese
805503c19a [ruff] Improve fix title for RUF102 invalid rule code (#22100)
## Summary

Updates the fix title for RUF102 to either specify which rule code to
remove, or clarify
that the entire suppression comment should be removed.

## Test Plan

Updated test snapshots.
2026-01-07 17:23:18 -08:00
Charlie Marsh
68a2f6c57d [ty] Fix super() with TypeVar-annotated self and cls parameter (#22208)
## Summary

This PR fixes `super()` handling when the first parameter (`self` or
`cls`) is annotated with a TypeVar, like `Self`.

Previously, `super()` would incorrectly resolve TypeVars to their bounds
before creating the `BoundSuperType`. So if you had `self: Self` where
`Self` is bounded by `Parent`, we'd process `Parent` as a
`NominalInstance` and end up with `SuperOwnerKind::Instance(Parent)`.

As a result:

```python
class Parent:
    @classmethod
    def create(cls) -> Self:
        return cls()

class Child(Parent):
    @classmethod
    def create(cls) -> Self:
        return super().create()  # Error: Argument type `Self@create` does not satisfy upper bound `Parent`
```

We now track two additional variants on `SuperOwnerKind` for TypeVar
owners:

- `InstanceTypeVar`: for instance methods where self is a TypeVar (e.g.,
`self: Self`).
- `ClassTypeVar`: for classmethods where `cls` is a `TypeVar` wrapped in
`type[...]` (e.g., `cls: type[Self]`).

Closes https://github.com/astral-sh/ty/issues/2122.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2026-01-07 19:56:09 -05:00
Alex Waygood
abaa735e1d [ty] Improve UnionBuilder performance by changing Type::is_subtype_of calls to Type::is_redundant_with (#22337) 2026-01-07 22:17:44 +00:00
Jelle Zijlstra
c02d164357 Check required-version before parsing rules (#22410)
Co-authored-by: Micha Reiser <micha@reiser.io>
2026-01-07 17:29:27 +00:00
Andrew Gallant
88aa3f82f0 [ty] Fix generally poor ranking in playground completions
We enabled [`CompletionListisIncomplete`] in our LSP server a while back
in order to have more of a say in how we rank and filter completions.
When it isn't set, the client tends to ask for completions less
frequently and will instead do its own filtering.

But... we did not enable it for the playground. Which I guess didn't
result in anything noticeably bad until we started limiting completions
to 1,000 suggestions. This meant that if the _initial_ completion
response didn't include the ultimate desired answer, then it would never
show up in the results until the client requested completions again.
This in turn led to some very poor completions in some cases.

This all gets fixed by simply enabling `isIncomplete` for Monaco.

Fixes astral-sh/ty#2340

[`CompletionList::isIncomplete`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionList)
2026-01-07 12:25:26 -05:00
Carl Meyer
30902497db [ty] Make signature return and parameter types non-optional (#22425)
## Summary

Fixes https://github.com/astral-sh/ty/issues/2363
Fixes https://github.com/astral-sh/ty/issues/2013

And several other bugs with the same root cause. And makes any similar
bugs impossible by construction.

Previously we distinguished "no annotation" (Rust `None`) from
"explicitly annotated with something of type `Unknown`" (which is not an
error, and results in the annotation being of Rust type
`Some(Type::DynamicType(Unknown))`), even though semantically these
should be treated the same.

This was a bit of a bug magnet, because it was easy to forget to make
this `None` -> `Unknown` translation everywhere we needed to. And in
fact we did fail to do it in the case of materializing a callable,
leading to a top-materialized callable still having (rust) `None` return
type, which should have instead materialized to `object`.

This also fixes several other bugs related to not handling un-annotated
return types correctly:
1. We previously considered the return type of an unannotated `async
def` to be `Unknown`, where it should be `CoroutineType[Any, Any,
Unknown]`.
2. We previously failed to infer a ParamSpec if the return type of the
callable we are inferring against was not annotated.
3. We previously wrongly returned `Unknown` from `some_dict.get("key",
None)` if the value type of `some_dict` included a callable type with
un-annotated return type.

We now make signature return types and annotated parameter types
required, and we eagerly insert `Unknown` if there's no annotation. Most
of the diff is just a bunch of mechanical code changes where we
construct these types, and simplifications where we use them.

One exception is type display: when a callable type has un-annotated
parameters, we want to display them as un-annotated, but if it has a
parameter explicitly annotated with something of `Unknown` type, we want
to display that parameter as `x: Unknown` (it would be confusing if it
looked like your annotation just disappeared entirely).

Fortunately, we already have a mechanism in place for handling this: the
`inferred_annotation` flag, which suppresses display of an annotation.
Previously we used it only for `self` and `cls` parameters with an
inferred annotated type -- but we now also set it for any un-annotated
parameter, for which we infer `Unknown` type.

We also need to normalize `inferred_annotation`, since it's display-only
and shouldn't impact type equivalence. (This is technically a
previously-existing bug, it just never came up when it only affected
self types -- now it comes up because we have tests asserting that `def
f(x)` and `def g(x: Unknown)` are equivalent.)

## Test Plan

Added mdtests.
2026-01-07 09:18:39 -08:00
Alex Waygood
3ad99fb1f4 [ty] Fix an mdtest title (#22439) 2026-01-07 16:34:56 +00:00
Micha Reiser
d0ff59cfe5 [ty] Use Pool from regex_automata to reuse the matches allocations (#22438) 2026-01-07 17:22:35 +01:00
Andrew Gallant
952193e0c6 [ty] Offer completions for T when a value has type Unknown | T
Fixes astral-sh/ty#2197
2026-01-07 10:15:36 -05:00
139 changed files with 4667 additions and 1592 deletions

View File

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

View File

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

58
.github/workflows/build-wasm.yml vendored Normal file
View File

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

View File

@@ -1,25 +1,18 @@
# Build and publish ruff-api for wasm.
# Publish ruff_wasm to npm.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish
# job within `cargo-dist`.
name: "Build and publish wasm"
name: "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:
ruff_wasm:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
@@ -29,31 +22,19 @@ jobs:
target: [web, bundler, nodejs]
fail-fast: false
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
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: artifacts-wasm-${{ matrix.target }}
path: 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 crates/ruff_wasm/pkg
run: npm publish --dry-run pkg
- name: "Publish"
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
run: npm publish --provenance --access public crates/ruff_wasm/pkg
run: npm publish --provenance --access public pkg
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -112,12 +112,22 @@ 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 }}
@@ -165,9 +175,10 @@ 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') }}
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') }}
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 # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
enable-cache: true
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
lookup-only: false
- name: Install Rust toolchain
run: rustup show

View File

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

View File

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

14
Cargo.lock generated
View File

@@ -2912,7 +2912,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.10"
version = "0.14.11"
dependencies = [
"anyhow",
"argfile",
@@ -2928,6 +2928,7 @@ dependencies = [
"filetime",
"globwalk",
"ignore",
"indexmap",
"indoc",
"insta",
"insta-cmd",
@@ -3171,7 +3172,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.10"
version = "0.14.11"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3529,7 +3530,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.10"
version = "0.14.11"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3645,7 +3646,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
dependencies = [
"boxcar",
"compact_str",
@@ -3670,12 +3671,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
[[package]]
name = "salsa-macros"
version = "0.25.2"
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
dependencies = [
"proc-macro2",
"quote",
@@ -4443,6 +4444,7 @@ 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 = "309c249088fdeef0129606fa34ec2eefc74736ff", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "9860ff6ca0f1f8f3a8d6b832020002790b501254", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -150,8 +150,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.10/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.10/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.11/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.11/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -184,7 +184,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.10
rev: v0.14.11
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ 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,6 +50,56 @@ 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

@@ -0,0 +1,410 @@
---
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::{EnvironmentOptions, Options};
use ty_project::metadata::options::{AnalysisOptions, EnvironmentOptions, Options};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ChangedKind};
use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata};
@@ -67,6 +67,7 @@ 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();
@@ -85,6 +86,10 @@ 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()
});
@@ -755,7 +760,7 @@ fn datetype(criterion: &mut Criterion) {
max_dep_date: "2025-07-04",
python_version: PythonVersion::PY313,
},
2,
4,
);
bench_project(&benchmark, criterion);

View File

@@ -71,6 +71,8 @@ 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();
@@ -79,6 +81,12 @@ 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(
@@ -101,7 +109,7 @@ static ALTAIR: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
1000,
850,
);
static COLOUR_SCIENCE: Benchmark = Benchmark::new(
@@ -120,7 +128,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310,
},
1070,
350,
);
static FREQTRADE: Benchmark = Benchmark::new(
@@ -163,7 +171,7 @@ static PANDAS: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
4000,
3800,
);
static PYDANTIC: Benchmark = Benchmark::new(
@@ -181,7 +189,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
7000,
3200,
);
static SYMPY: Benchmark = Benchmark::new(
@@ -194,7 +202,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13116,
13400,
);
static TANJUN: Benchmark = Benchmark::new(
@@ -207,7 +215,7 @@ static TANJUN: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
320,
110,
);
static STATIC_FRAME: Benchmark = Benchmark::new(
@@ -223,7 +231,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
1100,
1700,
);
#[track_caller]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,8 +102,8 @@ impl Relativity {
#[derive(Debug)]
pub struct Resolver<'a> {
pyproject_config: &'a PyprojectConfig,
/// All [`Settings`] that have been added to the resolver.
settings: Vec<Settings>,
/// All [`Settings`] that have been added to the resolver, along with their config file paths.
settings: Vec<(Settings, PathBuf)>,
/// 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) {
self.settings.push(settings);
fn add(&mut self, path: &Path, settings: Settings, config_path: PathBuf) {
self.settings.push((settings, config_path));
// Normalize the path to use `/` separators and escape the '{' and '}' characters,
// which matchit uses for routing parameters.
@@ -172,13 +172,27 @@ 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,
PyprojectDiscoveryStrategy::Fixed => (
&self.pyproject_config.settings,
self.pyproject_config.path.as_deref(),
),
PyprojectDiscoveryStrategy::Hierarchical => self
.router
.at(path.to_slash_lossy().as_ref())
.map(|Match { value, .. }| &self.settings[*value])
.unwrap_or(&self.pyproject_config.settings),
.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(),
)),
}
}
@@ -255,7 +269,8 @@ 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)
std::iter::once(&self.pyproject_config.settings)
.chain(self.settings.iter().map(|(settings, _)| settings))
}
}
@@ -379,17 +394,17 @@ pub fn resolve_configuration(
/// Extract the project root (scope) and [`Settings`] from a given
/// `pyproject.toml`.
fn resolve_scoped_settings<'a>(
pyproject: &'a Path,
fn resolve_scoped_settings(
pyproject: &Path,
transformer: &dyn ConfigurationTransformer,
origin: ConfigurationOrigin,
) -> Result<(&'a Path, Settings)> {
) -> Result<(PathBuf, 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, settings))
Ok((project_root.to_path_buf(), settings))
}
/// Extract the [`Settings`] from a given `pyproject.toml` and process the
@@ -455,7 +470,7 @@ pub fn python_files_in_path<'a>(
transformer,
ConfigurationOrigin::Ancestor,
)?;
resolver.add(root, settings);
resolver.add(&root, settings, pyproject);
// We found the closest configuration.
break;
}
@@ -647,7 +662,11 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> {
ConfigurationOrigin::Ancestor,
) {
Ok((root, settings)) => {
self.global.resolver.write().unwrap().add(root, settings);
self.global
.resolver
.write()
.unwrap()
.add(&root, settings, pyproject);
}
Err(err) => {
self.local_error = Err(err);
@@ -767,7 +786,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);
resolver.add(&root, settings, pyproject);
break;
}
}

View File

@@ -467,7 +467,7 @@ def test(): -> "int":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L50" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L54" 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#L75" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L79" 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 'ignore'."><code>ignore</code></a> ·
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%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 `type: ignore` or `ty: ignore` directives that are no longer applicable.
Checks for `ty: ignore` or `type: ignore` directives that are no longer applicable.
**Why is this bad?**
A `type: ignore` directive that no longer matches any diagnostic violations is likely
A `ty: ignore` directive that no longer matches any diagnostic violations is likely
included by mistake, and should be removed to avoid confusion.
**Examples**
@@ -2923,6 +2923,11 @@ 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,9 +1,10 @@
name,file,index,rank
auto-import-includes-modules,main.py,0,1
auto-import-includes-modules,main.py,1,7
auto-import-includes-modules,main.py,1,2
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
@@ -16,8 +17,10 @@ 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,159
numpy-array,main.py,0,57
numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
object-attr-instance-methods,main.py,1,1
@@ -26,7 +29,12 @@ 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,9
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,253
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
1 name file index rank
2 auto-import-includes-modules main.py 0 1
3 auto-import-includes-modules main.py 1 7 2
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
7 exact-over-fuzzy main.py 0 1
8 fstring-completions main.py 0 1
9 higher-level-symbols-preferred main.py 0
10 higher-level-symbols-preferred main.py 1 1
17 import-deprioritizes-type_check_only main.py 4 3
18 import-keyword-completion main.py 0 1
19 internal-typeshed-hidden main.py 0 2
20 local-over-auto-import main.py 0 1
21 modules-over-other-symbols main.py 0 1
22 none-completion main.py 0 1
23 numpy-array main.py 0 159 57
24 numpy-array main.py 1 1
25 object-attr-instance-methods main.py 0 1
26 object-attr-instance-methods main.py 1 1
29 scope-existing-over-new-import main.py 0 1
30 scope-prioritize-closer main.py 0 2
31 scope-simple-long-identifier main.py 0 1
32 third-party-over-stdlib main.py 0 1
33 tighter-over-looser-scope main.py 0 3
34 tstring-completions main.py 0 1
35 ty-extensions-lower-stdlib main.py 0 9 1
36 type-var-typing-over-ast typing-gets-priority main.py 0 3 1
37 type-var-typing-over-ast typing-gets-priority main.py 1 253 1
38 typing-gets-priority main.py 2 1
39 typing-gets-priority main.py 3 1
40 typing-gets-priority main.py 4 1

View File

@@ -540,16 +540,17 @@ 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![];
for result in walkdir::WalkDir::new(src_dir.as_std_path()) {
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 {
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

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

View File

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

View File

@@ -0,0 +1,11 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
os = 1

View File

@@ -0,0 +1,78 @@
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

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

View File

@@ -0,0 +1,12 @@
# 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

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

View File

@@ -0,0 +1,78 @@
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

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

View File

@@ -0,0 +1,10 @@
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

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

View File

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

View File

@@ -1,12 +0,0 @@
# 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

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

View File

@@ -0,0 +1,19 @@
# 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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1032,4 +1032,125 @@ 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,9 +1682,7 @@ def sequence4(cls: type) -> type:
class Foo: ...
ordered_foo = dataclass(order=True)(Foo)
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)
reveal_type(ordered_foo) # revealed: <class 'Foo'>
reveal_type(ordered_foo()) # revealed: Foo
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
```

View File

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

View File

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

View File

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

View File

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

View File

@@ -140,7 +140,11 @@ 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:
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.
<!-- snapshot-diagnostics -->
@@ -156,13 +160,31 @@ 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`:
def method(self, x: str) -> None: ... # error: [invalid-method-override]
# 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: ...
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: ...
@@ -190,8 +212,9 @@ class C(B):
foo = get
class D(C):
# compatible with `C.get` and `B.get`, but not with `A.get`
def get(self, my_default): ... # error: [invalid-method-override]
# 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): ...
```
## Non-generic methods on generic classes work as expected

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1064,7 +1064,7 @@ impl<'db> Type<'db> {
}
}
const fn is_dynamic(&self) -> bool {
pub(crate) const fn is_dynamic(&self) -> bool {
matches!(self, Type::Dynamic(_))
}
@@ -1073,7 +1073,7 @@ impl<'db> Type<'db> {
}
/// Is a value of this type only usable in typing contexts?
pub(crate) fn is_type_check_only(&self, db: &'db dyn Db) -> bool {
pub fn is_type_check_only(&self, db: &'db dyn Db) -> bool {
match self {
Type::ClassLiteral(class_literal) => class_literal.type_check_only(db),
Type::FunctionLiteral(f) => {
@@ -1833,7 +1833,7 @@ impl<'db> Type<'db> {
///
/// This method may have false negatives, but it should not have false positives. It should be
/// a cheap shallow check, not an exhaustive recursive check.
fn subtyping_is_always_reflexive(self) -> bool {
const fn subtyping_is_always_reflexive(self) -> bool {
match self {
Type::Never
| Type::FunctionLiteral(..)
@@ -1854,6 +1854,7 @@ impl<'db> Type<'db> {
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::PropertyInstance(_)
| Type::TypeVar(_)
// might inherit `Any`, but subtyping is still reflexive
| Type::ClassLiteral(_)
=> true,
@@ -1865,7 +1866,6 @@ impl<'db> Type<'db> {
| Type::Union(_)
| Type::Intersection(_)
| Type::Callable(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_)
| Type::TypeGuard(_)
@@ -1925,7 +1925,7 @@ impl<'db> Type<'db> {
SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => {
Some(CallableTypes::one(CallableType::single(
db,
Signature::new(Parameters::unknown(), Some(Type::from(subclass_of_ty))),
Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)),
)))
}
},
@@ -1968,7 +1968,7 @@ impl<'db> Type<'db> {
[Parameter::positional_only(None)
.with_annotated_type(newtype.base(db).instance_type(db))],
),
Some(Type::NewTypeInstance(newtype)),
Type::NewTypeInstance(newtype),
),
)))
}
@@ -3811,7 +3811,7 @@ impl<'db> Type<'db> {
[Parameter::positional_only(Some(Name::new_static("func")))
.with_annotated_type(Type::object())],
),
None,
Type::unknown(),
),
)
.into(),
@@ -3836,7 +3836,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any()),
],
),
Some(KnownClass::ConstraintSet.to_instance(db)),
KnownClass::ConstraintSet.to_instance(db),
),
)
.into(),
@@ -3851,7 +3851,7 @@ impl<'db> Type<'db> {
.type_form()
.with_annotated_type(Type::any())],
),
Some(KnownClass::Bool.to_instance(db)),
KnownClass::Bool.to_instance(db),
),
)
.into()
@@ -3878,7 +3878,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any()),
],
),
Some(Type::TypeVar(val_ty)),
Type::TypeVar(val_ty),
),
)
.into()
@@ -3897,7 +3897,7 @@ impl<'db> Type<'db> {
// errors instead of `type-assertion-failure` errors.
.with_annotated_type(Type::any())],
),
Some(Type::none(db)),
Type::none(db),
),
)
.into()
@@ -3916,7 +3916,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any()),
],
),
Some(Type::any()),
Type::any(),
),
)
.into(),
@@ -3932,7 +3932,7 @@ impl<'db> Type<'db> {
[Parameter::positional_only(Some(Name::new_static("cls")))
.with_annotated_type(Type::none(db))],
),
None,
Type::unknown(),
),
// def dataclass(cls: type[_T], /) -> type[_T]: ...
Signature::new(
@@ -3941,7 +3941,7 @@ impl<'db> Type<'db> {
[Parameter::positional_only(Some(Name::new_static("cls")))
.with_annotated_type(KnownClass::Type.to_instance(db))],
),
None,
Type::unknown(),
),
// TODO: make this overload Python-version-dependent
@@ -3994,7 +3994,7 @@ impl<'db> Type<'db> {
.with_default_type(Type::BooleanLiteral(false)),
],
),
None,
Type::unknown(),
),
],
)
@@ -4029,7 +4029,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any())
.with_default_type(Type::BooleanLiteral(false))],
),
Some(KnownClass::Bool.to_instance(db)),
KnownClass::Bool.to_instance(db),
),
)
.into()
@@ -4053,7 +4053,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::object())
.with_default_type(Type::string_literal(db, ""))],
),
Some(KnownClass::Str.to_instance(db)),
KnownClass::Str.to_instance(db),
),
Signature::new(
Parameters::new(
@@ -4083,7 +4083,7 @@ impl<'db> Type<'db> {
.with_default_type(Type::string_literal(db, "strict")),
],
),
Some(KnownClass::Str.to_instance(db)),
KnownClass::Str.to_instance(db),
),
],
)
@@ -4110,7 +4110,7 @@ impl<'db> Type<'db> {
[Parameter::positional_only(Some(Name::new_static("o")))
.with_annotated_type(Type::any())],
),
Some(type_instance),
type_instance,
),
Signature::new(
Parameters::new(
@@ -4127,12 +4127,12 @@ impl<'db> Type<'db> {
.with_annotated_type(
KnownClass::Dict.to_specialized_instance(
db,
[str_instance, Type::any()],
&[str_instance, Type::any()],
),
),
],
),
Some(type_instance),
type_instance,
),
],
)
@@ -4145,11 +4145,8 @@ impl<'db> Type<'db> {
// def __init__(self) -> None: ...
// def __new__(cls) -> Self: ...
// ```
Binding::single(
self,
Signature::new(Parameters::empty(), Some(Type::object())),
)
.into()
Binding::single(self, Signature::new(Parameters::empty(), Type::object()))
.into()
}
Some(KnownClass::Enum) => {
@@ -4179,7 +4176,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any()),
],
),
Some(KnownClass::Super.to_instance(db)),
KnownClass::Super.to_instance(db),
),
Signature::new(
Parameters::new(
@@ -4187,12 +4184,9 @@ impl<'db> Type<'db> {
[Parameter::positional_only(Some(Name::new_static("t")))
.with_annotated_type(Type::any())],
),
Some(KnownClass::Super.to_instance(db)),
),
Signature::new(
Parameters::empty(),
Some(KnownClass::Super.to_instance(db)),
KnownClass::Super.to_instance(db),
),
Signature::new(Parameters::empty(), KnownClass::Super.to_instance(db)),
],
)
.into()
@@ -4234,7 +4228,7 @@ impl<'db> Type<'db> {
.with_default_type(Type::IntLiteral(1)),
],
),
Some(KnownClass::Deprecated.to_instance(db)),
KnownClass::Deprecated.to_instance(db),
),
)
.into()
@@ -4276,7 +4270,7 @@ impl<'db> Type<'db> {
.with_default_type(Type::empty_tuple(db)),
],
),
None,
Type::unknown(),
),
)
.into()
@@ -4288,7 +4282,7 @@ impl<'db> Type<'db> {
db,
[Parameter::positional_only(None).with_annotated_type(Type::any())],
),
Some(Type::any()),
Type::any(),
);
let setter_signature = Signature::new(
Parameters::new(
@@ -4298,14 +4292,14 @@ impl<'db> Type<'db> {
Parameter::positional_only(None).with_annotated_type(Type::any()),
],
),
Some(Type::none(db)),
Type::none(db),
);
let deleter_signature = Signature::new(
Parameters::new(
db,
[Parameter::positional_only(None).with_annotated_type(Type::any())],
),
Some(Type::any()),
Type::any(),
);
Binding::single(
@@ -4349,7 +4343,7 @@ impl<'db> Type<'db> {
.with_default_type(Type::none(db)),
],
),
None,
Type::unknown(),
),
)
.into()
@@ -4372,7 +4366,7 @@ impl<'db> Type<'db> {
CallableBinding::from_overloads(
self,
[
Signature::new(Parameters::empty(), Some(Type::empty_tuple(db))),
Signature::new(Parameters::empty(), Type::empty_tuple(db)),
Signature::new_generic(
Some(GenericContext::from_typevar_instances(db, [element_ty])),
Parameters::new(
@@ -4383,11 +4377,11 @@ impl<'db> Type<'db> {
.with_annotated_type(
KnownClass::Iterable.to_specialized_instance(
db,
[Type::TypeVar(element_ty)],
&[Type::TypeVar(element_ty)],
),
)],
),
Some(Type::homogeneous_tuple(db, Type::TypeVar(element_ty))),
Type::homogeneous_tuple(db, Type::TypeVar(element_ty)),
),
],
)
@@ -4404,7 +4398,7 @@ impl<'db> Type<'db> {
Signature::new_generic(
class.generic_context(db),
Parameters::gradual_form(),
self.to_instance(db),
self.to_instance(db).unwrap_or(Type::unknown()),
),
)
.into(),
@@ -4430,7 +4424,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any()),
],
),
None,
Type::unknown(),
),
)
.into()
@@ -4445,7 +4439,10 @@ impl<'db> Type<'db> {
// TODO check call vs signatures of `__new__` and/or `__init__`
Binding::single(
self,
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
Signature::new(
Parameters::gradual_form(),
self.to_instance(db).unwrap_or(Type::unknown()),
),
)
.into()
}
@@ -4463,7 +4460,10 @@ impl<'db> Type<'db> {
// TODO check call vs signatures of `__new__` and/or `__init__`
SubclassOfInner::TypeVar(_) => Binding::single(
self,
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
Signature::new(
Parameters::gradual_form(),
self.to_instance(db).unwrap_or(Type::unknown()),
),
)
.into(),
},
@@ -4530,11 +4530,8 @@ impl<'db> Type<'db> {
// Intersect with `Any` for the return type to reflect the fact that the `dataclass()`
// decorator adds methods to the class
let returns = IntersectionType::from_elements(db, [typevar_meta, Type::any()]);
let signature = Signature::new_generic(
Some(context),
Parameters::new(db, parameters),
Some(returns),
);
let signature =
Signature::new_generic(Some(context), Parameters::new(db, parameters), returns);
Binding::single(self, signature).into()
}
@@ -4551,7 +4548,7 @@ impl<'db> Type<'db> {
[Parameter::positional_only(None)
.with_annotated_type(newtype.base(db).instance_type(db))],
),
Some(Type::NewTypeInstance(newtype)),
Type::NewTypeInstance(newtype),
),
)
.into(),
@@ -5843,7 +5840,7 @@ impl<'db> Type<'db> {
pub(crate) fn dunder_class(self, db: &'db dyn Db) -> Type<'db> {
if self.is_typed_dict() {
return KnownClass::Dict
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object()])
.to_specialized_class_type(db, &[KnownClass::Str.to_instance(db), Type::object()])
.map(Type::from)
// Guard against user-customized typesheds with a broken `dict` class
.unwrap_or_else(Type::unknown);
@@ -8373,7 +8370,7 @@ impl<'db> BoundTypeVarInstance<'db> {
let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind {
ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()),
ParamSpecAttrKind::Kwargs => KnownClass::Dict
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()])
.to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()])
.top_materialization(db),
});
@@ -10387,7 +10384,7 @@ impl<'db> CallableType<'db> {
) -> CallableType<'db> {
CallableType::new(
db,
CallableSignature::single(Signature::new(parameters, None)),
CallableSignature::single(Signature::new(parameters, Type::unknown())),
CallableTypeKind::ParamSpecValue,
)
}
@@ -10990,7 +10987,7 @@ impl<'db> KnownBoundMethodType<'db> {
.with_annotated_type(KnownClass::Type.to_instance(db)),
],
),
None,
Type::unknown(),
),
Signature::new(
Parameters::new(
@@ -11006,7 +11003,7 @@ impl<'db> KnownBoundMethodType<'db> {
.with_default_type(Type::none(db)),
],
),
None,
Type::unknown(),
),
]
.into_iter(),
@@ -11025,7 +11022,7 @@ impl<'db> KnownBoundMethodType<'db> {
.with_annotated_type(Type::object()),
],
),
None,
Type::unknown(),
)))
}
KnownBoundMethodType::StrStartswith(_) => {
@@ -11058,7 +11055,7 @@ impl<'db> KnownBoundMethodType<'db> {
.with_default_type(Type::none(db)),
],
),
Some(KnownClass::Bool.to_instance(db)),
KnownClass::Bool.to_instance(db),
)))
}
@@ -11078,7 +11075,7 @@ impl<'db> KnownBoundMethodType<'db> {
.with_annotated_type(Type::any()),
],
),
Some(KnownClass::ConstraintSet.to_instance(db)),
KnownClass::ConstraintSet.to_instance(db),
)))
}
@@ -11086,7 +11083,7 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetNever => {
Either::Right(std::iter::once(Signature::new(
Parameters::empty(),
Some(KnownClass::ConstraintSet.to_instance(db)),
KnownClass::ConstraintSet.to_instance(db),
)))
}
@@ -11103,7 +11100,7 @@ impl<'db> KnownBoundMethodType<'db> {
.with_annotated_type(Type::any()),
],
),
Some(KnownClass::ConstraintSet.to_instance(db)),
KnownClass::ConstraintSet.to_instance(db),
)))
}
@@ -11114,7 +11111,7 @@ impl<'db> KnownBoundMethodType<'db> {
[Parameter::positional_only(Some(Name::new_static("other")))
.with_annotated_type(KnownClass::ConstraintSet.to_instance(db))],
),
Some(KnownClass::ConstraintSet.to_instance(db)),
KnownClass::ConstraintSet.to_instance(db),
)))
}
@@ -11130,7 +11127,7 @@ impl<'db> KnownBoundMethodType<'db> {
))
.with_default_type(Type::none(db))],
),
Some(KnownClass::Bool.to_instance(db)),
KnownClass::Bool.to_instance(db),
)))
}
@@ -11143,10 +11140,10 @@ impl<'db> KnownBoundMethodType<'db> {
.with_annotated_type(KnownClass::ConstraintSet.to_instance(db)),
],
),
Some(UnionType::from_elements(
UnionType::from_elements(
db,
[KnownClass::Specialization.to_instance(db), Type::none(db)],
)),
),
)))
}
}
@@ -11193,7 +11190,7 @@ impl WrapperDescriptorKind {
.with_annotated_type(type_instance),
],
),
None,
Type::unknown(),
),
Signature::new(
Parameters::new(
@@ -11211,7 +11208,7 @@ impl WrapperDescriptorKind {
.with_default_type(none),
],
),
None,
Type::unknown(),
),
]
}
@@ -11237,7 +11234,7 @@ impl WrapperDescriptorKind {
.with_annotated_type(object),
],
),
None,
Type::unknown(),
)))
}
}
@@ -11727,8 +11724,8 @@ pub(super) struct MetaclassCandidate<'db> {
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct UnionType<'db> {
/// The union type includes values in any of these types.
#[returns(deref)]
pub elements: Box<[Type<'db>]>,
#[returns(ref)]
pub elements: FxOrderSet<Type<'db>>,
/// Whether the value pointed to by this type is recursively defined.
/// If `Yes`, union literal widening is performed early.
recursively_defined: RecursivelyDefined,
@@ -13082,11 +13079,11 @@ pub(crate) mod tests {
let recursive = UnionType::from_elements(
&db,
[
KnownClass::List.to_specialized_instance(&db, [div]),
KnownClass::List.to_specialized_instance(&db, &[div]),
Type::none(&db),
],
);
let nested_rec = KnownClass::List.to_specialized_instance(&db, [recursive]);
let nested_rec = KnownClass::List.to_specialized_instance(&db, &[recursive]);
assert_eq!(
nested_rec.display(&db).to_string(),
"list[list[Divergent] | None]"

View File

@@ -8,9 +8,10 @@ use crate::{
Db, DisplaySettings,
place::{Place, PlaceAndQualifiers},
types::{
ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy,
NominalInstanceType, NormalizedVisitor, SpecialFormType, SubclassOfInner, Type,
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
BoundTypeVarInstance, ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass,
MemberLookupPolicy, NominalInstanceType, NormalizedVisitor, SpecialFormType,
SubclassOfInner, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarConstraints,
TypeVarInstance, UnionBuilder,
context::InferContext,
diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS},
todo_type, visitor,
@@ -185,6 +186,12 @@ pub enum SuperOwnerKind<'db> {
Dynamic(DynamicType<'db>),
Class(ClassType<'db>),
Instance(NominalInstanceType<'db>),
/// An instance-like type variable owner (e.g., `self: Self` in an instance method).
/// The second element is the class extracted from the `TypeVar` bound for MRO lookup.
InstanceTypeVar(BoundTypeVarInstance<'db>, ClassType<'db>),
/// A class-like type variable owner (e.g., `cls: type[Self]` in a classmethod).
/// The second element is the class extracted from the `TypeVar` bound for MRO lookup.
ClassTypeVar(BoundTypeVarInstance<'db>, ClassType<'db>),
}
impl<'db> SuperOwnerKind<'db> {
@@ -199,6 +206,16 @@ impl<'db> SuperOwnerKind<'db> {
.as_nominal_instance()
.map(Self::Instance)
.unwrap_or(Self::Dynamic(DynamicType::Any)),
SuperOwnerKind::InstanceTypeVar(bound_typevar, class) => {
SuperOwnerKind::InstanceTypeVar(
bound_typevar.normalized_impl(db, visitor),
class.normalized_impl(db, visitor),
)
}
SuperOwnerKind::ClassTypeVar(bound_typevar, class) => SuperOwnerKind::ClassTypeVar(
bound_typevar.normalized_impl(db, visitor),
class.normalized_impl(db, visitor),
),
}
}
@@ -218,6 +235,10 @@ impl<'db> SuperOwnerKind<'db> {
SuperOwnerKind::Instance(instance) => Some(SuperOwnerKind::Instance(
instance.recursive_type_normalized_impl(db, div, nested)?,
)),
SuperOwnerKind::InstanceTypeVar(_, _) | SuperOwnerKind::ClassTypeVar(_, _) => {
// TODO: we might need to normalize the nested class here?
Some(self)
}
}
}
@@ -228,6 +249,9 @@ impl<'db> SuperOwnerKind<'db> {
}
SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)),
SuperOwnerKind::Instance(instance) => Either::Right(instance.class(db).iter_mro(db)),
SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => {
Either::Right(class.iter_mro(db))
}
}
}
@@ -236,16 +260,31 @@ impl<'db> SuperOwnerKind<'db> {
SuperOwnerKind::Dynamic(_) => None,
SuperOwnerKind::Class(class) => Some(class),
SuperOwnerKind::Instance(instance) => Some(instance.class(db)),
SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => {
Some(class)
}
}
}
}
impl<'db> From<SuperOwnerKind<'db>> for Type<'db> {
fn from(owner: SuperOwnerKind<'db>) -> Self {
match owner {
/// Returns the `TypeVar` instance if this owner is a `TypeVar` variant.
fn typevar(self, db: &'db dyn Db) -> Option<TypeVarInstance<'db>> {
match self {
SuperOwnerKind::InstanceTypeVar(bound_typevar, _)
| SuperOwnerKind::ClassTypeVar(bound_typevar, _) => Some(bound_typevar.typevar(db)),
_ => None,
}
}
/// Returns the type representation of this owner.
pub(super) fn owner_type(self, db: &'db dyn Db) -> Type<'db> {
match self {
SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic),
SuperOwnerKind::Class(class) => class.into(),
SuperOwnerKind::Instance(instance) => instance.into(),
SuperOwnerKind::InstanceTypeVar(bound_typevar, _) => Type::TypeVar(bound_typevar),
SuperOwnerKind::ClassTypeVar(bound_typevar, _) => {
SubclassOfType::from(db, SubclassOfInner::TypeVar(bound_typevar))
}
}
}
}
@@ -266,7 +305,7 @@ pub(super) fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
visitor: &V,
) {
visitor.visit_type(db, Type::from(bound_super.pivot_class(db)));
visitor.visit_type(db, Type::from(bound_super.owner(db)));
visitor.visit_type(db, bound_super.owner(db).owner_type(db));
}
impl<'db> BoundSuperType<'db> {
@@ -285,6 +324,7 @@ impl<'db> BoundSuperType<'db> {
let delegate_to =
|type_to_delegate_to| BoundSuperType::build(db, pivot_class_type, type_to_delegate_to);
// Delegate but rewrite errors to preserve TypeVar context.
let delegate_with_error_mapped =
|type_to_delegate_to, error_context: Option<TypeVarInstance<'db>>| {
delegate_to(type_to_delegate_to).map_err(|err| match err {
@@ -315,19 +355,128 @@ impl<'db> BoundSuperType<'db> {
})
};
// We don't use `ClassBase::try_from_type` here because:
// - There are objects that may validly be present in a class's bases list
// but are not valid as pivot classes, e.g. `typing.ChainMap`
// - There are objects that are not valid in a class's bases list
// but are valid as pivot classes, e.g. unsubscripted `typing.Generic`
let pivot_class = match pivot_class_type {
Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)),
Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
_ => match subclass_of.subclass_of().into_class(db) {
Some(class) => ClassBase::Class(class),
None => {
return Err(BoundSuperError::InvalidPivotClassType {
pivot_class: pivot_class_type,
});
}
},
},
Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol,
Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic,
Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict,
Type::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
_ => {
return Err(BoundSuperError::InvalidPivotClassType {
pivot_class: pivot_class_type,
});
}
};
// Helper to build a union of bound-super instances for constrained TypeVars.
// Each constraint must be a subclass of the pivot class.
let build_constrained_union =
|constraints: TypeVarConstraints<'db>,
bound_typevar: BoundTypeVarInstance<'db>,
typevar: TypeVarInstance<'db>,
make_owner: fn(BoundTypeVarInstance<'db>, ClassType<'db>) -> SuperOwnerKind<'db>|
-> Result<Type<'db>, BoundSuperError<'db>> {
let pivot_class_literal = pivot_class.into_class().map(|c| c.class_literal(db).0);
let mut builder = UnionBuilder::new(db);
for constraint in constraints.elements(db) {
let class = match constraint {
Type::NominalInstance(instance) => Some(instance.class(db)),
_ => constraint.to_class_type(db),
};
match class {
Some(class) => {
// Validate constraint is a subclass of pivot class.
if let Some(pivot) = pivot_class_literal {
if !class.iter_mro(db).any(|superclass| match superclass {
ClassBase::Dynamic(_) => true,
ClassBase::Generic
| ClassBase::Protocol
| ClassBase::TypedDict => false,
ClassBase::Class(superclass) => {
superclass.class_literal(db).0 == pivot
}
}) {
return Err(BoundSuperError::FailingConditionCheck {
pivot_class: pivot_class_type,
owner: owner_type,
typevar_context: Some(typevar),
});
}
}
let owner = make_owner(bound_typevar, class);
builder = builder.add(Type::BoundSuper(BoundSuperType::new(
db,
pivot_class,
owner,
)));
}
None => {
// Delegate to the constraint to get better error messages
// if the constraint is incompatible with the pivot class.
builder = builder.add(delegate_to(*constraint)?);
}
}
}
Ok(builder.build())
};
let owner = match owner_type {
Type::Never => SuperOwnerKind::Dynamic(DynamicType::Unknown),
Type::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic),
Type::ClassLiteral(class) => SuperOwnerKind::Class(ClassType::NonGeneric(class)),
Type::SubclassOf(subclass_of_type) => {
match subclass_of_type.subclass_of().with_transposed_type_var(db) {
SubclassOfInner::Class(class) => SuperOwnerKind::Class(class),
SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic),
SubclassOfInner::TypeVar(bound_typevar) => {
return delegate_to(Type::TypeVar(bound_typevar));
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
SubclassOfInner::Class(class) => SuperOwnerKind::Class(class),
SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic),
SubclassOfInner::TypeVar(bound_typevar) => {
let typevar = bound_typevar.typevar(db);
match typevar.bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
let class = match bound {
Type::NominalInstance(instance) => Some(instance.class(db)),
Type::ProtocolInstance(protocol) => protocol
.to_nominal_instance()
.map(|instance| instance.class(db)),
_ => None,
};
if let Some(class) = class {
SuperOwnerKind::ClassTypeVar(bound_typevar, class)
} else {
let subclass_of = SubclassOfType::try_from_instance(db, bound)
.unwrap_or_else(SubclassOfType::subclass_of_unknown);
return delegate_with_error_mapped(subclass_of, Some(typevar));
}
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
return build_constrained_union(
constraints,
bound_typevar,
typevar,
SuperOwnerKind::ClassTypeVar,
);
}
None => {
// No bound means the implicit upper bound is `object`.
SuperOwnerKind::ClassTypeVar(bound_typevar, ClassType::object(db))
}
}
}
}
},
Type::NominalInstance(instance) => SuperOwnerKind::Instance(instance),
Type::ProtocolInstance(protocol) => {
@@ -375,19 +524,38 @@ impl<'db> BoundSuperType<'db> {
return Ok(builder.build());
}
Type::TypeAlias(alias) => {
return delegate_with_error_mapped(alias.value_type(db), None);
return delegate_to(alias.value_type(db));
}
Type::TypeVar(type_var) => {
let type_var = type_var.typevar(db);
return match type_var.bound_or_constraints(db) {
Type::TypeVar(bound_typevar) => {
let typevar = bound_typevar.typevar(db);
match typevar.bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
delegate_with_error_mapped(bound, Some(type_var))
let class = match bound {
Type::NominalInstance(instance) => Some(instance.class(db)),
Type::ProtocolInstance(protocol) => protocol
.to_nominal_instance()
.map(|instance| instance.class(db)),
_ => None,
};
if let Some(class) = class {
SuperOwnerKind::InstanceTypeVar(bound_typevar, class)
} else {
return delegate_with_error_mapped(bound, Some(typevar));
}
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
delegate_with_error_mapped(constraints.as_type(db), Some(type_var))
return build_constrained_union(
constraints,
bound_typevar,
typevar,
SuperOwnerKind::InstanceTypeVar,
);
}
None => delegate_with_error_mapped(Type::object(), Some(type_var)),
};
None => {
// No bound means the implicit upper bound is `object`.
SuperOwnerKind::InstanceTypeVar(bound_typevar, ClassType::object(db))
}
}
}
Type::BooleanLiteral(_) | Type::TypeIs(_) | Type::TypeGuard(_) => {
return delegate_to(KnownClass::Bool.to_instance(db));
@@ -435,7 +603,7 @@ impl<'db> BoundSuperType<'db> {
}
return delegate_to(
KnownClass::Dict
.to_specialized_instance(db, [key_builder.build(), value_builder.build()]),
.to_specialized_instance(db, &[key_builder.build(), value_builder.build()]),
);
}
Type::NewTypeInstance(newtype) => {
@@ -456,35 +624,6 @@ impl<'db> BoundSuperType<'db> {
}
};
// We don't use `Classbase::try_from_type` here because:
// - There are objects that may validly be present in a class's bases list
// but are not valid as pivot classes, e.g. `typing.ChainMap`
// - There are objects that are not valid in a class's bases list
// but are valid as pivot classes, e.g. unsubscripted `typing.Generic`
let pivot_class = match pivot_class_type {
Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)),
Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
_ => match subclass_of.subclass_of().into_class(db) {
Some(class) => ClassBase::Class(class),
None => {
return Err(BoundSuperError::InvalidPivotClassType {
pivot_class: pivot_class_type,
});
}
},
},
Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol,
Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic,
Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict,
Type::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
_ => {
return Err(BoundSuperError::InvalidPivotClassType {
pivot_class: pivot_class_type,
});
}
};
if let Some(pivot_class) = pivot_class.into_class()
&& let Some(owner_class) = owner.into_class(db)
{
@@ -497,7 +636,7 @@ impl<'db> BoundSuperType<'db> {
return Err(BoundSuperError::FailingConditionCheck {
pivot_class: pivot_class_type,
owner: owner_type,
typevar_context: None,
typevar_context: owner.typevar(db),
});
}
}
@@ -556,18 +695,30 @@ impl<'db> BoundSuperType<'db> {
db,
attribute,
Type::none(db),
Type::from(owner),
owner.owner_type(db),
)
.0,
),
SuperOwnerKind::Instance(_) => {
let owner = Type::from(owner);
SuperOwnerKind::Instance(_) | SuperOwnerKind::InstanceTypeVar(..) => {
let owner_type = owner.owner_type(db);
Some(
Type::try_call_dunder_get_on_attribute(
db,
attribute,
owner,
owner.to_meta_type(db),
owner_type,
owner_type.to_meta_type(db),
)
.0,
)
}
SuperOwnerKind::ClassTypeVar(..) => {
let owner_type = owner.owner_type(db);
Some(
Type::try_call_dunder_get_on_attribute(
db,
attribute,
Type::none(db),
owner_type,
)
.0,
)
@@ -592,6 +743,9 @@ impl<'db> BoundSuperType<'db> {
}
SuperOwnerKind::Class(class) => class,
SuperOwnerKind::Instance(instance) => instance.class(db),
SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => {
class
}
};
let (class_literal, _) = class.class_literal(db);

View File

@@ -28,40 +28,55 @@
//!
//! In practice, there are two kinds of unions found in the wild: relatively-small unions made up
//! of normal user types (classes, etc), and large unions made up of literals, which can occur via
//! large enums (not yet implemented) or from string/integer/bytes literals, which can grow due to
//! literal arithmetic or operations on literal strings/bytes. For normal unions, it's most
//! efficient to just store the member types in a vector, and do O(n^2) `is_subtype_of` checks to
//! maintain the union in simplified form. But literal unions can grow to a size where this becomes
//! a performance problem. For this reason, we group literal types in `UnionBuilder`. Since every
//! different string literal type shares exactly the same possible super-types, and none of them
//! are subtypes of each other (unless exactly the same literal type), we can avoid many
//! unnecessary `is_subtype_of` checks.
//! large enums or from string/integer/bytes literals, which can grow due to literal arithmetic or
//! operations on literal strings/bytes. For normal unions, it's most efficient to just store the
//! member types in a vector, and do O(n^2) redundancy checks to maintain the union in simplified
//! form. But literal unions can grow to a size where this becomes a performance problem. For this
//! reason, we group literal types in `UnionBuilder`. Since every different string literal type
//! shares exactly the same possible super-types, and none of them are subtypes of each other
//! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks.
use std::hash::BuildHasherDefault;
use crate::types::enums::{enum_member_literals, enum_metadata};
use crate::types::type_ordering::union_or_intersection_elements_ordering;
use crate::types::{
BytesLiteralType, IntersectionType, KnownClass, NegativeIntersectionElements,
StringLiteralType, Type, TypeVarBoundOrConstraints, UnionType,
BytesLiteralType, ClassLiteral, EnumLiteralType, IntersectionType, KnownClass,
NegativeIntersectionElements, StringLiteralType, Type, TypeVarBoundOrConstraints, UnionType,
};
use crate::{Db, FxOrderSet};
use rustc_hash::FxHashSet;
use smallvec::SmallVec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LiteralKind {
enum LiteralKind<'db> {
Int,
String,
Bytes,
Enum { enum_class: ClassLiteral<'db> },
}
impl<'db> Type<'db> {
/// Return `true` if this type can be a supertype of some literals of `kind` and not others.
fn splits_literals(self, db: &'db dyn Db, kind: LiteralKind) -> bool {
match (self, kind) {
// Note that as of 2026-01-04, `AlwaysFalsy` and `AlwaysTruthy` never split
// enum literals, but that could change in the future. `Literal[Foo.X]` could
// plausibly be understood by ty as a subtype of `AlwaysFalsy` in the following
// snippet, because `Foo` is an IntEnum that does not override `__bool__` and
// `Foo.X` has a falsy value whereas `Foo.Y` does not:
//
// ```py
// class Foo(enum.IntEnum):
// X = 0
// Y = 1
// ```
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => true,
(Type::StringLiteral(_), LiteralKind::String) => true,
(Type::BytesLiteral(_), LiteralKind::Bytes) => true,
(Type::IntLiteral(_), LiteralKind::Int) => true,
(Type::EnumLiteral(enum_literal), LiteralKind::Enum { enum_class }) => {
enum_literal.enum_class(db) == enum_class
}
(Type::Intersection(intersection), _) => {
intersection
.positive(db)
@@ -86,17 +101,14 @@ enum UnionElement<'db> {
IntLiterals(FxOrderSet<i64>),
StringLiterals(FxOrderSet<StringLiteralType<'db>>),
BytesLiterals(FxOrderSet<BytesLiteralType<'db>>),
EnumLiterals {
enum_class: ClassLiteral<'db>,
literals: FxOrderSet<EnumLiteralType<'db>>,
},
Type(Type<'db>),
}
impl<'db> UnionElement<'db> {
const fn to_type_element(&self) -> Option<Type<'db>> {
match self {
UnionElement::Type(ty) => Some(*ty),
_ => None,
}
}
/// Try reducing this `UnionElement` given the presence in the same union of `other_type`.
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
let mut other_type_negated_cache = None;
@@ -110,22 +122,22 @@ impl<'db> UnionElement<'db> {
// to determine whether the element should be retained in the set.
//
// If `ignore` or `collapse` is `true` for any element in the set,
// we no longer need to do any expensive subtyping checks for any
// we no longer need to do any expensive redundancy checks for any
// further elements in the set:
//
// - if `ignore` is `true`, this indicates that `other_type` is a
// subtype of one of the literals in this set. Given this fact,
// - if `ignore` is `true`, this indicates that `other_type` is
// redundant with one of the literals in this set. Given this fact,
// it cannot be possible for any other literals in this set to be
// a subtype of `other_type`.
// redundant with `other_type`.
// - if `collapse` is `true`, all literals of this kind will be
// removed from the union, so it's irrelevant to answer the
// question of which literals should remain in this set.
//
// We therefore only ask if `ty` is a subtype of `other_type` if
// We therefore only ask if `ty` is redundant with `other_type` if
// both `ignore` and `collapse` are `false`. If either is `true`,
// we skip the expensive subtype check and return `true`.
// we skip the expensive redundancy check and return `true`.
let mut should_retain_type = |ty| {
if ignore || other_type.is_subtype_of(db, ty) {
if ignore || other_type.is_redundant_with(db, ty) {
ignore = true;
return true;
}
@@ -133,7 +145,7 @@ impl<'db> UnionElement<'db> {
collapse = true;
return true;
}
!ty.is_subtype_of(db, other_type)
!ty.is_redundant_with(db, other_type)
};
let should_keep = match self {
@@ -142,7 +154,7 @@ impl<'db> UnionElement<'db> {
literals.retain(|literal| should_retain_type(Type::IntLiteral(*literal)));
!literals.is_empty()
} else {
!Type::IntLiteral(literals[0]).is_subtype_of(db, other_type)
!Type::IntLiteral(literals[0]).is_redundant_with(db, other_type)
}
}
UnionElement::StringLiterals(literals) => {
@@ -150,7 +162,7 @@ impl<'db> UnionElement<'db> {
literals.retain(|literal| should_retain_type(Type::StringLiteral(*literal)));
!literals.is_empty()
} else {
!Type::StringLiteral(literals[0]).is_subtype_of(db, other_type)
!Type::StringLiteral(literals[0]).is_redundant_with(db, other_type)
}
}
UnionElement::BytesLiterals(literals) => {
@@ -158,7 +170,21 @@ impl<'db> UnionElement<'db> {
literals.retain(|literal| should_retain_type(Type::BytesLiteral(*literal)));
!literals.is_empty()
} else {
!Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type)
!Type::BytesLiteral(literals[0]).is_redundant_with(db, other_type)
}
}
UnionElement::EnumLiterals {
enum_class,
literals,
} => {
let literal_kind = LiteralKind::Enum {
enum_class: *enum_class,
};
if other_type.splits_literals(db, literal_kind) {
literals.retain(|literal| should_retain_type(Type::EnumLiteral(*literal)));
!literals.is_empty()
} else {
!Type::EnumLiteral(literals[0]).is_redundant_with(db, other_type)
}
}
UnionElement::Type(existing) => return ReduceResult::Type(*existing),
@@ -206,19 +232,26 @@ impl RecursivelyDefined {
}
}
/// If the value is defined recursively, widening is performed from fewer literal elements, resulting in faster convergence of the fixed-point iteration.
/// If the value is defined recursively, widening is performed from fewer literal elements,
/// resulting in faster convergence of the fixed-point iteration.
const MAX_RECURSIVE_UNION_LITERALS: usize = 10;
/// If the value is defined non-recursively, the fixed-point iteration will converge in one go,
/// so in principle we can have as many literal elements as we want, but to avoid unintended huge computational loads, we limit it to 256.
/// so in principle we can have as many literal elements as we want,
/// but to avoid unintended huge computational loads, we limit it to 256.
const MAX_NON_RECURSIVE_UNION_LITERALS: usize = 256;
/// However, we set a much larger limit for enum literals than for other kinds of literals.
/// Huge enums are not uncommon (especially in generated code), and it's annoying
/// if reachability analysis etc. fails when analysing these enums.
const MAX_NON_RECURSIVE_UNION_ENUM_LITERALS: usize = 8192;
pub(crate) struct UnionBuilder<'db> {
elements: Vec<UnionElement<'db>>,
db: &'db dyn Db,
unpack_aliases: bool,
order_elements: bool,
// This is enabled when joining types in a `cycle_recovery` function.
// Since a cycle cannot be created within a `cycle_recovery` function, execution of `is_redundant_with` is skipped.
/// This is enabled when joining types in a `cycle_recovery` function.
/// Since a cycle cannot be created within a `cycle_recovery` function,
/// execution of `is_redundant_with` is skipped.
cycle_recovery: bool,
recursively_defined: RecursivelyDefined,
}
@@ -281,6 +314,9 @@ impl<'db> UnionBuilder<'db> {
UnionElement::BytesLiterals(_) => {
replace_with.push(KnownClass::Bytes.to_instance(self.db));
}
UnionElement::EnumLiterals { literals, .. } => {
replace_with.push(literals[0].enum_class_instance(self.db));
}
UnionElement::Type(_) => {}
}
}
@@ -328,6 +364,7 @@ impl<'db> UnionBuilder<'db> {
UnionElement::IntLiterals(literals) => acc + literals.len(),
UnionElement::StringLiterals(literals) => acc + literals.len(),
UnionElement::BytesLiterals(literals) => acc + literals.len(),
UnionElement::EnumLiterals { literals, .. } => acc + literals.len(),
UnionElement::Type(_) => acc,
});
if should_widen(literals, self.recursively_defined) {
@@ -367,10 +404,10 @@ impl<'db> UnionBuilder<'db> {
UnionElement::Type(existing) => {
// e.g. `existing` could be `Literal[""] & Any`,
// and `ty` could be `Literal[""]`
if ty.is_subtype_of(self.db, *existing) {
if ty.is_redundant_with(self.db, *existing) {
return;
}
if existing.is_subtype_of(self.db, ty) {
if existing.is_redundant_with(self.db, ty) {
to_remove = Some(index);
continue;
}
@@ -412,12 +449,12 @@ impl<'db> UnionBuilder<'db> {
continue;
}
UnionElement::Type(existing) => {
if ty.is_subtype_of(self.db, *existing) {
if ty.is_redundant_with(self.db, *existing) {
return;
}
// e.g. `existing` could be `Literal[b""] & Any`,
// and `ty` could be `Literal[b""]`
if existing.is_subtype_of(self.db, ty) {
if existing.is_redundant_with(self.db, ty) {
to_remove = Some(index);
continue;
}
@@ -459,12 +496,12 @@ impl<'db> UnionBuilder<'db> {
continue;
}
UnionElement::Type(existing) => {
if ty.is_subtype_of(self.db, *existing) {
if ty.is_redundant_with(self.db, *existing) {
return;
}
// e.g. `existing` could be `Literal[1] & Any`,
// and `ty` could be `Literal[1]`
if existing.is_subtype_of(self.db, ty) {
if existing.is_redundant_with(self.db, ty) {
to_remove = Some(index);
continue;
}
@@ -493,32 +530,78 @@ impl<'db> UnionBuilder<'db> {
let metadata =
enum_metadata(self.db, enum_class).expect("Class of enum literal is an enum");
let enum_members_in_union = self
.elements
.iter()
.filter_map(UnionElement::to_type_element)
.filter_map(Type::as_enum_literal)
.map(|literal| literal.name(self.db))
.chain(std::iter::once(enum_member_to_add.name(self.db)))
.collect::<FxHashSet<_>>();
let all_members_are_in_union = metadata
.members
.keys()
.all(|name| enum_members_in_union.contains(name));
if all_members_are_in_union {
if metadata.members.len() == 1 {
self.add_in_place_impl(
enum_member_to_add.enum_class_instance(self.db),
seen_aliases,
);
} else if !self
.elements
.iter()
.filter_map(UnionElement::to_type_element)
.any(|ty| Type::EnumLiteral(enum_member_to_add).is_subtype_of(self.db, ty))
{
self.push_type(Type::EnumLiteral(enum_member_to_add), seen_aliases);
return;
}
let mut found = None;
let mut to_remove = None;
for (index, element) in self.elements.iter_mut().enumerate() {
match element {
UnionElement::EnumLiterals {
enum_class: existing_enum_class,
literals,
} => {
if *existing_enum_class != enum_class {
continue;
}
// See the doc-comment above `MAX_NON_RECURSIVE_UNION_ENUM_LITERALS`
// for why we avoid using the `should_widen` closure here.
let enum_literals_limit =
if self.recursively_defined.is_yes() && cycle_recovery {
MAX_RECURSIVE_UNION_LITERALS
} else {
MAX_NON_RECURSIVE_UNION_ENUM_LITERALS
};
if literals.len() >= enum_literals_limit {
let replace_with = literals[0].enum_class_instance(self.db);
self.add_in_place_impl(replace_with, seen_aliases);
return;
}
found = Some(literals);
continue;
}
UnionElement::Type(existing) => {
if ty.is_redundant_with(self.db, *existing) {
return;
}
// e.g. `existing` could be `Literal[Foo.X] & Any`,
// and `ty` could be `Literal[Foo.X]`
if existing.is_redundant_with(self.db, ty) {
to_remove = Some(index);
continue;
}
if ty_negated().is_subtype_of(self.db, *existing) {
// The type that includes both this new element, and its negation
// (or a supertype of its negation), must be simply `object`.
self.collapse_to_object();
return;
}
}
_ => {}
}
}
if let Some(found) = found {
let newly_added = found.insert(enum_member_to_add);
if newly_added && found.len() == metadata.members.len() {
self.add_in_place_impl(
enum_member_to_add.enum_class_instance(self.db),
seen_aliases,
);
return;
}
} else {
self.elements.push(UnionElement::EnumLiterals {
enum_class,
literals: FxOrderSet::from_iter([enum_member_to_add]),
});
}
if let Some(index) = to_remove {
self.elements.swap_remove(index);
}
}
// Adding `object` to a union results in `object`.
@@ -625,7 +708,10 @@ impl<'db> UnionBuilder<'db> {
}
pub(crate) fn try_build(self) -> Option<Type<'db>> {
let mut types = vec![];
let mut types: FxOrderSet<Type<'db>> = FxOrderSet::with_capacity_and_hasher(
self.elements.len(),
BuildHasherDefault::default(),
);
for element in self.elements {
match element {
UnionElement::IntLiterals(literals) => {
@@ -637,7 +723,12 @@ impl<'db> UnionBuilder<'db> {
UnionElement::BytesLiterals(literals) => {
types.extend(literals.into_iter().map(Type::BytesLiteral));
}
UnionElement::Type(ty) => types.push(ty),
UnionElement::EnumLiterals { literals, .. } => {
types.extend(literals.into_iter().map(Type::EnumLiteral));
}
UnionElement::Type(ty) => {
types.insert(ty);
}
}
}
if self.order_elements {
@@ -646,11 +737,14 @@ impl<'db> UnionBuilder<'db> {
match types.len() {
0 => None,
1 => Some(types[0]),
_ => Some(Type::Union(UnionType::new(
self.db,
types.into_boxed_slice(),
self.recursively_defined,
))),
_ => {
types.shrink_to_fit();
Some(Type::Union(UnionType::new(
self.db,
types,
self.recursively_defined,
)))
}
}
}
}
@@ -763,7 +857,7 @@ impl<'db> IntersectionBuilder<'db> {
db,
enum_member_literals(db, instance.class_literal(db), None)
.expect("Calling `enum_member_literals` on an enum class")
.collect::<Box<[_]>>(),
.collect::<FxOrderSet<_>>(),
RecursivelyDefined::No,
)),
seen_aliases,
@@ -1328,6 +1422,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
mod tests {
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
use crate::FxOrderSet;
use crate::db::tests::setup_db;
use crate::place::known_module_symbol;
use crate::types::enums::enum_member_literals;
@@ -1361,7 +1456,7 @@ mod tests {
let t1 = Type::IntLiteral(1);
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
assert_eq!(union.elements(&db), &[t0, t1]);
assert_eq!(union.elements(&db), &FxOrderSet::from_iter([t0, t1]));
}
#[test]

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